inspec-core 5.22.29 → 5.22.36
Sign up to get free protection for your applications and to get access to all the features.
- 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/profile.rb +329 -11
- data/lib/inspec/resources/security_policy.rb +7 -2
- data/lib/inspec/utils/profile_ast_helpers.rb +372 -0
- 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: bbe23835cae58303aa3ac665ecd59e269752bbe4f6448e33db3219df0f04dbc8
|
4
|
+
data.tar.gz: 9a6d1e0e7758054f4d2746355a0c5538ede8af58a4220b3160d999f5e3093f34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c074f69651db5a586e4fee3b1a621ef9cc43d05634136dae9e0008a22c2d74d0515fe2938f83df42aace4ad089bbab3e95cf3d70bf3825b88aaca5e8e0696518
|
7
|
+
data.tar.gz: f01196343c6f7109cb44ced5f3a23a2b3f7b543f88a48d34d0b49c2b2a47dae31c599af4f51febca1afecbc1355f471c543c2ac8484c5c1a669ea4fec41cfe45
|
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"] })
|
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
|
@@ -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
|
@@ -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
|
|
@@ -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
@@ -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.36
|
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: 2023-
|
11
|
+
date: 2023-11-14 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
|