inspec 1.51.0 → 1.51.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -15
  3. data/README.md +1 -1
  4. data/docs/glossary.md +99 -0
  5. data/docs/resources/aide_conf.md.erb +16 -9
  6. data/docs/resources/apache.md.erb +66 -0
  7. data/docs/resources/apache_conf.md.erb +11 -5
  8. data/docs/resources/apt.md.erb +1 -1
  9. data/docs/resources/audit_policy.md.erb +1 -1
  10. data/docs/resources/auditd_conf.md.erb +12 -9
  11. data/docs/resources/bash.md.erb +24 -12
  12. data/docs/resources/bond.md.erb +26 -24
  13. data/docs/resources/bridge.md.erb +18 -11
  14. data/docs/resources/bsd_service.md.erb +11 -2
  15. data/docs/resources/command.md.erb +30 -29
  16. data/docs/resources/cpan.md.erb +33 -17
  17. data/docs/resources/cran.md.erb +26 -17
  18. data/docs/resources/crontab.md.erb +18 -1
  19. data/docs/resources/csv.md.erb +13 -7
  20. data/docs/resources/{dh_params.md → dh_params.md.erb} +30 -6
  21. data/docs/resources/directory.md.erb +9 -4
  22. data/docs/resources/docker.md.erb +1 -1
  23. data/docs/resources/docker_container.md.erb +32 -26
  24. data/docs/resources/docker_image.md.erb +29 -26
  25. data/docs/resources/docker_service.md.erb +37 -31
  26. data/docs/resources/elasticsearch.md.erb +18 -32
  27. data/docs/resources/etc_fstab.md.erb +19 -15
  28. data/docs/resources/etc_group.md.erb +13 -39
  29. data/docs/resources/etc_hosts.md.erb +12 -5
  30. data/docs/resources/etc_hosts_allow.md.erb +9 -4
  31. data/docs/resources/etc_hosts_deny.md.erb +12 -7
  32. data/docs/resources/file.md.erb +139 -134
  33. data/docs/resources/filesystem.md.erb +5 -4
  34. data/docs/resources/firewalld.md.erb +1 -1
  35. data/docs/resources/gem.md.erb +2 -2
  36. data/docs/resources/group.md.erb +1 -1
  37. data/docs/resources/host.md.erb +1 -1
  38. data/docs/resources/iis_app.md.erb +1 -1
  39. data/docs/resources/iis_site.md.erb +1 -1
  40. data/docs/resources/interface.md.erb +1 -1
  41. data/docs/resources/iptables.md.erb +1 -1
  42. data/docs/resources/json.md.erb +1 -1
  43. data/docs/resources/kernel_module.md.erb +1 -1
  44. data/docs/resources/kernel_parameter.md.erb +1 -1
  45. data/docs/resources/launchd_service.md.erb +1 -1
  46. data/docs/resources/limits_conf.md.erb +1 -1
  47. data/docs/resources/login_def.md.erb +1 -1
  48. data/docs/resources/mount.md.erb +1 -1
  49. data/docs/resources/mysql_conf.md.erb +1 -1
  50. data/docs/resources/nginx_conf.md.erb +1 -1
  51. data/docs/resources/npm.md.erb +1 -1
  52. data/docs/resources/oneget.md.erb +1 -1
  53. data/docs/resources/os.md.erb +1 -1
  54. data/docs/resources/os_env.md.erb +2 -2
  55. data/docs/resources/package.md.erb +1 -1
  56. data/docs/resources/packages.md.erb +66 -0
  57. data/docs/resources/parse_config.md.erb +1 -1
  58. data/docs/resources/parse_config_file.md.erb +1 -1
  59. data/docs/resources/passwd.md.erb +1 -1
  60. data/docs/resources/pip.md.erb +1 -1
  61. data/docs/resources/port.md.erb +1 -1
  62. data/docs/resources/postgres_conf.md.erb +1 -1
  63. data/docs/resources/postgres_session.md.erb +1 -1
  64. data/docs/resources/powershell.md.erb +2 -2
  65. data/docs/resources/processes.md.erb +1 -1
  66. data/docs/resources/registry_key.md.erb +1 -1
  67. data/docs/resources/runit_service.md.erb +1 -1
  68. data/docs/resources/security_policy.md.erb +1 -1
  69. data/docs/resources/service.md.erb +1 -1
  70. data/docs/resources/shadow.md.erb +1 -1
  71. data/docs/resources/ssh_config.md.erb +1 -1
  72. data/docs/resources/sshd_config.md.erb +1 -1
  73. data/docs/resources/ssl.md.erb +1 -1
  74. data/docs/resources/sys_info.md.erb +1 -1
  75. data/docs/resources/systemd_service.md.erb +1 -1
  76. data/docs/resources/sysv_service.md.erb +1 -1
  77. data/docs/resources/upstart_service.md.erb +1 -1
  78. data/docs/resources/user.md.erb +1 -1
  79. data/docs/resources/users.md.erb +1 -1
  80. data/docs/resources/windows_feature.md.erb +1 -1
  81. data/docs/resources/windows_hotfix.md.erb +1 -1
  82. data/docs/resources/xinetd_conf.md.erb +1 -1
  83. data/docs/resources/xml.md.erb +1 -1
  84. data/docs/resources/yaml.md.erb +1 -1
  85. data/docs/resources/yum.md.erb +1 -1
  86. data/lib/inspec.rb +2 -1
  87. data/lib/inspec/base_cli.rb +98 -18
  88. data/lib/inspec/cli.rb +33 -21
  89. data/lib/inspec/formatters.rb +3 -0
  90. data/lib/inspec/formatters/base.rb +208 -0
  91. data/lib/inspec/formatters/json_rspec.rb +20 -0
  92. data/lib/inspec/formatters/show_progress.rb +12 -0
  93. data/lib/inspec/objects.rb +1 -0
  94. data/lib/inspec/objects/describe.rb +92 -0
  95. data/lib/inspec/reporters.rb +33 -0
  96. data/lib/inspec/reporters/base.rb +23 -0
  97. data/lib/inspec/reporters/cli.rb +395 -0
  98. data/lib/inspec/reporters/json.rb +132 -0
  99. data/lib/inspec/reporters/json_min.rb +44 -0
  100. data/lib/inspec/reporters/junit.rb +77 -0
  101. data/lib/inspec/runner.rb +14 -1
  102. data/lib/inspec/runner_rspec.rb +34 -14
  103. data/lib/inspec/schema.rb +1 -0
  104. data/lib/inspec/shell.rb +0 -1
  105. data/lib/inspec/version.rb +1 -1
  106. data/lib/resources/apache.rb +20 -0
  107. data/lib/resources/apache_conf.rb +33 -8
  108. data/lib/resources/audit_policy.rb +1 -1
  109. data/lib/resources/packages.rb +4 -3
  110. metadata +17 -4
  111. data/lib/inspec/rspec_json_formatter.rb +0 -940
@@ -0,0 +1,3 @@
1
+ require 'inspec/formatters/base'
2
+ require 'inspec/formatters/json_rspec'
3
+ require 'inspec/formatters/show_progress'
@@ -0,0 +1,208 @@
1
+ require 'rspec/core'
2
+ require 'rspec/core/formatters/base_formatter'
3
+
4
+ module Inspec::Formatters
5
+ class Base < RSpec::Core::Formatters::BaseFormatter
6
+ RSpec::Core::Formatters.register self, :close, :dump_summary, :stop
7
+
8
+ attr_accessor :backend, :run_data
9
+
10
+ def initialize(output)
11
+ super(output)
12
+
13
+ @run_data = {}
14
+ @profiles = []
15
+ @profiles_info = nil
16
+ @backend = nil
17
+ end
18
+
19
+ # RSpec Override: #dump_summary
20
+ #
21
+ # Supply run summary data, such as the InSpec version and the total duration.
22
+ def dump_summary(summary)
23
+ run_data[:version] = Inspec::VERSION
24
+ run_data[:statistics] = {
25
+ duration: summary.duration,
26
+ }
27
+ end
28
+
29
+ # RSpec Override: #stop
30
+ #
31
+ # Called at the end of a complete RSpec run.
32
+ # We use this to map tests to controls and flesh out the rest of the run_data
33
+ # hash to include details about the run, the platform, etc.
34
+ def stop(notification)
35
+ # This might be a bit confusing. The results are not actually organized
36
+ # by control. It is organized by test. So if a control has 3 tests, the
37
+ # output will have 3 control entries, each one with the same control id
38
+ # and different test results. An rspec example maps to an inspec test.
39
+ run_data[:controls] = notification.examples.map do |example|
40
+ format_example(example).tap do |hash|
41
+ e = example.exception
42
+ next unless e
43
+
44
+ if example.metadata[:sensitive]
45
+ hash[:message] = '*** sensitive output suppressed ***'
46
+ else
47
+ hash[:message] = exception_message(e)
48
+ end
49
+
50
+ next if e.is_a? RSpec::Expectations::ExpectationNotMetError
51
+ hash[:exception] = e.class.name
52
+ hash[:backtrace] = e.backtrace
53
+ end
54
+ end
55
+
56
+ # include any tests that were run that were not part of a control
57
+ run_data[:other_checks] = examples_without_controls
58
+ examples_with_controls.each do |example|
59
+ control = example2control(example)
60
+ move_example_into_control(example, control)
61
+ end
62
+
63
+ # flesh out the profiles key with additional profile information
64
+ run_data[:profiles] = profiles_info
65
+
66
+ # add the platform information for this particular target
67
+ run_data[:platform] = {
68
+ name: platform(:name),
69
+ release: platform(:release),
70
+ target: backend_target,
71
+ }
72
+ end
73
+
74
+ # Add the current profile to the list of executed profiles.
75
+ # Called by the runner during example collection.
76
+ def add_profile(profile)
77
+ @profiles.push(profile)
78
+ end
79
+
80
+ # Return all the collected output to the caller
81
+ def results
82
+ run_data
83
+ end
84
+
85
+ private
86
+
87
+ def exception_message(exception)
88
+ if exception.is_a?(RSpec::Core::MultipleExceptionError)
89
+ exception.all_exceptions.map(&:message).uniq.join("\n\n")
90
+ else
91
+ exception.message
92
+ end
93
+ end
94
+
95
+ # RSpec Override: #format_example
96
+ #
97
+ # Called after test execution, this allows us to populate our own hash with data
98
+ # for this test that is necessary for the rest of our reports.
99
+ def format_example(example) # rubocop:disable Metrics/AbcSize
100
+ if !example.metadata[:description_args].empty? && example.metadata[:skip]
101
+ # For skipped profiles, rspec returns in full_description the skip_message as well. We don't want
102
+ # to mix the two, so we pick the full_description from the example.metadata[:example_group] hash.
103
+ code_description = example.metadata[:example_group][:description]
104
+ else
105
+ code_description = example.metadata[:full_description]
106
+ end
107
+
108
+ res = {
109
+ id: example.metadata[:id],
110
+ profile_id: example.metadata[:profile_id],
111
+ status: example.execution_result.status.to_s,
112
+ code_desc: code_description,
113
+ run_time: example.execution_result.run_time,
114
+ start_time: example.execution_result.started_at.to_s,
115
+ resource_title: example.metadata[:described_class] || example.metadata[:example_group][:description],
116
+ expectation_message: format_expectation_message(example),
117
+ }
118
+
119
+ unless (pid = example.metadata[:profile_id]).nil?
120
+ res[:profile_id] = pid
121
+ end
122
+
123
+ if res[:status] == 'pending'
124
+ res[:status] = 'skipped'
125
+ res[:skip_message] = example.metadata[:description]
126
+ res[:resource] = example.metadata[:described_class].to_s
127
+ end
128
+
129
+ res
130
+ end
131
+
132
+ def format_expectation_message(example)
133
+ if (example.metadata[:example_group][:description_args].first == example.metadata[:example_group][:described_class]) ||
134
+ example.metadata[:example_group][:described_class].nil?
135
+ example.metadata[:description]
136
+ else
137
+ "#{example.metadata[:example_group][:description]} #{example.metadata[:description]}"
138
+ end
139
+ end
140
+
141
+ def platform(field)
142
+ return nil if @backend.nil?
143
+ @backend.platform[field]
144
+ end
145
+
146
+ def backend_target
147
+ return nil if @backend.nil?
148
+ connection = @backend.backend
149
+ connection.respond_to?(:uri) ? connection.uri : nil
150
+ end
151
+
152
+ def examples
153
+ run_data[:controls]
154
+ end
155
+
156
+ def examples_without_controls
157
+ examples.find_all { |example| example2control(example).nil? }
158
+ end
159
+
160
+ def examples_with_controls
161
+ examples.find_all { |example| !example2control(example).nil? }
162
+ end
163
+
164
+ def profiles_info
165
+ @profiles_info ||= @profiles.map(&:info!).map(&:dup)
166
+ end
167
+
168
+ def example2control(example)
169
+ profile = profile_from_example(example)
170
+ return nil unless profile&.[](:controls)
171
+ profile[:controls].find { |x| x[:id] == example[:id] }
172
+ end
173
+
174
+ def profile_from_example(example)
175
+ profiles_info.find { |p| profile_contains_example?(p, example) }
176
+ end
177
+
178
+ def profile_contains_example?(profile, example)
179
+ profile_name = profile[:name]
180
+ example_profile_id = example[:profile_id]
181
+
182
+ # if either the profile name is nil or the profile in the given example
183
+ # is nil, assume the profile doesn't contain the example and default
184
+ # to creating a new profile. Otherwise, for profiles that have no
185
+ # metadata, this may incorrectly match a profile that does not contain
186
+ # this example, leading to Ruby exceptions.
187
+ return false if profile_name.nil? || example_profile_id.nil?
188
+
189
+ # The correct profile is one where the name of the profile, and the profile
190
+ # name in the example match. Additionally, the list of controls in the
191
+ # profile must contain the example in question (which we match by ID).
192
+ #
193
+ # While the profile name match is usually good enough, we must also match by
194
+ # the control ID in the case where an InSpec runner has multiple profiles of
195
+ # the same name (i.e. when Test Kitchen is running concurrently using a
196
+ # single test suite that uses the Flat source reader, in which case InSpec
197
+ # creates a fake profile with a name like "tests from /path/to/tests")
198
+ profile_name == example_profile_id && profile[:controls].any? { |control| control[:id] == example[:id] }
199
+ end
200
+
201
+ def move_example_into_control(example, control)
202
+ control[:results] ||= []
203
+ example.delete(:id)
204
+ example.delete(:profile_id)
205
+ control[:results].push(example)
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,20 @@
1
+ module Inspec::Formatters
2
+ class RspecJson < RSpec::Core::Formatters::JsonFormatter
3
+ RSpec::Core::Formatters.register self
4
+
5
+ private
6
+
7
+ # We are cheating and overriding a private method in RSpec's core JsonFormatter.
8
+ # This is to avoid having to repeat this id functionality in both dump_summary
9
+ # and dump_profile (both of which call format_example).
10
+ # See https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/formatters/json_formatter.rb
11
+ #
12
+ # rspec's example id here corresponds to an inspec test's control name -
13
+ # either explicitly specified or auto-generated by rspec itself.
14
+ def format_example(example)
15
+ res = super(example)
16
+ res[:id] = example.metadata[:id]
17
+ res
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ module Inspec::Formatters
2
+ class ShowProgress < RSpec::Core::Formatters::ProgressFormatter
3
+ RSpec::Core::Formatters.register self
4
+
5
+ # remove summary output from progress
6
+ %w{dump_summary dump_failures dump_pending message seed start_dump}.each do |m|
7
+ define_method(m) do |*args|
8
+ # override
9
+ end
10
+ end
11
+ end
12
+ end
@@ -4,6 +4,7 @@ module Inspec
4
4
  autoload :Attribute, 'inspec/objects/attribute'
5
5
  autoload :Tag, 'inspec/objects/tag'
6
6
  autoload :Control, 'inspec/objects/control'
7
+ autoload :Describe, 'inspec/objects/describe'
7
8
  autoload :EachLoop, 'inspec/objects/each_loop'
8
9
  autoload :List, 'inspec/objects/list'
9
10
  autoload :OrTest, 'inspec/objects/or_test'
@@ -0,0 +1,92 @@
1
+ # encoding:utf-8
2
+
3
+ module Inspec
4
+ class Describe
5
+ # Internal helper to structure test objects.
6
+ # Should not be exposed to the user as it is hidden behind
7
+ # `add_test`, `to_hash`, and `to_ruby` in Inspec::Describe
8
+ Test = Struct.new(:its, :matcher, :expectation, :negated) do
9
+ def negate!
10
+ self.negated = !negated
11
+ end
12
+
13
+ def to_ruby
14
+ itsy = its.nil? ? 'it' : 'its(' + its.to_s.inspect + ')'
15
+ naughty = negated ? '_not' : ''
16
+ xpect = if expectation.nil?
17
+ ''
18
+ elsif expectation.class == Regexp
19
+ # without this, xpect values like / \/zones\// will not be parsed properly
20
+ "(#{expectation.inspect})"
21
+ else
22
+ ' ' + expectation.inspect
23
+ end
24
+ format('%s { should%s %s%s }', itsy, naughty, matcher, xpect)
25
+ end
26
+ end
27
+
28
+ # A qualifier describing the resource that will be tested. It may consist
29
+ # of the resource identification and multiple accessors to pinpoint the data
30
+ # the user wants to test.
31
+ attr_accessor :qualifier
32
+
33
+ # An array of individual tests for the qualifier. Every entry will be
34
+ # translated into an `it` or `its` clause.
35
+ attr_accessor :tests
36
+
37
+ # Optional variables which are used by tests.
38
+ attr_accessor :variables
39
+
40
+ # Optional method to skip this describe block altogether. If `skip` is
41
+ # defined it takes precendence and will print the skip statement instead
42
+ # of adding other tests.
43
+ attr_accessor :skip
44
+
45
+ include RubyHelper
46
+
47
+ def initialize
48
+ @qualifier = []
49
+ @tests = []
50
+ @variables = []
51
+ end
52
+
53
+ def add_test(its, matcher, expectation)
54
+ test = Inspec::Describe::Test.new(its, matcher, expectation, false)
55
+ tests.push(test)
56
+ test
57
+ end
58
+
59
+ def to_ruby
60
+ return rb_skip if !skip.nil?
61
+ rb_describe
62
+ end
63
+
64
+ def to_hash
65
+ { qualifier: qualifier, tests: tests.map(&:to_h), variables: variables, skip: skip }
66
+ end
67
+
68
+ def resource
69
+ return nil if qualifier.empty? || qualifier[0].empty? || qualifier[0][0].empty?
70
+ qualifier[0][0]
71
+ end
72
+
73
+ private
74
+
75
+ def rb_describe
76
+ vars = variables.map(&:to_ruby).join("\n")
77
+ vars += "\n" unless vars.empty?
78
+
79
+ objarr = @qualifier
80
+ objarr = [['unknown object'.inspect]] if objarr.nil? || objarr.empty?
81
+ obj = objarr.map { |q| ruby_qualifier(q) }.join('.')
82
+
83
+ rbtests = tests.map(&:to_ruby).join("\n ")
84
+ format("%sdescribe %s do\n %s\nend", vars, obj, rbtests)
85
+ end
86
+
87
+ def rb_skip
88
+ obj = @qualifier || skip.inspect
89
+ format("describe %s do\n skip %s\nend", obj, skip.inspect)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,33 @@
1
+ require 'inspec/reporters/base'
2
+ require 'inspec/reporters/cli'
3
+ require 'inspec/reporters/json'
4
+ require 'inspec/reporters/json_min'
5
+ require 'inspec/reporters/junit'
6
+
7
+ module Inspec::Reporters
8
+ def self.render(reporter, run_data)
9
+ name, config = reporter
10
+ config[:run_data] = run_data
11
+ case name
12
+ when 'cli'
13
+ reporter = Inspec::Reporters::CLI.new(config)
14
+ when 'json'
15
+ reporter = Inspec::Reporters::Json.new(config)
16
+ when 'json-min'
17
+ reporter = Inspec::Reporters::JsonMin.new(config)
18
+ when 'junit'
19
+ reporter = Inspec::Reporters::Junit.new(config)
20
+ else
21
+ raise NotImplementedError, "'#{name}' is not a valid reporter type."
22
+ end
23
+
24
+ reporter.render
25
+ output = reporter.rendered_output
26
+
27
+ if config['file']
28
+ File.write(config['file'], output)
29
+ elsif config['stdout'] == true
30
+ puts output
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ module Inspec::Reporters
2
+ class Base
3
+ attr_reader :run_data
4
+
5
+ def initialize(config)
6
+ @run_data = config[:run_data]
7
+ @output = ''
8
+ end
9
+
10
+ def output(str)
11
+ @output << "#{str}\n"
12
+ end
13
+
14
+ def rendered_output
15
+ @output
16
+ end
17
+
18
+ # each reporter must implement #render
19
+ def render
20
+ raise NotImplementedError, "#{self.class} must implement a `#render` method to format its output."
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,395 @@
1
+ # encoding: utf-8
2
+
3
+ module Inspec::Reporters
4
+ class CLI < Base
5
+ case RUBY_PLATFORM
6
+ when /windows|mswin|msys|mingw|cygwin/
7
+ # Most currently available Windows terminals have poor support
8
+ # for ANSI extended colors
9
+ COLORS = {
10
+ 'critical' => "\033[0;1;31m",
11
+ 'major' => "\033[0;1;31m",
12
+ 'minor' => "\033[0;36m",
13
+ 'failed' => "\033[0;1;31m",
14
+ 'passed' => "\033[0;1;32m",
15
+ 'skipped' => "\033[0;37m",
16
+ 'reset' => "\033[0m",
17
+ }.freeze
18
+
19
+ # Most currently available Windows terminals have poor support
20
+ # for UTF-8 characters so use these boring indicators
21
+ INDICATORS = {
22
+ 'critical' => '[CRIT]',
23
+ 'major' => '[MAJR]',
24
+ 'minor' => '[MINR]',
25
+ 'failed' => '[FAIL]',
26
+ 'skipped' => '[SKIP]',
27
+ 'passed' => '[PASS]',
28
+ 'unknown' => '[UNKN]',
29
+ }.freeze
30
+ else
31
+ # Extended colors for everyone else
32
+ COLORS = {
33
+ 'critical' => "\033[38;5;9m",
34
+ 'major' => "\033[38;5;208m",
35
+ 'minor' => "\033[0;36m",
36
+ 'failed' => "\033[38;5;9m",
37
+ 'passed' => "\033[38;5;41m",
38
+ 'skipped' => "\033[38;5;247m",
39
+ 'reset' => "\033[0m",
40
+ }.freeze
41
+
42
+ # Groovy UTF-8 characters for everyone else...
43
+ # ...even though they probably only work on Mac
44
+ INDICATORS = {
45
+ 'critical' => '×',
46
+ 'major' => '∅',
47
+ 'minor' => '⊚',
48
+ 'failed' => '×',
49
+ 'skipped' => '↺',
50
+ 'passed' => '✔',
51
+ 'unknown' => '?',
52
+ }.freeze
53
+ end
54
+
55
+ MULTI_TEST_CONTROL_SUMMARY_MAX_LEN = 60
56
+
57
+ def render
58
+ run_data[:profiles].each do |profile|
59
+ @control_count = 0
60
+ output('')
61
+ print_profile_header(profile)
62
+ print_standard_control_results(profile)
63
+ print_anonymous_control_results(profile)
64
+ output(format_message(
65
+ indentation: 5,
66
+ message: 'No tests executed.',
67
+ )) if @control_count == 0
68
+ end
69
+
70
+ output('')
71
+ print_profile_summary
72
+ print_tests_summary
73
+ end
74
+
75
+ private
76
+
77
+ def print_profile_header(profile)
78
+ output("Profile: #{format_profile_name(profile)}")
79
+ output("Version: #{profile[:version] || '(not specified)'}")
80
+ output("Target: #{run_data[:platform][:target]}") unless run_data[:platform][:target].nil?
81
+ output('')
82
+ end
83
+
84
+ def print_standard_control_results(profile)
85
+ standard_controls_from_profile(profile).each do |control_from_profile|
86
+ control = Control.new(control_from_profile)
87
+ next if control.results.nil?
88
+ output(format_control_header(control))
89
+ control.results.each do |result|
90
+ output(format_result(control, result, :standard))
91
+ @control_count += 1
92
+ end
93
+ end
94
+ output('') if @control_count > 0
95
+ end
96
+
97
+ def print_anonymous_control_results(profile)
98
+ anonymous_controls_from_profile(profile).each do |control_from_profile|
99
+ control = Control.new(control_from_profile)
100
+ next if control.results.nil?
101
+ output(format_control_header(control))
102
+ control.results.each do |result|
103
+ output(format_result(control, result, :anonymous))
104
+ @control_count += 1
105
+ end
106
+ end
107
+ end
108
+
109
+ def format_profile_name(profile)
110
+ if profile[:title].nil?
111
+ (profile[:name] || 'unknown').to_s
112
+ else
113
+ "#{profile[:title]} (#{profile[:name] || 'unknown'})"
114
+ end
115
+ end
116
+
117
+ def format_control_header(control)
118
+ impact = control.impact_string
119
+ format_message(
120
+ color: impact,
121
+ indicator: impact,
122
+ message: control.title_for_report,
123
+ )
124
+ end
125
+
126
+ def format_result(control, result, type)
127
+ impact = control.impact_string_for_result(result)
128
+
129
+ message = if result[:status] == 'skipped'
130
+ result[:skip_message]
131
+ elsif type == :anonymous
132
+ result[:expectation_message]
133
+ else
134
+ result[:code_desc]
135
+ end
136
+
137
+ # append any failure details to the message if they exist
138
+ message += "\n#{result[:message]}" if result[:message]
139
+
140
+ format_message(
141
+ color: impact,
142
+ indicator: impact,
143
+ indentation: 5,
144
+ message: message,
145
+ )
146
+ end
147
+
148
+ def format_message(message_info)
149
+ indicator = message_info[:indicator]
150
+ color = message_info[:color]
151
+ indentation = message_info.fetch(:indentation, 2)
152
+ message = message_info[:message]
153
+
154
+ message_to_format = ''
155
+ message_to_format += "#{INDICATORS[indicator]} " unless indicator.nil?
156
+ message_to_format += message.to_s.lstrip
157
+
158
+ format_with_color(color, indent_lines(message_to_format, indentation))
159
+ end
160
+
161
+ def format_with_color(color_name, text)
162
+ return text if defined?(RSpec.configuration) && !RSpec.configuration.color
163
+ return text unless COLORS.key?(color_name)
164
+
165
+ "#{COLORS[color_name]}#{text}#{COLORS['reset']}"
166
+ end
167
+
168
+ def all_unique_controls
169
+ return @unique_controls unless @unique_controls.nil?
170
+
171
+ @unique_controls = Set.new
172
+ run_data[:profiles].each do |profile|
173
+ profile[:controls].map { |control| @unique_controls.add(control) }
174
+ end
175
+
176
+ @unique_controls
177
+ end
178
+
179
+ def profile_summary
180
+ return @profile_summary unless @profile_summary.nil?
181
+
182
+ failed = 0
183
+ skipped = 0
184
+ passed = 0
185
+ critical = 0
186
+ major = 0
187
+ minor = 0
188
+
189
+ all_unique_controls.each do |control|
190
+ next if control[:id].start_with? '(generated from '
191
+ next unless control[:results]
192
+ if control[:results].any? { |r| r[:status] == 'failed' }
193
+ failed += 1
194
+ if control[:impact] >= 0.7
195
+ critical += 1
196
+ elsif control[:impact] >= 0.4
197
+ major += 1
198
+ else
199
+ minor += 1
200
+ end
201
+ elsif control[:results].any? { |r| r[:status] == 'skipped' }
202
+ skipped += 1
203
+ else
204
+ passed += 1
205
+ end
206
+ end
207
+
208
+ total = failed + passed + skipped
209
+
210
+ @profile_summary = {
211
+ 'total' => total,
212
+ 'failed' => {
213
+ 'total' => failed,
214
+ 'critical' => critical,
215
+ 'major' => major,
216
+ 'minor' => minor,
217
+ },
218
+ 'skipped' => skipped,
219
+ 'passed' => passed,
220
+ }
221
+ end
222
+
223
+ def tests_summary
224
+ return @tests_summary unless @tests_summary.nil?
225
+
226
+ total = 0
227
+ failed = 0
228
+ skipped = 0
229
+ passed = 0
230
+
231
+ all_unique_controls.each do |control|
232
+ next unless control[:results]
233
+ control[:results].each do |result|
234
+ if result[:status] == 'failed'
235
+ failed += 1
236
+ elsif result[:status] == 'skipped'
237
+ skipped += 1
238
+ else
239
+ passed += 1
240
+ end
241
+ end
242
+ end
243
+
244
+ @tests_summary = { 'total' => total, 'failed' => failed, 'skipped' => skipped, 'passed' => passed }
245
+ end
246
+
247
+ def print_profile_summary
248
+ summary = profile_summary
249
+ return unless summary['total'] > 0
250
+
251
+ success_str = summary['passed'] == 1 ? '1 successful control' : "#{summary['passed']} successful controls"
252
+ failed_str = summary['failed']['total'] == 1 ? '1 control failure' : "#{summary['failed']['total']} control failures"
253
+ skipped_str = summary['skipped'] == 1 ? '1 control skipped' : "#{summary['skipped']} controls skipped"
254
+
255
+ success_color = summary['passed'] > 0 ? 'passed' : 'no_color'
256
+ failed_color = summary['failed']['total'] > 0 ? 'failed' : 'no_color'
257
+ skipped_color = summary['skipped'] > 0 ? 'skipped' : 'no_color'
258
+
259
+ s = format(
260
+ 'Profile Summary: %s, %s, %s',
261
+ format_with_color(success_color, success_str),
262
+ format_with_color(failed_color, failed_str),
263
+ format_with_color(skipped_color, skipped_str),
264
+ )
265
+ output(s) if summary['total'] > 0
266
+ end
267
+
268
+ def print_tests_summary
269
+ summary = tests_summary
270
+
271
+ failed_str = summary['failed'] == 1 ? '1 failure' : "#{summary['failed']} failures"
272
+
273
+ success_color = summary['passed'] > 0 ? 'passed' : 'no_color'
274
+ failed_color = summary['failed'] > 0 ? 'failed' : 'no_color'
275
+ skipped_color = summary['skipped'] > 0 ? 'skipped' : 'no_color'
276
+
277
+ s = format(
278
+ 'Test Summary: %s, %s, %s',
279
+ format_with_color(success_color, "#{summary['passed']} successful"),
280
+ format_with_color(failed_color, failed_str),
281
+ format_with_color(skipped_color, "#{summary['skipped']} skipped"),
282
+ )
283
+
284
+ output(s)
285
+ end
286
+
287
+ def standard_controls_from_profile(profile)
288
+ profile[:controls].reject { |c| is_anonymous_control?(c) }
289
+ end
290
+
291
+ def anonymous_controls_from_profile(profile)
292
+ profile[:controls].select { |c| is_anonymous_control?(c) && !c[:results].nil? }
293
+ end
294
+
295
+ def is_anonymous_control?(control)
296
+ control[:id].start_with?('(generated from ')
297
+ end
298
+
299
+ def indent_lines(message, indentation)
300
+ message.lines.map { |line| ' ' * indentation + line }.join
301
+ end
302
+
303
+ class Control
304
+ IMPACT_SCORES = {
305
+ critical: 0.7,
306
+ major: 0.4,
307
+ }.freeze
308
+
309
+ attr_reader :data
310
+
311
+ def initialize(control_hash)
312
+ @data = control_hash
313
+ end
314
+
315
+ def id
316
+ data[:id]
317
+ end
318
+
319
+ def title
320
+ data[:title]
321
+ end
322
+
323
+ def results
324
+ data[:results]
325
+ end
326
+
327
+ def impact
328
+ data[:impact]
329
+ end
330
+
331
+ def anonymous?
332
+ id.start_with?('(generated from ')
333
+ end
334
+
335
+ def title_for_report
336
+ # if this is an anonymous control, just grab the resource title from any result entry
337
+ return results.first[:resource_title] if anonymous?
338
+
339
+ title_for_report = "#{id}: #{title || results.first[:resource_title]}"
340
+
341
+ # we will not add any additional data to the title if there's only
342
+ # zero or one test for this control.
343
+ return title_for_report if results.nil? || results.size <= 1
344
+
345
+ # append a failure summary if appropriate.
346
+ title_for_report += " (#{failure_count} failed)" if failure_count > 0
347
+ title_for_report += " (#{skipped_count} skipped)" if skipped_count > 0
348
+
349
+ title_for_report
350
+ end
351
+
352
+ def impact_string
353
+ if anonymous?
354
+ nil
355
+ elsif impact.nil?
356
+ 'unknown'
357
+ elsif results&.find { |r| r[:status] == 'skipped' }
358
+ 'skipped'
359
+ elsif results.nil? || results.empty? || results.all? { |r| r[:status] == 'passed' }
360
+ 'passed'
361
+ elsif impact >= IMPACT_SCORES[:critical]
362
+ 'critical'
363
+ elsif impact >= IMPACT_SCORES[:major]
364
+ 'major'
365
+ else
366
+ 'minor'
367
+ end
368
+ end
369
+
370
+ def impact_string_for_result(result)
371
+ if result[:status] == 'skipped'
372
+ 'skipped'
373
+ elsif result[:status] == 'passed'
374
+ 'passed'
375
+ elsif impact.nil?
376
+ 'unknown'
377
+ elsif impact >= IMPACT_SCORES[:critical]
378
+ 'critical'
379
+ elsif impact >= IMPACT_SCORES[:major]
380
+ 'major'
381
+ else
382
+ 'minor'
383
+ end
384
+ end
385
+
386
+ def failure_count
387
+ results.select { |r| r[:status] == 'failed' }.size
388
+ end
389
+
390
+ def skipped_count
391
+ results.select { |r| r[:status] == 'skipped' }.size
392
+ end
393
+ end
394
+ end
395
+ end