brakeman 1.1.0 → 1.2.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.
@@ -65,6 +65,42 @@ class Brakeman::FindAllCalls < Brakeman::BaseProcessor
65
65
  exp
66
66
  end
67
67
 
68
+ #Calls to render() are converted to s(:render, ...) but we would
69
+ #like them in the call cache still for speed
70
+ def process_render exp
71
+ process exp[-1] if sexp? exp[-1]
72
+
73
+ call = { :target => nil, :method => :render, :call => exp, :nested => false }
74
+
75
+ if @current_template
76
+ call[:location] = [:template, @current_template]
77
+ else
78
+ call[:location] = [:class, @current_class, @current_method]
79
+ end
80
+
81
+ @calls << call
82
+
83
+ exp
84
+ end
85
+
86
+ #Technically, `` is call to Kernel#`
87
+ #But we just need them in the call cache for speed
88
+ def process_dxstr exp
89
+ process exp[-1] if sexp? exp[-1]
90
+
91
+ call = { :target => nil, :method => :`, :call => exp, :nested => false }
92
+
93
+ if @current_template
94
+ call[:location] = [:template, @current_template]
95
+ else
96
+ call[:location] = [:class, @current_class, @current_method]
97
+ end
98
+
99
+ @calls << call
100
+
101
+ exp
102
+ end
103
+
68
104
  #Process an assignment like a call
69
105
  def process_attrasgn exp
70
106
  process_call exp
@@ -153,6 +153,7 @@ class Brakeman::Rails3RoutesProcessor < Brakeman::BaseProcessor
153
153
 
154
154
  self.current_controller = controller
155
155
  @tracker.routes[@current_controller] << action.to_sym
156
+ break
156
157
  end
157
158
  end
158
159
  end
@@ -73,7 +73,7 @@ class Brakeman::ModelProcessor < Brakeman::BaseProcessor
73
73
  else
74
74
  if @model
75
75
  @model[:options][method] ||= []
76
- @model[:options][method] << process(args)
76
+ @model[:options][method] << args
77
77
  end
78
78
  end
79
79
  end
@@ -36,7 +36,7 @@ class Brakeman::Report
36
36
  end
37
37
 
38
38
  #Generate summary table of what was parsed
39
- def generate_overview
39
+ def generate_overview html = false
40
40
  templates = Set.new(@tracker.templates.map {|k,v| v[:name].to_s[/[^.]+/]}).length
41
41
  warnings = checks.warnings.length +
42
42
  checks.controller_warnings.length +
@@ -49,7 +49,7 @@ class Brakeman::Report
49
49
  unless tracker.options[:output_format] == :to_csv
50
50
  summary = warnings_summary
51
51
 
52
- if tracker.options[:output_format] == :to_html
52
+ if html
53
53
  warnings = "#{warnings} <span class='high-confidence'>(#{summary[:high_confidence]})</span>"
54
54
  else
55
55
  warnings = "#{warnings} (#{summary[:high_confidence]})"
@@ -77,7 +77,7 @@ class Brakeman::Report
77
77
  end
78
78
 
79
79
  #Generate table of errors or return nil if no errors
80
- def generate_errors
80
+ def generate_errors html = false
81
81
  unless tracker.errors.empty?
82
82
  table = Ruport::Data::Table(["Error", "Location"])
83
83
 
@@ -85,7 +85,7 @@ class Brakeman::Report
85
85
  tracker.errors.each do |w|
86
86
  p w if tracker.options[:debug]
87
87
 
88
- if tracker.options[:output_format] == :to_html
88
+ if html
89
89
  w[:error] = CGI.escapeHTML w[:error]
90
90
  end
91
91
 
@@ -99,13 +99,13 @@ class Brakeman::Report
99
99
  end
100
100
 
101
101
  #Generate table of general security warnings
102
- def generate_warnings
102
+ def generate_warnings html = false
103
103
  table = Ruport::Data::Table(["Confidence", "Class", "Method", "Warning Type", "Message"])
104
104
  checks.warnings.each do |warning|
105
105
  next if warning.confidence > tracker.options[:min_confidence]
106
106
  w = warning.to_row
107
107
 
108
- if tracker.options[:output_format] == :to_html
108
+ if html
109
109
  w["Confidence"] = HTML_CONFIDENCE[w["Confidence"]]
110
110
  w["Message"] = with_context warning, w["Message"]
111
111
  else
@@ -128,14 +128,14 @@ class Brakeman::Report
128
128
  end
129
129
 
130
130
  #Generate table of template warnings or return nil if no warnings
131
- def generate_template_warnings
131
+ def generate_template_warnings html = false
132
132
  unless checks.template_warnings.empty?
133
133
  table = Ruport::Data::Table(["Confidence", "Template", "Warning Type", "Message"])
134
134
  checks.template_warnings.each do |warning|
135
135
  next if warning.confidence > tracker.options[:min_confidence]
136
136
  w = warning.to_row :template
137
137
 
138
- if tracker.options[:output_format] == :to_html
138
+ if html
139
139
  w["Confidence"] = HTML_CONFIDENCE[w["Confidence"]]
140
140
  w["Message"] = with_context warning, w["Message"]
141
141
  else
@@ -159,14 +159,14 @@ class Brakeman::Report
159
159
  end
160
160
 
161
161
  #Generate table of model warnings or return nil if no warnings
162
- def generate_model_warnings
162
+ def generate_model_warnings html = false
163
163
  unless checks.model_warnings.empty?
164
164
  table = Ruport::Data::Table(["Confidence", "Model", "Warning Type", "Message"])
165
165
  checks.model_warnings.each do |warning|
166
166
  next if warning.confidence > tracker.options[:min_confidence]
167
167
  w = warning.to_row :model
168
168
 
169
- if tracker.options[:output_format] == :to_html
169
+ if html
170
170
  w["Confidence"] = HTML_CONFIDENCE[w["Confidence"]]
171
171
  w["Message"] = with_context warning, w["Message"]
172
172
  else
@@ -190,14 +190,14 @@ class Brakeman::Report
190
190
  end
191
191
 
192
192
  #Generate table of controller warnings or nil if no warnings
193
- def generate_controller_warnings
193
+ def generate_controller_warnings html = false
194
194
  unless checks.controller_warnings.empty?
195
195
  table = Ruport::Data::Table(["Confidence", "Controller", "Warning Type", "Message"])
196
196
  checks.controller_warnings.each do |warning|
197
197
  next if warning.confidence > tracker.options[:min_confidence]
198
198
  w = warning.to_row :controller
199
199
 
200
- if tracker.options[:output_format] == :to_html
200
+ if html
201
201
  w["Confidence"] = HTML_CONFIDENCE[w["Confidence"]]
202
202
  w["Message"] = with_context warning, w["Message"]
203
203
  else
@@ -257,14 +257,14 @@ class Brakeman::Report
257
257
  end
258
258
 
259
259
  #Generate listings of templates and their output
260
- def generate_templates
260
+ def generate_templates html = false
261
261
  out_processor = Brakeman::OutputProcessor.new
262
262
  table = Ruport::Data::Table(["Name", "Output"])
263
263
  tracker.templates.each do |name, template|
264
264
  unless template[:outputs].empty?
265
265
  template[:outputs].each do |out|
266
266
  out = out_processor.format out
267
- out = CGI.escapeHTML(out) if tracker.options[:output_format] == :to_html
267
+ out = CGI.escapeHTML(out) if html
268
268
  table << { "Name" => name,
269
269
  "Output" => out.gsub("\n", ";").gsub(/\s+/, " ") }
270
270
  end
@@ -277,9 +277,14 @@ class Brakeman::Report
277
277
  def to_html
278
278
  out = html_header <<
279
279
  "<h2 id='summary'>Summary</h2>" <<
280
- generate_overview.to_html << "<br/>" <<
280
+ generate_overview(true).to_html << "<br/>" <<
281
281
  generate_warning_overview.to_html
282
282
 
283
+ #Return early if only summarizing
284
+ if tracker.options[:summary_only]
285
+ return out
286
+ end
287
+
283
288
  if tracker.options[:report_routes] or tracker.options[:debug]
284
289
  out << "<h2>Controllers</h2>" <<
285
290
  generate_controllers.to_html
@@ -287,24 +292,24 @@ class Brakeman::Report
287
292
 
288
293
  if tracker.options[:debug]
289
294
  out << "<h2>Templates</h2>" <<
290
- generate_templates.to_html
295
+ generate_templates(true).to_html
291
296
  end
292
297
 
293
- res = generate_errors
298
+ res = generate_errors(true)
294
299
  if res
295
300
  out << "<div onClick=\"toggle('errors_table');\"> <h2>Exceptions raised during the analysis (click to see them)</h2 ></div> <div id='errors_table' style='display:none'>" << res.to_html << '</div>'
296
301
  end
297
302
 
298
- res = generate_warnings
303
+ res = generate_warnings(true)
299
304
  out << "<h2>Security Warnings</h2>" << res.to_html if res
300
305
 
301
- res = generate_controller_warnings
306
+ res = generate_controller_warnings(true)
302
307
  out << res.to_html if res
303
308
 
304
- res = generate_model_warnings
309
+ res = generate_model_warnings(true)
305
310
  out << res.to_html if res
306
311
 
307
- res = generate_template_warnings
312
+ res = generate_template_warnings(true)
308
313
  out << res.to_html if res
309
314
 
310
315
  out << "</body></html>"
@@ -317,6 +322,11 @@ class Brakeman::Report
317
322
  generate_overview.to_s << "\n" <<
318
323
  generate_warning_overview.to_s << "\n"
319
324
 
325
+ #Return output early if only summarizing
326
+ if tracker.options[:summary_only]
327
+ return out
328
+ end
329
+
320
330
  if tracker.options[:report_routes] or tracker.options[:debug]
321
331
  out << "+CONTROLLERS+\n" <<
322
332
  generate_controllers.to_s << "\n"
@@ -352,6 +362,11 @@ class Brakeman::Report
352
362
  generate_overview.to_csv << "\n" <<
353
363
  generate_warning_overview.to_csv << "\n"
354
364
 
365
+ #Return output early if only summarizing
366
+ if tracker.options[:summary_only]
367
+ return out
368
+ end
369
+
355
370
  if tracker.options[:report_routes] or tracker.options[:debug]
356
371
  out << "CONTROLLERS\n" <<
357
372
  generate_controllers.to_csv << "\n"
@@ -483,107 +498,6 @@ class Brakeman::Report
483
498
  @warnings_summary = summary
484
499
  end
485
500
 
486
- #Return file name related to given warning. Uses +warning.file+ if it exists
487
- def file_for warning
488
- if warning.file
489
- File.expand_path warning.file, tracker.options[:app_path]
490
- else
491
- case warning.warning_set
492
- when :controller
493
- file_by_name warning.controller, :controller
494
- when :template
495
- file_by_name warning.template[:name], :template
496
- when :model
497
- file_by_name warning.model, :model
498
- when :warning
499
- file_by_name warning.class
500
- else
501
- nil
502
- end
503
- end
504
- end
505
-
506
- #Attempt to determine path to context file based on the reported name
507
- #in the warning.
508
- #
509
- #For example,
510
- #
511
- # file_by_name FileController #=> "/rails/root/app/controllers/file_controller.rb
512
- def file_by_name name, type = nil
513
- return nil unless name
514
- string_name = name.to_s
515
- name = name.to_sym
516
-
517
- unless type
518
- if string_name =~ /Controller$/
519
- type = :controller
520
- elsif camelize(string_name) == string_name
521
- type = :model
522
- else
523
- type = :template
524
- end
525
- end
526
-
527
- path = tracker.options[:app_path]
528
-
529
- case type
530
- when :controller
531
- if tracker.controllers[name] and tracker.controllers[name][:file]
532
- path = tracker.controllers[name][:file]
533
- else
534
- path += "/app/controllers/#{underscore(string_name)}.rb"
535
- end
536
- when :model
537
- if tracker.models[name] and tracker.models[name][:file]
538
- path = tracker.models[name][:file]
539
- else
540
- path += "/app/controllers/#{underscore(string_name)}.rb"
541
- end
542
- when :template
543
- if tracker.templates[name] and tracker.templates[name][:file]
544
- path = tracker.templates[name][:file]
545
- elsif string_name.include? " "
546
- name = string_name.split[0].to_sym
547
- path = file_for name, :template
548
- else
549
- path = nil
550
- end
551
- end
552
-
553
- path
554
- end
555
-
556
- #Return array of lines surrounding the warning location from the original
557
- #file.
558
- def context_for warning
559
- file = file_for warning
560
- context = []
561
- return context unless warning.line and file and File.exist? file
562
-
563
- current_line = 0
564
- start_line = warning.line - 5
565
- end_line = warning.line + 5
566
-
567
- start_line = 1 if start_line < 0
568
-
569
- File.open file do |f|
570
- f.each_line do |line|
571
- current_line += 1
572
-
573
- next if line.strip == ""
574
-
575
- if current_line > end_line
576
- break
577
- end
578
-
579
- if current_line >= start_line
580
- context << [current_line, line]
581
- end
582
- end
583
- end
584
-
585
- context
586
- end
587
501
 
588
502
  #Generate HTML for warnings, including context show/hidden via Javascript
589
503
  def with_context warning, message
@@ -0,0 +1,247 @@
1
+ require 'brakeman/scanner'
2
+
3
+ #Class for rescanning changed files after an initial scan
4
+ class Brakeman::Rescanner < Brakeman::Scanner
5
+
6
+ SCAN_ORDER = [:config, :gemfile, :initializer, :lib, :routes, :template,
7
+ :model, :controller]
8
+
9
+ #Create new Rescanner to scan changed files
10
+ def initialize options, processor, changed_files
11
+ super(options, processor)
12
+
13
+ @paths = changed_files.map {|f| File.expand_path f, tracker.options[:app_path] }
14
+ @old_results = tracker.checks #Old warnings from previous scan
15
+ @changes = nil #True if files had to be rescanned
16
+ @reindex = Set.new
17
+ end
18
+
19
+ #Runs checks.
20
+ #Will rescan files if they have not already been scanned
21
+ def recheck
22
+ rescan if @changes.nil?
23
+
24
+ tracker.run_checks if @changes
25
+
26
+ Brakeman::RescanReport.new @old_results, tracker.checks
27
+ end
28
+
29
+ #Rescans changed files
30
+ def rescan
31
+ tracker.template_cache.clear
32
+
33
+ paths_by_type = {}
34
+
35
+ SCAN_ORDER.each do |type|
36
+ paths_by_type[type] = []
37
+ end
38
+
39
+ @paths.each do |path|
40
+ type = file_type(path)
41
+ paths_by_type[type] << path unless type == :unknown
42
+ end
43
+
44
+ @changes = false
45
+
46
+ SCAN_ORDER.each do |type|
47
+ paths_by_type[type].each do |path|
48
+ warn "Rescanning #{path} as #{type}" if tracker.options[:debug]
49
+
50
+ if rescan_file path, type
51
+ @changes = true
52
+ end
53
+ end
54
+ end
55
+
56
+ if @changes and not @reindex.empty?
57
+ tracker.reindex_call_sites @reindex
58
+ end
59
+
60
+ self
61
+ end
62
+
63
+ #Rescans a single file
64
+ def rescan_file path, type = nil
65
+ type ||= file_type path
66
+
67
+ case type
68
+ when :controller
69
+ rescan_controller path
70
+ @reindex << :controllers << :templates
71
+ when :template
72
+ rescan_template path
73
+ @reindex << :templates
74
+ when :model
75
+ rescan_model path
76
+ when :lib
77
+ process_library path
78
+ when :config
79
+ process_config
80
+ when :initializer
81
+ process_initializer path
82
+ when :routes
83
+ # Routes affect which controller methods are treated as actions
84
+ # which affects which templates are rendered, so routes, controllers,
85
+ # and templates rendered from controllers must be rescanned
86
+ tracker.reset_routes
87
+ tracker.reset_templates :only_rendered => true
88
+ process_routes
89
+ process_controllers
90
+ @reindex << :controllers << :templates
91
+ when :gemfile
92
+ process_gems
93
+ else
94
+ return false #Nothing to do, file hopefully does not need to be rescanned
95
+ end
96
+
97
+ true
98
+ end
99
+
100
+ def rescan_controller path
101
+ #Process source
102
+ process_controller path
103
+
104
+ #Process data flow and template rendering
105
+ #from the controller
106
+ tracker.controllers.each do |name, controller|
107
+ if controller[:file] == path
108
+ tracker.templates.keys.each do |template_name|
109
+ if template_name.to_s.match /(.+)\.#{name}#/
110
+ tracker.templates.delete template_name
111
+ end
112
+ end
113
+
114
+ @processor.process_controller_alias controller[:src]
115
+ end
116
+ end
117
+ end
118
+
119
+ def rescan_template path
120
+ template_name = template_path_to_name(path)
121
+
122
+ tracker.reset_template template_name
123
+ process_template path
124
+
125
+ @processor.process_template_alias tracker.templates[template_name]
126
+
127
+ rescan = Set.new
128
+
129
+ rendered_from_controller = /^#{template_name}\.(.+Controller)#(.+)/
130
+ rendered_from_view = /^#{template_name}\.Template:(.+)/
131
+
132
+ #Search for processed template and process it.
133
+ #Search for rendered versions of template and re-render (if necessary)
134
+ tracker.templates.each do |name, template|
135
+ if template[:file] == path or template[:file].nil?
136
+ name = name.to_s
137
+
138
+ if name.match(rendered_from_controller)
139
+ #Rendered from controller, so reprocess controller
140
+
141
+ rescan << [:controller, $1.to_sym, $2.to_sym]
142
+ elsif name.match(rendered_from_view)
143
+ #Rendered from another template, so reprocess that template
144
+
145
+ rescan << [:template, $1.to_sym]
146
+ end
147
+ end
148
+ end
149
+
150
+ rescan.each do |r|
151
+ if r[0] == :controller
152
+ controller = tracker.controllers[r[1]]
153
+
154
+ unless @paths.include? controller[:file]
155
+ @processor.process_controller_alias controller[:src], r[2]
156
+ end
157
+ elsif r[0] == :template
158
+ template = tracker.templates[r[1]]
159
+
160
+ rescan_template template[:file]
161
+ end
162
+ end
163
+ end
164
+
165
+ def rescan_model path
166
+ num_models = tracker.models.length
167
+ tracker.reset_model path
168
+ process_model path if File.exists? path
169
+
170
+ #Only need to rescan other things if a model is added or removed
171
+ if num_models != tracker.models.length
172
+ process_templates
173
+ process_controllers
174
+ @reindex << :templates << :controllers
175
+ end
176
+
177
+ @reindex << :models
178
+ end
179
+
180
+ #Guess at what kind of file the path contains
181
+ def file_type path
182
+ case path
183
+ when /\/app\/controllers/
184
+ :controller
185
+ when /\/app\/views/
186
+ :template
187
+ when /\/app\/models/
188
+ :model
189
+ when /\/lib/
190
+ :lib
191
+ when /\/config\/initializers/
192
+ :initializer
193
+ when /config\/routes\.rb/
194
+ :routes
195
+ when /\/config/
196
+ :config
197
+ when /Gemfile/
198
+ :gemfile
199
+ else
200
+ :unknown
201
+ end
202
+ end
203
+ end
204
+
205
+ #Class to make reporting of rescan results simpler to deal with
206
+ class Brakeman::RescanReport
207
+ attr_reader :old_results, :new_results
208
+
209
+ def initialize old_results, new_results
210
+ @old_results = old_results
211
+ @new_results = new_results
212
+ @all_warnings = nil
213
+ @diff = nil
214
+ end
215
+
216
+ #Returns true if any warnings were found (new or old)
217
+ def any_warnings?
218
+ not all_warnings.empty?
219
+ end
220
+
221
+ #Returns an array of all warnings found
222
+ def all_warnings
223
+ @all_warnings ||= new_results.all_warnings
224
+ end
225
+
226
+ #Returns an array of warnings which were in the old report but are not in the
227
+ #new report after rescanning
228
+ def fixed_warnings
229
+ diff[:fixed]
230
+ end
231
+
232
+ #Returns an array of warnings which were in the new report but were not in
233
+ #the old report
234
+ def new_warnings
235
+ diff[:new]
236
+ end
237
+
238
+ #Returns true if there are any new or fixed warnings
239
+ def warnings_changed?
240
+ not (diff[:new].empty? and diff[:fixed].empty?)
241
+ end
242
+
243
+ #Returns a hash of arrays for :new and :fixed warnings
244
+ def diff
245
+ @diff ||= @new_results.diff(@old_results)
246
+ end
247
+ end