class Inspec::Profile

def self.copy_deps_into_cache(file_provider, opts)

TODO: use source reader for Cache as well
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)

assumes that the profile was checked before
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)

Returns:
  • (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

Returns:
  • (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

Returns:
  • (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!

return info using uncached params
def info!
  info(load_params.dup)
end

def initialize(source_reader, options = {})

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)
  @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

Returns:
  • (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