Skip to content

Commit

Permalink
Validate Hiera values
Browse files Browse the repository at this point in the history
* Add methods for retrieving profile names and confines
* Compile data the way that compliance_markup does and validate the
  results
* Handle a few error cases

Fixes simp#14
  • Loading branch information
silug committed Oct 11, 2022
1 parent a6868f0 commit 974cbf4
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 5 deletions.
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
133 changes: 128 additions & 5 deletions lib/scelint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,6 +55,8 @@ def initialize(paths = ['.'])
lint(file, data)
end

validate

@data # rubocop:disable Lint/Void
end

Expand All @@ -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'
Expand All @@ -74,7 +79,7 @@ def parse(file)
@errors << "#{file}: Failed to parse file: #{e.message}"
end

{}
nil
end

def files
Expand Down Expand Up @@ -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|
Expand Down Expand Up @@ -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)
Expand All @@ -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

0 comments on commit 974cbf4

Please sign in to comment.