Initial control isolation support

The goal of this change is to provide an isolated view of the available
profiles when the user calls the include_controls or require_controls
APIs.  Namely,

- A profile should only be able to reference profiles that are part of
  its transitive dependency tree. That is, if the dependency tree for a
  profile looks like the following:

  A
  |- B --> C
  |
  |- D --> E

  Then profile B should only be able to see profile C and fail if it
  tries to reference A, D, or E.

- The same profile should be include-able at different versions from
  different parts of the tree without conflict.  That is, if the
  dependency tree for a profile looks like the following:

  A
  |- B --> C@1.0
  |
  |- D --> C@2.0

  Then profile B should see the 1.0 version of C and profile D should
  see the 2.0 profile C with respect to the included controls.

To achieve these goals we:

- Ensure that we construct ProfileContext objects with respect to the
  correct dependencies in Inspec::DSL.

- Provide a method of accessing all transitively defined rules on a
  ProfileContext without pushing all of the rules onto the same global
  namespace.

This does not yet handle attributes or libraries.
This commit is contained in:
Steven Danna 2016-08-24 12:17:29 +01:00 committed by Christoph Hartmann
parent 86c501fdd8
commit 6034ece853
10 changed files with 93 additions and 43 deletions

View file

@ -1,8 +1,8 @@
# encoding: utf-8
# copyright: 2015, Dominik Richter
# license: All rights reserved
# author: Dominik Richter
# author: Christoph Hartmann
require 'inspec/log'
module Inspec::DSL
def require_controls(id, &block)
@ -32,28 +32,36 @@ module Inspec::DSL
end
def self.load_spec_files_for_profile(bind_context, opts, &block)
# get all spec files
target = opts[:dependencies].list[opts[:profile_id]] ||
fail("Can't find profile #{opts[:profile_id].inspect}, please add it as a dependency.")
profile = Inspec::Profile.for_target(target.path, opts)
context = load_profile_context(opts[:profile_id], profile, opts)
dependencies = opts[:dependencies]
profile_id = opts[:profile_id]
dep_entry = dependencies.list[profile_id]
if dep_entry.nil?
fail <<EOF
Cannot load #{profile_id} since it is not listed as a dependency
of #{bind_context.profile_name}.
Dependencies available from this context are:
#{dependencies.list.keys.join("\n ")}
EOF
end
context = load_profile_context(dep_entry.profile, opts[:backend])
# 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, opts, &block) if !opts[:include_all]
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?
# finally register all combined rules
context.rules.values.each do |control|
bind_context.register_control(control)
end
bind_context.add_subcontext(context)
end
def self.filter_included_controls(context, opts, &block)
def self.filter_included_controls(context, profile, &block)
mock = Inspec::Backend.create({ backend: 'mock' })
include_ctx = Inspec::ProfileContext.new(opts[:profile_id], mock, opts[:conf])
include_ctx = Inspec::ProfileContext.for_profile(profile, mock)
include_ctx.load(block) if block_given?
# remove all rules that were not registered
context.rules.keys.each do |id|
@ -63,8 +71,8 @@ module Inspec::DSL
end
end
def self.load_profile_context(id, profile, opts)
ctx = Inspec::ProfileContext.new(id, opts[:backend], opts[:conf])
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

View file

@ -53,6 +53,10 @@ module Inspec
Metadata.finalize(@source_reader.metadata, @profile_id)
end
def name
metadata.params[:name]
end
def params
@params ||= load_params
end
@ -293,14 +297,14 @@ module Inspec
test_collector: opts.delete(:test_collector),
)
runner.add_profile(self, opts)
runner.rules.values.each do |rule|
runner.rules.each do |rule|
f = load_rule_filepath(prefix, rule)
load_rule(rule, f, controls, groups)
end
params[:attributes] = runner.attributes
else
# load from context
@runner_context.rules.values.each do |rule|
@runner_context.all_rules.each do |rule|
f = load_rule_filepath(prefix, rule)
load_rule(rule, f, controls, groups)
end

View file

@ -1,7 +1,7 @@
# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann
require 'inspec/log'
require 'inspec/rule'
require 'inspec/dsl'
require 'inspec/require_loader'
@ -10,18 +10,21 @@ require 'inspec/objects/attribute'
module Inspec
class ProfileContext # rubocop:disable Metrics/ClassLength
attr_reader :rules
attr_reader :attributes
def self.for_profile(profile, backend)
new(profile.name, backend, { 'profile' => profile })
end
attr_reader :attributes, :rules, :profile_id
def initialize(profile_id, backend, conf)
if backend.nil?
fail 'ProfileContext is initiated with a backend == nil. ' \
'This is a backend error which must be fixed upstream.'
end
@profile_id = profile_id
@backend = backend
@conf = conf.dup
@rules = {}
@subcontexts = []
@dependencies = {}
@dependencies = conf['profile'].locked_dependencies unless conf['profile'].nil?
@require_loader = ::Inspec::RequireLoader.new
@ -35,6 +38,16 @@ module Inspec
@profile_context = ctx.new(@backend, @conf, @dependencies, @require_loader)
end
def all_rules
ret = @rules.values
ret += @subcontexts.map(&:all_rules).flatten
ret
end
def add_subcontext(context)
@subcontexts << context
end
def load_libraries(libs)
lib_prefix = 'libraries' + File::SEPARATOR
autoloads = []
@ -59,6 +72,7 @@ module Inspec
end
def load(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)
@ -167,7 +181,11 @@ module Inspec
end
def to_s
'Profile Context Run'
"Profile Context Run #{profile_name}"
end
define_method :profile_name do
profile_id
end
define_method :control do |*args, &block|
@ -189,6 +207,10 @@ module Inspec
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

View file

@ -18,7 +18,7 @@ module Inspec
extend Forwardable
attr_reader :backend, :rules, :attributes
def initialize(conf = {})
@rules = {}
@rules = []
@conf = conf.dup
@conf[:logger] ||= Logger.new(nil)
@ -132,15 +132,14 @@ module Inspec
end
# evaluate the test content
tests = [tests] unless tests.is_a? Array
tests.each { |t| add_test_to_context(t, ctx) }
Array(tests).each { |t| add_test_to_context(t, ctx) }
# merge and collect all attributes
@attributes |= ctx.attributes
# process the resulting rules
filter_controls(ctx.rules, options[:controls]).each do |rule_id, rule|
register_rule(rule_id, rule)
filter_controls(ctx.all_rules, options[:controls]).each do |rule|
register_rule(rule)
end
ctx
@ -151,7 +150,7 @@ module Inspec
ctx.rules.each do |rule_id, rule|
next if block_given? && !(yield rule_id, rule)
new_tests = true
register_rule(rule_id, rule)
register_rule(rule)
end
new_tests
end
@ -168,9 +167,9 @@ module Inspec
ctx.load(content, test[:ref], test[:line])
end
def filter_controls(controls_map, include_list)
return controls_map if include_list.nil? || include_list.empty?
controls_map.select do |_, c|
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
@ -219,8 +218,8 @@ module Inspec
nil
end
def register_rule(rule_id, rule)
@rules[rule_id] = rule
def register_rule(rule)
@rules << rule
checks = ::Inspec::Rule.prepare_checks(rule)
examples = checks.map do |m, a, b|
get_check_example(m, a, b)

View file

@ -4,11 +4,12 @@
title 'sample section'
include_controls 'profile_c'
# you can also use plain tests
describe file('/tmp') do
it { should be_directory }
end
#
# The following should fail even in the case where profile_d or
# profile_b is pulled in somewhere else in the dependency tree.
#
# include_controls 'profile_d'
# include_controls 'profile_b'
# you add controls here
control 'profilea-1' do # A unique ID for this control

View file

@ -3,11 +3,7 @@
# license: All rights reserved
title 'sample section'
# you can also use plain tests
describe file('/tmp') do
it { should be_directory }
end
include_controls 'profile_d'
# you add controls here
control 'profileb-1' do # A unique ID for this control

View file

@ -6,3 +6,6 @@ copyright_email: you@example.com
license: All Rights Reserved
summary: An InSpec Compliance Profile
version: 0.1.0
depends:
- name: profile_d
path: ../profile_d

View file

@ -0,0 +1,9 @@
# you add controls here
control 'profiled-1' do # A unique ID for this control
impact 0.7 # The criticality, if this control fails.
title 'Create /tmp directory (profile d)' # A human-readable title
desc 'An optional description...'
describe file('/tmp') do # The actual test
it { should be_directory }
end
end

View file

@ -0,0 +1,8 @@
name: profile_d
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: All Rights Reserved
summary: An InSpec Compliance Profile
version: 0.1.0