class Inspec::Input

NOTE: due to namespacing, this reopens and extends the existing Inspec::Input. This should be under Inspec::Objects but that ship has sailed.

Constants

DEFAULT_PRIORITY_FOR_DSL_ATTRIBUTES

TODO: this is not used anywhere? If you call `input` in a control file, the input will receive this priority. You can override that with a :priority option.

DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER

If you somehow manage to initialize an Input outside of the DSL, AND you don't provide an Input::Event, this is the priority you get.

DEFAULT_PRIORITY_FOR_VALUE_SET

If you directly call value=, this is the priority assigned. This is the highest priority within InSpec core; though plugins are free to go higher.

VALID_TYPES

Validation types for input values

Attributes

description[R]
events[R]
identifier[R]
name[R]
pattern[R]
required[R]
sensitive[R]
title[R]
type[R]

Public Class Methods

infer_event(options) click to toggle source

We can determine a value:

  1. By event.value (preferred)

  2. By options

  3. By options (deprecated)

# File lib/inspec/input.rb, line 241
def self.infer_event(options)
  # Don't rely on this working; you really should be passing a proper Input::Event
  # with the context information you have.
  location = Input::Event.probe_stack
  event = Input::Event.new(
    action: :set,
    provider: options[:provider] || :unknown,
    priority: options[:priority] || Inspec::Input::DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER,
    file: location.path,
    line: location.lineno
  )

  if options.key?(:default)
    Inspec.deprecate(:attrs_value_replaces_default, "attribute name: '#{name}'")
    if options.key?(:value)
      Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
      options.delete(:default)
    else
      options[:value] = options.delete(:default)
    end
  end
  event.value = options[:value] if options.key?(:value)
  options[:event] = event
end
new(name, options = {}) click to toggle source
# File lib/inspec/input.rb, line 183
def initialize(name, options = {})
  @name = name
  @opts = options
  if @opts.key?(:default)
    Inspec.deprecate(:attrs_value_replaces_default, "input name: '#{name}'")
    if @opts.key?(:value)
      Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
      @opts.delete(:default)
    end
  end

  # Array of Input::Event objects.  These compete with one another to determine
  # the value of the input when value() is called, as well as providing a
  # debugging record of when and how the value changed.
  @events = []
  events.push make_creation_event(options)
  update(options)
end

Public Instance Methods

diagnostic_string() click to toggle source
# File lib/inspec/input.rb, line 207
def diagnostic_string
  "Input #{name}, with history:\n" +
    events.map(&:diagnostic_string).map { |line| "  #{line}" }.join("\n")
end
has_value?() click to toggle source
# File lib/inspec/input.rb, line 329
def has_value?
  !current_value(false).is_a? NO_VALUE_SET
end
ruby_var_identifier() click to toggle source
# File lib/inspec/objects/input.rb, line 32
def ruby_var_identifier
  identifier || "attr_" + name.downcase.strip.gsub(/\s+/, "-").gsub(/[^\w-]/, "")
end
set_events() click to toggle source

TODO: is this here just for testing?

# File lib/inspec/input.rb, line 203
def set_events
  events.select { |e| e.action == :set }
end
to_hash() click to toggle source
# File lib/inspec/input.rb, line 333
def to_hash
  as_hash = { name: name, options: {} }
  %i{description title identifier type required value sensitive pattern}.each do |field|
    val = send(field)
    next if val.nil?

    as_hash[:options][field] = val
  end
  as_hash
end
to_ruby() click to toggle source
# File lib/inspec/objects/input.rb, line 36
def to_ruby
  res = ["#{ruby_var_identifier} = attribute('#{name}',{"]
  res.push "  title: '#{title}'," unless title.to_s.empty?
  res.push "  value: #{value.inspect}," unless value.to_s.empty?
  # to_ruby may generate code that is to be used by older versions of inspec.
  # Anything older than 3.4 will not recognize the value: option, so
  # send the default: option as well. See #3759
  res.push "  default: #{value.inspect}," unless value.to_s.empty?
  res.push "  description: '#{description}'," unless description.to_s.empty?
  res.push "})"
  res.join("\n")
end
to_s() click to toggle source
# File lib/inspec/input.rb, line 348
def to_s
  "Input #{name} with value " + (sensitive ? "*** (senstive)" : "#{current_value}")
end
update(options) click to toggle source
# File lib/inspec/input.rb, line 216
def update(options)
  _update_set_metadata(options)
  normalize_type_restriction!
  normalize_pattern_restriction!

  # Values are set by passing events in; but we can also infer an event.
  if options.key?(:value) || options.key?(:default)
    if options.key?(:event)
      if options.key?(:value) || options.key?(:default)
        Inspec::Log.warn "Do not provide both an Event and a value as an option to attribute('#{name}') - using value from event"
      end
    else
      self.class.infer_event(options) # Sets options[:event]
    end
  end
  events << options[:event] if options.key? :event

  enforce_type_restriction!
  enforce_pattern_restriction!
end
value() click to toggle source
# File lib/inspec/input.rb, line 324
def value
  enforce_required_validation!
  current_value
end
value=(new_value, priority = DEFAULT_PRIORITY_FOR_VALUE_SET) click to toggle source
# File lib/inspec/input.rb, line 308
def value=(new_value, priority = DEFAULT_PRIORITY_FOR_VALUE_SET)
  # Inject a new Event with the new value.
  location = Event.probe_stack
  events << Event.new(
    action: :set,
    provider: :value_setter,
    priority: priority,
    value: new_value,
    file: location.path,
    line: location.lineno
  )

  enforce_type_restriction!
  enforce_pattern_restriction!
end

Private Instance Methods

_update_set_metadata(options) click to toggle source
# File lib/inspec/input.rb, line 268
def _update_set_metadata(options)
  # Basic metadata
  @title = options[:title] if options.key?(:title)
  @description = options[:description] if options.key?(:description)
  @required = options[:required] if options.key?(:required)
  @identifier = options[:identifier] if options.key?(:identifier) # TODO: determine if this is ever used
  @type = options[:type] if options.key?(:type)
  @sensitive = options[:sensitive] if options.key?(:sensitive)
  @pattern = options[:pattern] if options.key?(:pattern)
end
current_value(warn_on_missing = true) click to toggle source

Determine the current winning value, but don't validate it

# File lib/inspec/input.rb, line 290
def current_value(warn_on_missing = true)
  # Examine the events to determine highest-priority value. Tie-break
  # by using the last one set.
  events_that_set_a_value = events.select(&:value_has_been_set?)
  winning_priority = events_that_set_a_value.map(&:priority).max
  winning_events = events_that_set_a_value.select { |e| e.priority == winning_priority }
  winning_event = winning_events.last # Last for tie-break

  if winning_event.nil?
    # No value has been set - return special no value object
    NO_VALUE_SET.new(name, warn_on_missing)
  else
    winning_event.value # May still be nil
  end
end
enforce_pattern_restriction!() click to toggle source
# File lib/inspec/input.rb, line 418
def enforce_pattern_restriction!
  return unless pattern
  return unless has_value?

  string_value = current_value(false).to_s

  valid_pattern = string_value.match?(pattern)
  unless valid_pattern
    error = Inspec::Input::ValidationError.new
    error.input_name = @name
    error.input_value = string_value
    error.input_pattern = pattern
    raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to pattern '#{error.input_pattern}'."
  end
end
enforce_required_validation!() click to toggle source
# File lib/inspec/input.rb, line 358
def enforce_required_validation!
  return unless required
  # skip if we are not doing an exec call (archive/vendor/check)
  return unless Inspec::BaseCLI.inspec_cli_command == :exec

  proposed_value = current_value(false)
  if proposed_value.nil? || proposed_value.is_a?(NO_VALUE_SET)
    error = Inspec::Input::RequiredError.new
    error.input_name = name
    raise error, "Input '#{error.input_name}' is required and does not have a value."
  end
end
enforce_type_restriction!() click to toggle source
# File lib/inspec/input.rb, line 371
def enforce_type_restriction! # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  return unless type
  return unless has_value?

  type_req = type
  return if type_req == "Any"

  proposed_value = current_value(false)

  invalid_type = false
  if type_req == "Regexp"
    invalid_type = true unless valid_regexp?(proposed_value)
  elsif type_req == "Numeric"
    invalid_type = true unless valid_numeric?(proposed_value)
  elsif type_req == "Boolean"
    invalid_type = true unless [true, false].include?(proposed_value)
  elsif proposed_value.is_a?(Module.const_get(type_req)) == false
    # TODO: why is this case here?
    invalid_type = true
  end

  if invalid_type == true
    error = Inspec::Input::ValidationError.new
    error.input_name = @name
    error.input_value = proposed_value
    error.input_type = type_req
    raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to type '#{error.input_type}'."
  end
end
make_creation_event(options) click to toggle source
# File lib/inspec/input.rb, line 279
def make_creation_event(options)
  loc = options[:location] || Event.probe_stack
  Input::Event.new(
    action: :create,
    provider: options[:provider],
    file: loc.path,
    line: loc.lineno
  )
end
normalize_pattern_restriction!() click to toggle source
# File lib/inspec/input.rb, line 434
def normalize_pattern_restriction!
  return unless pattern

  unless valid_regexp?(pattern)
    error = Inspec::Input::PatternError.new
    error.input_pattern = pattern
    raise error, "Pattern '#{error.input_pattern}' is not a valid regex pattern."
  end
  @pattern = pattern
end
normalize_type_restriction!() click to toggle source
# File lib/inspec/input.rb, line 401
def normalize_type_restriction!
  return unless type

  type_req = type.capitalize
  abbreviations = {
    "Num" => "Numeric",
    "Regex" => "Regexp",
  }
  type_req = abbreviations[type_req] if abbreviations.key?(type_req)
  unless VALID_TYPES.include?(type_req)
    error = Inspec::Input::TypeError.new
    error.input_type = type_req
    raise error, "Type '#{error.input_type}' is not a valid input type."
  end
  @type = type_req
end
valid_numeric?(value) click to toggle source
# File lib/inspec/input.rb, line 445
def valid_numeric?(value)
  Float(value)
  true
rescue
  false
end
valid_regexp?(value) click to toggle source
# File lib/inspec/input.rb, line 452
def valid_regexp?(value)
  # check for invalid regex syntax
  Regexp.new(value)
  true
rescue
  false
end