inspec-core 5.18.14 → 5.21.29

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +19 -16
  3. data/inspec-core.gemspec +22 -22
  4. data/lib/inspec/base_cli.rb +2 -0
  5. data/lib/inspec/cli.rb +6 -2
  6. data/lib/inspec/dsl.rb +10 -4
  7. data/lib/inspec/enhanced_outcomes.rb +19 -0
  8. data/lib/inspec/env_printer.rb +1 -1
  9. data/lib/inspec/exceptions.rb +2 -0
  10. data/lib/inspec/formatters/base.rb +69 -16
  11. data/lib/inspec/plugin/v2/loader.rb +19 -8
  12. data/lib/inspec/plugin/v2/plugin_types/reporter.rb +1 -0
  13. data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +54 -0
  14. data/lib/inspec/reporters/base.rb +1 -0
  15. data/lib/inspec/reporters/cli.rb +94 -3
  16. data/lib/inspec/reporters/json.rb +3 -1
  17. data/lib/inspec/reporters/yaml.rb +3 -1
  18. data/lib/inspec/reporters.rb +2 -1
  19. data/lib/inspec/resources/file.rb +1 -1
  20. data/lib/inspec/resources/http.rb +2 -2
  21. data/lib/inspec/resources/lxc.rb +65 -9
  22. data/lib/inspec/resources/oracledb_session.rb +13 -4
  23. data/lib/inspec/resources/podman.rb +353 -0
  24. data/lib/inspec/resources/podman_container.rb +84 -0
  25. data/lib/inspec/resources/podman_image.rb +108 -0
  26. data/lib/inspec/resources/podman_network.rb +81 -0
  27. data/lib/inspec/resources/podman_pod.rb +101 -0
  28. data/lib/inspec/resources/podman_volume.rb +87 -0
  29. data/lib/inspec/resources/service.rb +1 -1
  30. data/lib/inspec/rule.rb +54 -17
  31. data/lib/inspec/run_data/control.rb +6 -0
  32. data/lib/inspec/run_data/statistics.rb +8 -2
  33. data/lib/inspec/runner.rb +18 -8
  34. data/lib/inspec/runner_rspec.rb +3 -2
  35. data/lib/inspec/schema/exec_json.rb +78 -2
  36. data/lib/inspec/schema/output_schema.rb +4 -1
  37. data/lib/inspec/schema/profile_json.rb +46 -0
  38. data/lib/inspec/schema.rb +91 -0
  39. data/lib/inspec/utils/convert.rb +8 -0
  40. data/lib/inspec/utils/podman.rb +24 -0
  41. data/lib/inspec/utils/waivers/csv_file_reader.rb +34 -0
  42. data/lib/inspec/utils/waivers/excel_file_reader.rb +39 -0
  43. data/lib/inspec/utils/waivers/json_file_reader.rb +15 -0
  44. data/lib/inspec/version.rb +1 -1
  45. data/lib/inspec/waiver_file_reader.rb +61 -0
  46. data/lib/matchers/matchers.rb +7 -1
  47. data/lib/plugins/inspec-init/templates/profiles/alicloud/README.md +27 -0
  48. data/lib/plugins/inspec-init/templates/profiles/alicloud/controls/example.rb +10 -0
  49. data/lib/plugins/inspec-init/templates/profiles/alicloud/inputs.yml +1 -0
  50. data/lib/plugins/inspec-init/templates/profiles/alicloud/inspec.yml +14 -0
  51. data/lib/plugins/inspec-reporter-html2/README.md +1 -1
  52. data/lib/plugins/inspec-reporter-html2/templates/body.html.erb +7 -1
  53. data/lib/plugins/inspec-reporter-html2/templates/control.html.erb +10 -6
  54. data/lib/plugins/inspec-reporter-html2/templates/default.css +12 -0
  55. data/lib/plugins/inspec-reporter-html2/templates/selector.html.erb +7 -1
  56. data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +5 -2
  57. data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +39 -13
  58. metadata +25 -9
@@ -0,0 +1,101 @@
1
+ require "inspec/resources/command"
2
+ require "inspec/utils/podman"
3
+
4
+ module Inspec::Resources
5
+ class PodmanPod < Inspec.resource(1)
6
+ include Inspec::Utils::Podman
7
+
8
+ name "podman_pod"
9
+ supports platform: "unix"
10
+
11
+ desc "InSpec core resource to retrieve information about podman pod"
12
+
13
+ example <<~EXAMPLE
14
+ describe podman_pod("nginx-frontend") do
15
+ it { should exist }
16
+ its("id") { should eq "fcfe4d471cfface0d1b39bce23af7d31ab8736cd68c0360ade0b4afe364f79d4" }
17
+ its("name") { should eq "nginx-frontend" }
18
+ its("created_at") { should eq "2022-07-14T15:47:47.978078124+05:30" }
19
+ its("create_command") { should include "new:nginx-frontend" }
20
+ its("state") { should eq "Running" }
21
+ its("hostname") { should eq "" }
22
+ its("create_cgroup") { should eq true }
23
+ its("cgroup_parent") { should eq "user.slice" }
24
+ its("cgroup_path") { should eq "user.slice/user-libpod_pod_fcfe4d471cfface0d1b39bce23af7d31ab8736cd68c0360ade0b4afe364f79d4.slice" }
25
+ its("create_infra") { should eq true }
26
+ its("infra_container_id") { should eq "727538044b32a165934729dc2d47d9d5e981b6496aebfad7de470f7e76ea4251" }
27
+ its("infra_config") { should include "DNSOption" }
28
+ its("shared_namespaces") { should include "ipc" }
29
+ its("num_containers") { should eq 2 }
30
+ its("containers") { should_not be nil }
31
+ end
32
+
33
+ describe podman_pod("non-existing-pod") do
34
+ it { should_not exist }
35
+ end
36
+ EXAMPLE
37
+
38
+ attr_reader :pod_info, :pod_id
39
+
40
+ def initialize(pod_id)
41
+ skip_resource "The `podman_pod` resource is not yet available on your OS." unless inspec.os.unix?
42
+ raise Inspec::Exceptions::ResourceFailed, "Podman is not running. Please make sure it is installed and running." unless podman_running?
43
+
44
+ @pod_id = pod_id
45
+ @pod_info = get_pod_info
46
+ end
47
+
48
+ LABELS = {
49
+ "id" => "ID",
50
+ "name" => "Name",
51
+ "created_at" => "Created",
52
+ "create_command" => "CreateCommand",
53
+ "state" => "State",
54
+ "hostname" => "Hostname",
55
+ "create_cgroup" => "CreateCgroup",
56
+ "cgroup_parent" => "CgroupParent",
57
+ "cgroup_path" => "CgroupPath",
58
+ "create_infra" => "CreateInfra",
59
+ "infra_container_id" => "InfraContainerID",
60
+ "infra_config" => "InfraConfig",
61
+ "shared_namespaces" => "SharedNamespaces",
62
+ "num_containers" => "NumContainers",
63
+ "containers" => "Containers",
64
+ }.freeze
65
+
66
+ # This creates all the required properties methods dynamically.
67
+ LABELS.each do |k, _|
68
+ define_method(k) do
69
+ pod_info[k.to_s]
70
+ end
71
+ end
72
+
73
+ def exist?
74
+ !pod_info.empty?
75
+ end
76
+
77
+ def resource_id
78
+ pod_id
79
+ end
80
+
81
+ def to_s
82
+ "Podman Pod #{resource_id}"
83
+ end
84
+
85
+ private
86
+
87
+ def get_pod_info
88
+ json_key_label = generate_go_template(LABELS)
89
+
90
+ inspect_pod_cmd = inspec.command("podman pod inspect #{pod_id} --format '{#{json_key_label}}'")
91
+
92
+ if inspect_pod_cmd.exit_status == 0
93
+ parse_command_output(inspect_pod_cmd.stdout)
94
+ elsif inspect_pod_cmd.stderr =~ /no pod with name or ID/
95
+ {}
96
+ else
97
+ raise Inspec::Exceptions::ResourceFailed, "Unable to retrieve podman pod information for #{pod_id}.\nError message: #{inspect_pod_cmd.stderr}"
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,87 @@
1
+ require "inspec/resources/command"
2
+ require "inspec/utils/podman"
3
+
4
+ module Inspec::Resources
5
+ class PodmanVolume < Inspec.resource(1)
6
+ include Inspec::Utils::Podman
7
+
8
+ name "podman_volume"
9
+ supports platform: "unix"
10
+
11
+ desc "InSpec core resource to retrieve information about podman volume"
12
+
13
+ example <<~EXAMPLE
14
+ describe podman_volume("my_volume") do
15
+ it { should exist }
16
+ its("name") { should eq "my_volume" }
17
+ its("driver") { should eq "local" }
18
+ its("mountpoint") { should eq "/var/home/core/.local/share/containers/storage/volumes/my_volume/_data" }
19
+ its("created_at") { should eq "2022-07-14T13:21:19.965421792+05:30" }
20
+ its("labels") { should eq({}) }
21
+ its("scope") { should eq "local" }
22
+ its("options") { should eq({}) }
23
+ its("mount_count") { should eq 0 }
24
+ its("needs_copy_up") { should eq true }
25
+ its("needs_chown") { should eq true }
26
+ end
27
+ EXAMPLE
28
+
29
+ attr_reader :volume_info, :volume_name
30
+
31
+ def initialize(volume_name)
32
+ skip_resource "The `podman_volume` resource is not yet available on your OS." unless inspec.os.unix?
33
+ raise Inspec::Exceptions::ResourceFailed, "Podman is not running. Please make sure it is installed and running." unless podman_running?
34
+
35
+ @volume_name = volume_name
36
+ @volume_info = get_volume_info
37
+ end
38
+
39
+ LABELS = {
40
+ "name" => "Name",
41
+ "driver" => "Driver",
42
+ "mountpoint" => "Mountpoint",
43
+ "created_at" => "CreatedAt",
44
+ "labels" => "Labels",
45
+ "scope" => "Scope",
46
+ "options" => "Options",
47
+ "mount_count" => "MountCount",
48
+ "needs_copy_up" => "NeedsCopyUp",
49
+ "needs_chown" => "NeedsChown",
50
+ }.freeze
51
+
52
+ # This creates all the required properties methods dynamically.
53
+ LABELS.each do |k, _|
54
+ define_method(k) do
55
+ volume_info[k.to_s]
56
+ end
57
+ end
58
+
59
+ def exist?
60
+ !volume_info.empty?
61
+ end
62
+
63
+ def resource_id
64
+ volume_name
65
+ end
66
+
67
+ def to_s
68
+ "podman_volume #{resource_id}"
69
+ end
70
+
71
+ private
72
+
73
+ def get_volume_info
74
+ json_key_label = generate_go_template(LABELS)
75
+
76
+ inspect_volume_cmd = inspec.command("podman volume inspect #{volume_name} --format '{#{json_key_label}}'")
77
+
78
+ if inspect_volume_cmd.exit_status == 0
79
+ parse_command_output(inspect_volume_cmd.stdout)
80
+ elsif inspect_volume_cmd.stderr =~ /inspecting object: no such/
81
+ {}
82
+ else
83
+ raise Inspec::Exceptions::ResourceFailed, "Unable to retrieve podman volume information for #{volume_name}.\nError message: #{inspect_volume_cmd.stderr}"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -646,7 +646,7 @@ module Inspec::Resources
646
646
  return nil if srv.nil? || srv[0].nil?
647
647
 
648
648
  # extract values from service
649
- parsed_srv = /^(?<pid>[0-9-]+)\t(?<exit>[0-9]+)\t(?<name>\S*)$/.match(srv[0])
649
+ parsed_srv = /^(?<pid>[0-9-]+)\t(?<exit>[\-0-9]+)\t(?<name>\S*)$/.match(srv[0])
650
650
  enabled = !parsed_srv["name"].nil? # it's in the list
651
651
 
652
652
  # check if the service is running
data/lib/inspec/rule.rb CHANGED
@@ -8,13 +8,15 @@ require "inspec/impact"
8
8
  require "inspec/resource"
9
9
  require "inspec/resources/os"
10
10
  require "inspec/input_registry"
11
+ require "inspec/waiver_file_reader"
12
+ require "inspec/utils/convert"
11
13
 
12
14
  module Inspec
13
15
  class Rule
14
16
  include ::RSpec::Matchers
15
17
 
16
18
  attr_reader :__waiver_data
17
- attr_accessor :resource_dsl
19
+ attr_accessor :resource_dsl, :na_impact_freeze
18
20
  attr_reader :__profile_id
19
21
 
20
22
  def initialize(id, profile_id, resource_dsl, opts, &block)
@@ -38,6 +40,7 @@ module Inspec
38
40
  @__merge_count = 0
39
41
  @__merge_changes = []
40
42
  @__skip_only_if_eval = opts[:skip_only_if_eval]
43
+ @__na_rule = {}
41
44
 
42
45
  # evaluate the given definition
43
46
  return unless block_given?
@@ -73,10 +76,13 @@ module Inspec
73
76
  end
74
77
 
75
78
  def impact(v = nil)
76
- if v.is_a?(String)
77
- @impact = Inspec::Impact.impact_from_string(v)
78
- elsif !v.nil?
79
- @impact = v
79
+ # N/A impact freeze is required when only_applicable_if block has reset impact value to zero"
80
+ unless na_impact_freeze
81
+ if v.is_a?(String)
82
+ @impact = Inspec::Impact.impact_from_string(v)
83
+ elsif !v.nil?
84
+ @impact = v
85
+ end
80
86
  end
81
87
 
82
88
  @impact
@@ -133,15 +139,28 @@ module Inspec
133
139
  #
134
140
  # @param [Type] &block returns true if tests are added, false otherwise
135
141
  # @return [nil]
136
- def only_if(message = nil)
142
+ def only_if(message = nil, impact: nil)
137
143
  return unless block_given?
138
144
  return if @__skip_only_if_eval == true
139
145
 
146
+ self.impact(impact) if impact && !yield
140
147
  @__skip_rule[:result] ||= !yield
141
148
  @__skip_rule[:type] = :only_if
142
149
  @__skip_rule[:message] = message
143
150
  end
144
151
 
152
+ def only_applicable_if(message = nil)
153
+ return unless block_given?
154
+ return if yield
155
+
156
+ impact(0.0)
157
+ self.na_impact_freeze = true # this flag prevents impact value to reset to any other value
158
+
159
+ @__na_rule[:result] ||= !yield
160
+ @__na_rule[:type] = :only_applicable_if
161
+ @__na_rule[:message] = message
162
+ end
163
+
145
164
  # Describe will add one or more tests to this control. There is 2 ways
146
165
  # of calling it:
147
166
  #
@@ -252,6 +271,10 @@ module Inspec
252
271
  rule.instance_variable_get(:@__skip_rule)
253
272
  end
254
273
 
274
+ def self.na_status(rule)
275
+ rule.instance_variable_get(:@__na_rule)
276
+ end
277
+
255
278
  def self.set_skip_rule(rule, value, message = nil, type = :only_if)
256
279
  rule.instance_variable_set(:@__skip_rule,
257
280
  {
@@ -273,16 +296,26 @@ module Inspec
273
296
  # creates a dummay array of "checks" with a skip outcome
274
297
  def self.prepare_checks(rule)
275
298
  skip_check = skip_status(rule)
276
- return checks(rule) unless skip_check[:result].eql?(true)
299
+ na_check = na_status(rule)
300
+ return checks(rule) unless skip_check[:result].eql?(true) || na_check[:result].eql?(true)
277
301
 
278
- if skip_check[:message]
279
- msg = "Skipped control due to #{skip_check[:type]} condition: #{skip_check[:message]}"
302
+ resource = rule.noop
303
+ if skip_check[:result].eql?(true)
304
+ if skip_check[:message]
305
+ msg = "Skipped control due to #{skip_check[:type]} condition: #{skip_check[:message]}"
306
+ else
307
+ msg = "Skipped control due to #{skip_check[:type]} condition."
308
+ end
309
+ resource.skip_resource(msg)
280
310
  else
281
- msg = "Skipped control due to #{skip_check[:type]} condition."
311
+ if na_check[:message]
312
+ msg = "N/A control due to #{na_check[:type]} condition: #{na_check[:message]}"
313
+ else
314
+ msg = "N/A control due to #{na_check[:type]} condition."
315
+ end
316
+ resource.fail_resource(msg)
282
317
  end
283
318
 
284
- resource = rule.noop
285
- resource.skip_resource(msg)
286
319
  [["describe", [resource], nil]]
287
320
  end
288
321
 
@@ -337,17 +370,20 @@ module Inspec
337
370
  # only_if mechanism)
338
371
  # Double underscore: not intended to be called as part of the DSL
339
372
  def __apply_waivers
340
- input_name = @__rule_id # TODO: control ID slugging
341
- registry = Inspec::InputRegistry.instance
342
- input = registry.inputs_by_profile.dig(__profile_id, input_name)
343
- return unless input && input.has_value? && input.value.is_a?(Hash)
373
+ control_id = @__rule_id # TODO: control ID slugging
374
+ waiver_files = Inspec::Config.cached.final_options["waiver_file"] if Inspec::Config.cached.respond_to?(:final_options)
375
+
376
+ waiver_data_by_profile = Inspec::WaiverFileReader.fetch_waivers_by_profile(__profile_id, waiver_files) unless waiver_files.nil?
377
+
378
+ return unless waiver_data_by_profile && waiver_data_by_profile[control_id] && waiver_data_by_profile[control_id].is_a?(Hash)
344
379
 
345
380
  # An InSpec Input is a datastructure that tracks a profile parameter
346
381
  # over time. Its value can be set by many sources, and it keeps a
347
382
  # log of each "set" event so that when it is collapsed to a value,
348
383
  # it can determine the correct (highest priority) value.
349
384
  # Store in an instance variable for.. later reading???
350
- @__waiver_data = input.value
385
+ @__waiver_data = waiver_data_by_profile[control_id]
386
+
351
387
  __waiver_data["skipped_due_to_waiver"] = false
352
388
  __waiver_data["message"] = ""
353
389
 
@@ -376,6 +412,7 @@ module Inspec
376
412
  # expiration_date. We only care here if it has a "run" key and it
377
413
  # is false-like, since all non-skipped waiver operations are handled
378
414
  # during reporting phase.
415
+ __waiver_data["run"] = Converter.to_boolean(__waiver_data["run"]) if __waiver_data.key?("run")
379
416
  return unless __waiver_data.key?("run") && !__waiver_data["run"]
380
417
 
381
418
  # OK, apply a skip.
@@ -1,3 +1,5 @@
1
+ require "inspec/enhanced_outcomes"
2
+
1
3
  module Inspec
2
4
  class RunData
3
5
  Control = Struct.new(
@@ -31,6 +33,10 @@ module Inspec
31
33
  ].each do |field|
32
34
  self[field] = raw_ctl_data[field]
33
35
  end
36
+
37
+ def status
38
+ Inspec::EnhancedOutcomes.determine_status(results, impact)
39
+ end
34
40
  end
35
41
  end
36
42
 
@@ -16,14 +16,20 @@ module Inspec
16
16
  :total,
17
17
  :passed,
18
18
  :skipped,
19
- :failed
19
+ :failed,
20
+ :not_reviewed,
21
+ :not_applicable,
22
+ :error
20
23
  ) do
21
24
  include HashLikeStruct
22
25
  def initialize(raw_stat_ctl_data)
23
26
  self.total = raw_stat_ctl_data[:total]
24
27
  self.passed = Inspec::RunData::Statistics::Controls::Total.new(raw_stat_ctl_data[:passed][:total])
25
- self.skipped = Inspec::RunData::Statistics::Controls::Total.new(raw_stat_ctl_data[:skipped][:total])
26
28
  self.failed = Inspec::RunData::Statistics::Controls::Total.new(raw_stat_ctl_data[:failed][:total])
29
+ self.skipped = Inspec::RunData::Statistics::Controls::Total.new(raw_stat_ctl_data[:skipped][:total]) if raw_stat_ctl_data[:skipped]
30
+ self.not_reviewed = Inspec::RunData::Statistics::Controls::Total.new(raw_stat_ctl_data[:not_reviewed][:total]) if raw_stat_ctl_data[:not_reviewed]
31
+ self.not_applicable = Inspec::RunData::Statistics::Controls::Total.new(raw_stat_ctl_data[:not_applicable][:total]) if raw_stat_ctl_data[:not_applicable]
32
+ self.error = Inspec::RunData::Statistics::Controls::Total.new(raw_stat_ctl_data[:error][:total]) if raw_stat_ctl_data[:error]
27
33
  end
28
34
  end
29
35
  class Controls
data/lib/inspec/runner.rb CHANGED
@@ -60,9 +60,11 @@ module Inspec
60
60
  end
61
61
 
62
62
  if @conf[:waiver_file]
63
- waivers = @conf.delete(:waiver_file)
64
- @conf[:input_file] ||= []
65
- @conf[:input_file].concat waivers
63
+ @conf[:waiver_file].each do |file|
64
+ unless File.file?(file)
65
+ raise Inspec::Exceptions::WaiversFileDoesNotExist, "Waiver file #{file} does not exist."
66
+ end
67
+ end
66
68
  end
67
69
 
68
70
  # About reading inputs:
@@ -133,12 +135,20 @@ module Inspec
133
135
  all_controls.each do |rule|
134
136
  unless rule.nil?
135
137
  register_rule(rule)
136
- checks = ::Inspec::Rule.prepare_checks(rule)
137
- unless checks.empty?
138
+ total_checks = 0
139
+ control_describe_checks = ::Inspec::Rule.prepare_checks(rule)
140
+
141
+ examples = control_describe_checks.flat_map do |m, a, b|
142
+ get_check_example(m, a, b)
143
+ end.compact
144
+
145
+ examples.map { |example| total_checks += example.examples.count }
146
+
147
+ unless control_describe_checks.empty?
138
148
  # controls with empty tests are avoided
139
149
  # checks represent tests within control
140
- controls_count += 1
141
- control_checks_count_map[rule.to_s] = checks.count
150
+ controls_count += 1 if control_checks_count_map[rule.to_s].nil?
151
+ control_checks_count_map[rule.to_s] = control_checks_count_map[rule.to_s].to_i + total_checks
142
152
  end
143
153
  end
144
154
  end
@@ -158,7 +168,7 @@ module Inspec
158
168
  return if @conf["reporter"].nil?
159
169
 
160
170
  @conf["reporter"].each do |reporter|
161
- result = Inspec::Reporters.render(reporter, run_data)
171
+ result = Inspec::Reporters.render(reporter, run_data, @conf["enhanced_outcomes"])
162
172
  raise Inspec::ReporterError, "Error generating reporter '#{reporter[0]}'" if result == false
163
173
  end
164
174
  end
@@ -107,11 +107,11 @@ module Inspec
107
107
  stats = @formatter.results[:statistics][:controls]
108
108
  load_failures = @formatter.results[:profiles]&.select { |p| p[:status] == "failed" }&.any?
109
109
  skipped = @formatter.results.dig(:profiles, 0, :status) == "skipped"
110
- if stats[:failed][:total] == 0 && stats[:skipped][:total] == 0 && !skipped && !load_failures
110
+ if stats[:failed][:total] == 0 && stats[:skipped][:total] == 0 && !skipped && !load_failures && (stats[:error] && stats[:error][:total] == 0) # placed error count condition because of enhanced outcomes
111
111
  0
112
112
  elsif load_failures
113
113
  @conf["distinct_exit"] ? 102 : 1
114
- elsif stats[:failed][:total] > 0
114
+ elsif stats[:failed][:total] > 0 || (stats[:error] && stats[:error][:total] > 0)
115
115
  @conf["distinct_exit"] ? 100 : 1
116
116
  elsif stats[:skipped][:total] > 0 || skipped
117
117
  @conf["distinct_exit"] ? 101 : 0
@@ -196,6 +196,7 @@ module Inspec
196
196
  def configure_output
197
197
  RSpec.configuration.output_stream = $stdout
198
198
  @formatter = RSpec.configuration.add_formatter(Inspec::Formatters::Base)
199
+ @formatter.enhanced_outcomes = @conf.final_options["enhanced_outcomes"]
199
200
  RSpec.configuration.add_formatter(Inspec::Formatters::ShowProgress, $stderr) if @conf[:show_progress]
200
201
  set_optional_formatters
201
202
  RSpec.configuration.color = @conf["color"]
@@ -19,8 +19,8 @@ module Inspec
19
19
  # Lists the potential values for a control result
20
20
  CONTROL_RESULT_STATUS = Primitives::SchemaType.new("Control Result Status", {
21
21
  "type" => "string",
22
- "enum" => %w{passed failed skipped error},
23
- }, [], "The status of a control. Should be one of 'passed', 'failed', 'skipped', or 'error'.")
22
+ "enum" => %w{passed failed skipped},
23
+ }, [], "The status of a control. Should be one of 'passed', 'failed', or 'skipped'.")
24
24
 
25
25
  # Represents the statistics/result of a control"s execution
26
26
  CONTROL_RESULT = Primitives::SchemaType.new("Control Result", {
@@ -75,6 +75,36 @@ module Inspec
75
75
  },
76
76
  }, [CONTROL_DESCRIPTION, Primitives::REFERENCE, Primitives::SOURCE_LOCATION, CONTROL_RESULT], "Describes a control and any findings it has.")
77
77
 
78
+ # Represents a control produced with enhanced outcomes option
79
+ ENHANCED_OUTCOME_CONTROL = Primitives::SchemaType.new("Exec JSON Control", {
80
+ "type" => "object",
81
+ "additionalProperties" => true,
82
+ "required" => %w{id title desc impact refs tags code source_location results},
83
+ "properties" => {
84
+ "id" => Primitives.desc(Primitives::STRING, "The id."),
85
+ "title" => Primitives.desc({ "type" => %w{string null} }, "The title - is nullable."), # Nullable string
86
+ "desc" => Primitives.desc({ "type" => %w{string null} }, "The description for the overarching control."),
87
+ "descriptions" => Primitives.desc(Primitives.array(CONTROL_DESCRIPTION.ref), "A set of additional descriptions. Example: the 'fix' text."),
88
+ "impact" => Primitives.desc(Primitives::IMPACT, "The impactfulness or severity."),
89
+ "status" => {
90
+ "enum" => %w{passed failed not_applicable not_reviewed error},
91
+ "description" => Primitives.desc(Primitives::STRING, "The enhanced outcome status of the control"),
92
+ },
93
+ "refs" => Primitives.desc(Primitives.array(Primitives::REFERENCE.ref), "The set of references to external documents."),
94
+ "tags" => Primitives.desc(Primitives::TAGS, "A set of tags - usually metadata."),
95
+ "code" => Primitives.desc(Primitives::STRING, "The raw source code of the control. Note that if this is an overlay control, it does not include the underlying source code."),
96
+ "source_location" => Primitives.desc(Primitives::SOURCE_LOCATION.ref, "The explicit location of the control within the source code."),
97
+ "results" => Primitives.desc(Primitives.array(CONTROL_RESULT.ref), %q(
98
+ The set of all tests within the control and their results and findings. Example:
99
+ For Chef Inspec, if in the control's code we had the following:
100
+ describe sshd_config do
101
+ its('Port') { should cmp 22 }
102
+ end
103
+ The findings from this block would be appended to the results, as well as those of any other blocks within the control.
104
+ )),
105
+ },
106
+ }, [CONTROL_DESCRIPTION, Primitives::REFERENCE, Primitives::SOURCE_LOCATION, CONTROL_RESULT], "Describes a control and any findings it has.")
107
+
78
108
  # Based loosely on https://docs.chef.io/inspec/profiles/ as of July 3, 2019
79
109
  # However, concessions were made to the reality of current reporters, specifically
80
110
  # with how description is omitted and version/inspec_version aren't as advertised online
@@ -112,6 +142,40 @@ module Inspec
112
142
  },
113
143
  }, [CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::SUPPORT], "Information on the set of controls assessed. Example: it can include the name of the Inspec profile and any findings.")
114
144
 
145
+ ENHANCED_OUTCOME_PROFILE = Primitives::SchemaType.new("Exec JSON Profile", {
146
+ "type" => "object",
147
+ "additionalProperties" => true,
148
+ "required" => %w{name sha256 supports attributes groups controls},
149
+ # Name is mandatory in inspec.yml.
150
+ # supports, controls, groups, and attributes are always present, even if empty
151
+ # sha256, status, status_message
152
+ "properties" => {
153
+ # These are provided in inspec.yml
154
+ "name" => Primitives.desc(Primitives::STRING, "The name - must be unique."),
155
+ "title" => Primitives.desc(Primitives::STRING, "The title - should be human readable."),
156
+ "maintainer" => Primitives.desc(Primitives::STRING, "The maintainer(s)."),
157
+ "copyright" => Primitives.desc(Primitives::STRING, "The copyright holder(s)."),
158
+ "copyright_email" => Primitives.desc(Primitives::STRING, "The email address or other contact information of the copyright holder(s)."),
159
+ "depends" => Primitives.desc(Primitives.array(Primitives::DEPENDENCY.ref), "The set of dependencies this profile depends on. Example: an overlay profile is dependent on another profile."),
160
+ "parent_profile" => Primitives.desc(Primitives::STRING, "The name of the parent profile if the profile is a dependency of another."),
161
+ "license" => Primitives.desc(Primitives::STRING, "The copyright license. Example: the full text or the name, such as 'Apache License, Version 2.0'."),
162
+ "summary" => Primitives.desc(Primitives::STRING, "The summary. Example: the Security Technical Implementation Guide (STIG) header."),
163
+ "version" => Primitives.desc(Primitives::STRING, "The version of the profile."),
164
+ "supports" => Primitives.desc(Primitives.array(Primitives::SUPPORT.ref), "The set of supported platform targets."),
165
+ "description" => Primitives.desc(Primitives::STRING, "The description - should be more detailed than the summary."),
166
+ "inspec_version" => Primitives.desc(Primitives::STRING, "The version of Inspec."),
167
+
168
+ # These are generated at runtime, and all except status_message and skip_message are guaranteed
169
+ "sha256" => Primitives.desc(Primitives::STRING, "The checksum of the profile."),
170
+ "status" => Primitives.desc(Primitives::STRING, "The status. Example: loaded."), # enum? loaded, failed, skipped
171
+ "status_message" => Primitives.desc(Primitives::STRING, "The reason for the status. Example: why it was skipped or failed to load."),
172
+ "skip_message" => Primitives.desc(Primitives::STRING, "The reason for skipping if it was skipped."), # Deprecated field - status_message should be used instead.
173
+ "controls" => Primitives.desc(Primitives.array(CONTROL.ref), "The set of controls including any findings."),
174
+ "groups" => Primitives.desc(Primitives.array(Primitives::CONTROL_GROUP.ref), "A set of descriptions for the control groups. Example: the ids of the controls."),
175
+ "attributes" => Primitives.desc(Primitives.array(Primitives::INPUT), "The input(s) or attribute(s) used in the run."),
176
+ },
177
+ }, [ENHANCED_OUTCOME_CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::SUPPORT], "Information on the set of controls assessed. Example: it can include the name of the Inspec profile and any findings.")
178
+
115
179
  # Result of exec json. Top level value
116
180
  # TODO: Include the format of top level controls. This was omitted for lack of sufficient examples
117
181
  OUTPUT = Primitives::SchemaType.new("Exec JSON Output", {
@@ -125,6 +189,18 @@ module Inspec
125
189
  "version" => Primitives.desc(Primitives::STRING, "Version number of the tool that generated the findings. Example: '4.18.108' is a version of Chef InSpec."),
126
190
  },
127
191
  }, [Primitives::PLATFORM, PROFILE, Primitives::STATISTICS], "The top level value containing all of the results.")
192
+
193
+ ENHANCED_OUTCOME_OUTPUT = Primitives::SchemaType.new("Exec JSON Output", {
194
+ "type" => "object",
195
+ "additionalProperties" => true,
196
+ "required" => %w{platform profiles statistics version},
197
+ "properties" => {
198
+ "platform" => Primitives.desc(Primitives::PLATFORM.ref, "Information on the platform the run from the tool that generated the findings was from. Example: the name of the operating system."),
199
+ "profiles" => Primitives.desc(Primitives.array(PROFILE.ref), "Information on the run(s) from the tool that generated the findings. Example: the findings."),
200
+ "statistics" => Primitives.desc(Primitives::STATISTICS.ref, "Statistics for the run(s) from the tool that generated the findings. Example: the runtime duration."),
201
+ "version" => Primitives.desc(Primitives::STRING, "Version number of the tool that generated the findings. Example: '4.18.108' is a version of Chef InSpec."),
202
+ },
203
+ }, [Primitives::PLATFORM, ENHANCED_OUTCOME_PROFILE, Primitives::STATISTICS], "The top level value containing all of the results.")
128
204
  end
129
205
  end
130
206
  end
@@ -30,6 +30,8 @@ module Inspec
30
30
  "profile-json" => OutputSchema.finalize(Schema::ProfileJson::PROFILE),
31
31
  "exec-json" => OutputSchema.finalize(Schema::ExecJson::OUTPUT),
32
32
  "exec-jsonmin" => OutputSchema.finalize(Schema::ExecJsonMin::OUTPUT),
33
+ "profile-json-enhanced-outcomes" => OutputSchema.finalize(Schema::ProfileJson::ENHANCED_OUTCOME_PROFILE),
34
+ "exec-json-enhanced-outcomes" => OutputSchema.finalize(Schema::ExecJson::ENHANCED_OUTCOME_OUTPUT),
33
35
  "platforms" => PLATFORMS,
34
36
  }.freeze
35
37
 
@@ -37,7 +39,8 @@ module Inspec
37
39
  LIST.keys
38
40
  end
39
41
 
40
- def self.json(name)
42
+ def self.json(name, opts)
43
+ name += "-enhanced-outcomes" if opts["enhanced_outcomes"]
41
44
  if !LIST.key?(name)
42
45
  raise("Cannot find schema #{name.inspect}.")
43
46
  elsif LIST[name].is_a?(Proc)
@@ -31,6 +31,28 @@ module Inspec
31
31
  },
32
32
  }, [CONTROL_DESCRIPTIONS, Primitives::REFERENCE, Primitives::SOURCE_LOCATION], "The set of all tests within the control.")
33
33
 
34
+ # Represents a control with enhanced outcomes status information
35
+ ENHANCED_OUTCOME_CONTROL = Primitives::SchemaType.new("Profile JSON Control", {
36
+ "type" => "object",
37
+ "additionalProperties" => true,
38
+ "required" => %w{id title desc impact tags code},
39
+ "properties" => {
40
+ "id" => Primitives.desc(Primitives::STRING, "The id."),
41
+ "title" => Primitives.desc({ "type" => %w{string null} }, "The title - is nullable."),
42
+ "desc" => Primitives.desc({ "type" => %w{string null} }, "The description for the overarching control."),
43
+ "descriptions" => Primitives.desc(CONTROL_DESCRIPTIONS.ref, "A set of additional descriptions. Example: the 'fix' text."),
44
+ "impact" => Primitives.desc(Primitives::IMPACT, "The impactfulness or severity."),
45
+ "status" => {
46
+ "enum" => %w{passed failed not_applicable not_reviewed error},
47
+ "description" => Primitives.desc(Primitives::STRING, "The enhanced outcome status of the control"),
48
+ },
49
+ "refs" => Primitives.desc(Primitives.array(Primitives::REFERENCE.ref), "The set of references to external documents."),
50
+ "tags" => Primitives.desc(Primitives::TAGS, "A set of tags - usually metadata."),
51
+ "code" => Primitives.desc(Primitives::STRING, "The raw source code of the control. Note that if this is an overlay control, it does not include the underlying source code."),
52
+ "source_location" => Primitives.desc(Primitives::SOURCE_LOCATION.ref, "The explicit location of the control within the source code."),
53
+ },
54
+ }, [CONTROL_DESCRIPTIONS, Primitives::REFERENCE, Primitives::SOURCE_LOCATION], "The set of all tests within the control.")
55
+
34
56
  # A profile that has not been run.
35
57
  PROFILE = Primitives::SchemaType.new("Profile JSON Profile", {
36
58
  "type" => "object",
@@ -55,6 +77,30 @@ module Inspec
55
77
  "depends" => Primitives.desc(Primitives.array(Primitives::DEPENDENCY.ref), "The set of dependencies this profile depends on. Example: an overlay profile is dependent on another profile."), # Can have depends, but NOT a parentprofile
56
78
  },
57
79
  }, [Primitives::SUPPORT, CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::GENERATOR], "Information on the set of controls that can be assessed. Example: it can include the name of the Inspec profile.")
80
+
81
+ ENHANCED_OUTCOME_PROFILE = Primitives::SchemaType.new("Profile JSON Profile", {
82
+ "type" => "object",
83
+ "additionalProperties" => true, # Anything in the yaml will be put in here. LTTODO: Make this stricter!
84
+ "required" => %w{name supports controls groups sha256},
85
+ "properties" => {
86
+ "name" => Primitives.desc(Primitives::STRING, "The name - must be unique."),
87
+ "supports" => Primitives.desc(Primitives.array(Primitives::SUPPORT.ref), "The set of supported platform targets."),
88
+ "controls" => Primitives.desc(Primitives.array(CONTROL.ref), "The set of controls - contains no findings as the assessment has not yet occurred."),
89
+ "groups" => Primitives.desc(Primitives.array(Primitives::CONTROL_GROUP.ref), "A set of descriptions for the control groups. Example: the ids of the controls."),
90
+ "inputs" => Primitives.desc(Primitives.array(Primitives::INPUT), "The input(s) or attribute(s) used to be in the run."),
91
+ "sha256" => Primitives.desc(Primitives::STRING, "The checksum of the profile."),
92
+ "status" => Primitives.desc(Primitives::STRING, "The status. Example: skipped."),
93
+ "generator" => Primitives.desc(Primitives::GENERATOR.ref, "The tool that generated this file. Example: Chef Inspec."),
94
+ "version" => Primitives.desc(Primitives::STRING, "The version of the profile."),
95
+
96
+ # Other properties possible in inspec docs, but that aren"t guaranteed
97
+ "title" => Primitives.desc(Primitives::STRING, "The title - should be human readable."),
98
+ "maintainer" => Primitives.desc(Primitives::STRING, "The maintainer(s)."),
99
+ "copyright" => Primitives.desc(Primitives::STRING, "The copyright holder(s)."),
100
+ "copyright_email" => Primitives.desc(Primitives::STRING, "The email address or other contract information of the copyright holder(s)."),
101
+ "depends" => Primitives.desc(Primitives.array(Primitives::DEPENDENCY.ref), "The set of dependencies this profile depends on. Example: an overlay profile is dependent on another profile."), # Can have depends, but NOT a parentprofile
102
+ },
103
+ }, [Primitives::SUPPORT, ENHANCED_OUTCOME_CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::GENERATOR], "Information on the set of controls that can be assessed. Example: it can include the name of the Inspec profile.")
58
104
  end
59
105
  end
60
106
  end