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
@@ -26,7 +26,7 @@
26
26
  module Inspec::Resources
27
27
  class AuditPolicy < Inspec.resource(1)
28
28
  name 'audit_policy'
29
- desc 'Use the audit_policy InSpec audit resource to test auditing policies on the Microsoft Windows platform. An auditing policy is a category of security-related events to be audited. Auditing is disabled by default and may be enabled for categories like account management, logon events, policy changes, process tracking, privilege use, system events, or object access. For each auditing category property that is enabled, the auditing level may be set to No Auditing, Not Specified, Success, Success and Failure, or Failure.'
29
+ desc 'Use the audit_policy InSpec audit resource to test auditing policies on the Microsoft Windows platform. An auditing policy is a category of security-related events to be audited. Auditing is disabled by default and may be enabled for categories like account management, logon events, policy changes, process tracking, privilege use, system events, or object access. For each enabled auditing category property, the auditing level may be set to No Auditing, Not Specified, Success, Success and Failure, or Failure.'
30
30
  example "
31
31
  describe audit_policy do
32
32
  its('parameter') { should eq 'value' }
@@ -48,6 +48,7 @@ module Inspec::Resources
48
48
  .add(:statuses, field: 'status', style: :simple)
49
49
  .add(:names, field: 'name')
50
50
  .add(:versions, field: 'version')
51
+ .add(:architectures, field: 'architecture')
51
52
  .connect(self, :filtered_packages)
52
53
 
53
54
  private
@@ -69,7 +70,7 @@ module Inspec::Resources
69
70
  end
70
71
 
71
72
  class PkgsManagement
72
- PackageStruct = Struct.new(:status, :name, :version)
73
+ PackageStruct = Struct.new(:status, :name, :version, :architecture)
73
74
  attr_reader :inspec
74
75
  def initialize(inspec)
75
76
  @inspec = inspec
@@ -80,7 +81,7 @@ module Inspec::Resources
80
81
  class Debs < PkgsManagement
81
82
  def build_package_list
82
83
  # use two spaces as delimiter in case any of the fields has a space in it
83
- command = "dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\\n'"
84
+ command = "dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version} ${Architecture}\\n'"
84
85
  cmd = inspec.command(command)
85
86
  all = cmd.stdout.split("\n")
86
87
  return [] if all.nil?
@@ -97,7 +98,7 @@ module Inspec::Resources
97
98
  class Rpms < PkgsManagement
98
99
  def build_package_list
99
100
  # use two spaces as delimiter in case any of the fields has a space in it
100
- command = "rpm -qa --queryformat '%{NAME} %{VERSION}-%{RELEASE}\\n'" # rubocop:disable Style/FormatStringToken
101
+ command = "rpm -qa --queryformat '%{NAME} %{VERSION}-%{RELEASE} %{ARCH}\\n'" # rubocop:disable Style/FormatStringToken
101
102
  cmd = inspec.command(command)
102
103
  all = cmd.stdout.split("\n")
103
104
  return [] if all.nil?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.51.0
4
+ version: 1.51.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dominik Richter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-25 00:00:00.000000000 Z
11
+ date: 2018-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: train
@@ -292,6 +292,7 @@ files:
292
292
  - docs/README.md
293
293
  - docs/dsl_inspec.md
294
294
  - docs/dsl_resource.md
295
+ - docs/glossary.md
295
296
  - docs/habitat.md
296
297
  - docs/inspec_and_friends.md
297
298
  - docs/matchers.md
@@ -299,6 +300,7 @@ files:
299
300
  - docs/plugin_kitchen_inspec.md
300
301
  - docs/profiles.md
301
302
  - docs/resources/aide_conf.md.erb
303
+ - docs/resources/apache.md.erb
302
304
  - docs/resources/apache_conf.md.erb
303
305
  - docs/resources/apt.md.erb
304
306
  - docs/resources/audit_policy.md.erb
@@ -314,7 +316,7 @@ files:
314
316
  - docs/resources/cran.md.erb
315
317
  - docs/resources/crontab.md.erb
316
318
  - docs/resources/csv.md.erb
317
- - docs/resources/dh_params.md
319
+ - docs/resources/dh_params.md.erb
318
320
  - docs/resources/directory.md.erb
319
321
  - docs/resources/docker.md.erb
320
322
  - docs/resources/docker_container.md.erb
@@ -360,6 +362,7 @@ files:
360
362
  - docs/resources/os.md.erb
361
363
  - docs/resources/os_env.md.erb
362
364
  - docs/resources/package.md.erb
365
+ - docs/resources/packages.md.erb
363
366
  - docs/resources/parse_config.md.erb
364
367
  - docs/resources/parse_config_file.md.erb
365
368
  - docs/resources/passwd.md.erb
@@ -512,6 +515,10 @@ files:
512
515
  - lib/inspec/expect.rb
513
516
  - lib/inspec/fetcher.rb
514
517
  - lib/inspec/file_provider.rb
518
+ - lib/inspec/formatters.rb
519
+ - lib/inspec/formatters/base.rb
520
+ - lib/inspec/formatters/json_rspec.rb
521
+ - lib/inspec/formatters/show_progress.rb
515
522
  - lib/inspec/library_eval_context.rb
516
523
  - lib/inspec/log.rb
517
524
  - lib/inspec/metadata.rb
@@ -519,6 +526,7 @@ files:
519
526
  - lib/inspec/objects.rb
520
527
  - lib/inspec/objects/attribute.rb
521
528
  - lib/inspec/objects/control.rb
529
+ - lib/inspec/objects/describe.rb
522
530
  - lib/inspec/objects/each_loop.rb
523
531
  - lib/inspec/objects/list.rb
524
532
  - lib/inspec/objects/or_test.rb
@@ -536,9 +544,14 @@ files:
536
544
  - lib/inspec/profile.rb
537
545
  - lib/inspec/profile_context.rb
538
546
  - lib/inspec/profile_vendor.rb
547
+ - lib/inspec/reporters.rb
548
+ - lib/inspec/reporters/base.rb
549
+ - lib/inspec/reporters/cli.rb
550
+ - lib/inspec/reporters/json.rb
551
+ - lib/inspec/reporters/json_min.rb
552
+ - lib/inspec/reporters/junit.rb
539
553
  - lib/inspec/require_loader.rb
540
554
  - lib/inspec/resource.rb
541
- - lib/inspec/rspec_json_formatter.rb
542
555
  - lib/inspec/rule.rb
543
556
  - lib/inspec/runner.rb
544
557
  - lib/inspec/runner_mock.rb
@@ -1,940 +0,0 @@
1
- # encoding: utf-8
2
- # author: Dominik Richter
3
- # author: Christoph Hartmann
4
- # author: John Kerry
5
-
6
- require 'rspec/core'
7
- require 'rspec/core/formatters/json_formatter'
8
-
9
- # Vanilla RSpec JSON formatter with a slight extension to show example IDs.
10
- # TODO: Remove these lines when RSpec includes the ID natively
11
- class InspecRspecVanilla < RSpec::Core::Formatters::JsonFormatter
12
- RSpec::Core::Formatters.register self
13
-
14
- private
15
-
16
- # We are cheating and overriding a private method in RSpec's core JsonFormatter.
17
- # This is to avoid having to repeat this id functionality in both dump_summary
18
- # and dump_profile (both of which call format_example).
19
- # See https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/formatters/json_formatter.rb
20
- #
21
- # rspec's example id here corresponds to an inspec test's control name -
22
- # either explicitly specified or auto-generated by rspec itself.
23
- def format_example(example)
24
- res = super(example)
25
- res[:id] = example.metadata[:id]
26
- res
27
- end
28
- end
29
-
30
- # Minimal JSON formatter for inspec. Only contains limited information about
31
- # examples without any extras.
32
- class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
33
- # Don't re-register all the call-backs over and over - we automatically
34
- # inherit all callbacks registered by the parent class.
35
- RSpec::Core::Formatters.register self, :dump_summary, :stop
36
-
37
- # Called after stop has been called and the run is complete.
38
- def dump_summary(summary)
39
- @output_hash[:version] = Inspec::VERSION
40
- @output_hash[:statistics] = {
41
- duration: summary.duration,
42
- }
43
- end
44
-
45
- # Called at the end of a complete RSpec run.
46
- def stop(notification)
47
- # This might be a bit confusing. The results are not actually organized
48
- # by control. It is organized by test. So if a control has 3 tests, the
49
- # output will have 3 control entries, each one with the same control id
50
- # and different test results. An rspec example maps to an inspec test.
51
- @output_hash[:controls] = notification.examples.map do |example|
52
- format_example(example).tap do |hash|
53
- e = example.exception
54
- next unless e
55
-
56
- if example.metadata[:sensitive]
57
- hash[:message] = '*** sensitive output suppressed ***'
58
- else
59
- hash[:message] = exception_message(e)
60
- end
61
-
62
- next if e.is_a? RSpec::Expectations::ExpectationNotMetError
63
- hash[:exception] = e.class.name
64
- hash[:backtrace] = e.backtrace
65
-
66
- # if the exception indicates the resource author wants to skip the test,
67
- # we update the test status here.
68
- hash[:status] = 'skipped' if e.is_a?(Inspec::Exceptions::ResourceSkipped)
69
- end
70
- end
71
- end
72
-
73
- private
74
-
75
- def exception_message(exception)
76
- if exception.is_a?(RSpec::Core::MultipleExceptionError)
77
- exception.all_exceptions.map(&:message).uniq.join("\n\n")
78
- else
79
- exception.message
80
- end
81
- end
82
-
83
- def format_example(example)
84
- if !example.metadata[:description_args].empty? && example.metadata[:skip]
85
- # For skipped profiles, rspec returns in full_description the skip_message as well. We don't want
86
- # to mix the two, so we pick the full_description from the example.metadata[:example_group] hash.
87
- code_description = example.metadata[:example_group][:description]
88
- else
89
- code_description = example.metadata[:full_description]
90
- end
91
-
92
- res = {
93
- id: example.metadata[:id],
94
- profile_id: example.metadata[:profile_id],
95
- status: example.execution_result.status.to_s,
96
- code_desc: code_description,
97
- }
98
-
99
- unless (pid = example.metadata[:profile_id]).nil?
100
- res[:profile_id] = pid
101
- end
102
-
103
- if res[:status] == 'pending'
104
- res[:status] = 'skipped'
105
- res[:skip_message] = example.metadata[:description]
106
- res[:resource] = example.metadata[:described_class].to_s
107
- end
108
-
109
- res
110
- end
111
- end
112
-
113
- class InspecRspecJson < InspecRspecMiniJson
114
- RSpec::Core::Formatters.register self, :stop, :dump_summary
115
- attr_writer :backend
116
-
117
- def initialize(*args)
118
- super(*args)
119
- @profiles = []
120
- @profiles_info = nil
121
- @backend = nil
122
- end
123
-
124
- # Called by the runner during example collection.
125
- def add_profile(profile)
126
- @profiles.push(profile)
127
- end
128
-
129
- def stop(notification)
130
- super(notification)
131
-
132
- @output_hash[:other_checks] = examples_without_controls
133
- @output_hash[:profiles] = profiles_info
134
- @output_hash[:platform] = {
135
- name: os(:name),
136
- release: os(:release),
137
- }
138
-
139
- examples_with_controls.each do |example|
140
- control = example2control(example)
141
- move_example_into_control(example, control)
142
- end
143
- end
144
-
145
- private
146
-
147
- def os(field)
148
- return nil if @backend.nil?
149
- @backend.os.params[field]
150
- end
151
-
152
- def all_unique_controls
153
- Array(@all_controls).uniq
154
- end
155
-
156
- def profile_summary
157
- failed = 0
158
- skipped = 0
159
- passed = 0
160
- critical = 0
161
- major = 0
162
- minor = 0
163
-
164
- all_unique_controls.each do |control|
165
- next if control[:id].start_with? '(generated from '
166
- next unless control[:results]
167
- if control[:results].any? { |r| r[:status] == 'failed' }
168
- failed += 1
169
- if control[:impact] >= 0.7
170
- critical += 1
171
- elsif control[:impact] >= 0.4
172
- major += 1
173
- else
174
- minor += 1
175
- end
176
- elsif control[:results].any? { |r| r[:status] == 'skipped' }
177
- skipped += 1
178
- else
179
- passed += 1
180
- end
181
- end
182
-
183
- total = failed + passed + skipped
184
-
185
- { 'total' => total,
186
- 'failed' => {
187
- 'total' => failed,
188
- 'critical' => critical,
189
- 'major' => major,
190
- 'minor' => minor,
191
- },
192
- 'skipped' => skipped,
193
- 'passed' => passed }
194
- end
195
-
196
- def tests_summary
197
- total = 0
198
- failed = 0
199
- skipped = 0
200
- passed = 0
201
-
202
- all_unique_controls.each do |control|
203
- next unless control[:results]
204
- control[:results].each do |result|
205
- if result[:status] == 'failed'
206
- failed += 1
207
- elsif result[:status] == 'skipped'
208
- skipped += 1
209
- else
210
- passed += 1
211
- end
212
- end
213
- end
214
-
215
- { 'total' => total, 'failed' => failed, 'skipped' => skipped, 'passed' => passed }
216
- end
217
-
218
- def examples
219
- @output_hash[:controls]
220
- end
221
-
222
- def examples_without_controls
223
- examples.find_all { |example| example2control(example).nil? }
224
- end
225
-
226
- def examples_with_controls
227
- (examples - examples_without_controls)
228
- end
229
-
230
- def profiles_info
231
- @profiles_info ||= @profiles.map(&:info!).map(&:dup)
232
- end
233
-
234
- def example2control(example)
235
- profile = profile_from_example(example)
236
- return nil unless profile && profile[:controls]
237
- profile[:controls].find { |x| x[:id] == example[:id] }
238
- end
239
-
240
- def profile_from_example(example)
241
- profiles_info.find { |p| profile_contains_example?(p, example) }
242
- end
243
-
244
- def profile_contains_example?(profile, example)
245
- profile_name = profile[:name]
246
- example_profile_id = example[:profile_id]
247
-
248
- # if either the profile name is nil or the profile in the given example
249
- # is nil, assume the profile doesn't contain the example and default
250
- # to creating a new profile. Otherwise, for profiles that have no
251
- # metadata, this may incorrectly match a profile that does not contain
252
- # this example, leading to Ruby exceptions.
253
- return false if profile_name.nil? || example_profile_id.nil?
254
-
255
- # The correct profile is one where the name of the profile, and the profile
256
- # name in the example match. Additionally, the list of controls in the
257
- # profile must contain the example in question (which we match by ID).
258
- #
259
- # While the profile name match is usually good enough, we must also match by
260
- # the control ID in the case where an InSpec runner has multiple profiles of
261
- # the same name (i.e. when Test Kitchen is running concurrently using a
262
- # single test suite that uses the Flat source reader, in which case InSpec
263
- # creates a fake profile with a name like "tests from /path/to/tests")
264
- profile_name == example_profile_id && profile[:controls].any? { |control| control[:id] == example[:id] }
265
- end
266
-
267
- def move_example_into_control(example, control)
268
- control[:results] ||= []
269
- example.delete(:id)
270
- example.delete(:profile_id)
271
- control[:results].push(example)
272
- end
273
-
274
- def format_example(example)
275
- super(example).tap do |res|
276
- res[:run_time] = example.execution_result.run_time
277
- res[:start_time] = example.execution_result.started_at.to_s
278
- end
279
- end
280
- end
281
-
282
- class InspecRspecCli < InspecRspecJson
283
- RSpec::Core::Formatters.register self, :close
284
-
285
- case RUBY_PLATFORM
286
- when /windows|mswin|msys|mingw|cygwin/
287
-
288
- # Most currently available Windows terminals have poor support
289
- # for ANSI extended colors
290
- COLORS = {
291
- 'critical' => "\033[0;1;31m",
292
- 'major' => "\033[0;1;31m",
293
- 'minor' => "\033[0;36m",
294
- 'failed' => "\033[0;1;31m",
295
- 'passed' => "\033[0;1;32m",
296
- 'skipped' => "\033[0;37m",
297
- 'reset' => "\033[0m",
298
- }.freeze
299
-
300
- # Most currently available Windows terminals have poor support
301
- # for UTF-8 characters so use these boring indicators
302
- INDICATORS = {
303
- 'critical' => ' [CRIT] ',
304
- 'major' => ' [MAJR] ',
305
- 'minor' => ' [MINR] ',
306
- 'failed' => ' [FAIL] ',
307
- 'skipped' => ' [SKIP] ',
308
- 'passed' => ' [PASS] ',
309
- 'unknown' => ' [UNKN] ',
310
- 'empty' => ' ',
311
- 'small' => ' ',
312
- }.freeze
313
- else
314
- # Extended colors for everyone else
315
- COLORS = {
316
- 'critical' => "\033[38;5;9m",
317
- 'major' => "\033[38;5;208m",
318
- 'minor' => "\033[0;36m",
319
- 'failed' => "\033[38;5;9m",
320
- 'passed' => "\033[38;5;41m",
321
- 'skipped' => "\033[38;5;247m",
322
- 'reset' => "\033[0m",
323
- }.freeze
324
-
325
- # Groovy UTF-8 characters for everyone else...
326
- # ...even though they probably only work on Mac
327
- INDICATORS = {
328
- 'critical' => ' × ',
329
- 'major' => ' ∅ ',
330
- 'minor' => ' ⊚ ',
331
- 'failed' => ' × ',
332
- 'skipped' => ' ↺ ',
333
- 'passed' => ' ✔ ',
334
- 'unknown' => ' ? ',
335
- 'empty' => ' ',
336
- 'small' => ' ',
337
- }.freeze
338
- end
339
-
340
- MULTI_TEST_CONTROL_SUMMARY_MAX_LEN = 60
341
-
342
- def initialize(*args)
343
- @current_control = nil
344
- @all_controls = []
345
- @profile_printed = false
346
- super(*args)
347
- end
348
-
349
- #
350
- # This method is called through the RSpec Formatter interface for every
351
- # example found in the test suite.
352
- #
353
- # Within #format_example we are getting an example and:
354
- # * if this is an example, within a control, within a profile then we want
355
- # to display the profile header, display the control, and then display
356
- # the example.
357
- # * if this is another example, within the same control, within the same
358
- # profile we want to display the example.
359
- # * if this is an example that does not map to a control (anonymous) then
360
- # we want to store it for later to displayed at the end of a profile.
361
- #
362
- def format_example(example)
363
- example_data = super(example)
364
- control = create_or_find_control(example_data)
365
-
366
- # If we are switching to a new control then we want to print the control
367
- # we were previously collecting examples unless the last control is
368
- # anonymous (no control). Anonymous controls and their examples are handled
369
- # later on the profile change.
370
-
371
- if switching_to_new_control?(control)
372
- print_last_control_with_examples unless last_control_is_anonymous?
373
- end
374
-
375
- store_last_control(control)
376
-
377
- # Each profile may have zero or more anonymous examples. These are examples
378
- # that defined in a profile but outside of a control. They may be defined
379
- # at the start, in-between, or end of list of examples. To display them
380
- # at the very end of a profile, which means we have to wait for the profile
381
- # to change to know we are done with a profile.
382
-
383
- if switching_to_new_profile?(control.profile)
384
- output.puts ''
385
- print_anonymous_examples_associated_with_last_profile
386
- clear_anonymous_examples_associated_with_last_profile
387
- end
388
-
389
- print_profile(control.profile)
390
- store_last_profile(control.profile)
391
-
392
- # The anonymous controls should be added to a hash that we will display
393
- # when we are done examining all the examples within this profile.
394
-
395
- if control.anonymous?
396
- add_anonymous_example_within_this_profile(control.as_hash)
397
- end
398
-
399
- @all_controls.push(control.as_hash)
400
- example_data
401
- end
402
-
403
- #
404
- # This is the last method is invoked through the formatter interface.
405
- # Because the profile
406
- # we may have some remaining anonymous examples so we want to display them
407
- # as well as a summary of the profile and test stats.
408
- #
409
- def close(_notification)
410
- # when the profile has no controls or examples it will not have been printed.
411
- # then we want to ensure we print all the profiles
412
- print_last_control_with_examples unless last_control_is_anonymous?
413
- output.puts ''
414
- print_anonymous_examples_associated_with_last_profile
415
- print_profiles_without_examples
416
- print_profile_summary
417
- print_tests_summary
418
- end
419
-
420
- private
421
-
422
- #
423
- # With the example we can find the profile associated with it and if there
424
- # is already a control defined. If there is one then we will use that data
425
- # to build our control object. If there isn't we simply create a new hash of
426
- # controld data that will be populated from the examples that are found.
427
- #
428
- # @return [Control] A new control or one found associated with the example.
429
- #
430
- def create_or_find_control(example)
431
- profile = profile_from_example(example)
432
-
433
- control_data = {}
434
-
435
- if profile && profile[:controls]
436
- control_data = profile[:controls].find { |ctrl| ctrl[:id] == example[:id] }
437
- end
438
-
439
- control = Control.new(control_data, profile)
440
- control.add_example(example)
441
-
442
- control
443
- end
444
-
445
- #
446
- # If there is already a control we have have seen before and it is different
447
- # than the new control then we are indeed switching controls.
448
- #
449
- def switching_to_new_control?(control)
450
- @last_control && @last_control != control
451
- end
452
-
453
- def store_last_control(control)
454
- @last_control = control
455
- end
456
-
457
- def print_last_control_with_examples
458
- return unless @last_control
459
-
460
- print_control(@last_control)
461
- @last_control.examples.each { |example| print_result(example) }
462
- end
463
-
464
- def last_control_is_anonymous?
465
- @last_control && @last_control.anonymous?
466
- end
467
-
468
- #
469
- # If there is a profile we have seen before and it is different than the
470
- # new profile then we are indeed switching profiles.
471
- #
472
- def switching_to_new_profile?(new_profile)
473
- @last_profile && @last_profile != new_profile
474
- end
475
-
476
- #
477
- # Print all the anonymous examples that have been found for this profile
478
- #
479
- def print_anonymous_examples_associated_with_last_profile
480
- Array(anonymous_examples_within_this_profile).uniq.each do |control|
481
- print_anonymous_control(control)
482
- end
483
- output.puts '' unless Array(anonymous_examples_within_this_profile).empty?
484
- end
485
-
486
- #
487
- # As we process examples we need an accumulator that will allow us to store
488
- # all the examples that do not have a named control associated with them.
489
- #
490
- def anonymous_examples_within_this_profile
491
- @anonymous_examples_within_this_profile ||= []
492
- end
493
-
494
- #
495
- # Remove all controls from the anonymous examples that are tracked.
496
- #
497
- def clear_anonymous_examples_associated_with_last_profile
498
- @anonymous_examples_within_this_profile = []
499
- end
500
-
501
- #
502
- # Append a new control to the anonymous examples
503
- #
504
- def add_anonymous_example_within_this_profile(control)
505
- anonymous_examples_within_this_profile.push(control)
506
- end
507
-
508
- def store_last_profile(new_profile)
509
- @last_profile = new_profile
510
- end
511
-
512
- #
513
- # Print the profile
514
- #
515
- # * For anonymous profiles, where are generated for examples and controls
516
- # defined outside of a profile, simply display the target information
517
- # * For profiles without a title use the name (or 'unknown'), version,
518
- # and target information.
519
- # * For all other profiles display the title with name (or 'unknown'),
520
- # version, and target information.
521
- #
522
- def print_profile(profile)
523
- return if profile.nil? || profile[:already_printed]
524
- output.puts ''
525
-
526
- if profile[:name].nil?
527
- print_target
528
- profile[:already_printed] = true
529
- return
530
- end
531
-
532
- if profile[:title].nil?
533
- output.puts "Profile: #{profile[:name] || 'unknown'}"
534
- else
535
- output.puts "Profile: #{profile[:title]} (#{profile[:name] || 'unknown'})"
536
- end
537
-
538
- output.puts 'Version: ' + (profile[:version] || '(not specified)')
539
- print_target
540
- profile[:already_printed] = true
541
- end
542
-
543
- def print_profiles_without_examples
544
- profiles_info.reject { |p| p[:already_printed] }.each do |profile|
545
- print_profile(profile)
546
- print_line(
547
- color: '', indicator: INDICATORS['empty'], id: '', profile: '',
548
- summary: 'No tests executed.'
549
- )
550
- output.puts ''
551
- end
552
- end
553
-
554
- #
555
- # This target information displays which system that came under test
556
- #
557
- def print_target
558
- return if @backend.nil?
559
- connection = @backend.backend
560
- return unless connection.respond_to?(:uri)
561
- output.puts('Target: ' + connection.uri + "\n\n")
562
- end
563
-
564
- #
565
- # We want to print the details about the control
566
- #
567
- def print_control(control)
568
- print_line(
569
- color: control.summary_indicator,
570
- indicator: INDICATORS[control.summary_indicator] || INDICATORS['unknown'],
571
- summary: format_lines(control.summary, INDICATORS['empty']),
572
- id: "#{control.id}: ",
573
- profile: control.profile_id,
574
- )
575
- end
576
-
577
- def print_result(result)
578
- test_skipped = result[:status] == 'skipped'
579
- test_status = test_skipped ? 'skipped' : result[:status_type]
580
- indicator = INDICATORS[result[:status]]
581
- indicator = INDICATORS['empty'] if indicator.nil?
582
- if result[:message]
583
- msg = result[:code_desc] + "\n" + result[:message]
584
- else
585
- msg = result[:skip_message] || result[:code_desc]
586
- end
587
- print_line(
588
- color: test_status,
589
- indicator: INDICATORS['small'] + indicator,
590
- summary: format_lines(msg, INDICATORS['empty']),
591
- id: nil, profile: nil
592
- )
593
- end
594
-
595
- def print_anonymous_control(control)
596
- control_result = control[:results]
597
- title = control_result[0][:code_desc].split[0..1].join(' ')
598
- puts ' ' + title
599
- # iterate over all describe blocks in anonoymous control block
600
- control_result.each do |test|
601
- control_id = ''
602
- # display exceptions
603
- unless test[:exception].nil?
604
- test_result = test[:message]
605
- else
606
- # determine title
607
- test_result = test[:skip_message] || test[:code_desc].split[2..-1].join(' ')
608
- # show error message
609
- test_result += "\n" + test[:message] unless test[:message].nil?
610
- end
611
- status_indicator = test[:status_type]
612
- print_line(
613
- color: status_indicator,
614
- indicator: INDICATORS['small'] + INDICATORS[status_indicator] || INDICATORS['unknown'],
615
- summary: format_lines(test_result, INDICATORS['empty']),
616
- id: control_id,
617
- profile: control[:profile_id],
618
- )
619
- end
620
- end
621
-
622
- def print_profile_summary
623
- summary = profile_summary
624
- return unless summary['total'] > 0
625
-
626
- success_str = summary['passed'] == 1 ? '1 successful control' : "#{summary['passed']} successful controls"
627
- failed_str = summary['failed']['total'] == 1 ? '1 control failure' : "#{summary['failed']['total']} control failures"
628
- skipped_str = summary['skipped'] == 1 ? '1 control skipped' : "#{summary['skipped']} controls skipped"
629
-
630
- success_color = summary['passed'] > 0 ? 'passed' : 'no_color'
631
- failed_color = summary['failed']['total'] > 0 ? 'failed' : 'no_color'
632
- skipped_color = summary['skipped'] > 0 ? 'skipped' : 'no_color'
633
-
634
- s = format('Profile Summary: %s, %s, %s',
635
- format_with_color(success_color, success_str),
636
- format_with_color(failed_color, failed_str),
637
- format_with_color(skipped_color, skipped_str))
638
- output.puts(s) if summary['total'] > 0
639
- end
640
-
641
- def print_tests_summary
642
- summary = tests_summary
643
-
644
- failed_str = summary['failed'] == 1 ? '1 failure' : "#{summary['failed']} failures"
645
-
646
- success_color = summary['passed'] > 0 ? 'passed' : 'no_color'
647
- failed_color = summary['failed'] > 0 ? 'failed' : 'no_color'
648
- skipped_color = summary['skipped'] > 0 ? 'skipped' : 'no_color'
649
-
650
- s = format('Test Summary: %s, %s, %s',
651
- format_with_color(success_color, "#{summary['passed']} successful"),
652
- format_with_color(failed_color, failed_str),
653
- format_with_color(skipped_color, "#{summary['skipped']} skipped"))
654
-
655
- output.puts(s)
656
- end
657
-
658
- # Formats the line (called from print_line)
659
- def format_line(fields)
660
- format = '%indicator%id%summary'
661
- format.gsub(/%\w+/) do |x|
662
- term = x[1..-1]
663
- fields.key?(term.to_sym) ? fields[term.to_sym].to_s : x
664
- end
665
- end
666
-
667
- # Prints line; used to print results
668
- def print_line(fields)
669
- output.puts(format_with_color(fields[:color], format_line(fields)))
670
- end
671
-
672
- # Helps formatting summary lines (called from within print_line arguments)
673
- def format_lines(lines, indentation)
674
- lines.gsub(/\n/, "\n" + indentation)
675
- end
676
-
677
- def format_with_color(color_name, text)
678
- return text unless RSpec.configuration.color
679
- return text unless COLORS.key?(color_name)
680
-
681
- "#{COLORS[color_name]}#{text}#{COLORS['reset']}"
682
- end
683
-
684
- #
685
- # This class wraps a control hash object to provide a useful inteface for
686
- # maintaining the associated profile, ids, results, title, etc.
687
- #
688
- class Control
689
- include Comparable
690
-
691
- STATUS_TYPES = {
692
- 'unknown' => -3,
693
- 'passed' => -2,
694
- 'skipped' => -1,
695
- 'minor' => 1,
696
- 'major' => 2,
697
- 'failed' => 2.5,
698
- 'critical' => 3,
699
- }.freeze
700
-
701
- def initialize(control, profile)
702
- @control = control
703
- @profile = profile
704
- summary_calculation_is_needed
705
- end
706
-
707
- attr_reader :control, :profile
708
-
709
- alias as_hash control
710
-
711
- def id
712
- control[:id]
713
- end
714
-
715
- def anonymous?
716
- control[:id].to_s.start_with? '(generated from '
717
- end
718
-
719
- def profile_id
720
- control[:profile_id]
721
- end
722
-
723
- def examples
724
- control[:results]
725
- end
726
-
727
- def summary_indicator
728
- calculate_summary! if summary_calculation_needed?
729
- STATUS_TYPES.key(@summary_status)
730
- end
731
-
732
- def add_example(example)
733
- control[:id] = example[:id]
734
- control[:profile_id] = example[:profile_id]
735
-
736
- example[:status_type] = status_type(example)
737
- example.delete(:id)
738
- example.delete(:profile_id)
739
-
740
- control[:results] ||= []
741
- control[:results].push(example)
742
- summary_calculation_is_needed
743
- end
744
-
745
- # Determine title for control given current_control.
746
- # Called from current_control_summary.
747
- def title
748
- title = control[:title]
749
- if title
750
- title
751
- elsif examples.length == 1
752
- # If it's an anonymous control, just go with the only description
753
- # available for the underlying test.
754
- examples[0][:code_desc].to_s
755
- elsif examples.empty?
756
- # Empty control block - if it's anonymous, there's nothing we can do.
757
- # Is this case even possible?
758
- 'Empty anonymous control'
759
- else
760
- # Multiple tests - but no title. Do our best and generate some form of
761
- # identifier or label or name.
762
- title = (examples.map { |example| example[:code_desc] }).join('; ')
763
- max_len = MULTI_TEST_CONTROL_SUMMARY_MAX_LEN
764
- title = title[0..(max_len-1)] + '...' if title.length > max_len
765
- title
766
- end
767
- end
768
-
769
- # Return summary of the control which is usually a title with fails and skips
770
- def summary
771
- calculate_summary! if summary_calculation_needed?
772
- suffix =
773
- if examples.length == 1
774
- # Single test - be nice and just print the exception message if the test
775
- # failed. No need to say "1 failed".
776
- examples[0][:message].to_s
777
- else
778
- [
779
- !fails.empty? ? "#{fails.length} failed" : nil,
780
- !skips.empty? ? "#{skips.length} skipped" : nil,
781
- ].compact.join(' ')
782
- end
783
-
784
- suffix == '' ? title : title + ' (' + suffix + ')'
785
- end
786
-
787
- # We are interested in comparing controls against other controls. It is
788
- # important to compare their id values and the id values of their profiles.
789
- # In the event that a control has the same id in a different profile we
790
- # do not want them to be considered the same.
791
- #
792
- # Controls are never ordered so we don't care about the remaining
793
- # implementation of the spaceship operator.
794
- #
795
- def <=>(other)
796
- if id == other.id && profile_id == other.profile_id
797
- 0
798
- else
799
- -1
800
- end
801
- end
802
-
803
- private
804
-
805
- attr_reader :summary_calculation_needed, :skips, :fails, :passes
806
-
807
- alias summary_calculation_needed? summary_calculation_needed
808
-
809
- def summary_calculation_is_needed
810
- @summary_calculation_needed = true
811
- end
812
-
813
- def summary_has_been_calculated
814
- @summary_calculation_needed = false
815
- end
816
-
817
- def calculate_summary!
818
- @summary_status = STATUS_TYPES['unknown']
819
- @skips = []
820
- @fails = []
821
- @passes = []
822
- examples.each { |example| update_summary(example) }
823
- summary_has_been_calculated
824
- end
825
-
826
- def update_summary(example)
827
- test_skipped = example[:status] == 'skipped'
828
- status_type = test_skipped ? 'skipped' : example[:status_type]
829
- example_status = STATUS_TYPES[status_type]
830
- @summary_status = example_status if example_status > @summary_status
831
- fails.push(example) if example_status > 0
832
- passes.push(example) if example_status == STATUS_TYPES['passed']
833
- skips.push(example) if example_status == STATUS_TYPES['skipped']
834
- end
835
-
836
- # Determines 'status_type' (critical, major, minor) of control given
837
- # status (failed/passed/skipped) and impact value (0.0 - 1.0).
838
- # Called from format_example, sets the 'status_type' for each 'example'
839
- def status_type(example)
840
- status = example[:status]
841
- return status if status != 'failed' || control[:impact].nil?
842
- if control[:impact] >= 0.7
843
- 'critical'
844
- elsif control[:impact] >= 0.4
845
- 'major'
846
- else
847
- 'minor'
848
- end
849
- end
850
- end
851
- end
852
-
853
- class InspecRspecJUnit < InspecRspecJson
854
- RSpec::Core::Formatters.register self, :close
855
-
856
- #
857
- # This is the last method is invoked through the formatter interface.
858
- # Converts the junit formatter constructed output_hash into REXML generated
859
- # XML and writes it to output.
860
- #
861
- def close(_notification)
862
- require 'rexml/document'
863
- xml_output = REXML::Document.new
864
- xml_output.add(REXML::XMLDecl.new)
865
-
866
- testsuites = REXML::Element.new('testsuites')
867
- xml_output.add(testsuites)
868
-
869
- @output_hash[:profiles].each do |profile|
870
- testsuites.add(build_profile_xml(profile))
871
- end
872
-
873
- formatter = REXML::Formatters::Pretty.new
874
- formatter.compact = true
875
- output.puts formatter.write(xml_output.xml_decl, '')
876
- output.puts formatter.write(xml_output.root, '')
877
- end
878
-
879
- private
880
-
881
- def build_profile_xml(profile)
882
- profile_name = profile[:name]
883
- profile_xml = REXML::Element.new('testsuite')
884
- profile_xml.add_attribute('name', profile_name)
885
- profile_xml.add_attribute('tests', count_profile_tests(profile))
886
- profile_xml.add_attribute('failed', count_profile_failed_tests(profile))
887
-
888
- profile[:controls].each do |control|
889
- next if control[:results].nil?
890
-
891
- control[:results].each do |result|
892
- profile_xml.add(build_result_xml(profile_name, control, result))
893
- end
894
- end
895
-
896
- profile_xml
897
- end
898
-
899
- def build_result_xml(profile_name, control, result)
900
- result_xml = REXML::Element.new('testcase')
901
- result_xml.add_attribute('name', result[:code_desc])
902
- # if there is no control title, we are likely receiving test results from a
903
- # "naked" test (a test not located within a control block). Therefore, rather
904
- # than outputting the auto-generated ID, i.e.
905
- #
906
- # "(generated from test_spec.rb:1 de0ce10e4bbbd4d0ff7a65f4234de8c1)")
907
- #
908
- # ... we'll output "Anonymous" instead.
909
- result_xml.add_attribute('classname', control[:title].nil? ? "#{profile_name}.Anonymous" : "#{profile_name}.#{control[:id]}")
910
- result_xml.add_attribute('time', result[:run_time])
911
-
912
- if result[:status] == 'failed'
913
- failure_element = REXML::Element.new('failure')
914
- failure_element.add_attribute('message', result[:message])
915
- result_xml.add(failure_element)
916
- elsif result[:status] == 'skipped'
917
- result_xml.add_element('skipped')
918
- end
919
-
920
- result_xml
921
- end
922
-
923
- def count_profile_tests(profile)
924
- profile[:controls].reduce(0) { |acc, elem|
925
- acc + (elem[:results].nil? ? 0 : elem[:results].count)
926
- }
927
- end
928
-
929
- def count_profile_failed_tests(profile)
930
- profile[:controls].reduce(0) { |acc, elem|
931
- if elem[:results].nil?
932
- acc
933
- else
934
- acc + elem[:results].reduce(0) { |fail_test_total, test_case|
935
- test_case[:status] == 'failed' ? fail_test_total + 1 : fail_test_total
936
- }
937
- end
938
- }
939
- end
940
- end