inspec-core 5.18.14 → 5.22.3

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