Merge pull request #53 from chef/rule-context

overhaul rule structure
This commit is contained in:
Christoph Hartmann 2015-09-25 19:31:54 +02:00
commit 89f05efc94
4 changed files with 280 additions and 279 deletions

View file

@ -1,92 +0,0 @@
# encoding: utf-8
# copyright: 2015, Dominik Richter
# license: All rights reserved
class VulcanoBaseRule
def initialize(id, _opts, &block)
@id = id
@impact = nil
@__code = ''
@__block = block
@title = nil
@desc = nil
# not changeable by the user:
@profile_id = nil
@checks = []
# evaluate the given definition
instance_eval(&block) if block_given?
end
def id(*_)
# never overwrite the ID
@id
end
def impact(v = nil)
@impact = v unless v.nil?
@impact
end
def title(v = nil)
@title = v unless v.nil?
@title
end
def desc(v = nil)
@desc = v unless v.nil?
@desc
end
def self.merge(dst, src)
if src.id != dst.id
# TODO: register an error, this case should not happen
return
end
sp = src.instance_variable_get(:@profile_id)
dp = dst.instance_variable_get(:@profile_id)
if sp != dp
# TODO: register an error, this case should not happen
return
end
# merge all fields
dst.impact(src.impact) unless src.impact.nil?
dst.title(src.title) unless src.title.nil?
dst.desc(src.desc) unless src.desc.nil?
# merge indirect fields
# checks defined in the source will completely eliminate
# all checks that were defined in the destination
sc = src.instance_variable_get(:@checks)
unless sc.nil? || sc.empty?
dst.instance_variable_set(:@checks, sc)
end
end
# Get the full id consisting of profile id + rule id
# for the rule. If the rule's profile id is empty,
# the given profile_id will be used instead and also
# set for the rule.
def self.full_id(profile_id, rule)
if rule.is_a?(String) or rule.nil?
rid = rule
else
# As the profile context is exclusively pulled with a
# profile ID, attach it to the rule if necessary.
rid = rule.instance_variable_get(:@id)
if rid.nil?
# TODO: Message about skipping this rule
# due to missing ID
return nil
end
end
pid = rule.instance_variable_get(:@profile_id)
if pid.nil?
rule.instance_variable_set(:@profile_id, profile_id)
pid = profile_id
end
if pid.nil? or pid.empty?
return rid
else
return "#{pid}/#{rid}"
end
end
end

151
lib/vulcano/dsl.rb Normal file
View file

@ -0,0 +1,151 @@
# encoding: utf-8
# copyright: 2015, Dominik Richter
# license: All rights reserved
module Vulcano::DSL
def require_rules(id, &block)
::Vulcano::DSL.load_spec_files_for_profile self, id, false, &block
end
def include_rules(id, &block)
::Vulcano::DSL.load_spec_files_for_profile self, id, true, &block
end
# Register a given rule with RSpec and
# let it run. This happens after everything
# else is merged in.
def self.execute_rule(r, profile_id)
checks = r.instance_variable_get(:@checks)
fid = VulcanoBaseRule.full_id(r, profile_id)
checks.each do |m, a, b|
# check if the resource is skippable and skipped
if a.is_a?(Array) && !a.empty? &&
a[0].respond_to?(:resource_skipped) &&
!a[0].resource_skipped.nil?
cres = ::Vulcano::Rule.__send__(m, *a) do
it a[0].resource_skipped
end
else
# execute the method
cres = ::Vulcano::Rule.__send__(m, *a, &b)
end
if m == 'describe'
set_rspec_ids(cres, fid)
end
end
end
private
# merge two rules completely; all defined
# fields from src will be overwritten in dst
def self.merge_rules(dst, src)
VulcanoBaseRule.merge dst, src
end
# Attach an ID attribute to the
# metadata of all examples
# TODO: remove this once IDs are in rspec-core
def self.set_rspec_ids(obj, id)
obj.examples.each {|ex|
ex.metadata[:id] = id
}
obj.children.each {|c|
set_rspec_ids(c, id)
}
end
def self.load_spec_file_for_profile(profile_id, file, rule_registry, only_ifs)
raw = File.read(file)
# TODO: error-handling
ctx = Vulcano::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
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 create a set of rules from it
block_registry = {}
if block_given?
ctx = Vulcano::ProfileContext.new(profile_id, block_registry, only_ifs)
ctx.instance_eval(&block)
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) }
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
rule_registry.each do |_id, rule|
bind_context.__register_rule rule
end
end
def self.get_spec_files_for_profile(id)
base_path = '/etc/vulcanosec/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')]
end
files
end
end
module Vulcano::GlobalDSL
def __register_rule(r)
# make sure the profile id is attached to the rule
::Vulcano::DSL.execute_rule(r, __profile_id)
end
def __unregister_rule(_id)
end
end
module Vulcano::DSLHelper
def self.bind_dsl(scope)
# rubocop:disable Lint/NestedMethodDefinition
(class << scope; self; end).class_exec do
include Vulcano::DSL
include Vulcano::GlobalDSL
def __profile_id
ENV['VULCANOSEC_PROFILE_ID']
end
end
# rubocop:enable all
end
end
::Vulcano::DSLHelper.bind_dsl(self)

View file

@ -1,5 +1,7 @@
# encoding: utf-8
require 'vulcano/backend'
require 'vulcano/rule'
require 'vulcano/dsl'
module Vulcano
class ProfileContext
@ -15,11 +17,54 @@ module Vulcano
@only_ifs = only_ifs
profile_context_owner = self
dsl = Module.new do
Vulcano::Resource.registry.each do |id, r|
define_method id.to_sym do |*args|
r.new(backend, *args)
end
end
end
rule_class = Class.new(Vulcano::Rule) do
include RSpec::Core::DSL
include dsl
end
outer_dsl = Class.new do
include dsl
define_method :rule do |*args, &block|
id = args[0]
opts = args[1] || {}
return if @skip_profile
__register_rule rule_class.new(id, opts, &block)
end
define_method :describe do |*args, &block|
path = block.source_location[0]
line = block.source_location[1]
id = "#{File.basename(path)}:#{line}"
rule = rule_class.new(id, {}) do
describe(*args, &block)
end
__register_rule rule, &block
end
def skip_rule(id)
__unregister_rule id
end
def only_if(&block)
return unless block_given?
@skip_profile = !block.call
end
end
# This is the heart of the profile context
# An instantiated object which has all resources registered to it
# and exposes them to the a test file.
# rubocop:disable Lint/NestedMethodDefinition
ctx = Class.new do
ctx = Class.new(outer_dsl) do
include Vulcano::DSL
define_method :__register_rule do |*args|
@ -29,12 +74,6 @@ module Vulcano
profile_context_owner.unregister_rule(*args)
end
Vulcano::Resource.registry.each do |id, r|
define_method id.to_sym do |*args|
r.new(backend, *args)
end
end
def to_s
'Profile Context Run'
end
@ -49,13 +88,13 @@ module Vulcano
end
def unregister_rule(id)
full_id = VulcanoBaseRule.full_id(@profile_id, id)
full_id = Vulcano::Rule.full_id(@profile_id, id)
@rules[full_id] = nil
end
def register_rule(r)
# get the full ID
full_id = VulcanoBaseRule.full_id(@profile_id, r)
full_id = Vulcano::Rule.full_id(@profile_id, r)
if full_id.nil?
# TODO: error
return
@ -65,7 +104,7 @@ module Vulcano
if existing.nil?
@rules[full_id] = r
else
VulcanoBaseRule.merge(existing, r)
Vulcano::Rule.merge(existing, r)
end
end
end

View file

@ -1,195 +1,98 @@
# encoding: utf-8
# copyright: 2015, Dominik Richter
# license: All rights reserved
require 'vulcano/base_rule'
module Vulcano
class Rule < VulcanoBaseRule
include RSpec::Core::DSL
class Rule
def initialize(id, _opts, &block)
@id = id
@impact = nil
@__code = ''
@__block = block
@title = nil
@desc = nil
# not changeable by the user:
@profile_id = nil
@checks = []
# evaluate the given definition
instance_eval(&block) if block_given?
end
def id(*_)
# never overwrite the ID
@id
end
def impact(v = nil)
@impact = v unless v.nil?
@impact
end
def title(v = nil)
@title = v unless v.nil?
@title
end
def desc(v = nil)
@desc = v unless v.nil?
@desc
end
# Override RSpec methods to add
# IDs to each example group
# TODO: remove this once IDs are in rspec-core
def describe(sth, &block)
@checks.push(['describe', [sth], block])
end
# redirect all regular method calls to the
# core DSL (which is attached to the class)
def method_missing(m, *a, &b)
Vulcano::Rule.__send__(m, *a, &b)
def self.merge(dst, src)
if src.id != dst.id
# TODO: register an error, this case should not happen
return
end
sp = src.instance_variable_get(:@profile_id)
dp = dst.instance_variable_get(:@profile_id)
if sp != dp
# TODO: register an error, this case should not happen
return
end
# merge all fields
dst.impact(src.impact) unless src.impact.nil?
dst.title(src.title) unless src.title.nil?
dst.desc(src.desc) unless src.desc.nil?
# merge indirect fields
# checks defined in the source will completely eliminate
# all checks that were defined in the destination
sc = src.instance_variable_get(:@checks)
unless sc.nil? || sc.empty?
dst.instance_variable_set(:@checks, sc)
end
end
end
end
module Vulcano::DSL
def rule(id, opts = {}, &block)
return if @skip_profile
__register_rule Vulcano::Rule.new(id, opts, &block)
end
def describe(*args, &block)
path = block.source_location[0]
line = block.source_location[1]
id = "#{File.basename(path)}:#{line}"
rule = Vulcano::Rule.new(id, {}) do
describe(*args, &block)
end
__register_rule rule, &block
end
def skip_rule(id)
__unregister_rule id
end
def only_if(&block)
return unless block_given?
@skip_profile = !block.call
end
def require_rules(id, &block)
::Vulcano::DSL.load_spec_files_for_profile self, id, false, &block
end
def include_rules(id, &block)
::Vulcano::DSL.load_spec_files_for_profile self, id, true, &block
end
# Register a given rule with RSpec and
# let it run. This happens after everything
# else is merged in.
def self.execute_rule(r, profile_id)
checks = r.instance_variable_get(:@checks)
fid = VulcanoBaseRule.full_id(r, profile_id)
checks.each do |m, a, b|
# check if the resource is skippable and skipped
if a.is_a?(Array) && !a.empty? &&
a[0].respond_to?(:resource_skipped) &&
!a[0].resource_skipped.nil?
cres = ::Vulcano::Rule.__send__(m, *a) do
it a[0].resource_skipped
# Get the full id consisting of profile id + rule id
# for the rule. If the rule's profile id is empty,
# the given profile_id will be used instead and also
# set for the rule.
def self.full_id(profile_id, rule)
if rule.is_a?(String) or rule.nil?
rid = rule
else
# As the profile context is exclusively pulled with a
# profile ID, attach it to the rule if necessary.
rid = rule.instance_variable_get(:@id)
if rid.nil?
# TODO: Message about skipping this rule
# due to missing ID
return nil
end
end
pid = rule.instance_variable_get(:@profile_id)
if pid.nil?
rule.instance_variable_set(:@profile_id, profile_id)
pid = profile_id
end
if pid.nil? or pid.empty?
return rid
else
# execute the method
cres = ::Vulcano::Rule.__send__(m, *a, &b)
end
if m == 'describe'
set_rspec_ids(cres, fid)
return "#{pid}/#{rid}"
end
end
end
private
# merge two rules completely; all defined
# fields from src will be overwritten in dst
def self.merge_rules(dst, src)
VulcanoBaseRule.merge dst, src
end
# Attach an ID attribute to the
# metadata of all examples
# TODO: remove this once IDs are in rspec-core
def self.set_rspec_ids(obj, id)
obj.examples.each {|ex|
ex.metadata[:id] = id
}
obj.children.each {|c|
set_rspec_ids(c, id)
}
end
def self.load_spec_file_for_profile(profile_id, file, rule_registry, only_ifs)
raw = File.read(file)
# TODO: error-handling
ctx = Vulcano::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
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 create a set of rules from it
block_registry = {}
if block_given?
ctx = Vulcano::ProfileContext.new(profile_id, block_registry, only_ifs)
ctx.instance_eval(&block)
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) }
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
rule_registry.each do |_id, rule|
bind_context.__register_rule rule
end
end
def self.get_spec_files_for_profile(id)
base_path = '/etc/vulcanosec/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')]
end
files
end
end
module Vulcano::GlobalDSL
def __register_rule(r)
# make sure the profile id is attached to the rule
::Vulcano::DSL.execute_rule(r, __profile_id)
end
def __unregister_rule(_id)
end
end
module Vulcano::DSLHelper
def self.bind_dsl(scope)
# rubocop:disable Lint/NestedMethodDefinition
(class << scope; self; end).class_exec do
include Vulcano::DSL
include Vulcano::GlobalDSL
def __profile_id
ENV['VULCANOSEC_PROFILE_ID']
end
end
# rubocop:enable all
end
end
::Vulcano::DSLHelper.bind_dsl(self)