inspec-core 5.22.29 → 6.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Chef-EULA +9 -0
- data/Gemfile +10 -1
- data/etc/features.sig +6 -0
- data/etc/features.yaml +94 -0
- data/inspec-core.gemspec +14 -5
- data/lib/inspec/backend.rb +2 -0
- data/lib/inspec/base_cli.rb +80 -4
- data/lib/inspec/cached_fetcher.rb +24 -3
- data/lib/inspec/cli.rb +300 -230
- data/lib/inspec/config.rb +24 -2
- data/lib/inspec/dependencies/cache.rb +33 -0
- data/lib/inspec/enhanced_outcomes.rb +1 -0
- data/lib/inspec/errors.rb +5 -0
- data/lib/inspec/exceptions.rb +2 -0
- data/lib/inspec/feature/config.rb +75 -0
- data/lib/inspec/feature/runner.rb +26 -0
- data/lib/inspec/feature.rb +34 -0
- data/lib/inspec/fetcher/git.rb +5 -0
- data/lib/inspec/globals.rb +6 -0
- data/lib/inspec/plugin/v1/plugin_types/fetcher.rb +7 -0
- data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +30 -2
- data/lib/inspec/profile.rb +373 -12
- data/lib/inspec/reporters/cli.rb +1 -1
- data/lib/inspec/reporters.rb +67 -54
- data/lib/inspec/resources/security_policy.rb +7 -2
- data/lib/inspec/run_data.rb +7 -5
- data/lib/inspec/runner.rb +34 -5
- data/lib/inspec/runner_rspec.rb +12 -9
- data/lib/inspec/secrets/yaml.rb +9 -3
- data/lib/inspec/shell.rb +10 -0
- data/lib/inspec/ui.rb +4 -0
- data/lib/inspec/utils/licensing_config.rb +9 -0
- data/lib/inspec/utils/profile_ast_helpers.rb +372 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/inspec/waiver_file_reader.rb +68 -27
- data/lib/inspec.rb +2 -1
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +189 -168
- data/lib/plugins/inspec-habitat/lib/inspec-habitat/cli.rb +10 -3
- data/lib/plugins/inspec-init/lib/inspec-init/cli.rb +1 -0
- data/lib/plugins/inspec-init/lib/inspec-init/cli_plugin.rb +23 -21
- data/lib/plugins/inspec-init/lib/inspec-init/cli_profile.rb +15 -13
- data/lib/plugins/inspec-init/lib/inspec-init/cli_resource.rb +15 -13
- data/lib/plugins/inspec-license/README.md +16 -0
- data/lib/plugins/inspec-license/inspec-license.gemspec +6 -0
- data/lib/plugins/inspec-license/lib/inspec-license/cli.rb +26 -0
- data/lib/plugins/inspec-license/lib/inspec-license.rb +14 -0
- data/lib/plugins/inspec-parallel/README.md +27 -0
- data/lib/plugins/inspec-parallel/inspec-parallel.gemspec +6 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/child_status_reporter.rb +61 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/cli.rb +39 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/command.rb +219 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/runner.rb +265 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/base.rb +24 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/silent.rb +7 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/status.rb +124 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/text.rb +23 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/validator.rb +170 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel.rb +18 -0
- data/lib/plugins/inspec-reporter-html2/templates/control.html.erb +7 -6
- data/lib/plugins/inspec-reporter-html2/templates/default.js +6 -6
- data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +6 -2
- data/lib/plugins/inspec-sign/lib/inspec-sign/cli.rb +11 -4
- data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +6 -13
- metadata +54 -13
data/lib/inspec/profile.rb
CHANGED
@@ -15,6 +15,8 @@ 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"
|
19
|
+
require "plugins/inspec-sign/lib/inspec-sign/base"
|
18
20
|
|
19
21
|
module Inspec
|
20
22
|
class Profile
|
@@ -81,7 +83,7 @@ module Inspec
|
|
81
83
|
end
|
82
84
|
|
83
85
|
attr_reader :source_reader, :backend, :runner_context, :check_mode
|
84
|
-
attr_accessor :parent_profile, :profile_id, :profile_name
|
86
|
+
attr_accessor :parent_profile, :profile_id, :profile_name, :target
|
85
87
|
def_delegator :@source_reader, :tests
|
86
88
|
def_delegator :@source_reader, :libraries
|
87
89
|
def_delegator :@source_reader, :metadata
|
@@ -180,6 +182,26 @@ module Inspec
|
|
180
182
|
@state == :failed
|
181
183
|
end
|
182
184
|
|
185
|
+
def verify_if_signed
|
186
|
+
if signed?
|
187
|
+
verify_signed_profile
|
188
|
+
true
|
189
|
+
else
|
190
|
+
false
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def signed?
|
195
|
+
# Signed profiles have .iaf extension
|
196
|
+
(@source_reader&.target&.parent&.class == Inspec::IafProvider)
|
197
|
+
end
|
198
|
+
|
199
|
+
def verify_signed_profile
|
200
|
+
# Kitchen inspec send target profile in Hash format in some scenarios. For example: While using local profile with kitchen, {:path => "path/to/kitchen/lcoal-profile"}
|
201
|
+
target_profile = target.is_a?(Hash) ? target.values[0] : target
|
202
|
+
InspecPlugins::Sign::Base.profile_verify(target_profile, true) if target_profile
|
203
|
+
end
|
204
|
+
|
183
205
|
#
|
184
206
|
# Is this profile is supported on the current platform of the
|
185
207
|
# backend machine and the current inspec version.
|
@@ -214,8 +236,30 @@ module Inspec
|
|
214
236
|
@params ||= load_params
|
215
237
|
end
|
216
238
|
|
239
|
+
def virtual_profile?
|
240
|
+
# A virtual profile is for virtual profile evaluation
|
241
|
+
# Used by shell & inspec detect command.
|
242
|
+
(name == "inspec-shell") && (files&.length == 1) && (files[0] == "inspec.yml")
|
243
|
+
end
|
244
|
+
|
217
245
|
def collect_tests
|
218
246
|
unless @tests_collected || failed?
|
247
|
+
|
248
|
+
# This is that one common place in InSpec engine which is used to collect tests of InSpec profile
|
249
|
+
# One common place used by most of the CLI commands using profile, like exec, export etc
|
250
|
+
# Checking for profile signature in parent profile only
|
251
|
+
# Child profiles of a signed profile are extracted to cache dir
|
252
|
+
# Hence they are not in .iaf format
|
253
|
+
# Only runs this block when preview flag CHEF_PREVIEW_MANDATORY_PROFILE_SIGNING is set
|
254
|
+
Inspec.with_feature("inspec-mandatory-profile-signing") {
|
255
|
+
if !parent_profile && !virtual_profile?
|
256
|
+
cfg = Inspec::Config.cached
|
257
|
+
if cfg.is_a?(Inspec::Config) && !cfg.allow_unsigned_profiles?
|
258
|
+
raise Inspec::ProfileSignatureRequired, "Signature required for profile: #{name}. Please provide a signed profile. Or set CHEF_ALLOW_UNSIGNED_PROFILES in the environment. Or use `--allow-unsigned-profiles` flag with InSpec CLI." unless verify_if_signed
|
259
|
+
end
|
260
|
+
end
|
261
|
+
}
|
262
|
+
|
219
263
|
return unless supports_platform?
|
220
264
|
|
221
265
|
locked_dependencies.each(&:collect_tests)
|
@@ -514,6 +558,135 @@ module Inspec
|
|
514
558
|
res
|
515
559
|
end
|
516
560
|
|
561
|
+
# Return data like profile.info(params), but try to do so without evaluating the profile.
|
562
|
+
def info_from_parse(include_tests: false)
|
563
|
+
return @info_from_parse unless @info_from_parse.nil?
|
564
|
+
|
565
|
+
@info_from_parse = {
|
566
|
+
controls: [],
|
567
|
+
groups: [],
|
568
|
+
}
|
569
|
+
|
570
|
+
# TODO - look at the various source contents
|
571
|
+
# PASS 1: parse them using rubocop-ast
|
572
|
+
# Look for controls, top-level metadata, and inputs
|
573
|
+
# PASS 2: Using the control IDs, deterimine the extents -
|
574
|
+
# line locations - of the coontrol IDs in each file, and
|
575
|
+
# then extract each source code block. Use this to populate the source code
|
576
|
+
# locations and 'code' properties.
|
577
|
+
|
578
|
+
# TODO: Verify that it doesn't do evaluation (ideally shouldn't because it is reading simply yaml file)
|
579
|
+
@info_from_parse = @info_from_parse.merge(metadata.params)
|
580
|
+
|
581
|
+
inputs_hash = {}
|
582
|
+
# Note: This only handles the case when inputs are defined in metadata file
|
583
|
+
if @profile_id.nil?
|
584
|
+
# identifying inputs using profile name
|
585
|
+
inputs_hash = Inspec::InputRegistry.list_inputs_for_profile(@info_from_parse[:name])
|
586
|
+
else
|
587
|
+
inputs_hash = Inspec::InputRegistry.list_inputs_for_profile(@profile_id)
|
588
|
+
end
|
589
|
+
|
590
|
+
# TODO: Verify if I need to do the below conversion for inputs to array
|
591
|
+
if inputs_hash.nil? || inputs_hash.empty?
|
592
|
+
# convert to array for backwards compatability
|
593
|
+
@info_from_parse[:inputs] = []
|
594
|
+
else
|
595
|
+
@info_from_parse[:inputs] = inputs_hash.values.map(&:to_hash)
|
596
|
+
end
|
597
|
+
|
598
|
+
@info_from_parse[:sha256] = sha256
|
599
|
+
|
600
|
+
# Populate :status and :status_message
|
601
|
+
if supports_platform?
|
602
|
+
@info_from_parse[:status_message] = @status_message || ""
|
603
|
+
@info_from_parse[:status] = failed? ? "failed" : "loaded"
|
604
|
+
else
|
605
|
+
@info_from_parse[:status] = "skipped"
|
606
|
+
msg = "Skipping profile: '#{name}' on unsupported platform: '#{backend.platform.name}/#{backend.platform.release}'."
|
607
|
+
@info_from_parse[:status_message] = msg
|
608
|
+
end
|
609
|
+
|
610
|
+
# @source_reader.tests contains a hash mapping control filenames to control file contents
|
611
|
+
@source_reader.tests.each do |control_filename, control_file_source|
|
612
|
+
# Parse the source code
|
613
|
+
src = RuboCop::AST::ProcessedSource.new(control_file_source, RUBY_VERSION.to_f)
|
614
|
+
source_location_ref = @source_reader.target.abs_path(control_filename)
|
615
|
+
|
616
|
+
input_collector = Inspec::Profile::AstHelper::InputCollectorOutsideControlBlock.new(@info_from_parse)
|
617
|
+
ctl_id_collector = Inspec::Profile::AstHelper::ControlIDCollector.new(@info_from_parse, source_location_ref,
|
618
|
+
include_tests: include_tests)
|
619
|
+
|
620
|
+
# Collect all metadata defined in the control block and inputs defined inside the control block
|
621
|
+
src.ast.each_node { |n|
|
622
|
+
ctl_id_collector.process(n)
|
623
|
+
input_collector.process(n)
|
624
|
+
}
|
625
|
+
|
626
|
+
# For each control ID
|
627
|
+
# Look for per-control metadata
|
628
|
+
# Filter controls by --controls, list of controls to include is available in include_controls_list
|
629
|
+
|
630
|
+
# NOTE: This is a hack to duplicate refs.
|
631
|
+
# TODO: Fix this in the ref collector or the way we traverse the AST
|
632
|
+
@info_from_parse[:controls].each { |control| control[:refs].uniq! }
|
633
|
+
|
634
|
+
@info_from_parse[:controls] = filter_controls_by_id_and_tags(@info_from_parse[:controls])
|
635
|
+
|
636
|
+
# Update groups after filtering controls to handle --controls option
|
637
|
+
update_groups_from(control_filename, src)
|
638
|
+
|
639
|
+
# NOTE: This is a hack to duplicate inputs.
|
640
|
+
# TODO: Fix this in the input collector or the way we traverse the AST
|
641
|
+
@info_from_parse[:inputs] = @info_from_parse[:inputs].uniq
|
642
|
+
end
|
643
|
+
@info_from_parse
|
644
|
+
end
|
645
|
+
|
646
|
+
def filter_controls_by_id_and_tags(controls)
|
647
|
+
controls.select do |control|
|
648
|
+
tag_ids = get_all_tags_list(control[:tags])
|
649
|
+
(include_controls_list.empty? || include_controls_list.any? { |control_id| control_id.match?(control[:id]) }) &&
|
650
|
+
(include_tags_list.empty? || include_tags_list.any? { |tag_id| tag_ids.any? { |tag| tag_id.match?(tag) } })
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
def get_all_tags_list(control_tags)
|
655
|
+
all_tags = []
|
656
|
+
control_tags.each do |tags|
|
657
|
+
all_tags.push(tags)
|
658
|
+
end
|
659
|
+
all_tags.flatten.compact.uniq.map(&:to_s)
|
660
|
+
rescue
|
661
|
+
[]
|
662
|
+
end
|
663
|
+
|
664
|
+
def include_group_data?(group_data)
|
665
|
+
unless include_controls_list.empty?
|
666
|
+
# {:id=>"controls/example-tmp.rb", :title=>"/ profile", :controls=>["tmp-1.0"]}
|
667
|
+
# Check if the group should be included based on the controls it contains
|
668
|
+
group_data[:controls].any? do |control_id|
|
669
|
+
include_controls_list.any? { |id| id.match?(control_id) }
|
670
|
+
end
|
671
|
+
else
|
672
|
+
true
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
def update_groups_from(control_filename, src)
|
677
|
+
group_data = {
|
678
|
+
id: control_filename,
|
679
|
+
title: nil,
|
680
|
+
}
|
681
|
+
source_location_ref = @source_reader.target.abs_path(control_filename)
|
682
|
+
Inspec::Profile::AstHelper::TitleCollector.new(group_data)
|
683
|
+
.process(src.ast.child_nodes.first) # Picking the title defined for the whole controls file
|
684
|
+
group_controls = @info_from_parse[:controls].select { |control| control[:source_location][:ref] == source_location_ref }
|
685
|
+
group_data[:controls] = group_controls.map { |control| control[:id] }
|
686
|
+
|
687
|
+
@info_from_parse[:groups].push(group_data) if include_group_data?(group_data)
|
688
|
+
end
|
689
|
+
|
517
690
|
def cookstyle_linting_check
|
518
691
|
msgs = []
|
519
692
|
return msgs if Inspec.locally_windows? # See #5723
|
@@ -553,6 +726,122 @@ module Inspec
|
|
553
726
|
end
|
554
727
|
end
|
555
728
|
|
729
|
+
def legacy_check # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
730
|
+
# initial values for response object
|
731
|
+
result = {
|
732
|
+
summary: {
|
733
|
+
valid: false,
|
734
|
+
timestamp: Time.now.iso8601,
|
735
|
+
location: @target,
|
736
|
+
profile: nil,
|
737
|
+
controls: 0,
|
738
|
+
},
|
739
|
+
errors: [],
|
740
|
+
warnings: [],
|
741
|
+
offenses: [],
|
742
|
+
}
|
743
|
+
|
744
|
+
entry = lambda { |file, line, column, control, msg|
|
745
|
+
{
|
746
|
+
file: file,
|
747
|
+
line: line,
|
748
|
+
column: column,
|
749
|
+
control_id: control,
|
750
|
+
msg: msg,
|
751
|
+
}
|
752
|
+
}
|
753
|
+
|
754
|
+
warn = lambda { |file, line, column, control, msg|
|
755
|
+
@logger.warn(msg)
|
756
|
+
result[:warnings].push(entry.call(file, line, column, control, msg))
|
757
|
+
}
|
758
|
+
|
759
|
+
error = lambda { |file, line, column, control, msg|
|
760
|
+
@logger.error(msg)
|
761
|
+
result[:errors].push(entry.call(file, line, column, control, msg))
|
762
|
+
}
|
763
|
+
|
764
|
+
offense = lambda { |file, line, column, control, msg|
|
765
|
+
result[:offenses].push(entry.call(file, line, column, control, msg))
|
766
|
+
}
|
767
|
+
|
768
|
+
@logger.info "Checking profile in #{@target}"
|
769
|
+
meta_path = @source_reader.target.abs_path(@source_reader.metadata.ref)
|
770
|
+
|
771
|
+
# verify metadata
|
772
|
+
m_errors, m_warnings = metadata.valid
|
773
|
+
m_errors.each { |msg| error.call(meta_path, 0, 0, nil, msg) }
|
774
|
+
m_warnings.each { |msg| warn.call(meta_path, 0, 0, nil, msg) }
|
775
|
+
m_unsupported = metadata.unsupported
|
776
|
+
m_unsupported.each { |u| warn.call(meta_path, 0, 0, nil, "doesn't support: #{u}") }
|
777
|
+
@logger.info "Metadata OK." if m_errors.empty? && m_unsupported.empty?
|
778
|
+
|
779
|
+
# only run the vendor check if the legacy profile-path is not used as argument
|
780
|
+
if @legacy_profile_path == false
|
781
|
+
# verify that a lockfile is present if we have dependencies
|
782
|
+
unless metadata.dependencies.empty?
|
783
|
+
error.call(meta_path, 0, 0, nil, "Your profile needs to be vendored with `inspec vendor`.") unless lockfile_exists?
|
784
|
+
end
|
785
|
+
|
786
|
+
if lockfile_exists?
|
787
|
+
# verify if metadata and lockfile are out of sync
|
788
|
+
if lockfile.deps.size != metadata.dependencies.size
|
789
|
+
error.call(meta_path, 0, 0, nil, "inspec.yml and inspec.lock are out-of-sync. Please re-vendor with `inspec vendor`.")
|
790
|
+
end
|
791
|
+
|
792
|
+
# verify if metadata and lockfile have the same dependency names
|
793
|
+
metadata.dependencies.each do |dep|
|
794
|
+
# Skip if the dependency does not specify a name
|
795
|
+
next if dep[:name].nil?
|
796
|
+
|
797
|
+
# TODO: should we also verify that the soure is the same?
|
798
|
+
unless lockfile.deps.map { |x| x[:name] }.include? dep[:name]
|
799
|
+
error.call(meta_path, 0, 0, nil, "Cannot find #{dep[:name]} in lockfile. Please re-vendor with `inspec vendor`.")
|
800
|
+
end
|
801
|
+
end
|
802
|
+
end
|
803
|
+
end
|
804
|
+
|
805
|
+
# extract profile name
|
806
|
+
result[:summary][:profile] = metadata.params[:name]
|
807
|
+
|
808
|
+
count = params[:controls].values.length
|
809
|
+
result[:summary][:controls] = count
|
810
|
+
if count == 0
|
811
|
+
warn.call(nil, nil, nil, nil, "No controls or tests were defined.")
|
812
|
+
else
|
813
|
+
@logger.info("Found #{count} controls.")
|
814
|
+
end
|
815
|
+
|
816
|
+
# iterate over hash of groups
|
817
|
+
params[:controls].each do |id, control|
|
818
|
+
sfile = control[:source_location][:ref]
|
819
|
+
sline = control[:source_location][:line]
|
820
|
+
error.call(sfile, sline, nil, id, "Avoid controls with empty IDs") if id.nil? || id.empty?
|
821
|
+
next if id.start_with? "(generated "
|
822
|
+
|
823
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has no title") if control[:title].to_s.empty?
|
824
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has no descriptions") if control[:descriptions][:default].to_s.empty?
|
825
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has impact > 1.0") if control[:impact].to_f > 1.0
|
826
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has impact < 0.0") if control[:impact].to_f < 0.0
|
827
|
+
warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? || control[:checks].empty?
|
828
|
+
end
|
829
|
+
|
830
|
+
# Running cookstyle to check for code offenses
|
831
|
+
if @check_cookstyle
|
832
|
+
cookstyle_linting_check.each do |lint_output|
|
833
|
+
data = lint_output.split(":")
|
834
|
+
msg = "#{data[-2]}:#{data[-1]}"
|
835
|
+
offense.call(data[0], data[1], data[2], nil, msg)
|
836
|
+
end
|
837
|
+
end
|
838
|
+
# profile is valid if we could not find any error & offenses
|
839
|
+
result[:summary][:valid] = result[:errors].empty? && result[:offenses].empty?
|
840
|
+
|
841
|
+
@logger.info "Control definitions OK." if result[:warnings].empty?
|
842
|
+
result
|
843
|
+
end
|
844
|
+
|
556
845
|
# Check if the profile is internally well-structured. The logger will be
|
557
846
|
# used to print information on errors and warnings which are found.
|
558
847
|
#
|
@@ -572,6 +861,9 @@ module Inspec
|
|
572
861
|
offenses: [],
|
573
862
|
}
|
574
863
|
|
864
|
+
# memoize `info_from_parse` with tests
|
865
|
+
info_from_parse(include_tests: true)
|
866
|
+
|
575
867
|
entry = lambda { |file, line, column, control, msg|
|
576
868
|
{
|
577
869
|
file: file,
|
@@ -600,7 +892,7 @@ module Inspec
|
|
600
892
|
meta_path = @source_reader.target.abs_path(@source_reader.metadata.ref)
|
601
893
|
|
602
894
|
# verify metadata
|
603
|
-
m_errors, m_warnings =
|
895
|
+
m_errors, m_warnings = validity_check
|
604
896
|
m_errors.each { |msg| error.call(meta_path, 0, 0, nil, msg) }
|
605
897
|
m_warnings.each { |msg| warn.call(meta_path, 0, 0, nil, msg) }
|
606
898
|
m_unsupported = metadata.unsupported
|
@@ -634,9 +926,9 @@ module Inspec
|
|
634
926
|
end
|
635
927
|
|
636
928
|
# extract profile name
|
637
|
-
result[:summary][:profile] =
|
929
|
+
result[:summary][:profile] = info_from_parse[:name]
|
638
930
|
|
639
|
-
count =
|
931
|
+
count = info_from_parse[:controls].count
|
640
932
|
result[:summary][:controls] = count
|
641
933
|
if count == 0
|
642
934
|
warn.call(nil, nil, nil, nil, "No controls or tests were defined.")
|
@@ -645,9 +937,10 @@ module Inspec
|
|
645
937
|
end
|
646
938
|
|
647
939
|
# iterate over hash of groups
|
648
|
-
|
940
|
+
info_from_parse[:controls].each do |control|
|
649
941
|
sfile = control[:source_location][:ref]
|
650
942
|
sline = control[:source_location][:line]
|
943
|
+
id = control[:id]
|
651
944
|
error.call(sfile, sline, nil, id, "Avoid controls with empty IDs") if id.nil? || id.empty?
|
652
945
|
next if id.start_with? "(generated "
|
653
946
|
|
@@ -673,8 +966,74 @@ module Inspec
|
|
673
966
|
result
|
674
967
|
end
|
675
968
|
|
676
|
-
def
|
677
|
-
|
969
|
+
def validity_check # rubocop:disable Metrics/AbcSize
|
970
|
+
errors = []
|
971
|
+
warnings = []
|
972
|
+
info_from_parse.merge!(metadata.params)
|
973
|
+
|
974
|
+
%w{name version}.each do |field|
|
975
|
+
next unless info_from_parse[field.to_sym].nil?
|
976
|
+
|
977
|
+
errors.push("Missing profile #{field} in #{metadata.ref}")
|
978
|
+
end
|
979
|
+
|
980
|
+
if %r{[\/\\]} =~ info_from_parse[:name]
|
981
|
+
errors.push("The profile name (#{info_from_parse[:name]}) contains a slash" \
|
982
|
+
" which is not permitted. Please remove all slashes from `inspec.yml`.")
|
983
|
+
end
|
984
|
+
|
985
|
+
# if version is set, ensure it is correct
|
986
|
+
if !info_from_parse[:version].nil? && !metadata.valid_version?(info_from_parse[:version])
|
987
|
+
errors.push("Version needs to be in SemVer format")
|
988
|
+
end
|
989
|
+
|
990
|
+
if info_from_parse[:entitlement_id] && info_from_parse[:entitlement_id].strip.empty?
|
991
|
+
errors.push("Entitlement ID should not be blank.")
|
992
|
+
end
|
993
|
+
|
994
|
+
unless metadata.supports_runtime?
|
995
|
+
warnings.push("The current inspec version #{Inspec::VERSION} cannot satisfy profile inspec_version constraint #{info_from_parse[:inspec_version]}")
|
996
|
+
end
|
997
|
+
|
998
|
+
%w{title summary maintainer copyright license}.each do |field|
|
999
|
+
next unless info_from_parse[field.to_sym].nil?
|
1000
|
+
|
1001
|
+
warnings.push("Missing profile #{field} in #{metadata.ref}")
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
# if license is set, ensure it is in SPDX format or marked as proprietary
|
1005
|
+
if !info_from_parse[:license].nil? && !metadata.valid_license?(info_from_parse[:license])
|
1006
|
+
warnings.push("License '#{info_from_parse[:license]}' needs to be in SPDX format or marked as 'Proprietary'. See https://spdx.org/licenses/.")
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
# If gem_dependencies is set, it must be an array of hashes with keys name and optional version
|
1010
|
+
unless info_from_parse[:gem_dependencies].nil?
|
1011
|
+
list = info_from_parse[:gem_dependencies]
|
1012
|
+
if list.is_a?(Array) && list.all? { |e| e.is_a? Hash }
|
1013
|
+
list.each do |entry|
|
1014
|
+
errors.push("gem_dependencies entries must all have a 'name' field") unless entry.key?(:name)
|
1015
|
+
if entry[:version]
|
1016
|
+
orig = entry[:version]
|
1017
|
+
begin
|
1018
|
+
# Split on commas as we may have a complex dep
|
1019
|
+
orig.split(",").map { |c| Gem::Requirement.parse(c) }
|
1020
|
+
rescue Gem::Requirement::BadRequirementError
|
1021
|
+
errors.push "Unparseable gem dependency '#{orig}' for #{entry[:name]}"
|
1022
|
+
rescue Inspec::GemDependencyInstallError => e
|
1023
|
+
errors.push e.message
|
1024
|
+
end
|
1025
|
+
end
|
1026
|
+
extra = (entry.keys - %i{name version})
|
1027
|
+
unless extra.empty?
|
1028
|
+
warnings.push "Unknown gem_dependencies key(s) #{extra.join(",")} seen for entry '#{entry[:name]}'"
|
1029
|
+
end
|
1030
|
+
end
|
1031
|
+
else
|
1032
|
+
errors.push("gem_dependencies must be a List of Hashes")
|
1033
|
+
end
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
[errors, warnings]
|
678
1037
|
end
|
679
1038
|
|
680
1039
|
def set_status_message(msg)
|
@@ -698,9 +1057,11 @@ module Inspec
|
|
698
1057
|
# TODO ignore all .files, but add the files to debug output
|
699
1058
|
|
700
1059
|
# Generate temporary inspec.json for archive
|
701
|
-
|
1060
|
+
export_opt_enabled = opts[:export] || opts[:legacy_export]
|
1061
|
+
if export_opt_enabled
|
1062
|
+
info_for_profile_summary = opts[:legacy_export] ? info : info_from_parse
|
702
1063
|
Inspec::Utils::JsonProfileSummary.produce_json(
|
703
|
-
info:
|
1064
|
+
info: info_for_profile_summary,
|
704
1065
|
write_path: "#{root_path}inspec.json",
|
705
1066
|
suppress_output: true
|
706
1067
|
)
|
@@ -709,9 +1070,9 @@ module Inspec
|
|
709
1070
|
# display all files that will be part of the archive
|
710
1071
|
@logger.debug "Add the following files to archive:"
|
711
1072
|
files.each { |f| @logger.debug " " + f }
|
712
|
-
@logger.debug " inspec.json" if
|
1073
|
+
@logger.debug " inspec.json" if export_opt_enabled
|
713
1074
|
|
714
|
-
archive_files =
|
1075
|
+
archive_files = export_opt_enabled ? files.push("inspec.json") : files
|
715
1076
|
if opts[:zip]
|
716
1077
|
# generate zip archive
|
717
1078
|
require "inspec/archive/zip"
|
@@ -725,7 +1086,7 @@ module Inspec
|
|
725
1086
|
end
|
726
1087
|
|
727
1088
|
# Cleanup
|
728
|
-
FileUtils.rm_f("#{root_path}inspec.json") if
|
1089
|
+
FileUtils.rm_f("#{root_path}inspec.json") if export_opt_enabled
|
729
1090
|
|
730
1091
|
@logger.info "Finished archive generation."
|
731
1092
|
true
|
data/lib/inspec/reporters/cli.rb
CHANGED
data/lib/inspec/reporters.rb
CHANGED
@@ -4,76 +4,89 @@ require "inspec/reporters/json"
|
|
4
4
|
require "inspec/reporters/json_automate"
|
5
5
|
require "inspec/reporters/automate"
|
6
6
|
require "inspec/reporters/yaml"
|
7
|
+
require "inspec/feature"
|
7
8
|
|
8
9
|
module Inspec::Reporters
|
9
10
|
# rubocop:disable Metrics/CyclomaticComplexity
|
10
11
|
def self.render(reporter, run_data, enhanced_outcomes = false)
|
11
12
|
name, config = reporter.dup
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
13
|
+
Inspec.with_feature("inspec-reporter-#{name}") {
|
14
|
+
config[:run_data] = run_data
|
15
|
+
case name
|
16
|
+
when "cli"
|
17
|
+
reporter = Inspec::Reporters::CLI.new(config)
|
18
|
+
when "json"
|
19
|
+
reporter = Inspec::Reporters::Json.new(config)
|
20
|
+
# This reporter is only used for Chef internal. We reserve the
|
21
|
+
# right to introduce breaking changes to this reporter at any time.
|
22
|
+
when "json-automate"
|
23
|
+
reporter = Inspec::Reporters::JsonAutomate.new(config)
|
24
|
+
when "automate"
|
25
|
+
reporter = Inspec::Reporters::Automate.new(config)
|
26
|
+
when "yaml"
|
27
|
+
reporter = Inspec::Reporters::Yaml.new(config)
|
28
|
+
else
|
29
|
+
# If we made it here, it must be a plugin, and we know it exists (because we validated it in config.rb)
|
30
|
+
activator = Inspec::Plugin::V2::Registry.instance.find_activator(plugin_type: :reporter, activator_name: name.to_sym)
|
31
|
+
activator.activate!
|
32
|
+
reporter = activator.implementation_class.new(config)
|
33
|
+
end
|
33
34
|
|
34
|
-
|
35
|
-
|
35
|
+
if enhanced_outcomes
|
36
|
+
Inspec.with_feature("inspec-enhanced-outcomes") {
|
37
|
+
reporter.enhanced_outcomes = enhanced_outcomes
|
38
|
+
}
|
39
|
+
else
|
40
|
+
reporter.enhanced_outcomes = enhanced_outcomes
|
41
|
+
end
|
36
42
|
|
37
|
-
|
38
|
-
|
43
|
+
# optional send_report method on reporter
|
44
|
+
return reporter.send_report if defined?(reporter.send_report)
|
39
45
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
reporter.render
|
47
|
+
output = reporter.rendered_output
|
48
|
+
config_file = config["file"]
|
49
|
+
if config_file
|
50
|
+
config_file.gsub!("CHILD_PID", Process.pid.to_s)
|
51
|
+
# create destination directory if it does not exist
|
52
|
+
dirname = File.dirname(config_file)
|
53
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
44
54
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
55
|
+
File.write(config_file, output)
|
56
|
+
elsif config["stdout"] == true
|
57
|
+
print output
|
58
|
+
$stdout.flush
|
59
|
+
end
|
60
|
+
}
|
50
61
|
end
|
51
62
|
|
52
63
|
def self.report(reporter, run_data)
|
53
64
|
name, config = reporter.dup
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
65
|
+
Inspec.with_feature("inspec-reporter-#{name}") {
|
66
|
+
config[:run_data] = run_data
|
67
|
+
case name
|
68
|
+
when "json"
|
69
|
+
reporter = Inspec::Reporters::Json.new(config)
|
70
|
+
when "json-automate"
|
71
|
+
reporter = Inspec::Reporters::JsonAutomate.new(config)
|
72
|
+
when "yaml"
|
73
|
+
reporter = Inspec::Reporters::Yaml.new(config)
|
74
|
+
else
|
75
|
+
# If we made it here, it might be a plugin
|
76
|
+
begin
|
77
|
+
activator = Inspec::Plugin::V2::Registry.instance.find_activator(plugin_type: :reporter, activator_name: name.to_sym)
|
78
|
+
activator.activate!
|
79
|
+
reporter = activator.implementation_class.new(config)
|
80
|
+
unless reporter.respond_to(:report?)
|
81
|
+
return run_data
|
82
|
+
end
|
83
|
+
rescue Inspec::Plugin::V2::LoadError
|
84
|
+
# Must not have been a plugin - just return the run_data
|
69
85
|
return run_data
|
70
86
|
end
|
71
|
-
rescue Inspec::Plugin::V2::LoadError
|
72
|
-
# Must not have been a plugin - just return the run_data
|
73
|
-
return run_data
|
74
87
|
end
|
75
|
-
end
|
76
88
|
|
77
|
-
|
89
|
+
reporter.report
|
90
|
+
}
|
78
91
|
end
|
79
92
|
end
|
@@ -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/run_data.rb
CHANGED
@@ -27,11 +27,13 @@ module Inspec
|
|
27
27
|
) do
|
28
28
|
include HashLikeStruct
|
29
29
|
def initialize(raw_run_data)
|
30
|
-
|
31
|
-
|
32
|
-
self.
|
33
|
-
self.
|
34
|
-
self.
|
30
|
+
@raw_run_data = raw_run_data
|
31
|
+
|
32
|
+
self.controls = @raw_run_data[:controls].map { |c| Inspec::RunData::Control.new(c) }
|
33
|
+
self.profiles = @raw_run_data[:profiles].map { |p| Inspec::RunData::Profile.new(p) }
|
34
|
+
self.statistics = Inspec::RunData::Statistics.new(@raw_run_data[:statistics])
|
35
|
+
self.platform = Inspec::RunData::Platform.new(@raw_run_data[:platform])
|
36
|
+
self.version = @raw_run_data[:version]
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|