inspec-core 5.22.3 → 5.22.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)
@@ -682,7 +998,6 @@ module Inspec
682
998
  end
683
999
 
684
1000
  # generates a archive of a folder profile
685
- # assumes that the profile was checked before
686
1001
  def archive(opts)
687
1002
  # check if file exists otherwise overwrite the archive
688
1003
  dst = archive_name(opts)
@@ -699,31 +1014,36 @@ module Inspec
699
1014
  # TODO ignore all .files, but add the files to debug output
700
1015
 
701
1016
  # Generate temporary inspec.json for archive
702
- Inspec::Utils::JsonProfileSummary.produce_json(
703
- info: info,
704
- write_path: "#{root_path}inspec.json",
705
- suppress_output: true
706
- )
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
1020
+ Inspec::Utils::JsonProfileSummary.produce_json(
1021
+ info: info_for_profile_summary,
1022
+ write_path: "#{root_path}inspec.json",
1023
+ suppress_output: true
1024
+ )
1025
+ end
707
1026
 
708
1027
  # display all files that will be part of the archive
709
1028
  @logger.debug "Add the following files to archive:"
710
1029
  files.each { |f| @logger.debug " " + f }
711
- @logger.debug " inspec.json"
1030
+ @logger.debug " inspec.json" if export_opt_enabled
712
1031
 
1032
+ archive_files = export_opt_enabled ? files.push("inspec.json") : files
713
1033
  if opts[:zip]
714
1034
  # generate zip archive
715
1035
  require "inspec/archive/zip"
716
1036
  zag = Inspec::Archive::ZipArchiveGenerator.new
717
- zag.archive(root_path, files.push("inspec.json"), dst)
1037
+ zag.archive(root_path, archive_files, dst)
718
1038
  else
719
1039
  # generate tar archive
720
1040
  require "inspec/archive/tar"
721
1041
  tag = Inspec::Archive::TarArchiveGenerator.new
722
- tag.archive(root_path, files.push("inspec.json"), dst)
1042
+ tag.archive(root_path, archive_files, dst)
723
1043
  end
724
1044
 
725
1045
  # Cleanup
726
- FileUtils.rm_f("#{root_path}inspec.json")
1046
+ FileUtils.rm_f("#{root_path}inspec.json") if export_opt_enabled
727
1047
 
728
1048
  @logger.info "Finished archive generation."
729
1049
  true
@@ -829,10 +1149,12 @@ module Inspec
829
1149
  return Pathname.new(name)
830
1150
  end
831
1151
 
832
- name = params[:name] ||
1152
+ # Using metadata to fetch basic info of name and version
1153
+ metadata = @source_reader.metadata.params
1154
+ name = metadata[:name] ||
833
1155
  raise("Cannot create an archive without a profile name! Please "\
834
1156
  "specify the name in metadata or use --output to create the archive.")
835
- version = params[:version] ||
1157
+ version = metadata[:version] ||
836
1158
  raise("Cannot create an archive without a profile version! Please "\
837
1159
  "specify the version in metadata or use --output to create the archive.")
838
1160
  ext = opts[:zip] ? "zip" : "tar.gz"
@@ -319,15 +319,9 @@ module Inspec::Resources
319
319
  return nil
320
320
  end
321
321
 
322
- resolve_ipv4 = resolve_ipv4.inject(:merge) if resolve_ipv4.is_a?(Array)
323
-
324
322
  # Append the ipv4 addresses
325
- resolve_ipv4.each_value do |ip|
326
- matched = ip.to_s.chomp.match(Resolv::IPv4::Regex)
327
- next if matched.nil? || addresses.include?(matched.to_s)
328
-
329
- addresses << matched.to_s
330
- end
323
+ resolve_ipv4 = [resolve_ipv4] unless resolve_ipv4.is_a?(Array)
324
+ resolve_ipv4.each { |entry| addresses << entry["IPAddress"] }
331
325
 
332
326
  # -Type AAAA is the DNS query for IPv6 server Address.
333
327
  cmd = inspec.command("Resolve-DnsName –Type AAAA #{hostname} | ConvertTo-Json")
@@ -337,15 +331,9 @@ module Inspec::Resources
337
331
  return nil
338
332
  end
339
333
 
340
- resolve_ipv6 = resolve_ipv6.inject(:merge) if resolve_ipv6.is_a?(Array)
341
-
342
334
  # Append the ipv6 addresses
343
- resolve_ipv6.each_value do |ip|
344
- matched = ip.to_s.chomp.match(Resolv::IPv6::Regex)
345
- next if matched.nil? || addresses.include?(matched.to_s)
346
-
347
- addresses << matched.to_s
348
- end
335
+ resolve_ipv6 = [resolve_ipv6] unless resolve_ipv6.is_a?(Array)
336
+ resolve_ipv6.each { |entry| addresses << entry["IPAddress"] }
349
337
 
350
338
  addresses
351
339
  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
- # 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
 
data/lib/inspec/rule.rb CHANGED
@@ -63,6 +63,11 @@ module Inspec
63
63
  # Rubocop thinks we are raising an exception - we're actually calling RSpec's fail()
64
64
  its(location) { fail e.message } # rubocop: disable Style/SignalException
65
65
  end
66
+
67
+ # instance_eval evaluates the describe block and raise errors if at the resource level any execution is failed
68
+ # Waived controls expect not to raise any controls and get skipped if run is false so __apply_waivers needs to be called here too
69
+ # so that waived control are actually gets waived.
70
+ __apply_waivers
66
71
  end
67
72
  end
68
73