mirror of
https://github.com/inspec/inspec
synced 2024-11-23 21:23:29 +00:00
Merge pull request #524 from chef/dr/fix-inheritance
bugfix: inheritance of local profiles
This commit is contained in:
commit
139fa1473c
10 changed files with 130 additions and 72 deletions
|
@ -20,6 +20,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
|
|||
desc: 'Attach a profile ID to all test results'
|
||||
option :output, aliases: :o, type: :string,
|
||||
desc: 'Save the created profile to a path'
|
||||
profile_options
|
||||
def json(target)
|
||||
diagnose
|
||||
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'
|
||||
option :format, type: :string
|
||||
profile_options
|
||||
def check(path) # rubocop:disable Metrics/AbcSize
|
||||
diagnose
|
||||
o = opts.dup
|
||||
|
|
19
examples/inheritance/README.md
Normal file
19
examples/inheritance/README.md
Normal 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
|
||||
```
|
11
examples/inheritance/controls/example.rb
Normal file
11
examples/inheritance/controls/example.rb
Normal 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
|
10
examples/inheritance/inspec.yml
Normal file
10
examples/inheritance/inspec.yml
Normal 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
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
module Inspec::DSL
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
alias require_rules require_controls
|
||||
|
@ -60,72 +62,63 @@ module Inspec::DSL
|
|||
}
|
||||
end
|
||||
|
||||
def self.load_spec_file_for_profile(profile_id, file, rule_registry, only_ifs)
|
||||
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)
|
||||
def self.load_spec_files_for_profile(bind_context, opts, &block)
|
||||
# get all spec files
|
||||
files = get_spec_files_for_profile profile_id
|
||||
# load all rules from spec files
|
||||
rule_registry = {}
|
||||
# TODO: handling of only_ifs
|
||||
only_ifs = []
|
||||
files.each do |file|
|
||||
load_spec_file_for_profile(profile_id, file, rule_registry, only_ifs)
|
||||
end
|
||||
target = get_reference_profile(opts[:profile_id], opts[:conf])
|
||||
profile = Inspec::Profile.for_target(target, opts)
|
||||
context = load_profile_context(opts[:profile_id], profile, opts)
|
||||
|
||||
# interpret the block and create a set of rules from it
|
||||
block_registry = {}
|
||||
if block_given?
|
||||
ctx = Inspec::ProfileContext.new(profile_id, block_registry, only_ifs)
|
||||
ctx.instance_eval(&block)
|
||||
end
|
||||
# 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]
|
||||
|
||||
# if all rules are not included, select only the ones
|
||||
# that were defined in the block
|
||||
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
|
||||
# interpret the block and skip/modify as required
|
||||
context.load(block) if block_given?
|
||||
|
||||
# finally register all combined rules
|
||||
rule_registry.each do |_id, rule|
|
||||
bind_context.__register_rule rule
|
||||
context.rules.values.each do |control|
|
||||
bind_context.register_control(control)
|
||||
end
|
||||
end
|
||||
|
||||
def self.get_spec_files_for_profile(id)
|
||||
base_path = '/etc/inspec/tests'
|
||||
path = File.join(base_path, id)
|
||||
# find all files to be included
|
||||
files = []
|
||||
if File.directory? path
|
||||
# include all library paths, if they exist
|
||||
libdir = File.join(path, 'lib')
|
||||
if File.directory? libdir and !$LOAD_PATH.include?(libdir)
|
||||
$LOAD_PATH.unshift(libdir)
|
||||
def self.filter_included_controls(context, opts, &block)
|
||||
mock = Inspec::Backend.create({ backend: 'mock' })
|
||||
include_ctx = Inspec::ProfileContext.new(opts[:profile_id], mock, opts[:conf])
|
||||
include_ctx.load(block) if block_given?
|
||||
# remove all rules that were not registered
|
||||
context.rules.keys.each do |id|
|
||||
unless include_ctx.rules[id]
|
||||
context.rules[id] = nil
|
||||
end
|
||||
files = Dir[File.join(path, 'spec', '*_spec.rb')]
|
||||
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
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ module Inspec
|
|||
extend Forwardable
|
||||
attr_reader :path
|
||||
|
||||
def self.for_target(target, opts)
|
||||
def self.resolve_target(target, opts)
|
||||
# Fetchers retrieve file contents
|
||||
opts[:target] = target
|
||||
fetcher = Inspec::Fetcher.resolve(target)
|
||||
|
@ -27,7 +27,11 @@ module Inspec
|
|||
fail("Don't understand inspec profile in #{target.inspect}, it "\
|
||||
"doesn't look like a supported profile structure.")
|
||||
end
|
||||
new(reader, opts)
|
||||
reader
|
||||
end
|
||||
|
||||
def self.for_target(target, opts)
|
||||
new(resolve_target(target, opts), opts)
|
||||
end
|
||||
|
||||
attr_reader :source_reader
|
||||
|
|
|
@ -7,18 +7,18 @@ require 'inspec/dsl'
|
|||
require 'securerandom'
|
||||
|
||||
module Inspec
|
||||
class ProfileContext
|
||||
attr_reader :rules, :only_ifs
|
||||
def initialize(profile_id, backend, profile_registry = {}, only_ifs = [])
|
||||
class ProfileContext # rubocop:disable Metrics/ClassLength
|
||||
attr_reader :rules
|
||||
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
|
||||
@rules = profile_registry
|
||||
@only_ifs = only_ifs
|
||||
@backend = backend
|
||||
@conf = conf.dup
|
||||
@rules = {}
|
||||
|
||||
reload_dsl
|
||||
end
|
||||
|
@ -26,12 +26,16 @@ module Inspec
|
|||
def reload_dsl
|
||||
resources_dsl = Inspec::Resource.create_dsl(@backend)
|
||||
ctx = create_context(resources_dsl, rule_context(resources_dsl))
|
||||
@profile_context = ctx.new
|
||||
@profile_context = ctx.new(@backend, @conf)
|
||||
end
|
||||
|
||||
def load(content, source = nil, line = nil)
|
||||
@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
|
||||
|
||||
def unregister_rule(id)
|
||||
|
@ -93,6 +97,11 @@ module Inspec
|
|||
include Inspec::DSL
|
||||
include resources_dsl
|
||||
|
||||
def initialize(backend, conf) # rubocop:disable Lint/NestedMethodDefinition, Lint/DuplicateMethods
|
||||
@backend = backend
|
||||
@conf = conf
|
||||
end
|
||||
|
||||
define_method :title do |arg|
|
||||
profile_context_owner.set_header(:title, arg)
|
||||
end
|
||||
|
@ -116,6 +125,10 @@ module Inspec
|
|||
|
||||
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|
|
||||
loc = block_location(block, caller[0])
|
||||
id = "(generated from #{loc} #{SecureRandom.hex})"
|
||||
|
@ -133,7 +146,7 @@ module Inspec
|
|||
nil
|
||||
end
|
||||
|
||||
def skip_control(id)
|
||||
define_method :skip_control do |id|
|
||||
profile_context_owner.unregister_rule(id)
|
||||
end
|
||||
|
||||
|
|
|
@ -67,8 +67,8 @@ module Inspec
|
|||
end
|
||||
end
|
||||
|
||||
def create_context
|
||||
Inspec::ProfileContext.new(@profile_id, @backend)
|
||||
def create_context(options = {})
|
||||
Inspec::ProfileContext.new(@profile_id, @backend, @conf.merge(options))
|
||||
end
|
||||
|
||||
def add_content(test, libs, options = {})
|
||||
|
@ -76,7 +76,7 @@ module Inspec
|
|||
return if content.nil? || content.empty?
|
||||
|
||||
# load all libraries
|
||||
ctx = create_context
|
||||
ctx = create_context(options)
|
||||
libs.each do |lib|
|
||||
ctx.load(lib[:content].to_s, lib[:ref], lib[:line] || 1)
|
||||
ctx.reload_dsl
|
||||
|
|
|
@ -39,10 +39,16 @@ module Inspec
|
|||
desc: 'Set the log level: info (default), debug, warn, error'
|
||||
end
|
||||
|
||||
def self.profile_options
|
||||
option :profiles_path, type: :string,
|
||||
desc: 'Folder which contains referenced profiles.'
|
||||
end
|
||||
|
||||
def self.exec_options
|
||||
option :id, type: :string,
|
||||
desc: 'Attach a profile ID to all test results'
|
||||
target_options
|
||||
profile_options
|
||||
option :controls, type: :array,
|
||||
desc: 'A list of controls to run. Ignore all other tests.'
|
||||
option :format, type: :string,
|
||||
|
|
|
@ -53,7 +53,7 @@ end
|
|||
|
||||
describe Inspec::ProfileContext do
|
||||
let(:backend) { MockLoader.new.backend }
|
||||
let(:profile) { Inspec::ProfileContext.new(nil, backend) }
|
||||
let(:profile) { Inspec::ProfileContext.new(nil, backend, {}) }
|
||||
|
||||
def get_rule
|
||||
profile.rules.values[0]
|
||||
|
|
Loading…
Reference in a new issue