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.
- checksums.yaml +4 -4
- data/Gemfile +19 -16
- data/inspec-core.gemspec +22 -22
- data/lib/inspec/base_cli.rb +2 -0
- data/lib/inspec/cli.rb +6 -2
- data/lib/inspec/dsl.rb +10 -4
- data/lib/inspec/enhanced_outcomes.rb +19 -0
- data/lib/inspec/env_printer.rb +1 -1
- data/lib/inspec/exceptions.rb +2 -0
- data/lib/inspec/formatters/base.rb +69 -16
- data/lib/inspec/plugin/v2/loader.rb +19 -8
- data/lib/inspec/plugin/v2/plugin_types/reporter.rb +1 -0
- data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +54 -0
- data/lib/inspec/reporters/base.rb +1 -0
- data/lib/inspec/reporters/cli.rb +94 -3
- data/lib/inspec/reporters/json.rb +3 -1
- data/lib/inspec/reporters/yaml.rb +3 -1
- data/lib/inspec/reporters.rb +2 -1
- data/lib/inspec/resources/file.rb +1 -1
- data/lib/inspec/resources/http.rb +2 -2
- data/lib/inspec/resources/lxc.rb +65 -9
- data/lib/inspec/resources/oracledb_session.rb +13 -4
- data/lib/inspec/resources/podman.rb +353 -0
- data/lib/inspec/resources/podman_container.rb +84 -0
- data/lib/inspec/resources/podman_image.rb +108 -0
- data/lib/inspec/resources/podman_network.rb +81 -0
- data/lib/inspec/resources/podman_pod.rb +101 -0
- data/lib/inspec/resources/podman_volume.rb +87 -0
- data/lib/inspec/resources/service.rb +1 -1
- data/lib/inspec/rule.rb +54 -17
- data/lib/inspec/run_data/control.rb +6 -0
- data/lib/inspec/run_data/statistics.rb +8 -2
- data/lib/inspec/runner.rb +18 -8
- data/lib/inspec/runner_rspec.rb +3 -2
- data/lib/inspec/schema/exec_json.rb +78 -2
- data/lib/inspec/schema/output_schema.rb +4 -1
- data/lib/inspec/schema/profile_json.rb +46 -0
- data/lib/inspec/schema.rb +91 -0
- data/lib/inspec/utils/convert.rb +8 -0
- data/lib/inspec/utils/podman.rb +24 -0
- data/lib/inspec/utils/waivers/csv_file_reader.rb +34 -0
- data/lib/inspec/utils/waivers/excel_file_reader.rb +39 -0
- data/lib/inspec/utils/waivers/json_file_reader.rb +15 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/inspec/waiver_file_reader.rb +61 -0
- data/lib/matchers/matchers.rb +7 -1
- data/lib/plugins/inspec-init/templates/profiles/alicloud/README.md +27 -0
- data/lib/plugins/inspec-init/templates/profiles/alicloud/controls/example.rb +10 -0
- data/lib/plugins/inspec-init/templates/profiles/alicloud/inputs.yml +1 -0
- data/lib/plugins/inspec-init/templates/profiles/alicloud/inspec.yml +14 -0
- data/lib/plugins/inspec-reporter-html2/README.md +1 -1
- data/lib/plugins/inspec-reporter-html2/templates/body.html.erb +7 -1
- data/lib/plugins/inspec-reporter-html2/templates/control.html.erb +10 -6
- data/lib/plugins/inspec-reporter-html2/templates/default.css +12 -0
- data/lib/plugins/inspec-reporter-html2/templates/selector.html.erb +7 -1
- data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +5 -2
- data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +39 -13
- 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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
299
|
+
na_check = na_status(rule)
|
300
|
+
return checks(rule) unless skip_check[:result].eql?(true) || na_check[:result].eql?(true)
|
277
301
|
|
278
|
-
|
279
|
-
|
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
|
-
|
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
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
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 =
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
137
|
-
|
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] =
|
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
|
data/lib/inspec/runner_rspec.rb
CHANGED
@@ -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
|
23
|
-
}, [], "The status of a control. Should be one of 'passed', 'failed',
|
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
|