inspec-core 5.18.14 → 5.22.3
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 -17
- data/inspec-core.gemspec +22 -22
- data/lib/inspec/base_cli.rb +19 -17
- data/lib/inspec/cli.rb +27 -25
- data/lib/inspec/dependencies/dependency_set.rb +2 -2
- data/lib/inspec/dsl.rb +9 -5
- 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/profile.rb +9 -8
- 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 +5 -5
- data/lib/inspec/resources/lxc.rb +65 -9
- data/lib/inspec/resources/mongodb_session.rb +5 -0
- data/lib/inspec/resources/nftables.rb +251 -0
- 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/postgres_session.rb +2 -1
- data/lib/inspec/resources/service.rb +1 -1
- data/lib/inspec/resources.rb +1 -0
- 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/simpleconfig.rb +10 -2
- 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 +15 -2
- 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 +26 -9
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
|
data/lib/inspec/schema.rb
CHANGED
@@ -111,6 +111,43 @@ module Inspec
|
|
111
111
|
},
|
112
112
|
}.freeze
|
113
113
|
|
114
|
+
CONTROL_ENHANCED_OUTCOME = {
|
115
|
+
"type" => "object",
|
116
|
+
"additionalProperties" => false,
|
117
|
+
"properties" => {
|
118
|
+
"id" => { "type" => "string" },
|
119
|
+
"title" => { "type" => %w{string null} },
|
120
|
+
"desc" => { "type" => %w{string null} },
|
121
|
+
"descriptions" => { "type" => %w{array} },
|
122
|
+
"impact" => { "type" => "number" },
|
123
|
+
"status" => {
|
124
|
+
"enum" => %w{passed failed not_applicable not_reviewed error},
|
125
|
+
"description" => Primitives.desc(Primitives::STRING, "The enhanced outcome status of the control"),
|
126
|
+
},
|
127
|
+
"refs" => REFS,
|
128
|
+
"tags" => TAGS,
|
129
|
+
"code" => { "type" => "string" },
|
130
|
+
"source_location" => {
|
131
|
+
"type" => "object",
|
132
|
+
"properties" => {
|
133
|
+
"ref" => { "type" => "string" },
|
134
|
+
"line" => { "type" => "number" },
|
135
|
+
},
|
136
|
+
},
|
137
|
+
"results" => { "type" => "array", "items" => RESULT },
|
138
|
+
"waiver_data" => {
|
139
|
+
"type" => "object",
|
140
|
+
"properties" => {
|
141
|
+
"skipped_due_to_waiver" => { "type" => "string" },
|
142
|
+
"run" => { "type" => "boolean" },
|
143
|
+
"message" => { "type" => "string" },
|
144
|
+
"expiration_date" => { "type" => "string" },
|
145
|
+
"justification" => { "type" => "string" },
|
146
|
+
},
|
147
|
+
},
|
148
|
+
},
|
149
|
+
}.freeze
|
150
|
+
|
114
151
|
SUPPORTS = {
|
115
152
|
"type" => "object",
|
116
153
|
"additionalProperties" => false,
|
@@ -173,6 +210,45 @@ module Inspec
|
|
173
210
|
},
|
174
211
|
}.freeze
|
175
212
|
|
213
|
+
PROFILE_ENHANCED_OUTCOME = {
|
214
|
+
"type" => "object",
|
215
|
+
"additionalProperties" => false,
|
216
|
+
"properties" => {
|
217
|
+
"name" => { "type" => "string" },
|
218
|
+
"version" => { "type" => "string", "optional" => true },
|
219
|
+
"sha256" => { "type" => "string", "optional" => false },
|
220
|
+
|
221
|
+
"title" => { "type" => "string", "optional" => true },
|
222
|
+
"maintainer" => { "type" => "string", "optional" => true },
|
223
|
+
"copyright" => { "type" => "string", "optional" => true },
|
224
|
+
"copyright_email" => { "type" => "string", "optional" => true },
|
225
|
+
"license" => { "type" => "string", "optional" => true },
|
226
|
+
"summary" => { "type" => "string", "optional" => true },
|
227
|
+
"status" => { "type" => "string", "optional" => false },
|
228
|
+
"status_message" => { "type" => "string", "optional" => true },
|
229
|
+
# skip_message is deprecated, status_message should be used to store the reason for skipping
|
230
|
+
"skip_message" => { "type" => "string", "optional" => true },
|
231
|
+
|
232
|
+
"supports" => {
|
233
|
+
"type" => "array",
|
234
|
+
"items" => SUPPORTS,
|
235
|
+
"optional" => true,
|
236
|
+
},
|
237
|
+
"controls" => {
|
238
|
+
"type" => "array",
|
239
|
+
"items" => CONTROL_ENHANCED_OUTCOME,
|
240
|
+
},
|
241
|
+
"groups" => {
|
242
|
+
"type" => "array",
|
243
|
+
"items" => CONTROL_GROUP,
|
244
|
+
},
|
245
|
+
"attributes" => { # TODO: rename to inputs, refs #3802
|
246
|
+
"type" => "array",
|
247
|
+
# TODO: more detailed specification needed
|
248
|
+
},
|
249
|
+
},
|
250
|
+
}.freeze
|
251
|
+
|
176
252
|
EXEC_JSON = {
|
177
253
|
"type" => "object",
|
178
254
|
"additionalProperties" => false,
|
@@ -187,6 +263,20 @@ module Inspec
|
|
187
263
|
},
|
188
264
|
}.freeze
|
189
265
|
|
266
|
+
EXEC_JSON_ENHANCED_OUTCOME = {
|
267
|
+
"type" => "object",
|
268
|
+
"additionalProperties" => false,
|
269
|
+
"properties" => {
|
270
|
+
"platform" => PLATFORM,
|
271
|
+
"profiles" => {
|
272
|
+
"type" => "array",
|
273
|
+
"items" => PROFILE_ENHANCED_OUTCOME,
|
274
|
+
},
|
275
|
+
"statistics" => STATISTICS,
|
276
|
+
"version" => { "type" => "string" },
|
277
|
+
},
|
278
|
+
}.freeze
|
279
|
+
|
190
280
|
MIN_CONTROL = {
|
191
281
|
"type" => "object",
|
192
282
|
"additionalProperties" => false,
|
@@ -228,6 +318,7 @@ module Inspec
|
|
228
318
|
LIST = {
|
229
319
|
"exec-json" => EXEC_JSON,
|
230
320
|
"exec-jsonmin" => EXEC_JSONMIN,
|
321
|
+
"exec-json-enhanced-outcome" => EXEC_JSON_ENHANCED_OUTCOME,
|
231
322
|
"platforms" => PLATFORMS,
|
232
323
|
}.freeze
|
233
324
|
|
data/lib/inspec/utils/convert.rb
CHANGED
@@ -5,4 +5,12 @@ module Converter
|
|
5
5
|
val = val.to_i if val =~ /^\d+$/
|
6
6
|
val
|
7
7
|
end
|
8
|
+
|
9
|
+
def self.to_boolean(value)
|
10
|
+
if ["true", "True", "TRUE", true, "yes", "y", "YES", "Y"].include? value
|
11
|
+
true
|
12
|
+
elsif ["false", "False", "FALSE", false, "no", "n", "NO", "N"].include? value
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
8
16
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "inspec/resources/command"
|
2
|
+
|
3
|
+
module Inspec
|
4
|
+
module Utils
|
5
|
+
module Podman
|
6
|
+
def podman_running?
|
7
|
+
inspec.command("podman version").exit_status == 0
|
8
|
+
end
|
9
|
+
|
10
|
+
# Generates the template in this format using labels hash: "\"id\": {{json .ID}}, \"name\": {{json .Name}}",
|
11
|
+
def generate_go_template(labels)
|
12
|
+
(labels.map { |k, v| "\"#{k}\": {{json .#{v}}}" }).join(", ")
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_command_output(output)
|
16
|
+
require "json" unless defined?(JSON)
|
17
|
+
JSON.parse(output)
|
18
|
+
rescue JSON::ParserError => _e
|
19
|
+
warn "Could not parse the command output"
|
20
|
+
{}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -57,11 +57,18 @@ class SimpleConfig
|
|
57
57
|
m = opts[:assignment_regex].match(line)
|
58
58
|
return nil if m.nil?
|
59
59
|
|
60
|
+
values = parse_values(m, opts[:key_values])
|
61
|
+
|
60
62
|
if opts[:multiple_values]
|
61
63
|
@vals[m[1]] ||= []
|
62
|
-
|
64
|
+
if opts[:multiple_value_regex] # can be used only if multiple values is set as true
|
65
|
+
value_to_array = values.split(opts[:multiple_value_regex])
|
66
|
+
@vals[m[1]].concat(value_to_array)
|
67
|
+
else
|
68
|
+
@vals[m[1]].push(values)
|
69
|
+
end
|
63
70
|
else
|
64
|
-
@vals[m[1]] =
|
71
|
+
@vals[m[1]] = values
|
65
72
|
end
|
66
73
|
end
|
67
74
|
|
@@ -116,6 +123,7 @@ class SimpleConfig
|
|
116
123
|
key_values: 1, # default for key=value, may require for 'key val1 val2 val3'
|
117
124
|
standalone_comments: false,
|
118
125
|
multiple_values: false,
|
126
|
+
multiple_value_regex: nil,
|
119
127
|
}
|
120
128
|
end
|
121
129
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "csv" unless defined?(CSV)
|
2
|
+
|
3
|
+
module Waivers
|
4
|
+
class CSVFileReader
|
5
|
+
def self.resolve(path)
|
6
|
+
return nil unless File.file?(path)
|
7
|
+
|
8
|
+
@headers ||= []
|
9
|
+
fetch_data(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.fetch_data(path)
|
13
|
+
waiver_data_hash = {}
|
14
|
+
CSV.foreach(path, headers: true) do |row|
|
15
|
+
row_hash = row.to_hash
|
16
|
+
@headers = row_hash.keys if @headers.empty?
|
17
|
+
control_id = row_hash["control_id"]
|
18
|
+
# delete keys and values not required in final hash
|
19
|
+
row_hash.delete("control_id")
|
20
|
+
row_hash.delete_if { |k, v| k.nil? || v.nil? }
|
21
|
+
|
22
|
+
waiver_data_hash[control_id] = row_hash if control_id && !row_hash.blank?
|
23
|
+
end
|
24
|
+
|
25
|
+
waiver_data_hash
|
26
|
+
rescue CSV::MalformedCSVError => e
|
27
|
+
raise "Error reading InSpec waivers in CSV: #{e}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.headers
|
31
|
+
@headers
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "roo"
|
2
|
+
require "roo-xls"
|
3
|
+
|
4
|
+
module Waivers
|
5
|
+
class ExcelFileReader
|
6
|
+
def self.resolve(path)
|
7
|
+
return nil unless File.file?(path)
|
8
|
+
|
9
|
+
@headers ||= []
|
10
|
+
fetch_data(path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.fetch_data(path)
|
14
|
+
waiver_data_hash = {}
|
15
|
+
file_extension = File.extname(path) == ".xlsx" ? :xlsx : :xls
|
16
|
+
excel_file = Roo::Spreadsheet.open(path, extension: file_extension)
|
17
|
+
excel_file.sheet(0).parse(headers: true).each_with_index do |row, index|
|
18
|
+
if index == 0
|
19
|
+
@headers = row.keys
|
20
|
+
else
|
21
|
+
row_hash = row
|
22
|
+
control_id = row_hash["control_id"]
|
23
|
+
# delete keys and values not required in final hash
|
24
|
+
row_hash.delete("control_id")
|
25
|
+
row_hash.delete_if { |k, v| k.nil? || v.nil? }
|
26
|
+
end
|
27
|
+
|
28
|
+
waiver_data_hash[control_id] = row_hash if control_id && !row_hash.blank?
|
29
|
+
end
|
30
|
+
waiver_data_hash
|
31
|
+
rescue Exception => e
|
32
|
+
raise "Error reading InSpec waivers in Excel: #{e}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.headers
|
36
|
+
@headers
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Waivers
|
2
|
+
class JSONFileReader
|
3
|
+
def self.resolve(path)
|
4
|
+
return nil unless File.file?(path)
|
5
|
+
|
6
|
+
fetch_data(path)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.fetch_data(path)
|
10
|
+
JSON.parse(File.read(path))
|
11
|
+
rescue JSON::ParserError => e
|
12
|
+
raise "Error reading InSpec waivers in JSON: #{e}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/inspec/version.rb
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
require "inspec/secrets/yaml"
|
2
|
+
require "inspec/utils/waivers/csv_file_reader"
|
3
|
+
require "inspec/utils/waivers/json_file_reader"
|
4
|
+
|
5
|
+
module Inspec
|
6
|
+
class WaiverFileReader
|
7
|
+
|
8
|
+
def self.fetch_waivers_by_profile(profile_id, files)
|
9
|
+
read_waivers_from_file(profile_id, files) if @waivers_data.nil? || @waivers_data[profile_id].nil?
|
10
|
+
@waivers_data[profile_id]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.read_waivers_from_file(profile_id, files)
|
14
|
+
@waivers_data ||= {}
|
15
|
+
output = {}
|
16
|
+
|
17
|
+
files.each do |file_path|
|
18
|
+
file_extension = File.extname(file_path)
|
19
|
+
data = nil
|
20
|
+
if [".yaml", ".yml"].include? file_extension
|
21
|
+
data = Secrets::YAML.resolve(file_path)
|
22
|
+
data = data.inputs unless data.nil?
|
23
|
+
validate_json_yaml(data)
|
24
|
+
elsif file_extension == ".csv"
|
25
|
+
data = Waivers::CSVFileReader.resolve(file_path)
|
26
|
+
headers = Waivers::CSVFileReader.headers
|
27
|
+
validate_headers(headers)
|
28
|
+
elsif file_extension == ".json"
|
29
|
+
data = Waivers::JSONFileReader.resolve(file_path)
|
30
|
+
validate_json_yaml(data)
|
31
|
+
end
|
32
|
+
output.merge!(data) if !data.nil? && data.is_a?(Hash)
|
33
|
+
|
34
|
+
if data.nil?
|
35
|
+
raise Inspec::Exceptions::WaiversFileNotReadable,
|
36
|
+
"Cannot find parser for waivers file '#{file_path}'. " \
|
37
|
+
"Check to make sure file has the appropriate extension."
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@waivers_data[profile_id] = output
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.validate_headers(headers, json_yaml = false)
|
45
|
+
required_fields = json_yaml ? %w{justification} : %w{control_id justification}
|
46
|
+
all_fields = %w{control_id justification expiration_date run}
|
47
|
+
|
48
|
+
Inspec::Log.warn "Missing column headers: #{(required_fields - headers)}" unless (required_fields - headers).empty?
|
49
|
+
Inspec::Log.warn "Invalid column header: Column can't be nil" if headers.include? nil
|
50
|
+
Inspec::Log.warn "Extra column headers: #{(headers - all_fields)}" unless (headers - all_fields).empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.validate_json_yaml(data)
|
54
|
+
headers = []
|
55
|
+
data.each_value do |value|
|
56
|
+
headers.push value.keys
|
57
|
+
end
|
58
|
+
validate_headers(headers.flatten.uniq, true)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/matchers/matchers.rb
CHANGED
@@ -148,7 +148,7 @@ RSpec::Matchers.define :be_resolvable do
|
|
148
148
|
end
|
149
149
|
end
|
150
150
|
|
151
|
-
# matcher for iptables and
|
151
|
+
# matcher for iptables, ip6tables and nftables
|
152
152
|
RSpec::Matchers.define :have_rule do |rule|
|
153
153
|
match do |tables|
|
154
154
|
tables.has_rule?(rule)
|
@@ -163,6 +163,13 @@ RSpec::Matchers.define :have_rule do |rule|
|
|
163
163
|
end
|
164
164
|
end
|
165
165
|
|
166
|
+
# matcher for nftables sets
|
167
|
+
RSpec::Matchers.define :have_element do |elem|
|
168
|
+
match do |sets|
|
169
|
+
sets.has_element?(elem)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
166
173
|
# `be_in` matcher
|
167
174
|
# You can use it in the following cases:
|
168
175
|
# - check if an item or array is included in a given array
|
@@ -225,7 +232,11 @@ RSpec::Matchers.define :cmp do |first_expected| # rubocop:disable Metrics/BlockL
|
|
225
232
|
end
|
226
233
|
|
227
234
|
def boolean?(value)
|
228
|
-
|
235
|
+
if value.respond_to?("downcase")
|
236
|
+
%w{true false}.include?(value.downcase)
|
237
|
+
else
|
238
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
239
|
+
end
|
229
240
|
end
|
230
241
|
|
231
242
|
def version?(value)
|
@@ -252,6 +263,8 @@ RSpec::Matchers.define :cmp do |first_expected| # rubocop:disable Metrics/BlockL
|
|
252
263
|
return actual.send(op, expected.to_i)
|
253
264
|
elsif expected.is_a?(String) && boolean?(expected) && [true, false].include?(actual)
|
254
265
|
return actual.send(op, to_boolean(expected))
|
266
|
+
elsif boolean?(expected) && %w{true false}.include?(actual)
|
267
|
+
return actual.send(op, expected.to_s)
|
255
268
|
elsif expected.is_a?(Integer) && actual.is_a?(String) && integer?(actual)
|
256
269
|
return actual.to_i.send(op, expected)
|
257
270
|
elsif expected.is_a?(Float) && float?(actual)
|