Merge pull request #524 from chef/dr/fix-inheritance

bugfix: inheritance of local profiles
This commit is contained in:
Christoph Hartmann 2016-03-08 15:20:18 +01:00
commit 139fa1473c
10 changed files with 130 additions and 72 deletions

View file

@ -20,6 +20,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
desc: 'Attach a profile ID to all test results' desc: 'Attach a profile ID to all test results'
option :output, aliases: :o, type: :string, option :output, aliases: :o, type: :string,
desc: 'Save the created profile to a path' desc: 'Save the created profile to a path'
profile_options
def json(target) def json(target)
diagnose diagnose
o = opts.dup o = opts.dup
@ -42,6 +43,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
desc 'check PATH', 'verify all tests at the specified PATH' desc 'check PATH', 'verify all tests at the specified PATH'
option :format, type: :string option :format, type: :string
profile_options
def check(path) # rubocop:disable Metrics/AbcSize def check(path) # rubocop:disable Metrics/AbcSize
diagnose diagnose
o = opts.dup o = opts.dup

View file

@ -0,0 +1,19 @@
# Example InSpec Profile
This example shows the use of InSpec [profile](../../docs/profiles.rst) inheritance.
## Verify a profile
InSpec ships with built-in features to verify a profile structure.
```bash
$ inspec check examples/inheritance --profiles-path examples
```
## Execute a profile
To run a profile on a local machine use `inspec exec /path/to/profile`.
```bash
$ inspec exec examples/inheritance --profiles-path examples
```

View file

@ -0,0 +1,11 @@
# encoding: utf-8
# copyright: 2015, Chef Software, Inc.
# license: All rights reserved
include_controls 'profile' do
skip_control 'tmp-1.0'
control 'gordon-1.0' do
impact 0.0
end
end

View file

@ -0,0 +1,10 @@
name: inheritance
title: InSpec example inheritance
maintainer: Chef Software, Inc.
copyright: Chef Software, Inc.
copyright_email: support@chef.io
license: Apache 2 license
summary: Demonstrates the use of InSpec profile inheritance
version: 1.0.0
supports:
- os-family: linux

View file

@ -6,11 +6,13 @@
module Inspec::DSL module Inspec::DSL
def require_controls(id, &block) def require_controls(id, &block)
::Inspec::DSL.load_spec_files_for_profile self, id, false, &block opts = { profile_id: id, include_all: false, backend: @backend, conf: @conf }
::Inspec::DSL.load_spec_files_for_profile(self, opts, &block)
end end
def include_controls(id, &block) def include_controls(id, &block)
::Inspec::DSL.load_spec_files_for_profile self, id, true, &block opts = { profile_id: id, include_all: true, backend: @backend, conf: @conf }
::Inspec::DSL.load_spec_files_for_profile(self, opts, &block)
end end
alias require_rules require_controls alias require_rules require_controls
@ -60,72 +62,63 @@ module Inspec::DSL
} }
end end
def self.load_spec_file_for_profile(profile_id, file, rule_registry, only_ifs) def self.load_spec_files_for_profile(bind_context, opts, &block)
raw = File.read(file)
# TODO: error-handling
ctx = Inspec::ProfileContext.new(profile_id, rule_registry, only_ifs)
ctx.instance_eval(raw, file, 1)
end
def self.load_spec_files_for_profile(bind_context, profile_id, include_all, &block)
# get all spec files # get all spec files
files = get_spec_files_for_profile profile_id target = get_reference_profile(opts[:profile_id], opts[:conf])
# load all rules from spec files profile = Inspec::Profile.for_target(target, opts)
rule_registry = {} context = load_profile_context(opts[:profile_id], profile, opts)
# TODO: handling of only_ifs
only_ifs = []
files.each do |file|
load_spec_file_for_profile(profile_id, file, rule_registry, only_ifs)
end
# interpret the block and create a set of rules from it # if we don't want all the rules, then just make 1 pass to get all rule_IDs
block_registry = {} # that we want to keep from the original
if block_given? filter_included_controls(context, opts, &block) if !opts[:include_all]
ctx = Inspec::ProfileContext.new(profile_id, block_registry, only_ifs)
ctx.instance_eval(&block)
end
# if all rules are not included, select only the ones # interpret the block and skip/modify as required
# that were defined in the block context.load(block) if block_given?
unless include_all
remove = rule_registry.keys - block_registry.keys
remove.each { |key| rule_registry.delete(key) }
end
# merge the rules in the block_registry (adjustments) with
# the rules in the rule_registry (included)
block_registry.each do |id, r|
org = rule_registry[id]
if org.nil?
# TODO: print error because we write alter a rule that doesn't exist
elsif r.nil?
rule_registry.delete(id)
else
merge_rules(org, r)
end
end
# finally register all combined rules # finally register all combined rules
rule_registry.each do |_id, rule| context.rules.values.each do |control|
bind_context.__register_rule rule bind_context.register_control(control)
end end
end end
def self.get_spec_files_for_profile(id) def self.filter_included_controls(context, opts, &block)
base_path = '/etc/inspec/tests' mock = Inspec::Backend.create({ backend: 'mock' })
path = File.join(base_path, id) include_ctx = Inspec::ProfileContext.new(opts[:profile_id], mock, opts[:conf])
# find all files to be included include_ctx.load(block) if block_given?
files = [] # remove all rules that were not registered
if File.directory? path context.rules.keys.each do |id|
# include all library paths, if they exist unless include_ctx.rules[id]
libdir = File.join(path, 'lib') context.rules[id] = nil
if File.directory? libdir and !$LOAD_PATH.include?(libdir)
$LOAD_PATH.unshift(libdir)
end end
files = Dir[File.join(path, 'spec', '*_spec.rb')]
end end
files end
def self.get_reference_profile(id, opts)
profiles_path = opts['profiles_path'] ||
fail('You must supply a --profiles-path to inherit from other profiles.')
abs_path = File.expand_path(profiles_path.to_s)
unless File.directory? abs_path
fail("Cannot find profiles path #{abs_path}")
end
id_path = File.join(abs_path, id)
unless File.directory? id_path
fail("Cannot find referenced profile #{id} in #{id_path}")
end
id_path
end
def self.load_profile_context(id, profile, opts)
ctx = Inspec::ProfileContext.new(id, opts[:backend], opts[:conf])
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
end end

View file

@ -13,7 +13,7 @@ module Inspec
extend Forwardable extend Forwardable
attr_reader :path attr_reader :path
def self.for_target(target, opts) def self.resolve_target(target, opts)
# Fetchers retrieve file contents # Fetchers retrieve file contents
opts[:target] = target opts[:target] = target
fetcher = Inspec::Fetcher.resolve(target) fetcher = Inspec::Fetcher.resolve(target)
@ -27,7 +27,11 @@ module Inspec
fail("Don't understand inspec profile in #{target.inspect}, it "\ fail("Don't understand inspec profile in #{target.inspect}, it "\
"doesn't look like a supported profile structure.") "doesn't look like a supported profile structure.")
end end
new(reader, opts) reader
end
def self.for_target(target, opts)
new(resolve_target(target, opts), opts)
end end
attr_reader :source_reader attr_reader :source_reader

View file

@ -7,18 +7,18 @@ require 'inspec/dsl'
require 'securerandom' require 'securerandom'
module Inspec module Inspec
class ProfileContext class ProfileContext # rubocop:disable Metrics/ClassLength
attr_reader :rules, :only_ifs attr_reader :rules
def initialize(profile_id, backend, profile_registry = {}, only_ifs = []) def initialize(profile_id, backend, conf)
if backend.nil? if backend.nil?
fail 'ProfileContext is initiated with a backend == nil. ' \ fail 'ProfileContext is initiated with a backend == nil. ' \
'This is a backend error which must be fixed upstream.' 'This is a backend error which must be fixed upstream.'
end end
@profile_id = profile_id @profile_id = profile_id
@rules = profile_registry
@only_ifs = only_ifs
@backend = backend @backend = backend
@conf = conf.dup
@rules = {}
reload_dsl reload_dsl
end end
@ -26,12 +26,16 @@ module Inspec
def reload_dsl def reload_dsl
resources_dsl = Inspec::Resource.create_dsl(@backend) resources_dsl = Inspec::Resource.create_dsl(@backend)
ctx = create_context(resources_dsl, rule_context(resources_dsl)) ctx = create_context(resources_dsl, rule_context(resources_dsl))
@profile_context = ctx.new @profile_context = ctx.new(@backend, @conf)
end end
def load(content, source = nil, line = nil) def load(content, source = nil, line = nil)
@current_load = { file: source } @current_load = { file: source }
@profile_context.instance_eval(content, source || 'unknown', line || 1) if content.is_a? Proc
@profile_context.instance_eval(&content)
else
@profile_context.instance_eval(content, source || 'unknown', line || 1)
end
end end
def unregister_rule(id) def unregister_rule(id)
@ -93,6 +97,11 @@ module Inspec
include Inspec::DSL include Inspec::DSL
include resources_dsl include resources_dsl
def initialize(backend, conf) # rubocop:disable Lint/NestedMethodDefinition, Lint/DuplicateMethods
@backend = backend
@conf = conf
end
define_method :title do |arg| define_method :title do |arg|
profile_context_owner.set_header(:title, arg) profile_context_owner.set_header(:title, arg)
end end
@ -116,6 +125,10 @@ module Inspec
alias_method :rule, :control alias_method :rule, :control
define_method :register_control do |control|
profile_context_owner.register_rule(control) unless control.nil?
end
define_method :describe do |*args, &block| define_method :describe do |*args, &block|
loc = block_location(block, caller[0]) loc = block_location(block, caller[0])
id = "(generated from #{loc} #{SecureRandom.hex})" id = "(generated from #{loc} #{SecureRandom.hex})"
@ -133,7 +146,7 @@ module Inspec
nil nil
end end
def skip_control(id) define_method :skip_control do |id|
profile_context_owner.unregister_rule(id) profile_context_owner.unregister_rule(id)
end end

View file

@ -67,8 +67,8 @@ module Inspec
end end
end end
def create_context def create_context(options = {})
Inspec::ProfileContext.new(@profile_id, @backend) Inspec::ProfileContext.new(@profile_id, @backend, @conf.merge(options))
end end
def add_content(test, libs, options = {}) def add_content(test, libs, options = {})
@ -76,7 +76,7 @@ module Inspec
return if content.nil? || content.empty? return if content.nil? || content.empty?
# load all libraries # load all libraries
ctx = create_context ctx = create_context(options)
libs.each do |lib| libs.each do |lib|
ctx.load(lib[:content].to_s, lib[:ref], lib[:line] || 1) ctx.load(lib[:content].to_s, lib[:ref], lib[:line] || 1)
ctx.reload_dsl ctx.reload_dsl

View file

@ -39,10 +39,16 @@ module Inspec
desc: 'Set the log level: info (default), debug, warn, error' desc: 'Set the log level: info (default), debug, warn, error'
end end
def self.profile_options
option :profiles_path, type: :string,
desc: 'Folder which contains referenced profiles.'
end
def self.exec_options def self.exec_options
option :id, type: :string, option :id, type: :string,
desc: 'Attach a profile ID to all test results' desc: 'Attach a profile ID to all test results'
target_options target_options
profile_options
option :controls, type: :array, option :controls, type: :array,
desc: 'A list of controls to run. Ignore all other tests.' desc: 'A list of controls to run. Ignore all other tests.'
option :format, type: :string, option :format, type: :string,

View file

@ -53,7 +53,7 @@ end
describe Inspec::ProfileContext do describe Inspec::ProfileContext do
let(:backend) { MockLoader.new.backend } let(:backend) { MockLoader.new.backend }
let(:profile) { Inspec::ProfileContext.new(nil, backend) } let(:profile) { Inspec::ProfileContext.new(nil, backend, {}) }
def get_rule def get_rule
profile.rules.values[0] profile.rules.values[0]