Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bugfix: inheritance of local profiles #524

Merged
merged 2 commits into from
Mar 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bin/inspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions examples/inheritance/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions examples/inheritance/controls/example.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions examples/inheritance/inspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: inheritance
title: InSpec example inheritance
maintainer: Chef Software, Inc.
copyright: Chef Software, Inc.
copyright_email: [email protected]
license: Apache 2 license
summary: Demonstrates the use of InSpec profile inheritance
version: 1.0.0
supports:
- os-family: linux
103 changes: 48 additions & 55 deletions lib/inspec/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -60,72 +62,63 @@ def self.set_rspec_ids(obj, id)
}
end

def self.load_spec_file_for_profile(profile_id, file, rule_registry, only_ifs)
raw = File.read(file)
# TODO: error-handling
def self.load_spec_files_for_profile(bind_context, opts, &block)
# get all spec files
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)

ctx = Inspec::ProfileContext.new(profile_id, rule_registry, only_ifs)
ctx.instance_eval(raw, file, 1)
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]

def self.load_spec_files_for_profile(bind_context, profile_id, include_all, &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
# interpret the block and skip/modify as required
context.load(block) if block_given?

# 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)
# finally register all combined rules
context.rules.values.each do |control|
bind_context.register_control(control)
end
end

# 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) }
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
end
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
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

# finally register all combined rules
rule_registry.each do |_id, rule|
bind_context.__register_rule rule
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.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)
end
files = Dir[File.join(path, 'spec', '*_spec.rb')]
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
files
ctx
end
end

Expand Down
8 changes: 6 additions & 2 deletions lib/inspec/profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Profile # rubocop:disable Metrics/ClassLength
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)
Expand All @@ -27,7 +27,11 @@ def self.for_target(target, opts)
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
Expand Down
29 changes: 21 additions & 8 deletions lib/inspec/profile_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,35 @@
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

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)
Expand Down Expand Up @@ -93,6 +97,11 @@ def create_context(resources_dsl, rule_class) # rubocop:disable Metrics/AbcSize,
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
Expand All @@ -116,6 +125,10 @@ def to_s

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})"
Expand All @@ -133,7 +146,7 @@ def to_s
nil
end

def skip_control(id)
define_method :skip_control do |id|
profile_context_owner.unregister_rule(id)
end

Expand Down
6 changes: 3 additions & 3 deletions lib/inspec/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ def add_profile(profile, options = {})
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 = {})
content = test[:content]
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
Expand Down
6 changes: 6 additions & 0 deletions lib/utils/base_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ def self.target_options
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,
Expand Down
2 changes: 1 addition & 1 deletion test/unit/profile_context_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ module DescribeOneTest

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]
Expand Down