inspec-core 5.18.14 → 5.21.29

Sign up to get free protection for your applications and to get access to all the features.
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