diff --git a/examples/profile-attribute.yml b/examples/profile-attribute.yml new file mode 100644 index 000000000..7458333af --- /dev/null +++ b/examples/profile-attribute.yml @@ -0,0 +1,2 @@ +user: bob +password: secret diff --git a/examples/profile-attribute/README.md b/examples/profile-attribute/README.md new file mode 100644 index 000000000..6aa3de000 --- /dev/null +++ b/examples/profile-attribute/README.md @@ -0,0 +1,3 @@ +# Example InSpec Profile with Attributes + +This profile uses InSpec attributes to parameterize a profile. diff --git a/examples/profile-attribute/controls/example.rb b/examples/profile-attribute/controls/example.rb new file mode 100644 index 000000000..26c16d4f4 --- /dev/null +++ b/examples/profile-attribute/controls/example.rb @@ -0,0 +1,20 @@ +# encoding: utf-8 +val_user = attribute('user', default: 'alice', required: true) +val_password = attribute('password', required: true) + +# that works +describe 'bob' do + it { should eq val_user.value } +end + +describe 'secret' do + it { should eq val_password.value } +end + +describe val_user.value do + it { should eq 'bob' } +end + +describe val_password.value do + it { should eq 'secret' } +end diff --git a/examples/profile-attribute/inspec.yml b/examples/profile-attribute/inspec.yml new file mode 100644 index 000000000..3193991fc --- /dev/null +++ b/examples/profile-attribute/inspec.yml @@ -0,0 +1,8 @@ +name: profile-attribute +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 diff --git a/examples/profile-inheritance/inspec.yml b/examples/profile-inheritance/inspec.yml new file mode 100644 index 000000000..3cc2de922 --- /dev/null +++ b/examples/profile-inheritance/inspec.yml @@ -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: unix diff --git a/lib/inspec/cli.rb b/lib/inspec/cli.rb index 80ea5e119..662930874 100644 --- a/lib/inspec/cli.rb +++ b/lib/inspec/cli.rb @@ -9,6 +9,7 @@ require 'json' require 'pp' require 'utils/base_cli' require 'utils/json_log' +require 'yaml' class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength class_option :diagnose, type: :boolean, @@ -106,9 +107,18 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength desc 'exec PATHS', 'run all test files at the specified PATH.' exec_options + option :attr_file, type: :array def exec(*targets) diagnose - run_tests(targets, opts) + o = opts.dup + # parse attribute file + unless o['attr_file'].nil? + o['attrs'] = {} + o['attr_file'].each do |file| + o['attrs'] = o['attrs'].merge(YAML.load_file(file)) + end + end + run_tests(targets, o) end desc 'detect', 'detect the target OS' diff --git a/lib/inspec/objects.rb b/lib/inspec/objects.rb index 8b4a63b8d..21342ff8b 100644 --- a/lib/inspec/objects.rb +++ b/lib/inspec/objects.rb @@ -1,6 +1,7 @@ # encoding: utf-8 module Inspec + autoload :Attribute, 'inspec/objects/attribute' autoload :Control, 'inspec/objects/control' autoload :EachLoop, 'inspec/objects/each_loop' autoload :List, 'inspec/objects/list' diff --git a/lib/inspec/objects/attribute.rb b/lib/inspec/objects/attribute.rb new file mode 100644 index 000000000..078a4c159 --- /dev/null +++ b/lib/inspec/objects/attribute.rb @@ -0,0 +1,35 @@ +# encoding:utf-8 + +module Inspec + class Attribute + attr_accessor :name + def initialize(name, options) + @name = name + @opts = options + @value = nil + end + + # implicit call is done by inspec to determine the value of an attribute + def value(newvalue = nil) + unless newvalue.nil? + @value = newvalue + end + @value || default + end + + def default + @opts[:default] + end + + def to_hash + { + name: @name, + options: @opts, + } + end + + def to_s + "Attribute #{@name} with #{@value}" + end + end +end diff --git a/lib/inspec/profile.rb b/lib/inspec/profile.rb index de2cb9923..9314f5a0f 100644 --- a/lib/inspec/profile.rb +++ b/lib/inspec/profile.rb @@ -57,6 +57,7 @@ module Inspec def info res = params.dup + # add information about the controls controls = res[:controls].map do |id, rule| next if id.to_s.empty? data = rule.dup @@ -67,6 +68,9 @@ module Inspec [id, data] end res[:controls] = Hash[controls.compact] + + # add information about the required attributes + res[:attributes] = res[:attributes].map(&:to_hash) unless res[:attributes].nil? || res[:attributes].empty? res end @@ -248,13 +252,16 @@ module Inspec 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| f = load_rule_filepath(prefix, rule) load_rule(rule, f, controls, groups) end + params[:attributes] = @runner_context.attributes end + params end def load_rule_filepath(prefix, rule) diff --git a/lib/inspec/profile_context.rb b/lib/inspec/profile_context.rb index cf9ded7f2..42713672f 100644 --- a/lib/inspec/profile_context.rb +++ b/lib/inspec/profile_context.rb @@ -6,10 +6,12 @@ require 'inspec/rule' require 'inspec/dsl' require 'inspec/require_loader' require 'securerandom' +require 'inspec/objects/attribute' module Inspec class ProfileContext # rubocop:disable Metrics/ClassLength attr_reader :rules + attr_reader :attributes def initialize(profile_id, backend, conf) if backend.nil? fail 'ProfileContext is initiated with a backend == nil. ' \ @@ -21,7 +23,7 @@ module Inspec @conf = conf.dup @rules = {} @require_loader = ::Inspec::RequireLoader.new - + @attributes = [] reload_dsl end @@ -84,6 +86,19 @@ module Inspec end end + def register_value(&block) + @values.push(block) + end + + def register_attribute(name, options = {}) + # we need to return an attribute object, in order to allow lazy access + attr = Attribute.new(name, options) + # set value + attr.value(@conf['attrs'][attr.name]) unless @conf['attrs'].nil? + @attributes.push(attr) + attr + end + def set_header(field, val) @current_load[field] = val end @@ -180,9 +195,9 @@ module Inspec profile_context_owner.register_rule(control, &block) unless control.nil? end - # TODO: mock method for attributes; import attribute handling - define_method :attributes do |_name, _options| - nil + # method for attributes; import attribute handling + define_method :attribute do |name, options| + profile_context_owner.register_attribute(name, options) end define_method :skip_control do |id| diff --git a/lib/inspec/runner.rb b/lib/inspec/runner.rb index e03a45063..07e893d46 100644 --- a/lib/inspec/runner.rb +++ b/lib/inspec/runner.rb @@ -15,7 +15,7 @@ require 'inspec/metadata' module Inspec class Runner # rubocop:disable Metrics/ClassLength extend Forwardable - attr_reader :backend, :rules + attr_reader :backend, :rules, :attributes def initialize(conf = {}) @rules = {} @conf = conf.dup @@ -26,6 +26,9 @@ module Inspec RunnerRspec.new(@conf) end + # list of profile attributes + @attributes = [] + load_attributes(@conf) configure_transport end @@ -110,6 +113,9 @@ module Inspec tests = [tests] unless tests.is_a? Array tests.each { |t| add_test_to_context(t, ctx) } + # merge all 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)