class Inspec::Metadata
Use valid? to determine if the metadata is coherent.
This allows the check CLI command to analyse the issues.
A Metadata object may be created and finalized with invalid data.
See lib/inspec/profile.rb for the runtime representation of a profile.
This class does NOT represent the runtime state of a profile during execution.
metadata like if this profile supports the current runtime and the intended target.
This includes the metadata stored in the profile’s metadata.rb file, as well as inferred
The Metadata class represents a profile’s metadata.
def self.finalize(metadata, profile_id, options, logger = nil)
def self.finalize(metadata, profile_id, options, logger = nil) return nil if metadata.nil? param = metadata.params || {} options ||= {} param["version"] = param["version"].to_s unless param["version"].nil? metadata.params = symbolize_keys(param) metadata.params[:supports] = finalize_supports(metadata.params[:supports], logger) finalize_name(metadata, profile_id, options[:target]) metadata end
def self.finalize_name(metadata, profile_id, original_target)
def self.finalize_name(metadata, profile_id, original_target) # profile_id always overwrites whatever already exists as the name unless profile_id.to_s.empty? metadata.params[:name] = profile_id.to_s return end # don't overwrite an existing name return unless metadata.params[:name].nil? # if there's a title, there is no need to set a name too return unless metadata.params[:title].nil? # create a new name based on the original target if it exists # Crudely slug the target to not contain slashes, to avoid breaking # unit tests that look for warning sequences return if original_target.to_s.empty? metadata.params[:title] = "tests from #{original_target}" metadata.params[:name] = metadata.params[:title].gsub(%r{[\/\\]}, ".") end
def self.finalize_supports(supports, logger)
def self.finalize_supports(supports, logger) case x = supports when Hash then [finalize_supports_elem(x, logger)] when Array then x.map { |e| finalize_supports_elem(e, logger) }.compact when nil then [] end end
def self.finalize_supports_elem(elem, logger)
def self.finalize_supports_elem(elem, logger) case x = elem when Hash x[:release] = x[:release].to_s if x[:release] x when Array logger.warn( "Failed to read supports entry that is an array. Please use "\ "the `supports: {os-family: xyz}` syntax." ) nil when nil then nil else Inspec.deprecate( :supports_syntax, "Do not use deprecated `supports: #{x}` syntax. Instead use:\n"\ "supports:\n - os-family: #{x}\n\n" ) { :'os-family' => x } # rubocop:disable Style/HashSyntax end end
def self.from_file(path, profile_id, logger = nil)
def self.from_file(path, profile_id, logger = nil) unless File.file?(path) logger ||= Logger.new(nil) logger.error "Can't find metadata file #{path}" return nil end from_ref(File.basename(path), File.read(path), profile_id, logger) end
def self.from_ref(ref, content, profile_id, logger = nil)
def self.from_ref(ref, content, profile_id, logger = nil) # NOTE there doesn't have to exist an actual file, it may come from an # archive (i.e., content) case File.basename(ref) when "inspec.yml" from_yaml(ref, content, profile_id, logger) when "metadata.rb" from_ruby(ref, content, profile_id, logger) else logger ||= Logger.new(nil) logger.error "Don't know how to handle metadata in #{ref}" nil end end
def self.from_ruby(ref, content, profile_id, logger = nil)
def self.from_ruby(ref, content, profile_id, logger = nil) res = Metadata.new(ref, logger) res.instance_eval(content, ref, 1) res.content = content finalize(res, profile_id, {}, logger) end
def self.from_yaml(ref, content, profile_id, logger = nil)
def self.from_yaml(ref, content, profile_id, logger = nil) require "erb" unless defined?(Erb) res = Metadata.new(ref, logger) res.params = YAML.load(ERB.new(content).result) res.content = content finalize(res, profile_id, {}, logger) end
def self.symbolize_keys(obj)
def self.symbolize_keys(obj) return obj.map { |i| symbolize_keys(i) } if obj.is_a?(Array) return obj unless obj.is_a?(Hash) obj.each_with_object({}) do |(k, v), h| v = symbolize_keys(v) if v.is_a?(Hash) v = symbolize_keys(v) if v.is_a?(Array) h[k.to_sym] = v end end
def dependencies
def dependencies params[:depends] || [] end
def gem_dependencies
def gem_dependencies params[:gem_dependencies] || [] end
def initialize(ref, logger = nil)
def initialize(ref, logger = nil) @ref = ref @logger = logger || Logger.new(nil) @content = "" @params = {} @missing_methods = [] end
def inspec_requirement
def inspec_requirement # using Gem::Requirement here to allow nil values which # translate to [">= 0"] Gem::Requirement.create(params[:inspec_version]) end
def method_missing(sth, *args)
def method_missing(sth, *args) @logger.warn "#{ref} doesn't support: #{sth} #{args}" @missing_methods.push(sth) end
def supports(sth, version = nil)
def supports(sth, version = nil) # Ignore supports with metadata.rb. This file is legacy and the way it # it handles `supports` deprecated. A deprecation warning will be printed # already. end
def supports_platform?(backend)
def supports_platform?(backend) require "inspec/resources/platform" # break circularity in load backend.platform.supported?(params[:supports]) end
def supports_runtime?
def supports_runtime? running = Gem::Version.new(Inspec::VERSION) inspec_requirement.satisfied_by?(running) end
def unsupported
def unsupported @missing_methods end
def valid # rubocop:disable Metrics/AbcSize
return all warn and errors
def valid # rubocop:disable Metrics/AbcSize errors = [] warnings = [] %w{name version}.each do |field| next unless params[field.to_sym].nil? errors.push("Missing profile #{field} in #{ref}") end if %r{[\/\\]} =~ params[:name] errors.push("The profile name (#{params[:name]}) contains a slash" \ " which is not permitted. Please remove all slashes from `inspec.yml`.") end # if version is set, ensure it is correct if !params[:version].nil? && !valid_version?(params[:version]) errors.push("Version needs to be in SemVer format") end if params[:entitlement_id] && params[:entitlement_id].strip.empty? errors.push("Entitlement ID should not be blank.") end unless supports_runtime? warnings.push("The current inspec version #{Inspec::VERSION} cannot satisfy profile inspec_version constraint #{params[:inspec_version]}") end %w{title summary maintainer copyright license}.each do |field| next unless params[field.to_sym].nil? warnings.push("Missing profile #{field} in #{ref}") end # if license is set, ensure it is in SPDX format or marked as proprietary if !params[:license].nil? && !valid_license?(params[:license]) warnings.push("License '#{params[:license]}' needs to be in SPDX format or marked as 'Proprietary'. See https://spdx.org/licenses/.") end # If gem_dependencies is set, it must be an array of hashes with keys name and optional version unless params[:gem_dependencies].nil? list = params[:gem_dependencies] if list.is_a?(Array) && list.all? { |e| e.is_a? Hash } list.each do |entry| errors.push("gem_dependencies entries must all have a 'name' field") unless entry.key?(:name) if entry[:version] orig = entry[:version] begin # Split on commas as we may have a complex dep orig.split(",").map { |c| Gem::Requirement.parse(c) } rescue Gem::Requirement::BadRequirementError errors.push "Unparseable gem dependency '#{orig}' for #{entry[:name]}" rescue Inspec::GemDependencyInstallError => e errors.push e.message end end extra = (entry.keys - %i{name version}) unless extra.empty? warnings.push "Unknown gem_dependencies key(s) #{extra.join(",")} seen for entry '#{entry[:name]}'" end end else errors.push("gem_dependencies must be a List of Hashes") end end [errors, warnings] end
def valid?
def valid? errors, _warnings = valid errors.empty? && unsupported.empty? end
def valid_license?(value)
def valid_license?(value) value =~ /^Proprietary[,;]?\b/ || Spdx.valid_license?(value) end
def valid_version?(value)
def valid_version?(value) Semverse::Version.new(value) true rescue Semverse::InvalidVersionFormat false end