brakeman 1.5.3 → 1.6.0.pre1

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 (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