1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
|
require 'yaml'
require 'base64'
module PuppetSyntax
class Hiera
def check_hiera_key(key)
if key.is_a? Symbol
if key.to_s.start_with?(':')
"Puppet automatic lookup will not use leading '::'"
elsif !/^[a-z]+$/.match?(key) # we allow Hiera's own configuration
'Puppet automatic lookup will not look up symbols'
end
elsif !/^[a-z][a-z0-9_]+(::[a-z][a-z0-9_]+)*$/.match?(key)
return 'Looks like a missing colon' if /[^:]:[^:]/.match?(key)
# be extra helpful
'Not a valid Puppet variable name for automatic lookup'
end
end
def check_hiera_data(_key, value)
# using filter_map to remove nil values
# there will be nil values if check_broken_function_call didn't return a string
# this is a shorthand for filter.compact
# https://blog.saeloun.com/2019/05/25/ruby-2-7-enumerable-filter-map/
keys_and_values(value).filter_map do |element|
check_broken_function_call(element)
end
end
# Recurse through complex data structures. Return on first error.
def check_eyaml_data(name, val)
error = nil
if val.is_a? String
err = check_eyaml_blob(val)
error = "Key #{name} #{err}" if err
elsif val.is_a? Array
val.each_with_index do |v, idx|
error = check_eyaml_data("#{name}[#{idx}]", v)
break if error
end
elsif val.is_a? Hash
val.each do |k, v|
error = check_eyaml_data("#{name}['#{k}']", v)
break if error
end
end
error
end
def check_eyaml_blob(val)
return unless /^ENC\[/.match?(val)
val.sub!('ENC[', '')
val.gsub!(/\s+/, '')
return 'has unterminated eyaml value' unless /\]$/.match?(val)
val.sub!(/\]$/, '')
method, base64 = val.split(',')
if base64.nil?
base64 = method
method = 'PKCS7'
end
known_methods = %w[PKCS7 GPG GKMS KMS TWOFAC SecretBox VAULT GCPKMS RSA SSHAGENT VAULT_RS cli]
return "has unknown eyaml method #{method}" unless known_methods.include? method
return 'has unpadded or truncated base64 data' unless base64.length % 4 == 0
# Base64#decode64 will silently ignore characters outside the alphabet,
# so we check resulting length of binary data instead
pad_length = base64.gsub(/[^=]/, '').length
return unless Base64.decode64(base64).length != (base64.length * 3 / 4) - pad_length
'has corrupt base64 data'
end
def check(filelist)
raise 'Expected an array of files' unless filelist.is_a?(Array)
errors = []
yamlargs = (Psych::VERSION >= '4.0') ? { aliases: true } : {}
filelist.each do |hiera_file|
begin
yamldata = YAML.load_file(hiera_file, **yamlargs)
rescue Exception => e
errors << "ERROR: Failed to parse #{hiera_file}: #{e}"
next
end
next unless yamldata
yamldata.each do |k, v|
if PuppetSyntax.check_hiera_keys
key_msg = check_hiera_key(k)
errors << "WARNING: #{hiera_file}: Key :#{k}: #{key_msg}" if key_msg
end
if PuppetSyntax.check_hiera_data
check_hiera_data(k, v).each do |value_msg|
errors << "WARNING: #{hiera_file}: Key :#{k}: #{value_msg}"
end
end
eyaml_msg = check_eyaml_data(k, v)
errors << "WARNING: #{hiera_file}: #{eyaml_msg}" if eyaml_msg
end
end
errors.map! { |e| e.to_s }
errors
end
private
# you can call functions in hiera, like this:
# "%{lookup('this_is_ok')}"
# you can do this in everywhere in a hiera value
# you cannot do string concatenation within {}:
# "%{lookup('this_is_ok'):3306}"
# You can do string concatenation outside of {}:
# "%{lookup('this_is_ok')}:3306"
def check_broken_function_call(element)
'string after a function call but before `}` in the value' if element.is_a?(String) && /%{[^}]+\('[^}]*'\)[^}\s]+}/.match?(element)
end
# gets a hash or array, returns all keys + values as array
def flatten_data(data, parent_key = [])
if data.is_a?(Hash)
data.flat_map do |key, value|
current_key = parent_key + [key.to_s]
if value.is_a?(Hash) || value.is_a?(Array)
flatten_data(value, current_key)
else
[current_key.join('.'), value]
end
end
elsif data.is_a?(Array) && !data.empty?
data.flat_map do |value|
if value.is_a?(Hash) || value.is_a?(Array)
flatten_data(value, parent_key)
else
[parent_key.join('.'), value]
end
end
else
[parent_key.join('.')]
end
end
# gets a string, hash or array, returns all keys + values as flattened + unique array
def keys_and_values(value)
if value.is_a?(Hash) || value.is_a?(Array)
flatten_data(value).flatten.uniq
else
[value]
end
end
end
end
|