brakeman-lib 4.8.1 → 4.10.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +39 -0
  3. data/README.md +5 -3
  4. data/lib/brakeman.rb +20 -0
  5. data/lib/brakeman/checks/base_check.rb +1 -1
  6. data/lib/brakeman/checks/check_basic_auth.rb +2 -0
  7. data/lib/brakeman/checks/check_csrf_token_forgery_cve.rb +28 -0
  8. data/lib/brakeman/checks/check_deserialize.rb +21 -1
  9. data/lib/brakeman/checks/check_execute.rb +1 -1
  10. data/lib/brakeman/checks/check_json_entity_escape.rb +38 -0
  11. data/lib/brakeman/checks/check_mass_assignment.rb +19 -4
  12. data/lib/brakeman/checks/check_model_attr_accessible.rb +1 -1
  13. data/lib/brakeman/checks/check_model_attributes.rb +1 -1
  14. data/lib/brakeman/checks/check_page_caching_cve.rb +37 -0
  15. data/lib/brakeman/checks/check_permit_attributes.rb +1 -1
  16. data/lib/brakeman/checks/check_regex_dos.rb +1 -1
  17. data/lib/brakeman/checks/check_skip_before_filter.rb +4 -4
  18. data/lib/brakeman/checks/check_sql.rb +1 -1
  19. data/lib/brakeman/checks/check_template_injection.rb +32 -0
  20. data/lib/brakeman/commandline.rb +25 -1
  21. data/lib/brakeman/file_parser.rb +5 -0
  22. data/lib/brakeman/options.rb +21 -1
  23. data/lib/brakeman/processors/alias_processor.rb +4 -5
  24. data/lib/brakeman/processors/controller_processor.rb +1 -1
  25. data/lib/brakeman/processors/haml_template_processor.rb +8 -1
  26. data/lib/brakeman/processors/lib/call_conversion_helper.rb +1 -1
  27. data/lib/brakeman/processors/lib/find_all_calls.rb +27 -12
  28. data/lib/brakeman/processors/output_processor.rb +1 -1
  29. data/lib/brakeman/processors/template_alias_processor.rb +5 -0
  30. data/lib/brakeman/report.rb +7 -0
  31. data/lib/brakeman/report/ignore/config.rb +4 -0
  32. data/lib/brakeman/report/report_sarif.rb +114 -0
  33. data/lib/brakeman/report/report_text.rb +37 -16
  34. data/lib/brakeman/scanner.rb +4 -1
  35. data/lib/brakeman/tracker.rb +3 -1
  36. data/lib/brakeman/tracker/config.rb +6 -4
  37. data/lib/brakeman/tracker/constants.rb +8 -7
  38. data/lib/brakeman/tracker/controller.rb +1 -1
  39. data/lib/brakeman/util.rb +18 -2
  40. data/lib/brakeman/version.rb +1 -1
  41. data/lib/brakeman/warning_codes.rb +6 -0
  42. data/lib/ruby_parser/bm_sexp.rb +9 -9
  43. metadata +38 -5
@@ -102,6 +102,13 @@ module Brakeman
102
102
  app_path = "."
103
103
  end
104
104
 
105
+ if options[:ensure_ignore_notes] and options[:previous_results_json]
106
+ warn '[Notice] --ensure-ignore-notes may not be used at the same ' \
107
+ 'time as --compare. Deactivating --ensure-ignore-notes. ' \
108
+ 'Please see `brakeman --help` for valid options'
109
+ options[:ensure_ignore_notes] = false
110
+ end
111
+
105
112
  return options, app_path
106
113
  end
107
114
 
@@ -115,7 +122,20 @@ module Brakeman
115
122
 
116
123
  # Runs a regular report based on the options provided.
117
124
  def regular_report options
118
- tracker = run_brakeman options
125
+ tracker = run_brakeman options
126
+
127
+ ensure_ignore_notes_failed = false
128
+ if tracker.options[:ensure_ignore_notes]
129
+ fingerprints = Brakeman::ignore_file_entries_with_empty_notes tracker.ignored_filter&.file
130
+
131
+ unless fingerprints.empty?
132
+ ensure_ignore_notes_failed = true
133
+ warn '[Error] Notes required for all ignored warnings when ' \
134
+ '--ensure-ignore-notes is set. No notes provided for these ' \
135
+ 'warnings: '
136
+ fingerprints.each { |f| warn f }
137
+ end
138
+ end
119
139
 
120
140
  if tracker.options[:exit_on_warn] and not tracker.filtered_warnings.empty?
121
141
  quit Brakeman::Warnings_Found_Exit_Code
@@ -124,6 +144,10 @@ module Brakeman
124
144
  if tracker.options[:exit_on_error] and tracker.errors.any?
125
145
  quit Brakeman::Errors_Found_Exit_Code
126
146
  end
147
+
148
+ if ensure_ignore_notes_failed
149
+ quit Brakeman::Empty_Ignore_Note_Exit_Code
150
+ end
127
151
  end
128
152
 
129
153
  # Actually run Brakeman.
@@ -33,7 +33,12 @@ module Brakeman
33
33
  end
34
34
  end
35
35
 
36
+ # _path_ can be a string or a Brakeman::FilePath
36
37
  def parse_ruby input, path
38
+ if path.is_a? Brakeman::FilePath
39
+ path = path.relative
40
+ end
41
+
37
42
  begin
38
43
  Brakeman.debug "Parsing #{path}"
39
44
  RubyParser.new.parse input, path, @timeout
@@ -67,6 +67,10 @@ module Brakeman::Options
67
67
  options[:ensure_latest] = true
68
68
  end
69
69
 
70
+ opts.on "--ensure-ignore-notes", "Fail when an ignored warnings does not include a note" do
71
+ options[:ensure_ignore_notes] = true
72
+ end
73
+
70
74
  opts.on "-3", "--rails3", "Force Rails 3 mode" do
71
75
  options[:rails3] = true
72
76
  end
@@ -225,7 +229,7 @@ module Brakeman::Options
225
229
 
226
230
  opts.on "-f",
227
231
  "--format TYPE",
228
- [:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc, :plain, :table, :junit],
232
+ [:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc, :plain, :table, :junit, :sarif],
229
233
  "Specify output formats. Default is text" do |type|
230
234
 
231
235
  type = "s" if type == :text
@@ -301,6 +305,22 @@ module Brakeman::Options
301
305
  options[:github_repo] = repo
302
306
  end
303
307
 
308
+ opts.on "--text-fields field1,field2,etc.", Array, "Specify fields for text report format" do |format|
309
+ valid_options = [:category, :category_id, :check, :code, :confidence, :file, :fingerprint, :line, :link, :message, :render_path]
310
+
311
+ options[:text_fields] = format.map(&:to_sym)
312
+
313
+ if options[:text_fields] == [:all]
314
+ options[:text_fields] = valid_options
315
+ else
316
+ invalid_options = (options[:text_fields] - valid_options)
317
+
318
+ unless invalid_options.empty?
319
+ raise OptionParser::ParseError, "\nInvalid format options: #{invalid_options.inspect}"
320
+ end
321
+ end
322
+ end
323
+
304
324
  opts.on "-w",
305
325
  "--confidence-level LEVEL",
306
326
  ["1", "2", "3"],
@@ -82,7 +82,6 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
82
82
  def replace exp, int = 0
83
83
  return exp if int > 3
84
84
 
85
-
86
85
  if replacement = env[exp] and not duplicate? replacement
87
86
  replace(replacement.deep_clone(exp.line), int + 1)
88
87
  elsif tracker and replacement = tracker.constant_lookup(exp) and not duplicate? replacement
@@ -237,7 +236,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
237
236
  env[target_var] = target
238
237
  return target
239
238
  elsif string? target and string_interp? first_arg
240
- exp = Sexp.new(:dstr, target.value + first_arg[1]).concat(first_arg[2..-1])
239
+ exp = Sexp.new(:dstr, target.value + first_arg[1]).concat(first_arg.sexp_body(2))
241
240
  env[target_var] = exp
242
241
  elsif string? first_arg and string_interp? target
243
242
  if string? target.last
@@ -731,14 +730,14 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
731
730
  def array_include_all_literals? exp
732
731
  call? exp and
733
732
  exp.method == :include? and
734
- all_literals? exp.target
733
+ (all_literals? exp.target or dir_glob? exp.target)
735
734
  end
736
735
 
737
736
  def array_detect_all_literals? exp
738
737
  call? exp and
739
738
  [:detect, :find].include? exp.method and
740
739
  exp.first_arg.nil? and
741
- all_literals? exp.target
740
+ (all_literals? exp.target or dir_glob? exp.target)
742
741
  end
743
742
 
744
743
  #Sets @inside_if = true
@@ -942,7 +941,7 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
942
941
  args = exp.args
943
942
  exp.pop # remove last arg
944
943
  if args.length > 1
945
- exp.arglist = args[1..-1]
944
+ exp.arglist = args.sexp_body
946
945
  end
947
946
  end
948
947
 
@@ -202,7 +202,7 @@ class Brakeman::ControllerProcessor < Brakeman::BaseProcessor
202
202
  end
203
203
 
204
204
  if node_type? exp.block, :block
205
- block_inner = exp.block[1..-1]
205
+ block_inner = exp.block.sexp_body
206
206
  else
207
207
  block_inner = [exp.block]
208
208
  end
@@ -76,6 +76,13 @@ class Brakeman::HamlTemplateProcessor < Brakeman::TemplateProcessor
76
76
  end
77
77
  end
78
78
 
79
+ ESCAPE_METHODS = [
80
+ :html_escape,
81
+ :html_escape_without_haml_xss,
82
+ :escape_once,
83
+ :escape_once_without_haml_xss
84
+ ]
85
+
79
86
  def get_pushed_value exp, default = :output
80
87
  return exp unless sexp? exp
81
88
 
@@ -105,7 +112,7 @@ class Brakeman::HamlTemplateProcessor < Brakeman::TemplateProcessor
105
112
  when :call
106
113
  if exp.method == :to_s or exp.method == :strip
107
114
  get_pushed_value(exp.target, default)
108
- elsif haml_helpers? exp.target and exp.method == :html_escape
115
+ elsif haml_helpers? exp.target and ESCAPE_METHODS.include? exp.method
109
116
  get_pushed_value(exp.first_arg, :escaped_output)
110
117
  elsif @javascript and (exp.method == :j or exp.method == :escape_javascript) # TODO: Remove - this is not safe
111
118
  get_pushed_value(exp.first_arg, :escaped_output)
@@ -10,7 +10,7 @@ module Brakeman
10
10
  def join_arrays lhs, rhs, original_exp = nil
11
11
  if array? lhs and array? rhs
12
12
  result = Sexp.new(:array)
13
- result.line(lhs.line || rhs.line)
13
+ result.line(lhs.line || rhs.line || 1)
14
14
  result.concat lhs[1..-1]
15
15
  result.concat rhs[1..-1]
16
16
  result
@@ -20,6 +20,7 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
20
20
  @current_template = opts[:template]
21
21
  @current_file = opts[:file]
22
22
  @current_call = nil
23
+ @full_call = nil
23
24
  process exp
24
25
  end
25
26
 
@@ -137,7 +138,8 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
137
138
  :call => exp,
138
139
  :nested => false,
139
140
  :location => make_location,
140
- :parent => @current_call }.freeze
141
+ :parent => @current_call,
142
+ :full_call => @full_call }.freeze
141
143
  end
142
144
 
143
145
  #Gets the target of a call as a Symbol
@@ -214,34 +216,47 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
214
216
  #Return info hash for a call Sexp
215
217
  def create_call_hash exp
216
218
  target = get_target exp.target
217
-
218
- if call? target or node_type? target, :dxstr # need to index `` even if target of a call
219
- already_in_target = @in_target
220
- @in_target = true
221
- process target
222
- @in_target = already_in_target
223
-
224
- target = get_target(target, :include_calls)
225
- end
219
+ target_symbol = get_target(target, :include_calls)
226
220
 
227
221
  method = exp.method
228
222
 
229
223
  call_hash = {
230
- :target => target,
224
+ :target => target_symbol,
231
225
  :method => method,
232
226
  :call => exp,
233
227
  :nested => @in_target,
234
228
  :chain => get_chain(exp),
235
229
  :location => make_location,
236
- :parent => @current_call
230
+ :parent => @current_call,
231
+ :full_call => @full_call
237
232
  }
238
233
 
234
+ unless @in_target
235
+ @full_call = call_hash
236
+ end
237
+
238
+ # Process up the call chain
239
+ if call? target or node_type? target, :dxstr # need to index `` even if target of a call
240
+ already_in_target = @in_target
241
+ @in_target = true
242
+ process target
243
+ @in_target = already_in_target
244
+ end
245
+
246
+ # Process call arguments
247
+ # but add the current call as the 'parent'
248
+ # to any calls in the arguments
239
249
  old_parent = @current_call
240
250
  @current_call = call_hash
241
251
 
252
+ # Do not set @full_call when processing arguments
253
+ old_full_call = @full_call
254
+ @full_call = nil
255
+
242
256
  process_call_args exp
243
257
 
244
258
  @current_call = old_parent
259
+ @full_call = old_full_call
245
260
 
246
261
  call_hash
247
262
  end
@@ -88,7 +88,7 @@ class Brakeman::OutputProcessor < Ruby2Ruby
88
88
 
89
89
  def process_iter exp
90
90
  call = process exp[1]
91
- block = process_rlist exp[3..-1]
91
+ block = process_rlist exp.sexp_body(3)
92
92
  out = "#{call} do\n #{block}\n end"
93
93
 
94
94
  out
@@ -20,6 +20,11 @@ class Brakeman::TemplateAliasProcessor < Brakeman::AliasProcessor
20
20
 
21
21
  #Process template
22
22
  def process_template name, args, _, line = nil
23
+ # Strip forward slash from beginning of template path.
24
+ # This also happens in RenderHelper#process_template but
25
+ # we need it here too to accurately avoid circular renders below.
26
+ name = name.to_s.gsub(/^\//, "")
27
+
23
28
  if @called_from
24
29
  if @called_from.include_template? name
25
30
  Brakeman.debug "Skipping circular render from #{@template.name} to #{name}"
@@ -43,6 +43,8 @@ class Brakeman::Report
43
43
  when :to_junit
44
44
  require_report 'junit'
45
45
  Brakeman::Report::JUnit
46
+ when :to_sarif
47
+ return self.to_sarif
46
48
  else
47
49
  raise "Invalid format: #{format}. Should be one of #{VALID_FORMATS.inspect}"
48
50
  end
@@ -85,6 +87,11 @@ class Brakeman::Report
85
87
  alias to_plain to_text
86
88
  alias to_s to_text
87
89
 
90
+ def to_sarif
91
+ require_report 'sarif'
92
+ generate Brakeman::Report::SARIF
93
+ end
94
+
88
95
  def generate reporter
89
96
  reporter.new(@tracker).generate_report
90
97
  end
@@ -94,6 +94,10 @@ module Brakeman
94
94
  end
95
95
  end
96
96
 
97
+ def already_ignored_entries_with_empty_notes
98
+ @already_ignored.select { |i| i if i[:note].strip.empty? }
99
+ end
100
+
97
101
  # Read configuration to file
98
102
  def read_from_file file = @file
99
103
  if File.exist? file
@@ -0,0 +1,114 @@
1
+ class Brakeman::Report::SARIF < Brakeman::Report::Base
2
+ def generate_report
3
+ sarif_log = {
4
+ :version => '2.1.0',
5
+ :$schema => 'https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json',
6
+ :runs => runs,
7
+ }
8
+ JSON.pretty_generate sarif_log
9
+ end
10
+
11
+ def runs
12
+ [
13
+ {
14
+ :tool => {
15
+ :driver => {
16
+ :name => 'Brakeman',
17
+ :informationUri => 'https://brakemanscanner.org',
18
+ :semanticVersion => Brakeman::Version,
19
+ :rules => rules,
20
+ },
21
+ },
22
+ :results => results,
23
+ },
24
+ ]
25
+ end
26
+
27
+ def rules
28
+ @rules ||= unique_warnings_by_warning_code.map do |warning|
29
+ rule_id = render_id warning
30
+ check_name = warning.check.gsub(/^Brakeman::Check/, '')
31
+ check_description = render_message check_descriptions[check_name]
32
+ {
33
+ :id => rule_id,
34
+ :name => "#{check_name}/#{warning.warning_type}",
35
+ :fullDescription => {
36
+ :text => check_description,
37
+ },
38
+ :helpUri => warning.link,
39
+ :help => {
40
+ :text => "More info: #{warning.link}.",
41
+ :markdown => "[More info](#{warning.link}).",
42
+ },
43
+ :properties => {
44
+ :tags => [check_name],
45
+ },
46
+ }
47
+ end
48
+ end
49
+
50
+ def results
51
+ @results ||= all_warnings.map do |warning|
52
+ rule_id = render_id warning
53
+ result_level = infer_level warning
54
+ message_text = render_message warning.message.to_s
55
+ result = {
56
+ :ruleId => rule_id,
57
+ :ruleIndex => rules.index { |r| r[:id] == rule_id },
58
+ :level => result_level,
59
+ :message => {
60
+ :text => message_text,
61
+ },
62
+ :locations => [
63
+ :physicalLocation => {
64
+ :artifactLocation => {
65
+ :uri => warning.file.relative,
66
+ :uriBaseId => '%SRCROOT%',
67
+ },
68
+ :region => {
69
+ :startLine => warning.line.is_a?(Integer) ? warning.line : 1,
70
+ },
71
+ },
72
+ ],
73
+ }
74
+
75
+ result
76
+ end
77
+ end
78
+
79
+ # Returns a hash of all check descriptions, keyed by check namne
80
+ def check_descriptions
81
+ @check_descriptions ||= Brakeman::Checks.checks.map do |check|
82
+ [check.name.gsub(/^Check/, ''), check.description]
83
+ end.to_h
84
+ end
85
+
86
+ # Returns a de-duplicated set of warnings, used to generate rules
87
+ def unique_warnings_by_warning_code
88
+ @unique_warnings_by_warning_code ||= all_warnings.uniq { |w| w.warning_code }
89
+ end
90
+
91
+ def render_id warning
92
+ # Include alpha prefix to provide 'compiler error' appearance
93
+ "BRAKE#{'%04d' % warning.warning_code}" # 46 becomes BRAKE0046, for example
94
+ end
95
+
96
+ def render_message message
97
+ # Ensure message ends with a period
98
+ if message.end_with? "."
99
+ message
100
+ else
101
+ "#{message}."
102
+ end
103
+ end
104
+
105
+ def infer_level warning
106
+ # Infer result level from warning confidence
107
+ @@levels_from_confidence ||= Hash.new('warning').update({
108
+ 0 => 'error', # 0 represents 'high confidence', which we infer as 'error'
109
+ 1 => 'warning', # 1 represents 'medium confidence' which we infer as 'warning'
110
+ 2 => 'note', # 2 represents 'weak, or low, confidence', which we infer as 'note'
111
+ })
112
+ @@levels_from_confidence[warning.confidence]
113
+ end
114
+ end
@@ -145,24 +145,45 @@ class Brakeman::Report::Text < Brakeman::Report::Base
145
145
  end
146
146
 
147
147
  def output_warning w
148
- out = [
149
- label('Confidence', confidence(w.confidence)),
150
- label('Category', w.warning_type.to_s),
151
- label('Check', w.check.gsub(/^Brakeman::Check/, '')),
148
+ text_format = tracker.options[:text_fields] ||
149
+ [:confidence, :category, :check, :message, :code, :file, :line]
150
+
151
+ text_format.map do |option|
152
+ format_line(w, option)
153
+ end.compact
154
+ end
155
+
156
+ def format_line w, option
157
+ case option
158
+ when :confidence
159
+ label('Confidence', confidence(w.confidence))
160
+ when :category
161
+ label('Category', w.warning_type.to_s)
162
+ when :check
163
+ label('Check', w.check.gsub(/^Brakeman::Check/, ''))
164
+ when :message
152
165
  label('Message', w.message)
153
- ]
154
-
155
- if w.code
156
- out << label('Code', format_code(w))
157
- end
158
-
159
- out << label('File', warning_file(w))
160
-
161
- if w.line
162
- out << label('Line', w.line)
166
+ when :code
167
+ if w.code
168
+ label('Code', format_code(w))
169
+ end
170
+ when :file
171
+ label('File', warning_file(w))
172
+ when :line
173
+ if w.line
174
+ label('Line', w.line)
175
+ end
176
+ when :link
177
+ label('Link', w.link)
178
+ when :fingerprint
179
+ label('Fingerprint', w.fingerprint)
180
+ when :category_id
181
+ label('Category ID', w.warning_code)
182
+ when :render_path
183
+ if w.called_from
184
+ label('Render Path', w.called_from.join(" > "))
185
+ end
163
186
  end
164
-
165
- out
166
187
  end
167
188
 
168
189
  def double_space title, values