brakeman-min 3.1.2 → 3.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6cf98ba25b2de28c2edab38924f2f97f9af4cfc2
4
- data.tar.gz: 2d6ed02c1df44f91dc4336ca6b6d7cdd0c6a7a5b
3
+ metadata.gz: 17b94583aaceeebe181fc74d3aa2e36b908f8ba7
4
+ data.tar.gz: dd2bc29bb6c6f7d63afa4790c43e790ad034cb62
5
5
  SHA512:
6
- metadata.gz: 42f7962aef1b7572d508a4204b613644dc43107d5c1e311a5f30c5dc2e98abe7868790060379b4c1c429046650c30f0d58948e78457a6294337ec8bfce54d45b
7
- data.tar.gz: 61ff86b7dcd0b07d6d9ba5aee27f56381c7322555e5dfc3a47f56f6be749330ffbda36d741149d619ff97a1fd26476eb035ea0ff44ebbf7dc51e990294d0dba0
6
+ metadata.gz: 7bc0de2fca84e2d003049b6d20d2ab15f9d58631a84882c39c9af09cc7639620cfae761a6c2a48d40fe0cb45fb7ec8c706984eabc5648a1faaf8c3df287a539a
7
+ data.tar.gz: 304a4df1052211e5286a3bfa84a8af9550e9f55048c38cff48f7d3ebe263d480b6cda31b76ed529fc18684b64f96c191fe9978c9535e1ac54fbfb6739b493487
data/CHANGES CHANGED
@@ -1,3 +1,17 @@
1
+ # 3.1.3
2
+
3
+ * Check for session secret in secrets.yml
4
+ * Respect `exit_on_warn` in config file
5
+ * Avoid warning on `without_protection: true` with hash literals
6
+ * Make sure before_filter call with block is still a call
7
+ * CallIndex improvements
8
+ * Restore minimum Highline version (Kevin Glowacz)
9
+ * Add Code Climate output format (Ashley Baldwin-Hunter/Devon Blandin/John Pignata/Michael Bernstein)
10
+ * Iteratively replace values
11
+ * Output nil instead of false for user_input in JSON
12
+ * Depend on safe_yaml 1.0 or later
13
+ * Test coverage improvements for Brakema module (Bethany Rentz)
14
+
1
15
  # 3.1.2
2
16
 
3
17
  * Treat `current_user` like a model
data/README.md CHANGED
@@ -6,7 +6,9 @@
6
6
 
7
7
  # Brakeman
8
8
 
9
- Brakeman is a static analysis tool which checks Ruby on Rails applications for security vulnerabilities.
9
+ Brakeman is an open source static analysis tool which checks Ruby on Rails applications for security vulnerabilities.
10
+
11
+ Check out [Brakeman Pro](https://brakemanpro.com/) if you are looking for a commercially-supported version with a GUI and advanced features.
10
12
 
11
13
  # Installation
12
14
 
@@ -42,7 +44,7 @@ To specify an output file for the results:
42
44
 
43
45
  brakeman -o output_file
44
46
 
45
- The output format is determined by the file extension or by using the `-f` option. Current options are: `text`, `html`, `tabs`, `json`, `markdown`, and `csv`.
47
+ The output format is determined by the file extension or by using the `-f` option. Current options are: `text`, `html`, `tabs`, `json`, `markdown`, `csv`, and `codeclimate`.
46
48
 
47
49
  Multiple output files can be specified:
48
50
 
@@ -78,7 +78,7 @@ begin
78
78
  tracker = Brakeman.run options.merge(:print_report => true, :quiet => options[:quiet])
79
79
 
80
80
  #Return error code if --exit-on-warn is used and warnings were found
81
- if options[:exit_on_warn] and not tracker.filtered_warnings.empty?
81
+ if tracker.options[:exit_on_warn] and not tracker.filtered_warnings.empty?
82
82
  exit Brakeman::Warnings_Found_Exit_Code
83
83
  end
84
84
  end
@@ -97,7 +97,7 @@ module Brakeman
97
97
 
98
98
  if options
99
99
  options.each { |k, v| options[k] = Set.new v if v.is_a? Array }
100
-
100
+
101
101
  # After parsing the yaml config file for options, convert any string keys into symbols.
102
102
  options.keys.select {|k| k.is_a? String}.map {|k| k.to_sym }.each {|k| options[k] = options[k.to_s]; options.delete(k.to_s) }
103
103
 
@@ -180,6 +180,8 @@ module Brakeman
180
180
  [:to_json]
181
181
  when :markdown, :to_markdown
182
182
  [:to_markdown]
183
+ when :cc, :to_cc, :codeclimate, :to_codeclimate
184
+ [:to_codeclimate]
183
185
  else
184
186
  [:to_s]
185
187
  end
@@ -201,6 +203,8 @@ module Brakeman
201
203
  :to_json
202
204
  when /\.md$/i
203
205
  :to_markdown
206
+ when /(\.cc|\.codeclimate)$/i
207
+ :to_codeclimate
204
208
  else
205
209
  :to_s
206
210
  end
@@ -303,11 +307,10 @@ module Brakeman
303
307
  File.open file, "w" do |f|
304
308
  YAML.dump options, f
305
309
  end
306
- puts "Output configuration to #{file}"
310
+ notify "Output configuration to #{file}"
307
311
  else
308
- puts YAML.dump(options)
312
+ notify YAML.dump(options)
309
313
  end
310
- exit
311
314
  end
312
315
 
313
316
  #Run a scan. Generally called from Brakeman.run instead of directly.
@@ -155,7 +155,7 @@ class Brakeman::CheckMassAssignment < Brakeman::BaseCheck
155
155
  # Look for and warn about uses of Parameters#permit! for mass assignment
156
156
  def check_permit!
157
157
  tracker.find_call(:method => :permit!).each do |result|
158
- if params? result[:target]
158
+ if params? result[:call].target
159
159
  warn_on_permit! result
160
160
  end
161
161
  end
@@ -19,13 +19,17 @@ class Brakeman::CheckSessionSettings < Brakeman::BaseCheck
19
19
  def run_check
20
20
  settings = tracker.config.session_settings
21
21
 
22
- check_for_issues settings, "#{tracker.app_path}/config/environment.rb"
22
+ check_for_issues settings, @app_tree.expand_path("config/environment.rb")
23
23
 
24
24
  ["session_store.rb", "secret_token.rb"].each do |file|
25
25
  if tracker.initializers[file] and not ignored? file
26
26
  process tracker.initializers[file]
27
27
  end
28
28
  end
29
+
30
+ if tracker.options[:rails4]
31
+ check_secrets_yaml
32
+ end
29
33
  end
30
34
 
31
35
  #Looks for ActionController::Base.session = { ... }
@@ -38,13 +42,13 @@ class Brakeman::CheckSessionSettings < Brakeman::BaseCheck
38
42
  #in Rails 4.x apps
39
43
  def process_attrasgn exp
40
44
  if not tracker.options[:rails3] and exp.target == @session_settings and exp.method == :session=
41
- check_for_issues exp.first_arg, "#{tracker.app_path}/config/initializers/session_store.rb"
45
+ check_for_issues exp.first_arg, @app_tree.expand_path("config/initializers/session_store.rb")
42
46
  end
43
47
 
44
48
  if tracker.options[:rails3] and settings_target?(exp.target) and
45
49
  (exp.method == :secret_token= or exp.method == :secret_key_base=) and string? exp.first_arg
46
50
 
47
- warn_about_secret_token exp, "#{tracker.app_path}/config/initializers/secret_token.rb"
51
+ warn_about_secret_token exp.line, @app_tree.expand_path("config/initializers/secret_token.rb")
48
52
  end
49
53
 
50
54
  exp
@@ -54,7 +58,7 @@ class Brakeman::CheckSessionSettings < Brakeman::BaseCheck
54
58
  #in Rails 3.x apps
55
59
  def process_call exp
56
60
  if tracker.options[:rails3] and settings_target?(exp.target) and exp.method == :session_store
57
- check_for_rails3_issues exp.second_arg, "#{tracker.app_path}/config/initializers/session_store.rb"
61
+ check_for_rails3_issues exp.second_arg, @app_tree.expand_path("config/initializers/session_store.rb")
58
62
  end
59
63
 
60
64
  exp
@@ -76,13 +80,13 @@ class Brakeman::CheckSessionSettings < Brakeman::BaseCheck
76
80
  hash_access(settings, :httponly))
77
81
 
78
82
  if false? value
79
- warn_about_http_only value, file
83
+ warn_about_http_only value.line, file
80
84
  end
81
85
  end
82
86
 
83
87
  if value = hash_access(settings, :secret)
84
88
  if string? value
85
- warn_about_secret_token value, file
89
+ warn_about_secret_token value.line, file
86
90
  end
87
91
  end
88
92
  end
@@ -92,43 +96,61 @@ class Brakeman::CheckSessionSettings < Brakeman::BaseCheck
92
96
  if settings and hash? settings
93
97
  if value = hash_access(settings, :httponly)
94
98
  if false? value
95
- warn_about_http_only value, file
99
+ warn_about_http_only value.line, file
96
100
  end
97
101
  end
98
102
 
99
103
  if value = hash_access(settings, :secure)
100
104
  if false? value
101
- warn_about_secure_only value, file
105
+ warn_about_secure_only value.line, file
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def check_secrets_yaml
112
+ secrets_file = "config/secrets.yml"
113
+
114
+ if @app_tree.exists? secrets_file
115
+ yaml = @app_tree.read secrets_file
116
+ require 'safe_yaml/load'
117
+ secrets = SafeYAML.load yaml
118
+
119
+ if secrets["production"] and secret = secrets["production"]["secret_key_base"]
120
+ unless secret.include? "<%="
121
+ line = yaml.lines.find_index { |l| l.include? secret } + 1
122
+
123
+ warn_about_secret_token line, @app_tree.expand_path(secrets_file)
102
124
  end
103
125
  end
104
126
  end
105
127
  end
106
128
 
107
- def warn_about_http_only value, file
129
+ def warn_about_http_only line, file
108
130
  warn :warning_type => "Session Setting",
109
131
  :warning_code => :http_cookies,
110
132
  :message => "Session cookies should be set to HTTP only",
111
133
  :confidence => CONFIDENCE[:high],
112
- :line => value.line,
134
+ :line => line,
113
135
  :file => file
114
136
 
115
137
  end
116
138
 
117
- def warn_about_secret_token value, file
139
+ def warn_about_secret_token line, file
118
140
  warn :warning_type => "Session Setting",
119
141
  :warning_code => :session_secret,
120
142
  :message => "Session secret should not be included in version control",
121
143
  :confidence => CONFIDENCE[:high],
122
- :line => value.line,
144
+ :line => line,
123
145
  :file => file
124
146
  end
125
147
 
126
- def warn_about_secure_only value, file
148
+ def warn_about_secure_only line, file
127
149
  warn :warning_type => "Session Setting",
128
150
  :warning_code => :secure_cookies,
129
151
  :message => "Session cookie should be set to secure only",
130
152
  :confidence => CONFIDENCE[:high],
131
- :line => value.line,
153
+ :line => line,
132
154
  :file => file
133
155
  end
134
156
 
@@ -43,6 +43,8 @@ class Brakeman::CheckWithoutProtection < Brakeman::BaseCheck
43
43
 
44
44
  if input = include_user_input?(call.arglist)
45
45
  confidence = CONFIDENCE[:high]
46
+ elsif all_literals? call
47
+ return
46
48
  else
47
49
  confidence = CONFIDENCE[:med]
48
50
  end
@@ -59,4 +61,20 @@ class Brakeman::CheckWithoutProtection < Brakeman::BaseCheck
59
61
  end
60
62
  end
61
63
  end
64
+
65
+ def all_literals? call
66
+ call.each_arg do |arg|
67
+ if hash? arg
68
+ hash_iterate arg do |k, v|
69
+ unless node_type? k, :str, :lit, :false, :true and node_type? v, :str, :lit, :false, :true
70
+ return false
71
+ end
72
+ end
73
+ else
74
+ return false
75
+ end
76
+ end
77
+
78
+ true
79
+ end
62
80
  end
@@ -165,7 +165,7 @@ module Brakeman::Options
165
165
 
166
166
  opts.on "-f",
167
167
  "--format TYPE",
168
- [:pdf, :text, :html, :csv, :tabs, :json, :markdown],
168
+ [:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc],
169
169
  "Specify output formats. Default is text" do |type|
170
170
 
171
171
  type = "s" if type == :text
@@ -62,18 +62,23 @@ class Brakeman::AliasProcessor < Brakeman::SexpProcessor
62
62
  @tracker.error err if @tracker
63
63
  end
64
64
 
65
- #Generic replace
66
- if replacement = env[exp] and not duplicate? replacement
67
- result = replacement.deep_clone(exp.line)
68
- else
69
- result = exp
70
- end
71
-
65
+ result = replace(exp)
66
+
72
67
  @exp_context.pop
73
68
 
74
69
  result
75
70
  end
76
71
 
72
+ def replace exp, int = 0
73
+ return exp if int > 3
74
+
75
+ if replacement = env[exp] and not duplicate? replacement
76
+ replace(replacement.deep_clone(exp.line), int + 1)
77
+ else
78
+ exp
79
+ end
80
+ end
81
+
77
82
  ARRAY_CONST = s(:const, :Array)
78
83
  HASH_CONST = s(:const, :Hash)
79
84
 
@@ -236,9 +236,14 @@ class Brakeman::ControllerProcessor < Brakeman::BaseProcessor
236
236
 
237
237
  #Look for before_filters and add fake ones if necessary
238
238
  def process_iter exp
239
- block_call_name = exp.block_call.method
240
- if block_call_name == :before_filter or block_call_name == :before_action
241
- add_fake_filter exp
239
+ if @current_method.nil? and call? exp.block_call
240
+ block_call_name = exp.block_call.method
241
+
242
+ if block_call_name == :before_filter or block_call_name == :before_action
243
+ add_fake_filter exp
244
+ else
245
+ super
246
+ end
242
247
  else
243
248
  super
244
249
  end
@@ -126,7 +126,7 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
126
126
 
127
127
  #Gets the target of a call as a Symbol
128
128
  #if possible
129
- def get_target exp
129
+ def get_target exp, include_calls = false
130
130
  if sexp? exp
131
131
  case exp.node_type
132
132
  when :ivar, :lvar, :const, :lit
@@ -137,6 +137,23 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
137
137
  class_name exp
138
138
  when :self
139
139
  @current_class || @current_module || nil
140
+ when :params, :session, :cookies
141
+ exp.node_type
142
+ when :call
143
+ if include_calls
144
+ if exp.target.nil?
145
+ exp.method
146
+ else
147
+ t = get_target(exp.target, :include_calls)
148
+ if t.is_a? Symbol
149
+ :"#{t}.#{exp.method}"
150
+ else
151
+ exp
152
+ end
153
+ end
154
+ else
155
+ exp
156
+ end
140
157
  else
141
158
  exp
142
159
  end
@@ -187,6 +204,8 @@ class Brakeman::FindAllCalls < Brakeman::BasicProcessor
187
204
  @in_target = true
188
205
  process target
189
206
  @in_target = already_in_target
207
+
208
+ target = get_target(target, :include_calls)
190
209
  end
191
210
 
192
211
  method = exp.method
@@ -6,7 +6,7 @@ require 'brakeman/report/report_base'
6
6
  class Brakeman::Report
7
7
  attr_reader :tracker
8
8
 
9
- VALID_FORMATS = [:to_html, :to_pdf, :to_csv, :to_json, :to_tabs, :to_hash, :to_s, :to_markdown]
9
+ VALID_FORMATS = [:to_html, :to_pdf, :to_csv, :to_json, :to_tabs, :to_hash, :to_s, :to_markdown, :to_codeclimate]
10
10
 
11
11
  def initialize app_tree, tracker
12
12
  @app_tree = app_tree
@@ -15,6 +15,9 @@ class Brakeman::Report
15
15
 
16
16
  def format format
17
17
  reporter = case format
18
+ when :to_codeclimate
19
+ require_report 'codeclimate'
20
+ Brakeman::Report::CodeClimate
18
21
  when :to_csv
19
22
  require_report 'csv'
20
23
  Brakeman::Report::CSV
@@ -0,0 +1,71 @@
1
+ ---
2
+ basic_auth_password: 300000
3
+ cross_site_scripting: 300000
4
+ xss_content_tag: 300000
5
+ CVE_2014_3514_call: 600000
6
+ all_default_routes: 2000000
7
+ unsafe_deserialize: 2000000
8
+ local_request_config: 100000
9
+ CVE_2012_3424: 4000000
10
+ CVE_2011_2932: 8000000
11
+ code_eval: 2000000
12
+ command_injection: 2000000
13
+ file_access: 2000000
14
+ CVE_2014_7829: 4000000
15
+ CVE_2011_2929: 4000000
16
+ csrf_protection_disabled: 4000000
17
+ CVE_2013_6414: 4000000
18
+ CVE_2013_4491: 4000000
19
+ CVE_2013_1856: 4000000
20
+ CVE_2015_3226: 4000000
21
+ CVE_2013_0333: 4000000
22
+ xss_link_to: 300000
23
+ xss_link_to_href: 300000
24
+ CVE_2011_0446: 300000
25
+ mass_assign_call: 2000000
26
+ dangerous_attr_accessible: 2000000
27
+ no_attr_accessible: 2000000
28
+ CVE_2013_0277: 2000000
29
+ CVE_2010_3933: 4000000
30
+ CVE_2014_0081: 300000
31
+ CVE_2011_2930: 600000
32
+ open_redirect: 300000
33
+ regex_dos: 600000
34
+ dynamic_render_path: 4000000
35
+ CVE_2014_0082: 4000000
36
+ cross_site_scripting_inline: 600000
37
+ CVE_2011_3186: 2000000
38
+ safe_buffer_vuln: 4000000
39
+ CVE_2013_1855: 4000000
40
+ CVE_2013_1857: 4000000
41
+ CVE_2012_3463: 600000
42
+ select_options_vuln: 4000000
43
+ dangerous_send: 600000
44
+ session_key_manipulation: 600000
45
+ http_cookies: 600000
46
+ session_secret: 600000
47
+ secure_cookies: 600000
48
+ CVE_2013_6416: 600000
49
+ CVE_2012_3464: 4000000
50
+ csrf_blacklist: 300000
51
+ auth_blacklist: 300000
52
+ sql_injection: 1200000
53
+ CVE-2012-2660: 4000000
54
+ CVE-2012-2661: 4000000
55
+ CVE-2012-2695: 4000000
56
+ CVE-2012-5664: 4000000
57
+ CVE-2013-0155: 4000000
58
+ CVE-2013-6417: 4000000
59
+ CVE-2014-3482: 4000000
60
+ CVE-2014-3483: 4000000
61
+ ssl_verification_bypass: 2500000
62
+ CVE_2011_2931: 4000000
63
+ unsafe_symbol_creation: 300000
64
+ translate_vuln: 300000
65
+ unsafe_constantize: 600000
66
+ unscoped_find: 300000
67
+ validation_regex: 300000
68
+ mass_assign_without_protection: 600000
69
+ CVE_2015_3227: 4000000
70
+ CVE_2013_0156: 4000000
71
+ weak_hash_digest: 800000
@@ -0,0 +1,68 @@
1
+ require "json"
2
+ require "yaml"
3
+
4
+ class Brakeman::Report::CodeClimate < Brakeman::Report::Base
5
+ DOCUMENTATION_PATH = File.expand_path("../../../../docs/warning_types", __FILE__)
6
+ REMEDIATION_POINTS_CONFIG_PATH = File.expand_path("../config/remediation.yml", __FILE__)
7
+ REMEDIATION_POINTS_DEFAULT = 300_000
8
+
9
+ def generate_report
10
+ all_warnings.map { |warning| issue_json(warning) }.join("\0")
11
+ end
12
+
13
+ private
14
+
15
+ def issue_json(warning)
16
+ warning_code_name = name_for(warning.warning_code)
17
+
18
+ {
19
+ type: "Issue",
20
+ check_name: warning_code_name,
21
+ description: warning.message,
22
+ categories: ["Security"],
23
+ severity: severity_level_for(warning.confidence),
24
+ remediation_points: remediation_points_for(warning_code_name),
25
+ location: {
26
+ path: warning.relative_path,
27
+ lines: {
28
+ begin: warning.line || 1
29
+ }
30
+ },
31
+ content: {
32
+ body: content_for(warning.warning_code, warning.link)
33
+ }
34
+ }.to_json
35
+ end
36
+
37
+ def severity_level_for(confidence)
38
+ if confidence == 0
39
+ "critical"
40
+ else
41
+ "normal"
42
+ end
43
+ end
44
+
45
+ def remediation_points_for(warning_code)
46
+ @remediation_points ||= YAML.load_file(REMEDIATION_POINTS_CONFIG_PATH)
47
+ @remediation_points.fetch(name_for(warning_code), REMEDIATION_POINTS_DEFAULT)
48
+ end
49
+
50
+ def name_for(warning_code)
51
+ @warning_codes ||= Brakeman::WarningCodes::Codes.invert
52
+ @warning_codes[warning_code].to_s
53
+ end
54
+
55
+ def content_for(warning_code, link)
56
+ @contents ||= {}
57
+ unless link.nil?
58
+ @contents[warning_code] ||= local_content_for(link) || "Read more: #{link}"
59
+ end
60
+ end
61
+
62
+ def local_content_for(link)
63
+ directory = link.split("/").last
64
+ filename = File.join(DOCUMENTATION_PATH, directory, "index.markdown")
65
+
66
+ File.read(filename) if File.exist?(filename)
67
+ end
68
+ end
@@ -1,3 +1,3 @@
1
1
  module Brakeman
2
- Version = "3.1.2"
2
+ Version = "3.1.3"
3
3
  end
@@ -60,6 +60,8 @@ class Brakeman::Warning
60
60
  if @user_input.is_a? Brakeman::BaseCheck::Match
61
61
  @user_input_type = @user_input.type
62
62
  @user_input = @user_input.match
63
+ elsif @user_input == false
64
+ @user_input = nil
63
65
  end
64
66
 
65
67
  if not @line
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brakeman-min
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.2
4
+ version: 3.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Collins
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain:
11
11
  - brakeman-public_cert.pem
12
- date: 2015-10-28 00:00:00.000000000 Z
12
+ date: 2015-12-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: test-unit
@@ -79,14 +79,14 @@ dependencies:
79
79
  requirements:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '1.0'
83
83
  type: :runtime
84
84
  prerelease: false
85
85
  version_requirements: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: '1.0'
90
90
  description: Brakeman detects security vulnerabilities in Ruby on Rails applications
91
91
  via static analysis. This version of the gem only requires the minimum number of
92
92
  dependencies. Use the 'brakeman' gem for a full install.
@@ -206,12 +206,14 @@ files:
206
206
  - lib/brakeman/processors/template_alias_processor.rb
207
207
  - lib/brakeman/processors/template_processor.rb
208
208
  - lib/brakeman/report.rb
209
+ - lib/brakeman/report/config/remediation.yml
209
210
  - lib/brakeman/report/ignore/config.rb
210
211
  - lib/brakeman/report/ignore/interactive.rb
211
212
  - lib/brakeman/report/initializers/faster_csv.rb
212
213
  - lib/brakeman/report/initializers/multi_json.rb
213
214
  - lib/brakeman/report/renderer.rb
214
215
  - lib/brakeman/report/report_base.rb
216
+ - lib/brakeman/report/report_codeclimate.rb
215
217
  - lib/brakeman/report/report_csv.rb
216
218
  - lib/brakeman/report/report_hash.rb
217
219
  - lib/brakeman/report/report_html.rb