inspec 1.51.0 → 1.51.6

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