class Inspec::Config

def self.__reset

clear the cached config
def self.__reset
  @cached_config = nil
end

def self.cached

being required to pass it around everywhere.
Use this to get a cached version of the config. This prevents you from
def self.cached
  @cached_config ||= {}
end

def self.cached=(cfg)

def self.cached=(cfg)
  @cached_config ||= cfg
end

def self.mock(opts = {})

This makes it easy to make a config with a mock backend.
def self.mock(opts = {})
  Inspec::Config.new({ backend: :mock }.merge(opts), StringIO.new("{}"))
end

def self.stdin_contents # rubocop: disable Lint/IneffectiveAccessModifier

rubocop: disable Lint/IneffectiveAccessModifier
during unit testing. Refs #3792
Don't cache other IO objects though, as we use several different StringIOs
When reading STDIN, read it once into a class variable and cache it.
def self.stdin_contents # rubocop: disable Lint/IneffectiveAccessModifier
  @stdin_content ||= STDIN.read
end

def _utc_determine_backend(credentials)

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

def _utc_find_credset_name(_credentials, transport_name)

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

def _utc_generic_credentials

fetch any info that applies to all transports (like sudo information)
def _utc_generic_credentials
  @final_options.select { |option, _value| GENERIC_CREDENTIALS.include?(option) }
end

def _utc_merge_credset(credentials, transport_name)

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

def _utc_merge_transport_options(credentials, transport_name)

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

def check_for_piped_config(cli_opts)

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

def config_file_cli_options

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

def config_file_reporter_options

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

def determine_cfg_path(cli_opts)

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

def diagnose

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

def fetch_plugin_config(plugin_name)

-----------------------------------------------------------------------#
Handling Plugin Data
-----------------------------------------------------------------------#
def fetch_plugin_config(plugin_name)
  Thor::CoreExt::HashWithIndifferentAccess.new(@plugin_cfg[plugin_name] || {})
end

def file_version

def file_version
  @cfg_file_contents["version"] || :legacy
end

def finalize_compliance_login(options)

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

def finalize_handle_sudo(options)

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

def finalize_options

-----------------------------------------------------------------------#
Finalization
-----------------------------------------------------------------------#
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

def finalize_parse_reporters(options) # rubocop:disable Metrics/AbcSize

rubocop:disable Metrics/AbcSize
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

def finalize_set_top_level_command(options)

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

def finalize_sort_results(options)

def finalize_sort_results(options)
  if options.key?("sort_results_by")
    validate_sort_results_by!(options["sort_results_by"])
  end
end

def initialize(cli_opts = {}, cfg_io = nil, command_name = nil)

This gets called when the first config is created.
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

def legacy_file?

def legacy_file?
  file_version == :legacy
end

def merge_options

-----------------------------------------------------------------------#
Merging Options
-----------------------------------------------------------------------#
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

def merge_plugin_config(plugin_name, additional_plugin_config)

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

def read_cfg_file_io(cfg_io)

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

def resolve_cfg_io(cli_opts, cfg_io)

Regardless of our situation, end up with a readable IO object
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

def set_plugin_config(plugin_name, plugin_config)

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

def telemetry_options

Returns:
  • (Hash) -
def telemetry_options
  final_options.select { |key, _| key.include?("telemetry") }
end

def unpack_train_credentials

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

def validate_config_file_contents!

-----------------------------------------------------------------------#
Validation
-----------------------------------------------------------------------#
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

def validate_plugins!

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

def validate_reporters!(reporters)

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)
  streaming_reporters = Inspec::Plugin::V2::Registry.instance\
    .find_activators(plugin_type: :streaming_reporter)\
    .map(&:activator_name).map(&:to_s)
  valid_types = rspec_built_in_formatters + inspec_reporters_that_are_not_yet_plugins + plugin_reporters + streaming_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

def validate_sort_results_by!(option_value)

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