2016-08-31 15:48:51 +00:00
|
|
|
# encoding: utf-8
|
|
|
|
# author: Dominik Richter
|
|
|
|
# author: Christoph Hartmann
|
|
|
|
require 'inspec/dsl'
|
|
|
|
require 'inspec/dsl_shared'
|
|
|
|
|
|
|
|
module Inspec
|
|
|
|
#
|
|
|
|
# ControlEvalContext constructs an anonymous class that control
|
|
|
|
# files will be instance_exec'd against.
|
|
|
|
#
|
|
|
|
# The anonymous class includes the given passed resource_dsl as well
|
|
|
|
# as the basic DSL of the control files (describe, control, title,
|
|
|
|
# etc).
|
|
|
|
#
|
2017-12-07 19:22:55 +00:00
|
|
|
class ControlEvalContext
|
2016-08-31 15:48:51 +00:00
|
|
|
# Create the context for controls. This includes all components of the DSL,
|
|
|
|
# including matchers and resources.
|
|
|
|
#
|
|
|
|
# @param [ResourcesDSL] resources_dsl which has all resources to attach
|
|
|
|
# @return [RuleContext] the inner context of rules
|
2018-09-12 20:42:58 +00:00
|
|
|
def self.rule_context(resources_dsl, profile_id)
|
2016-08-31 15:48:51 +00:00
|
|
|
require 'rspec/core/dsl'
|
|
|
|
Class.new(Inspec::Rule) do
|
|
|
|
include RSpec::Core::DSL
|
2016-09-14 13:57:26 +00:00
|
|
|
with_resource_dsl resources_dsl
|
2018-09-12 20:42:58 +00:00
|
|
|
|
|
|
|
# allow attributes to be accessed within control blocks
|
|
|
|
define_method :attribute do |name|
|
|
|
|
Inspec::AttributeRegistry.find_attribute(name, profile_id).value
|
|
|
|
end
|
Plugin Type: DSLs (#3557)
This PR adds 5 closely related plugin types, which allow a plugin to implement new DSL methods / keywords. The mechanism to activate the plugins are all very similar - basically, in a particular location in the code, `method_missing` is implemented, and is used to activate the particular type of DSL being requested.
4 of the DSL plugin types relate to code that could appear in a profile control file.
* outer_profile_dsl plugins allow you to extend the code in profile Ruby files that appear outside `control` or `describe` blocks.
* control_dsl plugins allow you to extend the code within `control` blocks.
* describe_dsl plugins allow you to extend the code within `describe` blocks.
* test_dsl plugins allow you to extend the code within `it`/`its` blocks.
Finally, the `resource_dsl` plugin allows you to extend the code used within custom resources.
Basic unit tests are provided to prove that the plugin types are properly defined.
A simple plugin fixture defining DSL hooks (based on favorite foods) is included, and is exercised through a set of functional tests.
The plugin developer docs are updated to describe the 5 DSLs.
*Note*: Implementing a plugin using any of the DSL plugin types is experimental. The contexts that are exposed to the DSL methods are private and poorly documented. The InSpec project does not claim the APIs used by these plugin types are covered by SemVer. Plugin authors are encouraged to pin tightly to the `inspec` gem in their gemspecs.
Motivation for this plugin comes from the desire to allow passionate community members to implement things like "2 out of 3" tests, example groups, improved serverspec compatibility, "they/their" and other "fluency" changes, as well as make it possible for future work by the InSpec team to be implemented as a core plugin, rather than a direct change to the main codebase.
2018-11-29 19:14:06 +00:00
|
|
|
|
|
|
|
# Support for Control DSL plugins.
|
|
|
|
# This is called when an unknown method is encountered
|
|
|
|
# within a control block.
|
|
|
|
def method_missing(method_name, *arguments, &block)
|
|
|
|
# Check to see if there is a control_dsl plugin activator hook with the method name
|
|
|
|
registry = Inspec::Plugin::V2::Registry.instance
|
|
|
|
hook = registry.find_activators(plugin_type: :control_dsl, activator_name: method_name).first
|
|
|
|
if hook
|
|
|
|
# OK, load the hook if it hasn't been already. We'll then know a module,
|
|
|
|
# which we can then inject into the context
|
2019-02-06 17:22:51 +00:00
|
|
|
hook.activate
|
|
|
|
|
Plugin Type: DSLs (#3557)
This PR adds 5 closely related plugin types, which allow a plugin to implement new DSL methods / keywords. The mechanism to activate the plugins are all very similar - basically, in a particular location in the code, `method_missing` is implemented, and is used to activate the particular type of DSL being requested.
4 of the DSL plugin types relate to code that could appear in a profile control file.
* outer_profile_dsl plugins allow you to extend the code in profile Ruby files that appear outside `control` or `describe` blocks.
* control_dsl plugins allow you to extend the code within `control` blocks.
* describe_dsl plugins allow you to extend the code within `describe` blocks.
* test_dsl plugins allow you to extend the code within `it`/`its` blocks.
Finally, the `resource_dsl` plugin allows you to extend the code used within custom resources.
Basic unit tests are provided to prove that the plugin types are properly defined.
A simple plugin fixture defining DSL hooks (based on favorite foods) is included, and is exercised through a set of functional tests.
The plugin developer docs are updated to describe the 5 DSLs.
*Note*: Implementing a plugin using any of the DSL plugin types is experimental. The contexts that are exposed to the DSL methods are private and poorly documented. The InSpec project does not claim the APIs used by these plugin types are covered by SemVer. Plugin authors are encouraged to pin tightly to the `inspec` gem in their gemspecs.
Motivation for this plugin comes from the desire to allow passionate community members to implement things like "2 out of 3" tests, example groups, improved serverspec compatibility, "they/their" and other "fluency" changes, as well as make it possible for future work by the InSpec team to be implemented as a core plugin, rather than a direct change to the main codebase.
2018-11-29 19:14:06 +00:00
|
|
|
# Inject the module's methods into the context.
|
|
|
|
# implementation_class is the field name, but this is actually a module.
|
|
|
|
self.class.include(hook.implementation_class)
|
|
|
|
# Now that the module is loaded, it defined one or more methods
|
|
|
|
# (presumably the one we were looking for.)
|
|
|
|
# We still haven't called it, so do so now.
|
|
|
|
send(method_name, *arguments, &block)
|
|
|
|
else
|
|
|
|
# If we couldn't find a plugin to match, maybe something up above has it,
|
|
|
|
# or maybe it is just a unknown method error.
|
|
|
|
super
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-08-31 15:48:51 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Creates the heart of the control eval context:
|
|
|
|
#
|
|
|
|
# An instantiated object which has all resources registered to it
|
|
|
|
# and exposes them to the a test file.
|
|
|
|
#
|
|
|
|
# @param profile_context [Inspec::ProfileContext]
|
|
|
|
# @param outer_dsl [OuterDSLClass]
|
|
|
|
# @return [ProfileContextClass]
|
2017-10-17 12:47:30 +00:00
|
|
|
def self.create(profile_context, resources_dsl) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
2016-08-31 15:48:51 +00:00
|
|
|
profile_context_owner = profile_context
|
|
|
|
profile_id = profile_context.profile_id
|
2018-09-12 20:42:58 +00:00
|
|
|
rule_class = rule_context(resources_dsl, profile_id)
|
2017-11-21 07:49:41 +00:00
|
|
|
Class.new do # rubocop:disable Metrics/BlockLength
|
2016-08-31 15:48:51 +00:00
|
|
|
include Inspec::DSL
|
|
|
|
include Inspec::DSL::RequireOverride
|
|
|
|
include resources_dsl
|
|
|
|
|
2017-10-17 12:47:30 +00:00
|
|
|
attr_accessor :skip_file
|
|
|
|
|
2017-12-05 13:13:41 +00:00
|
|
|
def initialize(backend, conf, dependencies, require_loader, skip_only_if_eval)
|
2016-08-31 15:48:51 +00:00
|
|
|
@backend = backend
|
|
|
|
@conf = conf
|
|
|
|
@dependencies = dependencies
|
|
|
|
@require_loader = require_loader
|
2018-08-07 17:10:45 +00:00
|
|
|
@skip_file_message = nil
|
2017-10-17 12:47:30 +00:00
|
|
|
@skip_file = false
|
2017-12-05 13:13:41 +00:00
|
|
|
@skip_only_if_eval = skip_only_if_eval
|
2016-08-31 15:48:51 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
define_method :title do |arg|
|
|
|
|
profile_context_owner.set_header(:title, arg)
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_s
|
2016-09-05 08:28:50 +00:00
|
|
|
"Control Evaluation Context (#{profile_name})"
|
2016-08-31 15:48:51 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
define_method :profile_name do
|
|
|
|
profile_id
|
|
|
|
end
|
|
|
|
|
|
|
|
define_method :control do |*args, &block|
|
|
|
|
id = args[0]
|
|
|
|
opts = args[1] || {}
|
2017-12-05 13:13:41 +00:00
|
|
|
opts[:skip_only_if_eval] = @skip_only_if_eval
|
2016-08-31 15:48:51 +00:00
|
|
|
register_control(rule_class.new(id, profile_id, opts, &block))
|
|
|
|
end
|
|
|
|
|
2016-09-05 08:28:50 +00:00
|
|
|
#
|
|
|
|
# Describe allows users to write rspec-like bare describe
|
|
|
|
# blocks without declaring an inclosing control. Here, we
|
|
|
|
# generate a control for them automatically and then execute
|
|
|
|
# the describe block in the context of that control.
|
|
|
|
#
|
2016-08-31 15:48:51 +00:00
|
|
|
define_method :describe do |*args, &block|
|
2017-11-21 07:49:41 +00:00
|
|
|
loc = block_location(block, caller(1..1).first)
|
2016-08-31 15:48:51 +00:00
|
|
|
id = "(generated from #{loc} #{SecureRandom.hex})"
|
|
|
|
|
|
|
|
res = nil
|
|
|
|
rule = rule_class.new(id, profile_id, {}) do
|
|
|
|
res = describe(*args, &block)
|
|
|
|
end
|
|
|
|
register_control(rule, &block)
|
|
|
|
|
|
|
|
res
|
|
|
|
end
|
|
|
|
|
2016-09-15 07:54:15 +00:00
|
|
|
define_method :add_resource do |name, new_res|
|
|
|
|
resources_dsl.module_exec do
|
|
|
|
define_method name.to_sym do |*args|
|
|
|
|
new_res.new(@backend, name.to_s, *args)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-08-31 15:48:51 +00:00
|
|
|
define_method :add_resources do |context|
|
|
|
|
self.class.class_eval do
|
|
|
|
include context.to_resources_dsl
|
|
|
|
end
|
|
|
|
|
|
|
|
rule_class.class_eval do
|
|
|
|
include context.to_resources_dsl
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
define_method :add_subcontext do |context|
|
|
|
|
profile_context_owner.add_subcontext(context)
|
|
|
|
end
|
|
|
|
|
|
|
|
define_method :register_control do |control, &block|
|
2018-01-02 19:04:13 +00:00
|
|
|
if @skip_file
|
2018-08-07 17:10:45 +00:00
|
|
|
::Inspec::Rule.set_skip_rule(control, true, @skip_file_message)
|
2016-09-14 07:41:19 +00:00
|
|
|
end
|
2016-08-31 15:48:51 +00:00
|
|
|
|
2018-01-02 19:04:13 +00:00
|
|
|
unless profile_context_owner.profile_supports_platform?
|
|
|
|
platform = inspec.platform
|
|
|
|
msg = "Profile #{profile_context_owner.profile_id} is not supported on platform #{platform.name}/#{platform.release}."
|
2018-08-07 17:10:45 +00:00
|
|
|
::Inspec::Rule.set_skip_rule(control, true, msg)
|
2018-01-02 19:04:13 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
unless profile_context_owner.profile_supports_inspec_version?
|
|
|
|
msg = "Profile #{profile_context_owner.profile_id} is not supported on InSpec version (#{Inspec::VERSION})."
|
2018-08-07 17:10:45 +00:00
|
|
|
::Inspec::Rule.set_skip_rule(control, true, msg)
|
2018-01-02 19:04:13 +00:00
|
|
|
end
|
|
|
|
|
2016-08-31 15:48:51 +00:00
|
|
|
profile_context_owner.register_rule(control, &block) unless control.nil?
|
|
|
|
end
|
|
|
|
|
|
|
|
# method for attributes; import attribute handling
|
2018-10-03 05:57:25 +00:00
|
|
|
define_method :attribute do |name, options = nil|
|
|
|
|
if options.nil?
|
2018-09-12 20:42:58 +00:00
|
|
|
Inspec::AttributeRegistry.find_attribute(name, profile_id).value
|
|
|
|
else
|
|
|
|
profile_context_owner.register_attribute(name, options)
|
|
|
|
end
|
2016-08-31 15:48:51 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
define_method :skip_control do |id|
|
|
|
|
profile_context_owner.unregister_rule(id)
|
|
|
|
end
|
|
|
|
|
2018-08-07 17:10:45 +00:00
|
|
|
define_method :only_if do |message = nil, &block|
|
2017-10-17 12:47:30 +00:00
|
|
|
return unless block
|
2017-12-05 13:13:41 +00:00
|
|
|
return if @skip_file == true
|
|
|
|
return if @skip_only_if_eval == true
|
|
|
|
|
|
|
|
return if block.yield == true
|
2017-10-17 12:47:30 +00:00
|
|
|
# Apply `set_skip_rule` for other rules in the same file
|
|
|
|
profile_context_owner.rules.values.each do |r|
|
|
|
|
sources_match = r.source_file == block.source_location[0]
|
2018-08-07 17:10:45 +00:00
|
|
|
Inspec::Rule.set_skip_rule(r, true, message) if sources_match
|
2017-10-17 12:47:30 +00:00
|
|
|
end
|
|
|
|
|
2018-08-07 17:10:45 +00:00
|
|
|
@skip_file_message = message
|
2017-10-17 12:47:30 +00:00
|
|
|
@skip_file = true
|
2016-08-31 15:48:51 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
alias_method :rule, :control
|
|
|
|
alias_method :skip_rule, :skip_control
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def block_location(block, alternate_caller)
|
|
|
|
if block.nil?
|
|
|
|
alternate_caller[/^(.+:\d+):in .+$/, 1] || 'unknown'
|
|
|
|
else
|
|
|
|
path, line = block.source_location
|
|
|
|
"#{File.basename(path)}:#{line}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|