brakeman 1.5.3 → 1.6.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/bin/brakeman +20 -6
  2. data/lib/brakeman.rb +21 -1
  3. data/lib/brakeman/checks.rb +2 -7
  4. data/lib/brakeman/checks/base_check.rb +31 -26
  5. data/lib/brakeman/checks/check_basic_auth.rb +1 -7
  6. data/lib/brakeman/checks/check_cross_site_scripting.rb +38 -33
  7. data/lib/brakeman/checks/check_evaluation.rb +2 -1
  8. data/lib/brakeman/checks/check_execute.rb +5 -1
  9. data/lib/brakeman/checks/check_file_access.rb +6 -4
  10. data/lib/brakeman/checks/check_link_to.rb +8 -5
  11. data/lib/brakeman/checks/check_link_to_href.rb +6 -5
  12. data/lib/brakeman/checks/check_mail_to.rb +2 -4
  13. data/lib/brakeman/checks/check_mass_assignment.rb +12 -6
  14. data/lib/brakeman/checks/check_redirect.rb +17 -14
  15. data/lib/brakeman/checks/check_render.rb +4 -4
  16. data/lib/brakeman/checks/check_send.rb +4 -2
  17. data/lib/brakeman/checks/check_session_settings.rb +16 -21
  18. data/lib/brakeman/checks/check_skip_before_filter.rb +2 -4
  19. data/lib/brakeman/checks/check_sql.rb +8 -7
  20. data/lib/brakeman/checks/check_validation_regex.rb +2 -4
  21. data/lib/brakeman/checks/check_without_protection.rb +8 -9
  22. data/lib/brakeman/differ.rb +61 -0
  23. data/lib/brakeman/format/style.css +4 -0
  24. data/lib/brakeman/options.rb +8 -0
  25. data/lib/brakeman/processors/alias_processor.rb +5 -7
  26. data/lib/brakeman/processors/controller_alias_processor.rb +10 -3
  27. data/lib/brakeman/processors/erb_template_processor.rb +2 -0
  28. data/lib/brakeman/processors/erubis_template_processor.rb +2 -0
  29. data/lib/brakeman/processors/gem_processor.rb +8 -0
  30. data/lib/brakeman/processors/haml_template_processor.rb +2 -0
  31. data/lib/brakeman/processors/lib/rails2_route_processor.rb +2 -6
  32. data/lib/brakeman/processors/lib/rails3_route_processor.rb +2 -4
  33. data/lib/brakeman/processors/lib/render_helper.rb +3 -1
  34. data/lib/brakeman/processors/library_processor.rb +4 -6
  35. data/lib/brakeman/processors/template_alias_processor.rb +1 -1
  36. data/lib/brakeman/report.rb +257 -198
  37. data/lib/brakeman/rescanner.rb +112 -10
  38. data/lib/brakeman/scanner.rb +3 -4
  39. data/lib/brakeman/templates/controller_overview.html.erb +18 -0
  40. data/lib/brakeman/templates/controller_warnings.html.erb +17 -0
  41. data/lib/brakeman/templates/error_overview.html.erb +14 -0
  42. data/lib/brakeman/templates/header.html.erb +38 -0
  43. data/lib/brakeman/templates/model_warnings.html.erb +17 -0
  44. data/lib/brakeman/templates/overview.html.erb +28 -0
  45. data/lib/brakeman/templates/security_warnings.html.erb +28 -0
  46. data/lib/brakeman/templates/template_overview.html.erb +17 -0
  47. data/lib/brakeman/templates/view_warnings.html.erb +17 -0
  48. data/lib/brakeman/templates/warning_overview.html.erb +13 -0
  49. data/lib/brakeman/tracker.rb +1 -1
  50. data/lib/brakeman/util.rb +24 -4
  51. data/lib/brakeman/version.rb +1 -1
  52. data/lib/brakeman/warning.rb +11 -3
  53. data/lib/ruby_parser/bm_sexp.rb +5 -11
  54. metadata +84 -23
@@ -108,3 +108,7 @@ p {
108
108
  pre.context {
109
109
  margin-bottom: 1px;
110
110
  }
111
+
112
+ .user_input {
113
+ background-color: #fcecab;
114
+ }
@@ -144,6 +144,10 @@ module Brakeman::Options
144
144
  options[:combine_locations] = combine
145
145
  end
146
146
 
147
+ opts.on "--[no-]highlights", "Highlight user input in report" do |highlight|
148
+ options[:highlight_user_input] = highlight
149
+ end
150
+
147
151
  opts.on "-m", "--routes", "Report controller information" do
148
152
  options[:report_routes] = true
149
153
  end
@@ -173,6 +177,10 @@ module Brakeman::Options
173
177
  options[:min_confidence] = 3 - level.to_i
174
178
  end
175
179
 
180
+ opts.on "--compare FILE", "Compare the results of a previous brakeman scan (only JSON is supported)" do |file|
181
+ options[:previous_results_json] = File.expand_path(file)
182
+ end
183
+
176
184
  opts.separator ""
177
185
  opts.separator "Configuration files:"
178
186
 
@@ -28,6 +28,7 @@ class Brakeman::AliasProcessor < SexpProcessor
28
28
  @inside_if = false
29
29
  @ignore_ifs = false
30
30
  @exp_context = []
31
+ @current_module = nil
31
32
  @tracker = tracker #set in subclass as necessary
32
33
  set_env_defaults
33
34
  end
@@ -462,14 +463,11 @@ class Brakeman::AliasProcessor < SexpProcessor
462
463
  def process_hash_access target, args
463
464
  if args.length == 1
464
465
  index = args[0]
465
- hash_iterate(target) do |key, value|
466
- if key == index
467
- return value
468
- end
469
- end
470
- end
471
466
 
472
- nil
467
+ hash_access(target, index)
468
+ else
469
+ nil
470
+ end
473
471
  end
474
472
 
475
473
  #Join two array literals into one.
@@ -15,6 +15,7 @@ class Brakeman::ControllerAliasProcessor < Brakeman::AliasProcessor
15
15
  @tracker = tracker
16
16
  @rendered = false
17
17
  @current_class = @current_module = @current_method = nil
18
+ @method_cache = {} #Cache method lookups
18
19
  end
19
20
 
20
21
  def process_controller name, src
@@ -248,6 +249,11 @@ class Brakeman::ControllerAliasProcessor < Brakeman::AliasProcessor
248
249
  def find_method method_name, klass
249
250
  return nil if sexp? method_name
250
251
  method_name = method_name.to_sym
252
+
253
+ if method = @method_cache[method_name]
254
+ return method
255
+ end
256
+
251
257
  controller = @tracker.controllers[klass]
252
258
  controller ||= @tracker.libs[klass]
253
259
 
@@ -260,13 +266,14 @@ class Brakeman::ControllerAliasProcessor < Brakeman::AliasProcessor
260
266
  controller[:includes].each do |included|
261
267
  method = find_method method_name, included
262
268
  if method
263
- return { :controller => controller[:name], :method => method }
269
+ @method_cache[method_name] = method
270
+ return method
264
271
  end
265
272
  end
266
273
 
267
- find_method method_name, controller[:parent]
274
+ @method_cache[method_name] = find_method method_name, controller[:parent]
268
275
  else
269
- { :controller => controller[:name], :method => method }
276
+ @method_cache[method_name] = { :controller => controller[:name], :method => method }
270
277
  end
271
278
  else
272
279
  nil
@@ -46,8 +46,10 @@ class Brakeman::ErbTemplateProcessor < Brakeman::TemplateProcessor
46
46
  exp[3] = process(exp[3])
47
47
  make_render_in_view exp
48
48
  else
49
+ #TODO: Is it really necessary to create a new Sexp here?
49
50
  args = exp[3] = process(exp[3])
50
51
  call = Sexp.new :call, target, method, args
52
+ call.original_line(exp.original_line)
51
53
  call.line(exp.line)
52
54
  call
53
55
  end
@@ -43,8 +43,10 @@ class Brakeman::ErubisTemplateProcessor < Brakeman::TemplateProcessor
43
43
  exp[3] = process exp[3]
44
44
  make_render_in_view exp
45
45
  else
46
+ #TODO: Is it really necessary to create a new Sexp here?
46
47
  args = exp[3] = process(exp[3])
47
48
  call = Sexp.new :call, target, method, args
49
+ call.original_line(exp.original_line)
48
50
  call.line(exp.line)
49
51
  call
50
52
  end
@@ -17,6 +17,12 @@ class Brakeman::GemProcessor < Brakeman::BaseProcessor
17
17
  elsif @tracker.config[:gems][:rails] =~ /(\d+.\d+.\d+)/
18
18
  @tracker.config[:rails_version] = $1
19
19
  end
20
+
21
+ if @tracker.config[:gems][:rails_xss]
22
+ @tracker.config[:escape_html] = true
23
+
24
+ Brakeman.notify "[Notice] Escaping HTML by default"
25
+ end
20
26
  end
21
27
 
22
28
  def process_call exp
@@ -25,6 +31,8 @@ class Brakeman::GemProcessor < Brakeman::BaseProcessor
25
31
 
26
32
  if sexp? args[1]
27
33
  @tracker.config[:gems][args[0][1].to_sym] = args[1][1]
34
+ else
35
+ @tracker.config[:gems][args[0][1].to_sym] = ">=0.0.0"
28
36
  end
29
37
  end
30
38
 
@@ -94,8 +94,10 @@ class Brakeman::HamlTemplateProcessor < Brakeman::TemplateProcessor
94
94
  exp[3] = process exp[3]
95
95
  make_render_in_view exp
96
96
  else
97
+ #TODO: Do we really need a new Sexp here?
97
98
  args = process exp[3]
98
99
  call = Sexp.new :call, target, method, args
100
+ call.original_line(exp.original_line)
99
101
  call.line(exp.line)
100
102
  call
101
103
  end
@@ -258,12 +258,8 @@ class Brakeman::Rails2RoutesProcessor < Brakeman::BaseProcessor
258
258
  #Otherwise, returns nil.
259
259
  def check_for_controller_name args
260
260
  args.each do |a|
261
- if hash? a
262
- hash_iterate(a) do |k, v|
263
- if k[1] == :controller
264
- return v[1]
265
- end
266
- end
261
+ if hash? a and value = hash_access(a, :controller)
262
+ return value[1]
267
263
  end
268
264
  end
269
265
 
@@ -70,14 +70,12 @@ class Brakeman::Rails3RoutesProcessor < Brakeman::BaseProcessor
70
70
  def process_root exp
71
71
  args = exp[3][1..-1]
72
72
 
73
- hash_iterate args[0] do |k, v|
74
- if symbol? k and k[1] == :to and string? v[1]
73
+ if value = hash_access(args[0], :to)
74
+ if string? value[1]
75
75
  controller, action = extract_action v[1]
76
76
 
77
77
  self.current_controller = controller
78
78
  @tracker.routes[@current_controller] << action.to_sym
79
-
80
- break
81
79
  end
82
80
  end
83
81
 
@@ -115,7 +115,9 @@ module Brakeman::RenderHelper
115
115
  #that values came from another file
116
116
  template_env.all.each do |var, value|
117
117
  unless value.original_line
118
- value.original_line = value.line
118
+ #TODO: This has been broken for a while now and no one noticed
119
+ #so maybe we can skip it
120
+ value.original_line(value.line)
119
121
  end
120
122
  end
121
123
 
@@ -92,12 +92,11 @@ class Brakeman::LibraryProcessor < Brakeman::BaseProcessor
92
92
 
93
93
  def process_defn exp
94
94
  exp[0] = :methdef
95
- exp[3] = @alias_processor.process exp[3]
96
95
 
97
96
  if @current_class
98
- @current_class[:public][exp[1]] = exp[3]
97
+ @current_class[:public][exp[1]] = exp
99
98
  elsif @current_module
100
- @current_module[:public][exp[1]] = exp[3]
99
+ @current_module[:public][exp[1]] = exp
101
100
  end
102
101
 
103
102
  exp
@@ -105,12 +104,11 @@ class Brakeman::LibraryProcessor < Brakeman::BaseProcessor
105
104
 
106
105
  def process_defs exp
107
106
  exp[0] = :selfdef
108
- exp[4] = @alias_processor.process exp[4]
109
107
 
110
108
  if @current_class
111
- @current_class[:public][exp[2]] = exp[4]
109
+ @current_class[:public][exp[2]] = exp
112
110
  elsif @current_module
113
- @current_module[:public][exp[3]] = exp[4]
111
+ @current_module[:public][exp[3]] = exp
114
112
  end
115
113
 
116
114
  exp
@@ -17,7 +17,7 @@ class Brakeman::TemplateAliasProcessor < Brakeman::AliasProcessor
17
17
 
18
18
  #Process template
19
19
  def process_template name, args
20
- if @called_from and @called_from.match(/Template:#{Regexp.escape name}$/)
20
+ if @called_from and @called_from.match(/Template:#{name}$/)
21
21
  Brakeman.debug "Skipping circular render from #{@template[:name]} to #{name}"
22
22
  return
23
23
  end
@@ -1,18 +1,19 @@
1
1
  require 'cgi'
2
2
  require 'set'
3
- require 'ruport'
4
3
  require 'brakeman/processors/output_processor'
5
4
  require 'brakeman/util'
6
-
7
- #Fix for Ruport under 1.9
8
- #as reported here: https://github.com/ruport/ruport/pull/7
9
- module Ruport
10
- class Formatter::CSV < Formatter
11
- def csv_writer
12
- @csv_writer ||= options.formatter ||
13
- FCSV.instance(output, options.format_options || {})
14
- end
15
- end
5
+ require 'terminal-table'
6
+ require 'highline/system_extensions'
7
+ require "csv"
8
+ require 'brakeman/version'
9
+
10
+ if CSV.const_defined? :Reader
11
+ # Ruby 1.8 compatible
12
+ require 'fastercsv'
13
+ Object.send(:remove_const, :CSV)
14
+ CSV = FasterCSV
15
+ else
16
+ # CSV is now FasterCSV in ruby 1.9
16
17
  end
17
18
 
18
19
  #Generates a report based on the Tracker and the results of
@@ -33,66 +34,54 @@ class Brakeman::Report
33
34
  @checks = tracker.checks
34
35
  @element_id = 0 #Used for HTML ids
35
36
  @warnings_summary = nil
37
+ @highlight_user_input = tracker.options[:highlight_user_input]
36
38
  end
37
39
 
38
40
  #Generate summary table of what was parsed
39
41
  def generate_overview html = false
40
- templates = Set.new(@tracker.templates.map {|k,v| v[:name].to_s[/[^.]+/]}).length
41
- warnings = checks.warnings.length +
42
- checks.controller_warnings.length +
43
- checks.model_warnings.length +
44
- checks.template_warnings.length
45
-
46
- #Add number of high confidence warnings in summary.
47
- #Skipping for CSV because it makes the cell text instead of
48
- #a number.
49
- unless tracker.options[:output_format] == :to_csv
50
- summary = warnings_summary
42
+ warnings = all_warnings.length
51
43
 
52
- if html
53
- warnings = "#{warnings} <span class='high-confidence'>(#{summary[:high_confidence]})</span>"
54
- else
55
- warnings = "#{warnings} (#{summary[:high_confidence]})"
44
+ if html
45
+ load_and_render_erb('overview', binding)
46
+ else
47
+ Terminal::Table.new(:headings => ['Scanned/Reported', 'Total']) do |t|
48
+ t.add_row ['Controllers', tracker.controllers.length]
49
+ t.add_row ['Models', tracker.models.length - 1]
50
+ t.add_row ['Templates', number_of_templates(@tracker)]
51
+ t.add_row ['Errors', tracker.errors.length]
52
+ t.add_row ['Security Warnings', "#{warnings} (#{warnings_summary[:high_confidence]})"]
56
53
  end
57
54
  end
58
-
59
- table = Ruport::Data::Table(["Scanned/Reported", "Total"])
60
- table << { "Scanned/Reported" => "Controllers", "Total" => tracker.controllers.length }
61
- #One less because of the 'fake' one used for unknown models
62
- table << { "Scanned/Reported" => "Models", "Total" => tracker.models.length - 1 }
63
- table << { "Scanned/Reported" => "Templates", "Total" => templates }
64
- table << { "Scanned/Reported" => "Errors", "Total" => tracker.errors.length }
65
- table << { "Scanned/Reported" => "Security Warnings", "Total" => warnings}
66
55
  end
67
56
 
68
57
  #Generate table of how many warnings of each warning type were reported
69
- def generate_warning_overview
70
- table = Ruport::Data::Table(["Warning Type", "Total"])
58
+ def generate_warning_overview html = false
71
59
  types = warnings_summary.keys
72
60
  types.delete :high_confidence
73
- types.sort.each do |warning_type|
74
- table << { "Warning Type" => warning_type, "Total" => warnings_summary[warning_type] }
61
+
62
+ if html
63
+ load_and_render_erb('warning_overview', binding)
64
+ else
65
+ Terminal::Table.new(:headings => ['Warning Type', 'Total']) do |t|
66
+ types.sort.each do |warning_type|
67
+ t.add_row [warning_type, warnings_summary[warning_type]]
68
+ end
69
+ end
75
70
  end
76
- table
77
71
  end
78
72
 
79
73
  #Generate table of errors or return nil if no errors
80
74
  def generate_errors html = false
81
- unless tracker.errors.empty?
82
- table = Ruport::Data::Table(["Error", "Location"])
83
-
84
-
85
- tracker.errors.each do |w|
86
- Brakeman.debug w.inspect
87
-
88
- if html
89
- w[:error] = CGI.escapeHTML w[:error]
75
+ if tracker.errors.any?
76
+ if html
77
+ load_and_render_erb('error_overview', binding)
78
+ else
79
+ Terminal::Table.new(:headings => ['Error', 'Location']) do |t|
80
+ tracker.errors.each do |error|
81
+ t.add_row [error[:error], error[:backtrace][0]]
82
+ end
90
83
  end
91
-
92
- table << { "Error" => w[:error], "Location" => w[:backtrace][0] }
93
84
  end
94
-
95
- table
96
85
  else
97
86
  nil
98
87
  end
@@ -100,7 +89,7 @@ class Brakeman::Report
100
89
 
101
90
  #Generate table of general security warnings
102
91
  def generate_warnings html = false
103
- table = Ruport::Data::Table(["Confidence", "Class", "Method", "Warning Type", "Message"])
92
+ warning_messages = []
104
93
  checks.warnings.each do |warning|
105
94
  w = warning.to_row
106
95
 
@@ -109,27 +98,36 @@ class Brakeman::Report
109
98
  w["Message"] = with_context warning, w["Message"]
110
99
  else
111
100
  w["Confidence"] = TEXT_CONFIDENCE[w["Confidence"]]
101
+ w["Message"] = text_message warning, w["Message"]
112
102
  end
113
103
 
114
- table << w
104
+ warning_messages << w
115
105
  end
116
106
 
117
- table.sort_rows_by! "Class"
118
- table.sort_rows_by! "Warning Type"
119
- table.sort_rows_by! "Confidence"
107
+ stabilizer = 0
108
+ warning_messages = warning_messages.sort_by{|row| stabilizer += 1; [row['Confidence'], row['Warning Type'], row['Class'], stabilizer]}
120
109
 
121
- if table.empty?
122
- table = Ruport::Data::Table("General Warnings")
123
- table << { "General Warnings" => "[NONE]" }
110
+ if html
111
+ load_and_render_erb('security_warnings', binding)
112
+ else
113
+ if warning_messages.empty?
114
+ Terminal::Table.new(:headings => ['General Warnings']) do |t|
115
+ t.add_row ['[NONE]']
116
+ end
117
+ else
118
+ Terminal::Table.new(:headings => ["Confidence", "Class", "Method", "Warning Type", "Message"]) do |t|
119
+ warning_messages.each do |row|
120
+ t.add_row [row["Confidence"], row["Class"], row["Method"], row["Warning Type"], row["Message"]]
121
+ end
122
+ end
123
+ end
124
124
  end
125
-
126
- table
127
125
  end
128
126
 
129
127
  #Generate table of template warnings or return nil if no warnings
130
128
  def generate_template_warnings html = false
131
- unless checks.template_warnings.empty?
132
- table = Ruport::Data::Table(["Confidence", "Template", "Warning Type", "Message"])
129
+ if checks.template_warnings.any?
130
+ warnings = []
133
131
  checks.template_warnings.each do |warning|
134
132
  w = warning.to_row :template
135
133
 
@@ -138,18 +136,24 @@ class Brakeman::Report
138
136
  w["Message"] = with_context warning, w["Message"]
139
137
  else
140
138
  w["Confidence"] = TEXT_CONFIDENCE[w["Confidence"]]
139
+ w["Message"] = text_message warning, w["Message"]
141
140
  end
142
141
 
143
- table << w
142
+ warnings << w
144
143
  end
145
144
 
146
- if table.empty?
147
- nil
145
+ return nil if warnings.empty?
146
+
147
+ stabilizer = 0
148
+ warnings = warnings.sort_by{|row| stabilizer += 1; [row["Confidence"], row["Warning Type"], row["Template"], stabilizer]}
149
+ if html
150
+ load_and_render_erb('view_warnings', binding)
148
151
  else
149
- table.sort_rows_by! "Template"
150
- table.sort_rows_by! "Warning Type"
151
- table.sort_rows_by! "Confidence"
152
- table.to_group "View Warnings"
152
+ Terminal::Table.new(:headings => ["Confidence", "Template", "Warning Type", "Message"]) do |t|
153
+ warnings.each do |warning|
154
+ t.add_row [warning["Confidence"], warning["Template"], warning["Warning Type"], warning["Message"]]
155
+ end
156
+ end
153
157
  end
154
158
  else
155
159
  nil
@@ -158,8 +162,8 @@ class Brakeman::Report
158
162
 
159
163
  #Generate table of model warnings or return nil if no warnings
160
164
  def generate_model_warnings html = false
161
- unless checks.model_warnings.empty?
162
- table = Ruport::Data::Table(["Confidence", "Model", "Warning Type", "Message"])
165
+ if checks.model_warnings.any?
166
+ warnings = []
163
167
  checks.model_warnings.each do |warning|
164
168
  w = warning.to_row :model
165
169
 
@@ -168,18 +172,24 @@ class Brakeman::Report
168
172
  w["Message"] = with_context warning, w["Message"]
169
173
  else
170
174
  w["Confidence"] = TEXT_CONFIDENCE[w["Confidence"]]
175
+ w["Message"] = text_message warning, w["Message"]
171
176
  end
172
177
 
173
- table << w
178
+ warnings << w
174
179
  end
175
180
 
176
- if table.empty?
177
- nil
181
+ return nil if warnings.empty?
182
+ stabilizer = 0
183
+ warnings = warnings.sort_by{|row| stabilizer +=1; [row["Confidence"],row["Warning Type"], row["Model"], stabilizer]}
184
+
185
+ if html
186
+ load_and_render_erb('model_warnings', binding)
178
187
  else
179
- table.sort_rows_by! "Model"
180
- table.sort_rows_by! "Warning Type"
181
- table.sort_rows_by! "Confidence"
182
- table.to_group "Model Warnings"
188
+ Terminal::Table.new(:headings => ["Confidence", "Model", "Warning Type", "Message"]) do |t|
189
+ warnings.each do |warning|
190
+ t.add_row [warning["Confidence"], warning["Model"], warning["Warning Type"], warning["Message"]]
191
+ end
192
+ end
183
193
  end
184
194
  else
185
195
  nil
@@ -189,7 +199,7 @@ class Brakeman::Report
189
199
  #Generate table of controller warnings or nil if no warnings
190
200
  def generate_controller_warnings html = false
191
201
  unless checks.controller_warnings.empty?
192
- table = Ruport::Data::Table(["Confidence", "Controller", "Warning Type", "Message"])
202
+ warnings = []
193
203
  checks.controller_warnings.each do |warning|
194
204
  w = warning.to_row :controller
195
205
 
@@ -198,18 +208,25 @@ class Brakeman::Report
198
208
  w["Message"] = with_context warning, w["Message"]
199
209
  else
200
210
  w["Confidence"] = TEXT_CONFIDENCE[w["Confidence"]]
211
+ w["Message"] = text_message warning, w["Message"]
201
212
  end
202
213
 
203
- table << w
214
+ warnings << w
204
215
  end
205
216
 
206
- if table.empty?
207
- nil
217
+ return nil if warnings.empty?
218
+
219
+ stabilizer = 0
220
+ warnings = warnings.sort_by{|row| stabilizer +=1; [row["Confidence"], row["Warning Type"], row["Controller"], stabilizer]}
221
+
222
+ if html
223
+ load_and_render_erb('controller_warnings', binding)
208
224
  else
209
- table.sort_rows_by! "Controller"
210
- table.sort_rows_by! "Warning Type"
211
- table.sort_rows_by! "Confidence"
212
- table.to_group "Controller Warnings"
225
+ Terminal::Table.new(:headings => ["Confidence", "Controller", "Warning Type", "Message"]) do |t|
226
+ warnings.each do |warning|
227
+ t.add_row [warning["Confidence"], warning["Controller"], warning["Warning Type"], warning["Message"]]
228
+ end
229
+ end
213
230
  end
214
231
  else
215
232
  nil
@@ -217,8 +234,8 @@ class Brakeman::Report
217
234
  end
218
235
 
219
236
  #Generate table of controllers and routes found for those controllers
220
- def generate_controllers
221
- table = Ruport::Data::Table(["Name", "Parent", "Includes", "Routes"])
237
+ def generate_controllers html=false
238
+ controller_rows = []
222
239
  tracker.controllers.keys.map{|k| k.to_s}.sort.each do |name|
223
240
  name = name.to_sym
224
241
  c = tracker.controllers[name]
@@ -243,70 +260,86 @@ class Brakeman::Report
243
260
  routes = "[None]"
244
261
  end
245
262
 
246
- table << { "Name" => name.to_s,
263
+ controller_rows << { "Name" => name.to_s,
247
264
  "Parent" => c[:parent].to_s,
248
265
  "Includes" => c[:includes].join(", "),
249
266
  "Routes" => routes
250
267
  }
251
268
  end
252
- table.sort_rows_by "Name"
269
+ controller_rows = controller_rows.sort_by{|row| row['Name']}
270
+
271
+ if html
272
+ load_and_render_erb('controller_overview', binding)
273
+ else
274
+ Terminal::Table.new(:headings => ['Name', 'Parent', 'Includes', 'Routes']) do |t|
275
+ controller_rows.each do |row|
276
+ t.add_row [row['Name'], row['Parent'], row['Includes'], row['Routes']]
277
+ end
278
+ end
279
+ end
253
280
  end
254
281
 
255
282
  #Generate listings of templates and their output
256
283
  def generate_templates html = false
257
284
  out_processor = Brakeman::OutputProcessor.new
258
- table = Ruport::Data::Table(["Name", "Output"])
285
+ template_rows = {}
259
286
  tracker.templates.each do |name, template|
260
287
  unless template[:outputs].empty?
261
288
  template[:outputs].each do |out|
262
289
  out = out_processor.format out
263
290
  out = CGI.escapeHTML(out) if html
264
- table << { "Name" => name,
265
- "Output" => out.gsub("\n", ";").gsub(/\s+/, " ") }
291
+ template_rows[name] ||= []
292
+ template_rows[name] << out.gsub("\n", ";").gsub(/\s+/, " ")
293
+ end
294
+ end
295
+ end
296
+
297
+ template_rows = template_rows.sort_by{|name, value| name.to_s}
298
+
299
+ if html
300
+ load_and_render_erb('template_overview', binding)
301
+ else
302
+ output = ''
303
+ template_rows.each do |template|
304
+ output << template.first.to_s << "\n\n"
305
+ table = Terminal::Table.new(:headings => ['Output']) do |t|
306
+ # template[1] is an array of calls
307
+ template[1].each do |v|
308
+ t.add_row [v]
309
+ end
266
310
  end
311
+
312
+ output << table.to_s << "\n\n"
267
313
  end
314
+
315
+ output
268
316
  end
269
- Ruport::Data::Grouping(table, :by => "Name")
270
317
  end
271
318
 
272
319
  #Generate HTML output
273
320
  def to_html
274
321
  out = html_header <<
275
- "<h2 id='summary'>Summary</h2>" <<
276
- generate_overview(true).to_html << "<br/>" <<
277
- generate_warning_overview.to_html
322
+ generate_overview(true) <<
323
+ generate_warning_overview(true)
278
324
 
279
- #Return early if only summarizing
325
+ # Return early if only summarizing
280
326
  if tracker.options[:summary_only]
281
327
  return out
282
328
  end
283
329
 
284
330
  if tracker.options[:report_routes] or tracker.options[:debug]
285
- out << "<h2>Controllers</h2>" <<
286
- generate_controllers.to_html
331
+ out << generate_controllers(true).to_s
287
332
  end
288
333
 
289
334
  if tracker.options[:debug]
290
- out << "<h2>Templates</h2>" <<
291
- generate_templates(true).to_html
335
+ out << generate_templates(true).to_s
292
336
  end
293
337
 
294
- res = generate_errors(true)
295
- if res
296
- 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>'
297
- end
298
-
299
- res = generate_warnings(true)
300
- out << "<h2>Security Warnings</h2>" << res.to_html if res
301
-
302
- res = generate_controller_warnings(true)
303
- out << res.to_html if res
304
-
305
- res = generate_model_warnings(true)
306
- out << res.to_html if res
307
-
308
- res = generate_template_warnings(true)
309
- out << res.to_html if res
338
+ out << generate_errors(true).to_s
339
+ out << generate_warnings(true).to_s
340
+ out << generate_controller_warnings(true).to_s
341
+ out << generate_model_warnings(true).to_s
342
+ out << generate_template_warnings(true).to_s
310
343
 
311
344
  out << "</body></html>"
312
345
  end
@@ -314,9 +347,9 @@ class Brakeman::Report
314
347
  #Output text version of the report
315
348
  def to_s
316
349
  out = text_header <<
317
- "\n+SUMMARY+\n" <<
318
- generate_overview.to_s << "\n" <<
319
- generate_warning_overview.to_s << "\n"
350
+ "\n\n+SUMMARY+\n\n" <<
351
+ truncate_table(generate_overview.to_s) << "\n\n" <<
352
+ truncate_table(generate_warning_overview.to_s) << "\n"
320
353
 
321
354
  #Return output early if only summarizing
322
355
  if tracker.options[:summary_only]
@@ -324,71 +357,77 @@ class Brakeman::Report
324
357
  end
325
358
 
326
359
  if tracker.options[:report_routes] or tracker.options[:debug]
327
- out << "+CONTROLLERS+\n" <<
328
- generate_controllers.to_s << "\n"
360
+ out << "\n+CONTROLLERS+\n" <<
361
+ truncate_table(generate_controllers.to_s) << "\n"
329
362
  end
330
363
 
331
364
  if tracker.options[:debug]
332
- out << "+TEMPLATES+\n\n" <<
333
- generate_templates.to_s << "\n"
365
+ out << "\n+TEMPLATES+\n\n" <<
366
+ truncate_table(generate_templates.to_s) << "\n"
334
367
  end
335
368
 
336
369
  res = generate_errors
337
- out << "+ERRORS+\n" << res.to_s << "\n" if res
370
+ out << "+Errors+\n" << truncate_table(res.to_s) if res
338
371
 
339
372
  res = generate_warnings
340
- out << "+SECURITY WARNINGS+\n" << res.to_s << "\n" if res
373
+ out << "\n\n+SECURITY WARNINGS+\n\n" << truncate_table(res.to_s) if res
341
374
 
342
375
  res = generate_controller_warnings
343
- out << res.to_s << "\n" if res
376
+ out << "\n\n\nController Warnings:\n\n" << truncate_table(res.to_s) if res
344
377
 
345
378
  res = generate_model_warnings
346
- out << res.to_s << "\n" if res
379
+ out << "\n\n\nModel Warnings:\n\n" << truncate_table(res.to_s) if res
347
380
 
348
381
  res = generate_template_warnings
349
- out << res.to_s << "\n" if res
382
+ out << "\n\nView Warnings:\n\n" << truncate_table(res.to_s) if res
350
383
 
384
+ out << "\n"
351
385
  out
352
386
  end
353
387
 
354
388
  #Generate CSV output
355
389
  def to_csv
356
- out = csv_header <<
357
- "\nSUMMARY\n" <<
358
- generate_overview.to_csv << "\n" <<
359
- generate_warning_overview.to_csv << "\n"
390
+ output = csv_header
391
+ output << "\nSUMMARY\n"
392
+
393
+ output << table_to_csv(generate_overview) << "\n"
394
+
395
+ output << table_to_csv(generate_warning_overview) << "\n"
360
396
 
361
397
  #Return output early if only summarizing
362
398
  if tracker.options[:summary_only]
363
- return out
399
+ return output
364
400
  end
365
401
 
366
402
  if tracker.options[:report_routes] or tracker.options[:debug]
367
- out << "CONTROLLERS\n" <<
368
- generate_controllers.to_csv << "\n"
403
+ output << "CONTROLLERS\n"
404
+ output << table_to_csv(generate_controllers) << "\n"
369
405
  end
370
406
 
371
407
  if tracker.options[:debug]
372
- out << "TEMPLATES\n\n" <<
373
- generate_templates.to_csv << "\n"
408
+ output << "TEMPLATES\n\n"
409
+ output << table_to_csv(generate_templates) << "\n"
374
410
  end
375
411
 
376
412
  res = generate_errors
377
- out << "ERRORS\n" << res.to_csv << "\n" if res
413
+ output << "ERRORS\n" << table_to_csv(res) << "\n" if res
378
414
 
379
415
  res = generate_warnings
380
- out << "SECURITY WARNINGS\n" << res.to_csv << "\n" if res
416
+ output << "SECURITY WARNINGS\n" << table_to_csv(res) << "\n" if res
381
417
 
418
+ output << "Controller Warnings\n"
382
419
  res = generate_controller_warnings
383
- out << res.to_csv << "\n" if res
420
+ output << table_to_csv(res) << "\n" if res
384
421
 
422
+ output << "Model Warnings\n"
385
423
  res = generate_model_warnings
386
- out << res.to_csv << "\n" if res
424
+ output << table_to_csv(res) << "\n" if res
387
425
 
388
426
  res = generate_template_warnings
389
- out << res.to_csv << "\n" if res
427
+ output << "Template Warnings\n"
428
+ output << table_to_csv(res) << "\n" if res
390
429
 
391
- out
430
+ output
392
431
  end
393
432
 
394
433
  #Not yet implemented
@@ -414,44 +453,7 @@ class Brakeman::Report
414
453
  raise "Cannot find CSS stylesheet for HTML: #{tracker.options[:html_style]}"
415
454
  end
416
455
 
417
- <<-HTML
418
- <!DOCTYPE HTML SYSTEM>
419
- <html>
420
- <head>
421
- <title>Brakeman Report</title>
422
- <script type="text/javascript">
423
- function toggle(context) {
424
- var elem = document.getElementById(context);
425
-
426
- if (elem.style.display != "block")
427
- elem.style.display = "block";
428
- else
429
- elem.style.display = "none";
430
-
431
- elem.parentNode.scrollIntoView();
432
- }
433
- </script>
434
- <style type="text/css">
435
- #{css}
436
- </style>
437
- </head>
438
- <body>
439
- <h1>Brakeman Report</h1>
440
- <table>
441
- <tr>
442
- <th>Application Path</th>
443
- <th>Rails Version</th>
444
- <th>Report Generation Time</th>
445
- <th>Checks Performed</th>
446
- </tr>
447
- <tr>
448
- <td>#{File.expand_path tracker.options[:app_path]}</td>
449
- <td>#{rails_version}</td>
450
- <td>#{Time.now}</td>
451
- <td>#{checks.checks_run.sort.join(", ")}</td>
452
- </tr>
453
- </table>
454
- HTML
456
+ load_and_render_erb('header', binding)
455
457
  end
456
458
 
457
459
  #Generate header for text output
@@ -461,9 +463,9 @@ class Brakeman::Report
461
463
 
462
464
  #Generate header for CSV output
463
465
  def csv_header
464
- header = Ruport::Data::Table(["Application Path", "Report Generation Time", "Checks Performed", "Rails Version"])
465
- header << [File.expand_path(tracker.options[:app_path]), Time.now.to_s, checks.checks_run.sort.join(", "), rails_version]
466
- "BRAKEMAN REPORT\n\n" << header.to_csv
466
+ header = CSV.generate_line(["Application Path", "Report Generation Time", "Checks Performed", "Rails Version"])
467
+ header << CSV.generate_line([File.expand_path(tracker.options[:app_path]), Time.now.to_s, checks.checks_run.sort.join(", "), rails_version])
468
+ "BRAKEMAN REPORT\n\n" + header
467
469
  end
468
470
 
469
471
  #Return summary of warnings in hash and store in @warnings_summary
@@ -473,10 +475,7 @@ class Brakeman::Report
473
475
  summary = Hash.new(0)
474
476
  high_confidence_warnings = 0
475
477
 
476
- [checks.warnings,
477
- checks.controller_warnings,
478
- checks.model_warnings,
479
- checks.template_warnings].each do |warnings|
478
+ [all_warnings].each do |warnings|
480
479
 
481
480
  warnings.each do |warning|
482
481
  summary[warning.warning_type.to_s] += 1
@@ -491,6 +490,28 @@ class Brakeman::Report
491
490
  @warnings_summary = summary
492
491
  end
493
492
 
493
+ #Escape warning message and highlight user input in text output
494
+ def text_message warning, message
495
+ if @highlight_user_input and warning.user_input
496
+ user_input = warning.format_user_input
497
+ message.gsub(user_input, "+#{user_input}+")
498
+ else
499
+ message
500
+ end
501
+ end
502
+
503
+ #Escape warning message and highlight user input in HTML output
504
+ def html_message warning, message
505
+ message = CGI.escapeHTML(message)
506
+
507
+ if @highlight_user_input and warning.user_input
508
+ user_input = warning.format_user_input
509
+
510
+ message.gsub!(user_input, "<span class=\"user_input\">#{user_input}</span>")
511
+ end
512
+
513
+ message
514
+ end
494
515
 
495
516
  #Generate HTML for warnings, including context show/hidden via Javascript
496
517
  def with_context warning, message
@@ -501,12 +522,14 @@ class Brakeman::Report
501
522
  tracker.options[:message_limit] > 0 and
502
523
  message.length > tracker.options[:message_limit]
503
524
 
504
- full_message = message
525
+ full_message = html_message(warning, message)
505
526
  message = message[0..tracker.options[:message_limit]] << "..."
506
527
  end
507
528
 
529
+ message = html_message(warning, message)
530
+
508
531
  if context.empty? and not full_message
509
- return CGI.escapeHTML(message)
532
+ return message
510
533
  end
511
534
 
512
535
  @element_id += 1
@@ -516,10 +539,10 @@ class Brakeman::Report
516
539
  alt = false
517
540
  output = "<div class='warning_message' onClick=\"toggle('#{code_id}');toggle('#{message_id}');toggle('#{full_message_id}')\" >" <<
518
541
  if full_message
519
- "<span id='#{message_id}' style='display:block' >#{CGI.escapeHTML(message)}</span>" <<
520
- "<span id='#{full_message_id}' style='display:none'>#{CGI.escapeHTML(full_message)}</span>"
542
+ "<span id='#{message_id}' style='display:block' >#{message}</span>" <<
543
+ "<span id='#{full_message_id}' style='display:none'>#{full_message}</span>"
521
544
  else
522
- CGI.escapeHTML(message)
545
+ message
523
546
  end <<
524
547
  "<table id='#{code_id}' class='context' style='display:none'>" <<
525
548
  "<caption>#{(warning.file || '').gsub(tracker.options[:app_path], "")}</caption>"
@@ -614,6 +637,42 @@ class Brakeman::Report
614
637
  def to_json
615
638
  require 'json'
616
639
 
617
- @checks.all_warnings.map { |w| w.to_hash }.to_json
640
+ errors = tracker.errors.map{|e| { :error => e[:error], :location => e[:backtrace][0] }}
641
+ warnings = all_warnings.map { |w| w.to_hash }.sort_by{|w| w[:file]}
642
+ scan_info = {
643
+ :app_path => File.expand_path(tracker.options[:app_path]),
644
+ :rails_version => rails_version,
645
+ :security_warnings => all_warnings.length,
646
+ :timestamp => Time.now,
647
+ :checks_performed => checks.checks_run.sort,
648
+ :number_of_controllers =>tracker.controllers.length,
649
+ # ignore the "fake" model
650
+ :number_of_models => tracker.models.length - 1,
651
+ :number_of_templates => number_of_templates(@tracker),
652
+ :ruby_version => RUBY_VERSION,
653
+ :brakeman_version => Brakeman::Version
654
+ }
655
+
656
+ JSON.pretty_generate({
657
+ :scan_info => scan_info,
658
+ :warnings => warnings,
659
+ :errors => errors
660
+ })
661
+ end
662
+
663
+ def all_warnings
664
+ @all_warnings ||= @checks.all_warnings
665
+ end
666
+
667
+ def number_of_templates tracker
668
+ Set.new(tracker.templates.map {|k,v| v[:name].to_s[/[^.]+/]}).length
669
+ end
670
+
671
+ private
672
+
673
+ def load_and_render_erb file, bind
674
+ content = File.read(File.expand_path("templates/#{file}.html.erb", File.dirname(__FILE__)))
675
+ template = ERB.new(content)
676
+ template.result(bind)
618
677
  end
619
678
  end