Merge pull request #994 from chef/ssd/resource-isolation

Initial attempt at isolating resources between dependencies
This commit is contained in:
Christoph Hartmann 2016-09-05 11:07:29 +02:00 committed by GitHub
commit 90a8584d10
25 changed files with 573 additions and 391 deletions

View file

@ -70,7 +70,7 @@ module Inspec
o[:logger].level = get_log_level(o.log_level)
runner = Inspec::Runner.new(o)
targets.each { |target| runner.add_target(target, opts) }
targets.each { |target| runner.add_target(target) }
exit runner.run
rescue RuntimeError, Train::UserError => e
$stderr.puts e.message

View file

@ -0,0 +1,145 @@
# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann
require 'inspec/dsl'
require 'inspec/dsl_shared'
module Inspec
#
# ControlEvalContext constructs an anonymous class that control
# files will be instance_exec'd against.
#
# The anonymous class includes the given passed resource_dsl as well
# as the basic DSL of the control files (describe, control, title,
# etc).
#
class ControlEvalContext
# Create the context for controls. This includes all components of the DSL,
# including matchers and resources.
#
# @param [ResourcesDSL] resources_dsl which has all resources to attach
# @return [RuleContext] the inner context of rules
def self.rule_context(resources_dsl)
require 'rspec/core/dsl'
Class.new(Inspec::Rule) do
include RSpec::Core::DSL
include resources_dsl
end
end
# Creates the heart of the control eval context:
#
# An instantiated object which has all resources registered to it
# and exposes them to the a test file.
#
# @param profile_context [Inspec::ProfileContext]
# @param outer_dsl [OuterDSLClass]
# @return [ProfileContextClass]
#
# rubocop:disable Lint/NestedMethodDefinition
def self.create(profile_context, resources_dsl) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
rule_class = rule_context(resources_dsl)
profile_context_owner = profile_context
profile_id = profile_context.profile_id
Class.new do
include Inspec::DSL
include Inspec::DSL::RequireOverride
include resources_dsl
def initialize(backend, conf, dependencies, require_loader)
@backend = backend
@conf = conf
@dependencies = dependencies
@require_loader = require_loader
@skip_profile = false
end
define_method :title do |arg|
profile_context_owner.set_header(:title, arg)
end
def to_s
"Control Evaluation Context (#{profile_name})"
end
define_method :profile_name do
profile_id
end
define_method :control do |*args, &block|
id = args[0]
opts = args[1] || {}
register_control(rule_class.new(id, profile_id, opts, &block))
end
#
# Describe allows users to write rspec-like bare describe
# blocks without declaring an inclosing control. Here, we
# generate a control for them automatically and then execute
# the describe block in the context of that control.
#
define_method :describe do |*args, &block|
loc = block_location(block, caller[0])
id = "(generated from #{loc} #{SecureRandom.hex})"
res = nil
rule = rule_class.new(id, profile_id, {}) do
res = describe(*args, &block)
end
register_control(rule, &block)
res
end
define_method :add_resources do |context|
self.class.class_eval do
include context.to_resources_dsl
end
rule_class.class_eval do
include context.to_resources_dsl
end
end
define_method :add_subcontext do |context|
profile_context_owner.add_subcontext(context)
end
define_method :register_control do |control, &block|
::Inspec::Rule.set_skip_rule(control, true) if @skip_profile
profile_context_owner.register_rule(control, &block) unless control.nil?
end
# method for attributes; import attribute handling
define_method :attribute do |name, options|
profile_context_owner.register_attribute(name, options)
end
define_method :skip_control do |id|
profile_context_owner.unregister_rule(id)
end
def only_if
return unless block_given?
@skip_profile ||= !yield
end
alias_method :rule, :control
alias_method :skip_rule, :skip_control
private
def block_location(block, alternate_caller)
if block.nil?
alternate_caller[/^(.+:\d+):in .+$/, 1] || 'unknown'
else
path, line = block.source_location
"#{File.basename(path)}:#{line}"
end
end
end
end
end
end

View file

@ -18,14 +18,22 @@ module Inspec
# @param cwd [String] Current working directory for relative path includes
# @param vendor_path [String] Path to the vendor directory
#
def self.from_lockfile(lockfile, cwd, vendor_path)
def self.from_lockfile(lockfile, cwd, vendor_path, backend)
vendor_index = VendorIndex.new(vendor_path)
dep_tree = lockfile.deps.map do |dep|
Inspec::Requirement.from_lock_entry(dep, cwd, vendor_index)
Inspec::Requirement.from_lock_entry(dep, cwd, vendor_index, backend)
end
dep_list = flatten_dep_tree(dep_tree)
new(cwd, vendor_path, dep_list)
new(cwd, vendor_path, dep_list, backend)
end
def self.from_array(dependencies, cwd, vendor_path, backend)
dep_list = {}
dependencies.each do |d|
dep_list[d.name] = d
end
new(cwd, vendor_path, dep_list, backend)
end
# This is experimental code to test the working of the
@ -50,14 +58,21 @@ module Inspec
# @param cwd [String] current working directory for relative path includes
# @param vendor_path [String] path which contains vendored dependencies
# @return [dependencies] this
def initialize(cwd, vendor_path, dep_list = nil)
def initialize(cwd, vendor_path, dep_list, backend)
@cwd = cwd
@vendor_path = vendor_path
@dep_list = dep_list
@backend = backend
end
def each
@dep_list.each do |_k, v|
yield v.profile
end
end
def list
@dep_list
@dep_list || {}
end
def to_array
@ -77,7 +92,7 @@ module Inspec
def vendor(dependencies)
return nil if dependencies.nil? || dependencies.empty?
@vendor_index ||= VendorIndex.new(@vendor_path)
@dep_list = Resolver.resolve(dependencies, @vendor_index, @cwd)
@dep_list = Resolver.resolve(dependencies, @vendor_index, @cwd, @backend)
end
end
end

View file

@ -1,5 +1,6 @@
# encoding: utf-8
require 'inspec/fetcher'
require 'inspec/dependencies/dependency_set'
require 'digest'
module Inspec
@ -18,17 +19,18 @@ module Inspec
new(name, version, vendor_index, opts[:cwd], opts.merge(dep))
end
def self.from_lock_entry(entry, cwd, vendor_index)
def self.from_lock_entry(entry, cwd, vendor_index, backend)
req = new(entry['name'],
entry['version_constraints'],
vendor_index,
cwd, { url: entry['resolved_source'] })
cwd,
{ url: entry['resolved_source'],
backend: backend })
locked_deps = []
Array(entry['dependencies']).each do |dep_entry|
locked_deps << Inspec::Requirement.from_lock_entry(dep_entry, cwd, vendor_index)
locked_deps << Inspec::Requirement.from_lock_entry(dep_entry, cwd, vendor_index, backend)
end
req.lock_deps(locked_deps)
req
end
@ -38,6 +40,7 @@ module Inspec
@version_requirement = Gem::Requirement.new(Array(version_constraints))
@dep = Gem::Dependency.new(name, @version_requirement, :runtime)
@vendor_index = vendor_index
@backend = opts[:backend]
@opts = opts
@cwd = cwd
end
@ -123,7 +126,7 @@ module Inspec
def dependencies
@dependencies ||= profile.metadata.dependencies.map do |r|
Inspec::Requirement.from_metadata(r, @vendor_index, cwd: @cwd)
Inspec::Requirement.from_metadata(r, @vendor_index, cwd: @cwd, backend: @backend)
end
end
@ -137,7 +140,11 @@ module Inspec
def profile
return nil if path.nil?
@profile ||= Inspec::Profile.for_target(path, {})
opts = { backend: @backend }
if !@dependencies.nil?
opts[:dependencies] = Inspec::DependencySet.from_array(@dependencies, @cwd, @vendor_index, @backend)
end
@profile ||= Inspec::Profile.for_target(path, opts)
end
end
end

View file

@ -23,9 +23,9 @@ module Inspec
# implementation of the fetcher being used.
#
class Resolver
def self.resolve(dependencies, vendor_index, working_dir)
def self.resolve(dependencies, vendor_index, working_dir, backend)
reqs = dependencies.map do |dep|
req = Inspec::Requirement.from_metadata(dep, vendor_index, cwd: working_dir)
req = Inspec::Requirement.from_metadata(dep, vendor_index, cwd: working_dir, backend: backend)
req || fail("Cannot initialize dependency: #{req}")
end
new.resolve(reqs)

View file

@ -18,19 +18,6 @@ module Inspec::DSL
alias require_rules require_controls
alias include_rules include_controls
def self.rule_from_check(m, a, b)
if a.is_a?(Array) && !a.empty? &&
a[0].respond_to?(:resource_skipped) &&
!a[0].resource_skipped.nil?
::Inspec::Rule.__send__(m, *a) do
it a[0].resource_skipped
end
else
# execute the method
::Inspec::Rule.__send__(m, *a, &b)
end
end
def self.load_spec_files_for_profile(bind_context, opts, &block)
dependencies = opts[:dependencies]
profile_id = opts[:profile_id]
@ -43,25 +30,21 @@ of #{bind_context.profile_name}.
Dependencies available from this context are:
#{dependencies.list.keys.join("\n ")}
#{dependencies.list.keys.join("\n ")}
EOF
end
context = load_profile_context(dep_entry.profile, opts[:backend])
context = dep_entry.profile.runner_context
# if we don't want all the rules, then just make 1 pass to get all rule_IDs
# that we want to keep from the original
filter_included_controls(context, dep_entry.profile, &block) if !opts[:include_all]
# interpret the block and skip/modify as required
context.load(block) if block_given?
bind_context.add_subcontext(context)
end
def self.filter_included_controls(context, profile, &block)
mock = Inspec::Backend.create({ backend: 'mock' })
include_ctx = Inspec::ProfileContext.for_profile(profile, mock)
include_ctx = profile.runner_context
include_ctx.load(block) if block_given?
# remove all rules that were not registered
context.rules.keys.each do |id|
@ -70,16 +53,4 @@ EOF
end
end
end
def self.load_profile_context(profile, backend)
ctx = Inspec::ProfileContext.for_profile(profile, backend)
profile.libraries.each do |path, content|
ctx.load(content.to_s, path, 1)
ctx.reload_dsl
end
profile.tests.each do |path, content|
ctx.load(content.to_s, path, 1)
end
ctx
end
end

25
lib/inspec/dsl_shared.rb Normal file
View file

@ -0,0 +1,25 @@
# encoding: utf-8
module Inspec
#
# Contains methods we would like in multiple DSL
#
module DSL
module RequireOverride
# Save the toplevel require method to load all ruby dependencies.
# It is used whenever the `require 'lib'` is not in libraries.
alias __ruby_require require
def require(path)
rbpath = path + '.rb'
return __ruby_require(path) if !@require_loader.exists?(rbpath)
return false if @require_loader.loaded?(rbpath)
# This is equivalent to calling `require 'lib'` with lib on disk.
# We cannot rely on libraries residing on disk however.
# TODO: Sandboxing.
content, path, line = @require_loader.load(rbpath)
eval(content, TOPLEVEL_BINDING, path, line) # rubocop:disable Lint/Eval
end
end
end
end

View file

@ -0,0 +1,47 @@
# encoding: utf-8
# author: Steven Danna
# author: Victoria Jeffrey
require 'inspec/plugins/resource'
require 'inspec/dsl_shared'
module Inspec
#
# LibaryEvalContext constructs an instance of an anonymous class
# that library files will be instance_exec'd against.
#
# The anonymous class ensures that `Inspec.resource(1)` will return
# an anonymouse class that is suitable as the parent class of an
# inspec resource. The class returned will have the resource
# registry used by all dsl methods bound to the resource registry
# passed into the #create constructor.
#
#
class LibraryEvalContext
def self.create(registry, require_loader)
c = Class.new do
extend Inspec::ResourceDSL
include Inspec::ResourceBehaviors
define_singleton_method :__resource_registry do
registry
end
end
c2 = Class.new do
define_singleton_method :resource do |version|
Inspec.validate_resource_dsl_version!(version)
c
end
end
c3 = Class.new do
include Inspec::DSL::RequireOverride
def initialize(require_loader) # rubocop:disable Lint/NestedMethodDefinition
@require_loader = require_loader
end
end
c3.const_set(:Inspec, c2)
c3.new(require_loader)
end
end
end

View file

@ -3,83 +3,83 @@
# author: Christoph Hartmann
module Inspec
module Plugins
class Resource
def self.name(name = nil)
return if name.nil?
@name = name
Inspec::Plugins::Resource.__register(name, self)
end
def self.desc(description = nil)
return if description.nil?
Inspec::Resource.registry[@name].desc(description)
end
def self.example(example = nil)
return if example.nil?
Inspec::Resource.registry[@name].example(example)
end
def self.__register(name, obj)
# rubocop:disable Lint/NestedMethodDefinition, Lint/DuplicateMethods
cl = Class.new(obj) do
# add some common methods
include Inspec::Plugins::ResourceCommon
def initialize(backend, name, *args)
# attach the backend to this instance
@__backend_runner__ = backend
@__resource_name__ = name
# call the resource initializer
super(*args)
end
def self.desc(description = nil)
return @description if description.nil?
@description = description
end
def self.example(example = nil)
return @example if example.nil?
@example = example
end
def inspec
@__backend_runner__
end
end
# rubocop:enable Lint/NestedMethodDefinition
# add the resource to the registry by name with a newly-named registry class
klass_name = name.split('_').map(&:capitalize).join
Inspec::Resource::Registry.const_set(klass_name, cl)
Inspec::Resource.registry[name] = Inspec::Resource::Registry.const_get(klass_name)
end
# Define methods which are available to all resources
# and may be overwritten.
# Print the name of the resource
def to_s
@__resource_name__
end
# Overwrite inspect to provide better output to RSpec results.
#
# @return [String] full name of the resource
def inspect
to_s
end
module ResourceBehaviors
def to_s
@__resource_name__
end
module ResourceCommon
def resource_skipped
@resource_skipped
# Overwrite inspect to provide better output to RSpec results.
#
# @return [String] full name of the resource
def inspect
to_s
end
end
module ResourceDSL
def name(name = nil)
return if name.nil?
@name = name
__register(name, self)
end
def desc(description = nil)
return if description.nil?
__resource_registry[@name].desc(description)
end
def example(example = nil)
return if example.nil?
__resource_registry[@name].example(example)
end
def __resource_registry
Inspec::Resource.registry
end
def __register(name, obj)
# rubocop:disable Lint/NestedMethodDefinition
cl = Class.new(obj) do
def initialize(backend, name, *args)
# attach the backend to this instance
@__backend_runner__ = backend
@__resource_name__ = name
# call the resource initializer
super(*args)
end
def self.desc(description = nil)
return @description if description.nil?
@description = description
end
def self.example(example = nil)
return @example if example.nil?
@example = example
end
def resource_skipped
@resource_skipped
end
def skip_resource(message)
@resource_skipped = message
end
def inspec
@__backend_runner__
end
end
def skip_resource(message)
@resource_skipped = message
end
# rubocop:enable Lint/NestedMethodDefinition
__resource_registry[name] = cl
end
end
module Plugins
class Resource
extend Inspec::ResourceDSL
include Inspec::ResourceBehaviors
end
end
end

View file

@ -8,6 +8,9 @@ require 'inspec/polyfill'
require 'inspec/fetcher'
require 'inspec/source_reader'
require 'inspec/metadata'
require 'inspec/backend'
require 'inspec/rule'
require 'inspec/profile_context'
require 'inspec/dependencies/lockfile'
require 'inspec/dependencies/dependency_set'
@ -31,7 +34,7 @@ module Inspec
reader
end
def self.for_target(target, opts)
def self.for_target(target, opts = {})
new(resolve_target(target), opts.merge(target: target))
end
@ -47,9 +50,14 @@ module Inspec
@target = @options.delete(:target)
@logger = @options[:logger] || Logger.new(nil)
@source_reader = source_reader
if options[:dependencies]
@locked_dependencies = options[:dependencies]
end
@controls = options[:controls] || []
@profile_id = @options[:id]
@runner_context = nil
@backend = @options[:backend] || Inspec::Backend.create(options)
Metadata.finalize(@source_reader.metadata, @profile_id)
@runner_context = @options[:profile_context] || Inspec::ProfileContext.for_profile(self, @backend)
end
def name
@ -60,6 +68,46 @@ module Inspec
@params ||= load_params
end
def collect_tests(include_list = @controls)
if !@tests_collected
locked_dependencies.each(&:collect_tests)
tests.each do |path, content|
next if content.nil? || content.empty?
abs_path = source_reader.target.abs_path(path)
@runner_context.load_control_file(content, abs_path, nil)
end
@tests_collected = true
end
filter_controls(@runner_context.all_rules, include_list)
end
def filter_controls(controls_array, include_list)
return controls_array if include_list.nil? || include_list.empty?
controls_array.select do |c|
id = ::Inspec::Rule.rule_id(c)
include_list.include?(id)
end
end
def load_libraries
locked_dependencies.each do |d|
c = d.load_libraries
@runner_context.add_resources(c)
end
libs = libraries.map do |path, content|
[content, path]
end
@runner_context.load_libraries(libs)
@runner_context
end
def to_s
"Inspec::Profile<#{name}>"
end
def info
res = params.dup
# add information about the controls
@ -248,11 +296,15 @@ module Inspec
# @return [Inspec::Lockfile]
#
def generate_lockfile(vendor_path = nil)
res = Inspec::DependencySet.new(cwd, vendor_path)
res = Inspec::DependencySet.new(cwd, vendor_path, nil, @backend)
res.vendor(metadata.dependencies)
Inspec::Lockfile.from_dependency_set(res)
end
def load_dependencies
Inspec::DependencySet.from_lockfile(lockfile, cwd, nil, @backend)
end
private
# Create an archive name for this profile and an additional options
@ -281,34 +333,17 @@ module Inspec
params
end
#
# Returns a new runner for the current profile.
#
# @params [Symbol] The type of backend to use when constructing a
# new runner.
# @returns [Inspec::Runner]
#
def runner_for_profile(backend = :mock)
opts = @options.dup
opts[:ignore_supports] = true
r = Runner.new(id: @profile_id,
backend: backend,
test_collector: opts.delete(:test_collector))
r.add_profile(self, opts)
r
end
def load_checks_params(params)
load_libraries
tests = collect_tests
params[:controls] = controls = {}
params[:groups] = groups = {}
prefix = @source_reader.target.prefix || ''
# Load from the runner_context if it exists
runner = @runner_context || runner_for_profile
runner.all_rules.each do |rule|
tests.each do |rule|
f = load_rule_filepath(prefix, rule)
load_rule(rule, f, controls, groups)
end
params[:attributes] = runner.attributes
params[:attributes] = @runner_context.attributes
params
end
@ -337,9 +372,5 @@ module Inspec
}
groups[file][:controls].push(id)
end
def load_dependencies
Inspec::DependencySet.from_lockfile(lockfile, cwd, nil)
end
end
end

View file

@ -3,7 +3,9 @@
# author: Christoph Hartmann
require 'inspec/log'
require 'inspec/rule'
require 'inspec/dsl'
require 'inspec/resource'
require 'inspec/library_eval_context'
require 'inspec/control_eval_context'
require 'inspec/require_loader'
require 'securerandom'
require 'inspec/objects/attribute'
@ -14,7 +16,7 @@ module Inspec
new(profile.name, backend, { 'profile' => profile })
end
attr_reader :attributes, :rules, :profile_id
attr_reader :attributes, :rules, :profile_id, :resource_registry
def initialize(profile_id, backend, conf)
if backend.nil?
fail 'ProfileContext is initiated with a backend == nil. ' \
@ -25,17 +27,35 @@ module Inspec
@conf = conf.dup
@rules = {}
@subcontexts = []
@dependencies = {}
@dependencies = conf['profile'].locked_dependencies unless conf['profile'].nil?
@require_loader = ::Inspec::RequireLoader.new
@attributes = []
reload_dsl
# A local resource registry that only contains resources defined
# in the transitive dependency tree of the loaded profile.
@resource_registry = Inspec::Resource.new_registry
@library_eval_context = Inspec::LibraryEvalContext.create(@resource_registry, @require_loader)
end
def dependencies
if @conf['profile'].nil?
{}
else
@conf['profile'].locked_dependencies
end
end
def to_resources_dsl
Inspec::Resource.create_dsl(@backend, @resource_registry)
end
def control_eval_context
@control_eval_context ||= begin
ctx = Inspec::ControlEvalContext.create(self, to_resources_dsl)
ctx.new(@backend, @conf, dependencies, @require_loader)
end
end
def reload_dsl
resources_dsl = Inspec::Resource.create_dsl(@backend)
ctx = create_context(resources_dsl, rule_context(resources_dsl))
@profile_context = ctx.new(@backend, @conf, @dependencies, @require_loader)
@control_eval_context = nil
end
def all_rules
@ -44,16 +64,14 @@ module Inspec
ret
end
def add_subcontext(context)
@subcontexts << context
def add_resources(context)
@resource_registry.merge!(context.resource_registry)
control_eval_context.add_resources(context)
reload_dsl
end
def load_tests(tests)
tests.each do |t|
content = t[:content]
next if content.nil? || content.empty?
load(content, t[:ref], t[:line])
end
def add_subcontext(context)
@subcontexts << context
end
def load_libraries(libs)
@ -73,21 +91,29 @@ module Inspec
# load all files directly that are flat inside the libraries folder
autoloads.each do |path|
next unless path.end_with?('.rb')
load(*@require_loader.load(path)) unless @require_loader.loaded?(path)
load_library_file(*@require_loader.load(path)) unless @require_loader.loaded?(path)
end
reload_dsl
end
def load(content, source = nil, line = nil)
def load_control_file(*args)
load_with_context(control_eval_context, *args)
end
alias load load_control_file
def load_library_file(*args)
load_with_context(@library_eval_context, *args)
end
def load_with_context(context, content, source = nil, line = nil)
Inspec::Log.debug("Loading #{source || '<anonymous content>'} into #{self}")
@current_load = { file: source }
if content.is_a? Proc
@profile_context.instance_eval(&content)
context.instance_eval(&content)
elsif source.nil? && line.nil?
@profile_context.instance_eval(content)
context.instance_eval(content)
else
@profile_context.instance_eval(content, source || 'unknown', line || 1)
context.instance_eval(content, source || 'unknown', line || 1)
end
end
@ -129,131 +155,5 @@ module Inspec
return rid.to_s if pid.to_s.empty?
pid.to_s + '/' + rid.to_s
end
# Create the context for controls. This includes all components of the DSL,
# including matchers and resources.
#
# @param [ResourcesDSL] resources_dsl which has all resources to attach
# @return [RuleContext] the inner context of rules
def rule_context(resources_dsl)
require 'rspec/core/dsl'
Class.new(Inspec::Rule) do
include RSpec::Core::DSL
include resources_dsl
end
end
# Creates the heart of the profile context:
# An instantiated object which has all resources registered to it
# and exposes them to the a test file. The profile context serves as a
# container for all profiles which are registered. Within the context
# profiles get access to all DSL calls for creating tests and controls.
#
# @param outer_dsl [OuterDSLClass]
# @return [ProfileContextClass]
def create_context(resources_dsl, rule_class) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
profile_context_owner = self
profile_id = @profile_id
# rubocop:disable Lint/NestedMethodDefinition
Class.new do
include Inspec::DSL
include resources_dsl
def initialize(backend, conf, dependencies, require_loader) # rubocop:disable Lint/NestedMethodDefinition, Lint/DuplicateMethods
@backend = backend
@conf = conf
@dependencies = dependencies
@require_loader = require_loader
@skip_profile = false
end
# Save the toplevel require method to load all ruby dependencies.
# It is used whenever the `require 'lib'` is not in libraries.
alias_method :__ruby_require, :require
def require(path)
rbpath = path + '.rb'
return __ruby_require(path) if !@require_loader.exists?(rbpath)
return false if @require_loader.loaded?(rbpath)
# This is equivalent to calling `require 'lib'` with lib on disk.
# We cannot rely on libraries residing on disk however.
# TODO: Sandboxing.
content, path, line = @require_loader.load(rbpath)
eval(content, TOPLEVEL_BINDING, path, line) # rubocop:disable Lint/Eval
end
define_method :title do |arg|
profile_context_owner.set_header(:title, arg)
end
def to_s
"Profile Context Run #{profile_name}"
end
define_method :profile_name do
profile_id
end
define_method :control do |*args, &block|
id = args[0]
opts = args[1] || {}
register_control(rule_class.new(id, profile_id, opts, &block))
end
define_method :describe do |*args, &block|
loc = block_location(block, caller[0])
id = "(generated from #{loc} #{SecureRandom.hex})"
res = nil
rule = rule_class.new(id, profile_id, {}) do
res = describe(*args, &block)
end
register_control(rule, &block)
res
end
define_method :add_subcontext do |context|
profile_context_owner.add_subcontext(context)
end
define_method :register_control do |control, &block|
::Inspec::Rule.set_skip_rule(control, true) if @skip_profile
profile_context_owner.register_rule(control, &block) unless control.nil?
end
# method for attributes; import attribute handling
define_method :attribute do |name, options|
profile_context_owner.register_attribute(name, options)
end
define_method :skip_control do |id|
profile_context_owner.unregister_rule(id)
end
def only_if
return unless block_given?
@skip_profile ||= !yield
end
alias_method :rule, :control
alias_method :skip_rule, :skip_control
private
def block_location(block, alternate_caller)
if block.nil?
alternate_caller[/^(.+:\d+):in .+$/, 1] || 'unknown'
else
path, line = block.source_location
"#{File.basename(path)}:#{line}"
end
end
end
# rubocop:enable all
end
end
end

View file

@ -3,17 +3,20 @@
# license: All rights reserved
# author: Dominik Richter
# author: Christoph Hartmann
require 'inspec/plugins'
module Inspec
class Resource
class Registry
# empty class for namespacing resource classes in the registry
def self.default_registry
@default_registry ||= {}
end
def self.registry
@registry ||= {}
@registry ||= default_registry
end
def self.new_registry
default_registry.dup
end
# Creates the inner DSL which includes all resources for
@ -22,9 +25,8 @@ module Inspec
#
# @param backend [BackendRunner] exposing the target to resources
# @return [ResourcesDSL]
def self.create_dsl(backend)
def self.create_dsl(backend, my_registry = registry)
# need the local name, to use it in the module creation further down
my_registry = registry
Module.new do
my_registry.each do |id, r|
define_method id.to_sym do |*args|
@ -41,10 +43,14 @@ module Inspec
# @param [int] version the resource version to use
# @return [Resource] base class for creating a new resource
def self.resource(version)
validate_resource_dsl_version!(version)
Inspec::Plugins::Resource
end
def self.validate_resource_dsl_version!(version)
if version != 1
fail 'Only resource version 1 is supported!'
end
Inspec::Plugins::Resource
end
end

View file

@ -33,6 +33,10 @@ module Inspec
instance_eval(&block) if block_given?
end
def to_s
Inspec::Rule.rule_id(self)
end
def id(*_)
# never overwrite the ID
@id

View file

@ -30,11 +30,18 @@ module Inspec
#
class Runner # rubocop:disable Metrics/ClassLength
extend Forwardable
def_delegator :@test_collector, :report
def_delegator :@test_collector, :reset
attr_reader :backend, :rules, :attributes
def initialize(conf = {})
@rules = []
@conf = conf.dup
@conf[:logger] ||= Logger.new(nil)
@target_profiles = []
@controls = @conf[:controls] || []
@ignore_supports = @conf[:ignore_supports]
@test_collector = @conf.delete(:test_collector) || begin
require 'inspec/runner_rspec'
@ -52,19 +59,31 @@ module Inspec
@test_collector.tests
end
def normalize_map(hm)
res = {}
hm.each {|k, v|
res[k.to_s] = v
}
res
end
def configure_transport
@backend = Inspec::Backend.create(@conf)
@test_collector.backend = @backend
end
def run(with = nil)
Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}"
Inspec::Log.debug "Backend is #{@backend}"
all_controls = []
@target_profiles.each do |profile|
@test_collector.add_profile(profile)
profile.locked_dependencies
profile.load_libraries
@attributes |= profile.runner_context.attributes
all_controls += profile.collect_tests
end
all_controls.each do |rule|
register_rule(rule)
end
@test_collector.run(with)
end
# determine all attributes before the execution, fetch data from secrets backend
def load_attributes(options)
attributes = {}
@ -82,8 +101,7 @@ module Inspec
#
# add_target allows the user to add a target whose tests will be
# run when the user calls the run method. This is the main entry
# point to profile loading.
# run when the user calls the run method.
#
# A target is a path or URL that points to a profile. Using this
# target we generate a Profile and a ProfileContext. The content
@ -102,61 +120,15 @@ module Inspec
# TODO: Deduplicate/clarify the loading code that exists in here,
# the ProfileContext, the Profile, and Inspec::DSL
#
# TODO: Libraries of dependent profiles should be loaded even when
# include_content/required_content aren't called.
#
# @params target [String] A path or URL to a profile or raw test.
#
# @params options [Hash] An options hash. The relevant options
# considered by this call path are:
#
# - `:ignore_supports`: A boolean that controls whether to ignore
# the profile's metadata regarding supported Inspec versions or
# platforms.
#
# - `:controls`: An array of controls to run from this
# profile. Any returned rules that are not part of these
# controls will be filtered before being passed to @test_collector.
# @params _opts [Hash] Unused, but still here to avoid breaking kitchen-inspec
#
# @eturns [Inspec::ProfileContext]
#
def add_target(target, options = {})
profile = Inspec::Profile.for_target(target, options)
def add_target(target, _opts = [])
profile = Inspec::Profile.for_target(target, backend: @backend, controls: @controls)
fail "Could not resolve #{target} to valid input." if profile.nil?
add_profile(profile, options)
end
# Returns the profile context used to initialize this profile.
def add_profile(profile, options = {})
return if !options[:ignore_supports] && !supports_profile?(profile)
@test_collector.add_profile(profile)
add_content_from_profile(profile, options[:controls])
end
def add_content_from_profile(profile, controls)
return if profile.tests.nil? || profile.tests.empty?
ctx = Inspec::ProfileContext.for_profile(profile, @backend)
profile.runner_context = ctx
libs = profile.libraries.map do |k, v|
[v, k]
end
tests = profile.tests.map do |ref, content|
r = profile.source_reader.target.abs_path(ref)
{ ref: r, content: content }
end
ctx.load_libraries(libs)
ctx.load_tests(tests)
@attributes |= ctx.attributes
filter_controls(ctx.all_rules, controls).each do |rule|
register_rule(rule)
end
ctx
@target_profiles << profile if supports_profile?(profile)
end
#
@ -174,7 +146,7 @@ module Inspec
end
def supports_profile?(profile)
return true if profile.metadata.nil?
return true if profile.metadata.nil? || @ignore_supports
if !profile.metadata.supports_runtime?
fail 'This profile requires InSpec version '\
@ -207,20 +179,8 @@ module Inspec
new_tests
end
def_delegator :@test_collector, :run
def_delegator :@test_collector, :report
def_delegator :@test_collector, :reset
private
def filter_controls(controls_array, include_list)
return controls_array if include_list.nil? || include_list.empty?
controls_array.select do |c|
id = ::Inspec::Rule.rule_id(c)
include_list.include?(id)
end
end
def block_source_info(block)
return {} if block.nil? || !block.respond_to?(:source_location)
opts = {}
@ -265,6 +225,7 @@ module Inspec
end
def register_rule(rule)
Inspec::Log.debug "Registering rule #{rule}"
@rules << rule
checks = ::Inspec::Rule.prepare_checks(rule)
examples = checks.map do |m, a, b|

View file

@ -50,7 +50,7 @@ class DockerTester
puts "--> run test on docker #{container.id}"
opts = { 'target' => "docker://#{container.id}" }
runner = Inspec::Runner.new(opts)
@tests.each { |test| runner.add_target(test, opts) }
@tests.each { |test| runner.add_target(test) }
runner.tests.map { |g| g.run(report) }
end
end

View file

@ -292,6 +292,7 @@ class MockLoader
def self.load_profile(name, opts = {})
opts[:test_collector] = Inspec::RunnerMock.new
opts[:backend] = Inspec::Backend.create(opts)
Inspec::Profile.for_target(profile_path(name), opts)
end

View file

@ -10,7 +10,7 @@ describe 'controls' do
'inspec.yml' => "name: mock",
'controls/mock.rb' => "control '1' do\n#{content}\nend\n",
}
opts = { test_collector: Inspec::RunnerMock.new }
opts = { test_collector: Inspec::RunnerMock.new, backend: Inspec::Backend.create({ backend: 'mock' }) }
Inspec::Profile.for_target(data, opts)
.params[:controls]['1']
end

View file

@ -2,8 +2,3 @@
include_controls 'profile_a'
include_controls 'profile_b'
include_controls 'ssh-hardening' do
12.upto(12) do |i|
skip_control "ssh-%02d" % i
end
end

View file

@ -10,5 +10,3 @@ depends:
path: ../profile_a
- name: profile_b
path: ../profile_b
- name: ssh-hardening
url: https://github.com/dev-sec/tests-ssh-hardening/archive/master.tar.gz

View file

@ -20,3 +20,9 @@ control 'profilea-1' do # A unique ID for this control
it { should be_directory }
end
end
control 'profilea-2' do
describe gordon_config do
its('version') { should eq('1.0') }
end
end

View file

@ -14,3 +14,9 @@ control 'profileb-1' do # A unique ID for this control
it { should be_directory }
end
end
control 'profileb-2' do
describe gordon_config do
its('version') { should eq('2.0') }
end
end

View file

@ -0,0 +1,15 @@
class GordonConfig < Inspec.resource(1)
name 'gordon_config'
desc "Gordon's resource description ..."
example "
describe gordon_config do
its('version') { should eq('1.0') }
end
"
def version
"1.0"
end
end

View file

@ -0,0 +1,15 @@
class GordonConfig < Inspec.resource(1)
name 'gordon_config'
desc "Gordon's resource description ..."
example "
describe gordon_config do
its('version') { should eq('2.0') }
end
"
def version
"2.0"
end
end

View file

@ -14,12 +14,6 @@ describe Inspec::Plugins::Resource do
Class.new(base) do name 'hello'; end
Inspec::Resource.registry['hello'].wont_be :nil?
end
it "will create a good class name" do
Class.new(base) do name 'hello_world'; end
Inspec::Resource.registry['hello_world'].to_s
.must_equal 'Inspec::Resource::Registry::HelloWorld'
end
end
def create(&block)

View file

@ -0,0 +1,40 @@
# encoding: utf-8
# author: Steven Danna
require 'helper'
require 'inspec/resource'
require 'inspec/library_eval_context'
describe Inspec::LibraryEvalContext do
let(:resource_content) { <<EOF
class MyTestResource < Inspec.resource(1)
name 'my_test_resource'
desc 'A test description'
example 'Forgot to write docs, sorry'
def version
'2.0'
end
end
EOF
}
let(:registry) { Inspec::Resource.new_registry }
let(:eval_context) { Inspec::LibraryEvalContext.create(registry, nil) }
it 'accepts a registry' do
Inspec::LibraryEvalContext.create(registry, nil)
end
it 'adds the resource to our registry' do
eval_context.instance_eval(resource_content)
registry.keys.include?("my_test_resource").must_equal true
end
it 'adds nothing to the default registry' do
old_default_registry = Inspec::Resource.default_registry.dup
eval_context.instance_eval(resource_content)
old_default_registry.must_equal Inspec::Resource.default_registry
end
end