class Inspec::Profile
def self.copy_deps_into_cache(file_provider, opts)
TODO: use relative file provider
Check if the profile contains a vendored cache, move content into global cache
def self.copy_deps_into_cache(file_provider, opts) # filter content cache = file_provider.files.find_all do |entry| entry.start_with?('vendor') end content = Hash[cache.map { |x| [x, file_provider.binread(x)] }] keys = content.keys keys.each do |key| next if content[key].nil? # remove prefix rel = Pathname.new(key).relative_path_from(Pathname.new('vendor')).to_s tar = Pathname.new(opts[:vendor_cache].path).join(rel) FileUtils.mkdir_p tar.dirname.to_s Inspec::Log.debug "Copy #{tar} to cache directory" File.binwrite(tar.to_s, content[key]) end end
def self.for_fetcher(fetcher, opts)
def self.for_fetcher(fetcher, opts) opts[:vendor_cache] = opts[:vendor_cache] || Cache.new path, writable = fetcher.fetch for_path(path, opts.merge(target: fetcher.target, writable: writable)) end
def self.for_path(path, opts)
def self.for_path(path, opts) file_provider = FileProvider.for_path(path) rp = file_provider.relative_provider # copy embedded dependecies into global cache copy_deps_into_cache(rp, opts) unless opts[:vendor_cache].nil? reader = Inspec::SourceReader.resolve(rp) if reader.nil? raise("Don't understand inspec profile in #{path}, it " \ "doesn't look like a supported profile structure.") end new(reader, opts) end
def self.for_target(target, opts = {})
def self.for_target(target, opts = {}) opts[:vendor_cache] = opts[:vendor_cache] || Cache.new fetcher = resolve_target(target, opts[:vendor_cache]) for_fetcher(fetcher, opts) end
def self.resolve_target(target, cache)
def self.resolve_target(target, cache) Inspec::Log.debug "Resolve #{target} into cache #{cache.path}" Inspec::CachedFetcher.new(target, cache) end
def archive(opts)
generates a archive of a folder profile
def archive(opts) # check if file exists otherwise overwrite the archive dst = archive_name(opts) if dst.exist? && !opts[:overwrite] @logger.info "Archive #{dst} exists already. Use --overwrite." return false end # remove existing archive File.delete(dst) if dst.exist? @logger.info "Generate archive #{dst}." # filter files that should not be part of the profile # TODO ignore all .files, but add the files to debug output # display all files that will be part of the archive @logger.debug 'Add the following files to archive:' files.each { |f| @logger.debug ' ' + f } if opts[:zip] # generate zip archive require 'inspec/archive/zip' zag = Inspec::Archive::ZipArchiveGenerator.new zag.archive(root_path, files, dst) else # generate tar archive require 'inspec/archive/tar' tag = Inspec::Archive::TarArchiveGenerator.new tag.archive(root_path, files, dst) end @logger.info 'Finished archive generation.' true end
def archive_name(opts)
-
(Pathname)- path for the archive
Parameters:
-
configuration(Hash) -- options
def archive_name(opts) if (name = opts[:output]) return Pathname.new(name) end name = params[:name] || raise('Cannot create an archive without a profile name! Please '\ 'specify the name in metadata or use --output to create the archive.') version = params[:version] || raise('Cannot create an archive without a profile version! Please '\ 'specify the version in metadata or use --output to create the archive.') ext = opts[:zip] ? 'zip' : 'tar.gz' slug = name.downcase.strip.tr(' ', '-').gsub(/[^\w-]/, '_') Pathname.new(Dir.pwd).join("#{slug}-#{version}.#{ext}") end
def check # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
-
(Boolean)- true if no errors were found, false otherwise
def check # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength # initial values for response object result = { summary: { valid: false, timestamp: Time.now.iso8601, location: @target, profile: nil, controls: 0, }, errors: [], warnings: [], } entry = lambda { |file, line, column, control, msg| { file: file, line: line, column: column, control_id: control, msg: msg, } } warn = lambda { |file, line, column, control, msg| @logger.warn(msg) result[:warnings].push(entry.call(file, line, column, control, msg)) } error = lambda { |file, line, column, control, msg| @logger.error(msg) result[:errors].push(entry.call(file, line, column, control, msg)) } @logger.info "Checking profile in #{@target}" meta_path = @source_reader.target.abs_path(@source_reader.metadata.ref) if meta_path =~ /metadata\.rb$/ warn.call(@target, 0, 0, nil, 'The use of `metadata.rb` is deprecated. Use `inspec.yml`.') end # verify metadata m_errors, m_warnings = metadata.valid m_errors.each { |msg| error.call(meta_path, 0, 0, nil, msg) } m_warnings.each { |msg| warn.call(meta_path, 0, 0, nil, msg) } m_unsupported = metadata.unsupported m_unsupported.each { |u| warn.call(meta_path, 0, 0, nil, "doesn't support: #{u}") } @logger.info 'Metadata OK.' if m_errors.empty? && m_unsupported.empty? # extract profile name result[:summary][:profile] = metadata.params[:name] # check if the profile is using the old test directory instead of the # new controls directory if @source_reader.tests.keys.any? { |x| x =~ %r{^test/$} } warn.call(@target, 0, 0, nil, 'Profile uses deprecated `test` directory, rename it to `controls`.') end count = controls_count result[:summary][:controls] = count if count == 0 warn.call(nil, nil, nil, nil, 'No controls or tests were defined.') else @logger.info("Found #{count} controls.") end # iterate over hash of groups params[:controls].each { |id, control| sfile = control[:source_location][:ref] sline = control[:source_location][:line] error.call(sfile, sline, nil, id, 'Avoid controls with empty IDs') if id.nil? or id.empty? next if id.start_with? '(generated ' warn.call(sfile, sline, nil, id, "Control #{id} has no title") if control[:title].to_s.empty? warn.call(sfile, sline, nil, id, "Control #{id} has no description") if control[:desc].to_s.empty? warn.call(sfile, sline, nil, id, "Control #{id} has impact > 1.0") if control[:impact].to_f > 1.0 warn.call(sfile, sline, nil, id, "Control #{id} has impact < 0.0") if control[:impact].to_f < 0.0 warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? or control[:checks].empty? } # profile is valid if we could not find any error result[:summary][:valid] = result[:errors].empty? @logger.info 'Control definitions OK.' if result[:warnings].empty? result end
def collect_tests(include_list = @controls)
def collect_tests(include_list = @controls) if !@tests_collected locked_dependencies.each(&:collect_tests) tests.each do |path, content| next if content.nil? || content.empty? abs_path = source_reader.target.abs_path(path) @runner_context.load_control_file(content, abs_path, nil) end @tests_collected = true end filter_controls(@runner_context.all_rules, include_list) end
def controls_count
def controls_count params[:controls].values.length end
def cwd
tarballs.
thought through, especially with respect to relative paths in
TODO(ssd): Relative path handling really needs to be carefully
def cwd @target.is_a?(String) && File.directory?(@target) ? @target : './' end
def files
def files @source_reader.target.files end
def filter_controls(controls_array, include_list)
def filter_controls(controls_array, include_list) return controls_array if include_list.nil? || include_list.empty? # Check for anything that might be a regex in the list, and make it official include_list.each_with_index do |inclusion, index| next if inclusion.is_a?(Regexp) # Insist the user wrap the regex in slashes to demarcate it as a regex next unless inclusion.start_with?('/') && inclusion.end_with?('/') inclusion = inclusion[1..-2] # Trim slashes begin re = Regexp.new(inclusion) include_list[index] = re rescue RegexpError => e warn "Ignoring unparseable regex '/#{inclusion}/' in --control CLI option: #{e.message}" include_list[index] = nil end end include_list.compact! controls_array.select do |c| id = ::Inspec::Rule.rule_id(c) include_list.any? do |inclusion| # Try to see if the inclusion is a regex, and if it matches inclusion == id || (inclusion.is_a?(Regexp) && inclusion =~ id) end end end
def generate_lockfile
-
(Inspec::Lockfile)-
Parameters:
-
vendor_path(String) -- Path to the on-disk vendor dir
def generate_lockfile res = Inspec::DependencySet.new(cwd, @cache, nil, @backend) res.vendor(metadata.dependencies) Inspec::Lockfile.from_dependency_set(res) end
def info(res = params.dup)
def info(res = params.dup) # add information about the controls res[:controls] = res[:controls].map do |id, rule| next if id.to_s.empty? data = rule.dup data.delete(:checks) data[:impact] ||= 0.5 data[:impact] = 1.0 if data[:impact] > 1.0 data[:impact] = 0.0 if data[:impact] < 0.0 data[:id] = id data end.compact # resolve hash structure in groups res[:groups] = res[:groups].map do |id, group| group[:id] = id group end # add information about the required attributes res[:attributes] = res[:attributes].map(&:to_hash) unless res[:attributes].nil? || res[:attributes].empty? res[:sha256] = sha256 res[:parent_profile] = parent_profile unless parent_profile.nil? res end
def info!
def info! info(load_params.dup) end
def initialize(source_reader, options = {})
def initialize(source_reader, options = {}) @source_reader = source_reader @target = options[:target] @logger = options[:logger] || Logger.new(nil) @locked_dependencies = options[:dependencies] @controls = options[:controls] || [] @writable = options[:writable] || false @profile_id = options[:id] @cache = options[:vendor_cache] || Cache.new @attr_values = options[:attributes] @tests_collected = false @libraries_loaded = false @check_mode = options[:check_mode] || false Metadata.finalize(@source_reader.metadata, @profile_id, options) # if a backend has already been created, clone it so each profile has its own unique backend object # otherwise, create a new backend object # # This is necessary since we store the RuntimeProfile on the backend object. If a user runs `inspec exec` # with multiple profiles, only the RuntimeProfile for the last-loaded profile will be available if # we share the backend between profiles. # # This will cause issues if a profile attempts to load a file via `inspec.profile.file` train_options = options.reject { |k, _| k == 'target' } # See https://github.com/chef/inspec/pull/1646 @backend = options[:backend].nil? ? Inspec::Backend.create(train_options) : options[:backend].dup @runtime_profile = RuntimeProfile.new(self) @backend.profile = @runtime_profile @runner_context = options[:profile_context] || Inspec::ProfileContext.for_profile(self, @backend, @attr_values) @supports_platform = metadata.supports_platform?(@backend) @supports_runtime = metadata.supports_runtime? end
def load_checks_params(params)
def load_checks_params(params) load_libraries tests = collect_tests params[:controls] = controls = {} params[:groups] = groups = {} prefix = @source_reader.target.prefix || '' tests.each do |rule| next if rule.nil? f = load_rule_filepath(prefix, rule) load_rule(rule, f, controls, groups) end params[:attributes] = @runner_context.attributes params end
def load_dependencies
def load_dependencies config = { cwd: cwd, cache: @cache, backend: @backend, parent_profile: name, } Inspec::DependencySet.from_lockfile(lockfile, config, { attributes: @attr_values }) end
def load_libraries
def load_libraries return @runner_context if @libraries_loaded locked_dependencies.each do |d| c = d.load_libraries @runner_context.add_resources(c) end libs = libraries.map do |path, content| [content, path] end @runner_context.load_libraries(libs) @libraries_loaded = true @runner_context end
def load_params
def load_params params = @source_reader.metadata.params params[:name] = @profile_id unless @profile_id.nil? load_checks_params(params) @profile_id ||= params[:name] params end
def load_rule(rule, file, controls, groups)
def load_rule(rule, file, controls, groups) id = Inspec::Rule.rule_id(rule) location = rule.instance_variable_get(:@__source_location) controls[id] = { title: rule.title, desc: rule.desc, impact: rule.impact, refs: rule.ref, tags: rule.tag, checks: Inspec::Rule.checks(rule), code: Inspec::MethodSource.code_at(location, source_reader), source_location: location, } # try and grab code text from merge locations if controls[id][:code].empty? && Inspec::Rule.merge_count(rule) > 0 Inspec::Rule.merge_changes(rule).each do |merge_location| code = Inspec::MethodSource.code_at(merge_location, source_reader) if !code.empty? controls[id][:code] = code break end end end groups[file] ||= { title: rule.instance_variable_get(:@__group_title), controls: [], } groups[file][:controls].push(id) end
def load_rule_filepath(prefix, rule)
def load_rule_filepath(prefix, rule) file = rule.instance_variable_get(:@__file) file = file[prefix.length..-1] if file.start_with?(prefix) file end
def locked_dependencies
def locked_dependencies @locked_dependencies ||= load_dependencies end
def lockfile
def lockfile @lockfile ||= if lockfile_exists? Inspec::Lockfile.from_content(@source_reader.target.read('inspec.lock')) else generate_lockfile end end
def lockfile_exists?
def lockfile_exists? @source_reader.target.files.include?('inspec.lock') end
def lockfile_path
def lockfile_path File.join(cwd, 'inspec.lock') end
def name
def name metadata.params[:name] end
def params
def params @params ||= load_params end
def root_path
def root_path @source_reader.target.prefix end
def sha256
-
(Type)- description of returned object
def sha256 # get all dependency checksums deps = Hash[locked_dependencies.list.map { |k, v| [k, v.profile.sha256] }] res = OpenSSL::Digest::SHA256.new files = source_reader.tests.to_a + source_reader.libraries.to_a + source_reader.data_files.to_a + [['inspec.yml', source_reader.metadata.content]] + [['inspec.lock.deps', YAML.dump(deps)]] files.sort_by { |a| a[0] } .map { |f| res << f[0] << "\0" << f[1] << "\0" } res.digest.unpack('H*')[0] end
def supported?
@returns [TrueClass, FalseClass]
backend machine and the current inspec version.
Is this profile is supported on the current platform of the
def supported? supports_platform? && supports_runtime? end
def supports_platform?
def supports_platform? if @supports_platform.nil? @supports_platform = metadata.supports_platform?(@backend) end @supports_platform end
def supports_runtime?
def supports_runtime? if @supports_runtime.nil? @supports_runtime = metadata.supports_runtime? end @supports_runtime end
def to_s
def to_s "Inspec::Profile<#{name}>" end
def version
def version metadata.params[:version] end
def writable?
def writable? @writable end