brakeman.rb 12.1 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
  @debug = false
  @quiet = false
13
  @loaded_dependencies = []
14

J
Justin Collins 已提交
15 16 17 18 19
  #Run Brakeman scan. Returns Tracker object.
  #
  #Options:
  #
  #  * :app_path - path to root of Rails app (required)
J
Justin Collins 已提交
20
  #  * :assume_all_routes - assume all methods are routes (default: true)
J
Justin Collins 已提交
21 22 23 24 25
  #  * :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)
26
  #  * :exit_on_warn - return false if warnings found, true otherwise. Not recommended for library use (default: false)
27
  #  * :highlight_user_input - highlight user input in reported warnings (default: true)
J
Justin Collins 已提交
28 29
  #  * :html_style - path to CSS file
  #  * :ignore_model_output - consider models safe (default: false)
30
  #  * :interprocedural - limited interprocedural processing of method calls (default: false)
J
Justin Collins 已提交
31 32
  #  * :message_limit - limit length of messages
  #  * :min_confidence - minimum confidence (0-2, 0 is highest)
33 34
  #  * :output_files - files for output
  #  * :output_formats - formats for output (:to_s, :to_tabs, :to_csv, :to_html)
J
Justin Collins 已提交
35
  #  * :parallel_checks - run checks in parallel (default: true)
36 37
  #  * :print_report - if no output file specified, print to stdout (default: false)
  #  * :quiet - suppress most messages (default: true)
J
Justin Collins 已提交
38 39 40 41 42 43
  #  * :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)
44
  #  * :absolute_paths - show absolute path of each file (default: false)
45
  #  * :summary_only - only output summary section of report
J
Justin Collins 已提交
46
  #                    (does not apply to tabs format)
J
Justin Collins 已提交
47
  #
48
  #Alternatively, just supply a path as a string.
49
  def self.run options
50 51
    options = set_options options

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

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

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

    if options[:quiet] == :command_line
      command_line = true
      options.delete :quiet
    end

72
    options = default_options.merge(load_options(options[:config_file], options[:quiet])).merge(options)
73

74 75 76 77
    if options[:quiet].nil? and not command_line
      options[:quiet] = true
    end

78
    options[:app_path] = File.expand_path(options[:app_path])
79
    options[:output_formats] = get_output_formats options
80 81 82 83

    options
  end

G
grosser 已提交
84 85 86
  CONFIG_FILES = [
    File.expand_path("./config/brakeman.yml"),
    File.expand_path("~/.brakeman/config.yml"),
87
    File.expand_path("/etc/brakeman/config.yml")
G
grosser 已提交
88
  ]
89

G
grosser 已提交
90
  #Load options from YAML file
91
  def self.load_options custom_location, quiet
92
    #Load configuration file
G
grosser 已提交
93 94
    if config = config_file(custom_location)
      options = YAML.load_file config
95 96 97 98 99 100 101 102 103 104 105

      if options
        options.each { |k, v| options[k] = Set.new v if v.is_a? Array }

        # notify if options[:quiet] and quiet is nil||false
        notify "[Notice] Using configuration in #{config}" unless (options[:quiet] || quiet)
        options
      else
        notify "[Notice] Empty configuration file: #{config}" unless quiet
        {}
      end
G
grosser 已提交
106 107 108 109 110
    else
      {}
    end
  end

111 112 113
  def self.config_file custom_location = nil
    supported_locations = [File.expand_path(custom_location || "")] + CONFIG_FILES
    supported_locations.detect {|f| File.file?(f) }
114 115
  end

116
  #Default set of options
117
  def self.default_options
118 119
    { :assume_all_routes => true,
      :skip_checks => Set.new,
120
      :check_arguments => true,
121 122 123 124
      :safe_methods => Set.new,
      :min_confidence => 2,
      :combine_locations => true,
      :collapse_mass_assignment => true,
125
      :highlight_user_input => true,
126 127 128
      :ignore_redirect_to_model => true,
      :ignore_model_output => false,
      :message_limit => 100,
129
      :parallel_checks => true,
F
fsword 已提交
130
      :relative_path => false,
J
Justin Collins 已提交
131
      :report_progress => true,
132
      :html_style => "#{File.expand_path(File.dirname(__FILE__))}/brakeman/format/style.css"
133 134 135
    }
  end

136 137 138
  #Determine output formats based on options[:output_formats]
  #or options[:output_files]
  def self.get_output_formats options
139
    #Set output format
140 141 142
    if options[:output_format] && options[:output_files] && options[:output_files].size > 1
      raise ArgumentError, "Cannot specify output format if multiple output files specified"
    end
143
    if options[:output_format]
144 145 146
      get_formats_from_output_format options[:output_format]
    elsif options[:output_files]
      get_formats_from_output_files options[:output_files]
147
    else
148 149 150 151 152 153
      begin
        require 'terminal-table'
        return [:to_s]
      rescue LoadError
        return [:to_json]
      end
154 155
    end
  end
156

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
  def self.get_formats_from_output_format output_format
    case 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
  end
  private_class_method :get_formats_from_output_format
174

175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
  def self.get_formats_from_output_files output_files
    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
190 191 192
      end
    end
  end
193
  private_class_method :get_formats_from_output_files
194

195
  #Output list of checks (for `-k` option)
196
  def self.list_checks
J
Justin Collins 已提交
197
    require 'brakeman/scanner'
S
soffolk 已提交
198
    format_length = 30
199

200
    $stderr.puts "Available Checks:"
S
soffolk 已提交
201 202
    $stderr.puts "-" * format_length
    Checks.checks.each do |check|
203
      $stderr.printf("%-#{format_length}s%s\n", check.name, check.description)
S
soffolk 已提交
204
    end
205 206
  end

207 208 209
  #Installs Rake task for running Brakeman,
  #which basically means copying `lib/brakeman/brakeman.rake` to
  #`lib/tasks/brakeman.rake` in the current Rails application.
210 211 212 213 214 215 216 217 218 219 220 221 222
  def self.install_rake_task install_path = nil
    if install_path
      rake_path = File.join(install_path, "Rakefile")
      task_path = File.join(install_path, "lib", "tasks", "brakeman.rake")
    else
      rake_path = "Rakefile"
      task_path = File.join("lib", "tasks", "brakeman.rake")
    end

    if not File.exists? rake_path
      raise RakeInstallError, "No Rakefile detected"
    elsif File.exists? task_path
      raise RakeInstallError, "Task already exists"
223 224 225 226 227
    end

    require 'fileutils'

    if not File.exists? "lib/tasks"
228
      notify "Creating lib/tasks"
229 230 231 232 233
      FileUtils.mkdir_p "lib/tasks"
    end

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

234
    FileUtils.cp "#{path}/brakeman/brakeman.rake", task_path
235

236 237
    if File.exists? task_path
      notify "Task created in #{task_path}"
238
      notify "Usage: rake brakeman:run[output_file]"
239
    else
240
      raise RakeInstallError, "Could not create task"
241 242 243
    end
  end

244
  #Output configuration to YAML
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
  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

271
  #Run a scan. Generally called from Brakeman.run instead of directly.
272 273
  def self.scan options
    #Load scanner
274
    notify "Loading scanner..."
275 276

    begin
J
Justin Collins 已提交
277
      require 'brakeman/scanner'
278
    rescue LoadError
279
      raise NoBrakemanError, "Cannot find lib/ directory."
280 281 282
    end

    #Start scanning
283
    scanner = Scanner.new options
284

285
    notify "Processing application in #{options[:app_path]}"
286 287
    tracker = scanner.process

288
    if options[:parallel_checks]
289
      notify "Running checks in parallel..."
290
    else
291
      notify "Runnning checks..."
292
    end
J
Justin Collins 已提交
293

294 295
    tracker.run_checks

J
Justin Collins 已提交
296 297
    self.filter_warnings tracker, options

298
    if options[:output_files]
299
      notify "Generating report..."
300

301
      write_report_to_files tracker, options[:output_files]
302
    elsif options[:print_report]
303
      notify "Generating report..."
304

305
      write_report_to_formats tracker, options[:output_formats]
306
    end
307

308
    tracker
309
  end
310

311 312 313
  def self.write_report_to_files tracker, output_files
    output_files.each_with_index do |output_file, idx|
      File.open output_file, "w" do |f|
314
        f.write tracker.report.format(tracker.options[:output_formats][idx])
315 316 317 318 319
      end
      notify "Report saved in '#{output_file}'"
    end
  end
  private_class_method :write_report_to_files
320

321 322
  def self.write_report_to_formats tracker, output_formats
    output_formats.each do |output_format|
323
      puts tracker.report.format(output_format)
324 325 326
    end
  end
  private_class_method :write_report_to_formats
J
Justin Collins 已提交
327

328 329 330 331 332 333
  #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.
  #
334 335 336
  #Options may be given as a hash with the same values as Brakeman.run.
  #Note that these options will be merged into the Tracker.
  #
337 338
  #This method returns a RescanReport object with information about the scan.
  #However, the Tracker object will also be modified as the scan is run.
339
  def self.rescan tracker, files, options = {}
340 341
    require 'brakeman/rescanner'

342 343 344 345 346
    tracker.options.merge! options

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

347
    Rescanner.new(tracker.options, tracker.processor, files).recheck
J
Justin Collins 已提交
348
  end
349 350 351 352 353 354 355 356

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

  def self.debug message
    $stderr.puts message if @debug
  end
O
oreoshake 已提交
357 358 359

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

364
    begin
365 366
      previous_results = MultiJson.load(File.read(options[:previous_results_json]), :symbolize_keys => true)[:warnings]
    rescue MultiJson::DecodeError
367 368 369
      self.notify "Error parsing comparison file: #{options[:previous_results_json]}"
      exit!
    end
O
oreoshake 已提交
370 371

    tracker = run(options)
372

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

O
oreoshake 已提交
375
    Brakeman::Differ.new(new_results, previous_results).diff
O
oreoshake 已提交
376
  end
377

378
  def self.load_brakeman_dependency name
379 380 381 382 383 384 385 386 387 388 389
    return if @loaded_dependencies.include? name

    begin
      require name
    rescue LoadError => e
      $stderr.puts e.message
      $stderr.puts "Please install the appropriate dependency."
      exit! -1
    end
  end

J
Justin Collins 已提交
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
  def self.filter_warnings tracker, options
    require 'brakeman/report/ignore/config'

    app_tree = Brakeman::AppTree.from_options(options)

    if options[:ignore_file]
      file = options[:ignore_file]
    elsif app_tree.exists? "config/brakeman.ignore"
      file = app_tree.expand_path("config/brakeman.ignore")
    elsif not options[:interactive_ignore]
      return
    end

    notify "Filtering warnings..."

    if options[:interactive_ignore]
      require 'brakeman/report/ignore/interactive'
      config = InteractiveIgnorer.new(file, tracker.warnings).start
    else
      notify "[Notice] Using '#{file}' to filter warnings"
      config = IgnoreConfig.new(file, tracker.warnings)
      config.read_from_file
      config.filter_ignored
    end

    tracker.ignored_filter = config
  end

418
  class DependencyError < RuntimeError; end
419
  class RakeInstallError < RuntimeError; end
420
  class NoBrakemanError < RuntimeError; end
421
  class NoApplication < RuntimeError; end
422
end