mirror of
https://github.com/inspec/inspec
synced 2025-02-17 06:28:40 +00:00
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:
parent
86c501fdd8
commit
6034ece853
10 changed files with 93 additions and 43 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Add table
Reference in a new issue