diff --git a/lib/vulcano/base_rule.rb b/lib/vulcano/base_rule.rb deleted file mode 100644 index bc9abadb2..000000000 --- a/lib/vulcano/base_rule.rb +++ /dev/null @@ -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 diff --git a/lib/vulcano/dsl.rb b/lib/vulcano/dsl.rb new file mode 100644 index 000000000..5c5ef8967 --- /dev/null +++ b/lib/vulcano/dsl.rb @@ -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) diff --git a/lib/vulcano/profile_context.rb b/lib/vulcano/profile_context.rb index ec966c80e..8d8292977 100644 --- a/lib/vulcano/profile_context.rb +++ b/lib/vulcano/profile_context.rb @@ -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 diff --git a/lib/vulcano/rule.rb b/lib/vulcano/rule.rb index ec23bc37d..f59795498 100644 --- a/lib/vulcano/rule.rb +++ b/lib/vulcano/rule.rb @@ -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)