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.
- checksums.yaml +4 -4
- data/Chef-EULA +9 -0
- data/Gemfile +10 -1
- data/etc/features.sig +6 -0
- data/etc/features.yaml +94 -0
- data/inspec-core.gemspec +14 -5
- data/lib/inspec/backend.rb +2 -0
- data/lib/inspec/base_cli.rb +80 -4
- data/lib/inspec/cached_fetcher.rb +24 -3
- data/lib/inspec/cli.rb +300 -230
- data/lib/inspec/config.rb +24 -2
- data/lib/inspec/dependencies/cache.rb +33 -0
- data/lib/inspec/enhanced_outcomes.rb +1 -0
- data/lib/inspec/errors.rb +5 -0
- data/lib/inspec/exceptions.rb +2 -0
- data/lib/inspec/feature/config.rb +75 -0
- data/lib/inspec/feature/runner.rb +26 -0
- data/lib/inspec/feature.rb +34 -0
- data/lib/inspec/fetcher/git.rb +5 -0
- data/lib/inspec/globals.rb +6 -0
- data/lib/inspec/plugin/v1/plugin_types/fetcher.rb +7 -0
- data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +30 -2
- data/lib/inspec/profile.rb +373 -12
- data/lib/inspec/reporters/cli.rb +1 -1
- data/lib/inspec/reporters.rb +67 -54
- data/lib/inspec/resources/security_policy.rb +7 -2
- data/lib/inspec/run_data.rb +7 -5
- data/lib/inspec/runner.rb +34 -5
- data/lib/inspec/runner_rspec.rb +12 -9
- data/lib/inspec/secrets/yaml.rb +9 -3
- data/lib/inspec/shell.rb +10 -0
- data/lib/inspec/ui.rb +4 -0
- data/lib/inspec/utils/licensing_config.rb +9 -0
- data/lib/inspec/utils/profile_ast_helpers.rb +372 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/inspec/waiver_file_reader.rb +68 -27
- data/lib/inspec.rb +2 -1
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +189 -168
- data/lib/plugins/inspec-habitat/lib/inspec-habitat/cli.rb +10 -3
- data/lib/plugins/inspec-init/lib/inspec-init/cli.rb +1 -0
- data/lib/plugins/inspec-init/lib/inspec-init/cli_plugin.rb +23 -21
- data/lib/plugins/inspec-init/lib/inspec-init/cli_profile.rb +15 -13
- data/lib/plugins/inspec-init/lib/inspec-init/cli_resource.rb +15 -13
- data/lib/plugins/inspec-license/README.md +16 -0
- data/lib/plugins/inspec-license/inspec-license.gemspec +6 -0
- data/lib/plugins/inspec-license/lib/inspec-license/cli.rb +26 -0
- data/lib/plugins/inspec-license/lib/inspec-license.rb +14 -0
- data/lib/plugins/inspec-parallel/README.md +27 -0
- data/lib/plugins/inspec-parallel/inspec-parallel.gemspec +6 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/child_status_reporter.rb +61 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/cli.rb +39 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/command.rb +219 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/runner.rb +265 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/base.rb +24 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/silent.rb +7 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/status.rb +124 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/text.rb +23 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/validator.rb +170 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel.rb +18 -0
- data/lib/plugins/inspec-reporter-html2/templates/control.html.erb +7 -6
- data/lib/plugins/inspec-reporter-html2/templates/default.js +6 -6
- data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +6 -2
- data/lib/plugins/inspec-sign/lib/inspec-sign/cli.rb +11 -4
- data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +6 -13
- 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
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/inspec/runner_rspec.rb
CHANGED
@@ -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
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
data/lib/inspec/secrets/yaml.rb
CHANGED
@@ -24,12 +24,18 @@ module Secrets
|
|
24
24
|
@inputs = ::YAML.load_file(target)
|
25
25
|
end
|
26
26
|
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
data/lib/inspec/version.rb
CHANGED
@@ -15,49 +15,90 @@ module Inspec
|
|
15
15
|
output = {}
|
16
16
|
|
17
17
|
files.each do |file_path|
|
18
|
-
|
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
|
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.
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
57
|
-
data.
|
58
|
-
headers
|
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
|