inspec-core 5.22.29 → 5.22.40
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/Gemfile +4 -0
- data/lib/inspec/cli.rb +16 -3
- data/lib/inspec/config.rb +9 -0
- data/lib/inspec/dependencies/dependency_set.rb +2 -2
- data/lib/inspec/dsl.rb +1 -1
- data/lib/inspec/profile.rb +331 -13
- data/lib/inspec/reporters/cli.rb +1 -1
- data/lib/inspec/resources/security_policy.rb +7 -2
- data/lib/inspec/rule.rb +14 -9
- data/lib/inspec/runner.rb +1 -1
- data/lib/inspec/utils/profile_ast_helpers.rb +372 -0
- data/lib/inspec/utils/waivers/csv_file_reader.rb +1 -1
- data/lib/inspec/utils/waivers/excel_file_reader.rb +1 -1
- data/lib/inspec/version.rb +1 -1
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +2 -2
- data/lib/plugins/inspec-reporter-html2/templates/control.html.erb +7 -6
- data/lib/plugins/inspec-reporter-html2/templates/default.js +6 -6
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a95805ac2a4faab83d1826792a5d838caa5f1808fcc884e061cf3f7070ab6ef2
|
4
|
+
data.tar.gz: 984dfca55efec04f1e9b98ddfebf5be636b96a91c7fe7dc97f9ffb9789c60af3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb6e20f346e0e5ab27878931b1396c10a633b99767472774299d92fce3ae2ccf01399cb100e5ae4566456039a3d814ac4148670da65b41563e72139d29551ed7
|
7
|
+
data.tar.gz: 0c9c665c3afb6d90121bf40ac00736a900955e0ea5955af58f7e56378d18eecfd7560f40e2cce049afd7156ecb495c77b1c58765c0e69f82bfd03e976c2f367f
|
data/Gemfile
CHANGED
@@ -11,6 +11,10 @@ gem "inspec-bin", path: "./inspec-bin"
|
|
11
11
|
|
12
12
|
gem "ffi", ">= 1.9.14", "!= 1.13.0", "!= 1.14.2"
|
13
13
|
|
14
|
+
# We have a build issue 2023-11-13 with unf_ext 0.0.9 so we are pinning to 0.0.8.2
|
15
|
+
# See https://github.com/knu/ruby-unf_ext/issues/74 https://buildkite.com/chef/inspec-inspec-inspec-5-omnibus-release/builds/22
|
16
|
+
gem "unf_ext", "= 0.0.8.2"
|
17
|
+
|
14
18
|
# inspec tests depend text output that changed in the 3.10 release
|
15
19
|
# but our runtime dep is still 3.9+
|
16
20
|
gem "rspec", ">= 3.10"
|
data/lib/inspec/cli.rb
CHANGED
@@ -68,8 +68,14 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
68
68
|
desc: "A list of controls to include. Ignore all other tests."
|
69
69
|
option :tags, type: :array,
|
70
70
|
desc: "A list of tags to filter controls and include only those. Ignore all other tests."
|
71
|
+
option :legacy_export, type: :boolean, default: false,
|
72
|
+
desc: "Run with legacy export."
|
71
73
|
profile_options
|
72
74
|
def json(target)
|
75
|
+
# Config initialisation is needed before deprecation warning can be issued
|
76
|
+
# Deprecator calls config get method to fetch the config value
|
77
|
+
# Without config initialisation, the config value is not set and hence calling config get through deprecator will set the value of config as blank, making options of json command inaccessible.
|
78
|
+
config
|
73
79
|
# This deprecation warning is ignored currently.
|
74
80
|
Inspec.deprecate(:renamed_to_inspec_export)
|
75
81
|
export(target, true)
|
@@ -86,6 +92,8 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
86
92
|
desc: "For --what=profile, a list of controls to include. Ignore all other tests."
|
87
93
|
option :tags, type: :array,
|
88
94
|
desc: "For --what=profile, a list of tags to filter controls and include only those. Ignore all other tests."
|
95
|
+
option :legacy_export, type: :boolean, default: false,
|
96
|
+
desc: "Run with legacy export."
|
89
97
|
profile_options
|
90
98
|
def export(target, as_json = false)
|
91
99
|
o = config
|
@@ -121,16 +129,17 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
121
129
|
|
122
130
|
case what
|
123
131
|
when "profile"
|
132
|
+
profile_info = o[:legacy_export] ? profile.info : profile.info_from_parse
|
124
133
|
if format == "json"
|
125
134
|
require "json" unless defined?(JSON)
|
126
135
|
# Write JSON
|
127
136
|
Inspec::Utils::JsonProfileSummary.produce_json(
|
128
|
-
info:
|
137
|
+
info: profile_info,
|
129
138
|
write_path: dst
|
130
139
|
)
|
131
140
|
elsif format == "yaml"
|
132
141
|
Inspec::Utils::YamlProfileSummary.produce_yaml(
|
133
|
-
info:
|
142
|
+
info: profile_info,
|
134
143
|
write_path: dst
|
135
144
|
)
|
136
145
|
end
|
@@ -152,6 +161,8 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
152
161
|
desc: "The output format to use. Valid values: `json` and `doc`. Default value: `doc`."
|
153
162
|
option :with_cookstyle, type: :boolean,
|
154
163
|
desc: "Enable or disable cookstyle checks.", default: false
|
164
|
+
option :legacy_check, type: :boolean, default: false,
|
165
|
+
desc: "Run with legacy check."
|
155
166
|
profile_options
|
156
167
|
def check(path) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
157
168
|
o = config
|
@@ -166,7 +177,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
166
177
|
|
167
178
|
# run check
|
168
179
|
profile = Inspec::Profile.for_target(path, o)
|
169
|
-
result = profile.check
|
180
|
+
result = o[:legacy_check] ? profile.legacy_check : profile.check
|
170
181
|
|
171
182
|
if o["format"] == "json"
|
172
183
|
puts JSON.generate(result)
|
@@ -248,6 +259,8 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
248
259
|
desc: "Run profile check before archiving."
|
249
260
|
option :export, type: :boolean, default: false,
|
250
261
|
desc: "Export the profile to inspec.json and include in archive"
|
262
|
+
option :legacy_export, type: :boolean, default: false,
|
263
|
+
desc: "Export the profile in legacy mode to inspec.json and include in archive"
|
251
264
|
def archive(path, log_level = nil)
|
252
265
|
o = config
|
253
266
|
diagnose(o)
|
data/lib/inspec/config.rb
CHANGED
@@ -448,6 +448,15 @@ module Inspec
|
|
448
448
|
# Reporter options may be defined top-level.
|
449
449
|
options.merge!(config_file_reporter_options)
|
450
450
|
|
451
|
+
# when sent reporter from compliance-mode (via chef-client), the reporter is a symbol
|
452
|
+
if @cli_opts.key?(:reporter) && @cli_opts["reporter"].nil?
|
453
|
+
@cli_opts["reporter"] = @cli_opts[:reporter]
|
454
|
+
@cli_opts.delete(:reporter)
|
455
|
+
elsif @cli_opts.key?(:reporter) && @cli_opts.key?("reporter") && @cli_opts["reporter"].is_a?(Array)
|
456
|
+
# combine reporter and "reporter" options into "reporter" option
|
457
|
+
@cli_opts["reporter"] = @cli_opts[:reporter] + @cli_opts["reporter"]
|
458
|
+
end
|
459
|
+
|
451
460
|
if @cli_opts["reporter"]
|
452
461
|
# Add reporter_cli_opts in options to capture reporter cli opts separately
|
453
462
|
options.merge!({ "reporter_cli_opts" => @cli_opts["reporter"] })
|
@@ -26,7 +26,7 @@ module Inspec
|
|
26
26
|
dep_list = {}
|
27
27
|
dependencies.each do |d|
|
28
28
|
# if depedent profile does not have a source version then only name is used in dependency hash
|
29
|
-
key_name = (d.source_version.
|
29
|
+
key_name = ((d.source_version.nil? || d.source_version.empty?) ? "#{d.name}" : "#{d.name}-#{d.source_version}") rescue "#{d.name}"
|
30
30
|
dep_list[key_name] = d
|
31
31
|
end
|
32
32
|
new(cwd, cache, dep_list, backend)
|
@@ -42,7 +42,7 @@ module Inspec
|
|
42
42
|
dep_list = {}
|
43
43
|
dep_tree.each do |d|
|
44
44
|
# if depedent profile does not have a source version then only name is used in dependency hash
|
45
|
-
key_name = (d.source_version.
|
45
|
+
key_name = ((d.source_version.nil? || d.source_version.empty?) ? "#{d.name}" : "#{d.name}-#{d.source_version}") rescue "#{d.name}"
|
46
46
|
dep_list[key_name] = d
|
47
47
|
dep_list.merge!(flatten_dep_tree(d.dependencies))
|
48
48
|
end
|
data/lib/inspec/dsl.rb
CHANGED
@@ -95,7 +95,7 @@ module Inspec::DSL
|
|
95
95
|
# 1. Fetching VERSION from a profile dependency name which is in a format NAME-VERSION.
|
96
96
|
# 2. Matching original profile dependency name with profile name used with include or require control DSL.
|
97
97
|
source_version = value.source_version
|
98
|
-
unless source_version.
|
98
|
+
unless source_version.nil? || source_version.empty?
|
99
99
|
profile_id_key = key.split("-#{source_version}")[0]
|
100
100
|
new_profile_id = key if profile_id_key == profile_id
|
101
101
|
end
|
data/lib/inspec/profile.rb
CHANGED
@@ -15,6 +15,7 @@ require "inspec/dependencies/dependency_set"
|
|
15
15
|
require "inspec/utils/json_profile_summary"
|
16
16
|
require "inspec/dependency_loader"
|
17
17
|
require "inspec/dependency_installer"
|
18
|
+
require "inspec/utils/profile_ast_helpers"
|
18
19
|
|
19
20
|
module Inspec
|
20
21
|
class Profile
|
@@ -247,7 +248,7 @@ module Inspec
|
|
247
248
|
## Find the waivers file
|
248
249
|
# - TODO: cli_opts and instance_variable_get could be exposed
|
249
250
|
waiver_paths = cfg.instance_variable_get(:@cli_opts)["waiver_file"]
|
250
|
-
if waiver_paths.
|
251
|
+
if waiver_paths.nil? || waiver_paths.empty?
|
251
252
|
Inspec::Log.error "Must use --waiver-file with --filter-waived-controls"
|
252
253
|
Inspec::UI.new.exit(:usage_error)
|
253
254
|
end
|
@@ -275,7 +276,7 @@ module Inspec
|
|
275
276
|
# be processed and rendered
|
276
277
|
tests.each do |control_filename, source_code|
|
277
278
|
cleared_tests = source_code.scan(/control\s+['"].+?['"].+?(?=(?:control\s+['"].+?['"])|\z)/m).collect do |element|
|
278
|
-
next if element.
|
279
|
+
next if element.nil? || element.empty?
|
279
280
|
|
280
281
|
if element&.match?(waived_control_id_regex)
|
281
282
|
splitlines = element.split("\n")
|
@@ -514,6 +515,135 @@ module Inspec
|
|
514
515
|
res
|
515
516
|
end
|
516
517
|
|
518
|
+
# Return data like profile.info(params), but try to do so without evaluating the profile.
|
519
|
+
def info_from_parse(include_tests: false)
|
520
|
+
return @info_from_parse unless @info_from_parse.nil?
|
521
|
+
|
522
|
+
@info_from_parse = {
|
523
|
+
controls: [],
|
524
|
+
groups: [],
|
525
|
+
}
|
526
|
+
|
527
|
+
# TODO - look at the various source contents
|
528
|
+
# PASS 1: parse them using rubocop-ast
|
529
|
+
# Look for controls, top-level metadata, and inputs
|
530
|
+
# PASS 2: Using the control IDs, deterimine the extents -
|
531
|
+
# line locations - of the coontrol IDs in each file, and
|
532
|
+
# then extract each source code block. Use this to populate the source code
|
533
|
+
# locations and 'code' properties.
|
534
|
+
|
535
|
+
# TODO: Verify that it doesn't do evaluation (ideally shouldn't because it is reading simply yaml file)
|
536
|
+
@info_from_parse = @info_from_parse.merge(metadata.params)
|
537
|
+
|
538
|
+
inputs_hash = {}
|
539
|
+
# Note: This only handles the case when inputs are defined in metadata file
|
540
|
+
if @profile_id.nil?
|
541
|
+
# identifying inputs using profile name
|
542
|
+
inputs_hash = Inspec::InputRegistry.list_inputs_for_profile(@info_from_parse[:name])
|
543
|
+
else
|
544
|
+
inputs_hash = Inspec::InputRegistry.list_inputs_for_profile(@profile_id)
|
545
|
+
end
|
546
|
+
|
547
|
+
# TODO: Verify if I need to do the below conversion for inputs to array
|
548
|
+
if inputs_hash.nil? || inputs_hash.empty?
|
549
|
+
# convert to array for backwards compatability
|
550
|
+
@info_from_parse[:inputs] = []
|
551
|
+
else
|
552
|
+
@info_from_parse[:inputs] = inputs_hash.values.map(&:to_hash)
|
553
|
+
end
|
554
|
+
|
555
|
+
@info_from_parse[:sha256] = sha256
|
556
|
+
|
557
|
+
# Populate :status and :status_message
|
558
|
+
if supports_platform?
|
559
|
+
@info_from_parse[:status_message] = @status_message || ""
|
560
|
+
@info_from_parse[:status] = failed? ? "failed" : "loaded"
|
561
|
+
else
|
562
|
+
@info_from_parse[:status] = "skipped"
|
563
|
+
msg = "Skipping profile: '#{name}' on unsupported platform: '#{backend.platform.name}/#{backend.platform.release}'."
|
564
|
+
@info_from_parse[:status_message] = msg
|
565
|
+
end
|
566
|
+
|
567
|
+
# @source_reader.tests contains a hash mapping control filenames to control file contents
|
568
|
+
@source_reader.tests.each do |control_filename, control_file_source|
|
569
|
+
# Parse the source code
|
570
|
+
src = RuboCop::AST::ProcessedSource.new(control_file_source, RUBY_VERSION.to_f)
|
571
|
+
source_location_ref = @source_reader.target.abs_path(control_filename)
|
572
|
+
|
573
|
+
input_collector = Inspec::Profile::AstHelper::InputCollectorOutsideControlBlock.new(@info_from_parse)
|
574
|
+
ctl_id_collector = Inspec::Profile::AstHelper::ControlIDCollector.new(@info_from_parse, source_location_ref,
|
575
|
+
include_tests: include_tests)
|
576
|
+
|
577
|
+
# Collect all metadata defined in the control block and inputs defined inside the control block
|
578
|
+
src.ast.each_node { |n|
|
579
|
+
ctl_id_collector.process(n)
|
580
|
+
input_collector.process(n)
|
581
|
+
}
|
582
|
+
|
583
|
+
# For each control ID
|
584
|
+
# Look for per-control metadata
|
585
|
+
# Filter controls by --controls, list of controls to include is available in include_controls_list
|
586
|
+
|
587
|
+
# NOTE: This is a hack to duplicate refs.
|
588
|
+
# TODO: Fix this in the ref collector or the way we traverse the AST
|
589
|
+
@info_from_parse[:controls].each { |control| control[:refs].uniq! }
|
590
|
+
|
591
|
+
@info_from_parse[:controls] = filter_controls_by_id_and_tags(@info_from_parse[:controls])
|
592
|
+
|
593
|
+
# Update groups after filtering controls to handle --controls option
|
594
|
+
update_groups_from(control_filename, src)
|
595
|
+
|
596
|
+
# NOTE: This is a hack to duplicate inputs.
|
597
|
+
# TODO: Fix this in the input collector or the way we traverse the AST
|
598
|
+
@info_from_parse[:inputs] = @info_from_parse[:inputs].uniq
|
599
|
+
end
|
600
|
+
@info_from_parse
|
601
|
+
end
|
602
|
+
|
603
|
+
def filter_controls_by_id_and_tags(controls)
|
604
|
+
controls.select do |control|
|
605
|
+
tag_ids = get_all_tags_list(control[:tags])
|
606
|
+
(include_controls_list.empty? || include_controls_list.any? { |control_id| control_id.match?(control[:id]) }) &&
|
607
|
+
(include_tags_list.empty? || include_tags_list.any? { |tag_id| tag_ids.any? { |tag| tag_id.match?(tag) } })
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
def get_all_tags_list(control_tags)
|
612
|
+
all_tags = []
|
613
|
+
control_tags.each do |tags|
|
614
|
+
all_tags.push(tags)
|
615
|
+
end
|
616
|
+
all_tags.flatten.compact.uniq.map(&:to_s)
|
617
|
+
rescue
|
618
|
+
[]
|
619
|
+
end
|
620
|
+
|
621
|
+
def include_group_data?(group_data)
|
622
|
+
unless include_controls_list.empty?
|
623
|
+
# {:id=>"controls/example-tmp.rb", :title=>"/ profile", :controls=>["tmp-1.0"]}
|
624
|
+
# Check if the group should be included based on the controls it contains
|
625
|
+
group_data[:controls].any? do |control_id|
|
626
|
+
include_controls_list.any? { |id| id.match?(control_id) }
|
627
|
+
end
|
628
|
+
else
|
629
|
+
true
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
def update_groups_from(control_filename, src)
|
634
|
+
group_data = {
|
635
|
+
id: control_filename,
|
636
|
+
title: nil,
|
637
|
+
}
|
638
|
+
source_location_ref = @source_reader.target.abs_path(control_filename)
|
639
|
+
Inspec::Profile::AstHelper::TitleCollector.new(group_data)
|
640
|
+
.process(src.ast.child_nodes.first) # Picking the title defined for the whole controls file
|
641
|
+
group_controls = @info_from_parse[:controls].select { |control| control[:source_location][:ref] == source_location_ref }
|
642
|
+
group_data[:controls] = group_controls.map { |control| control[:id] }
|
643
|
+
|
644
|
+
@info_from_parse[:groups].push(group_data) if include_group_data?(group_data)
|
645
|
+
end
|
646
|
+
|
517
647
|
def cookstyle_linting_check
|
518
648
|
msgs = []
|
519
649
|
return msgs if Inspec.locally_windows? # See #5723
|
@@ -553,6 +683,122 @@ module Inspec
|
|
553
683
|
end
|
554
684
|
end
|
555
685
|
|
686
|
+
def legacy_check # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
687
|
+
# initial values for response object
|
688
|
+
result = {
|
689
|
+
summary: {
|
690
|
+
valid: false,
|
691
|
+
timestamp: Time.now.iso8601,
|
692
|
+
location: @target,
|
693
|
+
profile: nil,
|
694
|
+
controls: 0,
|
695
|
+
},
|
696
|
+
errors: [],
|
697
|
+
warnings: [],
|
698
|
+
offenses: [],
|
699
|
+
}
|
700
|
+
|
701
|
+
entry = lambda { |file, line, column, control, msg|
|
702
|
+
{
|
703
|
+
file: file,
|
704
|
+
line: line,
|
705
|
+
column: column,
|
706
|
+
control_id: control,
|
707
|
+
msg: msg,
|
708
|
+
}
|
709
|
+
}
|
710
|
+
|
711
|
+
warn = lambda { |file, line, column, control, msg|
|
712
|
+
@logger.warn(msg)
|
713
|
+
result[:warnings].push(entry.call(file, line, column, control, msg))
|
714
|
+
}
|
715
|
+
|
716
|
+
error = lambda { |file, line, column, control, msg|
|
717
|
+
@logger.error(msg)
|
718
|
+
result[:errors].push(entry.call(file, line, column, control, msg))
|
719
|
+
}
|
720
|
+
|
721
|
+
offense = lambda { |file, line, column, control, msg|
|
722
|
+
result[:offenses].push(entry.call(file, line, column, control, msg))
|
723
|
+
}
|
724
|
+
|
725
|
+
@logger.info "Checking profile in #{@target}"
|
726
|
+
meta_path = @source_reader.target.abs_path(@source_reader.metadata.ref)
|
727
|
+
|
728
|
+
# verify metadata
|
729
|
+
m_errors, m_warnings = metadata.valid
|
730
|
+
m_errors.each { |msg| error.call(meta_path, 0, 0, nil, msg) }
|
731
|
+
m_warnings.each { |msg| warn.call(meta_path, 0, 0, nil, msg) }
|
732
|
+
m_unsupported = metadata.unsupported
|
733
|
+
m_unsupported.each { |u| warn.call(meta_path, 0, 0, nil, "doesn't support: #{u}") }
|
734
|
+
@logger.info "Metadata OK." if m_errors.empty? && m_unsupported.empty?
|
735
|
+
|
736
|
+
# only run the vendor check if the legacy profile-path is not used as argument
|
737
|
+
if @legacy_profile_path == false
|
738
|
+
# verify that a lockfile is present if we have dependencies
|
739
|
+
unless metadata.dependencies.empty?
|
740
|
+
error.call(meta_path, 0, 0, nil, "Your profile needs to be vendored with `inspec vendor`.") unless lockfile_exists?
|
741
|
+
end
|
742
|
+
|
743
|
+
if lockfile_exists?
|
744
|
+
# verify if metadata and lockfile are out of sync
|
745
|
+
if lockfile.deps.size != metadata.dependencies.size
|
746
|
+
error.call(meta_path, 0, 0, nil, "inspec.yml and inspec.lock are out-of-sync. Please re-vendor with `inspec vendor`.")
|
747
|
+
end
|
748
|
+
|
749
|
+
# verify if metadata and lockfile have the same dependency names
|
750
|
+
metadata.dependencies.each do |dep|
|
751
|
+
# Skip if the dependency does not specify a name
|
752
|
+
next if dep[:name].nil?
|
753
|
+
|
754
|
+
# TODO: should we also verify that the soure is the same?
|
755
|
+
unless lockfile.deps.map { |x| x[:name] }.include? dep[:name]
|
756
|
+
error.call(meta_path, 0, 0, nil, "Cannot find #{dep[:name]} in lockfile. Please re-vendor with `inspec vendor`.")
|
757
|
+
end
|
758
|
+
end
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
# extract profile name
|
763
|
+
result[:summary][:profile] = metadata.params[:name]
|
764
|
+
|
765
|
+
count = params[:controls].values.length
|
766
|
+
result[:summary][:controls] = count
|
767
|
+
if count == 0
|
768
|
+
warn.call(nil, nil, nil, nil, "No controls or tests were defined.")
|
769
|
+
else
|
770
|
+
@logger.info("Found #{count} controls.")
|
771
|
+
end
|
772
|
+
|
773
|
+
# iterate over hash of groups
|
774
|
+
params[:controls].each do |id, control|
|
775
|
+
sfile = control[:source_location][:ref]
|
776
|
+
sline = control[:source_location][:line]
|
777
|
+
error.call(sfile, sline, nil, id, "Avoid controls with empty IDs") if id.nil? || id.empty?
|
778
|
+
next if id.start_with? "(generated "
|
779
|
+
|
780
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has no title") if control[:title].to_s.empty?
|
781
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has no descriptions") if control[:descriptions][:default].to_s.empty?
|
782
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has impact > 1.0") if control[:impact].to_f > 1.0
|
783
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has impact < 0.0") if control[:impact].to_f < 0.0
|
784
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? || control[:checks].empty?
|
785
|
+
end
|
786
|
+
|
787
|
+
# Running cookstyle to check for code offenses
|
788
|
+
if @check_cookstyle
|
789
|
+
cookstyle_linting_check.each do |lint_output|
|
790
|
+
data = lint_output.split(":")
|
791
|
+
msg = "#{data[-2]}:#{data[-1]}"
|
792
|
+
offense.call(data[0], data[1], data[2], nil, msg)
|
793
|
+
end
|
794
|
+
end
|
795
|
+
# profile is valid if we could not find any error & offenses
|
796
|
+
result[:summary][:valid] = result[:errors].empty? && result[:offenses].empty?
|
797
|
+
|
798
|
+
@logger.info "Control definitions OK." if result[:warnings].empty?
|
799
|
+
result
|
800
|
+
end
|
801
|
+
|
556
802
|
# Check if the profile is internally well-structured. The logger will be
|
557
803
|
# used to print information on errors and warnings which are found.
|
558
804
|
#
|
@@ -572,6 +818,9 @@ module Inspec
|
|
572
818
|
offenses: [],
|
573
819
|
}
|
574
820
|
|
821
|
+
# memoize `info_from_parse` with tests
|
822
|
+
info_from_parse(include_tests: true)
|
823
|
+
|
575
824
|
entry = lambda { |file, line, column, control, msg|
|
576
825
|
{
|
577
826
|
file: file,
|
@@ -600,7 +849,7 @@ module Inspec
|
|
600
849
|
meta_path = @source_reader.target.abs_path(@source_reader.metadata.ref)
|
601
850
|
|
602
851
|
# verify metadata
|
603
|
-
m_errors, m_warnings =
|
852
|
+
m_errors, m_warnings = validity_check
|
604
853
|
m_errors.each { |msg| error.call(meta_path, 0, 0, nil, msg) }
|
605
854
|
m_warnings.each { |msg| warn.call(meta_path, 0, 0, nil, msg) }
|
606
855
|
m_unsupported = metadata.unsupported
|
@@ -634,9 +883,9 @@ module Inspec
|
|
634
883
|
end
|
635
884
|
|
636
885
|
# extract profile name
|
637
|
-
result[:summary][:profile] =
|
886
|
+
result[:summary][:profile] = info_from_parse[:name]
|
638
887
|
|
639
|
-
count =
|
888
|
+
count = info_from_parse[:controls].count
|
640
889
|
result[:summary][:controls] = count
|
641
890
|
if count == 0
|
642
891
|
warn.call(nil, nil, nil, nil, "No controls or tests were defined.")
|
@@ -645,9 +894,10 @@ module Inspec
|
|
645
894
|
end
|
646
895
|
|
647
896
|
# iterate over hash of groups
|
648
|
-
|
897
|
+
info_from_parse[:controls].each do |control|
|
649
898
|
sfile = control[:source_location][:ref]
|
650
899
|
sline = control[:source_location][:line]
|
900
|
+
id = control[:id]
|
651
901
|
error.call(sfile, sline, nil, id, "Avoid controls with empty IDs") if id.nil? || id.empty?
|
652
902
|
next if id.start_with? "(generated "
|
653
903
|
|
@@ -673,8 +923,74 @@ module Inspec
|
|
673
923
|
result
|
674
924
|
end
|
675
925
|
|
676
|
-
def
|
677
|
-
|
926
|
+
def validity_check # rubocop:disable Metrics/AbcSize
|
927
|
+
errors = []
|
928
|
+
warnings = []
|
929
|
+
info_from_parse.merge!(metadata.params)
|
930
|
+
|
931
|
+
%w{name version}.each do |field|
|
932
|
+
next unless info_from_parse[field.to_sym].nil?
|
933
|
+
|
934
|
+
errors.push("Missing profile #{field} in #{metadata.ref}")
|
935
|
+
end
|
936
|
+
|
937
|
+
if %r{[\/\\]} =~ info_from_parse[:name]
|
938
|
+
errors.push("The profile name (#{info_from_parse[:name]}) contains a slash" \
|
939
|
+
" which is not permitted. Please remove all slashes from `inspec.yml`.")
|
940
|
+
end
|
941
|
+
|
942
|
+
# if version is set, ensure it is correct
|
943
|
+
if !info_from_parse[:version].nil? && !metadata.valid_version?(info_from_parse[:version])
|
944
|
+
errors.push("Version needs to be in SemVer format")
|
945
|
+
end
|
946
|
+
|
947
|
+
if info_from_parse[:entitlement_id] && info_from_parse[:entitlement_id].strip.empty?
|
948
|
+
errors.push("Entitlement ID should not be blank.")
|
949
|
+
end
|
950
|
+
|
951
|
+
unless metadata.supports_runtime?
|
952
|
+
warnings.push("The current inspec version #{Inspec::VERSION} cannot satisfy profile inspec_version constraint #{info_from_parse[:inspec_version]}")
|
953
|
+
end
|
954
|
+
|
955
|
+
%w{title summary maintainer copyright license}.each do |field|
|
956
|
+
next unless info_from_parse[field.to_sym].nil?
|
957
|
+
|
958
|
+
warnings.push("Missing profile #{field} in #{metadata.ref}")
|
959
|
+
end
|
960
|
+
|
961
|
+
# if license is set, ensure it is in SPDX format or marked as proprietary
|
962
|
+
if !info_from_parse[:license].nil? && !metadata.valid_license?(info_from_parse[:license])
|
963
|
+
warnings.push("License '#{info_from_parse[:license]}' needs to be in SPDX format or marked as 'Proprietary'. See https://spdx.org/licenses/.")
|
964
|
+
end
|
965
|
+
|
966
|
+
# If gem_dependencies is set, it must be an array of hashes with keys name and optional version
|
967
|
+
unless info_from_parse[:gem_dependencies].nil?
|
968
|
+
list = info_from_parse[:gem_dependencies]
|
969
|
+
if list.is_a?(Array) && list.all? { |e| e.is_a? Hash }
|
970
|
+
list.each do |entry|
|
971
|
+
errors.push("gem_dependencies entries must all have a 'name' field") unless entry.key?(:name)
|
972
|
+
if entry[:version]
|
973
|
+
orig = entry[:version]
|
974
|
+
begin
|
975
|
+
# Split on commas as we may have a complex dep
|
976
|
+
orig.split(",").map { |c| Gem::Requirement.parse(c) }
|
977
|
+
rescue Gem::Requirement::BadRequirementError
|
978
|
+
errors.push "Unparseable gem dependency '#{orig}' for #{entry[:name]}"
|
979
|
+
rescue Inspec::GemDependencyInstallError => e
|
980
|
+
errors.push e.message
|
981
|
+
end
|
982
|
+
end
|
983
|
+
extra = (entry.keys - %i{name version})
|
984
|
+
unless extra.empty?
|
985
|
+
warnings.push "Unknown gem_dependencies key(s) #{extra.join(",")} seen for entry '#{entry[:name]}'"
|
986
|
+
end
|
987
|
+
end
|
988
|
+
else
|
989
|
+
errors.push("gem_dependencies must be a List of Hashes")
|
990
|
+
end
|
991
|
+
end
|
992
|
+
|
993
|
+
[errors, warnings]
|
678
994
|
end
|
679
995
|
|
680
996
|
def set_status_message(msg)
|
@@ -698,9 +1014,11 @@ module Inspec
|
|
698
1014
|
# TODO ignore all .files, but add the files to debug output
|
699
1015
|
|
700
1016
|
# Generate temporary inspec.json for archive
|
701
|
-
|
1017
|
+
export_opt_enabled = opts[:export] || opts[:legacy_export]
|
1018
|
+
if export_opt_enabled
|
1019
|
+
info_for_profile_summary = opts[:legacy_export] ? info : info_from_parse
|
702
1020
|
Inspec::Utils::JsonProfileSummary.produce_json(
|
703
|
-
info:
|
1021
|
+
info: info_for_profile_summary,
|
704
1022
|
write_path: "#{root_path}inspec.json",
|
705
1023
|
suppress_output: true
|
706
1024
|
)
|
@@ -709,9 +1027,9 @@ module Inspec
|
|
709
1027
|
# display all files that will be part of the archive
|
710
1028
|
@logger.debug "Add the following files to archive:"
|
711
1029
|
files.each { |f| @logger.debug " " + f }
|
712
|
-
@logger.debug " inspec.json" if
|
1030
|
+
@logger.debug " inspec.json" if export_opt_enabled
|
713
1031
|
|
714
|
-
archive_files =
|
1032
|
+
archive_files = export_opt_enabled ? files.push("inspec.json") : files
|
715
1033
|
if opts[:zip]
|
716
1034
|
# generate zip archive
|
717
1035
|
require "inspec/archive/zip"
|
@@ -725,7 +1043,7 @@ module Inspec
|
|
725
1043
|
end
|
726
1044
|
|
727
1045
|
# Cleanup
|
728
|
-
FileUtils.rm_f("#{root_path}inspec.json") if
|
1046
|
+
FileUtils.rm_f("#{root_path}inspec.json") if export_opt_enabled
|
729
1047
|
|
730
1048
|
@logger.info "Finished archive generation."
|
731
1049
|
true
|
data/lib/inspec/reporters/cli.rb
CHANGED
@@ -169,9 +169,14 @@ module Inspec::Resources
|
|
169
169
|
# special handling for string values with "
|
170
170
|
elsif !(m = /^\"(.*)\"$/.match(val)).nil?
|
171
171
|
m[1]
|
172
|
+
# We get some values of Registry Path as MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Setup\\RecoveryConsole\\SecurityLevel=4,0
|
173
|
+
# which we are not going to split as there are chances that it will break if anyone is using string comparison.
|
174
|
+
# In some cases privilege value which does not have corresponding SID it returns the values in comma seprated which breakes it for some of
|
175
|
+
# the privileges like SeServiceLogonRight as it returns array if previlege values are SID
|
176
|
+
elsif !key.include?("\\") && val.match(/,/)
|
177
|
+
val.split(",")
|
172
178
|
else
|
173
|
-
|
174
|
-
key.include?("\\") ? val : val.split(",")
|
179
|
+
val
|
175
180
|
end
|
176
181
|
end
|
177
182
|
|
data/lib/inspec/rule.rb
CHANGED
@@ -375,19 +375,24 @@ module Inspec
|
|
375
375
|
# only_if mechanism)
|
376
376
|
# Double underscore: not intended to be called as part of the DSL
|
377
377
|
def __apply_waivers
|
378
|
+
@__waiver_data = nil
|
378
379
|
control_id = @__rule_id # TODO: control ID slugging
|
379
|
-
waiver_files = Inspec::Config.cached.final_options["waiver_file"] if Inspec::Config.cached.respond_to?(:final_options)
|
380
380
|
|
381
|
-
|
381
|
+
waiver_files = Inspec::Config.cached.final_options["waiver_file"] if Inspec::Config.cached.respond_to?(:final_options)
|
382
|
+
unless waiver_files.nil? || waiver_files.empty?
|
383
|
+
waiver_data_by_profile = Inspec::WaiverFileReader.fetch_waivers_by_profile(__profile_id, waiver_files)
|
384
|
+
return unless waiver_data_by_profile && waiver_data_by_profile[control_id] && waiver_data_by_profile[control_id].is_a?(Hash)
|
382
385
|
|
383
|
-
|
386
|
+
@__waiver_data = waiver_data_by_profile[control_id]
|
387
|
+
else
|
388
|
+
# Support for input registry is provided for backward compatibilty with compliance phase of chef-client
|
389
|
+
# Chef-client sends waiver information in inputs hash
|
390
|
+
input_registry = Inspec::InputRegistry.instance
|
391
|
+
waiver_data_via_input = input_registry.inputs_by_profile.dig(__profile_id, control_id)
|
392
|
+
return unless waiver_data_via_input && waiver_data_via_input.has_value? && waiver_data_via_input.value.is_a?(Hash)
|
384
393
|
|
385
|
-
|
386
|
-
|
387
|
-
# log of each "set" event so that when it is collapsed to a value,
|
388
|
-
# it can determine the correct (highest priority) value.
|
389
|
-
# Store in an instance variable for.. later reading???
|
390
|
-
@__waiver_data = waiver_data_by_profile[control_id]
|
394
|
+
@__waiver_data = waiver_data_via_input.value
|
395
|
+
end
|
391
396
|
|
392
397
|
__waiver_data["skipped_due_to_waiver"] = false
|
393
398
|
__waiver_data["message"] = ""
|
data/lib/inspec/runner.rb
CHANGED
@@ -142,7 +142,7 @@ module Inspec
|
|
142
142
|
get_check_example(m, a, b)
|
143
143
|
end.compact
|
144
144
|
|
145
|
-
examples.map { |example| total_checks += example.
|
145
|
+
examples.map { |example| total_checks += example.descendant_filtered_examples.count }
|
146
146
|
|
147
147
|
unless control_describe_checks.empty?
|
148
148
|
# controls with empty tests are avoided
|
@@ -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
|
@@ -19,7 +19,7 @@ module Waivers
|
|
19
19
|
row_hash.delete("control_id")
|
20
20
|
row_hash.delete_if { |k, v| k.nil? || v.nil? }
|
21
21
|
|
22
|
-
waiver_data_hash[control_id] = row_hash if control_id && !row_hash.
|
22
|
+
waiver_data_hash[control_id] = row_hash if control_id && !(row_hash.nil? || row_hash.empty?)
|
23
23
|
end
|
24
24
|
|
25
25
|
waiver_data_hash
|
@@ -25,7 +25,7 @@ module Waivers
|
|
25
25
|
row_hash.delete_if { |k, v| k.nil? || v.nil? }
|
26
26
|
end
|
27
27
|
|
28
|
-
waiver_data_hash[control_id] = row_hash if control_id && !row_hash.
|
28
|
+
waiver_data_hash[control_id] = row_hash if control_id && !(row_hash.nil? || row_hash.empty?)
|
29
29
|
end
|
30
30
|
waiver_data_hash
|
31
31
|
rescue Exception => e
|
data/lib/inspec/version.rb
CHANGED
@@ -168,10 +168,10 @@ module InspecPlugins
|
|
168
168
|
end
|
169
169
|
|
170
170
|
# read profile name from inspec.yml
|
171
|
-
profile_name = profile.
|
171
|
+
profile_name = profile.name
|
172
172
|
|
173
173
|
# read profile version from inspec.yml
|
174
|
-
profile_version = profile.
|
174
|
+
profile_version = profile.version
|
175
175
|
|
176
176
|
# check that the profile is not uploaded already,
|
177
177
|
# confirm upload to the user (overwrite with --force)
|
@@ -1,4 +1,5 @@
|
|
1
|
-
<%
|
1
|
+
<% slugged_control_id = control.id.tr(" ", "_") %>
|
2
|
+
<% slugged_profile_id = profile.name.gsub(/\W/, "_") %>
|
2
3
|
<%
|
3
4
|
if enhanced_outcomes
|
4
5
|
status = control.status
|
@@ -13,7 +14,7 @@
|
|
13
14
|
end
|
14
15
|
%>
|
15
16
|
|
16
|
-
<div class="control control-status-<%= status %>" id="control-<%=
|
17
|
+
<div class="control control-status-<%= status %>" id="profile-<%= slugged_profile_id %>-control-<%= slugged_control_id %>">
|
17
18
|
|
18
19
|
<%
|
19
20
|
# Determine range of impact
|
@@ -29,7 +30,7 @@
|
|
29
30
|
%>
|
30
31
|
|
31
32
|
<h3 class="control-title">Control <code><%= control.id %></code></h3>
|
32
|
-
<table class="control-metadata info" id="control-metadata-<%=
|
33
|
+
<table class="control-metadata info" id="profile-<%= slugged_profile_id %>-control-metadata-<%= slugged_control_id %>">
|
33
34
|
<caption>Control Table</caption>
|
34
35
|
<tr class="status status-<%= status %>"><th>Status:</th><td><div><%= status.capitalize %></div></td></tr>
|
35
36
|
<% if control.title %><tr class="title"><th>Title:</th><td><%= control.title %></td></tr> <% end %>
|
@@ -64,9 +65,9 @@
|
|
64
65
|
<tr class="code">
|
65
66
|
<th>Source Code:</th>
|
66
67
|
<td>
|
67
|
-
<input type="button" class="show-source-code" id="show-code-<%=
|
68
|
-
<input type="button" class="hide-source-code hidden" id="hide-code-<%=
|
69
|
-
<pre class="source-code hidden" id="source-code-<%=
|
68
|
+
<input type="button" class="show-source-code" id="show-code-<%= slugged_profile_id %>-<%= slugged_control_id %>" value="Show Source"/>
|
69
|
+
<input type="button" class="hide-source-code hidden" id="hide-code-<%= slugged_profile_id %>-<%= slugged_control_id %>" value="Hide Source"/>
|
70
|
+
<pre class="source-code hidden" id="source-code-<%= slugged_profile_id %>-<%= slugged_control_id %>">
|
70
71
|
<code>
|
71
72
|
<%= control.code %>
|
72
73
|
</code>
|
@@ -11,17 +11,17 @@ function removeCssClass(id, cls) {
|
|
11
11
|
}
|
12
12
|
|
13
13
|
function handleShowSource(evt) {
|
14
|
-
var
|
14
|
+
var slugged_id = evt.srcElement.id.replace("show-code-", "")
|
15
15
|
addCssClass(evt.srcElement.id, "hidden")
|
16
|
-
removeCssClass("hide-code-" +
|
17
|
-
removeCssClass("source-code-" +
|
16
|
+
removeCssClass("hide-code-" + slugged_id, "hidden")
|
17
|
+
removeCssClass("source-code-" + slugged_id, "hidden")
|
18
18
|
}
|
19
19
|
|
20
20
|
function handleHideSource(evt) {
|
21
|
-
var
|
21
|
+
var slugged_id = evt.srcElement.id.replace("hide-code-", "")
|
22
22
|
addCssClass(evt.srcElement.id, "hidden")
|
23
|
-
addCssClass("source-code-" +
|
24
|
-
removeCssClass("show-code-" +
|
23
|
+
addCssClass("source-code-" + slugged_id, "hidden")
|
24
|
+
removeCssClass("show-code-" + slugged_id, "hidden")
|
25
25
|
}
|
26
26
|
|
27
27
|
function handleSelectorChange(evt) {
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: inspec-core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.22.
|
4
|
+
version: 5.22.40
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chef InSpec Team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chef-telemetry
|
@@ -716,6 +716,7 @@ files:
|
|
716
716
|
- lib/inspec/utils/parser.rb
|
717
717
|
- lib/inspec/utils/pkey_reader.rb
|
718
718
|
- lib/inspec/utils/podman.rb
|
719
|
+
- lib/inspec/utils/profile_ast_helpers.rb
|
719
720
|
- lib/inspec/utils/run_data_filters.rb
|
720
721
|
- lib/inspec/utils/simpleconfig.rb
|
721
722
|
- lib/inspec/utils/spdx.rb
|