class Inspec::InputRegistry

The InputRegistry's responsibilities include:

- maintaining a list of Input objects that are bound to profiles
- assisting in the lookup and creation of Inputs

Attributes

cache_inputs[RW]
inputs_by_profile[R]
plugins[R]
profile_aliases[R]

Public Class Methods

new() click to toggle source
# File lib/inspec/input_registry.rb, line 36
def initialize
  # Keyed on String profile_name => Hash of String input_name => Input object
  @inputs_by_profile = {}

  # this is a list of optional profile name overrides set in the inspec.yml
  @profile_aliases = {}

  # Upon creation, activate all input plugins
  activators = Inspec::Plugin::V2::Registry.instance.find_activators(plugin_type: :input)

  @plugins = activators.map do |activator|
    activator.activate!
    activator.implementation_class.new
  end

  # Activate caching for inputs by default
  @cache_inputs = true
end

Public Instance Methods

__reset() click to toggle source

Used in testing

# File lib/inspec/input_registry.rb, line 339
def __reset
  @inputs_by_profile = {}
  @profile_aliases = {}
end
bind_profile_inputs(profile_name, sources = {}) click to toggle source

This method is called by the Profile as soon as it has enough context to allow binding inputs to it.

# File lib/inspec/input_registry.rb, line 146
def bind_profile_inputs(profile_name, sources = {})
  inputs_by_profile[profile_name] ||= {}

  # In a more perfect world, we could let the core plugins choose
  # self-determine what to do; but as-is, the APIs that call this
  # are a bit over-constrained.
  bind_inputs_from_metadata(profile_name, sources[:profile_metadata])
  bind_inputs_from_input_files(profile_name, sources[:cli_input_files])
  bind_inputs_from_runner_api(profile_name, sources[:runner_api])
  bind_inputs_from_cli_args(profile_name, sources[:cli_input_arg])
end
find_or_register_input(input_name, profile_name, options = {}) click to toggle source
# File lib/inspec/input_registry.rb, line 82
def find_or_register_input(input_name, profile_name, options = {})
  input_name = input_name.to_s
  profile_name = profile_name.to_s
  options[:event].value = Thor::CoreExt::HashWithIndifferentAccess.new(options[:event].value) if options[:event]&.value.is_a?(Hash)

  if profile_alias?(profile_name) && !profile_aliases[profile_name].nil?
    alias_name = profile_name
    profile_name = profile_aliases[profile_name]
    handle_late_arriving_alias(alias_name, profile_name) if profile_known?(alias_name)
  end

  # Find or create the input
  inputs_by_profile[profile_name] ||= {}
  if inputs_by_profile[profile_name].key?(input_name) && cache_inputs
    inputs_by_profile[profile_name][input_name].update(options)
  else
    inputs_by_profile[profile_name][input_name] = Inspec::Input.new(input_name, options)
    poll_plugins_for_update(profile_name, input_name)
  end

  inputs_by_profile[profile_name][input_name]
end
handle_late_arriving_alias(alias_name, profile_name) click to toggle source

It is possible for a wrapper profile to create an input in metadata, referring to the child profile by an alias that has not yet been registered. The registry will then store the inputs under the alias, as if the alias were a true profile. If that happens and the child profile also mentions the input, we will need to move some things - all inputs should be stored under the true profile name, and no inputs should be stored under the alias.

# File lib/inspec/input_registry.rb, line 126
def handle_late_arriving_alias(alias_name, profile_name)
  inputs_by_profile[profile_name] ||= {}
  inputs_by_profile[alias_name].each do |input_name, input_from_alias|
    # Move the inpuut, or if it exists, merge events
    existing = inputs_by_profile[profile_name][input_name]
    if existing
      existing.events.concat(input_from_alias.events)
    else
      inputs_by_profile[profile_name][input_name] = input_from_alias
    end
  end
  # Finally, delete the (now copied-out) entry for the alias
  inputs_by_profile.delete(alias_name)
end
list_inputs_for_profile(profile) click to toggle source

Returns an Hash, name => Input that have actually been mentioned

# File lib/inspec/input_registry.rb, line 64
def list_inputs_for_profile(profile)
  inputs_by_profile[profile] = {} unless profile_known?(profile)
  inputs_by_profile[profile]
end
list_potential_input_names_for_profile(profile_name) click to toggle source

Returns an Array of input names. This includes input names that plugins may be able to fetch, but have not actually been mentioned in the control code.

# File lib/inspec/input_registry.rb, line 72
def list_potential_input_names_for_profile(profile_name)
  input_names_from_dsl = inputs_by_profile[profile_name].keys
  input_names_from_plugins = plugins.map { |plugin| plugin.list_inputs(profile_name) }
  (input_names_from_dsl + input_names_from_plugins).flatten.uniq
end
poll_plugins_for_update(profile_name, input_name) click to toggle source
# File lib/inspec/input_registry.rb, line 105
def poll_plugins_for_update(profile_name, input_name)
  plugins.each do |plugin|
    response = plugin.fetch(profile_name, input_name)
    evt = Inspec::Input::Event.new(
      action: :fetch,
      provider: plugin.class.plugin_name,
      priority: plugin.default_priority,
      hit: !response.nil?
    )
    evt.value = response unless response.nil?
    inputs_by_profile[profile_name][input_name].events << evt
  end
end
register_profile_alias(name, alias_name) click to toggle source
# File lib/inspec/input_registry.rb, line 59
def register_profile_alias(name, alias_name)
  @profile_aliases[name] = alias_name
end

Private Instance Methods

bind_inputs_from_cli_args(profile_name, input_list) click to toggle source
# File lib/inspec/input_registry.rb, line 160
def bind_inputs_from_cli_args(profile_name, input_list)
  # TODO: move this into a core plugin

  return if input_list.nil?
  return if input_list.empty?

  # These arrive as an array of "name=value" strings
  # If the user used a comma, we'll see unfortunately see it as "name=value," pairs
  input_list.each do |pair|
    unless pair.include?("=")
      if pair.end_with?(".yaml")
        raise ArgumentError, "ERROR: --input is used for individual input values, as --input name=value. Use --input-file to load a YAML file."
      else
        raise ArgumentError, "ERROR: An '=' is required when using --input. Usage: --input input_name1=input_value1 input2=value2"
      end
    end
    pair = pair.match(/(.*?)=(.*)/)
    input_name, input_value = pair[1], pair[2]
    input_value = parse_cli_input_value(input_name, input_value)
    evt = Inspec::Input::Event.new(
      value: input_value,
      provider: :cli,
      priority: 50
    )
    find_or_register_input(input_name, profile_name, event: evt)
  end
end
bind_inputs_from_input_files(profile_name, file_list) click to toggle source
# File lib/inspec/input_registry.rb, line 239
def bind_inputs_from_input_files(profile_name, file_list)
  # TODO: move this into a core plugin

  return if file_list.nil?
  return if file_list.empty?

  file_list.each do |path|
    validate_inputs_file_readability!(path)

    # TODO: drop this SecretsBackend stuff, will be handled by plugin system
    data = Inspec::SecretsBackend.resolve(path)
    if data.nil?
      raise Inspec::Exceptions::SecretsBackendNotFound,
            "Cannot find parser for inputs file '#{path}'. " \
            "Check to make sure file has the appropriate extension."
    end

    next if data.inputs.nil?

    data.inputs.each do |input_name, input_value|
      evt = Inspec::Input::Event.new(
        value: input_value,
        provider: :cli_files,
        priority: 40,
        file: path
        # TODO: any way we could get a line number?
      )
      find_or_register_input(input_name, profile_name, event: evt)
    end
  end
end
bind_inputs_from_metadata(profile_name, profile_metadata_obj) click to toggle source
# File lib/inspec/input_registry.rb, line 287
def bind_inputs_from_metadata(profile_name, profile_metadata_obj)
  # TODO: move this into a core plugin
  return if profile_metadata_obj.nil? # Metadata files are technically optional

  if profile_metadata_obj.params.key?(:inputs)
    raw_inputs = profile_metadata_obj.params[:inputs]
  elsif profile_metadata_obj.params.key?(:attributes)
    Inspec.deprecate(:attrs_rename_in_metadata, "Profile: '#{profile_name}'.")
    raw_inputs = profile_metadata_obj.params[:attributes]
  else
    return
  end

  unless raw_inputs.is_a?(Array)
    Inspec::Log.warn "Inputs must be defined as an Array in metadata files. Skipping definition from #{profile_name}."
    return
  end

  raw_inputs.each { |i| handle_raw_input_from_metadata(i, profile_name) }
end
bind_inputs_from_runner_api(profile_name, input_hash) click to toggle source
# File lib/inspec/input_registry.rb, line 219
def bind_inputs_from_runner_api(profile_name, input_hash)
  # TODO: move this into a core plugin

  return if input_hash.nil?
  return if input_hash.empty?

  # These arrive as a bare hash - values are raw values, not options
  input_hash.each do |input_name, input_value|
    loc = Inspec::Input::Event.probe_stack # TODO: likely modify this to look for a kitchen.yml, if that is realistic
    evt = Inspec::Input::Event.new(
      value: input_value,
      provider: :runner_api, # TODO: suss out if audit cookbook or kitchen-inspec or something unknown
      priority: 40,
      file: loc.path,
      line: loc.lineno
    )
    find_or_register_input(input_name, profile_name, event: evt)
  end
end
handle_raw_input_from_metadata(input_orig, profile_name) click to toggle source
# File lib/inspec/input_registry.rb, line 308
def handle_raw_input_from_metadata(input_orig, profile_name)
  input_options = input_orig.dup
  input_name = input_options.delete(:name)
  input_options[:provider] = :profile_metadata
  input_options[:file] = File.join(profile_name, "inspec.yml")
  input_options[:priority] ||= 30
  evt = Inspec::Input.infer_event(input_options)

  # Profile metadata may set inputs in other profiles by naming them.
  if input_options[:profile]
    profile_name = input_options[:profile] || profile_name
    # Override priority to force this to win.  Allow user to set their own priority.
    evt.priority = input_orig[:priority] || 35
  end
  find_or_register_input(
    input_name,
    profile_name,
    type: input_options[:type],
    required: input_options[:required],
    sensitive: input_options[:sensitive],
    pattern: input_options[:pattern],
    event: evt
  )
end
parse_cli_input_value(input_name, given_value) click to toggle source

Remove trailing commas, resolve type.

# File lib/inspec/input_registry.rb, line 189
def parse_cli_input_value(input_name, given_value)
  value = given_value.chomp(",") # Trim trailing comma if any
  case value
  when /^true|false$/i
    value = !!(value =~ /true/i)
  when /^-?\d+$/
    value = value.to_i
  when /^-?\d+\.\d+$/
    value = value.to_f
  when /^(\[|\{).*(\]|\})$/
    # Look for complex values and try to parse them.
    require "yaml"
    begin
      value = YAML.load(value)
    rescue Psych::SyntaxError => yaml_error
      # It could be that we just tried to run JSON through the YAML parser.
      require "json" unless defined?(JSON)
      begin
        value = JSON.parse(value)
      rescue JSON::ParserError => json_error
        msg = "Unparseable value '#{value}' for --input #{input_name}.\n"
        msg += "When treated as YAML, error: #{yaml_error.message}\n"
        msg += "When treated as JSON, error: #{json_error.message}"
        Inspec::Log.warn msg
      end
    end
  end
  value
end
validate_inputs_file_readability!(path) click to toggle source
# File lib/inspec/input_registry.rb, line 271
def validate_inputs_file_readability!(path)
  unless File.exist?(path)
    raise Inspec::Exceptions::InputsFileDoesNotExist,
          "Cannot find input file '#{path}'. " \
          "Check to make sure file exists."
  end

  unless File.readable?(path)
    raise Inspec::Exceptions::InputsFileNotReadable,
          "Cannot read input file '#{path}'. " \
          "Check to make sure file is readable."
  end

  true
end