mirror of
https://github.com/inspec/inspec
synced 2024-11-24 05:33:17 +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'
|
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
|
||||||
|
|
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
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,13 +26,17 @@ 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 }
|
||||||
|
if content.is_a? Proc
|
||||||
|
@profile_context.instance_eval(&content)
|
||||||
|
else
|
||||||
@profile_context.instance_eval(content, source || 'unknown', line || 1)
|
@profile_context.instance_eval(content, source || 'unknown', line || 1)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def unregister_rule(id)
|
def unregister_rule(id)
|
||||||
full_id = Inspec::Rule.full_id(@profile_id, id)
|
full_id = Inspec::Rule.full_id(@profile_id, 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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Reference in a new issue