inspec-core 4.38.9 → 4.49.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -1
  3. data/etc/deprecations.json +1 -1
  4. data/lib/inspec/base_cli.rb +11 -1
  5. data/lib/inspec/cached_fetcher.rb +2 -2
  6. data/lib/inspec/cli.rb +14 -4
  7. data/lib/inspec/control_eval_context.rb +64 -17
  8. data/lib/inspec/dsl.rb +18 -3
  9. data/lib/inspec/fetcher/url.rb +45 -3
  10. data/lib/inspec/fetcher.rb +3 -3
  11. data/lib/inspec/plugin/v1/registry.rb +6 -2
  12. data/lib/inspec/profile.rb +146 -6
  13. data/lib/inspec/resources/apache_conf.rb +8 -6
  14. data/lib/inspec/resources/cassandra.rb +64 -0
  15. data/lib/inspec/resources/cassandradb_conf.rb +47 -0
  16. data/lib/inspec/resources/cassandradb_session.rb +68 -0
  17. data/lib/inspec/resources/chrony_conf.rb +55 -0
  18. data/lib/inspec/resources/csv.rb +26 -3
  19. data/lib/inspec/resources/groups.rb +22 -3
  20. data/lib/inspec/resources/ibmdb2_conf.rb +57 -0
  21. data/lib/inspec/resources/ibmdb2_session.rb +69 -0
  22. data/lib/inspec/resources/mongodb_session.rb +88 -0
  23. data/lib/inspec/resources/mssql_sys_conf.rb +48 -0
  24. data/lib/inspec/resources/opa.rb +26 -0
  25. data/lib/inspec/resources/opa_api.rb +39 -0
  26. data/lib/inspec/resources/opa_cli.rb +43 -0
  27. data/lib/inspec/resources/oracle.rb +66 -0
  28. data/lib/inspec/resources/oracledb_conf.rb +40 -0
  29. data/lib/inspec/resources/oracledb_listener_conf.rb +123 -0
  30. data/lib/inspec/resources/oracledb_session.rb +23 -6
  31. data/lib/inspec/resources/postgres_session.rb +15 -10
  32. data/lib/inspec/resources/registry_key.rb +1 -1
  33. data/lib/inspec/resources/security_identifier.rb +8 -14
  34. data/lib/inspec/resources/security_policy.rb +4 -3
  35. data/lib/inspec/resources/service.rb +7 -1
  36. data/lib/inspec/resources/sybase_conf.rb +37 -0
  37. data/lib/inspec/resources/sybase_session.rb +111 -0
  38. data/lib/inspec/resources/users.rb +16 -2
  39. data/lib/inspec/resources/windows_firewall.rb +1 -1
  40. data/lib/inspec/resources/wmi.rb +1 -1
  41. data/lib/inspec/resources.rb +12 -0
  42. data/lib/inspec/run_data/profile.rb +0 -2
  43. data/lib/inspec/runner.rb +2 -0
  44. data/lib/inspec/utils/filter.rb +1 -1
  45. data/lib/inspec/version.rb +1 -1
  46. data/lib/plugins/inspec-init/templates/profiles/aws/inspec.yml +1 -1
  47. data/lib/plugins/inspec-init/templates/profiles/azure/inspec.yml +1 -1
  48. data/lib/plugins/inspec-init/templates/profiles/gcp/inspec.yml +1 -1
  49. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb +16 -15
  50. metadata +18 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17449ad4c9680511a8fc11c6fdb11d9ece550a7942c9e734c95eac0d41913d9f
4
- data.tar.gz: ae5055ccc9bebd1aed4f22da4ad4dcd1be31e1bd2b5707e7b5fb088c916eda08
3
+ metadata.gz: 31dcab9ba9621f43755fc390ad061c08b7e6ab19466d26a38921d33960e1bbf5
4
+ data.tar.gz: eecdf0ec772ef012bfbf5185b379c47dd008fe902b9e91d4feee6979b0294c0f
5
5
  SHA512:
6
- metadata.gz: 6cec299ca48d7ca4c3fb9b3eecc79c8687541fbd83fc79e837ed13d2abb4bcb861f747782a68bf90f7e1083443a671079a1368a97e9f552e249e456616a92059
7
- data.tar.gz: 287e2d79dbc494c83d6f8b8046e0f9c54632c5a13ec75ac69b603bdf9fe9b6a89ff86c9c8f025f7e04372490adb1ffa5a5a7fc10f3ecab1e7fabd70f71f6767d
6
+ metadata.gz: 1059703ad59c2bf7c213a0914f94a8852cc550b1d305f634e07a08b364edae5c0f550927a8bcec9e712cd313949388082fdebf5c3164147ef12c21df952281b9
7
+ data.tar.gz: 1d4e6e64b8851c40349b7d696d46904fa73c0683d19c70afaa942a4c4bef4591a3ed4d7ec2c89aa5f7717da70617d70f182a7e9b894338da77592231b4c62234
data/Gemfile CHANGED
@@ -30,7 +30,11 @@ end
30
30
  group :test do
31
31
  gem "chefstyle", "~> 2.0.3"
32
32
  gem "concurrent-ruby", "~> 1.0"
33
- gem "html-proofer", platforms: :ruby # do not attempt to run proofer on windows
33
+ if Gem.ruby_version.to_s.start_with?("2.5")
34
+ gem "html-proofer", "= 3.19.1" , platforms: :ruby # do not attempt to run proofer on windows
35
+ else
36
+ gem "html-proofer", platforms: :ruby # do not attempt to run proofer on windows
37
+ end
34
38
  gem "json_schemer", ">= 0.2.1", "< 0.2.19"
35
39
  gem "m"
36
40
  gem "minitest-sprint", "~> 1.0"
@@ -4,7 +4,7 @@
4
4
  "groups": {
5
5
  "attrs_value_replaces_default": {
6
6
  "action": "warn",
7
- "prefix": "The 'default' option for attributes is being replaced by 'value' - please use it instead."
7
+ "prefix": "The 'default' option for inputs is being replaced by 'value' - please use it instead."
8
8
  },
9
9
  "attrs_dsl": {
10
10
  "action": "ignore",
@@ -43,11 +43,15 @@ module Inspec
43
43
  begin
44
44
  if (allowed_commands & ARGV.map(&:downcase)).empty? && # Did they use a non-exempt command?
45
45
  !ARGV.empty? # Did they supply at least one command?
46
- LicenseAcceptance::Acceptor.check_and_persist(
46
+ license_acceptor_output = LicenseAcceptance::Acceptor.check_and_persist(
47
47
  Inspec::Dist::EXEC_NAME,
48
48
  Inspec::VERSION,
49
49
  logger: Inspec::Log
50
50
  )
51
+ if license_acceptor_output && ARGV.count == 1 && (ARGV.first.include? "--chef-license")
52
+ Inspec::UI.new.exit
53
+ end
54
+ license_acceptor_output
51
55
  end
52
56
  rescue LicenseAcceptance::LicenseNotAcceptedError
53
57
  Inspec::Log.error "#{Inspec::Dist::PRODUCT_NAME} cannot execute without accepting the license"
@@ -136,6 +140,8 @@ module Inspec
136
140
  profile_options
137
141
  option :controls, type: :array,
138
142
  desc: "A list of control names to run, or a list of /regexes/ to match against control names. Ignore all other tests."
143
+ option :tags, type: :array,
144
+ desc: "A list of tags names that are part of controls to filter and run controls, or a list of /regexes/ to match against tags names of controls. Ignore all other tests."
139
145
  option :reporter, type: :array,
140
146
  banner: "one two:/output/file/path",
141
147
  desc: "Enable one or more output reporters: cli, documentation, html, progress, json, json-min, json-rspec, junit, yaml"
@@ -168,6 +174,10 @@ module Inspec
168
174
  desc: "After normal execution order, results are sorted by control ID, or by file (default), or randomly. None uses legacy unsorted mode."
169
175
  option :filter_empty_profiles, type: :boolean, default: false,
170
176
  desc: "Filter empty profiles (profiles without controls) from the report."
177
+ option :filter_waived_controls, type: :boolean,
178
+ desc: "Do not execute waived controls in InSpec at all. Must use with --waiver-file. Ignores `run` setting of waiver file."
179
+ option :retain_waiver_data, type: :boolean,
180
+ desc: "EXPERIMENTAL: Only works in conjunction with --filter-waived-controls, retains waiver data about controls that were skipped"
171
181
  option :command_timeout, type: :numeric,
172
182
  desc: "Maximum seconds to allow commands to run during execution.",
173
183
  long_desc: "Maximum seconds to allow commands to run during execution. A timed out command is considered an error."
@@ -6,9 +6,9 @@ module Inspec
6
6
  extend Forwardable
7
7
 
8
8
  attr_reader :cache, :target, :fetcher
9
- def initialize(target, cache)
9
+ def initialize(target, cache, opts = {})
10
10
  @target = target
11
- @fetcher = Inspec::Fetcher::Registry.resolve(target)
11
+ @fetcher = Inspec::Fetcher::Registry.resolve(target, opts)
12
12
 
13
13
  if @fetcher.nil?
14
14
  raise("Could not fetch inspec profile in #{target.inspect}.")
data/lib/inspec/cli.rb CHANGED
@@ -65,6 +65,8 @@ class Inspec::InspecCLI < Inspec::BaseCLI
65
65
  desc: "Save the created profile to a path"
66
66
  option :controls, type: :array,
67
67
  desc: "A list of controls to include. Ignore all other tests."
68
+ option :tags, type: :array,
69
+ desc: "A list of tags to filter controls and include only those. Ignore all other tests."
68
70
  profile_options
69
71
  def json(target)
70
72
  require "json" unless defined?(JSON)
@@ -91,7 +93,8 @@ class Inspec::InspecCLI < Inspec::BaseCLI
91
93
  end
92
94
 
93
95
  desc "check PATH", "verify all tests at the specified PATH"
94
- option :format, type: :string
96
+ option :format, type: :string,
97
+ desc: "The output format to use doc (default), json. If valid format is not provided then it will use the default."
95
98
  profile_options
96
99
  def check(path) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
97
100
  o = config
@@ -119,8 +122,8 @@ class Inspec::InspecCLI < Inspec::BaseCLI
119
122
  end
120
123
  puts
121
124
 
122
- if result[:errors].empty? && result[:warnings].empty?
123
- ui.plain_line("No errors or warnings")
125
+ if result[:errors].empty? && result[:warnings].empty? && result[:offenses].empty?
126
+ ui.plain_line("No errors, warnings, or offenses")
124
127
  else
125
128
  item_msg = lambda { |item|
126
129
  pos = [item[:file], item[:line], item[:column]].compact.join(":")
@@ -132,11 +135,18 @@ class Inspec::InspecCLI < Inspec::BaseCLI
132
135
 
133
136
  puts
134
137
 
138
+ unless result[:offenses].empty?
139
+ puts "Offenses:\n"
140
+ result[:offenses].each { |item| ui.cyan(" #{Inspec::UI::GLYPHS[:script_x]} #{item_msg.call(item)}\n\n") }
141
+ end
142
+
143
+ offenses = ui.cyan("#{result[:offenses].length} offenses", print: false)
135
144
  errors = ui.red("#{result[:errors].length} errors", print: false)
136
145
  warnings = ui.yellow("#{result[:warnings].length} warnings", print: false)
137
- ui.plain_line("Summary: #{errors}, #{warnings}")
146
+ ui.plain_line("Summary: #{errors}, #{warnings}, #{offenses}")
138
147
  end
139
148
  end
149
+
140
150
  ui.exit Inspec::UI::EXIT_USAGE_ERROR unless result[:summary][:valid]
141
151
  rescue StandardError => e
142
152
  pretty_handle_exception(e)
@@ -18,6 +18,7 @@ module Inspec
18
18
  attr_accessor :skip_file
19
19
  attr_accessor :profile_context
20
20
  attr_accessor :resources_dsl
21
+ attr_accessor :conf
21
22
 
22
23
  def initialize(profile_context, resources_dsl, backend, conf, dependencies, require_loader, skip_only_if_eval)
23
24
  @profile_context = profile_context
@@ -53,12 +54,30 @@ module Inspec
53
54
 
54
55
  def control(id, opts = {}, &block)
55
56
  opts[:skip_only_if_eval] = @skip_only_if_eval
56
- if control_exist_in_controls_list?(id) || controls_list_empty?
57
+ if (controls_list_empty? && tags_list_empty?) || control_exist_in_controls_list?(id)
57
58
  register_control(Inspec::Rule.new(id, profile_id, resources_dsl, opts, &block))
59
+ elsif !tags_list_empty?
60
+ # Inside elsif rule is initialised before registering it because it enables fetching of control tags
61
+ # This condition is only true when --tags option is used
62
+ inspec_rule = Inspec::Rule.new(id, profile_id, resources_dsl, opts, &block)
63
+ tag_ids = control_tags(inspec_rule)
64
+ register_control(inspec_rule) if tag_exist_in_control_tags?(tag_ids)
58
65
  end
59
66
  end
67
+
60
68
  alias rule control
61
69
 
70
+ def control_tags(inspec_rule)
71
+ all_tags = []
72
+ inspec_rule.tag.each do |key, value|
73
+ all_tags.push(key)
74
+ all_tags.push(value) unless value.nil?
75
+ end
76
+ all_tags.flatten.compact.uniq.map(&:to_s)
77
+ rescue
78
+ []
79
+ end
80
+
62
81
  # Describe allows users to write rspec-like bare describe
63
82
  # blocks without declaring an inclosing control. Here, we
64
83
  # generate a control for them automatically and then execute
@@ -74,7 +93,7 @@ module Inspec
74
93
  res = describe(*args, &block)
75
94
  end
76
95
 
77
- if control_exist_in_controls_list?(id) || controls_list_empty?
96
+ if controls_list_empty? || control_exist_in_controls_list?(id)
78
97
  register_control(rule, &block)
79
98
  end
80
99
 
@@ -171,6 +190,47 @@ module Inspec
171
190
  @skip_file = true
172
191
  end
173
192
 
193
+ # Check if the given control exist in the --tags option
194
+ def tag_exist_in_control_tags?(tag_ids)
195
+ tag_option_matches_with_list = false
196
+ if !tag_ids.empty? && !tag_ids.nil? && profile_tag_config_exist?
197
+ tag_option_matches_with_list = !(tag_ids & @conf["profile"].include_tags_list).empty?
198
+ unless tag_option_matches_with_list
199
+ @conf["profile"].include_tags_list.any? do |inclusion|
200
+ # Try to see if the inclusion is a regex, and if it matches
201
+ if inclusion.is_a?(Regexp)
202
+ tag_ids.each do |id|
203
+ tag_option_matches_with_list = (inclusion =~ id)
204
+ break if tag_option_matches_with_list
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ tag_option_matches_with_list
211
+ end
212
+
213
+ def tags_list_empty?
214
+ !@conf.empty? && @conf.key?("profile") && @conf["profile"].include_tags_list.empty? || @conf.empty?
215
+ end
216
+
217
+ # Check if the given control exist in the --controls option
218
+ def control_exist_in_controls_list?(id)
219
+ id_exist_in_list = false
220
+ if profile_config_exist?
221
+ id_exist_in_list = @conf["profile"].include_controls_list.any? do |inclusion|
222
+ # Try to see if the inclusion is a regex, and if it matches
223
+ inclusion == id || (inclusion.is_a?(Regexp) && inclusion =~ id)
224
+ end
225
+ end
226
+ id_exist_in_list
227
+ end
228
+
229
+ # Returns true if configuration hash is empty or configuration hash does not have the list of controls that needs to be included
230
+ def controls_list_empty?
231
+ !@conf.empty? && @conf.key?("profile") && @conf["profile"].include_controls_list.empty? || @conf.empty?
232
+ end
233
+
174
234
  private
175
235
 
176
236
  def block_location(block, alternate_caller)
@@ -187,21 +247,8 @@ module Inspec
187
247
  !@conf.empty? && @conf.key?("profile") && !@conf["profile"].include_controls_list.empty?
188
248
  end
189
249
 
190
- # Returns true if configuration hash is empty or configuration hash does not have the list of controls that needs to be included
191
- def controls_list_empty?
192
- !@conf.empty? && @conf.key?("profile") && @conf["profile"].include_controls_list.empty? || @conf.empty?
193
- end
194
-
195
- # Check if the given control exist in the --controls option
196
- def control_exist_in_controls_list?(id)
197
- id_exist_in_list = false
198
- if profile_config_exist?
199
- id_exist_in_list = @conf["profile"].include_controls_list.any? do |inclusion|
200
- # Try to see if the inclusion is a regex, and if it matches
201
- inclusion == id || (inclusion.is_a?(Regexp) && inclusion =~ id)
202
- end
203
- end
204
- id_exist_in_list
250
+ def profile_tag_config_exist?
251
+ !@conf.empty? && @conf.key?("profile") && !@conf["profile"].include_tags_list.empty?
205
252
  end
206
253
  end
207
254
  end
data/lib/inspec/dsl.rb CHANGED
@@ -93,23 +93,38 @@ module Inspec::DSL
93
93
  context = dep_entry.profile.runner_context
94
94
  # if we don't want all the rules, then just make 1 pass to get all rule_IDs
95
95
  # that we want to keep from the original
96
- filter_included_controls(context, dep_entry.profile, &block) unless opts[:include_all]
96
+ if !opts[:include_all] || !(opts[:conf]["profile"].include_tags_list.empty?) || !opts[:conf]["profile"].include_controls_list.empty?
97
+ filter_included_controls(context, dep_entry.profile, opts, &block)
98
+ end
97
99
  # interpret the block and skip/modify as required
98
100
  context.load(block) if block_given?
99
101
  bind_context.add_subcontext(context)
100
102
  end
101
103
 
102
- def self.filter_included_controls(context, profile, &block)
104
+ def self.filter_included_controls(context, profile, opts, &block)
103
105
  mock = Inspec::Backend.create(Inspec::Config.mock)
104
106
  include_ctx = Inspec::ProfileContext.for_profile(profile, mock)
105
107
  include_ctx.load(block) if block_given?
108
+ include_ctx.control_eval_context.conf = opts[:conf]
109
+ control_eval_ctx = include_ctx.control_eval_context
106
110
  # remove all rules that were not registered
107
111
  context.all_rules.each do |r|
108
112
  id = Inspec::Rule.rule_id(r)
109
113
  fid = Inspec::Rule.profile_id(r) + "/" + id
110
- unless include_ctx.rules[id] || include_ctx.rules[fid]
114
+ if !opts[:include_all] && !(include_ctx.rules[id] || include_ctx.rules[fid])
111
115
  context.remove_rule(fid)
112
116
  end
117
+
118
+ unless control_eval_ctx.controls_list_empty?
119
+ # filter the dependent profile controls which are not in the --controls options list
120
+ context.remove_rule(fid) unless control_eval_ctx.control_exist_in_controls_list?(id)
121
+ end
122
+
123
+ unless control_eval_ctx.tags_list_empty?
124
+ # filter included controls using --tags
125
+ tag_ids = control_eval_ctx.control_tags(r)
126
+ context.remove_rule(fid) unless control_eval_ctx.tag_exist_in_control_tags?(tag_ids)
127
+ end
113
128
  end
114
129
  end
115
130
  end
@@ -44,11 +44,17 @@ module Inspec::Fetcher
44
44
  # - Branch URL
45
45
  # - Commit URL
46
46
  #
47
- # master url:
47
+ # master url(default branch master):
48
48
  # https://github.com/nathenharvey/tmp_compliance_profile/ is transformed to
49
49
  # https://github.com/nathenharvey/tmp_compliance_profile/archive/master.tar.gz
50
50
  # https://bitbucket.org/username/repo is transformed to
51
51
  # https://bitbucket.org/username/repo/get/master.tar.gz
52
+
53
+ # main url(default branch main):
54
+ # https://github.com/nathenharvey/tmp_compliance_profile/ is transformed to
55
+ # https://github.com/nathenharvey/tmp_compliance_profile/archive/main.tar.gz
56
+ # https://bitbucket.org/username/repo is transformed to
57
+ # https://bitbucket.org/username/repo/get/main.tar.gz
52
58
  #
53
59
  # branch:
54
60
  # https://github.com/hardening-io/tests-os-hardening/tree/2.0 is transformed to
@@ -68,14 +74,18 @@ module Inspec::Fetcher
68
74
  BITBUCKET_URL_REGEX = %r{^https?://(www\.)?bitbucket\.org/(?<user>[\w-]+)/(?<repo>[\w-]+)(\.git)?(/)?$}.freeze
69
75
  BITBUCKET_URL_BRANCH_REGEX = %r{^https?://(www\.)?bitbucket\.org/(?<user>[\w-]+)/(?<repo>[\w-]+)/branch/(?<branch>[\w\.]+)(/)?$}.freeze
70
76
  BITBUCKET_URL_COMMIT_REGEX = %r{^https?://(www\.)?bitbucket\.org/(?<user>[\w-]+)/(?<repo>[\w-]+)/commits/(?<commit>[\w\.]+)(/)?$}.freeze
77
+ GITHUB_URL = "https://github.com".freeze
78
+ BITBUCKET_URL = "https://bitbucket.org".freeze
71
79
 
72
80
  def self.transform(target)
73
81
  transformed_target = if m = GITHUB_URL_REGEX.match(target) # rubocop:disable Lint/AssignmentInCondition
74
- "https://github.com/#{m[:user]}/#{m[:repo]}/archive/master.tar.gz"
82
+ default_branch = default_ref(m, GITHUB_URL)
83
+ "https://github.com/#{m[:user]}/#{m[:repo]}/archive/#{default_branch}.tar.gz"
75
84
  elsif m = GITHUB_URL_WITH_TREE_REGEX.match(target) # rubocop:disable Lint/AssignmentInCondition
76
85
  "https://github.com/#{m[:user]}/#{m[:repo]}/archive/#{m[:commit]}.tar.gz"
77
86
  elsif m = BITBUCKET_URL_REGEX.match(target) # rubocop:disable Lint/AssignmentInCondition
78
- "https://bitbucket.org/#{m[:user]}/#{m[:repo]}/get/master.tar.gz"
87
+ default_branch = default_ref(m, BITBUCKET_URL)
88
+ "https://bitbucket.org/#{m[:user]}/#{m[:repo]}/get/#{default_branch}.tar.gz"
79
89
  elsif m = BITBUCKET_URL_BRANCH_REGEX.match(target) # rubocop:disable Lint/AssignmentInCondition
80
90
  "https://bitbucket.org/#{m[:user]}/#{m[:repo]}/get/#{m[:branch]}.tar.gz"
81
91
  elsif m = BITBUCKET_URL_COMMIT_REGEX.match(target) # rubocop:disable Lint/AssignmentInCondition
@@ -120,6 +130,38 @@ module Inspec::Fetcher
120
130
 
121
131
  private
122
132
 
133
+ class << self
134
+ def default_ref(match_data, repo_url)
135
+ remote_url = "#{repo_url}/#{match_data[:user]}/#{match_data[:repo]}.git"
136
+ command_string = "git remote show #{remote_url}"
137
+ cmd = shellout(command_string)
138
+ unless cmd.exitstatus == 0
139
+ raise(Inspec::FetcherFailure, "Profile git dependency failed with default reference - #{remote_url} - error running '#{command_string}': #{cmd.stderr}")
140
+ else
141
+ ref = cmd.stdout.lines.detect { |l| l.include? "HEAD branch:" }&.split(":")&.last&.strip
142
+ unless ref
143
+ raise(Inspec::FetcherFailure, "Profile git dependency failed with default reference - #{remote_url} - error running '#{command_string}': NULL reference")
144
+ end
145
+
146
+ ref
147
+ end
148
+ end
149
+
150
+ def shellout(cmd, opts = {})
151
+ Inspec::Log.debug("Running external command: #{cmd} (#{opts})")
152
+ cmd = Mixlib::ShellOut.new(cmd, opts)
153
+ cmd.run_command
154
+ Inspec::Log.debug("External command: completed with exit status: #{cmd.exitstatus}")
155
+ Inspec::Log.debug("External command: STDOUT BEGIN")
156
+ Inspec::Log.debug(cmd.stdout)
157
+ Inspec::Log.debug("External command: STDOUT END")
158
+ Inspec::Log.debug("External command: STDERR BEGIN")
159
+ Inspec::Log.debug(cmd.stderr)
160
+ Inspec::Log.debug("External command: STDERR END")
161
+ cmd
162
+ end
163
+ end
164
+
123
165
  def parse_uri(target)
124
166
  return URI.parse(target) if target.is_a?(String)
125
167
 
@@ -2,12 +2,12 @@ require "inspec/plugin/v1"
2
2
 
3
3
  module Inspec
4
4
  class FetcherRegistry < PluginRegistry
5
- def resolve(target)
5
+ def resolve(target, opts = {})
6
6
  if fetcher_specified?(target)
7
- super(target)
7
+ super(target, opts)
8
8
  else
9
9
  Inspec::Log.debug("Assuming default supermarket source for #{target}")
10
- super(with_default_fetcher(target))
10
+ super(with_default_fetcher(target), opts)
11
11
  end
12
12
  end
13
13
 
@@ -9,9 +9,13 @@ class PluginRegistry
9
9
  #
10
10
  # @param [String] target to resolve
11
11
  # @return [Plugin] plugin instance if it can be resolved, nil otherwise
12
- def resolve(target)
12
+ def resolve(target, opts = {})
13
13
  modules.each do |m|
14
- res = m.resolve(target)
14
+ res = if Inspec::Fetcher::Url == m
15
+ m.resolve(target, opts)
16
+ else
17
+ m.resolve(target)
18
+ end
15
19
  return res unless res.nil?
16
20
  end
17
21
  nil
@@ -18,9 +18,9 @@ module Inspec
18
18
  class Profile
19
19
  extend Forwardable
20
20
 
21
- def self.resolve_target(target, cache)
21
+ def self.resolve_target(target, cache, opts = {})
22
22
  Inspec::Log.debug "Resolve #{target} into cache #{cache.path}"
23
- Inspec::CachedFetcher.new(target, cache)
23
+ Inspec::CachedFetcher.new(target, cache, opts)
24
24
  end
25
25
 
26
26
  # Check if the profile contains a vendored cache, move content into global cache
@@ -70,7 +70,11 @@ module Inspec
70
70
 
71
71
  def self.for_target(target, opts = {})
72
72
  opts[:vendor_cache] ||= Cache.new
73
- fetcher = resolve_target(target, opts[:vendor_cache])
73
+ config = {}
74
+ unless opts[:runner_conf].nil?
75
+ config = opts[:runner_conf].respond_to?(:final_options) ? opts[:runner_conf].final_options : opts[:runner_conf]
76
+ end
77
+ fetcher = resolve_target(target, opts[:vendor_cache], config)
74
78
  for_fetcher(fetcher, opts)
75
79
  end
76
80
 
@@ -87,6 +91,7 @@ module Inspec
87
91
  @logger = options[:logger] || Logger.new(nil)
88
92
  @locked_dependencies = options[:dependencies]
89
93
  @controls = options[:controls] || []
94
+ @tags = options[:tags] || []
90
95
  @writable = options[:writable] || false
91
96
  @profile_id = options[:id]
92
97
  @profile_name = options[:profile_name]
@@ -206,12 +211,15 @@ module Inspec
206
211
  @params ||= load_params
207
212
  end
208
213
 
209
- def collect_tests(include_list = @controls)
214
+ def collect_tests
210
215
  unless @tests_collected || failed?
211
216
  return unless supports_platform?
212
217
 
213
218
  locked_dependencies.each(&:collect_tests)
214
219
 
220
+ tests = filter_waived_controls
221
+
222
+ # Collect tests
215
223
  tests.each do |path, content|
216
224
  next if content.nil? || content.empty?
217
225
 
@@ -228,6 +236,65 @@ module Inspec
228
236
  @runner_context.all_rules
229
237
  end
230
238
 
239
+ # Wipe out waived controls
240
+ def filter_waived_controls
241
+ cfg = Inspec::Config.cached
242
+ return tests unless cfg["filter_waived_controls"]
243
+
244
+ ## Find the waivers file
245
+ # - TODO: cli_opts and instance_variable_get could be exposed
246
+ waiver_paths = cfg.instance_variable_get(:@cli_opts)["waiver_file"]
247
+ if waiver_paths.blank?
248
+ Inspec::Log.error "Must use --waiver-file with --filter-waived-controls"
249
+ Inspec::UI.new.exit(:usage_error)
250
+ end
251
+
252
+ # # Pull together waiver
253
+ waived_control_ids = []
254
+ waiver_paths.each do |waiver_path|
255
+ waiver_content = YAML.load_file(waiver_path)
256
+ unless waiver_content
257
+ # Note that we will have already issued a detailed warning
258
+ Inspec::Log.error "YAML parsing error in #{waiver_path}"
259
+ Inspec::UI.new.exit(:usage_error)
260
+ end
261
+ waived_control_ids << waiver_content.keys
262
+ end
263
+ waived_control_id_regex = "(#{waived_control_ids.join("|")})"
264
+
265
+ ## Purge tests (this could be doone in next block for performance)
266
+ ## TODO: implement earlier with pure AST and pure autocorrect AST
267
+ filtered_tests = {}
268
+ if cfg["retain_waiver_data"]
269
+ # VERY EXPERIMENTAL, but an empty describe block at the top level
270
+ # of the control blocks evaluation of ruby code until later-term
271
+ # waivers (behind the scenes this tells RSpec to hold on and use its internals to lazy load the code). This allows current waiver-data (e.g. skips) to still
272
+ # be processed and rendered
273
+ tests.each do |control_filename, source_code|
274
+ cleared_tests = source_code.scan(/control\s+['"].+?['"].+?(?=(?:control\s+['"].+?['"])|\z)/m).collect do |element|
275
+ next if element.blank?
276
+
277
+ if element&.match?(waived_control_id_regex)
278
+ splitlines = element.split("\n")
279
+ splitlines[0] + "\ndescribe '---' do\n" + splitlines[1..-1].join("\n") + "\nend\n"
280
+ else
281
+ element
282
+ end
283
+ end.join("")
284
+ filtered_tests[control_filename] = cleared_tests
285
+ end
286
+ else
287
+ tests.each do |control_filename, source_code|
288
+ cleared_tests = source_code.scan(/control\s+['"].+?['"].+?(?=(?:control\s+['"].+?['"])|\z)/m).select do |control_code|
289
+ !control_code.match?(waived_control_id_regex)
290
+ end.join("")
291
+
292
+ filtered_tests[control_filename] = cleared_tests
293
+ end
294
+ end
295
+ filtered_tests
296
+ end
297
+
231
298
  # This creates the list of controls provided in the --controls options which need to be include
232
299
  # for evaluation.
233
300
  def include_controls_list
@@ -253,6 +320,30 @@ module Inspec
253
320
  included_controls
254
321
  end
255
322
 
323
+ # This creates the list of controls to be filtered by tag values provided in the --tags options
324
+ def include_tags_list
325
+ return [] if @tags.nil? || @tags.empty?
326
+
327
+ included_tags = @tags
328
+ # Check for anything that might be a regex in the list, and make it official
329
+ included_tags.each_with_index do |inclusion, index|
330
+ next if inclusion.is_a?(Regexp)
331
+ # Insist the user wrap the regex in slashes to demarcate it as a regex
332
+ next unless inclusion.start_with?("/") && inclusion.end_with?("/")
333
+
334
+ inclusion = inclusion[1..-2] # Trim slashes
335
+ begin
336
+ re = Regexp.new(inclusion)
337
+ included_tags[index] = re
338
+ rescue RegexpError => e
339
+ warn "Ignoring unparseable regex '/#{inclusion}/' in --control CLI option: #{e.message}"
340
+ included_tags[index] = nil
341
+ end
342
+ end
343
+ included_tags.compact!
344
+ included_tags
345
+ end
346
+
256
347
  def load_libraries
257
348
  return @runner_context if @libraries_loaded
258
349
 
@@ -357,6 +448,43 @@ module Inspec
357
448
  res
358
449
  end
359
450
 
451
+ def cookstyle_linting_check
452
+ msgs = []
453
+ output = cookstyle_rake_output.split("Offenses:").last
454
+ msgs = output.split("\n").select { |x| x =~ /[A-Z]:/ } unless output.nil?
455
+ msgs
456
+ end
457
+
458
+ # Cookstyle linting rake run output
459
+ def cookstyle_rake_output
460
+ require "cookstyle"
461
+ require "rubocop/rake_task"
462
+ begin
463
+ RuboCop::RakeTask.new(:cookstyle_lint) do |spec|
464
+ spec.options += [
465
+ "--display-cop-names",
466
+ "--parallel",
467
+ "--only=InSpec/Deprecations",
468
+ ]
469
+ spec.patterns += Dir.glob("#{@target}/**/*.rb").reject { |f| File.directory?(f) }
470
+ spec.fail_on_error = false
471
+ end
472
+ rescue LoadError
473
+ puts "Rubocop is not available. Install the rubocop gem to run the lint tests."
474
+ end
475
+ begin
476
+ stdout = StringIO.new
477
+ $stdout = stdout
478
+ Rake::Task["cookstyle_lint"].invoke
479
+ $stdout = STDOUT
480
+ Rake.application["cookstyle_lint"].reenable
481
+ stdout.string
482
+ rescue => e
483
+ puts "Cookstyle lint checks could not be performed. Error while running cookstyle - #{e}"
484
+ ""
485
+ end
486
+ end
487
+
360
488
  # Check if the profile is internally well-structured. The logger will be
361
489
  # used to print information on errors and warnings which are found.
362
490
  #
@@ -373,6 +501,7 @@ module Inspec
373
501
  },
374
502
  errors: [],
375
503
  warnings: [],
504
+ offenses: [],
376
505
  }
377
506
 
378
507
  entry = lambda { |file, line, column, control, msg|
@@ -395,6 +524,10 @@ module Inspec
395
524
  result[:errors].push(entry.call(file, line, column, control, msg))
396
525
  }
397
526
 
527
+ offense = lambda { |file, line, column, control, msg|
528
+ result[:offenses].push(entry.call(file, line, column, control, msg))
529
+ }
530
+
398
531
  @logger.info "Checking profile in #{@target}"
399
532
  meta_path = @source_reader.target.abs_path(@source_reader.metadata.ref)
400
533
 
@@ -457,8 +590,15 @@ module Inspec
457
590
  warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? || control[:checks].empty?
458
591
  end
459
592
 
460
- # profile is valid if we could not find any error
461
- result[:summary][:valid] = result[:errors].empty?
593
+ # Running cookstyle to check for code offenses
594
+ cookstyle_linting_check.each do |lint_output|
595
+ data = lint_output.split(":")
596
+ msg = "#{data[-2]}:#{data[-1]}"
597
+ offense.call(data[0], data[1], data[2], nil, msg)
598
+ end
599
+
600
+ # profile is valid if we could not find any error & offenses
601
+ result[:summary][:valid] = result[:errors].empty? && result[:offenses].empty?
462
602
 
463
603
  @logger.info "Control definitions OK." if result[:warnings].empty?
464
604
  result
@@ -82,7 +82,7 @@ module Inspec::Resources
82
82
  end
83
83
  end
84
84
 
85
- @params.merge!(params)
85
+ @params.merge!(params) { |key, current_val, new_val| [*current_val].to_a + [*new_val].to_a }
86
86
 
87
87
  to_read = to_read.drop(1)
88
88
  to_read += include_files(params).find_all do |fp|
@@ -101,12 +101,14 @@ module Inspec::Resources
101
101
  include_files_optional = params["IncludeOptional"] || []
102
102
 
103
103
  includes = []
104
- (include_files + include_files_optional).each do |f|
105
- id = Pathname.new(f).absolute? ? f : File.join(conf_dir, f)
106
- files = find_files(id, depth: 1, type: "file")
107
- files += find_files(id, depth: 1, type: "link")
104
+ unless conf_dir.nil?
105
+ (include_files + include_files_optional).each do |f|
106
+ id = Pathname.new(f).absolute? ? f : File.join(conf_dir, f)
107
+ files = find_files(id, depth: 1, type: "file")
108
+ files += find_files(id, depth: 1, type: "link")
108
109
 
109
- includes.push(files) if files
110
+ includes.push(files) if files
111
+ end
110
112
  end
111
113
 
112
114
  # [].flatten! == nil