class Inspec::Config

Constants

GENERIC_CREDENTIALS

These are options that apply to any transport

KNOWN_VERSIONS

Attributes

final_options[R]

Public Class Methods

__reset() click to toggle source

clear the cached config

# File lib/inspec/config.rb, line 151
def self.__reset
  @cached_config = nil
end
cached() click to toggle source

Use this to get a cached version of the config. This prevents you from being required to pass it around everywhere.

# File lib/inspec/config.rb, line 46
def self.cached
  @cached_config ||= {}
end
cached=(cfg) click to toggle source
# File lib/inspec/config.rb, line 50
def self.cached=(cfg)
  @cached_config ||= cfg
end
mock(opts = {}) click to toggle source

This makes it easy to make a config with a mock backend.

# File lib/inspec/config.rb, line 40
def self.mock(opts = {})
  Inspec::Config.new({ backend: :mock }.merge(opts), StringIO.new("{}"))
end
new(cli_opts = {}, cfg_io = nil, command_name = nil) click to toggle source

This gets called when the first config is created.

# File lib/inspec/config.rb, line 55
def initialize(cli_opts = {}, cfg_io = nil, command_name = nil)
  @command_name = command_name || (ARGV.empty? ? nil : ARGV[0].to_sym)
  @defaults = Defaults.for_command(@command_name)
  @plugin_cfg = {}

  @cli_opts = cli_opts.dup
  cfg_io = resolve_cfg_io(@cli_opts, cfg_io)
  @cfg_file_contents = read_cfg_file_io(cfg_io)

  @merged_options = merge_options
  @final_options = finalize_options
  self.class.cached = self
end

Private Class Methods

stdin_contents() click to toggle source

When reading STDIN, read it once into a class variable and cache it. Don't cache other IO objects though, as we use several different StringIOs during unit testing. Refs #3792

# File lib/inspec/config.rb, line 274
def self.stdin_contents # rubocop: disable Lint/IneffectiveAccessModifier
  @stdin_content ||= STDIN.read
end

Public Instance Methods

diagnose() click to toggle source
# File lib/inspec/config.rb, line 69
def diagnose
  return unless self[:diagnose]

  puts "InSpec version: #{Inspec::VERSION}"
  puts "Train version: #{Train::VERSION}"
  puts "Command line configuration:"
  pp @cli_opts
  puts "JSON configuration file:"
  pp @cfg_file_contents
  puts "Merged configuration:"
  pp @merged_options
  puts
end
fetch_plugin_config(plugin_name) click to toggle source
# File lib/inspec/config.rb, line 133
def fetch_plugin_config(plugin_name)
  Thor::CoreExt::HashWithIndifferentAccess.new(@plugin_cfg[plugin_name] || {})
end
merge_plugin_config(plugin_name, additional_plugin_config) click to toggle source
# File lib/inspec/config.rb, line 143
def merge_plugin_config(plugin_name, additional_plugin_config)
  plugin_name = plugin_name.to_s unless plugin_name.is_a? String

  @plugin_cfg[plugin_name] = {} if @plugin_cfg[plugin_name].nil?
  @plugin_cfg[plugin_name].merge!(additional_plugin_config)
end
set_plugin_config(plugin_name, plugin_config) click to toggle source
# File lib/inspec/config.rb, line 137
def set_plugin_config(plugin_name, plugin_config)
  plugin_name = plugin_name.to_s unless plugin_name.is_a? String

  @plugin_cfg[plugin_name] = plugin_config
end
telemetry_options() click to toggle source

return all telemetry options from config @return [Hash]

# File lib/inspec/config.rb, line 85
def telemetry_options
  final_options.select { |key, _| key.include?("telemetry") }
end
unpack_train_credentials() click to toggle source

Returns a Hash with Symbol keys as follows:

backend: machine name of the Train transport needed
If present, any of the GENERIC_CREDENTIALS.
All other keys are specific to the backend.

The credentials are gleaned from:

* the Train transport defaults. Train handles this on transport creation,
    so this method doesn't load defaults.
* individual InSpec CLI options (which in many cases may have the
    transport name prefixed, which is stripped before being added
    to the creds hash)
* the --target CLI option, which is interpreted:
   - as a transport://credset format, which looks up the creds in
     the config file in the credentials section
   - as an arbitrary URI, which is parsed by Train.unpack_target_from_uri
# File lib/inspec/config.rb, line 109
def unpack_train_credentials
  # Internally, use indifferent access while we build the creds
  credentials = Thor::CoreExt::HashWithIndifferentAccess.new({})

  # Helper methods prefixed with _utc_ (Unpack Train Credentials)

  credentials.merge!(_utc_generic_credentials)

  _utc_determine_backend(credentials)
  transport_name = credentials[:backend].to_s

  _utc_merge_credset(credentials, transport_name)
  _utc_merge_transport_options(credentials, transport_name)

  # Convert to all-Symbol keys
  credentials.each_with_object({}) do |(option, value), creds|
    creds[option.to_sym] = value
    creds
  end
end

Private Instance Methods

_utc_determine_backend(credentials) click to toggle source
# File lib/inspec/config.rb, line 182
def _utc_determine_backend(credentials)
  return if credentials.key?(:backend)

  # Default to local
  unless @final_options.key?(:target)
    credentials[:backend] = "local"
    return
  end

  # Look into target
  %r{^(?<transport_name>[a-z_\-0-9]+)://.*$} =~ final_options[:target]
  unless transport_name
    raise ArgumentError, "Could not recognize a backend from the target #{final_options[:target]} - use a URI format with the backend name as the URI schema.  Example: 'ssh://somehost.com' or 'transport://credset' or 'transport://' if credentials are provided outside of InSpec."
  end

  credentials[:backend] = transport_name.to_s # these are indeed stored in Train as Strings.
end
_utc_find_credset_name(_credentials, transport_name) click to toggle source
# File lib/inspec/config.rb, line 221
def _utc_find_credset_name(_credentials, transport_name)
  return unless final_options[:target]

  match = final_options[:target].match(%r{^#{transport_name}://(?<credset_name>[\w\-]+)$})
  match ? match[:credset_name] : nil
end
_utc_generic_credentials() click to toggle source

fetch any info that applies to all transports (like sudo information)

# File lib/inspec/config.rb, line 178
def _utc_generic_credentials
  @final_options.select { |option, _value| GENERIC_CREDENTIALS.include?(option) }
end
_utc_merge_credset(credentials, transport_name) click to toggle source
# File lib/inspec/config.rb, line 200
def _utc_merge_credset(credentials, transport_name)
  # Look for Config File credentials/transport_name/credset
  credset_name = _utc_find_credset_name(credentials, transport_name)

  if credset_name
    credset = @cfg_file_contents.dig("credentials", transport_name, credset_name)
    if credset
      credentials.merge!(credset)
    else
      # OK, we had a target that looked like transport://something
      # But we don't know what that something is - there was no
      # matching credset with it.  Let train parse it.
      credentials.merge!(Train.unpack_target_from_uri(final_options[:target]))
    end
  elsif final_options.key?(:target)
    # Not sure what target looked like at all!
    # Let train parse it.
    credentials.merge!(Train.unpack_target_from_uri(final_options[:target]))
  end
end
_utc_merge_transport_options(credentials, transport_name) click to toggle source
# File lib/inspec/config.rb, line 157
def _utc_merge_transport_options(credentials, transport_name)
  # Ask Train for the names of the transport options
  transport_options = Train.options(transport_name).keys.map(&:to_s)

  # If there are any options with those (unprefixed) names, merge them in.
  unprefixed_transport_options = final_options.select do |option_name, _value|
    transport_options.include? option_name # e.g., 'host'
  end
  credentials.merge!(unprefixed_transport_options)

  # If there are any prefixed options, merge them in, stripping the prefix.
  transport_prefix = transport_name.downcase.tr("-", "_") + "_"
  transport_options.each do |bare_option_name|
    prefixed_option_name = transport_prefix + bare_option_name.to_s
    if final_options.key?(prefixed_option_name)
      credentials[bare_option_name.to_s] = final_options[prefixed_option_name]
    end
  end
end
check_for_piped_config(cli_opts) click to toggle source
# File lib/inspec/config.rb, line 245
def check_for_piped_config(cli_opts)
  cli_opt = cli_opts[:config] || cli_opts[:json_config]
  Inspec.deprecate(:cli_option_json_config) if cli_opts.key?(:json_config)

  return nil unless cli_opt
  return nil unless cli_opt == "-"

  # This warning is here so that if a user invokes inspec with --config=-,
  # they will have an explanation for why it appears to hang.
  Inspec::Log.warn "Reading JSON config from standard input" if STDIN.tty?
  STDIN
end
config_file_cli_options() click to toggle source
# File lib/inspec/config.rb, line 297
def config_file_cli_options
  if legacy_file?
    # Assume everything in the file is a CLI option
    @cfg_file_contents
  else
    @cfg_file_contents["cli_options"] || {}
  end
end
config_file_reporter_options() click to toggle source
# File lib/inspec/config.rb, line 306
def config_file_reporter_options
  # This is assumed to be top-level in both legacy and 1.1.
  # Technically, you could sneak it in the 1.1 cli opts area.
  @cfg_file_contents.key?("reporter") ? { "reporter" => @cfg_file_contents["reporter"] } : {}
end
determine_cfg_path(cli_opts) click to toggle source
# File lib/inspec/config.rb, line 258
def determine_cfg_path(cli_opts)
  path = cli_opts[:config] || cli_opts[:json_config]
  Inspec.deprecate(:cli_option_json_config) if cli_opts.key?(:json_config)

  if path.nil?
    default_path = File.join(Inspec.config_dir, "config.json")
    path = default_path if File.exist?(default_path)
  elsif !File.exist?(path)
    raise ArgumentError, "Could not read configuration file at #{path}"
  end
  path
end
file_version() click to toggle source
# File lib/inspec/config.rb, line 289
def file_version
  @cfg_file_contents["version"] || :legacy
end
finalize_compliance_login(options) click to toggle source
# File lib/inspec/config.rb, line 529
def finalize_compliance_login(options)
  # check for compliance settings
  # This is always a hash, comes from config file, not CLI opts
  if options.key?("compliance")
    require "plugins/inspec-compliance/lib/inspec-compliance/api"
    InspecPlugins::Compliance::API.login(options["compliance"])
  end
end
finalize_handle_sudo(options) click to toggle source
# File lib/inspec/config.rb, line 509
def finalize_handle_sudo(options)
  # Due to limitations in Thor it is not possible to set an argument to be
  # both optional and its value to be mandatory. E.g. the user supplying
  # the --password argument is optional and not always required, but
  # whenever it is used, it requires a value. Handle options that were
  # defined in such a way and require a value here:
  %w{password sudo-password}.each do |option_name|
    snake_case_option_name = option_name.tr("-", "_").to_s
    next unless options[snake_case_option_name] == -1 # Thor sets -1 for missing value - see #1918

    raise ArgumentError, "Please provide a value for --#{option_name}. For example: --#{option_name}=hello."
  end

  # Infer `--sudo` if using `--sudo-password` without `--sudo`
  if options["sudo_password"] && !options["sudo"]
    options["sudo"] = true
    Inspec::Log.warn "`--sudo-password` used without `--sudo`. Adding `--sudo`."
  end
end
finalize_options() click to toggle source
# File lib/inspec/config.rb, line 456
def finalize_options
  options = @merged_options.dup

  finalize_set_top_level_command(options)
  finalize_parse_reporters(options)
  finalize_handle_sudo(options)
  finalize_compliance_login(options)
  finalize_sort_results(options)

  Thor::CoreExt::HashWithIndifferentAccess.new(options)
end
finalize_parse_reporters(options) click to toggle source
# File lib/inspec/config.rb, line 474
def finalize_parse_reporters(options) # rubocop:disable Metrics/AbcSize
  # default to cli report for ad-hoc runners
  options["reporter"] = ["cli"] if options["reporter"].nil?

  # parse out cli to proper report format
  if options["reporter"].is_a?(Array)
    reports = {}
    options["reporter"].each do |report|
      reporter_name, destination = report.split(":", 2)
      if destination.nil? || destination.strip == "-"
        reports[reporter_name] = { "stdout" => true }
      else
        reports[reporter_name] = {
          "file" => destination,
          "stdout" => false,
        }
        reports[reporter_name]["target_id"] = options["target_id"] if options["target_id"]
      end
    end
    options["reporter"] = reports
  end

  # add in stdout if not specified
  if options["reporter"].is_a?(Hash)
    options["reporter"].each do |reporter_name, config|
      options["reporter"][reporter_name] = {} if config.nil?
      options["reporter"][reporter_name]["stdout"] = true if options["reporter"][reporter_name].empty?
      options["reporter"][reporter_name]["target_id"] = options["target_id"] if options["target_id"]
    end
  end

  validate_reporters!(options["reporter"])
  options
end
finalize_set_top_level_command(options) click to toggle source
# File lib/inspec/config.rb, line 468
def finalize_set_top_level_command(options)
  options[:type] = @command_name
  require "inspec/base_cli"
  Inspec::BaseCLI.inspec_cli_command = @command_name # TODO: move to a more relevant location
end
finalize_sort_results(options) click to toggle source
# File lib/inspec/config.rb, line 538
def finalize_sort_results(options)
  if options.key?("sort_results_by")
    validate_sort_results_by!(options["sort_results_by"])
  end
end
legacy_file?() click to toggle source
# File lib/inspec/config.rb, line 293
def legacy_file?
  file_version == :legacy
end
merge_options() click to toggle source
# File lib/inspec/config.rb, line 436
def merge_options
  options = Thor::CoreExt::HashWithIndifferentAccess.new({})

  # Lowest precedence: default, which may vary by command
  options.merge!(@defaults)

  # Middle precedence: merge in any CLI options defined from the config file
  options.merge!(config_file_cli_options)
  # Reporter options may be defined top-level.
  options.merge!(config_file_reporter_options)

  # Highest precedence: merge in any options defined via the CLI
  options.merge!(@cli_opts)

  options
end
read_cfg_file_io(cfg_io) click to toggle source
# File lib/inspec/config.rb, line 278
def read_cfg_file_io(cfg_io)
  contents = cfg_io == STDIN ? self.class.stdin_contents : cfg_io.read
  begin
    @cfg_file_contents = JSON.parse(contents)
    validate_config_file_contents!
  rescue JSON::ParserError => e
    raise Inspec::ConfigError::MalformedJson, "Failed to load JSON configuration: #{e}\nConfig was: #{contents}"
  end
  @cfg_file_contents
end
resolve_cfg_io(cli_opts, cfg_io) click to toggle source

Regardless of our situation, end up with a readable IO object

# File lib/inspec/config.rb, line 233
def resolve_cfg_io(cli_opts, cfg_io)
  raise(ArgumentError, "Inspec::Config must use an IO to read from") if cfg_io && !cfg_io.respond_to?(:read)

  cfg_io ||= check_for_piped_config(cli_opts)
  return cfg_io if cfg_io

  path = determine_cfg_path(cli_opts)

  ver = KNOWN_VERSIONS.max
  path ? File.open(path) : StringIO.new({ "version" => ver }.to_json)
end
validate_config_file_contents!() click to toggle source
# File lib/inspec/config.rb, line 315
def validate_config_file_contents!
  version = @cfg_file_contents["version"]

  # Assume legacy format, which is unconstrained
  return unless version

  unless KNOWN_VERSIONS.include?(version)
    raise Inspec::ConfigError::Invalid, "Unsupported config file version '#{version}' - currently supported versions: #{KNOWN_VERSIONS.join(",")}"
  end

  # Use Gem::Version for comparision operators
  cfg_version = Gem::Version.new(version)
  version_1_2 = Gem::Version.new("1.2")

  # TODO: proper schema version loading and validation
  valid_fields = %w{version cli_options credentials compliance reporter}.sort
  valid_fields << "plugins" if cfg_version >= version_1_2
  @cfg_file_contents.keys.each do |seen_field|
    unless valid_fields.include?(seen_field)
      raise Inspec::ConfigError::Invalid, "Unrecognized top-level configuration field #{seen_field}.  Recognized fields: #{valid_fields.join(", ")}"
    end
  end

  validate_plugins! if cfg_version >= version_1_2
end
validate_plugins!() click to toggle source
# File lib/inspec/config.rb, line 398
def validate_plugins!
  return unless @cfg_file_contents.key? "plugins"

  data = @cfg_file_contents["plugins"]
  unless data.is_a?(Hash)
    raise Inspec::ConfigError::Invalid, "The 'plugin' field in your config file must be a hash (key-value list), not an array."
  end

  data.each do |plugin_name, plugin_settings|
    # Enforce that every key is a valid inspec or train plugin name
    unless valid_plugin_name?(plugin_name)
      raise Inspec::ConfigError::Invalid, "Plugin settings should ne named after the the InSpec or Train plugin. Valid names must begin with inspec- or train-, not '#{plugin_name}' "
    end

    # Enforce that every entry is hash-valued
    unless plugin_settings.is_a?(Hash)
      raise Inspec::ConfigError::Invalid, "The plugin settings for '#{plugin_name}' in your config file should be a Hash (key-value list)."
    end
  end

  @plugin_cfg = data
end
validate_reporters!(reporters) click to toggle source
# File lib/inspec/config.rb, line 341
def validate_reporters!(reporters)
  return if reporters.nil?

  # These "reporters" are actually RSpec Formatters.
  # json-rspec is our alias for RSpec's json formatter.
  rspec_built_in_formatters = %w{
    documentation
    html
    json-rspec
    progress
  }

  # These are true reporters, but have not been migrated to be plugins yet.
  # Tracked on https://github.com/inspec/inspec/issues/3667
  inspec_reporters_that_are_not_yet_plugins = %w{
    automate
    cli
    json
    json-automate
    yaml
  }

  # Additional reporters may be loaded via plugins. They will have already been detected at
  # this point (see v2_loader.load_all in cli.rb) but they may not (and need not) be
  # activated at this point. We only care about their existance and their name, for validation's sake.
  plugin_reporters = Inspec::Plugin::V2::Registry.instance\
    .find_activators(plugin_type: :reporter)\
    .map(&:activator_name).map(&:to_s)

  valid_types = rspec_built_in_formatters + inspec_reporters_that_are_not_yet_plugins + plugin_reporters

  reporters.each do |reporter_name, reporter_config|
    raise NotImplementedError, "'#{reporter_name}' is not a valid reporter type." unless valid_types.include?(reporter_name)

    next unless reporter_name == "automate"

    %w{token url}.each do |option|
      raise Inspec::ReporterError, "You must specify a automate #{option} via the config file." if reporter_config[option].nil?
    end
  end

  # check to make sure we are only reporting one type to stdout
  stdout_reporters = 0
  reporters.each_value do |reporter_config|
    stdout_reporters += 1 if reporter_config["stdout"] == true
  end

  raise ArgumentError, "The option --reporter can only have a single report outputting to stdout." if stdout_reporters > 1

  # reporter_message_truncation needs to either be the string "ALL", an Integer, or a string representing an integer
  if (truncation = @merged_options["reporter_message_truncation"])
    unless truncation == "ALL" || truncation.is_a?(Integer) || truncation.to_i.to_s == truncation
      raise ArgumentError, "reporter_message_truncation is set to #{truncation}. It must be set to an integer value or ALL to indicate no truncation."
    end
  end
end
validate_sort_results_by!(option_value) click to toggle source
# File lib/inspec/config.rb, line 421
def validate_sort_results_by!(option_value)
  expected = %w{
    none
    control
    file
    random
  }
  return if expected.include? option_value

  raise Inspec::ConfigError::Invalid, "--sort-results-by must be one of #{expected.join(", ")}"
end