lib/inspec/profile.rb in inspec-2.0.16 vs lib/inspec/profile.rb in inspec-2.0.17

- old
+ new

@@ -1,510 +1,510 @@ -# encoding: utf-8 -# Copyright 2015 Dominik Richter -# author: Dominik Richter -# author: Christoph Hartmann - -require 'forwardable' -require 'openssl' -require 'inspec/polyfill' -require 'inspec/cached_fetcher' -require 'inspec/file_provider' -require 'inspec/source_reader' -require 'inspec/metadata' -require 'inspec/backend' -require 'inspec/rule' -require 'inspec/log' -require 'inspec/profile_context' -require 'inspec/runtime_profile' -require 'inspec/method_source' -require 'inspec/dependencies/cache' -require 'inspec/dependencies/lockfile' -require 'inspec/dependencies/dependency_set' - -module Inspec - class Profile - extend Forwardable - - def self.resolve_target(target, cache) - Inspec::Log.debug "Resolve #{target} into cache #{cache.path}" - Inspec::CachedFetcher.new(target, cache) - end - - # Check if the profile contains a vendored cache, move content into global cache - # TODO: use relative file provider - # TODO: use source reader for Cache as well - 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_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_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_target(target, opts = {}) - opts[:vendor_cache] = opts[:vendor_cache] || Cache.new - fetcher = resolve_target(target, opts[:vendor_cache]) - for_fetcher(fetcher, opts) - end - - attr_reader :source_reader, :backend, :runner_context, :check_mode - def_delegator :@source_reader, :tests - def_delegator :@source_reader, :libraries - def_delegator :@source_reader, :metadata - - # rubocop:disable Metrics/AbcSize - 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) - end - - def name - metadata.params[:name] - end - - def version - metadata.params[:version] - end - - def writable? - @writable - end - - # - # Is this profile is supported on the current platform of the - # backend machine and the current inspec version. - # - # @returns [TrueClass, FalseClass] - # - def supported? - supports_platform? && supports_runtime? - end - - def supports_platform? - if @supports_platform.nil? - @supports_platform = metadata.supports_platform?(@backend) - end - @supports_platform - end - - def supports_runtime? - if @supports_runtime.nil? - @supports_runtime = metadata.supports_runtime? - end - @supports_runtime - end - - def params - @params ||= load_params - end - - 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 filter_controls(controls_array, include_list) - return controls_array if include_list.nil? || include_list.empty? - controls_array.select do |c| - id = ::Inspec::Rule.rule_id(c) - include_list.include?(id) - end - end - - 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 to_s - "Inspec::Profile<#{name}>" - end - - # return info using uncached params - def info! - info(load_params.dup) - end - - 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 - end - - # Check if the profile is internally well-structured. The logger will be - # used to print information on errors and warnings which are found. - # - # @return [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 controls_count - params[:controls].values.length - end - - # generates a archive of a folder profile - # assumes that the profile was checked before - 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 locked_dependencies - @locked_dependencies ||= load_dependencies - end - - def lockfile_exists? - @source_reader.target.files.include?('inspec.lock') - end - - def lockfile_path - File.join(cwd, 'inspec.lock') - end - - def root_path - @source_reader.target.prefix - end - - def files - @source_reader.target.files - end - - # - # TODO(ssd): Relative path handling really needs to be carefully - # thought through, especially with respect to relative paths in - # tarballs. - # - def cwd - @target.is_a?(String) && File.directory?(@target) ? @target : './' - end - - def lockfile - @lockfile ||= if lockfile_exists? - Inspec::Lockfile.from_content(@source_reader.target.read('inspec.lock')) - else - generate_lockfile - end - end - - # - # Generate an in-memory lockfile. This won't render the lock file - # to disk, it must be explicitly written to disk by the caller. - # - # @param vendor_path [String] Path to the on-disk vendor dir - # @return [Inspec::Lockfile] - # - def generate_lockfile - res = Inspec::DependencySet.new(cwd, @cache, nil, @backend) - res.vendor(metadata.dependencies) - Inspec::Lockfile.from_dependency_set(res) - end - - def load_dependencies - Inspec::DependencySet.from_lockfile(lockfile, cwd, @cache, @backend, { attributes: @attr_values }) - end - - # Calculate this profile's SHA256 checksum. Includes metadata, dependencies, - # libraries, data files, and controls. - # - # @return [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 - - private - - # Create an archive name for this profile and an additional options - # configuration. Either use :output or generate the name from metadata. - # - # @param [Hash] configuration options - # @return [Pathname] path for the archive - 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 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_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_rule_filepath(prefix, rule) - file = rule.instance_variable_get(:@__file) - file = file[prefix.length..-1] if file.start_with?(prefix) - file - end - - 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, - } - - groups[file] ||= { - title: rule.instance_variable_get(:@__group_title), - controls: [], - } - groups[file][:controls].push(id) - end - end -end +# encoding: utf-8 +# Copyright 2015 Dominik Richter +# author: Dominik Richter +# author: Christoph Hartmann + +require 'forwardable' +require 'openssl' +require 'inspec/polyfill' +require 'inspec/cached_fetcher' +require 'inspec/file_provider' +require 'inspec/source_reader' +require 'inspec/metadata' +require 'inspec/backend' +require 'inspec/rule' +require 'inspec/log' +require 'inspec/profile_context' +require 'inspec/runtime_profile' +require 'inspec/method_source' +require 'inspec/dependencies/cache' +require 'inspec/dependencies/lockfile' +require 'inspec/dependencies/dependency_set' + +module Inspec + class Profile + extend Forwardable + + def self.resolve_target(target, cache) + Inspec::Log.debug "Resolve #{target} into cache #{cache.path}" + Inspec::CachedFetcher.new(target, cache) + end + + # Check if the profile contains a vendored cache, move content into global cache + # TODO: use relative file provider + # TODO: use source reader for Cache as well + 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_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_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_target(target, opts = {}) + opts[:vendor_cache] = opts[:vendor_cache] || Cache.new + fetcher = resolve_target(target, opts[:vendor_cache]) + for_fetcher(fetcher, opts) + end + + attr_reader :source_reader, :backend, :runner_context, :check_mode + def_delegator :@source_reader, :tests + def_delegator :@source_reader, :libraries + def_delegator :@source_reader, :metadata + + # rubocop:disable Metrics/AbcSize + 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) + end + + def name + metadata.params[:name] + end + + def version + metadata.params[:version] + end + + def writable? + @writable + end + + # + # Is this profile is supported on the current platform of the + # backend machine and the current inspec version. + # + # @returns [TrueClass, FalseClass] + # + def supported? + supports_platform? && supports_runtime? + end + + def supports_platform? + if @supports_platform.nil? + @supports_platform = metadata.supports_platform?(@backend) + end + @supports_platform + end + + def supports_runtime? + if @supports_runtime.nil? + @supports_runtime = metadata.supports_runtime? + end + @supports_runtime + end + + def params + @params ||= load_params + end + + 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 filter_controls(controls_array, include_list) + return controls_array if include_list.nil? || include_list.empty? + controls_array.select do |c| + id = ::Inspec::Rule.rule_id(c) + include_list.include?(id) + end + end + + 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 to_s + "Inspec::Profile<#{name}>" + end + + # return info using uncached params + def info! + info(load_params.dup) + end + + 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 + end + + # Check if the profile is internally well-structured. The logger will be + # used to print information on errors and warnings which are found. + # + # @return [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 controls_count + params[:controls].values.length + end + + # generates a archive of a folder profile + # assumes that the profile was checked before + 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 locked_dependencies + @locked_dependencies ||= load_dependencies + end + + def lockfile_exists? + @source_reader.target.files.include?('inspec.lock') + end + + def lockfile_path + File.join(cwd, 'inspec.lock') + end + + def root_path + @source_reader.target.prefix + end + + def files + @source_reader.target.files + end + + # + # TODO(ssd): Relative path handling really needs to be carefully + # thought through, especially with respect to relative paths in + # tarballs. + # + def cwd + @target.is_a?(String) && File.directory?(@target) ? @target : './' + end + + def lockfile + @lockfile ||= if lockfile_exists? + Inspec::Lockfile.from_content(@source_reader.target.read('inspec.lock')) + else + generate_lockfile + end + end + + # + # Generate an in-memory lockfile. This won't render the lock file + # to disk, it must be explicitly written to disk by the caller. + # + # @param vendor_path [String] Path to the on-disk vendor dir + # @return [Inspec::Lockfile] + # + def generate_lockfile + res = Inspec::DependencySet.new(cwd, @cache, nil, @backend) + res.vendor(metadata.dependencies) + Inspec::Lockfile.from_dependency_set(res) + end + + def load_dependencies + Inspec::DependencySet.from_lockfile(lockfile, cwd, @cache, @backend, { attributes: @attr_values }) + end + + # Calculate this profile's SHA256 checksum. Includes metadata, dependencies, + # libraries, data files, and controls. + # + # @return [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 + + private + + # Create an archive name for this profile and an additional options + # configuration. Either use :output or generate the name from metadata. + # + # @param [Hash] configuration options + # @return [Pathname] path for the archive + 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 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_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_rule_filepath(prefix, rule) + file = rule.instance_variable_get(:@__file) + file = file[prefix.length..-1] if file.start_with?(prefix) + file + end + + 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, + } + + groups[file] ||= { + title: rule.instance_variable_get(:@__group_title), + controls: [], + } + groups[file][:controls].push(id) + end + end +end