diff --git a/Gemfile b/Gemfile index 36ab187..0427169 100644 --- a/Gemfile +++ b/Gemfile @@ -12,3 +12,9 @@ group :tests do gem 'rubocop-rspec', '~> 2.10' gem 'rubocop-rake', '~> 0.6.0' end + +group :development do + gem 'pry', '~> 0.14.1' + gem 'pry-byebug', '~> 3.10' + gem 'rdoc', '~> 6.4' +end diff --git a/lib/scelint.rb b/lib/scelint.rb index d6507f1..72abf64 100644 --- a/lib/scelint.rb +++ b/lib/scelint.rb @@ -32,13 +32,16 @@ def initialize(paths = ['.']) ].each do |dir| ['yaml', 'json'].each do |type| Dir.glob("#{path}/#{dir}/**/*.#{type}").each do |file| - @data[file] = parse(file) + this_file = parse(file) + next if this_file.nil? + @data[file] = this_file merged_data = merged_data.deep_merge!(@data[file]) end end end elsif File.exist?(path) - @data[path] = parse(path) + this_file = parse(path) + @data[path] = this_file unless this_file.nil? else raise "Can't find path '#{path}'" end @@ -52,6 +55,8 @@ def initialize(paths = ['.']) lint(file, data) end + validate + @data # rubocop:disable Lint/Void end @@ -65,7 +70,7 @@ def parse(file) 'json' else @errors << "#{file}: Failed to determine file type" - nil + return nil end begin return YAML.safe_load(File.read(file)) if type == 'yaml' @@ -74,7 +79,7 @@ def parse(file) @errors << "#{file}: Failed to parse file: #{e.message}" end - {} + nil end def files @@ -252,7 +257,6 @@ def check_remediation(file, check, remediation_section) if remediation_section.is_a?(Hash) remediation_section.each do |section, value| - # require 'pry-byebug'; binding.pry if section == 'disabled' case section when 'scan-false-positive', 'disabled' value.each do |reason| @@ -368,6 +372,123 @@ def check_checks(file, data) end end + def profiles + return @profiles unless @profiles.nil? + + return nil unless @data.key?('merged data') + return nil unless @data['merged data']['profiles'].is_a?(Hash) + + @profiles = @data['merged data']['profiles'].keys + end + + def confines + return @confines unless @confines.nil? + + confine = {} + + @data.each do |key, value| + next if key == 'merged data' + next unless value.is_a?(Hash) + + ['profiles', 'ce', 'checks'].each do |type| + next unless value.key?(type) + next unless value[type].is_a?(Hash) + + value[type].each do |_k, v| + next unless v.is_a?(Hash) + confine = confine.merge(v['confine']) if v.key?('confine') + end + end + end + + @confines = [] + index = 0 + max_count = 1 + confine.each { |_key, value| max_count *= Array(value).count } + + confine.each do |key, value| + (index..(max_count-1)).each do |i| + @confines[i] ||= {} + @confines[i][key] = Array(value)[i % Array(value).count] + end + end + + @confines + end + + def apply_confinement(file, data, confine) + return data unless data.is_a?(Hash) + + def should_delete(file, key, specification, confine) + return false unless specification.key?('confine') + + # require 'pry-byebug'; binding.pry + unless specification['confine'].is_a?(Hash) + @warnings << "#{file}: 'confine' is not a Hash in key #{key}" + return false + end + + specification['confine'].each do |confinement_setting, confinement_value| + return true unless confine.is_a?(Hash) + return true unless confine.key?(confinement_setting) + Array(confine[confinement_setting]).each do |value| + return false if Array(confinement_value).include?(value) + end + end + + true + end + + value = Marshal.load(Marshal.dump(data)) + value.delete_if { |key, specification| should_delete(file, key, specification, confine) } + + value + end + + def compile(profile, confine = nil) + merged_data = {} + + # Pass 1: Merge everything with confined values removed. + @data.each do |file, data| + next if file == 'merged data' + data.each do |key, value| + confined_value = apply_confinement(file, value, confine) + + unless confined_value.is_a?(Hash) + if merged_data.key?(key) + @warnings << "#{file}#{confine.nil? ? '' : " (confined: #{confine})"}: key #{key} redefined (previous value: #{merged_data[key]}, new value: #{confined_value})" unless key == 'version' + end + + merged_data[key] = confined_value + next + end + + merged_data[key] ||= {} + confined_value.each do |k, v| + merged_data[key][k] ||= {} + merged_data[key][k] = merged_data[key][k].deep_merge!(v, {:knockout_prefix => '--'}) + end + end + end + + # Pass 2: Extract the relevant Hiera values. + # require 'pry-byebug'; binding.pry if @data.any? { |_file, file_data| file_data.is_a?(Hash) && file_data.any? { |_key, value| value.is_a?(Hash) && value.any? { |_k, v| v.is_a?(Hash) && v.key?('confine') } } } + end + + def validate + if profiles.nil? + @warnings << 'No profiles found, unable to validate Hiera data' + return nil + end + + profiles.each do |profile| + compile(profile) + confines.each do |confine| + compile(profile, confine) + end + end + end + def lint(file, data) check_version(file, data) check_keys(file, data) @@ -376,6 +497,8 @@ def lint(file, data) check_ce(file, data['ce']) if data['ce'] check_checks(file, data['checks']) if data['checks'] check_controls(file, data['controls']) if data['controls'] + rescue => e + @errors << "#{file}: #{e.message} (not a hash?)" end end end