inspec-core 5.22.50 → 6.8.1

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/Chef-EULA +9 -0
  3. data/Gemfile +14 -4
  4. data/etc/features.sig +6 -0
  5. data/etc/features.yaml +97 -0
  6. data/inspec-core.gemspec +17 -7
  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 +293 -236
  11. data/lib/inspec/config.rb +24 -2
  12. data/lib/inspec/dependencies/cache.rb +33 -0
  13. data/lib/inspec/enhanced_outcomes.rb +1 -0
  14. data/lib/inspec/errors.rb +5 -0
  15. data/lib/inspec/exceptions.rb +2 -0
  16. data/lib/inspec/feature/config.rb +75 -0
  17. data/lib/inspec/feature/runner.rb +29 -0
  18. data/lib/inspec/feature.rb +42 -0
  19. data/lib/inspec/fetcher/git.rb +5 -0
  20. data/lib/inspec/fetcher/url.rb +24 -4
  21. data/lib/inspec/globals.rb +6 -0
  22. data/lib/inspec/iaf_file.rb +3 -2
  23. data/lib/inspec/input_registry.rb +5 -1
  24. data/lib/inspec/plugin/v1/plugin_types/fetcher.rb +7 -0
  25. data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +30 -2
  26. data/lib/inspec/profile.rb +44 -1
  27. data/lib/inspec/reporters.rb +67 -54
  28. data/lib/inspec/resources/nftables.rb +14 -1
  29. data/lib/inspec/resources/oracledb_session.rb +12 -3
  30. data/lib/inspec/resources/ssh_config.rb +100 -9
  31. data/lib/inspec/resources/ssh_key.rb +124 -0
  32. data/lib/inspec/resources/sshd_active_config.rb +2 -0
  33. data/lib/inspec/resources/sybase_session.rb +11 -2
  34. data/lib/inspec/resources.rb +1 -0
  35. data/lib/inspec/rule.rb +6 -6
  36. data/lib/inspec/run_data.rb +7 -5
  37. data/lib/inspec/runner.rb +43 -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/telemetry/base.rb +149 -0
  44. data/lib/inspec/utils/telemetry/http.rb +40 -0
  45. data/lib/inspec/utils/telemetry/null.rb +11 -0
  46. data/lib/inspec/utils/telemetry/run_context_probe.rb +13 -1
  47. data/lib/inspec/utils/telemetry.rb +74 -3
  48. data/lib/inspec/version.rb +1 -1
  49. data/lib/inspec/waiver_file_reader.rb +68 -27
  50. data/lib/inspec.rb +2 -2
  51. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +189 -168
  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 +270 -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 +125 -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 +20 -8
  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. metadata +61 -19
  77. data/lib/inspec/utils/telemetry/collector.rb +0 -81
  78. data/lib/inspec/utils/telemetry/data_series.rb +0 -44
  79. data/lib/inspec/utils/telemetry/global_methods.rb +0 -22
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+ require "chef-licensing"
3
+ require "securerandom" unless defined?(SecureRandom)
4
+ require "digest" unless defined?(Digest)
5
+ require_relative "../../dist"
6
+ module Inspec
7
+ class Telemetry
8
+ class Base
9
+ VERSION = 2.0
10
+ TYPE = "job"
11
+ JOB_TYPE = "InSpec"
12
+
13
+ attr_accessor :scratch
14
+
15
+ def fetch_license_ids
16
+ Inspec::Log.debug "Fetching license IDs for telemetry"
17
+ @license_keys ||= ChefLicensing.license_keys
18
+ end
19
+
20
+ def create_wrapper
21
+ Inspec::Log.debug "Initialising wrapper for telemetry"
22
+ {
23
+ version: VERSION,
24
+ createdTimeUTC: Time.now.getutc.iso8601,
25
+ environment: Inspec::Telemetry::RunContextProbe.guess_run_context,
26
+ licenseIds: fetch_license_ids,
27
+ source: "#{Inspec::Dist::EXEC_NAME}:#{Inspec::VERSION}",
28
+ type: TYPE,
29
+ }
30
+ end
31
+
32
+ def note_feature_usage(feature_name)
33
+ @scratch ||= {}
34
+ @scratch[:features] ||= []
35
+ @scratch[:features] << feature_name
36
+ end
37
+
38
+ def run_starting(_opts = {})
39
+ Inspec::Log.debug "Initiating telemetry for InSpec"
40
+ @scratch ||= {}
41
+ @scratch[:features] ||= []
42
+ @scratch[:run_start_time] = Time.now.getutc.iso8601
43
+ end
44
+
45
+ def run_ending(opts)
46
+ note_per_run_features(opts)
47
+
48
+ payload = create_wrapper
49
+
50
+ train_platform = opts[:runner].backend.backend.platform
51
+ payload[:platform] = train_platform.name
52
+
53
+ payload[:jobs] = [{
54
+ type: JOB_TYPE,
55
+
56
+ # Target platform info
57
+ environment: {
58
+ host: obscure(URI(opts[:runner].backend.backend.uri).host) || "unknown",
59
+ os: train_platform.name,
60
+ version: train_platform.release,
61
+ architecture: train_platform.arch || "",
62
+ id: train_platform.uuid,
63
+ },
64
+
65
+ runtime: Inspec::VERSION,
66
+ content: [], # one content == one profile
67
+ steps: [], # one step == one control
68
+ }]
69
+
70
+ opts[:run_data][:profiles].each do |profile|
71
+ payload[:jobs][0][:content] << {
72
+ name: obscure(profile[:name]),
73
+ version: profile[:version],
74
+ sha256: profile[:sha256],
75
+ maintainer: profile[:maintainer] || "",
76
+ type: "profile",
77
+ }
78
+
79
+ profile[:controls].each do |control|
80
+ payload[:jobs][0][:steps] << {
81
+ id: obscure(control[:id]),
82
+ name: "inspec-control",
83
+ description: control[:desc] || "",
84
+ target: {
85
+ mode: opts[:runner].backend.backend.backend_type,
86
+ id: opts[:runner].backend.backend.platform.uuid,
87
+ },
88
+ resources: [],
89
+ features: [],
90
+ tags: format_control_tags(control[:tags]),
91
+ }
92
+
93
+ control[:results]&.each do |resource_block|
94
+ payload[:jobs][0][:steps].last[:resources] << {
95
+ type: "inspec-resource",
96
+ name: resource_block[:resource_class],
97
+ id: obscure(resource_block[:resource_title].respond_to?(:resource_id) ? resource_block[:resource_title].resource_id : nil) || "unknown",
98
+ }
99
+ end
100
+
101
+ # Per-control features.
102
+ payload[:jobs][0][:steps].last[:features] = scratch[:features].dup
103
+ end
104
+ end
105
+
106
+ Inspec::Log.debug "Final data for telemetry upload -> #{payload}"
107
+ Inspec::Log.debug "Finishing telemetry for InSpec"
108
+ # Return payload object for testing
109
+ payload
110
+ end
111
+
112
+ def format_control_tags(tags)
113
+ tags_list = []
114
+ tags.each do |key, value|
115
+ tags_list << { name: key.to_s, value: (value || "").to_s }
116
+ end
117
+ tags_list
118
+ end
119
+
120
+ # Hash text if non-nil
121
+ def obscure(cleartext)
122
+ return nil if cleartext.nil?
123
+ return nil if cleartext.empty?
124
+
125
+ Digest::SHA2.new(256).hexdigest(cleartext)
126
+ end
127
+
128
+ def note_per_run_features(opts)
129
+ note_all_invoked_features
130
+ note_gem_dependency_usage(opts)
131
+ end
132
+
133
+ def note_all_invoked_features
134
+ Inspec::Feature.list_all_invoked_features.each do |feature|
135
+ Inspec::Telemetry.note_feature_usage(feature.to_s)
136
+ end
137
+ end
138
+
139
+ def note_gem_dependency_usage(opts)
140
+ unless opts[:runner].target_profiles.map do |tp|
141
+ tp.metadata.gem_dependencies + \
142
+ tp.locked_dependencies.list.map { |_k, v| v.profile.metadata.gem_dependencies }.flatten
143
+ end.flatten.empty?
144
+ Inspec::Telemetry.note_feature_usage("inspec-gem-deps-in-profiles")
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require_relative "base"
3
+ require "faraday" unless defined?(Faraday)
4
+ require "inspec/utils/licensing_config"
5
+ module Inspec
6
+ class Telemetry
7
+ class HTTP < Base
8
+ TELEMETRY_JOBS_PATH = "v1/job"
9
+ TELEMETRY_URL = if ChefLicensing::Config.license_server_url&.match?("acceptance")
10
+ ENV["CHEF_TELEMETRY_URL"]
11
+ else
12
+ "https://services.chef.io/telemetry/"
13
+ end
14
+ def run_ending(opts)
15
+ payload = super
16
+ response = connection.post(TELEMETRY_JOBS_PATH) do |req|
17
+ req.body = payload.to_json
18
+ end
19
+ if response.success?
20
+ Inspec::Log.debug "HTTP connection with Telemetry Client successful."
21
+ Inspec::Log.debug "HTTP response from Telemetry Client -> #{response.to_hash}"
22
+ true
23
+ else
24
+ Inspec::Log.debug "HTTP connection with Telemetry Client faced an error."
25
+ Inspec::Log.debug "HTTP error -> #{response.to_hash[:body]["error"]}" if response.to_hash[:body] && response.to_hash[:body]["error"]
26
+ false
27
+ end
28
+ rescue Faraday::ConnectionFailed
29
+ Inspec::Log.debug "HTTP connection failure with telemetry url -> #{TELEMETRY_URL}"
30
+ end
31
+
32
+ def connection
33
+ Faraday.new(url: TELEMETRY_URL) do |config|
34
+ config.request :json
35
+ config.response :json
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require_relative "base"
3
+ module Inspec
4
+ class Telemetry
5
+ class Null < Base
6
+ def run_starting(_opts); end
7
+ def run_ending(_opts); end
8
+ end
9
+ end
10
+ end
11
+
@@ -1,9 +1,21 @@
1
1
  module Inspec
2
- module Telemetry
2
+ class Telemetry
3
3
  # Guesses the run context of InSpec - how were we invoked?
4
4
  # All stack values here are determined experimentally
5
5
 
6
6
  class RunContextProbe
7
+ # Guess if we are running under Automate
8
+ def self.under_automate?
9
+ # Currently assume we are under automate if we have an automate-based reporter
10
+ Inspec::Config.cached[:reporter]
11
+ .keys
12
+ .map(&:to_s)
13
+ .any? { |n| n =~ /automate/ }
14
+ end
15
+
16
+ # Guess, using stack introspection, if we were called under
17
+ # test-kitchen, cli, audit-cookbook, or otherwise.
18
+ # TODO add compliance-phase of chef-infra
7
19
  def self.guess_run_context(stack = nil)
8
20
  stack ||= caller_locations
9
21
  return "test-kitchen" if kitchen?(stack)
@@ -1,3 +1,74 @@
1
- require "inspec/utils/telemetry/collector"
2
- require "inspec/utils/telemetry/data_series"
3
- require "inspec/utils/telemetry/global_methods"
1
+ require "time" unless defined?(Time.zone_offset)
2
+ require "chef-licensing"
3
+ require_relative "telemetry/null"
4
+ require_relative "telemetry/http"
5
+ require_relative "telemetry/run_context_probe"
6
+
7
+ module Inspec
8
+ class Telemetry
9
+
10
+ @@instance = nil
11
+ @@config = nil
12
+
13
+ def self.instance
14
+ @@instance ||= determine_backend_class.new
15
+ end
16
+
17
+ def self.determine_backend_class
18
+ # Don't perform telemetry action for other InSpec distros
19
+ # Don't perform telemetry action if running under Automate - Automate does LDC tracking for us
20
+ # Don't perform telemetry action if license is a commercial license
21
+
22
+ if Inspec::Dist::EXEC_NAME != "inspec" ||
23
+ Inspec::Telemetry::RunContextProbe.under_automate? ||
24
+ license&.license_type&.downcase == "commercial"
25
+
26
+ Inspec::Log.debug "Determined telemetry operation is not applicable and hence aborting it."
27
+ return Inspec::Telemetry::Null
28
+ end
29
+
30
+ if Inspec::Dist::EXEC_NAME == "inspec" && telemetry_disabled?
31
+ # Issue a warning if an InSpec user is explicitly trying to opt out of telemetry using cli option
32
+ Inspec::Log.warn "Telemetry opt-out is not permissible."
33
+ end
34
+
35
+ Inspec::Log.debug "Determined HTTP instance for telemetry"
36
+
37
+ Inspec::Telemetry::HTTP
38
+ end
39
+
40
+ def self.license
41
+ Inspec::Log.debug "Fetching license context for telemetry check"
42
+ @license = ChefLicensing.license_context
43
+ end
44
+
45
+ ######
46
+ # These class methods make it convenient to call from anywhere within the InSpec codebase.
47
+ ######
48
+ def self.run_starting(opts)
49
+ @@config ||= opts[:conf]
50
+ instance.run_starting(opts)
51
+ rescue StandardError => e
52
+ Inspec::Log.debug "Encountered error in Telemetry start run call -> #{e.message}"
53
+ end
54
+
55
+ def self.run_ending(opts)
56
+ @@config ||= opts[:conf]
57
+ instance.run_ending(opts)
58
+ rescue StandardError => e
59
+ Inspec::Log.debug "Encountered error in Telemetry end run call -> #{e.message}"
60
+ end
61
+
62
+ def self.note_feature_usage(feature_name)
63
+ instance.note_feature_usage(feature_name)
64
+ end
65
+
66
+ def self.config
67
+ @@config
68
+ end
69
+
70
+ def self.telemetry_disabled?
71
+ config.telemetry_options["enable_telemetry"].nil? ? false : !config.telemetry_options["enable_telemetry"]
72
+ end
73
+ end
74
+ end
@@ -1,3 +1,3 @@
1
1
  module Inspec
2
- VERSION = "5.22.50".freeze
2
+ VERSION = "6.8.1".freeze
3
3
  end
@@ -15,49 +15,90 @@ module Inspec
15
15
  output = {}
16
16
 
17
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
- unless data.nil?
23
- data = data.inputs
24
- validate_json_yaml(data)
25
- end
26
- elsif file_extension == ".csv"
27
- data = Waivers::CSVFileReader.resolve(file_path)
28
- headers = Waivers::CSVFileReader.headers
29
- validate_headers(headers)
30
- elsif file_extension == ".json"
31
- data = Waivers::JSONFileReader.resolve(file_path)
32
- validate_json_yaml(data) unless data.nil?
33
- end
18
+ data = read_from_file(file_path)
34
19
  output.merge!(data) if !data.nil? && data.is_a?(Hash)
35
20
 
36
21
  if data.nil?
37
22
  raise Inspec::Exceptions::WaiversFileNotReadable,
38
- "Cannot find parser for waivers file '#{file_path}'. " \
23
+ "Cannot find parser for waivers file." \
39
24
  "Check to make sure file has the appropriate extension."
40
25
  end
26
+ rescue Inspec::Exceptions::WaiversFileNotReadable, Inspec::Exceptions::WaiversFileInvalidFormatting => e
27
+ Inspec::Log.error "Error reading waivers file #{file_path}. #{e.message}"
28
+ Inspec::UI.new.exit(:usage_error)
41
29
  end
42
30
 
43
31
  @waivers_data[profile_id] = output
44
32
  end
45
33
 
46
- def self.validate_headers(headers, json_yaml = false)
47
- required_fields = json_yaml ? %w{justification} : %w{control_id justification}
48
- all_fields = %w{control_id justification expiration_date run}
34
+ def self.read_from_file(file_path)
35
+ data = nil
36
+ file_extension = File.extname(file_path)
37
+ if [".yaml", ".yml"].include? file_extension
38
+ data = Secrets::YAML.resolve(file_path)
39
+ data = data.inputs unless data.nil?
40
+ validate_json_yaml(data)
41
+ elsif file_extension == ".csv"
42
+ data = Waivers::CSVFileReader.resolve(file_path)
43
+ headers = Waivers::CSVFileReader.headers
44
+ validate_csv_headers(headers)
45
+ elsif file_extension == ".json"
46
+ data = Waivers::JSONFileReader.resolve(file_path)
47
+ validate_json_yaml(data) unless data.nil?
48
+ end
49
+ data
50
+ end
51
+
52
+ def self.all_fields
53
+ %w{control_id justification expiration_date run}
54
+ end
55
+
56
+ def self.validate_csv_headers(headers)
57
+ invalid_headers_info = fetch_invalid_headers_info(headers)
58
+ # Warn if blank column found in csv file
59
+ Inspec::Log.warn "Invalid column headers: Column can't be nil" if invalid_headers_info[:blank_column]
60
+ # Warn if extra header found in csv file
61
+ Inspec::Log.warn "Extra header/s #{invalid_headers_info[:extra_headers]}" unless invalid_headers_info[:extra_headers].empty?
62
+ unless invalid_headers_info[:missing_required_fields].empty?
63
+ raise Inspec::Exceptions::WaiversFileInvalidFormatting,
64
+ "Missing required header/s #{invalid_headers_info[:missing_required_fields]}. Fix headers in file to proceed."
65
+ end
66
+ end
49
67
 
50
- Inspec::Log.warn "Missing column headers: #{(required_fields - headers)}" unless (required_fields - headers).empty?
51
- Inspec::Log.warn "Invalid column header: Column can't be nil" if headers.include? nil
52
- Inspec::Log.warn "Extra column headers: #{(headers - all_fields)}" unless (headers - all_fields).empty?
68
+ def self.fetch_invalid_headers_info(headers, json_yaml = false)
69
+ required_fields = json_yaml ? %w{justification} : %w{control_id justification}
70
+ data = {}
71
+ data[:missing_required_fields] = []
72
+ # Finds missing required fields
73
+ unless (required_fields - headers).empty?
74
+ data[:missing_required_fields] = required_fields - headers
75
+ end
76
+ # If column with no header found set the blank_column flag. Only applicable for csv
77
+ data[:blank_column] = headers.include?(nil) ? true : false
78
+ # Find extra headers/parameters
79
+ data[:extra_headers] = (headers - all_fields)
80
+ data
53
81
  end
54
82
 
55
83
  def self.validate_json_yaml(data)
56
- headers = []
57
- data.each_value do |value|
58
- headers.push value.keys
84
+ missing_required_field = false
85
+ data.each do |key, value|
86
+ # In case of yaml or json we need to validate headers/parametes for each value
87
+ invalid_headers_info = fetch_invalid_headers_info(value.keys, true)
88
+ # WARN in case of extra parameters found in each waived control
89
+ Inspec::Log.warn "Control ID #{key}: extra parameter/s #{invalid_headers_info[:extra_headers]}" unless invalid_headers_info[:extra_headers].empty?
90
+ unless invalid_headers_info[:missing_required_fields].empty?
91
+ missing_required_field = true
92
+ # Log error for each waived control
93
+ Inspec::Log.error "Control ID #{key}: missing required parameter/s #{invalid_headers_info[:missing_required_fields]}"
94
+ end
95
+ end
96
+
97
+ # Raise error if any of the waived control has missing required filed
98
+ if missing_required_field
99
+ raise Inspec::Exceptions::WaiversFileInvalidFormatting,
100
+ "Missing required parameter [justification]. Fix parameters in file to proceed."
59
101
  end
60
- validate_headers(headers.flatten.uniq, true)
61
102
  end
62
103
  end
63
104
  end
data/lib/inspec.rb CHANGED
@@ -4,6 +4,7 @@ libdir = File.dirname(__FILE__)
4
4
  $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
5
5
 
6
6
  require "inspec/version"
7
+ require "inspec/utils/licensing_config"
7
8
  require "inspec/exceptions"
8
9
  require "inspec/utils/deprecation"
9
10
  require "inspec/profile"
@@ -18,7 +19,6 @@ require "inspec/rspec_extensions"
18
19
  require "inspec/globals"
19
20
  require "inspec/impact"
20
21
  require "inspec/utils/telemetry"
21
- require "inspec/utils/telemetry/global_methods"
22
22
 
23
23
  require "inspec/plugin/v2"
24
24
  require "inspec/plugin/v1"
@@ -30,4 +30,4 @@ require "inspec/source_reader"
30
30
  require "inspec/resource"
31
31
 
32
32
  require "inspec/dependency_loader"
33
- require "inspec/dependency_installer"
33
+ require "inspec/dependency_installer"