mirror of
https://github.com/inspec/inspec
synced 2024-09-21 06:51:56 +00:00
Internal overhaul of Inputs system (#3875)
Internal overhaul of Inputs system
This commit is contained in:
commit
535d7ae6a9
43 changed files with 1295 additions and 816 deletions
|
@ -26,8 +26,23 @@ module Inspec
|
|||
with_resource_dsl resources_dsl
|
||||
|
||||
# allow attributes to be accessed within control blocks
|
||||
define_method :attribute do |name|
|
||||
Inspec::InputRegistry.find_input(name, profile_id).value
|
||||
# TODO: deprecate name, use input()
|
||||
define_method :attribute do |input_name, options = {}|
|
||||
if options.empty?
|
||||
# Simply an access, no event here
|
||||
Inspec::InputRegistry.find_or_register_input(input_name, profile_id).value
|
||||
else
|
||||
options[:priority] = 20
|
||||
options[:provider] = :inline_control_code
|
||||
evt = Inspec::Input.infer_event(options)
|
||||
Inspec::InputRegistry.find_or_register_input(input_name, profile_name, event: evt).value
|
||||
end
|
||||
end
|
||||
|
||||
# Find the Input object, but don't collapse to a value.
|
||||
# Will return nil on a miss.
|
||||
define_method :input_object do |input_name|
|
||||
Inspec::InputRegistry.find_or_register_input(input_name, profile_id)
|
||||
end
|
||||
|
||||
# Support for Control DSL plugins.
|
||||
|
@ -168,14 +183,25 @@ module Inspec
|
|||
end
|
||||
|
||||
# method for inputs; import input handling
|
||||
define_method :attribute do |name, options = nil|
|
||||
if options.nil?
|
||||
Inspec::InputRegistry.find_input(name, profile_id).value
|
||||
# TODO: deprecate name, use input()
|
||||
define_method :attribute do |input_name, options = {}|
|
||||
if options.empty?
|
||||
# Simply an access, no event here
|
||||
Inspec::InputRegistry.find_or_register_input(input_name, profile_id).value
|
||||
else
|
||||
profile_context_owner.register_input(name, options)
|
||||
options[:priority] = 20
|
||||
options[:provider] = :inline_control_code
|
||||
evt = Inspec::Input.infer_event(options)
|
||||
Inspec::InputRegistry.find_or_register_input(input_name, profile_name, event: evt).value
|
||||
end
|
||||
end
|
||||
|
||||
# Find the Input object, but don't collapse to a value.
|
||||
# Will return nil on a miss.
|
||||
define_method :input_object do |input_name|
|
||||
Inspec::InputRegistry.find_or_register_input(input_name, profile_id)
|
||||
end
|
||||
|
||||
define_method :skip_control do |id|
|
||||
profile_context_owner.unregister_rule(id)
|
||||
end
|
||||
|
|
|
@ -118,6 +118,7 @@ module Inspec
|
|||
return @profile unless @profile.nil?
|
||||
opts = @opts.dup
|
||||
opts[:backend] = @backend
|
||||
opts[:runner_conf] = Inspec::Config.cached
|
||||
if !@dependencies.nil? && !@dependencies.empty?
|
||||
opts[:dependencies] = Inspec::DependencySet.from_array(@dependencies, @cwd, @cache, @backend)
|
||||
end
|
||||
|
|
|
@ -23,6 +23,7 @@ module Inspec
|
|||
# implementation of the fetcher being used.
|
||||
#
|
||||
class Resolver
|
||||
# Here deps is an Array of Hashes
|
||||
def self.resolve(dependencies, cache, working_dir, backend)
|
||||
reqs = dependencies.map do |dep|
|
||||
req = Inspec::Requirement.from_metadata(dep, cache, cwd: working_dir, backend: backend)
|
||||
|
@ -47,6 +48,7 @@ module Inspec
|
|||
end
|
||||
end
|
||||
|
||||
# Here deps is an Array of Inspec::Requirement
|
||||
def resolve(deps, top_level = true, seen_items = {}, path_string = '') # rubocop:disable Metrics/AbcSize
|
||||
graph = {}
|
||||
if top_level
|
||||
|
|
|
@ -79,7 +79,7 @@ module Inspec::DSL
|
|||
|
||||
def self.filter_included_controls(context, profile, &block)
|
||||
mock = Inspec::Backend.create(Inspec::Config.mock)
|
||||
include_ctx = Inspec::ProfileContext.for_profile(profile, mock, {})
|
||||
include_ctx = Inspec::ProfileContext.for_profile(profile, mock)
|
||||
include_ctx.load(block) if block_given?
|
||||
# remove all rules that were not registered
|
||||
context.all_rules.each do |r|
|
||||
|
|
|
@ -1,83 +1,224 @@
|
|||
require 'forwardable'
|
||||
require 'singleton'
|
||||
require 'inspec/objects/input'
|
||||
require 'inspec/secrets'
|
||||
require 'inspec/exceptions'
|
||||
|
||||
module Inspec
|
||||
# The InputRegistry's responsibilities include:
|
||||
# - maintaining a list of Input objects that are bound to profiles
|
||||
# - assisting in the lookup and creation of Inputs
|
||||
class InputRegistry
|
||||
include Singleton
|
||||
extend Forwardable
|
||||
|
||||
attr_reader :list
|
||||
def_delegator :list, :each
|
||||
def_delegator :list, :[]
|
||||
def_delegator :list, :key?, :profile_exist?
|
||||
def_delegator :list, :select
|
||||
|
||||
# These self methods are convenience methods so you dont always
|
||||
# have to specify instance when calling the registry
|
||||
def self.find_input(name, profile)
|
||||
instance.find_input(name, profile)
|
||||
end
|
||||
|
||||
def self.register_input(name, profile, options = {})
|
||||
instance.register_input(name, profile, options)
|
||||
end
|
||||
|
||||
def self.register_profile_alias(name, alias_name)
|
||||
instance.register_profile_alias(name, alias_name)
|
||||
end
|
||||
|
||||
def self.list_inputs_for_profile(profile)
|
||||
instance.list_inputs_for_profile(profile)
|
||||
end
|
||||
attr_reader :inputs_by_profile, :profile_aliases
|
||||
def_delegator :inputs_by_profile, :each
|
||||
def_delegator :inputs_by_profile, :[]
|
||||
def_delegator :inputs_by_profile, :key?, :profile_known?
|
||||
def_delegator :inputs_by_profile, :select
|
||||
def_delegator :profile_aliases, :key?, :profile_alias?
|
||||
|
||||
def initialize
|
||||
# this is a collection of profiles which have a value of input objects
|
||||
@list = {}
|
||||
# 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 = {}
|
||||
end
|
||||
|
||||
def find_input(name, profile)
|
||||
profile = @profile_aliases[profile] if !profile_exist?(profile) && @profile_aliases[profile]
|
||||
unless profile_exist?(profile)
|
||||
error = Inspec::InputRegistry::ProfileLookupError.new
|
||||
error.profile_name = profile
|
||||
raise error, "Profile '#{error.profile_name}' does not have any inputs"
|
||||
end
|
||||
|
||||
unless list[profile].key?(name)
|
||||
error = Inspec::InputRegistry::InputLookupError.new
|
||||
error.input_name = name
|
||||
error.profile_name = profile
|
||||
raise error, "Profile '#{error.profile_name}' does not have an input with name '#{error.input_name}'"
|
||||
end
|
||||
list[profile][name]
|
||||
end
|
||||
|
||||
def register_input(name, profile, options = {})
|
||||
# check for a profile override name
|
||||
if profile_exist?(profile) && list[profile][name] && options.empty?
|
||||
list[profile][name]
|
||||
else
|
||||
list[profile] = {} unless profile_exist?(profile)
|
||||
list[profile][name] = Inspec::Input.new(name, options)
|
||||
end
|
||||
end
|
||||
#-------------------------------------------------------------#
|
||||
# Support for Profiles
|
||||
#-------------------------------------------------------------#
|
||||
|
||||
def register_profile_alias(name, alias_name)
|
||||
@profile_aliases[name] = alias_name
|
||||
end
|
||||
|
||||
def list_inputs_for_profile(profile)
|
||||
list[profile] = {} unless profile_exist?(profile)
|
||||
list[profile]
|
||||
inputs_by_profile[profile] = {} unless profile_known?(profile)
|
||||
inputs_by_profile[profile]
|
||||
end
|
||||
|
||||
#-------------------------------------------------------------#
|
||||
# Support for Individual Inputs
|
||||
#-------------------------------------------------------------#
|
||||
|
||||
def find_or_register_input(input_name, profile_name, options = {})
|
||||
if profile_alias?(profile_name)
|
||||
alias_name = profile_name
|
||||
profile_name = profile_aliases[profile_name]
|
||||
handle_late_arriving_alias(alias_name, profile_name) if profile_known?(alias_name)
|
||||
end
|
||||
|
||||
inputs_by_profile[profile_name] ||= {}
|
||||
if inputs_by_profile[profile_name].key?(input_name)
|
||||
inputs_by_profile[profile_name][input_name].update(options)
|
||||
else
|
||||
inputs_by_profile[profile_name][input_name] = Inspec::Input.new(input_name, options)
|
||||
end
|
||||
|
||||
inputs_by_profile[profile_name][input_name]
|
||||
end
|
||||
|
||||
# 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.
|
||||
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
|
||||
#-------------------------------------------------------------#
|
||||
# Support for Binding Inputs
|
||||
#-------------------------------------------------------------#
|
||||
|
||||
# This method is called by the Profile as soon as it has
|
||||
# enough context to allow binding inputs to it.
|
||||
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])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def bind_inputs_from_metadata(profile_name, profile_metadata_obj)
|
||||
# TODO: move this into a core plugin
|
||||
# TODO: add deprecation stuff
|
||||
return if profile_metadata_obj.nil? # Metadata files are technically optional
|
||||
|
||||
if profile_metadata_obj.params.key?(:attributes) && profile_metadata_obj.params[:attributes].is_a?(Array)
|
||||
profile_metadata_obj.params[:attributes].each do |input_orig|
|
||||
input_options = input_orig.dup
|
||||
input_name = input_options.delete(:name)
|
||||
input_options.merge!({ priority: 30, provider: :profile_metadata, file: File.join(profile_name, 'inspec.yml') })
|
||||
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],
|
||||
event: evt)
|
||||
end
|
||||
elsif profile_metadata_obj.params.key?(:attributes)
|
||||
Inspec::Log.warn 'Inputs must be defined as an Array. Skipping current definition.'
|
||||
end
|
||||
end
|
||||
|
||||
#-------------------------------------------------------------#
|
||||
# Other Support
|
||||
#-------------------------------------------------------------#
|
||||
public
|
||||
|
||||
# Used in testing
|
||||
def __reset
|
||||
@list = {}
|
||||
@inputs_by_profile = {}
|
||||
@profile_aliases = {}
|
||||
end
|
||||
|
||||
# These class methods are convenience methods so you don't always
|
||||
# have to call #instance when calling the registry
|
||||
[
|
||||
:find_or_register_input,
|
||||
:register_profile_alias,
|
||||
:list_inputs_for_profile,
|
||||
:bind_profile_inputs,
|
||||
].each do |meth|
|
||||
define_singleton_method(meth) do |*args|
|
||||
instance.send(meth, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,73 @@ end
|
|||
|
||||
module Inspec
|
||||
class Input
|
||||
#===========================================================================#
|
||||
# Class Input::Event
|
||||
#===========================================================================#
|
||||
|
||||
# Information about how the input obtained its value.
|
||||
# Each time it changes, an Input::Event is added to the #events array.
|
||||
class Event
|
||||
EVENT_PROPERTIES = [
|
||||
:action, # :create, :set, :fetch
|
||||
:provider, # Name of the plugin
|
||||
:priority, # Priority of this plugin for resolving conflicts. 1-100, higher numbers win.
|
||||
:value, # New value, if provided.
|
||||
:file, # File containing the input-changing action, if known
|
||||
:line, # Line in file containing the input-changing action, if known
|
||||
:hit, # if action is :fetch, true if the remote source had the input
|
||||
].freeze
|
||||
|
||||
# Value has a special handler
|
||||
EVENT_PROPERTIES.reject { |p| p == :value }.each do |prop|
|
||||
attr_accessor prop
|
||||
end
|
||||
|
||||
attr_reader :value
|
||||
|
||||
def initialize(properties = {})
|
||||
@value_has_been_set = false
|
||||
|
||||
properties.each do |prop_name, prop_value|
|
||||
if EVENT_PROPERTIES.include? prop_name
|
||||
# OK, save the property
|
||||
send((prop_name.to_s + '=').to_sym, prop_value)
|
||||
else
|
||||
raise "Unrecognized property to Input::Event: #{prop_name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def value=(the_val)
|
||||
# Even if set to nil or false, it has indeed been set; note that fact.
|
||||
@value_has_been_set = true
|
||||
@value = the_val
|
||||
end
|
||||
|
||||
def value_has_been_set?
|
||||
@value_has_been_set
|
||||
end
|
||||
|
||||
def diagnostic_string
|
||||
to_h.reject { |_, val| val.nil? }.to_a.map { |pair| "#{pair[0]}: '#{pair[1]}'" }.join(', ')
|
||||
end
|
||||
|
||||
def to_h
|
||||
EVENT_PROPERTIES.each_with_object({}) do |prop, hash|
|
||||
hash[prop] = send(prop)
|
||||
end
|
||||
end
|
||||
|
||||
def self.probe_stack
|
||||
frames = caller_locations(2, 40)
|
||||
frames.reject! { |f| f.path && f.path.include?('/lib/inspec/') }
|
||||
frames.first
|
||||
end
|
||||
end
|
||||
|
||||
#===========================================================================#
|
||||
# Class NO_VALUE_SET
|
||||
#===========================================================================#
|
||||
# This special class is used to represent the value when an input has
|
||||
# not been assigned a value. This allows a user to explicitly assign nil
|
||||
# to an input.
|
||||
|
@ -62,8 +129,11 @@ module Inspec
|
|||
end
|
||||
|
||||
class Input
|
||||
attr_accessor :name
|
||||
#===========================================================================#
|
||||
# Class Inspec::Input
|
||||
#===========================================================================#
|
||||
|
||||
# Validation types for input values
|
||||
VALID_TYPES = %w{
|
||||
String
|
||||
Numeric
|
||||
|
@ -74,6 +144,21 @@ module Inspec
|
|||
Any
|
||||
}.freeze
|
||||
|
||||
# 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_DSL_ATTRIBUTES = 20
|
||||
|
||||
# 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_UNKNOWN_CALLER = 10
|
||||
|
||||
# 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.
|
||||
DEFAULT_PRIORITY_FOR_VALUE_SET = 60
|
||||
|
||||
attr_reader :description, :events, :identifier, :name, :required, :title, :type
|
||||
|
||||
def initialize(name, options = {})
|
||||
@name = name
|
||||
@opts = options
|
||||
|
@ -82,49 +167,164 @@ module Inspec
|
|||
if @opts.key?(:value)
|
||||
Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
|
||||
@opts.delete(:default)
|
||||
else
|
||||
@opts[:value] = @opts.delete(:default)
|
||||
end
|
||||
end
|
||||
@value = @opts[:value]
|
||||
validate_value_type(@value) if @opts.key?(:type) && @opts.key?(:value)
|
||||
|
||||
# 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
|
||||
|
||||
def value=(new_value)
|
||||
validate_value_type(new_value) if @opts.key?(:type)
|
||||
@value = new_value
|
||||
def set_events
|
||||
events.select { |e| e.action == :set }
|
||||
end
|
||||
|
||||
def diagnostic_string
|
||||
"Input #{name}, with history:\n" +
|
||||
events.map(&:diagnostic_string).map { |line| " #{line}" }.join("\n")
|
||||
end
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Managing Value
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
def update(options)
|
||||
_update_set_metadata(options)
|
||||
normalize_type_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!
|
||||
end
|
||||
|
||||
# We can determine a value:
|
||||
# 1. By event.value (preferred)
|
||||
# 2. By options[:value]
|
||||
# 3. By options[:default] (deprecated)
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
# Determine the current winning value, but don't validate it
|
||||
def current_value
|
||||
# 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)
|
||||
else
|
||||
winning_event.value # May still be nil
|
||||
end
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
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!
|
||||
|
||||
new_value
|
||||
end
|
||||
|
||||
def value
|
||||
if @value.nil?
|
||||
validate_required(@value) if @opts[:required] == true
|
||||
@value = value_or_dummy
|
||||
else
|
||||
@value
|
||||
enforce_required_validation!
|
||||
current_value
|
||||
end
|
||||
|
||||
def has_value?
|
||||
!current_value.is_a? NO_VALUE_SET
|
||||
end
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Marshalling
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
def to_hash
|
||||
as_hash = { name: name, options: {} }
|
||||
[:description, :title, :identifier, :type, :required, :value].each do |field|
|
||||
val = send(field)
|
||||
next if val.nil?
|
||||
as_hash[:options][field] = val
|
||||
end
|
||||
end
|
||||
|
||||
def title
|
||||
@opts[:title]
|
||||
end
|
||||
|
||||
def description
|
||||
@opts[:description]
|
||||
as_hash
|
||||
end
|
||||
|
||||
def ruby_var_identifier
|
||||
@opts[:identifier] || 'attr_' + @name.downcase.strip.gsub(/\s+/, '-').gsub(/[^\w-]/, '')
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
name: @name,
|
||||
options: @opts,
|
||||
}
|
||||
identifier || 'attr_' + name.downcase.strip.gsub(/\s+/, '-').gsub(/[^\w-]/, '')
|
||||
end
|
||||
|
||||
def to_ruby
|
||||
res = ["#{ruby_var_identifier} = attribute('#{@name}',{"]
|
||||
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.
|
||||
|
@ -136,37 +336,78 @@ module Inspec
|
|||
res.join("\n")
|
||||
end
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Value Type Coercion
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
def to_s
|
||||
"Input #{@name} with #{@value}"
|
||||
"Input #{name} with #{current_value}"
|
||||
end
|
||||
|
||||
#--------------------------------------------------------------------------#
|
||||
# Validation
|
||||
#--------------------------------------------------------------------------#
|
||||
|
||||
private
|
||||
|
||||
def validate_required(value)
|
||||
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
|
||||
|
||||
# value will be set already if a secrets file was passed in
|
||||
if (!@opts.key?(:default) && value.nil?) || (@opts[:default].nil? && value.nil?)
|
||||
proposed_value = current_value
|
||||
if proposed_value.nil? || proposed_value.is_a?(NO_VALUE_SET)
|
||||
error = Inspec::Input::RequiredError.new
|
||||
error.input_name = @name
|
||||
error.input_name = name
|
||||
raise error, "Input '#{error.input_name}' is required and does not have a value."
|
||||
end
|
||||
end
|
||||
|
||||
def validate_type(type)
|
||||
type = type.capitalize
|
||||
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
|
||||
|
||||
invalid_type = false
|
||||
if type_req == 'Regexp'
|
||||
invalid_type = true if !valid_regexp?(proposed_value)
|
||||
elsif type_req == 'Numeric'
|
||||
invalid_type = true if !valid_numeric?(proposed_value)
|
||||
elsif type_req == 'Boolean'
|
||||
invalid_type = true if ![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
|
||||
|
||||
def normalize_type_restriction!
|
||||
return unless type
|
||||
|
||||
type_req = type.capitalize
|
||||
abbreviations = {
|
||||
'Num' => 'Numeric',
|
||||
'Regex' => 'Regexp',
|
||||
}
|
||||
type = abbreviations[type] if abbreviations.key?(type)
|
||||
if !VALID_TYPES.include?(type)
|
||||
type_req = abbreviations[type_req] if abbreviations.key?(type_req)
|
||||
if !VALID_TYPES.include?(type_req)
|
||||
error = Inspec::Input::TypeError.new
|
||||
error.input_type = type
|
||||
error.input_type = type_req
|
||||
raise error, "Type '#{error.input_type}' is not a valid input type."
|
||||
end
|
||||
type
|
||||
@type = type_req
|
||||
end
|
||||
|
||||
def valid_numeric?(value)
|
||||
|
@ -177,41 +418,11 @@ module Inspec
|
|||
end
|
||||
|
||||
def valid_regexp?(value)
|
||||
# check for invalid regex syntex
|
||||
# check for invalid regex syntax
|
||||
Regexp.new(value)
|
||||
true
|
||||
rescue
|
||||
false
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
def validate_value_type(value)
|
||||
type = validate_type(@opts[:type])
|
||||
return if type == 'Any'
|
||||
|
||||
invalid_type = false
|
||||
if type == 'Regexp'
|
||||
invalid_type = true if !value.is_a?(String) || !valid_regexp?(value)
|
||||
elsif type == 'Numeric'
|
||||
invalid_type = true if !valid_numeric?(value)
|
||||
elsif type == 'Boolean'
|
||||
invalid_type = true if ![true, false].include?(value)
|
||||
elsif value.is_a?(Module.const_get(type)) == false
|
||||
invalid_type = true
|
||||
end
|
||||
|
||||
if invalid_type == true
|
||||
error = Inspec::Input::ValidationError.new
|
||||
error.input_name = @name
|
||||
error.input_value = value
|
||||
error.input_type = type
|
||||
raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to type '#{error.input_type}'."
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
|
||||
def value_or_dummy
|
||||
@opts.key?(:value) ? @opts[:value] : Inspec::Input::NO_VALUE_SET.new(@name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -81,7 +81,7 @@ module Inspec
|
|||
end
|
||||
|
||||
attr_reader :source_reader, :backend, :runner_context, :check_mode
|
||||
attr_accessor :parent_profile, :profile_name
|
||||
attr_accessor :parent_profile, :profile_id, :profile_name
|
||||
def_delegator :@source_reader, :tests
|
||||
def_delegator :@source_reader, :libraries
|
||||
def_delegator :@source_reader, :metadata
|
||||
|
@ -118,25 +118,32 @@ module Inspec
|
|||
@runtime_profile = RuntimeProfile.new(self)
|
||||
@backend.profile = @runtime_profile
|
||||
|
||||
# The AttributeRegistry is in charge of keeping track of inputs;
|
||||
# it is the single source of truth. Now that we have a profile object,
|
||||
# we can create any inputs that were provided by various mechanisms.
|
||||
options[:runner_conf] ||= Inspec::Config.cached
|
||||
|
||||
if options[:runner_conf].key?(:attrs)
|
||||
Inspec.deprecate(:rename_attributes_to_inputs, 'Use --input-file on the command line instead of --attrs.')
|
||||
options[:runner_conf][:input_file] = options[:runner_conf].delete(:attrs)
|
||||
end
|
||||
|
||||
Inspec::InputRegistry.bind_profile_inputs(
|
||||
# Every input only exists in the context of a profile
|
||||
metadata.params[:name], # TODO: test this with profile aliasing
|
||||
# Remaining args are possible sources of inputs
|
||||
cli_input_files: options[:runner_conf][:input_file], # From CLI --input-file
|
||||
profile_metadata: metadata,
|
||||
# TODO: deprecation checks here
|
||||
runner_api: options[:runner_conf][:attributes], # This is the route the audit_cookbook and kitchen-inspec take
|
||||
)
|
||||
|
||||
@runner_context =
|
||||
options[:profile_context] ||
|
||||
Inspec::ProfileContext.for_profile(self, @backend, @input_values)
|
||||
Inspec::ProfileContext.for_profile(self, @backend)
|
||||
|
||||
@supports_platform = metadata.supports_platform?(@backend)
|
||||
@supports_runtime = metadata.supports_runtime?
|
||||
register_metadata_inputs
|
||||
end
|
||||
|
||||
def register_metadata_inputs # TODO: deprecate
|
||||
if metadata.params.key?(:attributes) && metadata.params[:attributes].is_a?(Array)
|
||||
metadata.params[:attributes].each do |attribute|
|
||||
attr_dup = attribute.dup
|
||||
name = attr_dup.delete(:name)
|
||||
@runner_context.register_input(name, attr_dup)
|
||||
end
|
||||
elsif metadata.params.key?(:attributes)
|
||||
Inspec::Log.warn 'Inputs must be defined as an Array. Skipping current definition.'
|
||||
end
|
||||
end
|
||||
|
||||
def name
|
||||
|
@ -595,7 +602,7 @@ module Inspec
|
|||
f = load_rule_filepath(prefix, rule)
|
||||
load_rule(rule, f, controls, groups)
|
||||
end
|
||||
params[:inputs] = @runner_context.inputs
|
||||
params[:inputs] = Inspec::InputRegistry.list_inputs_for_profile(@profile_id)
|
||||
params
|
||||
end
|
||||
|
||||
|
|
|
@ -12,13 +12,11 @@ require 'inspec/objects/input'
|
|||
|
||||
module Inspec
|
||||
class ProfileContext
|
||||
def self.for_profile(profile, backend, inputs)
|
||||
new(profile.name, backend, { 'profile' => profile,
|
||||
'inputs' => inputs,
|
||||
'check_mode' => profile.check_mode })
|
||||
def self.for_profile(profile, backend)
|
||||
new(profile.name, backend, { 'profile' => profile, 'check_mode' => profile.check_mode })
|
||||
end
|
||||
|
||||
attr_reader :inputs, :backend, :profile_name, :profile_id, :resource_registry
|
||||
attr_reader :backend, :profile_name, :profile_id, :resource_registry
|
||||
attr_accessor :rules
|
||||
def initialize(profile_id, backend, conf)
|
||||
if backend.nil?
|
||||
|
@ -35,7 +33,8 @@ module Inspec
|
|||
@lib_subcontexts = []
|
||||
@require_loader = ::Inspec::RequireLoader.new
|
||||
Inspec::InputRegistry.register_profile_alias(@profile_id, @profile_name) if @profile_id != @profile_name
|
||||
@inputs = Inspec::InputRegistry.list_inputs_for_profile(@profile_id)
|
||||
# TODO: consider polling input source plugins; this is a bulk fetch opportunity
|
||||
|
||||
# A local resource registry that only contains resources defined
|
||||
# in the transitive dependency tree of the loaded profile.
|
||||
@resource_registry = Inspec::Resource.new_registry
|
||||
|
@ -43,6 +42,10 @@ module Inspec
|
|||
@current_load = nil
|
||||
end
|
||||
|
||||
def attributes
|
||||
Inspec::AttributeRegistry.list_attributes_for_profile(@profile_id)
|
||||
end
|
||||
|
||||
def dependencies
|
||||
if @conf['profile'].nil?
|
||||
{}
|
||||
|
@ -187,13 +190,6 @@ module Inspec
|
|||
end
|
||||
end
|
||||
|
||||
def register_input(name, options = {})
|
||||
# we need to return an input object, to allow dermination of values
|
||||
input = Inspec::InputRegistry.register_input(name, @profile_id, options)
|
||||
input.value = @conf['inputs'][name] unless @conf['inputs'].nil? || @conf['inputs'][name].nil?
|
||||
input.value
|
||||
end
|
||||
|
||||
def set_header(field, val)
|
||||
@current_load[field] = val
|
||||
end
|
||||
|
|
|
@ -66,9 +66,13 @@ end
|
|||
class RSpec::Core::ExampleGroup
|
||||
# This DSL method allows us to access the values of inputs within InSpec tests
|
||||
def attribute(name)
|
||||
Inspec::InputRegistry.find_input(name, self.class.metadata[:profile_id]).value
|
||||
Inspec::InputRegistry.find_or_register_input(name, self.class.metadata[:profile_id]).value
|
||||
end
|
||||
define_example_method :attribute
|
||||
def input_obj(name)
|
||||
Inspec::InputRegistry.find_or_register_input(name, self.class.metadata[:profile_id])
|
||||
end
|
||||
define_example_method :input_obj
|
||||
|
||||
# Here, we have to ensure our method_missing gets called prior
|
||||
# to RSpec::Core::ExampleGroup.method_missing (the class method).
|
||||
|
|
|
@ -9,7 +9,6 @@ require 'inspec/backend'
|
|||
require 'inspec/profile_context'
|
||||
require 'inspec/profile'
|
||||
require 'inspec/metadata'
|
||||
require 'inspec/secrets'
|
||||
require 'inspec/config'
|
||||
require 'inspec/dependencies/cache'
|
||||
# spec requirements
|
||||
|
@ -32,7 +31,7 @@ module Inspec
|
|||
class Runner
|
||||
extend Forwardable
|
||||
|
||||
attr_reader :backend, :rules, :inputs
|
||||
attr_reader :backend, :rules
|
||||
|
||||
def attributes
|
||||
Inspec.deprecate(:rename_attributes_to_inputs, "Don't call runner.attributes, call runner.inputs")
|
||||
|
@ -57,10 +56,17 @@ module Inspec
|
|||
RunnerRspec.new(@conf)
|
||||
end
|
||||
|
||||
# list of profile inputs
|
||||
@inputs = {}
|
||||
# About reading inputs:
|
||||
# @conf gets passed around a lot, eventually to
|
||||
# Inspec::InputRegistry.register_external_inputs.
|
||||
#
|
||||
# @conf may contain the key :attributes or :inputs, which is to be a Hash
|
||||
# of values passed in from the Runner API.
|
||||
# This is how kitchen-inspec and the audit_cookbook pass in inputs.
|
||||
#
|
||||
# @conf may contain the key :attrs or :input_file, which is to be an Array
|
||||
# of file paths, each a YAML file. This how --input-file works.
|
||||
|
||||
load_inputs(@conf)
|
||||
configure_transport
|
||||
end
|
||||
|
||||
|
@ -101,7 +107,6 @@ module Inspec
|
|||
@test_collector.add_profile(requirement.profile)
|
||||
end
|
||||
|
||||
@inputs = profile.runner_context.inputs if @inputs.empty?
|
||||
tests = profile.collect_tests
|
||||
all_controls += tests unless tests.nil?
|
||||
end
|
||||
|
@ -149,35 +154,6 @@ module Inspec
|
|||
@test_collector.exit_code
|
||||
end
|
||||
|
||||
# determine all inputs before the execution, fetch data from secrets backend
|
||||
def load_inputs(options)
|
||||
# TODO: - rename :attributes - it is user-visible
|
||||
options[:attributes] ||= {}
|
||||
|
||||
if options.key?(:attrs)
|
||||
Inspec.deprecate(:rename_attributes_to_inputs, 'Use --input-file on the command line instead of --attrs.')
|
||||
options[:input_file] = options.delete(:attrs)
|
||||
end
|
||||
secrets_targets = options[:input_file]
|
||||
return options[:attributes] if secrets_targets.nil?
|
||||
|
||||
secrets_targets.each do |target|
|
||||
validate_inputs_file_readability!(target)
|
||||
|
||||
secrets = Inspec::SecretsBackend.resolve(target)
|
||||
if secrets.nil?
|
||||
raise Inspec::Exceptions::SecretsBackendNotFound,
|
||||
"Cannot find parser for inputs file '#{target}'. " \
|
||||
'Check to make sure file has the appropriate extension.'
|
||||
end
|
||||
|
||||
next if secrets.inputs.nil?
|
||||
options[:attributes].merge!(secrets.inputs)
|
||||
end
|
||||
|
||||
options[:attributes]
|
||||
end
|
||||
|
||||
#
|
||||
# add_target allows the user to add a target whose tests will be
|
||||
# run when the user calls the run method.
|
||||
|
@ -209,7 +185,7 @@ module Inspec
|
|||
vendor_cache: @cache,
|
||||
backend: @backend,
|
||||
controls: @controls,
|
||||
inputs: @conf[:attributes]) # TODO: read form :inputs here (user visible)
|
||||
runner_conf: @conf)
|
||||
raise "Could not resolve #{target} to valid input." if profile.nil?
|
||||
@target_profiles << profile if supports_profile?(profile)
|
||||
end
|
||||
|
@ -300,22 +276,6 @@ module Inspec
|
|||
examples.each { |e| @test_collector.add_test(e, rule) }
|
||||
end
|
||||
|
||||
def validate_inputs_file_readability!(target)
|
||||
unless File.exist?(target)
|
||||
raise Inspec::Exceptions::InputsFileDoesNotExist,
|
||||
"Cannot find input file '#{target}'. " \
|
||||
'Check to make sure file exists.'
|
||||
end
|
||||
|
||||
unless File.readable?(target)
|
||||
raise Inspec::Exceptions::InputsFileNotReadable,
|
||||
"Cannot read input file '#{target}'. " \
|
||||
'Check to make sure file is readable.'
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def rspec_skipped_block(arg, opts, message)
|
||||
@test_collector.example_group(*arg, opts) do
|
||||
# Send custom `it` block to RSpec
|
||||
|
|
|
@ -52,6 +52,27 @@ module Inspec
|
|||
suffix = stdout.end_with?("\n") ? "\n" : ''
|
||||
stdout.split("\n").reject { |l| l.include? ' DEPRECATION: ' }.join("\n") + suffix
|
||||
end
|
||||
|
||||
# This works if you use json: true on an exec call
|
||||
def must_have_all_controls_passing
|
||||
# Strategy: assemble an array of tests that failed or skipped, and insist it is empty
|
||||
# result.payload.json['profiles'][0]['controls'][0]['results'][0]['status']
|
||||
failed_tests = []
|
||||
payload.json['profiles'].each do |profile_struct|
|
||||
profile_name = profile_struct['name']
|
||||
profile_struct['controls'].each do |control_struct|
|
||||
control_name = control_struct['id']
|
||||
control_struct['results'].compact.each do |test_struct|
|
||||
test_desc = test_struct['code_desc']
|
||||
if test_struct['status'] != 'passed'
|
||||
failed_tests << "#{profile_name}/#{control_name}/#{test_desc}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
failed_tests.must_be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,9 @@ require 'functional/helper'
|
|||
describe 'inputs' do
|
||||
include FunctionalHelper
|
||||
let(:inputs_profiles_path) { File.join(profile_path, 'inputs') }
|
||||
|
||||
# This tests being able to load complex structures from
|
||||
# cli option-specified files.
|
||||
[
|
||||
'flat',
|
||||
'nested',
|
||||
|
@ -15,9 +18,9 @@ describe 'inputs' do
|
|||
cmd += ' --no-create-lockfile'
|
||||
cmd += ' --input-file ' + File.join(inputs_profiles_path, 'basic', 'files', "#{input_file}.yaml")
|
||||
cmd += ' --controls ' + input_file
|
||||
out = inspec(cmd)
|
||||
out.stderr.must_equal ''
|
||||
out.exit_status.must_equal 0
|
||||
result = run_inspec_process(cmd)
|
||||
result.stderr.must_equal ''
|
||||
result.exit_status.must_equal 0
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -58,52 +61,49 @@ describe 'inputs' do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'run profile with yaml inputs' do
|
||||
it "runs using yml inputs" do
|
||||
describe 'when accessing inputs in a variety of scopes' do
|
||||
it "is able to read the inputs" do
|
||||
cmd = 'exec '
|
||||
cmd += File.join(inputs_profiles_path, 'global')
|
||||
cmd += ' --no-create-lockfile'
|
||||
cmd += ' --input-file ' + File.join(inputs_profiles_path, 'global', 'files', "inputs.yml")
|
||||
out = inspec(cmd)
|
||||
out.stderr.must_equal ''
|
||||
# TODO: fix attribute inheritance override test
|
||||
# we have one failing case on this - run manually to see
|
||||
# For now, reduce cases to 20; we'll be reworking all this soon anyway
|
||||
# out.stdout.must_include '21 successful'
|
||||
# out.exit_status.must_equal 0
|
||||
|
||||
out.stdout.must_include '20 successful' # and one failing
|
||||
cmd += File.join(inputs_profiles_path, 'scoping')
|
||||
result = run_inspec_process(cmd, json: true)
|
||||
result.must_have_all_controls_passing
|
||||
end
|
||||
end
|
||||
|
||||
describe 'run profile with metadata inputs' do
|
||||
it "does not error when inputs are empty" do
|
||||
cmd = 'exec '
|
||||
cmd += File.join(inputs_profiles_path, 'metadata-empty')
|
||||
cmd += ' --no-create-lockfile'
|
||||
out = inspec(cmd)
|
||||
out.stdout.must_include 'WARN: Inputs must be defined as an Array. Skipping current definition.'
|
||||
out.exit_status.must_equal 0
|
||||
result = run_inspec_process(cmd, json: true)
|
||||
result.stderr.must_include 'WARN: Inputs must be defined as an Array. Skipping current definition.'
|
||||
result.exit_status.must_equal 0
|
||||
end
|
||||
|
||||
it "errors with invalid input types" do
|
||||
cmd = 'exec '
|
||||
cmd += File.join(inputs_profiles_path, 'metadata-invalid')
|
||||
cmd += ' --no-create-lockfile'
|
||||
out = inspec(cmd)
|
||||
out.stderr.must_equal "Type 'Color' is not a valid input type.\n"
|
||||
out.stdout.must_equal ''
|
||||
out.exit_status.must_equal 1
|
||||
result = run_inspec_process(cmd, json: true)
|
||||
result.stderr.must_equal "Type 'Color' is not a valid input type.\n"
|
||||
result.exit_status.must_equal 1
|
||||
end
|
||||
|
||||
it "errors with required input not defined" do
|
||||
cmd = 'exec '
|
||||
cmd += File.join(inputs_profiles_path, 'required')
|
||||
cmd += ' --no-create-lockfile'
|
||||
out = inspec(cmd)
|
||||
out.stderr.must_equal "Input 'username' is required and does not have a value.\n"
|
||||
out.stdout.must_equal ''
|
||||
out.exit_status.must_equal 1
|
||||
cmd += File.join(inputs_profiles_path, 'metadata-required')
|
||||
result = run_inspec_process(cmd, json: true)
|
||||
result.stderr.must_include "Input 'a_required_input' is required and does not have a value.\n"
|
||||
result.exit_status.must_equal 1
|
||||
end
|
||||
|
||||
# TODO - add test for backwards compatibility using 'attribute' in DSL
|
||||
describe 'when profile inheritance is used' do
|
||||
it 'should correctly assign input values using namespacing' do
|
||||
cmd = 'exec ' + File.join(inputs_profiles_path, 'inheritance', 'wrapper')
|
||||
result = run_inspec_process(cmd, json: true)
|
||||
result.must_have_all_controls_passing
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# # TODO - add test for backwards compatibility using 'attribute' in DSL
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,4 +6,6 @@ require 'minitest/pride'
|
|||
require 'json'
|
||||
require 'ostruct'
|
||||
|
||||
require 'byebug'
|
||||
|
||||
require_relative 'lib/resource_support/aws'
|
||||
|
|
131
test/unit/inputs/events_test.rb
Normal file
131
test/unit/inputs/events_test.rb
Normal file
|
@ -0,0 +1,131 @@
|
|||
require 'helper'
|
||||
require 'inspec/objects/input'
|
||||
|
||||
describe 'Inspec::Input and Events' do
|
||||
let(:ipt) { Inspec::Input.new('input') }
|
||||
|
||||
#==============================================================#
|
||||
# Create Event
|
||||
#==============================================================#
|
||||
describe 'when creating an input' do
|
||||
it 'should have a creation event' do
|
||||
creation_events = ipt.events.select { |e| e.action == :create }
|
||||
creation_events.wont_be_empty
|
||||
end
|
||||
|
||||
it 'should only have a creation event if no value was provided' do
|
||||
creation_events = ipt.events.select { |e| e.action == :create }
|
||||
creation_events.count.must_equal 1
|
||||
end
|
||||
|
||||
it 'should have a create and a set event if a value was provided' do
|
||||
ipt = Inspec::Input.new('input', value: 42)
|
||||
creation_events = ipt.events.select { |e| e.action == :create }
|
||||
creation_events.count.must_equal 1
|
||||
set_events = ipt.set_events
|
||||
set_events.count.must_equal 1
|
||||
set_events.first.value.must_equal 42
|
||||
end
|
||||
end
|
||||
|
||||
#==============================================================#
|
||||
# Set Events
|
||||
#==============================================================#
|
||||
describe 'when setting an input using value=' do
|
||||
it 'should add a set event' do
|
||||
ipt.set_events.count.must_equal 0
|
||||
ipt.value = 42
|
||||
ipt.set_events.count.must_equal 1
|
||||
end
|
||||
|
||||
it 'should add one event for each value= operation' do
|
||||
ipt.set_events.count.must_equal 0
|
||||
ipt.value = 1
|
||||
ipt.value = 2
|
||||
ipt.value = 3
|
||||
ipt.set_events.count.must_equal 3
|
||||
end
|
||||
end
|
||||
|
||||
#==============================================================#
|
||||
# Picking a Winner
|
||||
#==============================================================#
|
||||
|
||||
# For more real-world testing of metadata vs --attrs vs inline, see
|
||||
# test/functional/inputs_test.rb
|
||||
describe 'priority voting' do
|
||||
it 'value() should return the correct value when there is just one set operation' do
|
||||
evt = Inspec::Input::Event.new(value: 42, priority: 25, action: :set)
|
||||
ipt.update(event: evt)
|
||||
ipt.value.must_equal 42
|
||||
end
|
||||
|
||||
it 'should return the highest priority regardless of order' do
|
||||
evt1 = Inspec::Input::Event.new(value: 1, priority: 25, action: :set)
|
||||
ipt.update(event: evt1)
|
||||
evt2 = Inspec::Input::Event.new(value: 2, priority: 35, action: :set)
|
||||
ipt.update(event: evt2)
|
||||
evt3 = Inspec::Input::Event.new(value: 3, priority: 15, action: :set)
|
||||
ipt.update(event: evt3)
|
||||
|
||||
ipt.value.must_equal 2
|
||||
end
|
||||
|
||||
it 'breaks ties using the last event of the highest priority' do
|
||||
evt1 = Inspec::Input::Event.new(value: 1, priority: 15, action: :set)
|
||||
ipt.update(event: evt1)
|
||||
evt2 = Inspec::Input::Event.new(value: 2, priority: 25, action: :set)
|
||||
ipt.update(event: evt2)
|
||||
evt3 = Inspec::Input::Event.new(value: 3, priority: 25, action: :set)
|
||||
ipt.update(event: evt3)
|
||||
|
||||
ipt.value.must_equal 3
|
||||
end
|
||||
end
|
||||
|
||||
#==============================================================#
|
||||
# Stack Hueristics
|
||||
#==============================================================#
|
||||
|
||||
describe 'when determining where the call came from' do
|
||||
it 'should get the line and file correct in the constructor' do
|
||||
expected_file = __FILE__
|
||||
expected_line = __LINE__; ipt = Inspec::Input.new('some_input') # Important to keep theses on one line
|
||||
event = ipt.events.first
|
||||
event.file.must_equal expected_file
|
||||
event.line.must_equal expected_line
|
||||
end
|
||||
end
|
||||
|
||||
#==============================================================#
|
||||
# Diagnostics
|
||||
#==============================================================#
|
||||
|
||||
describe 'input diagnostics' do
|
||||
it 'should dump the events' do
|
||||
evt1 = Inspec::Input::Event.new(value: {a:1, b:2}, priority: 15, action: :set, provider: :unit_test)
|
||||
ipt.update(event: evt1)
|
||||
evt2 = Inspec::Input::Event.new(action: :fetch, provider: :alcubierre, hit: false)
|
||||
ipt.update(event: evt2)
|
||||
evt3 = Inspec::Input::Event.new(value: 12, action: :set, provider: :control_dsl, file: '/tmp/some/file.rb', line: 2)
|
||||
ipt.update(event: evt3)
|
||||
|
||||
text = ipt.diagnostic_string
|
||||
lines = text.split("\n")
|
||||
lines.count.must_equal 5 # 3 events above + 1 create + 1 input name line
|
||||
lines.shift # Not testing the inputs top line here
|
||||
|
||||
lines.each do |line|
|
||||
line.must_match /^\s\s([a-z]+:\s\'.+\',\s)*?([a-z]+:\s\'.+\')$/ # key: 'value', key: 'value' ...
|
||||
end
|
||||
|
||||
lines[0].must_include "action: 'create',"
|
||||
|
||||
lines[1].must_include "action: 'set',"
|
||||
lines[1].must_include "value: '{" # It should to_s the value
|
||||
lines[1].must_include "provider: 'unit_test'"
|
||||
lines[1].must_include "priority: '15'"
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
require 'helper'
|
||||
require 'inspec/input_registry'
|
||||
require 'inspec/secrets'
|
||||
|
||||
describe Inspec::InputRegistry do
|
||||
let(:registry) { Inspec::InputRegistry }
|
||||
|
@ -12,60 +13,142 @@ describe Inspec::InputRegistry do
|
|||
|
||||
describe 'creating a profile input' do
|
||||
it 'creates an input without options' do
|
||||
registry.register_input('test_input', 'dummy_profile')
|
||||
# confirm we get the dummy default
|
||||
registry.find_input('test_input', 'dummy_profile').value.class.must_equal Inspec::Input::NO_VALUE_SET
|
||||
registry.find_or_register_input('test_input', 'dummy_profile')
|
||||
# confirm we get the dummy value
|
||||
registry.find_or_register_input('test_input', 'dummy_profile').value.class.must_equal Inspec::Input::NO_VALUE_SET
|
||||
end
|
||||
|
||||
it 'creates an input with a default value' do
|
||||
registry.register_input('color', 'dummy_profile', default: 'silver')
|
||||
registry.find_input('color', 'dummy_profile').value.must_equal 'silver'
|
||||
it 'creates an input with a value' do
|
||||
registry.find_or_register_input('color', 'dummy_profile', value: 'silver')
|
||||
registry.find_or_register_input('color', 'dummy_profile').value.must_equal 'silver'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'creating a profile with a name alias' do
|
||||
it 'creates a default input on a profile with an alias' do
|
||||
it 'creates a value input on a profile with an alias' do
|
||||
registry.register_profile_alias('old_profile', 'new_profile')
|
||||
registry.register_input('color', 'new_profile', default: 'blue')
|
||||
registry.find_input('color', 'new_profile').value.must_equal 'blue'
|
||||
registry.find_input('color', 'old_profile').value.must_equal 'blue'
|
||||
registry.find_or_register_input('color', 'new_profile', value: 'blue')
|
||||
registry.find_or_register_input('color', 'new_profile').value.must_equal 'blue'
|
||||
registry.find_or_register_input('color', 'old_profile').value.must_equal 'blue'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'creating a profile and select it' do
|
||||
it 'creates a profile with inputs' do
|
||||
registry.register_input('color', 'dummy_profile', default: 'silver')
|
||||
registry.register_input('color2', 'dummy_profile', default: 'blue')
|
||||
registry.register_input('color3', 'dummy_profile', default: 'green')
|
||||
registry.find_or_register_input('color', 'dummy_profile', value: 'silver')
|
||||
registry.find_or_register_input('color2', 'dummy_profile', value: 'blue')
|
||||
registry.find_or_register_input('color3', 'dummy_profile', value: 'green')
|
||||
registry.list_inputs_for_profile('dummy_profile').size.must_equal 3
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validate the correct objects are getting created' do
|
||||
it 'creates a profile with inputs' do
|
||||
registry.register_input('color', 'dummy_profile', default: 'silver').class.must_equal Inspec::Input
|
||||
registry.find_or_register_input('color', 'dummy_profile', value: 'silver').class.must_equal Inspec::Input
|
||||
registry.list_inputs_for_profile('dummy_profile').size.must_equal 1
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validate find_input method' do
|
||||
describe 'validate find_or_register_input method' do
|
||||
it 'find an input which exist' do
|
||||
input = registry.register_input('color', 'dummy_profile')
|
||||
input = registry.find_or_register_input('color', 'dummy_profile')
|
||||
input.value = 'black'
|
||||
|
||||
registry.find_input('color', 'dummy_profile').value.must_equal 'black'
|
||||
registry.find_or_register_input('color', 'dummy_profile').value.must_equal 'black'
|
||||
end
|
||||
end
|
||||
|
||||
# =============================================================== #
|
||||
# Loading inputs from --attrs
|
||||
# =============================================================== #
|
||||
describe '#bind_profile_inputs' do
|
||||
before do
|
||||
Inspec::InputRegistry.any_instance.stubs(:validate_inputs_file_readability!)
|
||||
end
|
||||
let(:seen_inputs) do
|
||||
registry.bind_profile_inputs('test_fixture_profile', sources)
|
||||
inputs = registry.list_inputs_for_profile('test_fixture_profile')
|
||||
# Flatten Input objects down to their values
|
||||
inputs.keys.each { |input_name| inputs[input_name] = inputs[input_name].value }
|
||||
inputs
|
||||
end
|
||||
|
||||
it 'errors when trying to find an input on an unknown profile' do
|
||||
input = registry.register_input('color', 'dummy_profile')
|
||||
ex = assert_raises(Inspec::InputRegistry::ProfileLookupError) { registry.find_input('color', 'unknown_profile') }
|
||||
ex.message.must_match "Profile 'unknown_profile' does not have any inputs"
|
||||
describe 'when no CLI --attrs are specified' do
|
||||
let(:sources) { { cli_input_files: [] } }
|
||||
it 'returns an empty hash' do
|
||||
seen_inputs.must_equal({})
|
||||
end
|
||||
end
|
||||
|
||||
it 'errors when trying to find an unknown input on a known profile' do
|
||||
input = registry.register_input('color', 'dummy_profile')
|
||||
ex = assert_raises(Inspec::InputRegistry::InputLookupError) { registry.find_input('unknown_input', 'dummy_profile') }
|
||||
ex.message.must_match "Profile 'dummy_profile' does not have an input with name 'unknown_input'"
|
||||
describe 'when a CLI --attrs option is provided and does not resolve' do
|
||||
let(:sources) { { cli_input_files: ['nope.jpg'] } }
|
||||
it 'raises an exception' do
|
||||
Inspec::SecretsBackend.expects(:resolve).with('nope.jpg').returns(nil)
|
||||
proc { seen_inputs }.must_raise Inspec::Exceptions::SecretsBackendNotFound
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when a CLI --attrs option is provided and has no inputs' do
|
||||
let(:sources) { { cli_input_files: ['empty.yaml'] } }
|
||||
it 'returns an empty hash' do
|
||||
secrets = mock
|
||||
secrets.stubs(:inputs).returns(nil)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('empty.yaml').returns(secrets)
|
||||
|
||||
seen_inputs.must_equal({})
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when a CLI --attrs file is provided and has inputs' do
|
||||
let(:sources) { { cli_input_files: ['file1.yaml'] } }
|
||||
it 'returns a hash containing the inputs' do
|
||||
fixture_inputs = { foo: 'bar' }
|
||||
secrets = mock
|
||||
secrets.stubs(:inputs).returns(fixture_inputs)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets)
|
||||
|
||||
seen_inputs.must_equal(fixture_inputs)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when multiple CLI --attrs option args are provided and one fails' do
|
||||
let(:sources) { { cli_input_files: ['file1.yaml', 'file2.yaml'] } }
|
||||
it 'raises an exception' do
|
||||
secrets = mock
|
||||
secrets.stubs(:inputs).returns(nil)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(nil)
|
||||
proc { seen_inputs }.must_raise Inspec::Exceptions::SecretsBackendNotFound
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when multiple CLI --attrs option args are provided and one has no inputs' do
|
||||
let(:sources) { { cli_input_files: ['file1.yaml', 'file2.yaml'] } }
|
||||
|
||||
it 'returns a hash containing the inputs from the valid files' do
|
||||
inputs = { foo: 'bar' }
|
||||
secrets1 = mock
|
||||
secrets1.stubs(:inputs).returns(nil)
|
||||
secrets2 = mock
|
||||
secrets2.stubs(:inputs).returns(inputs)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets1)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(secrets2)
|
||||
seen_inputs.must_equal(inputs)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when multiple CLI --attrs option args are provided and all have inputs' do
|
||||
let(:sources) { { cli_input_files: ['file1.yaml', 'file2.yaml'] } }
|
||||
it 'returns a hash containing all the inputs' do
|
||||
options = { attrs: ['file1.yaml', 'file2.yaml'] }
|
||||
secrets1 = mock
|
||||
secrets1.stubs(:inputs).returns({ key1: 'value1' })
|
||||
secrets2 = mock
|
||||
secrets2.stubs(:inputs).returns({ key2: 'value2' })
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets1)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(secrets2)
|
||||
seen_inputs.must_equal({ key1: 'value1', key2: 'value2' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,13 +4,82 @@ require 'helper'
|
|||
require 'inspec/objects/input'
|
||||
|
||||
describe Inspec::Input do
|
||||
let(:input) { Inspec::Input.new('test_input') }
|
||||
let(:opts) { { } }
|
||||
let(:input) { Inspec::Input.new('test_input', opts) }
|
||||
|
||||
it 'support storing and returning false' do
|
||||
input.value = false
|
||||
input.value.must_equal false
|
||||
#==============================================================#
|
||||
# Metadata
|
||||
#==============================================================#
|
||||
describe 'setting and reading metadata' do
|
||||
{
|
||||
description: 'My favorite attribute',
|
||||
identifier: 'a_ruby_permitted_name',
|
||||
required: true,
|
||||
title: 'how is this different than description',
|
||||
type: 'Numeric'
|
||||
}.each do |field, value|
|
||||
it "should be able to recall the #{field} field" do
|
||||
opts[field] = value
|
||||
ipt = Inspec::Input.new('test_attribute', opts)
|
||||
seen_value = ipt.send(field)
|
||||
seen_value.must_equal value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#==============================================================#
|
||||
# Code Generation
|
||||
#==============================================================#
|
||||
describe 'to_ruby method' do
|
||||
it 'generates the code for the input' do
|
||||
input = Inspec::Input.new('application_port', description: 'The port my application uses', value: 80)
|
||||
|
||||
ruby_code = input.to_ruby
|
||||
ruby_code.must_include "attr_application_port = " # Should assign to a var
|
||||
ruby_code.must_include "attribute('application_port'" # Should have the DSL call
|
||||
ruby_code.must_include 'value: 80'
|
||||
ruby_code.must_include 'default: 80'
|
||||
ruby_code.must_include "description: 'The port my application uses'"
|
||||
|
||||
# Try to eval the code to verify that the generated code was valid ruby.
|
||||
# Note that the attribute() method is part of the DSL, so we need to
|
||||
# alter the call into something that can respond - the constructor will do
|
||||
ruby_code_for_eval = ruby_code.sub(/attribute\(/,'Inspec::Input.new(')
|
||||
|
||||
# This will throw exceptions if there is a problem
|
||||
new_attr = eval(ruby_code_for_eval) # Could use ripper!
|
||||
new_attr.value.must_equal 80
|
||||
end
|
||||
end
|
||||
# TODO - deprecate this, not sure it is used
|
||||
describe 'to_hash method' do
|
||||
it 'generates a similar hash' do
|
||||
ipt = Inspec::Input.new(
|
||||
'some_attr',
|
||||
description: 'The port my application uses',
|
||||
value: 80,
|
||||
identifier: 'app_port',
|
||||
required: false,
|
||||
type: 'numeric'
|
||||
)
|
||||
expected = {
|
||||
name: 'some_attr',
|
||||
options: {
|
||||
description: 'The port my application uses',
|
||||
value: 80,
|
||||
identifier: 'app_port',
|
||||
required: false,
|
||||
type: 'Numeric', # This gets normalized
|
||||
}
|
||||
}
|
||||
ipt.to_hash.must_equal expected
|
||||
end
|
||||
end
|
||||
|
||||
#==============================================================#
|
||||
# Setting Value - One Shot
|
||||
# (see events_test.rb for overwrite support)
|
||||
#==============================================================#
|
||||
describe 'the dummy value used when value is not set' do
|
||||
it 'returns the actual value, not the dummy object, if one is assigned' do
|
||||
input.value = 'new_value'
|
||||
|
@ -37,7 +106,7 @@ describe Inspec::Input do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'input with a value set' do
|
||||
describe 'setting a value in the constructor using value:' do
|
||||
it 'returns the user-configured value' do
|
||||
input = Inspec::Input.new('test_input', value: 'some_value')
|
||||
input.value.must_equal 'some_value'
|
||||
|
@ -70,187 +139,20 @@ describe Inspec::Input do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'validate required method' do
|
||||
it 'does not error if a value is set' do
|
||||
input = Inspec::Input.new('test_input', value: 'some_value', required: true)
|
||||
input.value.must_equal 'some_value'
|
||||
describe 'setting a value using value=' do
|
||||
it 'supports storing and returning a value' do
|
||||
input.value = 'a_value'
|
||||
input.value.must_equal 'a_value'
|
||||
end
|
||||
|
||||
it 'does not error if a value is specified by value=' do
|
||||
input = Inspec::Input.new('test_input', required: true)
|
||||
input.value = 'test_value'
|
||||
input.value.must_equal 'test_value'
|
||||
it 'supports storing and returning false' do
|
||||
input.value = false
|
||||
input.value.must_equal false
|
||||
end
|
||||
|
||||
it 'returns an error if no value is set' do
|
||||
# Assigning the cli_command is needed because in check mode, we don't error
|
||||
# on unset inputs. This is how you tell the input system we are not in
|
||||
# check mode, apparently.
|
||||
Inspec::BaseCLI.inspec_cli_command = :exec
|
||||
input = Inspec::Input.new('test_input', required: true)
|
||||
ex = assert_raises(Inspec::Input::RequiredError) { input.value }
|
||||
ex.message.must_match /Input 'test_input' is required and does not have a value./
|
||||
Inspec::BaseCLI.inspec_cli_command = nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validate value type method' do
|
||||
let(:opts) { {} }
|
||||
let(:input) { Inspec::Input.new('test_input', opts) }
|
||||
|
||||
it 'validates a string type' do
|
||||
opts[:type] = 'string'
|
||||
input.send(:validate_value_type, 'string')
|
||||
end
|
||||
|
||||
it 'returns an error if a invalid string is set' do
|
||||
opts[:type] = 'string'
|
||||
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, 123) }
|
||||
ex.message.must_match /Input 'test_input' with value '123' does not validate to type 'String'./
|
||||
end
|
||||
|
||||
it 'validates a numeric type' do
|
||||
opts[:type] = 'numeric'
|
||||
input.send(:validate_value_type, 123.33)
|
||||
end
|
||||
|
||||
it 'returns an error if a invalid numeric is set' do
|
||||
opts[:type] = 'numeric'
|
||||
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, 'invalid') }
|
||||
ex.message.must_match /Input 'test_input' with value 'invalid' does not validate to type 'Numeric'./
|
||||
end
|
||||
|
||||
it 'validates a regex type' do
|
||||
opts[:type] = 'regex'
|
||||
input.send(:validate_value_type, '/^\d*$/')
|
||||
end
|
||||
|
||||
it 'returns an error if a invalid regex is set' do
|
||||
opts[:type] = 'regex'
|
||||
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, '/(.+/') }
|
||||
ex.message.must_match "Input 'test_input' with value '/(.+/' does not validate to type 'Regexp'."
|
||||
end
|
||||
|
||||
it 'validates a array type' do
|
||||
opts[:type] = 'Array'
|
||||
value = [1, 2, 3]
|
||||
input.send(:validate_value_type, value)
|
||||
end
|
||||
|
||||
it 'returns an error if a invalid array is set' do
|
||||
opts[:type] = 'Array'
|
||||
value = { a: 1, b: 2, c: 3 }
|
||||
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, value) }
|
||||
ex.message.must_match /Input 'test_input' with value '{:a=>1, :b=>2, :c=>3}' does not validate to type 'Array'./
|
||||
end
|
||||
|
||||
it 'validates a hash type' do
|
||||
opts[:type] = 'Hash'
|
||||
value = { a: 1, b: 2, c: 3 }
|
||||
input.send(:validate_value_type, value)
|
||||
end
|
||||
|
||||
it 'returns an error if a invalid hash is set' do
|
||||
opts[:type] = 'hash'
|
||||
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, 'invalid') }
|
||||
ex.message.must_match /Input 'test_input' with value 'invalid' does not validate to type 'Hash'./
|
||||
end
|
||||
|
||||
it 'validates a boolean type' do
|
||||
opts[:type] = 'boolean'
|
||||
input.send(:validate_value_type, false)
|
||||
input.send(:validate_value_type, true)
|
||||
end
|
||||
|
||||
it 'returns an error if a invalid boolean is set' do
|
||||
opts[:type] = 'boolean'
|
||||
ex = assert_raises(Inspec::Input::ValidationError) { input.send(:validate_value_type, 'not_true') }
|
||||
ex.message.must_match /Input 'test_input' with value 'not_true' does not validate to type 'Boolean'./
|
||||
end
|
||||
|
||||
it 'validates a any type' do
|
||||
opts[:type] = 'any'
|
||||
input.send(:validate_value_type, false)
|
||||
input.send(:validate_value_type, true)
|
||||
input.send(:validate_value_type, 1)
|
||||
input.send(:validate_value_type, 'bob')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validate type method' do
|
||||
it 'converts regex to Regexp' do
|
||||
input.send(:validate_type, 'regex').must_equal 'Regexp'
|
||||
end
|
||||
|
||||
it 'returns the same value if there is nothing to clean' do
|
||||
input.send(:validate_type, 'String').must_equal 'String'
|
||||
end
|
||||
|
||||
it 'returns an error if a invalid type is sent' do
|
||||
ex = assert_raises(Inspec::Input::TypeError) { input.send(:validate_type, 'dressing') }
|
||||
ex.message.must_match /Type 'Dressing' is not a valid input type./
|
||||
end
|
||||
end
|
||||
|
||||
describe 'valid_regexp? method' do
|
||||
it 'validates a string regex' do
|
||||
input.send(:valid_regexp?, '/.*/').must_equal true
|
||||
end
|
||||
|
||||
it 'validates a slash regex' do
|
||||
input.send(:valid_regexp?, /.*/).must_equal true
|
||||
end
|
||||
|
||||
it 'does not vaildate a invalid regex' do
|
||||
input.send(:valid_regexp?, '/.*(/').must_equal false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'valid_numeric? method' do
|
||||
it 'validates a string number' do
|
||||
input.send(:valid_numeric?, '123').must_equal true
|
||||
end
|
||||
|
||||
it 'validates a float number' do
|
||||
input.send(:valid_numeric?, 44.55).must_equal true
|
||||
end
|
||||
|
||||
it 'validats a wrong padded number' do
|
||||
input.send(:valid_numeric?, '00080').must_equal true
|
||||
end
|
||||
|
||||
it 'does not vaildate a invalid number' do
|
||||
input.send(:valid_numeric?, '55.55.55.5').must_equal false
|
||||
end
|
||||
|
||||
it 'does not vaildate a invalid string' do
|
||||
input.send(:valid_numeric?, 'one').must_equal false
|
||||
end
|
||||
|
||||
it 'does not vaildate a fraction' do
|
||||
input.send(:valid_numeric?, '1/2').must_equal false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'to_ruby method' do
|
||||
it 'generates the code for the input' do
|
||||
input = Inspec::Input.new('application_port', description: 'The port my application uses', value: 80)
|
||||
|
||||
ruby_code = input.to_ruby
|
||||
ruby_code.must_include "attr_application_port = " # Should assign to a var
|
||||
ruby_code.must_include "attribute('application_port'" # Should have the DSL call
|
||||
ruby_code.must_include 'value: 80'
|
||||
ruby_code.must_include 'default: 80'
|
||||
ruby_code.must_include "description: 'The port my application uses'"
|
||||
|
||||
# Try to eval the code to verify that the generated code was valid ruby.
|
||||
# Note that the attribute() method is part of the DSL, so we need to
|
||||
# alter the call into something that can respond - the constructor will do
|
||||
ruby_code_for_eval = ruby_code.sub(/attribute\(/,'Inspec::Input.new(')
|
||||
|
||||
# This will throw exceptions if there is a problem
|
||||
new_attr = eval(ruby_code_for_eval) # Could use ripper!
|
||||
new_attr.value.must_equal 80
|
||||
it 'supports storing and returning nil' do
|
||||
input.value = nil
|
||||
input.value.must_be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
141
test/unit/inputs/validation_test.rb
Normal file
141
test/unit/inputs/validation_test.rb
Normal file
|
@ -0,0 +1,141 @@
|
|||
require 'helper'
|
||||
require 'inspec/objects/input'
|
||||
|
||||
describe 'type validation' do
|
||||
let(:opts) { {} }
|
||||
let(:input) { Inspec::Input.new('test_input', opts) }
|
||||
|
||||
#==============================================================#
|
||||
# Requiredness
|
||||
#==============================================================#
|
||||
describe 'enforce_required_validation' do
|
||||
it 'does not error if a value is set' do
|
||||
input = Inspec::Input.new('test_input', value: 'some_value', required: true)
|
||||
input.value.must_equal 'some_value'
|
||||
end
|
||||
|
||||
it 'does not error if a value is specified by value=' do
|
||||
input = Inspec::Input.new('test_input', required: true)
|
||||
input.value = 'test_value'
|
||||
input.value.must_equal 'test_value'
|
||||
end
|
||||
|
||||
it 'returns an error if no value is set' do
|
||||
# Assigning the cli_command is needed because in check mode, we don't error
|
||||
# on unset inputs. This is how you tell the input system we are not in
|
||||
# check mode, apparently.
|
||||
Inspec::BaseCLI.inspec_cli_command = :exec
|
||||
input = Inspec::Input.new('test_input', required: true)
|
||||
ex = assert_raises(Inspec::Input::RequiredError) { input.value }
|
||||
ex.message.must_match /Input 'test_input' is required and does not have a value./
|
||||
Inspec::BaseCLI.inspec_cli_command = nil
|
||||
end
|
||||
end
|
||||
|
||||
#==============================================================#
|
||||
# Type Validation
|
||||
#==============================================================#
|
||||
describe 'enforce_type_validation' do
|
||||
{
|
||||
'string' => { good: 'a_string', bad: 123.3, norm: 'String' },
|
||||
'numeric' => { good: 123, bad: 'not a number', norm: 'Numeric' },
|
||||
'regex' => { good: /\d+.+/, bad: '/(.+/', norm: 'Regexp' },
|
||||
'array' => { good: [1, 2, 3], bad: { a: 1, b: 2, c: 3 }, norm: 'Array' },
|
||||
'hash' => { good: { a: 1, b: 2, c: 3 }, bad: 'i am not a hash', norm: 'Hash' },
|
||||
'boolean' => { good: true, bad: 'i am not a boolean', norm: 'Boolean' },
|
||||
}.each do |type, examples|
|
||||
it "validates a #{type} in the constructor - (good)" do
|
||||
opts = { type: type, value: examples[:good] }
|
||||
Inspec::Input.new('test_input', opts) # No exception
|
||||
end
|
||||
|
||||
it "validates a #{type} in the constructor - (bad)" do
|
||||
opts = { type: type, value: examples[:bad] }
|
||||
ex = assert_raises(Inspec::Input::ValidationError) { Inspec::Input.new('test_input', opts) }
|
||||
ex.message.must_include 'test_input'
|
||||
ex.message.must_include "'#{examples[:bad]}'"
|
||||
ex.message.must_include "does not validate to type '#{examples[:norm]}'"
|
||||
end
|
||||
|
||||
it "validates a #{type} in value= (good)" do
|
||||
att = Inspec::Input.new('test_input', type: type)
|
||||
att.value = examples[:good]
|
||||
end
|
||||
|
||||
it "validates a #{type} in the value= - (bad)" do
|
||||
att = Inspec::Input.new('test_input', type: type)
|
||||
ex = assert_raises(Inspec::Input::ValidationError) { att.value = examples[:bad] }
|
||||
ex.message.must_include 'test_input'
|
||||
ex.message.must_include "'#{examples[:bad]}'"
|
||||
ex.message.must_include "does not validate to type '#{examples[:norm]}'"
|
||||
end
|
||||
end
|
||||
it 'validates the Any type' do
|
||||
Inspec::Input.new('test_input', type: 'any', value: false) # No exception
|
||||
Inspec::Input.new('test_input', type: 'any', value: true) # No exception
|
||||
Inspec::Input.new('test_input', type: 'any', value: 'bob') # No exception
|
||||
Inspec::Input.new('test_input', type: 'any', value: 1) # No exception
|
||||
end
|
||||
end
|
||||
|
||||
#==============================================================#
|
||||
# Type Option Validation and Normalization
|
||||
#==============================================================#
|
||||
describe 'validate type option' do
|
||||
it 'converts regex to Regexp' do
|
||||
opts[:type] = 'regex'
|
||||
input.type.must_equal 'Regexp'
|
||||
end
|
||||
|
||||
it 'returns the same value if there is nothing to clean' do
|
||||
opts[:type] = 'String'
|
||||
input.type.must_equal 'String'
|
||||
end
|
||||
|
||||
it 'returns an error if a invalid type is sent' do
|
||||
opts[:type] = 'dressing'
|
||||
ex = assert_raises(Inspec::Input::TypeError) { input }
|
||||
ex.message.must_match /Type 'Dressing' is not a valid input type./
|
||||
end
|
||||
end
|
||||
|
||||
describe 'valid_regexp? method' do
|
||||
it 'validates a string regex' do
|
||||
input.send(:valid_regexp?, '/.*/').must_equal true
|
||||
end
|
||||
|
||||
it 'validates a slash regex' do
|
||||
input.send(:valid_regexp?, /.*/).must_equal true
|
||||
end
|
||||
|
||||
it 'does not validate a invalid regex' do
|
||||
input.send(:valid_regexp?, '/.*(/').must_equal false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'valid_numeric? method' do
|
||||
it 'validates a string number' do
|
||||
input.send(:valid_numeric?, '123').must_equal true
|
||||
end
|
||||
|
||||
it 'validates a float number' do
|
||||
input.send(:valid_numeric?, 44.55).must_equal true
|
||||
end
|
||||
|
||||
it 'validats a wrong padded number' do
|
||||
input.send(:valid_numeric?, '00080').must_equal true
|
||||
end
|
||||
|
||||
it 'does not vaildate a invalid number' do
|
||||
input.send(:valid_numeric?, '55.55.55.5').must_equal false
|
||||
end
|
||||
|
||||
it 'does not validate a invalid string' do
|
||||
input.send(:valid_numeric?, 'one').must_equal false
|
||||
end
|
||||
|
||||
it 'does not validate a fraction' do
|
||||
input.send(:valid_numeric?, '1/2').must_equal false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,15 +11,14 @@ tests = expecteds.keys.map do |test_name|
|
|||
name: test_name,
|
||||
expected: expecteds[test_name],
|
||||
input_via_string: attribute(test_name.to_s, value: "#{test_name}_default"),
|
||||
input_via_symbol: attribute(test_name, value: "#{test_name}_default"),
|
||||
}
|
||||
end
|
||||
|
||||
control 'flat' do
|
||||
tests.each do |info|
|
||||
describe "#{info[:name]} using string key" do
|
||||
subject { info[:input_via_string] }
|
||||
it { should eq info[:expected] }
|
||||
tests.each do |details|
|
||||
describe "#{details[:name]} using string key" do
|
||||
subject { details[:input_via_string] }
|
||||
it { should eq details[:expected] }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,47 +0,0 @@
|
|||
describe 'test the val_string input set in the global inspec.yml' do
|
||||
subject { attribute('val_string') }
|
||||
it { should cmp 'test-attr' }
|
||||
end
|
||||
|
||||
describe 'test the val_numeric input set in the global inspec.yml' do
|
||||
subject { attribute('val_numeric') }
|
||||
it { should cmp 443 }
|
||||
end
|
||||
|
||||
describe 'test the val_boolean input set in the global inspec.yml' do
|
||||
subject { attribute('val_boolean') }
|
||||
it { should cmp true }
|
||||
end
|
||||
|
||||
describe 'test the val_regex input set in the global inspec.yml' do
|
||||
subject { attribute('val_regex') }
|
||||
it { should cmp '/^\d*/'}
|
||||
end
|
||||
|
||||
describe 'test the val_array input set in the global inspec.yml' do
|
||||
subject { attribute('val_array') }
|
||||
check_array = [ 'a', 'b', 'c' ]
|
||||
it { should cmp check_array }
|
||||
end
|
||||
|
||||
describe 'test the val_hash input set in the global inspec.yml' do
|
||||
subject { attribute('val_hash') }
|
||||
check_hash = { a: true, b: false, c: '123' }
|
||||
it { should cmp check_hash }
|
||||
end
|
||||
|
||||
describe 'test input when no default or value is set' do
|
||||
subject { attribute('val_no_default').respond_to?(:fake_method) }
|
||||
it { should cmp true }
|
||||
end
|
||||
|
||||
describe 'test input with no defualt but has type' do
|
||||
subject { attribute('val_no_default_with_type').respond_to?(:fake_method) }
|
||||
it { should cmp true }
|
||||
end
|
||||
|
||||
empty_hash_input = attribute('val_with_empty_hash_default', {})
|
||||
describe 'test input with default as empty hash' do
|
||||
subject { empty_hash_input }
|
||||
it { should cmp 'success' }
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
include_controls 'child_profile_NEW_NAME'
|
||||
|
||||
include_controls 'child_profile2' do
|
||||
control 'test override control on parent using child attribute' do
|
||||
describe attribute('val_numeric') do
|
||||
it { should cmp 654321 }
|
||||
end
|
||||
end
|
||||
|
||||
control 'test override control on parent using parent attribute' do
|
||||
describe Inspec::InputRegistry.find_input('val_numeric', 'inputs').value do
|
||||
it { should cmp 443 }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +0,0 @@
|
|||
control 'test using the val_numeric_override with a default in the inspec.yml overridden by the secrets file' do
|
||||
desc 'test the val_numeric_override attr'
|
||||
describe attribute('val_numeric_override') do
|
||||
it { should cmp 9999 }
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
val_user = attribute('val_user', default: 'alice', description: 'An identification for the user')
|
||||
val_user_override = attribute('val_user_override', default: 'alice', description: 'An identification for the user')
|
||||
|
||||
describe 'reading an input in a file-level definition with a default value' do
|
||||
subject { val_user }
|
||||
it { should cmp 'alice' }
|
||||
end
|
||||
|
||||
describe 'reading an input in a file-level definition with a default value and a value in secrets file' do
|
||||
subject { val_user_override }
|
||||
it { should cmp 'bob' }
|
||||
end
|
||||
|
||||
control 'test using an input inside a control block as the describe subject' do
|
||||
desc 'test the val_numeric attr'
|
||||
describe attribute('val_user') do
|
||||
it { should cmp 'alice' }
|
||||
end
|
||||
end
|
||||
|
||||
# test using a input outside controls and as the describe subject
|
||||
describe attribute('val_user') do
|
||||
it { should cmp 'alice' }
|
||||
end
|
||||
|
||||
control "test using inputs in the test it's block" do
|
||||
describe 'alice' do
|
||||
it { should cmp attribute('val_user') }
|
||||
end
|
||||
end
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
val_user_override: bob
|
||||
val_numeric_override: 9999
|
||||
val_with_empty_hash_default: 'success'
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
lockfile_version: 1
|
||||
depends:
|
||||
- name: child_profile_NEW_NAME
|
||||
resolved_source:
|
||||
url: https://example.com/child_profile.tar.gz
|
||||
sha256: e39eb85366b272bae98e5eecdfac9f84c50a9ae9dd625fba2ce847268a6c3477
|
||||
version_constraints: ">= 0"
|
||||
- name: child_profile2
|
||||
resolved_source:
|
||||
url: https://example.com/child_profile2.tar.gz
|
||||
sha256: f24eb85366b272bae98e5eecdfac9f84c50a9ae9dd625fba2ce847268a6c3477
|
||||
version_constraints: ">= 0"
|
|
@ -1,43 +0,0 @@
|
|||
name: global-inputs
|
||||
title: Profile to test inputs in a variety of locations
|
||||
maintainer: The Authors
|
||||
copyright: The Authors
|
||||
copyright_email: you@example.com
|
||||
license: Apache-2.0
|
||||
summary: An InSpec Compliance Profile
|
||||
version: 0.1.0
|
||||
depends:
|
||||
- name: child_profile_NEW_NAME
|
||||
url: https://example.com/child_profile.tar.gz
|
||||
- url: https://example.com/child_profile2.tar.gz
|
||||
attributes:
|
||||
- name: val_numeric
|
||||
type: numeric
|
||||
default: 443
|
||||
- name: val_numeric_override
|
||||
type: numeric
|
||||
default: '72.88'
|
||||
- name: val_string
|
||||
type: string
|
||||
default: 'test-attr'
|
||||
- name: val_boolean
|
||||
type: boolean
|
||||
default: true
|
||||
- name: val_regex
|
||||
type: regex
|
||||
default: '/^\d*/'
|
||||
- name: val_array
|
||||
type: array
|
||||
default:
|
||||
- a
|
||||
- b
|
||||
- c
|
||||
- name: val_hash
|
||||
type: hash
|
||||
default:
|
||||
a: true
|
||||
b: false
|
||||
c: '123'
|
||||
- name: val_no_default
|
||||
- name: val_no_default_with_type
|
||||
type: hash
|
|
@ -1,4 +0,0 @@
|
|||
# Demo Compliance Profile
|
||||
|
||||
copyright: Demo Company Ltd
|
||||
license: All rights reserved
|
|
@ -1,10 +0,0 @@
|
|||
describe 'test child attribute when using a profile with a name override' do
|
||||
subject { attribute('val_numeric') }
|
||||
it { should cmp '123456' }
|
||||
end
|
||||
|
||||
control 'test child attribute inside a it block when using a profile with a name override' do
|
||||
describe '123456' do
|
||||
it { should cmp attribute('val_numeric') }
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
name: child_profile
|
||||
title: Child Profile
|
||||
maintainer: Demo, Inc.
|
||||
copyright: Demo, Inc.
|
||||
copyright_email: support@example.com
|
||||
license: Apache-2.0
|
||||
summary: My Profile 1 summary
|
||||
version: 1.0.0
|
||||
attributes:
|
||||
- name: val_numeric
|
||||
type: numeric
|
||||
default: 123456
|
|
@ -1,4 +0,0 @@
|
|||
# Demo Compliance Profile
|
||||
|
||||
copyright: Demo Company Ltd
|
||||
license: All rights reserved
|
|
@ -1,22 +0,0 @@
|
|||
describe 'test child attribute when using a profile without a name override' do
|
||||
subject { attribute('val_numeric') }
|
||||
it { should cmp 654321 }
|
||||
end
|
||||
|
||||
control 'test override control on parent using child attribute' do
|
||||
describe 'dummy' do
|
||||
it { should cmp 9999 }
|
||||
end
|
||||
end
|
||||
|
||||
control 'test override control on parent using parent attribute' do
|
||||
describe 'dummy' do
|
||||
it { should cmp 9999 }
|
||||
end
|
||||
end
|
||||
|
||||
control 'test child attribute inside a it block when using a profile without a name override' do
|
||||
describe '654321' do
|
||||
it { should cmp attribute('val_numeric') }
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
name: child_profile2
|
||||
title: Child Profile2
|
||||
maintainer: Demo, Inc.
|
||||
copyright: Demo, Inc.
|
||||
copyright_email: support@example.com
|
||||
license: Apache-2.0
|
||||
summary: My Profile 1 summary
|
||||
version: 1.0.0
|
||||
attributes:
|
||||
- name: val_numeric
|
||||
type: numeric
|
||||
default: 654321
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
control 'child-01-control-01' do
|
||||
describe attribute('test-01') do
|
||||
# This is an independent value, inheritance-child-01/test-01
|
||||
it { should cmp 'value-from-child-01-metadata' }
|
||||
end
|
||||
end
|
||||
|
||||
control 'child-01-control-02' do
|
||||
describe attribute('test-02') do
|
||||
# This value was set by the wrapper, inheritance-child-01/test-02
|
||||
it { should cmp 'value-from-wrapper-metadata' }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
name: inheritance-child-01
|
||||
title: Tests inputs being set accross inheritance boundaries
|
||||
maintainer: InSpec Core Engineering Team
|
||||
copyright: InSpec Core Engineering Team
|
||||
copyright_email: inspec@chef.io
|
||||
license: Apache-2.0
|
||||
summary: Tests inputs being set accross inheritance boundaries
|
||||
version: 0.1.0
|
||||
supports:
|
||||
platform: os
|
||||
|
||||
attributes:
|
||||
- name: test-01
|
||||
value: value-from-child-01-metadata
|
||||
- name: test-02
|
||||
value: value-from-child-01-metadata
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
control 'child-02-control-01' do
|
||||
describe attribute('test-03') do
|
||||
# This value was set by the wrapper via an alias, inheritance-child-02/test-02
|
||||
it { should cmp 'value-from-wrapper-metadata' }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
name: inheritance-child-02
|
||||
title: Tests inputs being set accross inheritance boundaries
|
||||
maintainer: InSpec Core Engineering Team
|
||||
copyright: InSpec Core Engineering Team
|
||||
copyright_email: inspec@chef.io
|
||||
license: Apache-2.0
|
||||
summary: Tests inputs being set accross inheritance boundaries
|
||||
version: 0.1.0
|
||||
supports:
|
||||
platform: os
|
||||
|
||||
attributes:
|
||||
# Set a value in an aliased child profile
|
||||
- name: test-03
|
||||
value: value-from-child-02-metadata
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
# inheritance-child-01 is a simple dependency
|
||||
include_controls('inheritance-child-01')
|
||||
|
||||
# inheritance-child-02 is an aliased dependency
|
||||
include_controls('inheritance-child-02-aliased')
|
||||
|
||||
control 'wrapper-control-01' do
|
||||
describe attribute('test-01') do
|
||||
# This is an independent value, inheritance-wrapper/test-01
|
||||
it { should cmp 'value-from-wrapper-metadata' }
|
||||
end
|
||||
input_object('test-01')
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
name: inheritance-wrapper
|
||||
title: Tests inputs being set accross inheritance boundaries
|
||||
maintainer: InSpec Core Engineering Team
|
||||
copyright: InSpec Core Engineering Team
|
||||
copyright_email: inspec@chef.io
|
||||
license: Apache-2.0
|
||||
summary: Tests inputs being set accross inheritance boundaries
|
||||
version: 0.1.0
|
||||
supports:
|
||||
platform: os
|
||||
|
||||
depends:
|
||||
- path: ../child-01
|
||||
# Assign this one a name, which does not match the name in child-02/inspec.yml
|
||||
# This should cause an alias record to be created in InputRegistry
|
||||
- name: inheritance-child-02-aliased
|
||||
path: ../child-02
|
||||
|
||||
attributes:
|
||||
# test-01 is set in both wrapper metadata and child-01 metadata
|
||||
# and tested for each profile's idea of the value
|
||||
# All profiles are namespaced, so they exist independently.
|
||||
- name: test-01
|
||||
value: value-from-wrapper-metadata
|
||||
# Set a value in a child profile from a wrapper profile
|
||||
- name: test-02
|
||||
profile: inheritance-child-01
|
||||
value: value-from-wrapper-metadata
|
||||
# Set a value in an aliased child profile
|
||||
- name: test-03
|
||||
profile: inheritance-child-02-aliased
|
||||
value: value-from-wrapper-metadata
|
|
@ -0,0 +1 @@
|
|||
attribute('a_required_input')
|
|
@ -7,6 +7,6 @@ license: Apache-2.0
|
|||
summary: An InSpec Compliance Profile
|
||||
version: 0.1.0
|
||||
attributes:
|
||||
- name: username
|
||||
- name: a_required_input
|
||||
type: string
|
||||
required: true
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
# This should simply not error
|
||||
unless attribute('test-01') == 'test-01'
|
||||
raise 'Failed bare input access'
|
||||
end
|
||||
|
||||
describe attribute('test-02') do
|
||||
it { should cmp 'test-02' }
|
||||
end
|
||||
|
||||
describe 'mentioning an input in a bare describe block as a redirected subject' do
|
||||
subject { attribute('test-03') }
|
||||
it { should cmp 'test-03' }
|
||||
end
|
||||
|
||||
control 'test using an input inside a control block as the describe subject' do
|
||||
desc 'test the val_numeric attr'
|
||||
describe attribute('test-04') do
|
||||
it { should cmp 'test-04' }
|
||||
end
|
||||
end
|
||||
|
||||
control "test using inputs in the test its block" do
|
||||
describe 'test-05' do
|
||||
it { should cmp attribute('test-05') }
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: add test for OR
|
20
test/unit/mock/profiles/inputs/scoping/inspec.yml
Normal file
20
test/unit/mock/profiles/inputs/scoping/inspec.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
name: input-scoping
|
||||
title: Profile to test reading attributes in a variety of scopes
|
||||
maintainer: Chef InSpec team
|
||||
copyright: Chef InSpec team
|
||||
copyright_email: inspec@chef.io
|
||||
license: Apache-2.0
|
||||
summary: Profile to test reading attributes in a variety of scopes
|
||||
version: 0.1.0
|
||||
|
||||
attributes:
|
||||
- name: test-01
|
||||
value: test-01
|
||||
- name: test-02
|
||||
value: test-02
|
||||
- name: test-03
|
||||
value: test-03
|
||||
- name: test-04
|
||||
value: test-04
|
||||
- name: test-05
|
||||
value: test-05
|
|
@ -24,12 +24,5 @@ module PluginV2BackCompat
|
|||
assert_equal Inspec::Plugins::SourceReader, klass
|
||||
end
|
||||
|
||||
def test_get_plugin_v1_base_for_secrets
|
||||
klass = Inspec.secrets(1)
|
||||
assert_kind_of Class, klass
|
||||
assert Inspec::Plugins.const_defined? :Secret
|
||||
assert_equal Inspec::Plugins::Secret, klass
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -2,141 +2,74 @@
|
|||
# copyright: 2017, Chef Software Inc.
|
||||
|
||||
require 'helper'
|
||||
require 'inspec/secrets'
|
||||
|
||||
describe Inspec::Runner do
|
||||
describe '#load_inputs' do
|
||||
let(:runner) { Inspec::Runner.new({ command_runner: :generic }) }
|
||||
let(:runner) { Inspec::Runner.new({ command_runner: :generic }) }
|
||||
|
||||
before do
|
||||
Inspec::Runner.any_instance.stubs(:validate_inputs_file_readability!)
|
||||
# =============================================================== #
|
||||
# Reporter Options
|
||||
# =============================================================== #
|
||||
|
||||
describe 'confirm reporter defaults to cli' do
|
||||
it 'defaults to cli when format and reporter not set' do
|
||||
opts = { command_runner: :generic, backend_cache: true }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
config = runner.instance_variable_get(:"@conf")
|
||||
expected = { 'cli' => { 'stdout' => true } }
|
||||
config['reporter'].must_equal expected
|
||||
end
|
||||
|
||||
describe 'confirm reporter defaults to cli' do
|
||||
it 'defaults to cli when format and reporter not set' do
|
||||
opts = { command_runner: :generic, backend_cache: true }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
config = runner.instance_variable_get(:"@conf")
|
||||
expected = { 'cli' => { 'stdout' => true } }
|
||||
config['reporter'].must_equal expected
|
||||
end
|
||||
|
||||
it 'does not default when format is set' do
|
||||
opts = { command_runner: :generic, backend_cache: true, 'reporter' => ['json'] }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
config = runner.instance_variable_get(:"@conf")
|
||||
expected = { 'json' => { 'stdout' => true } }
|
||||
config['reporter'].must_equal expected
|
||||
end
|
||||
|
||||
it 'delets format if set to a rspec format' do
|
||||
opts = { command_runner: :generic, backend_cache: true, 'reporter' => ['progress'] }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
config = runner.instance_variable_get(:"@conf")
|
||||
config['reporter'].must_equal Hash.new
|
||||
end
|
||||
it 'does not default when format is set' do
|
||||
opts = { command_runner: :generic, backend_cache: true, 'reporter' => ['json'] }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
config = runner.instance_variable_get(:"@conf")
|
||||
expected = { 'json' => { 'stdout' => true } }
|
||||
config['reporter'].must_equal expected
|
||||
end
|
||||
|
||||
describe 'testing runner.run exit codes' do
|
||||
it 'returns proper exit code when no profile is added' do
|
||||
proc { runner.run.must_equal 0 }
|
||||
end
|
||||
it 'delets format if set to a rspec format' do
|
||||
opts = { command_runner: :generic, backend_cache: true, 'reporter' => ['progress'] }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
config = runner.instance_variable_get(:"@conf")
|
||||
config['reporter'].must_equal Hash.new
|
||||
end
|
||||
end
|
||||
|
||||
# =============================================================== #
|
||||
# Exit Codes
|
||||
# =============================================================== #
|
||||
|
||||
describe 'testing runner.run exit codes' do
|
||||
it 'returns proper exit code when no profile is added' do
|
||||
proc { runner.run.must_equal 0 }
|
||||
end
|
||||
end
|
||||
|
||||
# =============================================================== #
|
||||
# Backend Caching
|
||||
# =============================================================== #
|
||||
|
||||
describe 'when backend caching is enabled' do
|
||||
it 'returns a backend with caching' do
|
||||
opts = { command_runner: :generic, backend_cache: true }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
backend = runner.instance_variable_get(:@backend)
|
||||
backend.backend.cache_enabled?(:command).must_equal true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when backend caching is disabled' do
|
||||
it 'returns a backend without caching' do
|
||||
opts = { command_runner: :generic, backend_cache: false }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
backend = runner.instance_variable_get(:@backend)
|
||||
backend.backend.cache_enabled?(:command).must_equal false
|
||||
end
|
||||
|
||||
describe 'when backend caching is enabled' do
|
||||
it 'returns a backend with caching' do
|
||||
opts = { command_runner: :generic, backend_cache: true }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
backend = runner.instance_variable_get(:@backend)
|
||||
backend.backend.cache_enabled?(:command).must_equal true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when backend caching is disabled' do
|
||||
it 'returns a backend without caching' do
|
||||
opts = { command_runner: :generic, backend_cache: false }
|
||||
runner = Inspec::Runner.new(opts)
|
||||
backend = runner.instance_variable_get(:@backend)
|
||||
backend.backend.cache_enabled?(:command).must_equal false
|
||||
end
|
||||
|
||||
it 'returns a backend without caching as default' do
|
||||
backend = runner.instance_variable_get(:@backend)
|
||||
backend.backend.cache_enabled?(:command).must_equal false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when no input files are specified' do
|
||||
it 'returns an empty hash' do
|
||||
options = {}
|
||||
runner.load_inputs(options).must_equal({})
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when an input file is provided and does not resolve' do
|
||||
it 'raises an exception' do
|
||||
options = { input_file: ['nope.jpg'] }
|
||||
Inspec::SecretsBackend.expects(:resolve).with('nope.jpg').returns(nil)
|
||||
proc { runner.load_inputs(options) }.must_raise Inspec::Exceptions::SecretsBackendNotFound
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when an input file is provided and has no inputs' do
|
||||
it 'returns an empty hash' do
|
||||
secrets = mock
|
||||
secrets.stubs(:inputs).returns(nil)
|
||||
options = { input_file: ['empty.yaml'] }
|
||||
Inspec::SecretsBackend.expects(:resolve).with('empty.yaml').returns(secrets)
|
||||
runner.load_inputs(options).must_equal({})
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when an input file is provided and has inputs' do
|
||||
it 'returns a hash containing the inputs' do
|
||||
options = { input_file: ['file1.yaml'] }
|
||||
inputs = { foo: 'bar' }
|
||||
secrets = mock
|
||||
secrets.stubs(:inputs).returns(inputs)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets)
|
||||
runner.load_inputs(options).must_equal(inputs)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when multiple input files are provided and one fails' do
|
||||
it 'raises an exception' do
|
||||
options = { input_file: ['file1.yaml', 'file2.yaml'] }
|
||||
secrets = mock
|
||||
secrets.stubs(:inputs).returns(nil)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(nil)
|
||||
proc { runner.load_inputs(options) }.must_raise Inspec::Exceptions::SecretsBackendNotFound
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when multiple input files are provided and one has no inputs' do
|
||||
it 'returns a hash containing the inputs from the valid files' do
|
||||
options = { input_file: ['file1.yaml', 'file2.yaml'] }
|
||||
inputs = { foo: 'bar' }
|
||||
secrets1 = mock
|
||||
secrets1.stubs(:inputs).returns(nil)
|
||||
secrets2 = mock
|
||||
secrets2.stubs(:inputs).returns(inputs)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets1)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(secrets2)
|
||||
runner.load_inputs(options).must_equal(inputs)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when multiple input files are provided and all have inputs' do
|
||||
it 'returns a hash containing all the inputs' do
|
||||
options = { input_file: ['file1.yaml', 'file2.yaml'] }
|
||||
secrets1 = mock
|
||||
secrets1.stubs(:inputs).returns({ key1: 'value1' })
|
||||
secrets2 = mock
|
||||
secrets2.stubs(:inputs).returns({ key2: 'value2' })
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file1.yaml').returns(secrets1)
|
||||
Inspec::SecretsBackend.expects(:resolve).with('file2.yaml').returns(secrets2)
|
||||
runner.load_inputs(options).must_equal({ key1: 'value1', key2: 'value2' })
|
||||
end
|
||||
it 'returns a backend without caching as default' do
|
||||
backend = runner.instance_variable_get(:@backend)
|
||||
backend.backend.cache_enabled?(:command).must_equal false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue