inspec-core 5.22.72 → 6.6.0

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/Chef-EULA +9 -0
  3. data/Gemfile +24 -38
  4. data/etc/features.sig +6 -0
  5. data/etc/features.yaml +94 -0
  6. data/inspec-core.gemspec +16 -15
  7. data/lib/inspec/backend.rb +2 -0
  8. data/lib/inspec/base_cli.rb +80 -4
  9. data/lib/inspec/cached_fetcher.rb +24 -3
  10. data/lib/inspec/cli.rb +292 -235
  11. data/lib/inspec/config.rb +24 -11
  12. data/lib/inspec/dependencies/cache.rb +33 -0
  13. data/lib/inspec/dependencies/dependency_set.rb +2 -2
  14. data/lib/inspec/dsl.rb +1 -1
  15. data/lib/inspec/enhanced_outcomes.rb +1 -0
  16. data/lib/inspec/errors.rb +5 -0
  17. data/lib/inspec/exceptions.rb +2 -0
  18. data/lib/inspec/feature/config.rb +75 -0
  19. data/lib/inspec/feature/runner.rb +26 -0
  20. data/lib/inspec/feature.rb +34 -0
  21. data/lib/inspec/fetcher/git.rb +5 -0
  22. data/lib/inspec/fetcher/url.rb +7 -29
  23. data/lib/inspec/globals.rb +6 -0
  24. data/lib/inspec/input_registry.rb +1 -5
  25. data/lib/inspec/plugin/v1/plugin_types/fetcher.rb +7 -0
  26. data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +30 -2
  27. data/lib/inspec/profile.rb +46 -3
  28. data/lib/inspec/reporters/cli.rb +1 -1
  29. data/lib/inspec/reporters.rb +67 -54
  30. data/lib/inspec/resources/groups.rb +0 -52
  31. data/lib/inspec/resources/nftables.rb +1 -14
  32. data/lib/inspec/resources/oracledb_session.rb +3 -9
  33. data/lib/inspec/resources/postgres_session.rb +5 -9
  34. data/lib/inspec/resources/sybase_session.rb +2 -11
  35. data/lib/inspec/resources/virtualization.rb +1 -1
  36. data/lib/inspec/rule.rb +9 -14
  37. data/lib/inspec/run_data.rb +7 -5
  38. data/lib/inspec/runner.rb +35 -6
  39. data/lib/inspec/runner_rspec.rb +12 -9
  40. data/lib/inspec/secrets/yaml.rb +9 -3
  41. data/lib/inspec/shell.rb +10 -0
  42. data/lib/inspec/ui.rb +4 -0
  43. data/lib/inspec/utils/licensing_config.rb +9 -0
  44. data/lib/inspec/utils/profile_ast_helpers.rb +2 -1
  45. data/lib/inspec/utils/waivers/csv_file_reader.rb +1 -1
  46. data/lib/inspec/utils/waivers/excel_file_reader.rb +1 -1
  47. data/lib/inspec/version.rb +1 -1
  48. data/lib/inspec/waiver_file_reader.rb +68 -27
  49. data/lib/inspec.rb +2 -1
  50. data/lib/matchers/matchers.rb +3 -3
  51. data/lib/plugins/inspec-compliance/README.md +1 -11
  52. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +189 -170
  53. data/lib/plugins/inspec-habitat/lib/inspec-habitat/cli.rb +10 -3
  54. data/lib/plugins/inspec-init/lib/inspec-init/cli.rb +1 -0
  55. data/lib/plugins/inspec-init/lib/inspec-init/cli_plugin.rb +23 -21
  56. data/lib/plugins/inspec-init/lib/inspec-init/cli_profile.rb +15 -13
  57. data/lib/plugins/inspec-init/lib/inspec-init/cli_resource.rb +15 -13
  58. data/lib/plugins/inspec-license/README.md +16 -0
  59. data/lib/plugins/inspec-license/inspec-license.gemspec +6 -0
  60. data/lib/plugins/inspec-license/lib/inspec-license/cli.rb +26 -0
  61. data/lib/plugins/inspec-license/lib/inspec-license.rb +14 -0
  62. data/lib/plugins/inspec-parallel/README.md +27 -0
  63. data/lib/plugins/inspec-parallel/inspec-parallel.gemspec +6 -0
  64. data/lib/plugins/inspec-parallel/lib/inspec-parallel/child_status_reporter.rb +61 -0
  65. data/lib/plugins/inspec-parallel/lib/inspec-parallel/cli.rb +39 -0
  66. data/lib/plugins/inspec-parallel/lib/inspec-parallel/command.rb +219 -0
  67. data/lib/plugins/inspec-parallel/lib/inspec-parallel/runner.rb +265 -0
  68. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/base.rb +24 -0
  69. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/silent.rb +7 -0
  70. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/status.rb +124 -0
  71. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/text.rb +23 -0
  72. data/lib/plugins/inspec-parallel/lib/inspec-parallel/validator.rb +170 -0
  73. data/lib/plugins/inspec-parallel/lib/inspec-parallel.rb +18 -0
  74. data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +10 -11
  75. data/lib/plugins/inspec-sign/lib/inspec-sign/cli.rb +11 -4
  76. data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +6 -13
  77. data/lib/source_readers/inspec.rb +1 -1
  78. metadata +45 -25
data/lib/inspec/config.rb CHANGED
@@ -7,6 +7,7 @@ require "forwardable" unless defined?(Forwardable)
7
7
  require "thor" unless defined?(Thor)
8
8
  require "base64" unless defined?(Base64)
9
9
  require "inspec/plugin/v2/filter"
10
+ require "inspec/feature"
10
11
 
11
12
  module Inspec
12
13
  class Config
@@ -25,6 +26,12 @@ module Inspec
25
26
  shell_command
26
27
  }.freeze
27
28
 
29
+ AUDIT_LOG_OPTIONS = %w{
30
+ audit_log_location
31
+ enable_audit_log
32
+ audit_log_app_name
33
+ }.freeze
34
+
28
35
  KNOWN_VERSIONS = [
29
36
  "1.1",
30
37
  "1.2",
@@ -80,6 +87,10 @@ module Inspec
80
87
  puts
81
88
  end
82
89
 
90
+ def allow_unsigned_profiles?
91
+ self["allow_unsigned_profiles"] || ENV["CHEF_ALLOW_UNSIGNED_PROFILES"]
92
+ end
93
+
83
94
  # return all telemetry options from config
84
95
  # @return [Hash]
85
96
  def telemetry_options
@@ -109,11 +120,14 @@ module Inspec
109
120
  def unpack_train_credentials
110
121
  # Internally, use indifferent access while we build the creds
111
122
  credentials = Thor::CoreExt::HashWithIndifferentAccess.new({})
112
-
113
123
  # Helper methods prefixed with _utc_ (Unpack Train Credentials)
114
124
 
115
125
  credentials.merge!(_utc_generic_credentials)
116
126
 
127
+ # Only runs this block when preview flag CHEF_PREVIEW_AUDIT_LOGGING is set
128
+ Inspec.with_feature("inspec-audit-logging") {
129
+ credentials.merge!(_utc_merge_audit_log_options)
130
+ }
117
131
  _utc_determine_backend(credentials)
118
132
  transport_name = credentials[:backend].to_s
119
133
 
@@ -154,13 +168,21 @@ module Inspec
154
168
 
155
169
  private
156
170
 
171
+ def _utc_merge_audit_log_options
172
+ @final_options.select { |option, _value| AUDIT_LOG_OPTIONS.include?(option) }
173
+ end
174
+
157
175
  def _utc_merge_transport_options(credentials, transport_name)
158
176
  # Ask Train for the names of the transport options
159
177
  transport_options = Train.options(transport_name).keys.map(&:to_s)
160
178
 
161
179
  # If there are any options with those (unprefixed) names, merge them in.
180
+ # e.g., 'host'
162
181
  unprefixed_transport_options = final_options.select do |option_name, _value|
163
- transport_options.include? option_name # e.g., 'host'
182
+ # We currently want this option only to be set if CHEF_PREVIEW_AUDIT_LOGGING is set
183
+ # and we already merging the audit_log_options within _utc_merge_audit_log_options method which only invoke if feature flag is on
184
+ # we don't want audit log option to get set from here again so this is a safe check and can be removed when we remove preview feature flag.
185
+ transport_options.include? option_name unless option_name.match?("enable_audit_log")
164
186
  end
165
187
  credentials.merge!(unprefixed_transport_options)
166
188
 
@@ -448,15 +470,6 @@ module Inspec
448
470
  # Reporter options may be defined top-level.
449
471
  options.merge!(config_file_reporter_options)
450
472
 
451
- # when sent reporter from compliance-mode (via chef-client), the reporter is a symbol
452
- if @cli_opts.key?(:reporter) && @cli_opts["reporter"].nil?
453
- @cli_opts["reporter"] = @cli_opts[:reporter]
454
- @cli_opts.delete(:reporter)
455
- elsif @cli_opts.key?(:reporter) && @cli_opts.key?("reporter") && @cli_opts["reporter"].is_a?(Array)
456
- # combine reporter and "reporter" options into "reporter" option
457
- @cli_opts["reporter"] = @cli_opts[:reporter] + @cli_opts["reporter"]
458
- end
459
-
460
473
  if @cli_opts["reporter"]
461
474
  # Add reporter_cli_opts in options to capture reporter cli opts separately
462
475
  options.merge!({ "reporter_cli_opts" => @cli_opts["reporter"] })
@@ -70,5 +70,38 @@ module Inspec
70
70
  def base_path_for(cache_key)
71
71
  File.join(@path, cache_key)
72
72
  end
73
+
74
+ #
75
+ # For given cache key, return true if the
76
+ # cache path is locked
77
+ def locked?(key)
78
+ locked = false
79
+ path = base_path_for(key)
80
+ # For archive there is no need to lock the directory so we skip those and return false for archive formatted cache
81
+ if File.directory?(path)
82
+ locked = File.exist?("#{path}/.lock")
83
+ end
84
+ locked
85
+ end
86
+
87
+ def lock(cache_path)
88
+ lock_file_path = File.join(cache_path, ".lock")
89
+ begin
90
+ FileUtils.mkdir_p(cache_path)
91
+ Inspec::Log.debug("Locking cache ..... #{cache_path}")
92
+ FileUtils.touch(lock_file_path)
93
+ rescue Errno::EACCES
94
+ raise "Permission denied while creating cache lock #{cache_path}/.lock."
95
+ end
96
+ end
97
+
98
+ def unlock(cache_path)
99
+ Inspec::Log.debug("Unlocking cache..... #{cache_path}")
100
+ begin
101
+ FileUtils.rm("#{cache_path}/.lock") if File.exist?("#{cache_path}/.lock")
102
+ rescue Errno::EACCES
103
+ raise "Permission denied while removing cache lock #{cache_path}/.lock"
104
+ end
105
+ end
73
106
  end
74
107
  end
@@ -26,7 +26,7 @@ module Inspec
26
26
  dep_list = {}
27
27
  dependencies.each do |d|
28
28
  # if depedent profile does not have a source version then only name is used in dependency hash
29
- key_name = ((d.source_version.nil? || d.source_version.empty?) ? "#{d.name}" : "#{d.name}-#{d.source_version}") rescue "#{d.name}"
29
+ key_name = (d.source_version.blank? ? "#{d.name}" : "#{d.name}-#{d.source_version}") rescue "#{d.name}"
30
30
  dep_list[key_name] = d
31
31
  end
32
32
  new(cwd, cache, dep_list, backend)
@@ -42,7 +42,7 @@ module Inspec
42
42
  dep_list = {}
43
43
  dep_tree.each do |d|
44
44
  # if depedent profile does not have a source version then only name is used in dependency hash
45
- key_name = ((d.source_version.nil? || d.source_version.empty?) ? "#{d.name}" : "#{d.name}-#{d.source_version}") rescue "#{d.name}"
45
+ key_name = (d.source_version.blank? ? "#{d.name}" : "#{d.name}-#{d.source_version}") rescue "#{d.name}"
46
46
  dep_list[key_name] = d
47
47
  dep_list.merge!(flatten_dep_tree(d.dependencies))
48
48
  end
data/lib/inspec/dsl.rb CHANGED
@@ -95,7 +95,7 @@ module Inspec::DSL
95
95
  # 1. Fetching VERSION from a profile dependency name which is in a format NAME-VERSION.
96
96
  # 2. Matching original profile dependency name with profile name used with include or require control DSL.
97
97
  source_version = value.source_version
98
- unless source_version.nil? || source_version.empty?
98
+ unless source_version.blank?
99
99
  profile_id_key = key.split("-#{source_version}")[0]
100
100
  new_profile_id = key if profile_id_key == profile_id
101
101
  end
@@ -15,5 +15,6 @@ module Inspec
15
15
  "passed"
16
16
  end
17
17
  end
18
+
18
19
  end
19
20
  end
data/lib/inspec/errors.rb CHANGED
@@ -24,4 +24,9 @@ module Inspec
24
24
  end
25
25
 
26
26
  class InvalidProfileSignature < Error; end
27
+
28
+ class FeatureConfigMissingError < Error; end
29
+ class FeatureConfigTamperedError < Error; end
30
+
31
+ class ProfileSignatureRequired < Error; end
27
32
  end
@@ -12,5 +12,7 @@ module Inspec
12
12
  class ProfileSigningKeyNotFound < ArgumentError; end
13
13
  class WaiversFileNotReadable < ArgumentError; end
14
14
  class WaiversFileDoesNotExist < ArgumentError; end
15
+ class WaiversFileInvalidFormatting < ArgumentError; end
16
+ class InvalidAuditLogOption < ArgumentError; end
15
17
  end
16
18
  end
@@ -0,0 +1,75 @@
1
+ require "inspec/iaf_file" # Uses some of the same encryption routines
2
+
3
+ module Inspec
4
+ class Feature
5
+ class Config
6
+
7
+ VERIFICATION_KEY_NAME = "progress-2022-05-04".freeze
8
+
9
+ attr_reader :cfg_data, :valid
10
+
11
+ def initialize(conf_path = nil)
12
+ # If conf path is nil, read from source installation
13
+ conf_path ||= File.join(Inspec.src_root, "etc", "features.yaml")
14
+
15
+ # Verify path and sig file exists or else throw exception
16
+ sig_path = conf_path.sub(/\.yaml/, ".sig")
17
+ [conf_path, sig_path].each do |file|
18
+ raise Inspec::FeatureConfigMissingError.new("No such file #{file}") unless File.exist?(file)
19
+ end
20
+
21
+ # Verify sig matches contents
22
+ validation_key_path = Inspec::IafFile.find_validation_key(VERIFICATION_KEY_NAME)
23
+ verification_key = Inspec::IafFile::KEY_ALG.new File.read validation_key_path
24
+ signature = Base64.decode64 File.read sig_path
25
+ digest = Inspec::IafFile::ARTIFACT_DIGEST.new
26
+ unless verification_key.verify digest, signature, File.read(conf_path)
27
+ # If not load default empty config and raise exception
28
+ @cfg_data = load_error_data
29
+ raise Inspec::FeatureConfigTamperedError.new("Feature yaml file does not match signature - tampered?")
30
+ end
31
+
32
+ # Read YAML data from path
33
+ @cfg_data = YAML.load_file(conf_path)
34
+ @features_by_name = {}
35
+ end
36
+
37
+ def with_each_feature
38
+ cfg_data["features"].each do |feature_name, raw_info|
39
+ feat = @features_by_name[feature_name] ||= Inspec::Feature.new(feature_name.to_sym, raw_info)
40
+ yield(feat)
41
+ end
42
+ end
43
+
44
+ def [](feature_name)
45
+ raw_info = cfg_data["features"][feature_name]
46
+ return nil unless raw_info
47
+
48
+ @features_by_name[feature_name] ||= Inspec::Feature.new(feature_name.to_sym, raw_info)
49
+ end
50
+
51
+ def feature_name?(query)
52
+ cfg_data["features"].key?(query.to_s)
53
+ end
54
+
55
+ def features
56
+ @features ||= load_features
57
+ end
58
+
59
+ private
60
+
61
+ def load_features
62
+ feats = []
63
+ with_each_feature { |f| feats << f }
64
+ feats
65
+ end
66
+
67
+ # Default data for when the config is in an error state.
68
+ def load_error_data
69
+ {
70
+ "features": {},
71
+ }
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,26 @@
1
+ module Inspec
2
+ class Feature
3
+ class Runner
4
+ def self.with_feature(feature_name, opts = {}, &feature_implementation)
5
+ config = opts[:config] || Inspec::Feature::Config.new
6
+ logger = opts[:logger] || Inspec::Log
7
+
8
+ # Emit log message saying we're running a feature
9
+ logger.debug("Prepping to run feature '#{feature_name}'")
10
+
11
+ # Validate that the feature is recognized
12
+ feature = config[feature_name]
13
+ unless feature
14
+ logger.warn "Unrecognized feature name '#{feature_name}'"
15
+ end
16
+
17
+ # If the feature is not recognized
18
+ # If the feature has no env_preview flag set in config
19
+ # If the feature is previewable
20
+ if feature.nil? || feature&.no_preview? || feature&.previewable?
21
+ yield feature_implementation
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ require_relative "feature/config"
2
+ require_relative "feature/runner"
3
+
4
+ module Inspec
5
+ def self.with_feature(feature_name, opts = {}, &feature_implementation)
6
+ Inspec::Feature::Runner.with_feature(feature_name, opts, &feature_implementation)
7
+ end
8
+
9
+ class Feature
10
+ attr_reader :name, :description, :env_preview
11
+ def initialize(feature_name, feature_yaml_opts)
12
+ @name = feature_name
13
+ feature_yaml_opts ||= {}
14
+ @description = feature_yaml_opts["description"]
15
+ @env_preview = feature_yaml_opts["env_preview"]
16
+ end
17
+
18
+ def previewable?
19
+ # If the feature is previewable in config (features.yaml) & has an environment value set to use previewed feature
20
+ !!env_preview && !env_preview_value.nil?
21
+ end
22
+
23
+ def no_preview?
24
+ env_preview.nil?
25
+ end
26
+
27
+ def env_preview_value
28
+ # Examples: If feature name is "inspec-test-feature"
29
+ # ENV used for this feature preview would be CHEF_PREVIEW_TEST_FEATURE
30
+ env_preview_feature_name = name.to_s.split("inspec-")[-1]
31
+ ENV["CHEF_PREVIEW_#{env_preview_feature_name.gsub("-", "_").upcase}"]
32
+ end
33
+ end
34
+ end
@@ -127,6 +127,11 @@ module Inspec::Fetcher
127
127
  %i{branch tag ref}.map { |opt_name| update_ivar_from_opt(opt_name, opts) }.any?
128
128
  end
129
129
 
130
+ # Git fetcher is sensitive to cache contention so it needs cache locking mechanism.
131
+ def requires_locking?
132
+ true
133
+ end
134
+
130
135
  private
131
136
 
132
137
  def resolved_ref
@@ -93,7 +93,7 @@ module Inspec::Fetcher
93
93
  end
94
94
 
95
95
  if transformed_target
96
- Inspec::Log.debug("URL target #{target} transformed to #{transformed_target}. Consider using the git fetcher")
96
+ Inspec::Log.warn("URL target #{target} transformed to #{transformed_target}. Consider using the git fetcher")
97
97
  transformed_target
98
98
  else
99
99
  target
@@ -133,14 +133,12 @@ module Inspec::Fetcher
133
133
  class << self
134
134
  def default_ref(match_data, repo_url)
135
135
  remote_url = "#{repo_url}/#{match_data[:user]}/#{match_data[:repo]}.git"
136
- command_string = "git ls-remote #{remote_url} HEAD"
136
+ command_string = "git remote show #{remote_url}"
137
137
  cmd = shellout(command_string)
138
138
  unless cmd.exitstatus == 0
139
139
  raise(Inspec::FetcherFailure, "Profile git dependency failed with default reference - #{remote_url} - error running '#{command_string}': #{cmd.stderr}")
140
140
  else
141
- # cmd.stdout of "git ls-remote #{remote_url} HEAD" looks like this:
142
- # "457d14843ab7c1c3740169eb47cf129a6f417964\tHEAD\n"
143
- ref = cmd.stdout.split("\t").first
141
+ ref = cmd.stdout.lines.detect { |l| l.include? "HEAD branch:" }&.split(":")&.last&.strip
144
142
  unless ref
145
143
  raise(Inspec::FetcherFailure, "Profile git dependency failed with default reference - #{remote_url} - error running '#{command_string}': NULL reference")
146
144
  end
@@ -248,30 +246,10 @@ module Inspec::Fetcher
248
246
  @temp_archive_path = archive.path
249
247
  end
250
248
 
251
- # Opens a URI or local file specified by `target` with options `opts`.
252
- # If `target` is a valid URI (http://, https://, ftp://), opens it using URI.open.
253
- # If `target` is a local file path, opens it using File.open.
254
- # Raises ArgumentError for invalid `target` that is neither a valid URI nor a local file path.
255
- # Logs or handles exceptions gracefully using `pretty_handle_exception`.
256
- def open(target, opts)
257
- if valid_uri?(target)
258
- URI(target).open(opts) # Open URI if it's a valid HTTP, HTTPS, or FTP URI
259
- elsif File.file?(target)
260
- File.open(target, opts) # Open local file if it exists
261
- else
262
- raise ArgumentError, "Invalid target: #{target}. Must be a valid URI or a local file path."
263
- end
264
- rescue StandardError => e
265
- raise Inspec::FetcherFailure, "Profile URL dependency #{target} could not be fetched: #{e.message}"
266
- end
267
-
268
- # Checks if the given `target` string is a valid URI by attempting to parse it.
269
- # Returns true if `target` is a valid HTTP, HTTPS, or FTP URI; false otherwise.
270
- def valid_uri?(target)
271
- uri = URI.parse(target)
272
- uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) || uri.is_a?(URI::FTP)
273
- rescue URI::InvalidURIError
274
- false
249
+ def open(target, opts) # overridden so we can control who we're talking to
250
+ URI.open(target, opts)
251
+ rescue NoMethodError # TODO: remove when we drop ruby 2.4
252
+ super(target, opts) # Kernel#open
275
253
  end
276
254
 
277
255
  def open_via_uri(target)
@@ -23,4 +23,10 @@ module Inspec
23
23
  require "rbconfig" unless defined?(RbConfig)
24
24
  RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
25
25
  end
26
+
27
+ def self.log_dir
28
+ log_dir_path = "#{config_dir}/logs"
29
+ FileUtils.mkdir_p(log_dir_path) unless File.directory?(log_dir_path)
30
+ log_dir_path
31
+ end
26
32
  end
@@ -189,11 +189,7 @@ module Inspec
189
189
  def parse_cli_input_value(input_name, given_value)
190
190
  value = given_value.chomp(",") # Trim trailing comma if any
191
191
  case value
192
- # Changed regex to use \A and \z instead of ^ and $ for stricter start and end of string matching.
193
- # This prevents potential bypass issues with multi-line input and ensures the entire string
194
- # is exactly "true" or "false", enhancing security when dealing with untrusted input.
195
- # Issue detected here: https://github.com/inspec/inspec/security/code-scanning/41
196
- when /\A(true|false)\z/i
192
+ when /^true|false$/i
197
193
  value = !!(value =~ /true/i)
198
194
  when /^-?\d+$/
199
195
  value = value.to_i
@@ -105,6 +105,13 @@ module Inspec
105
105
  file_provider = Inspec::FileProvider.for_path(archive_path)
106
106
  file_provider.relative_provider
107
107
  end
108
+
109
+ # Returns false by default
110
+ # This is used to regulate cache contention.
111
+ # Fetchers that are sensitive to cache contention should return true.
112
+ def requires_locking?
113
+ false
114
+ end
108
115
  end
109
116
  end
110
117
  end
@@ -12,6 +12,7 @@ module Inspec::Plugin::V2::PluginType
12
12
  @control_checks_count_map = {}
13
13
  @controls_count = nil
14
14
  @notifications = {}
15
+ @enhanced_outcome_control_wise = {}
15
16
  end
16
17
 
17
18
  private
@@ -29,16 +30,43 @@ module Inspec::Plugin::V2::PluginType
29
30
 
30
31
  # method to identify when the control ended running
31
32
  # this will be useful in executing operations on control's level end
32
- def control_ended?(control_id)
33
+ def control_ended?(notification, control_id)
33
34
  set_control_checks_count_map_value
34
35
  unless @control_checks_count_map[control_id].nil?
35
36
  @control_checks_count_map[control_id] -= 1
36
- @control_checks_count_map[control_id] == 0
37
+ control_ended = @control_checks_count_map[control_id] == 0
38
+ # after a control has ended it checks for certain operations, like enhanced outcomes
39
+ run_control_operations(notification, control_id) if control_ended
40
+ control_ended
37
41
  else
38
42
  false
39
43
  end
40
44
  end
41
45
 
46
+ def run_control_operations(notification, control_id)
47
+ check_for_enhanced_outcomes(notification, control_id)
48
+ end
49
+
50
+ def check_for_enhanced_outcomes(notification, control_id)
51
+ if enhanced_outcomes
52
+ control_outcome = add_enhanced_outcomes(control_id)
53
+ @enhanced_outcome_control_wise[control_id] = control_outcome
54
+ end
55
+ end
56
+
57
+ def format_message(indicator, control_id, title, full_description)
58
+ message_to_format = ""
59
+ message_to_format += "#{indicator} "
60
+ message_to_format += "#{control_id.to_s.strip.dup.force_encoding(Encoding::UTF_8)} "
61
+ message_to_format += "#{title.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " if title
62
+ message_to_format += "#{full_description.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " unless title
63
+ message_to_format
64
+ end
65
+
66
+ def control_outcome(control_id)
67
+ @enhanced_outcome_control_wise[control_id]
68
+ end
69
+
42
70
  # method to identify total no. of controls
43
71
  def controls_count
44
72
  @controls_count ||= RSpec.configuration.formatters.grep(Inspec::Formatters::Base).first.get_controls_count
@@ -16,6 +16,7 @@ require "inspec/utils/json_profile_summary"
16
16
  require "inspec/dependency_loader"
17
17
  require "inspec/dependency_installer"
18
18
  require "inspec/utils/profile_ast_helpers"
19
+ require "plugins/inspec-sign/lib/inspec-sign/base"
19
20
 
20
21
  module Inspec
21
22
  class Profile
@@ -82,7 +83,7 @@ module Inspec
82
83
  end
83
84
 
84
85
  attr_reader :source_reader, :backend, :runner_context, :check_mode
85
- attr_accessor :parent_profile, :profile_id, :profile_name
86
+ attr_accessor :parent_profile, :profile_id, :profile_name, :target
86
87
  def_delegator :@source_reader, :tests
87
88
  def_delegator :@source_reader, :libraries
88
89
  def_delegator :@source_reader, :metadata
@@ -181,6 +182,26 @@ module Inspec
181
182
  @state == :failed
182
183
  end
183
184
 
185
+ def verify_if_signed
186
+ if signed?
187
+ verify_signed_profile
188
+ true
189
+ else
190
+ false
191
+ end
192
+ end
193
+
194
+ def signed?
195
+ # Signed profiles have .iaf extension
196
+ (@source_reader&.target&.parent&.class == Inspec::IafProvider)
197
+ end
198
+
199
+ def verify_signed_profile
200
+ # Kitchen inspec send target profile in Hash format in some scenarios. For example: While using local profile with kitchen, {:path => "path/to/kitchen/lcoal-profile"}
201
+ target_profile = target.is_a?(Hash) ? target.values[0] : target
202
+ InspecPlugins::Sign::Base.profile_verify(target_profile, true) if target_profile
203
+ end
204
+
184
205
  #
185
206
  # Is this profile is supported on the current platform of the
186
207
  # backend machine and the current inspec version.
@@ -215,8 +236,30 @@ module Inspec
215
236
  @params ||= load_params
216
237
  end
217
238
 
239
+ def virtual_profile?
240
+ # A virtual profile is for virtual profile evaluation
241
+ # Used by shell & inspec detect command.
242
+ (name == "inspec-shell") && (files&.length == 1) && (files[0] == "inspec.yml")
243
+ end
244
+
218
245
  def collect_tests
219
246
  unless @tests_collected || failed?
247
+
248
+ # This is that one common place in InSpec engine which is used to collect tests of InSpec profile
249
+ # One common place used by most of the CLI commands using profile, like exec, export etc
250
+ # Checking for profile signature in parent profile only
251
+ # Child profiles of a signed profile are extracted to cache dir
252
+ # Hence they are not in .iaf format
253
+ # Only runs this block when preview flag CHEF_PREVIEW_MANDATORY_PROFILE_SIGNING is set
254
+ Inspec.with_feature("inspec-mandatory-profile-signing") {
255
+ if !parent_profile && !virtual_profile?
256
+ cfg = Inspec::Config.cached
257
+ if cfg.is_a?(Inspec::Config) && !cfg.allow_unsigned_profiles?
258
+ raise Inspec::ProfileSignatureRequired, "Signature required for profile: #{name}. Please provide a signed profile. Or set CHEF_ALLOW_UNSIGNED_PROFILES in the environment. Or use `--allow-unsigned-profiles` flag with InSpec CLI." unless verify_if_signed
259
+ end
260
+ end
261
+ }
262
+
220
263
  return unless supports_platform?
221
264
 
222
265
  locked_dependencies.each(&:collect_tests)
@@ -248,7 +291,7 @@ module Inspec
248
291
  ## Find the waivers file
249
292
  # - TODO: cli_opts and instance_variable_get could be exposed
250
293
  waiver_paths = cfg.instance_variable_get(:@cli_opts)["waiver_file"]
251
- if waiver_paths.nil? || waiver_paths.empty?
294
+ if waiver_paths.blank?
252
295
  Inspec::Log.error "Must use --waiver-file with --filter-waived-controls"
253
296
  Inspec::UI.new.exit(:usage_error)
254
297
  end
@@ -276,7 +319,7 @@ module Inspec
276
319
  # be processed and rendered
277
320
  tests.each do |control_filename, source_code|
278
321
  cleared_tests = source_code.scan(/control\s+['"].+?['"].+?(?=(?:control\s+['"].+?['"])|\z)/m).collect do |element|
279
- next if element.nil? || element.empty?
322
+ next if element.blank?
280
323
 
281
324
  if element&.match?(waived_control_id_regex)
282
325
  splitlines = element.split("\n")
@@ -317,7 +317,7 @@ module Inspec::Reporters
317
317
  not_applicable = 0
318
318
 
319
319
  all_unique_controls.each do |control|
320
- next if control[:status].nil? || control[:status].empty?
320
+ next if control[:status].blank?
321
321
 
322
322
  if control[:status] == "failed"
323
323
  failed += 1