inspec-core 5.22.65 → 6.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/Chef-EULA +9 -0
  3. data/Gemfile +24 -32
  4. data/etc/features.sig +6 -0
  5. data/etc/features.yaml +94 -0
  6. data/inspec-core.gemspec +15 -14
  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/nftables.rb +1 -14
  31. data/lib/inspec/resources/oracledb_session.rb +3 -9
  32. data/lib/inspec/resources/postgres_session.rb +1 -1
  33. data/lib/inspec/resources/sybase_session.rb +2 -11
  34. data/lib/inspec/resources/virtualization.rb +1 -1
  35. data/lib/inspec/rule.rb +9 -14
  36. data/lib/inspec/run_data.rb +7 -5
  37. data/lib/inspec/runner.rb +35 -6
  38. data/lib/inspec/runner_rspec.rb +12 -9
  39. data/lib/inspec/secrets/yaml.rb +9 -3
  40. data/lib/inspec/shell.rb +10 -0
  41. data/lib/inspec/ui.rb +4 -0
  42. data/lib/inspec/utils/licensing_config.rb +9 -0
  43. data/lib/inspec/utils/profile_ast_helpers.rb +2 -1
  44. data/lib/inspec/utils/waivers/csv_file_reader.rb +1 -1
  45. data/lib/inspec/utils/waivers/excel_file_reader.rb +1 -1
  46. data/lib/inspec/version.rb +1 -1
  47. data/lib/inspec/waiver_file_reader.rb +68 -27
  48. data/lib/inspec.rb +2 -1
  49. data/lib/matchers/matchers.rb +3 -3
  50. data/lib/plugins/inspec-compliance/README.md +1 -11
  51. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +189 -170
  52. data/lib/plugins/inspec-habitat/lib/inspec-habitat/cli.rb +10 -3
  53. data/lib/plugins/inspec-init/lib/inspec-init/cli.rb +1 -0
  54. data/lib/plugins/inspec-init/lib/inspec-init/cli_plugin.rb +23 -21
  55. data/lib/plugins/inspec-init/lib/inspec-init/cli_profile.rb +15 -13
  56. data/lib/plugins/inspec-init/lib/inspec-init/cli_resource.rb +15 -13
  57. data/lib/plugins/inspec-license/README.md +16 -0
  58. data/lib/plugins/inspec-license/inspec-license.gemspec +6 -0
  59. data/lib/plugins/inspec-license/lib/inspec-license/cli.rb +26 -0
  60. data/lib/plugins/inspec-license/lib/inspec-license.rb +14 -0
  61. data/lib/plugins/inspec-parallel/README.md +27 -0
  62. data/lib/plugins/inspec-parallel/inspec-parallel.gemspec +6 -0
  63. data/lib/plugins/inspec-parallel/lib/inspec-parallel/child_status_reporter.rb +61 -0
  64. data/lib/plugins/inspec-parallel/lib/inspec-parallel/cli.rb +39 -0
  65. data/lib/plugins/inspec-parallel/lib/inspec-parallel/command.rb +219 -0
  66. data/lib/plugins/inspec-parallel/lib/inspec-parallel/runner.rb +265 -0
  67. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/base.rb +24 -0
  68. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/silent.rb +7 -0
  69. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/status.rb +124 -0
  70. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/text.rb +23 -0
  71. data/lib/plugins/inspec-parallel/lib/inspec-parallel/validator.rb +170 -0
  72. data/lib/plugins/inspec-parallel/lib/inspec-parallel.rb +18 -0
  73. data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +10 -11
  74. data/lib/plugins/inspec-sign/lib/inspec-sign/cli.rb +11 -4
  75. data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +6 -13
  76. data/lib/source_readers/inspec.rb +1 -1
  77. metadata +45 -19
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