brakeman.rb 9.6 KB
Newer Older
J
Justin Collins 已提交
1
require 'rubygems'
2 3
require 'yaml'
require 'set'
4 5

module Brakeman
J
Justin Collins 已提交
6

7 8 9 10
  #This exit code is used when warnings are found and the --exit-on-warn
  #option is set
  Warnings_Found_Exit_Code = 3

11 12 13
  @debug = false
  @quiet = false

J
Justin Collins 已提交
14 15 16 17 18
  #Run Brakeman scan. Returns Tracker object.
  #
  #Options:
  #
  #  * :app_path - path to root of Rails app (required)
J
Justin Collins 已提交
19
  #  * :assume_all_routes - assume all methods are routes (default: true)
J
Justin Collins 已提交
20 21 22 23 24
  #  * :check_arguments - check arguments of methods (default: true)
  #  * :collapse_mass_assignment - report unprotected models in single warning (default: true)
  #  * :combine_locations - combine warning locations (default: true)
  #  * :config_file - configuration file
  #  * :escape_html - escape HTML by default (automatic)
25
  #  * :exit_on_warn - return false if warnings found, true otherwise. Not recommended for library use (default: false)
26
  #  * :highlight_user_input - highlight user input in reported warnings (default: true)
J
Justin Collins 已提交
27 28
  #  * :html_style - path to CSS file
  #  * :ignore_model_output - consider models safe (default: false)
29
  #  * :interprocedural - limited interprocedural processing of method calls (default: false)
J
Justin Collins 已提交
30 31
  #  * :message_limit - limit length of messages
  #  * :min_confidence - minimum confidence (0-2, 0 is highest)
32 33
  #  * :output_files - files for output
  #  * :output_formats - formats for output (:to_s, :to_tabs, :to_csv, :to_html)
J
Justin Collins 已提交
34
  #  * :parallel_checks - run checks in parallel (default: true)
35 36
  #  * :print_report - if no output file specified, print to stdout (default: false)
  #  * :quiet - suppress most messages (default: true)
J
Justin Collins 已提交
37 38 39 40 41 42
  #  * :rails3 - force Rails 3 mode (automatic)
  #  * :report_routes - show found routes on controllers (default: false)
  #  * :run_checks - array of checks to run (run all if not specified)
  #  * :safe_methods - array of methods to consider safe
  #  * :skip_libs - do not process lib/ directory (default: false)
  #  * :skip_checks - checks not to run (run all if not specified)
F
fsword 已提交
43
  #  * :relative_path - show relative path of each file(default: false)
44
  #  * :summary_only - only output summary section of report
J
Justin Collins 已提交
45
  #                    (does not apply to tabs format)
J
Justin Collins 已提交
46
  #
47
  #Alternatively, just supply a path as a string.
48
  def self.run options
49 50
    options = set_options options

51 52 53
    @quiet = !!options[:quiet]
    @debug = !!options[:debug]

J
Justin Collins 已提交
54 55 56
    if @quiet
      options[:report_progress] = false
    end
57
    scan options
58 59
  end

60
  #Sets up options for run, checks given application path
61
  def self.set_options options
62 63 64 65
    if options.is_a? String
      options = { :app_path => options }
    end

66 67
    options[:app_path] = File.expand_path(options[:app_path])

68 69 70 71 72 73
    file_options = load_options(options[:config_file])

    options = file_options.merge options

    options[:quiet] = true if options[:quiet].nil? && file_options[:quiet]

74
    options = get_defaults.merge! options
75
    options[:output_formats] = get_output_formats options
76 77 78 79

    options
  end

G
grosser 已提交
80 81 82
  CONFIG_FILES = [
    File.expand_path("./config/brakeman.yml"),
    File.expand_path("~/.brakeman/config.yml"),
83
    File.expand_path("/etc/brakeman/config.yml")
G
grosser 已提交
84
  ]
85

G
grosser 已提交
86 87
  #Load options from YAML file
  def self.load_options custom_location
88
    #Load configuration file
G
grosser 已提交
89 90 91
    if config = config_file(custom_location)
      options = YAML.load_file config
      options.each { |k, v| options[k] = Set.new v if v.is_a? Array }
92
      notify "[Notice] Using configuration in #{config}" unless options[:quiet]
G
grosser 已提交
93 94 95 96 97 98
      options
    else
      {}
    end
  end

99 100 101
  def self.config_file custom_location = nil
    supported_locations = [File.expand_path(custom_location || "")] + CONFIG_FILES
    supported_locations.detect {|f| File.file?(f) }
102 103
  end

104
  #Default set of options
105
  def self.get_defaults
106 107
    { :assume_all_routes => true,
      :skip_checks => Set.new,
108
      :check_arguments => true,
109 110 111 112
      :safe_methods => Set.new,
      :min_confidence => 2,
      :combine_locations => true,
      :collapse_mass_assignment => true,
113
      :highlight_user_input => true,
114 115 116
      :ignore_redirect_to_model => true,
      :ignore_model_output => false,
      :message_limit => 100,
117
      :parallel_checks => true,
F
fsword 已提交
118
      :relative_path => false,
119
      :quiet => true,
J
Justin Collins 已提交
120
      :report_progress => true,
121
      :html_style => "#{File.expand_path(File.dirname(__FILE__))}/brakeman/format/style.css"
122 123 124
    }
  end

125 126 127
  #Determine output formats based on options[:output_formats]
  #or options[:output_files]
  def self.get_output_formats options
128
    #Set output format
129 130 131
    if options[:output_format] && options[:output_files] && options[:output_files].size > 1
      raise ArgumentError, "Cannot specify output format if multiple output files specified"
    end
132
    if options[:output_format]
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
      [
        case options[:output_format]
        when :html, :to_html
          :to_html
        when :csv, :to_csv
          :to_csv
        when :pdf, :to_pdf
          :to_pdf
        when :tabs, :to_tabs
          :to_tabs
        when :json, :to_json
          :to_json
        else
          :to_s
        end
      ]
149
    else
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
      return [:to_s] unless options[:output_files]
      options[:output_files].map do |output_file|
        case output_file
        when /\.html$/i
          :to_html
        when /\.csv$/i
          :to_csv
        when /\.pdf$/i
          :to_pdf
        when /\.tabs$/i
          :to_tabs
        when /\.json$/i
          :to_json
        else
          :to_s
        end
166 167 168 169
      end
    end
  end

170
  #Output list of checks (for `-k` option)
171
  def self.list_checks
J
Justin Collins 已提交
172
    require 'brakeman/scanner'
173 174
    $stderr.puts "Available Checks:"
    $stderr.puts "-" * 30
175 176 177
    $stderr.puts Checks.checks.map { |c|
      c.to_s.match(/^Brakeman::(.*)$/)[1].ljust(27) << c.description
    }.sort.join "\n"
178 179
  end

180 181 182
  #Installs Rake task for running Brakeman,
  #which basically means copying `lib/brakeman/brakeman.rake` to
  #`lib/tasks/brakeman.rake` in the current Rails application.
183 184 185 186 187 188 189 190 191 192
  def self.install_rake_task
    if not File.exists? "Rakefile"
      abort "No Rakefile detected"
    elsif File.exists? "lib/tasks/brakeman.rake"
      abort "Task already exists"
    end

    require 'fileutils'

    if not File.exists? "lib/tasks"
193
      notify "Creating lib/tasks"
194 195 196 197 198 199 200 201
      FileUtils.mkdir_p "lib/tasks"
    end

    path = File.expand_path(File.dirname(__FILE__))

    FileUtils.cp "#{path}/brakeman/brakeman.rake", "lib/tasks/brakeman.rake"

    if File.exists? "lib/tasks/brakeman.rake"
202 203
      notify "Task created in lib/tasks/brakeman.rake"
      notify "Usage: rake brakeman:run[output_file]"
204
    else
205
      notify "Could not create task"
206 207 208
    end
  end

209
  #Output configuration to YAML
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
  def self.dump_config options
    if options[:create_config].is_a? String
      file = options[:create_config]
    else
      file = nil
    end

    options.delete :create_config

    options.each do |k,v|
      if v.is_a? Set
        options[k] = v.to_a
      end
    end

    if file
      File.open file, "w" do |f|
        YAML.dump options, f
      end
      puts "Output configuration to #{file}"
    else
      puts YAML.dump(options)
    end
    exit
  end

236
  #Run a scan. Generally called from Brakeman.run instead of directly.
237 238
  def self.scan options
    #Load scanner
239
    notify "Loading scanner..."
240 241

    begin
J
Justin Collins 已提交
242
      require 'brakeman/scanner'
243 244 245 246 247
    rescue LoadError
      abort "Cannot find lib/ directory."
    end

    #Start scanning
248
    scanner = Scanner.new options
249

250
    notify "Processing application in #{options[:app_path]}"
251 252
    tracker = scanner.process

253
    if options[:parallel_checks]
254
      notify "Running checks in parallel..."
255
    else
256
      notify "Runnning checks..."
257
    end
258 259
    tracker.run_checks

260
    if options[:output_files]
261
      notify "Generating report..."
262

263 264 265 266 267
      options[:output_files].each_with_index do |output_file, idx|
        File.open output_file, "w" do |f|
          f.write tracker.report.send(options[:output_formats][idx])
        end
        notify "Report saved in '#{output_file}'"
268
      end
269
    elsif options[:print_report]
270
      notify "Generating report..."
271

272 273 274
      options[:output_formats].each do |output_format|
        puts tracker.report.send(output_format)
      end
275
    end
276

277
    tracker
278
  end
J
Justin Collins 已提交
279

280 281 282 283 284 285
  #Rescan a subset of files in a Rails application.
  #
  #A full scan must have been run already to use this method.
  #The returned Tracker object from Brakeman.run is used as a starting point
  #for the rescan.
  #
286 287 288
  #Options may be given as a hash with the same values as Brakeman.run.
  #Note that these options will be merged into the Tracker.
  #
289 290
  #This method returns a RescanReport object with information about the scan.
  #However, the Tracker object will also be modified as the scan is run.
291
  def self.rescan tracker, files, options = {}
292 293
    require 'brakeman/rescanner'

294 295 296 297 298
    tracker.options.merge! options

    @quiet = !!tracker.options[:quiet]
    @debug = !!tracker.options[:debug]

299
    Rescanner.new(tracker.options, tracker.processor, files).recheck
J
Justin Collins 已提交
300
  end
301 302 303 304 305 306 307 308

  def self.notify message
    $stderr.puts message unless @quiet
  end

  def self.debug message
    $stderr.puts message if @debug
  end
O
oreoshake 已提交
309 310 311

  # Compare JSON ouptut from a previous scan and return the diff of the two scans
  def self.compare options
312
    require 'multi_json'
O
oreoshake 已提交
313
    require 'brakeman/differ'
O
oreoshake 已提交
314 315
    raise ArgumentError.new("Comparison file doesn't exist") unless File.exists? options[:previous_results_json]

316
    begin
317 318
      previous_results = MultiJson.load(File.read(options[:previous_results_json]), :symbolize_keys => true)[:warnings]
    rescue MultiJson::DecodeError
319 320 321
      self.notify "Error parsing comparison file: #{options[:previous_results_json]}"
      exit!
    end
O
oreoshake 已提交
322 323

    tracker = run(options)
324 325

    new_results = MultiJson.load(tracker.report.to_json, :symbolize_keys => true)[:warnings]
326

O
oreoshake 已提交
327
    Brakeman::Differ.new(new_results, previous_results).diff
O
oreoshake 已提交
328
  end
329
end