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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +19 -17
  3. data/inspec-core.gemspec +22 -22
  4. data/lib/inspec/base_cli.rb +19 -17
  5. data/lib/inspec/cli.rb +27 -25
  6. data/lib/inspec/dependencies/dependency_set.rb +2 -2
  7. data/lib/inspec/dsl.rb +9 -5
  8. data/lib/inspec/enhanced_outcomes.rb +19 -0
  9. data/lib/inspec/env_printer.rb +1 -1
  10. data/lib/inspec/exceptions.rb +2 -0
  11. data/lib/inspec/formatters/base.rb +69 -16
  12. data/lib/inspec/plugin/v2/loader.rb +19 -8
  13. data/lib/inspec/plugin/v2/plugin_types/reporter.rb +1 -0
  14. data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +54 -0
  15. data/lib/inspec/profile.rb +9 -8
  16. data/lib/inspec/reporters/base.rb +1 -0
  17. data/lib/inspec/reporters/cli.rb +94 -3
  18. data/lib/inspec/reporters/json.rb +3 -1
  19. data/lib/inspec/reporters/yaml.rb +3 -1
  20. data/lib/inspec/reporters.rb +2 -1
  21. data/lib/inspec/resources/file.rb +1 -1
  22. data/lib/inspec/resources/http.rb +5 -5
  23. data/lib/inspec/resources/lxc.rb +65 -9
  24. data/lib/inspec/resources/mongodb_session.rb +5 -0
  25. data/lib/inspec/resources/nftables.rb +251 -0
  26. data/lib/inspec/resources/oracledb_session.rb +13 -4
  27. data/lib/inspec/resources/podman.rb +353 -0
  28. data/lib/inspec/resources/podman_container.rb +84 -0
  29. data/lib/inspec/resources/podman_image.rb +108 -0
  30. data/lib/inspec/resources/podman_network.rb +81 -0
  31. data/lib/inspec/resources/podman_pod.rb +101 -0
  32. data/lib/inspec/resources/podman_volume.rb +87 -0
  33. data/lib/inspec/resources/postgres_session.rb +2 -1
  34. data/lib/inspec/resources/service.rb +1 -1
  35. data/lib/inspec/resources.rb +1 -0
  36. data/lib/inspec/rule.rb +54 -17
  37. data/lib/inspec/run_data/control.rb +6 -0
  38. data/lib/inspec/run_data/statistics.rb +8 -2
  39. data/lib/inspec/runner.rb +18 -8
  40. data/lib/inspec/runner_rspec.rb +3 -2
  41. data/lib/inspec/schema/exec_json.rb +78 -2
  42. data/lib/inspec/schema/output_schema.rb +4 -1
  43. data/lib/inspec/schema/profile_json.rb +46 -0
  44. data/lib/inspec/schema.rb +91 -0
  45. data/lib/inspec/utils/convert.rb +8 -0
  46. data/lib/inspec/utils/podman.rb +24 -0
  47. data/lib/inspec/utils/simpleconfig.rb +10 -2
  48. data/lib/inspec/utils/waivers/csv_file_reader.rb +34 -0
  49. data/lib/inspec/utils/waivers/excel_file_reader.rb +39 -0
  50. data/lib/inspec/utils/waivers/json_file_reader.rb +15 -0
  51. data/lib/inspec/version.rb +1 -1
  52. data/lib/inspec/waiver_file_reader.rb +61 -0
  53. data/lib/matchers/matchers.rb +15 -2
  54. data/lib/plugins/inspec-init/templates/profiles/alicloud/README.md +27 -0
  55. data/lib/plugins/inspec-init/templates/profiles/alicloud/controls/example.rb +10 -0
  56. data/lib/plugins/inspec-init/templates/profiles/alicloud/inputs.yml +1 -0
  57. data/lib/plugins/inspec-init/templates/profiles/alicloud/inspec.yml +14 -0
  58. data/lib/plugins/inspec-reporter-html2/README.md +1 -1
  59. data/lib/plugins/inspec-reporter-html2/templates/body.html.erb +7 -1
  60. data/lib/plugins/inspec-reporter-html2/templates/control.html.erb +10 -6
  61. data/lib/plugins/inspec-reporter-html2/templates/default.css +12 -0
  62. data/lib/plugins/inspec-reporter-html2/templates/selector.html.erb +7 -1
  63. data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +5 -2
  64. data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +39 -13
  65. 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
- 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
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
 
@@ -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
- @vals[m[1]].push(parse_values(m, opts[:key_values]))
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]] = parse_values(m, opts[:key_values])
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
@@ -1,3 +1,3 @@
1
1
  module Inspec
2
- VERSION = "5.18.14".freeze
2
+ VERSION = "5.22.3".freeze
3
3
  end
@@ -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
@@ -148,7 +148,7 @@ RSpec::Matchers.define :be_resolvable do
148
148
  end
149
149
  end
150
150
 
151
- # matcher for iptables and ip6tables
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
- %w{true false}.include?(value.downcase)
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)