inspec-core 5.22.3 → 5.22.36

Sign up to get free protection for your applications and to get access to all the features.
@@ -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