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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31c8e898abb240d79f4c564ae182f620d291f0e895b1451185f891c0c03a4d3b
4
- data.tar.gz: 513a01ebe59969076c1a9e2c210f41b1366dbb17603e6370bdcb88d162d0d06a
3
+ metadata.gz: bbe23835cae58303aa3ac665ecd59e269752bbe4f6448e33db3219df0f04dbc8
4
+ data.tar.gz: 9a6d1e0e7758054f4d2746355a0c5538ede8af58a4220b3160d999f5e3093f34
5
5
  SHA512:
6
- metadata.gz: af17576ae657e9ca435998a6a57655f0c78d48782e3c7b1f37f62be74d7f7c40192330c3bb34f81c84c1de5b60aea6694c40c8b9ccdedd7d00dfe756614edc00
7
- data.tar.gz: dc279dd3ab134e92c7c318e859f0f4683347980eafaf80385330b3aeb762c9f72863cf66b2def9a2f61daabb378d3356b5110ac981d119e0e52eae009e785818
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: profile.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: profile.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"] })
@@ -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 = metadata.valid
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] = metadata.params[:name]
886
+ result[:summary][:profile] = info_from_parse[:name]
638
887
 
639
- count = controls_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
- params[:controls].each do |id, control|
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 controls_count
677
- params[:controls].values.length
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
- if opts[:export]
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: info, # TODO: conditionalize and call info_from_parse
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 opts[:export]
1030
+ @logger.debug " inspec.json" if export_opt_enabled
713
1031
 
714
- archive_files = opts[:export] ? files.push("inspec.json") : 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 opts[:export]
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
- # When there is Registry Values we are not spliting the value for backward compatibility
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
@@ -1,3 +1,3 @@
1
1
  module Inspec
2
- VERSION = "5.22.29".freeze
2
+ VERSION = "5.22.36".freeze
3
3
  end
@@ -168,10 +168,10 @@ module InspecPlugins
168
168
  end
169
169
 
170
170
  # read profile name from inspec.yml
171
- profile_name = profile.params[:name]
171
+ profile_name = profile.name
172
172
 
173
173
  # read profile version from inspec.yml
174
- profile_version = profile.params[:version]
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
- <% slugged_id = control.id.tr(" ", "_") %>
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-<%= slugged_id %>">
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-<%= slugged_id %>">
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-<%= slugged_id %>" value="Show Source"/>
68
- <input type="button" class="hide-source-code hidden" id="hide-code-<%= slugged_id %>" value="Hide Source"/>
69
- <pre class="source-code hidden" id="source-code-<%= slugged_id %>">
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 control_id = evt.srcElement.id.replace("show-code-", "")
14
+ var slugged_id = evt.srcElement.id.replace("show-code-", "")
15
15
  addCssClass(evt.srcElement.id, "hidden")
16
- removeCssClass("hide-code-" + control_id, "hidden")
17
- removeCssClass("source-code-" + control_id, "hidden")
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 control_id = evt.srcElement.id.replace("hide-code-", "")
21
+ var slugged_id = evt.srcElement.id.replace("hide-code-", "")
22
22
  addCssClass(evt.srcElement.id, "hidden")
23
- addCssClass("source-code-" + control_id, "hidden")
24
- removeCssClass("show-code-" + control_id, "hidden")
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.29
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-10-24 00:00:00.000000000 Z
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