inspec-core 5.22.29 → 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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/Chef-EULA +9 -0
  3. data/Gemfile +10 -1
  4. data/etc/features.sig +6 -0
  5. data/etc/features.yaml +94 -0
  6. data/inspec-core.gemspec +14 -5
  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 +300 -230
  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 +26 -0
  18. data/lib/inspec/feature.rb +34 -0
  19. data/lib/inspec/fetcher/git.rb +5 -0
  20. data/lib/inspec/globals.rb +6 -0
  21. data/lib/inspec/plugin/v1/plugin_types/fetcher.rb +7 -0
  22. data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +30 -2
  23. data/lib/inspec/profile.rb +373 -12
  24. data/lib/inspec/reporters/cli.rb +1 -1
  25. data/lib/inspec/reporters.rb +67 -54
  26. data/lib/inspec/resources/security_policy.rb +7 -2
  27. data/lib/inspec/run_data.rb +7 -5
  28. data/lib/inspec/runner.rb +34 -5
  29. data/lib/inspec/runner_rspec.rb +12 -9
  30. data/lib/inspec/secrets/yaml.rb +9 -3
  31. data/lib/inspec/shell.rb +10 -0
  32. data/lib/inspec/ui.rb +4 -0
  33. data/lib/inspec/utils/licensing_config.rb +9 -0
  34. data/lib/inspec/utils/profile_ast_helpers.rb +372 -0
  35. data/lib/inspec/version.rb +1 -1
  36. data/lib/inspec/waiver_file_reader.rb +68 -27
  37. data/lib/inspec.rb +2 -1
  38. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +189 -168
  39. data/lib/plugins/inspec-habitat/lib/inspec-habitat/cli.rb +10 -3
  40. data/lib/plugins/inspec-init/lib/inspec-init/cli.rb +1 -0
  41. data/lib/plugins/inspec-init/lib/inspec-init/cli_plugin.rb +23 -21
  42. data/lib/plugins/inspec-init/lib/inspec-init/cli_profile.rb +15 -13
  43. data/lib/plugins/inspec-init/lib/inspec-init/cli_resource.rb +15 -13
  44. data/lib/plugins/inspec-license/README.md +16 -0
  45. data/lib/plugins/inspec-license/inspec-license.gemspec +6 -0
  46. data/lib/plugins/inspec-license/lib/inspec-license/cli.rb +26 -0
  47. data/lib/plugins/inspec-license/lib/inspec-license.rb +14 -0
  48. data/lib/plugins/inspec-parallel/README.md +27 -0
  49. data/lib/plugins/inspec-parallel/inspec-parallel.gemspec +6 -0
  50. data/lib/plugins/inspec-parallel/lib/inspec-parallel/child_status_reporter.rb +61 -0
  51. data/lib/plugins/inspec-parallel/lib/inspec-parallel/cli.rb +39 -0
  52. data/lib/plugins/inspec-parallel/lib/inspec-parallel/command.rb +219 -0
  53. data/lib/plugins/inspec-parallel/lib/inspec-parallel/runner.rb +265 -0
  54. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/base.rb +24 -0
  55. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/silent.rb +7 -0
  56. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/status.rb +124 -0
  57. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/text.rb +23 -0
  58. data/lib/plugins/inspec-parallel/lib/inspec-parallel/validator.rb +170 -0
  59. data/lib/plugins/inspec-parallel/lib/inspec-parallel.rb +18 -0
  60. data/lib/plugins/inspec-reporter-html2/templates/control.html.erb +7 -6
  61. data/lib/plugins/inspec-reporter-html2/templates/default.js +6 -6
  62. data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +6 -2
  63. data/lib/plugins/inspec-sign/lib/inspec-sign/cli.rb +11 -4
  64. data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +6 -13
  65. metadata +54 -13
data/lib/inspec/runner.rb CHANGED
@@ -11,6 +11,7 @@ require "inspec/dependencies/cache"
11
11
  require "inspec/dist"
12
12
  require "inspec/reporters"
13
13
  require "inspec/runner_rspec"
14
+ require "chef-licensing"
14
15
  # spec requirements
15
16
 
16
17
  module Inspec
@@ -60,11 +61,13 @@ module Inspec
60
61
  end
61
62
 
62
63
  if @conf[:waiver_file]
63
- @conf[:waiver_file].each do |file|
64
- unless File.file?(file)
65
- raise Inspec::Exceptions::WaiversFileDoesNotExist, "Waiver file #{file} does not exist."
64
+ Inspec.with_feature("inspec-waivers") {
65
+ @conf[:waiver_file].each do |file|
66
+ unless File.file?(file)
67
+ raise Inspec::Exceptions::WaiversFileDoesNotExist, "Waiver file #{file} does not exist."
68
+ end
66
69
  end
67
- end
70
+ }
68
71
  end
69
72
 
70
73
  # About reading inputs:
@@ -159,16 +162,42 @@ module Inspec
159
162
  end
160
163
 
161
164
  def run(with = nil)
165
+ ChefLicensing.check_software_entitlement! if Inspec::Dist::EXEC_NAME == "inspec"
166
+
167
+ # Validate if profiles are signed and verified
168
+ # Additional check is required to provide error message in case of inspec exec command (exec command can use multiple profiles as well)
169
+ # Only runs this block when preview flag CHEF_PREVIEW_MANDATORY_PROFILE_SIGNING is set
170
+ Inspec.with_feature("inspec-mandatory-profile-signing") {
171
+ unless @conf.allow_unsigned_profiles?
172
+ verify_target_profiles_if_signed(@target_profiles)
173
+ end
174
+ }
175
+
162
176
  Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}"
163
177
  load
164
178
  run_tests(with)
179
+ rescue ChefLicensing::SoftwareNotEntitled
180
+ Inspec::Log.error "License is not entitled to use InSpec."
181
+ Inspec::UI.new.exit(:license_not_entitled)
182
+ rescue ChefLicensing::Error => e
183
+ Inspec::Log.error e.message
184
+ Inspec::UI.new.exit(:usage_error)
185
+ end
186
+
187
+ def verify_target_profiles_if_signed(target_profiles)
188
+ unsigned_profiles = []
189
+ target_profiles.each do |profile|
190
+ unsigned_profiles << profile.name unless profile.verify_if_signed
191
+ end
192
+ raise Inspec::ProfileSignatureRequired, "Signature required for profile/s: #{unsigned_profiles.join(", ")}. Please provide a signed profile. Or set CHEF_ALLOW_UNSIGNED_PROFILES in the environment. Or use `--allow-unsigned-profiles` flag with InSpec CLI. " unless unsigned_profiles.empty?
165
193
  end
166
194
 
167
195
  def render_output(run_data)
168
196
  return if @conf["reporter"].nil?
169
197
 
170
198
  @conf["reporter"].each do |reporter|
171
- result = Inspec::Reporters.render(reporter, run_data, @conf["enhanced_outcomes"])
199
+ enhanced_outcome_flag = @conf["enhanced_outcomes"]
200
+ result = Inspec::Reporters.render(reporter, run_data, enhanced_outcome_flag)
172
201
  raise Inspec::ReporterError, "Error generating reporter '#{reporter[0]}'" if result == false
173
202
  end
174
203
  end
@@ -177,16 +177,18 @@ module Inspec
177
177
  next unless streaming_reporters.include? streaming_reporter_name
178
178
 
179
179
  # Activate the plugin so the formatter ID gets registered with RSpec, presumably
180
- activator = reg.find_activator(plugin_type: :streaming_reporter, activator_name: streaming_reporter_name.to_sym)
181
- activator.activate!
182
180
 
183
- # We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
184
- if file_target&.[]("file").nil?
185
- RSpec.configuration.add_formatter(activator.implementation_class)
186
- else
187
- RSpec.configuration.add_formatter(activator.implementation_class, file_target["file"])
188
- end
189
- @conf["reporter"].delete(streaming_reporter_name)
181
+ Inspec.with_feature("inspec-reporter-#{streaming_reporter_name}") {
182
+ activator = reg.find_activator(plugin_type: :streaming_reporter, activator_name: streaming_reporter_name.to_sym)
183
+ activator.activate!
184
+ # We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
185
+ if file_target&.[]("file").nil?
186
+ RSpec.configuration.add_formatter(activator.implementation_class)
187
+ else
188
+ RSpec.configuration.add_formatter(activator.implementation_class, file_target["file"])
189
+ end
190
+ @conf["reporter"].delete(streaming_reporter_name)
191
+ }
190
192
  end
191
193
  end
192
194
 
@@ -196,6 +198,7 @@ module Inspec
196
198
  def configure_output
197
199
  RSpec.configuration.output_stream = $stdout
198
200
  @formatter = RSpec.configuration.add_formatter(Inspec::Formatters::Base)
201
+
199
202
  @formatter.enhanced_outcomes = @conf.final_options["enhanced_outcomes"]
200
203
  RSpec.configuration.add_formatter(Inspec::Formatters::ShowProgress, $stderr) if @conf[:show_progress]
201
204
  set_optional_formatters
@@ -24,12 +24,18 @@ module Secrets
24
24
  @inputs = ::YAML.load_file(target)
25
25
  end
26
26
 
27
- if @inputs == false || !@inputs.is_a?(Hash)
28
- Inspec::Log.warn("#{self.class} unable to parse #{target}: invalid YAML or contents is not a Hash")
27
+ # In case of empty yaml file raise the warning else raise the parsing error.
28
+ if !@inputs || @inputs.empty?
29
+ Inspec::Log.warn("Unable to parse #{target}: YAML file is empty.")
29
30
  @inputs = nil
31
+ elsif !@inputs.is_a?(Hash)
32
+ # Exits with usage error.
33
+ Inspec::Log.error("Unable to parse #{target}: invalid YAML or contents is not a Hash")
34
+ Inspec::UI.new.exit(:usage_error)
30
35
  end
31
36
  rescue => e
32
- raise "Error reading InSpec inputs: #{e}"
37
+ # Any other error related to Yaml parsing will be raised here.
38
+ raise "Error reading YAML file #{target}: #{e}"
33
39
  end
34
40
  end
35
41
  end
data/lib/inspec/shell.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require "chef-licensing"
2
+ require "inspec/dist"
3
+
1
4
  autoload :Pry, "pry"
2
5
 
3
6
  module Inspec
@@ -10,6 +13,7 @@ module Inspec
10
13
  end
11
14
 
12
15
  def start
16
+ ChefLicensing.check_software_entitlement! if Inspec::Dist::EXEC_NAME == "inspec"
13
17
  # This will hold a single evaluation binding context as opened within
14
18
  # the instance_eval context of the anonymous class that the profile
15
19
  # context creates to evaluate each individual test file. We want to
@@ -18,6 +22,12 @@ module Inspec
18
22
  @ctx_binding = @runner.eval_with_virtual_profile("binding")
19
23
  configure_pry
20
24
  @ctx_binding.pry
25
+ rescue ChefLicensing::SoftwareNotEntitled
26
+ Inspec::Log.error "License is not entitled to use InSpec."
27
+ Inspec::UI.new.exit(:license_not_entitled)
28
+ rescue ChefLicensing::Error => e
29
+ Inspec::Log.error e.message
30
+ Inspec::UI.new.exit(:usage_error)
21
31
  end
22
32
 
23
33
  def configure_pry # rubocop:disable Metrics/AbcSize
data/lib/inspec/ui.rb CHANGED
@@ -32,9 +32,13 @@ module Inspec
32
32
  EXIT_FATAL_DEPRECATION = 3
33
33
  EXIT_GEM_DEPENDENCY_LOAD_ERROR = 4
34
34
  EXIT_BAD_SIGNATURE = 5
35
+ EXIT_SIGNATURE_REQUIRED = 6
35
36
  EXIT_LICENSE_NOT_ACCEPTED = 172
37
+ EXIT_LICENSE_NOT_ENTITLED = 173
38
+ EXIT_LICENSE_NOT_SET = 174
36
39
  EXIT_FAILED_TESTS = 100
37
40
  EXIT_SKIPPED_TESTS = 101
41
+ EXIT_TERMINATED_BY_CTL_C = 130
38
42
 
39
43
  attr_reader :io
40
44
 
@@ -0,0 +1,9 @@
1
+ require_relative "../log"
2
+ require "chef-licensing"
3
+ ChefLicensing.configure do |config|
4
+ config.chef_product_name = "InSpec"
5
+ config.chef_entitlement_id = "3ff52c37-e41f-4f6c-ad4d-365192205968"
6
+ config.chef_executable_name = "inspec"
7
+ config.license_server_url = "https://services.chef.io/licensing"
8
+ config.logger = Inspec::Log
9
+ end
@@ -0,0 +1,372 @@
1
+ require "ast"
2
+ require "rubocop-ast"
3
+ module Inspec
4
+ class Profile
5
+ class AstHelper
6
+ class CollectorBase
7
+ include Parser::AST::Processor::Mixin
8
+ include RuboCop::AST::Traversal
9
+
10
+ attr_reader :memo
11
+ def initialize(memo)
12
+ @memo = memo
13
+ end
14
+ end
15
+
16
+ class InputCollectorBase < CollectorBase
17
+ VALID_INPUT_OPTIONS = %i{name value type required priority pattern profile sensitive}.freeze
18
+
19
+ REQUIRED_VALUES_MAP = {
20
+ true: true,
21
+ false: false,
22
+ }.freeze
23
+
24
+ def initialize(memo)
25
+ @memo = memo
26
+ end
27
+
28
+ def collect_input(input_children)
29
+ input_name = input_children.children[2].value
30
+
31
+ # Check if memo[:inputs] already has a value for the input_name, if yes, then skip adding it to the array
32
+ unless memo[:inputs].any? { |input| input[:name] == input_name }
33
+ # The value will be updated if available in the input_children
34
+ opts = {
35
+ value: "Input '#{input_name}' does not have a value. Skipping test.",
36
+ }
37
+
38
+ if input_children.children[3]&.type == :hash
39
+ input_children.children[3].children.each do |child_node|
40
+ if VALID_INPUT_OPTIONS.include?(child_node.key.value)
41
+ if child_node.value.class == RuboCop::AST::Node && REQUIRED_VALUES_MAP.key?(child_node.value.type)
42
+ opts.merge!(child_node.key.value => REQUIRED_VALUES_MAP[child_node.value.type])
43
+ elsif child_node.value.class == RuboCop::AST::HashNode
44
+ # Here value will be a hash
45
+ values = {}
46
+ child_node.value.children.each do |grand_child_node|
47
+ values.merge!(grand_child_node.key.value => grand_child_node.value.value)
48
+ end
49
+ opts.merge!(child_node.key.value => values)
50
+ else
51
+ opts.merge!(child_node.key.value => child_node.value.value)
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ # TODO: Add rules for handling the input options or use existing rules if available
58
+ # 1. Handle pattern matching for the given input value
59
+ # 2. Handle data-type matching for the given input value
60
+ # 3. Handle required flag for the given input value
61
+ # 4. Handle sensitive flag for the given input value
62
+ memo[:inputs] ||= []
63
+ input_hash = {
64
+ name: input_name,
65
+ options: opts,
66
+ }
67
+ memo[:inputs] << input_hash
68
+ end
69
+ end
70
+
71
+ def check_and_collect_input(node)
72
+ if input_pattern_match?(node)
73
+ collect_input(node)
74
+ else
75
+ node.children.each do |child_node|
76
+ check_and_collect_input(child_node) if input_pattern_match?(child_node)
77
+ end
78
+ end
79
+ end
80
+
81
+ def input_pattern_match?(node)
82
+ RuboCop::AST::NodePattern.new("(send nil? :input ...)").match(node)
83
+ end
84
+ end
85
+
86
+ class ImpactCollector < CollectorBase
87
+ def on_send(node)
88
+ if RuboCop::AST::NodePattern.new("(send nil? :impact ...)").match(node)
89
+ memo[:impact] = node.children[2].value
90
+ end
91
+ end
92
+ end
93
+
94
+ class DescCollector < CollectorBase
95
+ def on_send(node)
96
+ if RuboCop::AST::NodePattern.new("(send nil? :desc ...)").match(node)
97
+ memo[:descriptions] ||= {}
98
+ if node.children[2] && node.children[3]
99
+ # NOTE: This assumes the description is as below
100
+ # desc 'label', 'An optional description with a label' # Pair a part of the description with a label
101
+ memo[:descriptions] = memo[:descriptions].merge(node.children[2].value => node.children[3].value)
102
+ else
103
+ memo[:desc] = node.children[2].value
104
+ memo[:descriptions] = memo[:descriptions].merge(default: node.children[2].value)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ class TitleCollector < CollectorBase
111
+ def on_send(node)
112
+ if RuboCop::AST::NodePattern.new("(send nil? :title ...)").match(node)
113
+ # TODO - title may not be a simple string
114
+ memo[:title] = node.children[2].value
115
+ end
116
+ end
117
+ end
118
+
119
+ class TagCollector < CollectorBase
120
+
121
+ ACCPETABLE_TAG_TYPE_TO_VALUES = {
122
+ false: false,
123
+ true: true,
124
+ nil: nil,
125
+ }.freeze
126
+
127
+ def on_send(node)
128
+ if RuboCop::AST::NodePattern.new("(send nil? :tag ...)").match(node)
129
+ memo[:tags] ||= {}
130
+
131
+ node.children[2..-1].each do |tag_node|
132
+ collect_tags(tag_node)
133
+ end
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ def collect_tags(tag_node)
140
+ if tag_node.type == :str || tag_node.type == :sym
141
+ memo[:tags] = memo[:tags].merge(tag_node.value => nil)
142
+ elsif tag_node.type == :hash
143
+ tags_coll = {}
144
+ tag_node.children.each do |child_tag|
145
+ key = child_tag.key.value
146
+ if child_tag.value.type == :array
147
+ value = child_tag.value.children.map { |child_node| child_node.type == :str ? child_node.children.first : nil }
148
+ elsif ACCPETABLE_TAG_TYPE_TO_VALUES.key?(child_tag.value.type)
149
+ value = ACCPETABLE_TAG_TYPE_TO_VALUES[child_tag.value.type]
150
+ else
151
+ if child_tag.value.children.first.class == RuboCop::AST::SendNode
152
+ # Cases like this: (where there is no assignment of the value to a variable like gcp_project_id)
153
+ # tag project: gcp_project_id.to_s
154
+ #
155
+ # Lecacy evaluates gcp_project_id.to_s and then passes the value to the tag
156
+ # We are not evaluating the value here, so we are just passing the value as it is
157
+ #
158
+ # TODO: Do we need to evaluate the value here?
159
+ # (byebug) child_tag.value
160
+ # s(:send,
161
+ # s(:send, nil, :gcp_project_id), :to_s)
162
+ value = child_tag.value.children.first.children[1]
163
+ elsif child_tag.value.children.first.class == RuboCop::AST::Node
164
+ # Cases like this:
165
+ # control_id = '1.1'
166
+ # tag cis_gcp: control_id.to_s
167
+ value = child_tag.value.children.first.children[0]
168
+ else
169
+ value = child_tag.value.value
170
+ end
171
+ end
172
+ tags_coll.merge!(key => value)
173
+ end
174
+ memo[:tags] = memo[:tags].merge(tags_coll)
175
+ end
176
+ end
177
+ end
178
+
179
+ class RefCollector < CollectorBase
180
+ def on_send(node)
181
+ if RuboCop::AST::NodePattern.new("(send nil? :ref ...)").match(node)
182
+ # Construct the array of refs hash as below
183
+
184
+ # "refs": [
185
+ # {
186
+ # "url": "http://",
187
+ # "ref": "Some ref"
188
+ # },
189
+ # {
190
+ # "ref": "https://",
191
+ # }
192
+ # ]
193
+
194
+ # node.children[1] && node.children[1] == :ref - we don't need this check as the pattern match above will take care of it
195
+ return unless node.children[2]
196
+
197
+ references = {}
198
+
199
+ if node.children[2].type == :begin
200
+ # Case for: ref ({:ref=>"Some ref", :url=>"https://"})
201
+ # find the hash node
202
+ iterate_child_and_collect_ref(node.children[2].children, references)
203
+ elsif node.children[2].type == :str
204
+ # Case for: ref "ref1", url: "http://",
205
+ references.merge!(ref: node.children[2].value)
206
+ iterate_child_and_collect_ref(node.children[3..-1], references)
207
+ end
208
+
209
+ memo[:refs] ||= []
210
+ memo[:refs] << references
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ def iterate_child_and_collect_ref(child_node, references = {})
217
+ child_node.each do |ref_node|
218
+ if ref_node.type == :hash
219
+ iterate_hash_node(ref_node, references)
220
+ elsif ref_node.type == :str
221
+ references.merge!(ref_node.value => nil)
222
+ end
223
+ end
224
+ end
225
+
226
+ def iterate_hash_node(hash_node, references = {})
227
+ # hash node like this:
228
+ # s(:hash,
229
+ # s(:pair,
230
+ # s(:sym, :url),
231
+ # s(:str, "https://")))
232
+ #
233
+ # or like this:
234
+ # (byebug) hash_node
235
+ # s(:hash,
236
+ # s(:pair,
237
+ # s(:sym, :url),
238
+ # s(:send,
239
+ # s(:send, nil, :cis_url), :to_s)))
240
+ hash_node.children.each do |child_node|
241
+ if child_node.type == :pair
242
+ if child_node.value.children.first.class == RuboCop::AST::SendNode
243
+ # Case like this (where there is no assignment of the value to a variable like cis_url)
244
+ # ref 'CIS Benchmark', url: cis_url.to_s
245
+ # Lecacy evaluates cis_url.to_s and then passes the value to the ref
246
+ # We are not evaluating the value here, so we are just passing the value as it is
247
+ #
248
+ # TODO: Do we need to evaluate the value here?
249
+ #
250
+ # (byebug) child_node.value.children.first
251
+ # s(:send, nil, :cis_url)
252
+ value = child_node.value.children.first.children[1]
253
+ elsif child_node.value.class == RuboCop::AST::SendNode
254
+ # Cases like this:
255
+ # cis_url = attribute('cis_url')
256
+ # ref 'CIS Benchmark', url: cis_url.to_s
257
+ value = child_node.value.children.first.children[0]
258
+ else
259
+ # Cases like this: ref 'CIS Benchmark - 2', url: "https://"
260
+ # require 'byebug'; byebug
261
+ value = child_node.value.value
262
+ end
263
+ references.merge!(child_node.key.value => value)
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ class ControlIDCollector < CollectorBase
270
+ attr_reader :seen_control_ids, :source_location_ref, :include_tests
271
+ def initialize(memo, source_location_ref, include_tests: false)
272
+ @memo = memo
273
+ @seen_control_ids = {}
274
+ @source_location_ref = source_location_ref
275
+ @include_tests = include_tests
276
+ end
277
+
278
+ def on_block(block_node)
279
+ if RuboCop::AST::NodePattern.new("(block (send nil? :control ...) ...)").match(block_node)
280
+ # NOTE: Assuming begin block is at the index 2
281
+ begin_block = block_node.children[2]
282
+ control_node = block_node.children[0]
283
+
284
+ # TODO - This assumes the control ID is always a plain string, which we know it is often not!
285
+ control_id = control_node.children[2].value
286
+ # TODO - BUG - this keeps seeing the same nodes over and over againa, and so repeating control IDs. We are ignoring duplicate control IDs, which is incorrect.
287
+ return if seen_control_ids[control_id]
288
+
289
+ seen_control_ids[control_id] = true
290
+
291
+ control_data = {
292
+ id: control_id,
293
+ code: block_node.source,
294
+ source_location: {
295
+ line: block_node.first_line,
296
+ ref: source_location_ref,
297
+ },
298
+ title: nil,
299
+ desc: nil,
300
+ descriptions: {},
301
+ impact: 0.5,
302
+ refs: [],
303
+ tags: {},
304
+ }
305
+ control_data[:checks] = [] if include_tests
306
+
307
+ # Scan the code block for per-control metadata
308
+ collectors = []
309
+ collectors.push ImpactCollector.new(control_data)
310
+ collectors.push DescCollector.new(control_data)
311
+ collectors.push TitleCollector.new(control_data)
312
+ collectors.push TagCollector.new(control_data)
313
+ collectors.push RefCollector.new(control_data)
314
+ collectors.push InputCollectorWithinControlBlock.new(@memo)
315
+ collectors.push TestsCollector.new(control_data) if include_tests
316
+
317
+ begin_block.each_node do |node_within_control|
318
+ collectors.each { |collector| collector.process(node_within_control) }
319
+ end
320
+
321
+ memo[:controls].push control_data
322
+ end
323
+ end
324
+ end
325
+
326
+ class InputCollectorWithinControlBlock < InputCollectorBase
327
+ def initialize(memo)
328
+ @memo = memo
329
+ end
330
+
331
+ def on_send(node)
332
+ check_and_collect_input(node)
333
+ end
334
+ end
335
+
336
+ class InputCollectorOutsideControlBlock < InputCollectorBase
337
+ def initialize(memo)
338
+ @memo = memo
339
+ end
340
+
341
+ # TODO: There is scope to refactor InputCollectorOutsideControlBlock and InputCollectorWithinControlBlock
342
+ # 1. We can have a single class for both the collectors
343
+ # 2. We can have a on_send and on_lvasgn method in the same class
344
+ # :lvasgn in ast stands for "local variable assignment"
345
+ def on_lvasgn(node)
346
+ # We are looking for the following pattern in the AST
347
+ # (lvasgn :var_name (send nil? :input ...))
348
+ # example: a = input('a') or a = input('a', value: 'b')
349
+ # and not this: a = 1
350
+ if RuboCop::AST::NodePattern.new("(lvasgn _ (send nil? :input ...))").match(node)
351
+ input_children = node.children[1]
352
+ collect_input(input_children)
353
+ end
354
+ end
355
+
356
+ def on_send(node)
357
+ check_and_collect_input(node)
358
+ end
359
+ end
360
+
361
+ class TestsCollector < CollectorBase
362
+
363
+ def on_block(node)
364
+ if RuboCop::AST::NodePattern.new("(block (send nil? :describe ...) ...)").match(node) ||
365
+ RuboCop::AST::NodePattern.new("(block (send nil? :expect ...) ...)").match(node)
366
+ memo[:checks] << node.source
367
+ end
368
+ end
369
+ end
370
+ end
371
+ end
372
+ end
@@ -1,3 +1,3 @@
1
1
  module Inspec
2
- VERSION = "5.22.29".freeze
2
+ VERSION = "6.6.0".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