brakeman-lib 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +46 -0
  3. data/README.md +10 -1
  4. data/lib/brakeman.rb +23 -8
  5. data/lib/brakeman/checks/check_detailed_exceptions.rb +1 -1
  6. data/lib/brakeman/checks/check_evaluation.rb +1 -1
  7. data/lib/brakeman/checks/check_execute.rb +10 -0
  8. data/lib/brakeman/checks/check_mass_assignment.rb +4 -6
  9. data/lib/brakeman/checks/check_render.rb +15 -1
  10. data/lib/brakeman/checks/check_sanitize_methods.rb +2 -1
  11. data/lib/brakeman/checks/check_sql.rb +58 -8
  12. data/lib/brakeman/checks/check_verb_confusion.rb +1 -1
  13. data/lib/brakeman/commandline.rb +1 -1
  14. data/lib/brakeman/file_parser.rb +45 -15
  15. data/lib/brakeman/options.rb +7 -2
  16. data/lib/brakeman/parsers/template_parser.rb +24 -0
  17. data/lib/brakeman/processors/alias_processor.rb +105 -18
  18. data/lib/brakeman/processors/base_processor.rb +4 -4
  19. data/lib/brakeman/processors/controller_alias_processor.rb +6 -43
  20. data/lib/brakeman/processors/lib/call_conversion_helper.rb +10 -6
  21. data/lib/brakeman/processors/lib/rails4_config_processor.rb +2 -1
  22. data/lib/brakeman/processors/library_processor.rb +9 -0
  23. data/lib/brakeman/processors/model_processor.rb +31 -0
  24. data/lib/brakeman/report.rb +4 -1
  25. data/lib/brakeman/report/ignore/config.rb +4 -4
  26. data/lib/brakeman/report/ignore/interactive.rb +1 -1
  27. data/lib/brakeman/report/report_github.rb +31 -0
  28. data/lib/brakeman/report/report_sarif.rb +21 -2
  29. data/lib/brakeman/rescanner.rb +1 -1
  30. data/lib/brakeman/scanner.rb +4 -1
  31. data/lib/brakeman/tracker.rb +33 -4
  32. data/lib/brakeman/tracker/collection.rb +57 -7
  33. data/lib/brakeman/tracker/method_info.rb +70 -0
  34. data/lib/brakeman/util.rb +34 -18
  35. data/lib/brakeman/version.rb +1 -1
  36. data/lib/ruby_parser/bm_sexp.rb +14 -0
  37. metadata +18 -2
@@ -1,11 +1,5 @@
1
1
  module Brakeman
2
2
  module CallConversionHelper
3
- def all_literals? exp, expected_type = :array
4
- node_type? exp, expected_type and
5
- exp.length > 1 and
6
- exp.all? { |e| e.is_a? Symbol or node_type? e, :lit, :str }
7
- end
8
-
9
3
  # Join two array literals into one.
10
4
  def join_arrays lhs, rhs, original_exp = nil
11
5
  if array? lhs and array? rhs
@@ -76,6 +70,8 @@ module Brakeman
76
70
 
77
71
  #Have to do this because first element is :array and we have to skip it
78
72
  array[1..-1][index] or original_exp
73
+ elsif all_literals? array
74
+ safe_literal(array.line)
79
75
  else
80
76
  original_exp
81
77
  end
@@ -92,5 +88,13 @@ module Brakeman
92
88
  original_exp
93
89
  end
94
90
  end
91
+
92
+ def hash_values_at hash, keys
93
+ values = keys.map do |key|
94
+ process_hash_access hash, key
95
+ end
96
+
97
+ Sexp.new(:array).concat(values).line(hash.line)
98
+ end
95
99
  end
96
100
  end
@@ -2,10 +2,11 @@ require 'brakeman/processors/lib/rails3_config_processor'
2
2
 
3
3
  class Brakeman::Rails4ConfigProcessor < Brakeman::Rails3ConfigProcessor
4
4
  APPLICATION_CONFIG = s(:call, s(:call, s(:const, :Rails), :application), :configure)
5
+ ALT_APPLICATION_CONFIG = s(:call, s(:call, s(:colon3, :Rails), :application), :configure)
5
6
 
6
7
  # Look for Rails.application.configure do ... end
7
8
  def process_iter exp
8
- if exp.block_call == APPLICATION_CONFIG
9
+ if exp.block_call == APPLICATION_CONFIG or exp.block_call == ALT_APPLICATION_CONFIG
9
10
  @inside_config = true
10
11
  process exp.block if sexp? exp.block
11
12
  @inside_config = false
@@ -54,6 +54,15 @@ class Brakeman::LibraryProcessor < Brakeman::BaseProcessor
54
54
 
55
55
  def process_call exp
56
56
  if process_call_defn? exp
57
+ exp
58
+ elsif @current_method.nil? and exp.target.nil? and (@current_class or @current_module)
59
+ # Methods called inside class / module
60
+ case exp.method
61
+ when :include
62
+ module_name = class_name(exp.first_arg)
63
+ (@current_class || @current_module).add_include module_name
64
+ end
65
+
57
66
  exp
58
67
  else
59
68
  process_default exp
@@ -73,6 +73,8 @@ class Brakeman::ModelProcessor < Brakeman::BaseProcessor
73
73
  @current_class.set_attr_accessible exp
74
74
  when :attr_protected
75
75
  @current_class.set_attr_protected exp
76
+ when :enum
77
+ add_enum_method exp
76
78
  else
77
79
  if @current_class
78
80
  @current_class.add_option method, exp
@@ -87,4 +89,33 @@ class Brakeman::ModelProcessor < Brakeman::BaseProcessor
87
89
  call
88
90
  end
89
91
  end
92
+
93
+ def add_enum_method call
94
+ arg = call.first_arg
95
+ return unless hash? arg
96
+
97
+ enum_name = arg[1].value # first key
98
+ enums = arg[2] # first value
99
+ enums_name = pluralize(enum_name.to_s).to_sym
100
+
101
+ call_line = call.line
102
+
103
+ if hash? enums
104
+ enum_values = enums
105
+ elsif array? enums
106
+ # Build hash for enum values like Rails does
107
+ enum_values = s(:hash).line(call_line)
108
+
109
+ enums.each_sexp.with_index do |v, index|
110
+ enum_values << v
111
+ enum_values << s(:lit, index).line(call_line)
112
+ end
113
+ end
114
+
115
+ enum_method = s(:defn, enum_name, s(:args), safe_literal(call_line))
116
+ enums_method = s(:defs, s(:self), enums_name, s(:args), enum_values)
117
+
118
+ @current_class.add_method :public, enum_name, enum_method, @current_file
119
+ @current_class.add_method :public, enums_name, enums_method, @current_file
120
+ end
90
121
  end
@@ -6,7 +6,7 @@ require 'brakeman/report/report_base'
6
6
  class Brakeman::Report
7
7
  attr_reader :tracker
8
8
 
9
- VALID_FORMATS = [:to_html, :to_pdf, :to_csv, :to_json, :to_tabs, :to_hash, :to_s, :to_markdown, :to_codeclimate, :to_plain, :to_text, :to_junit]
9
+ VALID_FORMATS = [:to_html, :to_pdf, :to_csv, :to_json, :to_tabs, :to_hash, :to_s, :to_markdown, :to_codeclimate, :to_plain, :to_text, :to_junit, :to_github]
10
10
 
11
11
  def initialize tracker
12
12
  @app_tree = tracker.app_tree
@@ -48,6 +48,9 @@ class Brakeman::Report
48
48
  when :to_sonar
49
49
  require_report 'sonar'
50
50
  Brakeman::Report::Sonar
51
+ when :to_github
52
+ require_report 'github'
53
+ Brakeman::Report::Github
51
54
  else
52
55
  raise "Invalid format: #{format}. Should be one of #{VALID_FORMATS.inspect}"
53
56
  end
@@ -100,14 +100,14 @@ module Brakeman
100
100
 
101
101
  # Read configuration to file
102
102
  def read_from_file file = @file
103
- if File.exist? file
103
+ if File.exist? file.absolute
104
104
  begin
105
105
  @already_ignored = JSON.parse(File.read(file), :symbolize_names => true)[:ignored_warnings]
106
106
  rescue => e
107
- raise e, "\nError[#{e.class}] while reading brakeman ignore file: #{file}\n"
107
+ raise e, "\nError[#{e.class}] while reading brakeman ignore file: #{file.relative}\n"
108
108
  end
109
109
  else
110
- Brakeman.notify "[Notice] Could not find ignore configuration in #{file}"
110
+ Brakeman.notify "[Notice] Could not find ignore configuration in #{file.relative}"
111
111
  @already_ignored = []
112
112
  end
113
113
 
@@ -134,7 +134,7 @@ module Brakeman
134
134
  :brakeman_version => Brakeman::Version
135
135
  }
136
136
 
137
- File.open file, "w" do |f|
137
+ File.open file.absolute, "w" do |f|
138
138
  f.puts JSON.pretty_generate(output)
139
139
  end
140
140
  end
@@ -62,7 +62,7 @@ module Brakeman
62
62
  process_warnings
63
63
  end
64
64
 
65
- m.choice "Hide previously ignored warnings" do
65
+ m.choice "Inspect new warnings" do
66
66
  @skip_ignored = true
67
67
  pre_show_help
68
68
  process_warnings
@@ -0,0 +1,31 @@
1
+ # Github Actions Formatter
2
+ # Formats warnings as workflow commands to create annotations in GitHub UI
3
+ class Brakeman::Report::Github < Brakeman::Report::Base
4
+ def generate_report
5
+ # @see https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
6
+ errors.concat(warnings).join("\n")
7
+ end
8
+
9
+ def warnings
10
+ all_warnings
11
+ .map { |warning| "::warning file=#{warning_file(warning)},line=#{warning.line}::#{warning.message}" }
12
+ end
13
+
14
+ def errors
15
+ tracker.errors.map do |error|
16
+ if error[:exception].is_a?(Racc::ParseError)
17
+ # app/services/balance.rb:4 :: parse error on value "..." (tDOT3)
18
+ file, line = error[:exception].message.split(':').map(&:strip)[0,2]
19
+ "::error file=#{file},line=#{line}::#{clean_message(error[:error])}"
20
+ else
21
+ "::error ::#{clean_message(error[:error])}"
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def clean_message(msg)
29
+ msg.gsub('::','').squeeze(' ')
30
+ end
31
+ end
@@ -48,7 +48,7 @@ class Brakeman::Report::SARIF < Brakeman::Report::Base
48
48
  end
49
49
 
50
50
  def results
51
- @results ||= all_warnings.map do |warning|
51
+ @results ||= tracker.checks.all_warnings.map do |warning|
52
52
  rule_id = render_id warning
53
53
  result_level = infer_level warning
54
54
  message_text = render_message warning.message.to_s
@@ -72,6 +72,23 @@ class Brakeman::Report::SARIF < Brakeman::Report::Base
72
72
  ],
73
73
  }
74
74
 
75
+ if @ignore_filter && @ignore_filter.ignored?(warning)
76
+ result[:suppressions] = [
77
+ {
78
+ :kind => 'external',
79
+ :justification => @ignore_filter.note_for(warning),
80
+ :location => {
81
+ :physicalLocation => {
82
+ :artifactLocation => {
83
+ :uri => @ignore_filter.file.relative,
84
+ :uriBaseId => '%SRCROOT%',
85
+ },
86
+ },
87
+ },
88
+ }
89
+ ]
90
+ end
91
+
75
92
  result
76
93
  end
77
94
  end
@@ -85,7 +102,7 @@ class Brakeman::Report::SARIF < Brakeman::Report::Base
85
102
 
86
103
  # Returns a de-duplicated set of warnings, used to generate rules
87
104
  def unique_warnings_by_warning_code
88
- @unique_warnings_by_warning_code ||= all_warnings.uniq { |w| w.warning_code }
105
+ @unique_warnings_by_warning_code ||= tracker.checks.all_warnings.uniq { |w| w.warning_code }
89
106
  end
90
107
 
91
108
  def render_id warning
@@ -94,6 +111,8 @@ class Brakeman::Report::SARIF < Brakeman::Report::Base
94
111
  end
95
112
 
96
113
  def render_message message
114
+ return message if message.nil?
115
+
97
116
  # Ensure message ends with a period
98
117
  if message.end_with? "."
99
118
  message
@@ -391,7 +391,7 @@ class Brakeman::Rescanner < Brakeman::Scanner
391
391
 
392
392
  def parse_ruby_files list
393
393
  paths = list.select(&:exists?)
394
- file_parser = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout])
394
+ file_parser = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout], tracker.options[:parallel_checks])
395
395
  file_parser.parse_files paths
396
396
  tracker.add_errors(file_parser.errors)
397
397
  file_parser.file_list
@@ -71,7 +71,7 @@ class Brakeman::Scanner
71
71
  end
72
72
 
73
73
  def parse_files
74
- fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout])
74
+ fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout], tracker.options[:parallel_checks])
75
75
 
76
76
  fp.parse_files tracker.app_tree.ruby_file_paths
77
77
 
@@ -353,6 +353,9 @@ class Brakeman::Scanner
353
353
  def parse_ruby_file file
354
354
  fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout])
355
355
  fp.parse_ruby(file.read, file)
356
+ rescue Exception => e
357
+ tracker.error(e)
358
+ nil
356
359
  end
357
360
  end
358
361
 
@@ -35,6 +35,7 @@ class Brakeman::Tracker
35
35
  #class they are.
36
36
  @models = {}
37
37
  @models[UNKNOWN_MODEL] = Brakeman::Model.new(UNKNOWN_MODEL, nil, @app_tree.file_path("NOT_REAL.rb"), nil, self)
38
+ @method_cache = {}
38
39
  @routes = {}
39
40
  @initializers = {}
40
41
  @errors = []
@@ -99,8 +100,8 @@ class Brakeman::Tracker
99
100
  classes.each do |set|
100
101
  set.each do |set_name, collection|
101
102
  collection.each_method do |method_name, definition|
102
- src = definition[:src]
103
- yield src, set_name, method_name, definition[:file]
103
+ src = definition.src
104
+ yield src, set_name, method_name, definition.file
104
105
  end
105
106
  end
106
107
  end
@@ -220,6 +221,34 @@ class Brakeman::Tracker
220
221
  nil
221
222
  end
222
223
 
224
+ def find_method method_name, class_name, method_type = :instance
225
+ return nil unless method_name.is_a? Symbol
226
+
227
+ klass = find_class(class_name)
228
+ return nil unless klass
229
+
230
+ cache_key = [klass, method_name, method_type]
231
+
232
+ if method = @method_cache[cache_key]
233
+ return method
234
+ end
235
+
236
+ if method = klass.get_method(method_name, method_type)
237
+ return method
238
+ else
239
+ # Check modules included for method definition
240
+ # TODO: only for instance methods, otherwise check extends!
241
+ klass.includes.each do |included_name|
242
+ if method = find_method(method_name, included_name, method_type)
243
+ return (@method_cache[cache_key] = method)
244
+ end
245
+ end
246
+
247
+ # Not in any included modules, check the parent
248
+ @method_cache[cache_key] = find_method(method_name, klass.parent)
249
+ end
250
+ end
251
+
223
252
  def index_call_sites
224
253
  finder = Brakeman::FindAllCalls.new self
225
254
 
@@ -285,8 +314,8 @@ class Brakeman::Tracker
285
314
  method_sets.each do |set|
286
315
  set.each do |set_name, info|
287
316
  info.each_method do |method_name, definition|
288
- src = definition[:src]
289
- finder.process_source src, :class => set_name, :method => method_name, :file => definition[:file]
317
+ src = definition.src
318
+ finder.process_source src, :class => set_name, :method => method_name, :file => definition.file
290
319
  end
291
320
  end
292
321
  end
@@ -1,4 +1,5 @@
1
1
  require 'brakeman/util'
2
+ require 'brakeman/tracker/method_info'
2
3
 
3
4
  module Brakeman
4
5
  class Collection
@@ -13,6 +14,8 @@ module Brakeman
13
14
  @src = {}
14
15
  @includes = []
15
16
  @methods = { :public => {}, :private => {}, :protected => {} }
17
+ @class_methods = {}
18
+ @simple_methods = { :class => {}, instance: {} }
16
19
  @options = {}
17
20
  @tracker = tracker
18
21
 
@@ -22,7 +25,7 @@ module Brakeman
22
25
  def ancestor? parent, seen={}
23
26
  seen[self.name] = true
24
27
 
25
- if self.parent == parent or seen[self.parent]
28
+ if self.parent == parent or self.name == parent or seen[self.parent]
26
29
  true
27
30
  elsif parent_model = collection[self.parent]
28
31
  parent_model.ancestor? parent, seen
@@ -37,7 +40,7 @@ module Brakeman
37
40
  end
38
41
 
39
42
  def add_include class_name
40
- @includes << class_name
43
+ @includes << class_name unless ancestor?(class_name)
41
44
  end
42
45
 
43
46
  def add_option name, exp
@@ -46,11 +49,17 @@ module Brakeman
46
49
  end
47
50
 
48
51
  def add_method visibility, name, src, file_name
52
+ meth_info = Brakeman::MethodInfo.new(name, src, self, file_name)
53
+ add_simple_method_maybe meth_info
54
+
49
55
  if src.node_type == :defs
56
+ @class_methods[name] = meth_info
57
+
58
+ # TODO fix this weirdness
50
59
  name = :"#{src[1]}.#{name}"
51
60
  end
52
61
 
53
- @methods[visibility][name] = { :src => src, :file => file_name }
62
+ @methods[visibility][name] = meth_info
54
63
  end
55
64
 
56
65
  def each_method
@@ -61,16 +70,31 @@ module Brakeman
61
70
  end
62
71
  end
63
72
 
64
- def get_method name
65
- each_method do |n, info|
66
- if n == name
67
- return info
73
+ def get_method name, type = :instance
74
+ case type
75
+ when :class
76
+ get_class_method name
77
+ when :instance
78
+ get_instance_method name
79
+ else
80
+ raise "Unexpected method type: #{type.inspect}"
81
+ end
82
+ end
83
+
84
+ def get_instance_method name
85
+ @methods.each do |_vis, meths|
86
+ if meths[name]
87
+ return meths[name]
68
88
  end
69
89
  end
70
90
 
71
91
  nil
72
92
  end
73
93
 
94
+ def get_class_method name
95
+ @class_methods[name]
96
+ end
97
+
74
98
  def file
75
99
  @files.first
76
100
  end
@@ -90,5 +114,31 @@ module Brakeman
90
114
  def methods_public
91
115
  @methods[:public]
92
116
  end
117
+
118
+ def get_simple_method_return_value type, name
119
+ @simple_methods[type][name]
120
+ end
121
+
122
+ private
123
+
124
+ def add_simple_method_maybe meth_info
125
+ if meth_info.very_simple_method?
126
+ add_simple_method meth_info
127
+ end
128
+ end
129
+
130
+ def add_simple_method meth_info
131
+ name = meth_info.name
132
+ value = meth_info.return_value
133
+
134
+ case meth_info.src.node_type
135
+ when :defn
136
+ @simple_methods[:instance][name] = value
137
+ when :defs
138
+ @simple_methods[:class][name] = value
139
+ else
140
+ raise "Expected sexp type: #{src.node_type}"
141
+ end
142
+ end
93
143
  end
94
144
  end