diff --git a/.tests.yaml b/.tests.yaml new file mode 100644 index 000000000..e61dd83eb --- /dev/null +++ b/.tests.yaml @@ -0,0 +1,4 @@ +images: +- ubuntu-1204-20150612 +- ubuntu-1404-20150320 +- ubuntu-latest diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..88b2f5c5f --- /dev/null +++ b/Rakefile @@ -0,0 +1,24 @@ +require 'rake/testtask' + +task :default => :test +Rake::TestTask.new do |t| + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.warning = true + t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) +end + +namespace :test do + task :isolated do + Dir.glob("test/**/*_test.rb").all? do |file| + sh(Gem.ruby, '-w', '-Ilib:test', file) + end or raise "Failures" + end + + task :integration do + tests = Dir["test/resource/*.rb"] + return if tests.empty? + sh(Gem.ruby, 'test/docker.rb', *tests) + end +end diff --git a/bin/vulcano b/bin/vulcano index 12b0cf026..aa0a46ff0 100755 --- a/bin/vulcano +++ b/bin/vulcano @@ -45,9 +45,9 @@ class VulcanoCLI < Thor desc "exec PATHS", "run all test files" option :id, type: :string, desc: 'Attach a profile ID to all test results' - option :target, type: :string, default: nil, + option :target, aliases: :t, type: :string, default: nil, desc: 'Simple targeting option using URIs, e.g. ssh://user:pass@host:port' - option :backend, type: :string, default: nil, + option :backend, aliases: :b, type: :string, default: nil, desc: 'Choose a backend: exec (run locally), ssh, winrm.' option :host, type: :string, desc: 'Specify a remote host which is tested.' @@ -69,10 +69,13 @@ class VulcanoCLI < Thor desc: 'Allow remote scans with WinRM to run on self-signed certificates.' option :winrm_ssl, type: :boolean, default: false, desc: 'Configure WinRM scans to run via SSL instead of pure HTTP.' - def exec(*resources) + option :format, type: :string, default: 'progress' + def exec(*tests) runner = Vulcano::Runner.new(options[:id], options) - runner.add_resources(resources) + runner.add_tests(tests) runner.run + rescue RuntimeError => e + puts e.message end end diff --git a/lib/resources/apache_conf.rb b/lib/resources/apache_conf.rb index 5e6718c24..da5e1db6f 100644 --- a/lib/resources/apache_conf.rb +++ b/lib/resources/apache_conf.rb @@ -5,12 +5,12 @@ require 'utils/simpleconfig' require 'utils/find_files' -class ApacheConf +class ApacheConf < Vulcano.resource(1) + name 'apache_conf' def initialize( conf_path ) - @runner = Specinfra::Runner @conf_path = conf_path - @conf_dir = File.expand_path(File.dirname @conf_path) + @conf_dir = File.dirname(@conf_path) @files_contents = {} @content = nil @params = nil @@ -31,13 +31,13 @@ class ApacheConf end def filter_comments data - contents = "" + content = "" data.each_line do |line| if (!line.match(/^\s*#/)) then - contents << line + content << line end end - return contents + return content end def read_content @@ -45,11 +45,13 @@ class ApacheConf @params = {} # skip if the main configuration file doesn't exist - if !@runner.check_file_is_file(@conf_path) + file = vulcano.file(@conf_path) + if !file.file? return skip_resource "Can't find file \"#{@conf_path}\"" end - raw_conf = read_file(@conf_path) - if raw_conf.empty? && @runner.get_file_size(@conf_path).stdout.strip.to_i > 0 + + raw_conf = file.content + if raw_conf.empty? && file.size > 0 return skip_resource("Can't read file \"#{@conf_path}\"") end @@ -100,6 +102,6 @@ class ApacheConf end def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout + @files_contents[path] ||= vulcano.file(path).content end end diff --git a/lib/resources/audit_policy.rb b/lib/resources/audit_policy.rb index 1562549cc..c139c44fb 100644 --- a/lib/resources/audit_policy.rb +++ b/lib/resources/audit_policy.rb @@ -3,14 +3,14 @@ # license: All rights reserved ## Advanced Auditing -# As soon as you start applying Advanced Audit Configuration Policy, legacy policies will be completely ignored. +# As soon as you start applying Advanced Audit Configuration Policy, legacy policies will be completely ignored. # reference: https://technet.microsoft.com/en-us/library/cc753632.aspx -# use: +# use: # - list all categories: Auditpol /list /subcategory:* /r # - list parameters: Auditpol /get /category:"System" /subcategory:"IPsec Driver" # - list specific parameter: Auditpol /get /subcategory:"IPsec Driver" -# -# @link: http://blogs.technet.com/b/askds/archive/2011/03/11/getting-the-effective-audit-policy-in-windows-7-and-2008-r2.aspx +# +# @link: http://blogs.technet.com/b/askds/archive/2011/03/11/getting-the-effective-audit-policy-in-windows-7-and-2008-r2.aspx =begin Category/Subcategory,GUID @@ -92,9 +92,8 @@ Further information is available at: https://msdn.microsoft.com/en-us/library/dd =end -include Serverspec::Type - -class AuditPolicy < Serverspec::Type::Base +class AuditPolicy < Vulcano.resource(1) + name 'audit_policy' def method_missing(method) key = method.to_s @@ -102,12 +101,11 @@ class AuditPolicy < Serverspec::Type::Base # expected result: # Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting # WIN-MB8NINQ388J,System,Kerberos Authentication Service,{0CCE9242-69AE-11D9-BED3-505054503030},No Auditing, - command_result ||= @runner.run_command("Auditpol /get /subcategory:'#{key}' /r") - result = command_result.stdout + result ||= vulcano.run_command("Auditpol /get /subcategory:'#{key}' /r").stdout # find line target = nil - result.each_line {|s| + result.each_line {|s| target = s.strip if s.match(/\b.*#{key}.*\b/) } @@ -115,7 +113,7 @@ class AuditPolicy < Serverspec::Type::Base if target != nil # split csv values and return value value = target.split(',')[4] - else + else value = nil end @@ -123,13 +121,7 @@ class AuditPolicy < Serverspec::Type::Base end def to_s - %Q[Windows Advanced Auditing] + 'Windows Advanced Auditing' end end - -module Serverspec::Type - def audit_policy() - AuditPolicy.new() - end -end \ No newline at end of file diff --git a/lib/resources/auditd_conf.rb b/lib/resources/auditd_conf.rb index 029983e6c..007774d36 100644 --- a/lib/resources/auditd_conf.rb +++ b/lib/resources/auditd_conf.rb @@ -4,10 +4,10 @@ require 'utils/simpleconfig' -class AuditDaemonConf < Vulcano::Resource +class AuditDaemonConf < Vulcano.resource(1) + name 'audit_daemon_conf' def initialize - @runner = Specinfra::Runner @conf_path = '/etc/audit/auditd.conf' @files_contents = {} @content = nil @@ -26,27 +26,19 @@ class AuditDaemonConf < Vulcano::Resource def read_content # read the file - if !@runner.check_file_is_file(@conf_path) + file = vulcano.file(@conf_path) + if !file.file? return skip_resource "Can't find file \"#{@conf_path}\"" end - @content = read_file(@conf_path) - if @content.empty? && @runner.get_file_size(@conf_path).stdout.strip.to_i > 0 + + @content = file.content + if @content.empty? && file.size > 0 return skip_resource "Can't read file \"#{@conf_path}\"" end # parse the file @params = SimpleConfig.new(@content, multiple_values: false ).params - @content end - def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout - end end - -module Serverspec::Type - def audit_daemon_conf() - AuditDaemonConf.new() - end -end \ No newline at end of file diff --git a/lib/resources/auditd_rules.rb b/lib/resources/auditd_rules.rb index 1f01793c9..87d33a5a8 100644 --- a/lib/resources/auditd_rules.rb +++ b/lib/resources/auditd_rules.rb @@ -2,25 +2,24 @@ # copyright: 2015, Vulcano Security GmbH # license: All rights reserved -include Serverspec::Type - -class AuditDaemonRules < Vulcano::Resource +class AuditDaemonRules < Vulcano.resource(1) + name 'audit_daemon_rules' def initialize - @runner = Specinfra::Runner - @command_result ||= @runner.run_command("/sbin/auditctl -l") - @content = @command_result.stdout.chomp + @content = vulcano.run_command("/sbin/auditctl -l").stdout.chomp @opts = { assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/, multiple_values: true } - @params = SimpleConfig.new(@content, @opts).params - + end + + def params + @params ||= SimpleConfig.new(@content, @opts).params end def method_missing name - @params[name.to_s] + params[name.to_s] end def status name @@ -28,7 +27,7 @@ class AuditDaemonRules < Vulcano::Resource assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/, multiple_values: false } - @status_content ||= @runner.run_command("/sbin/auditctl -s").stdout.chomp + @status_content ||= vulcano.run_command("/sbin/auditctl -s").stdout.chomp @status_params = SimpleConfig.new(@status_content, @status_opts).params status = @status_params["AUDIT_STATUS"] if (status == nil) then return nil end @@ -38,13 +37,7 @@ class AuditDaemonRules < Vulcano::Resource end def to_s - %Q[AuditDaemonRules] + 'Audit Daemon Rules' end end - -module Serverspec::Type - def audit_daemon_rules() - AuditDaemonRules.new() - end -end \ No newline at end of file diff --git a/lib/resources/command.rb b/lib/resources/command.rb index a9da41ede..314810ed6 100644 --- a/lib/resources/command.rb +++ b/lib/resources/command.rb @@ -1,14 +1,32 @@ -# encoding: utf-8 # copyright: 2015, Vulcano Security GmbH +# encoding: utf-8 # license: All rights reserved -module Serverspec::Type - class Command < Base - # Check if a given command (executable) exists - # in the default path - def exists? - cmd = @name - Command.new("type \"#{cmd}\" > /dev/null").exit_status == 0 - end +class Command < Vulcano.resource(1) + name 'command' + def initialize(cmd) + @command = cmd end -end \ No newline at end of file + + def result + @result ||= vulcano.run_command(@command) + end + + def stdout + result.stdout + end + + def stderr + result.stderr + end + + def exit_status + result.exit_status.to_i + end + + def exists? + res = vulcano.run_command("type \"#{@command}\" > /dev/null") + res.exit_status.to_i == 0 + end + +end diff --git a/lib/resources/directory.rb b/lib/resources/directory.rb new file mode 100644 index 000000000..1b30f4a2a --- /dev/null +++ b/lib/resources/directory.rb @@ -0,0 +1,9 @@ +# encoding: utf-8 + +require 'resources/file' + +module Vulcano::Resources + class Directory < File + name 'directory' + end +end diff --git a/lib/resources/env.rb b/lib/resources/env.rb index 000cd1b83..d470f9fb2 100644 --- a/lib/resources/env.rb +++ b/lib/resources/env.rb @@ -2,40 +2,31 @@ # copyright: 2015, Vulcano Security GmbH # license: All rights reserved -include Serverspec::Type +class OsEnv < Vulcano.resource(1) + name 'os_env' -class EnvironmentVariable < Serverspec::Type::Base - - def method_missing(method) - @command_result ||= @runner.run_command("su - root -c 'echo $#{name}'") - end - - def content - command_result.stdout.chomp + attr_reader :content + def initialize(field) + @command_result = vulcano.run_command("su - root -c 'echo $#{name}'") + @content = @command_result.stdout.chomp end def split # -1 is required to catch cases like dir1::dir2: # where we have a trailing : - command_result.stdout.chomp.split(':', -1) + @content.split(':', -1) end def stderr - command_result.stderr + @command_result.stderr end def exit_status - command_result.exit_status.to_i + @command_result.exit_status.to_i end def to_s - %Q[Environment Variable] + "Environment variable #{field}" end end - -module Serverspec::Type - def os_env(name) - EnvironmentVariable.new(name) - end -end \ No newline at end of file diff --git a/lib/resources/etc_group.rb b/lib/resources/etc_group.rb index 340f826ea..4027f2740 100644 --- a/lib/resources/etc_group.rb +++ b/lib/resources/etc_group.rb @@ -8,24 +8,17 @@ # - gid # - group list, comma seperated list -include Serverspec::Type +class EtcGroup < Vulcano.resource(1) + name 'etc_group' -class EtcGroup < Serverspec::Type::File - - attr_accessor :gid + attr_accessor :gid, :entries + def initialize(path = nil) + @path = path || '/etc/group' + @entries = parse(@path) + end def to_s - %Q[/etc/group] - end - - def parse - content().split("\n").map do |line| - line.split(':') - end - end - - def entries - @entries ||= parse + @path end def groups @@ -61,10 +54,13 @@ class EtcGroup < Serverspec::Type::File self end -end + private -module Serverspec::Type - def etc_group(path = nil) - EtcGroup.new(path || '/etc/group') + def parse(path) + @content = vulcano.file(path).content + @content.split("\n").map do |line| + line.split(':') + end end -end \ No newline at end of file + +end diff --git a/lib/resources/file.rb b/lib/resources/file.rb index 0a4f8fcd0..bd46b9ac5 100644 --- a/lib/resources/file.rb +++ b/lib/resources/file.rb @@ -2,12 +2,68 @@ # copyright: 2015, Vulcano Security GmbH # license: All rights reserved -module Serverspec::Type - class File < Base - # Overwrite the to_s method to show path - # instead of type - def to_s - %Q.Path "#{@name}". +module Vulcano::Resources + class File < Vulcano.resource(1) + name 'file' + + def initialize(path) + @path = path + @file = vulcano.file(@path) end + + %w{ + type exists? file? block_device? character_device? socket? directory? + symlink? pipe? + mode mode? owner owned_by? group grouped_into? link_target linked_to? + content mtime size selinux_label + mounted? immutable? product_version file_version version? + md5sum sha256sum + }.each do |m| + define_method m.to_sym do |*args| + @file.method(m.to_sym).call(*args) + end + end + + def contain(pattern, from, to) + raise ' not yet implemented ' + end + + def readable?(by_owner, by_user) + if by_user.nil? + m = unix_mode_mask(by_owner, 'r') || + raise("#{by_owner} is not a valid unix owner.") + ( @file.mask & m ) != 0 + else + # TODO: REMOVE THIS FALLBACK + Specinfra::Runner.check_file_is_accessible_by_user(@path, by_user, 'r') + end + end + + def writable?(by_owner, by_user) + if by_user.nil? + m = unix_mode_mask(by_owner, 'w') || + raise("#{by_owner} is not a valid unix owner.") + ( @file.mask & m ) != 0 + else + # TODO: REMOVE THIS FALLBACK + Specinfra::Runner.check_file_is_accessible_by_user(@path, by_user, 'w') + end + end + + def executable?(by_owner, by_user) + if by_user.nil? + m = unix_mode_mask(by_owner, 'x') || + raise("#{by_owner} is not a valid unix owner.") + ( @file.mask & m ) != 0 + else + # TODO: REMOVE THIS FALLBACK + Specinfra::Runner.check_file_is_accessible_by_user(@path, by_user, 'x') + end + end + + def to_s + "Path '#{@path}'" + end + end -end \ No newline at end of file +end diff --git a/lib/resources/group_policy.rb b/lib/resources/group_policy.rb index a70fbdaf8..8cc7cf945 100644 --- a/lib/resources/group_policy.rb +++ b/lib/resources/group_policy.rb @@ -4,8 +4,6 @@ require 'json' -include Serverspec::Type - # return JSON object def gpo (policy_path, policy_name) file = ::File.read(::File.join ::File.dirname(__FILE__), "gpo.json") @@ -15,12 +13,13 @@ def gpo (policy_path, policy_name) end # Group Policy -class GroupPolicy < Serverspec::Type::Base +class GroupPolicy < Vulcano.resource(1) + name 'group_policy' def getRegistryValue(entry) keys = entry['registry_information'][0] cmd = "(Get-Item 'Registry::#{keys['path']}').GetValue('#{keys['key']}')" - command_result ||= @runner.run_command(cmd) + command_result ||= vulcano.run_command(cmd) val = { :exit_code => command_result.exit_status.to_i, :data => command_result.stdout } val end @@ -52,9 +51,3 @@ class GroupPolicy < Serverspec::Type::Base end end - -module Serverspec::Type - def group_policy(policy_path) - GroupPolicy.new(policy_path) - end -end \ No newline at end of file diff --git a/lib/resources/inetd_conf.rb b/lib/resources/inetd_conf.rb index eea2a2563..e24613bb9 100644 --- a/lib/resources/inetd_conf.rb +++ b/lib/resources/inetd_conf.rb @@ -4,10 +4,10 @@ require 'utils/simpleconfig' -class InetdConf < Vulcano::Resource +class InetdConf < Vulcano.resource(1) + name 'inetd_config' def initialize(path) - @runner = Specinfra::Runner @conf_path = path @files_contents = {} @content = nil @@ -26,11 +26,12 @@ class InetdConf < Vulcano::Resource def read_content # read the file - if !@runner.check_file_is_file(@conf_path) + file = vulcano.file(@conf_path) + if !file.file? return skip_resource "Can't find file \"#{@conf_path}\"" end - @content = read_file(@conf_path) - if @content.empty? && @runner.get_file_size(@conf_path).stdout.strip.to_i > 0 + @content = file.content + if @content.empty? && file.size > 0 return skip_resource "Can't read file \"#{@conf_path}\"" end # parse the file @@ -42,15 +43,4 @@ class InetdConf < Vulcano::Resource @content end - def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout - end end - -module Serverspec::Type - def inetd_conf(path = nil) - @inetd_conf ||= {} - dpath = path || '/etc/inetd.conf' - @inetd_conf[dpath] = InetdConf.new(dpath) - end -end \ No newline at end of file diff --git a/lib/resources/limits_conf.rb b/lib/resources/limits_conf.rb index a0b6550b8..48701bbd1 100644 --- a/lib/resources/limits_conf.rb +++ b/lib/resources/limits_conf.rb @@ -4,10 +4,10 @@ require 'utils/simpleconfig' -class LimitsConf < Vulcano::Resource +class LimitsConf < Vulcano.resource(1) + name 'limits_conf' def initialize(path) - @runner = Specinfra::Runner @conf_path = path @files_contents = {} @content = nil @@ -26,11 +26,12 @@ class LimitsConf < Vulcano::Resource def read_content # read the file - if !@runner.check_file_is_file(@conf_path) + file = vulcano.file(@conf_path) + if !file.file? return skip_resource "Can't find file \"#{@conf_path}\"" end - @content = read_file(@conf_path) - if @content.empty? && @runner.get_file_size(@conf_path).stdout.strip.to_i > 0 + @content = file.content + if @content.empty? && file.size > 0 return skip_resource "Can't read file \"#{@conf_path}\"" end # parse the file @@ -42,15 +43,4 @@ class LimitsConf < Vulcano::Resource @content end - def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout - end end - -module Serverspec::Type - def limits_conf(path = nil) - @limits_conf ||= {} - dpath = path || '/etc/security/limits.conf' - @limits_conf[dpath] ||= LimitsConf.new(dpath) - end -end \ No newline at end of file diff --git a/lib/resources/login_def.rb b/lib/resources/login_def.rb index 1b730ef86..09e0023eb 100644 --- a/lib/resources/login_def.rb +++ b/lib/resources/login_def.rb @@ -4,10 +4,10 @@ require 'utils/simpleconfig' -class LoginDef < Vulcano::Resource +class LoginDef < Vulcano.resource(1) + name 'login_defs' def initialize(path = nil) - @runner = Specinfra::Runner @conf_path = path || '/etc/login.defs' @files_contents = {} @content = nil @@ -26,11 +26,12 @@ class LoginDef < Vulcano::Resource def read_content # read the file - if !@runner.check_file_is_file(@conf_path) + file = vulcano.file(@conf_path) + if !file.file? return skip_resource "Can't find file \"#{@conf_path}\"" end - @content = read_file(@conf_path) - if @content.empty? && @runner.get_file_size(@conf_path).stdout.strip.to_i > 0 + @content = file.content + if @content.empty? && file.size > 0 return skip_resource "Can't read file \"#{@conf_path}\"" end # parse the file @@ -41,13 +42,4 @@ class LoginDef < Vulcano::Resource @content end - def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout - end end - -module Serverspec::Type - def login_def(path = nil) - LoginDef.new(path) - end -end \ No newline at end of file diff --git a/lib/resources/mysql.rb b/lib/resources/mysql.rb index 8f2d0e56e..dd998ac90 100644 --- a/lib/resources/mysql.rb +++ b/lib/resources/mysql.rb @@ -2,7 +2,9 @@ # copyright: 2015, Vulcano Security GmbH # license: All rights reserved -class Mysql +class Mysql < Vulcano.resource(1) + name 'mysql' + attr_reader :package, :service, :conf_dir, :conf_path, :data_dir, :log_dir, :log_path, :log_group, :log_dir_group def initialize # set OS-dependent filenames and paths @@ -55,9 +57,3 @@ class Mysql end end end - -module Serverspec::Type - def mysql - @mysql ||= Mysql.new() - end -end \ No newline at end of file diff --git a/lib/resources/mysql_conf.rb b/lib/resources/mysql_conf.rb index d6c43e1c0..0fea1e508 100644 --- a/lib/resources/mysql_conf.rb +++ b/lib/resources/mysql_conf.rb @@ -8,16 +8,17 @@ require 'resources/mysql' class MysqlConfEntry def initialize( path, params ) - @runner = Specinfra::Runner @params = params @path = path end + def method_missing name, *args k = name.to_s res = @params[k] return true if res.nil? && @params.key?(k) @params[k] end + def to_s group = ' ' group = "[#{@path.join('][')}] " unless @path.nil? or @path.empty? @@ -25,10 +26,10 @@ class MysqlConfEntry end end -class MysqlConf < Vulcano::Resource +class MysqlConf < Vulcano.resource(1) + name 'mysql_conf' def initialize( conf_path ) - @runner = Specinfra::Runner @conf_path = conf_path @files_contents = {} @content = nil @@ -59,11 +60,11 @@ class MysqlConf < Vulcano::Resource @params = {} # skip if the main configuration file doesn't exist - if !@runner.check_file_is_file(@conf_path) + if !vulcano.file(@conf_path).file? return skip_resource "Can't find file \"#{@conf_path}\"" end raw_conf = read_file(@conf_path) - if raw_conf.empty? && @runner.get_file_size(@conf_path).stdout.strip.to_i > 0 + if raw_conf.empty? && vulcano.file(@conf_path).size > 0 return skip_resource("Can't read file \"#{@conf_path}\"") end @@ -91,14 +92,6 @@ class MysqlConf < Vulcano::Resource end def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout + @files_contents[path] ||= vulcano.file(path).content end end - -module Serverspec::Type - def mysql_conf(path = nil) - @mysql_conf ||= {} - dpath = path || mysql.conf_path - @mysql_conf[dpath] ||= MysqlConf.new( dpath ) - end -end \ No newline at end of file diff --git a/lib/resources/mysql_session.rb b/lib/resources/mysql_session.rb index d1f9871f3..192e892e5 100644 --- a/lib/resources/mysql_session.rb +++ b/lib/resources/mysql_session.rb @@ -4,11 +4,12 @@ $__SCOPE = self -class MysqlSession < Vulcano::Resource +class MysqlSession < Vulcano.resource(1) + name 'mysql_session' + def initialize user, pass @user = user @pass = pass - @runner = Specinfra::Runner initialize_fallback if user.nil? or pass.nil? skip_resource("Can't run MySQL SQL checks without authentication") if @user.nil? or @pass.nil? end @@ -18,7 +19,7 @@ class MysqlSession < Vulcano::Resource # that does this securely escaped_query = query.gsub(/\\/, '\\\\').gsub(/"/,'\\"').gsub(/\$/,'\\$') # run the query - cmd = Serverspec::Type::Command.new("mysql -u#{@user} -p#{@pass} #{db} -s -e \"#{escaped_query}\"") + cmd = vulcano.run_command("mysql -u#{@user} -p#{@pass} #{db} -s -e \"#{escaped_query}\"") out = cmd.stdout + "\n" + cmd.stderr if out =~ /Can't connect to .* MySQL server/ or out.downcase =~ /^error/ @@ -33,7 +34,7 @@ class MysqlSession < Vulcano::Resource def initialize_fallback # support debian mysql administration login - debian = @runner.run_command("test -f /etc/mysql/debian.cnf && cat /etc/mysql/debian.cnf").stdout + debian = vulcano.run_command("test -f /etc/mysql/debian.cnf && cat /etc/mysql/debian.cnf").stdout unless debian.empty? user = debian.match(/^\s*user\s*=\s*([^ ]*)\s*$/) pass = debian.match(/^\s*password\s*=\s*([^ ]*)\s*$/) @@ -44,9 +45,3 @@ class MysqlSession < Vulcano::Resource end end end - -module Serverspec::Type - def mysql_session( user=nil, password=nil ) - MysqlSession.new(user, password) - end -end diff --git a/lib/resources/ntp_conf.rb b/lib/resources/ntp_conf.rb index a8f1b6f16..ea3f0a8b8 100644 --- a/lib/resources/ntp_conf.rb +++ b/lib/resources/ntp_conf.rb @@ -4,10 +4,10 @@ require 'utils/simpleconfig' -class NtpConf < Vulcano::Resource +class NtpConf < Vulcano.resource(1) + name 'ntp_conf' def initialize(path = nil) - @runner = Specinfra::Runner @conf_path = path || '/etc/ntp.conf' @files_contents = {} @content = nil @@ -26,11 +26,11 @@ class NtpConf < Vulcano::Resource def read_content # read the file - if !@runner.check_file_is_file(@conf_path) + if !vulcano.file(@conf_path).file? return skip_resource "Can't find file \"#{@conf_path}\"" end - @content = read_file(@conf_path) - if @content.empty? && @runner.get_file_size(@conf_path).stdout.strip.to_i > 0 + @content = vulcano.file(@conf_path).content + if @content.empty? && vulcano.file(@conf_path).size > 0 return skip_resource "Can't read file \"#{@conf_path}\"" end # parse the file @@ -41,13 +41,4 @@ class NtpConf < Vulcano::Resource @content end - def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout - end end - -module Serverspec::Type - def ntp_conf(path = nil) - NtpConf.new(path) - end -end \ No newline at end of file diff --git a/lib/resources/parse_config.rb b/lib/resources/parse_config.rb index 20d8fa121..45ee28087 100644 --- a/lib/resources/parse_config.rb +++ b/lib/resources/parse_config.rb @@ -11,13 +11,13 @@ # } # describe parse_config(audit, options ) do -class PConfig < Vulcano::Resource +class PConfig < Vulcano.resource(1) + name 'parse_config' def initialize ( content=nil, useropts = {} ) default_options = {} @opts = default_options.merge(useropts) - @runner = Specinfra::Runner @content = content @files_contents = {} @params = nil @@ -40,11 +40,11 @@ class PConfig < Vulcano::Resource @conf_path = conf_path # read the file - if !@runner.check_file_is_file(conf_path) + if !vulcano.file(conf_path).file? return skip_resource "Can't find file \"#{conf_path}\"" end @content = read_file(conf_path) - if @content.empty? && @runner.get_file_size(conf_path).stdout.strip.to_i > 0 + if @content.empty? && vulcano.file(conf_path).size > 0 return skip_resource "Can't read file \"#{conf_path}\"" end @@ -52,7 +52,7 @@ class PConfig < Vulcano::Resource end def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout + @files_contents[path] ||= vulcano.file(path).content end def read_content @@ -62,14 +62,11 @@ class PConfig < Vulcano::Resource end end -module Serverspec::Type - def parse_config(content, opts={}) - PConfig.new(content, opts) - end +class PConfigFile < PConfig + name 'parse_config_file' - def parse_config_file(file, opts={}) - p = PConfig.new(nil, opts) - p.parse_file(file) - p + def initialize(path, opts) + super(nil, opts) + parse_file(path) end end diff --git a/lib/resources/passwd.rb b/lib/resources/passwd.rb index f33158642..cd0c64a61 100644 --- a/lib/resources/passwd.rb +++ b/lib/resources/passwd.rb @@ -11,20 +11,24 @@ # - home directory # - command -include Serverspec::Type - -class Passwd < Serverspec::Type::File +class Passwd < Vulcano.resource(1) + name 'passwd' attr_accessor :uid + def initialize(path = nil, uid: nil) + @path = path || '/etc/passwd' + @content = vulcano.file(@path).content + @parsed = parse(@content) + end + def to_s - %Q[/etc/passwd] + @path end def determine_uid () - parsed = parse() uids = Array.new - parsed.each {|x| + @parsed.each {|x| if ( x.at(2) == "#{@uid}") then uids.push(x.at(0)) end @@ -43,8 +47,7 @@ class Passwd < Serverspec::Type::File end def map_data (id) - parsed = parse() - parsed.map {|x| + @parsed.map {|x| x.at(id) } end @@ -66,8 +69,7 @@ class Passwd < Serverspec::Type::File end def users - parsed = parse() - parsed.map {|x| + @parsed.map {|x| { "name" => x.at(0), "password" => x.at(1), @@ -80,20 +82,12 @@ class Passwd < Serverspec::Type::File } end - def parse - entries = Array.new - content().split("\n").each do |line| - entries.push(line.split(':')) + private + + def parse(content) + content.split("\n").map do |line| + line.split(':') end - entries end end - -module Serverspec::Type - def passwd(uid=nil) - i = Passwd.new('/etc/passwd') - i.uid = uid - i - end -end \ No newline at end of file diff --git a/lib/resources/postgres.rb b/lib/resources/postgres.rb index d41a552de..79bf18bc9 100644 --- a/lib/resources/postgres.rb +++ b/lib/resources/postgres.rb @@ -2,14 +2,16 @@ # copyright: 2015, Vulcano Security GmbH # license: All rights reserved -class Postgres +class Postgres < Vulcano.resource(1) + name 'postgres' + attr_reader :service, :data_dir, :conf_dir, :conf_path def initialize case os[:family] when 'ubuntu', 'debian' @service = 'postgresql' @data_dir = '/var/lib/postgresql' - @version = command('ls /etc/postgresql/').stdout.chomp + @version = vulcano.run_command('ls /etc/postgresql/').stdout.chomp @conf_dir = "/etc/postgresql/#{@version}/main" @conf_path = File.join @conf_dir, 'postgresql.conf' @@ -27,9 +29,3 @@ class Postgres end end end - -module Serverspec::Type - def postgres - @postgres ||= Postgres.new() - end -end \ No newline at end of file diff --git a/lib/resources/postgres_conf.rb b/lib/resources/postgres_conf.rb index 6cc6ea223..16097f2dd 100644 --- a/lib/resources/postgres_conf.rb +++ b/lib/resources/postgres_conf.rb @@ -6,10 +6,10 @@ require 'utils/simpleconfig' require 'utils/find_files' require 'resources/postgres' -class PostgresConf +class PostgresConf < Vulcano.resource(1) + name 'postgres_conf' def initialize( conf_path ) - @runner = Specinfra::Runner @conf_path = conf_path @conf_dir = File.expand_path(File.dirname @conf_path) @files_contents = {} @@ -36,11 +36,11 @@ class PostgresConf @params = {} # skip if the main configuration file doesn't exist - if !@runner.check_file_is_file(@conf_path) + if !vulcano.file(@conf_path).file? return skip_resource "Can't find file \"#{@conf_path}\"" end raw_conf = read_file(@conf_path) - if raw_conf.empty? && @runner.get_file_size(@conf_path).stdout.strip.to_i > 0 + if raw_conf.empty? && vulcano.file(@conf_path).size > 0 return skip_resource("Can't read file \"#{@conf_path}\"") end @@ -70,15 +70,6 @@ class PostgresConf end def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout + @files_contents[path] ||= vulcano.file(path).content end end - - -module Serverspec::Type - def postgres_conf(path = nil) - @postgres_conf ||= {} - dpath = path || postgres.conf_path - @postgres_conf[dpath] ||= PostgresConf.new( dpath ) - end -end \ No newline at end of file diff --git a/lib/resources/postgres_session.rb b/lib/resources/postgres_session.rb index 8c3b59a8a..85598489a 100644 --- a/lib/resources/postgres_session.rb +++ b/lib/resources/postgres_session.rb @@ -2,25 +2,22 @@ # copyright: 2015, Vulcano Security GmbH # license: All rights reserved -module Serverspec end -module Serverspec::Type - class Lines - def initialize raw, desc - @raw = raw - @desc = desc - end +class Lines + def initialize raw, desc + @raw = raw + @desc = desc + end - def output - @raw - end + def output + @raw + end - def lines - @raw.split("\n") - end + def lines + @raw.split("\n") + end - def to_s - @desc - end + def to_s + @desc end end @@ -36,7 +33,7 @@ class PostgresSession # that does this securely escaped_query = query.gsub(/\\/, '\\\\').gsub(/"/,'\\"').gsub(/\$/,'\\$') # run the query - cmd = Serverspec::Type::Command.new("PGPASSWORD='#{@pass}' psql -U #{@user} #{dbs} -c \"#{escaped_query}\"") + cmd = vulcano.run_command("PGPASSWORD='#{@pass}' psql -U #{@user} #{dbs} -c \"#{escaped_query}\"") out = cmd.stdout + "\n" + cmd.stderr if out =~ /could not connect to .*/ or out.downcase =~ /^error/ @@ -51,15 +48,9 @@ class PostgresSession sub(/(.*\n)+([-]+[+])*[-]+\n/,''). # remove the tail sub(/\n[^\n]*\n\n$/,'') - l = Serverspec::Type::Lines.new(lines.strip, "PostgreSQL query: #{query}") + l = Lines.new(lines.strip, "PostgreSQL query: #{query}") RSpec.__send__( 'describe', l, &block ) end end end - -module Serverspec::Type - def postgres_session( user, password ) - PostgresSession.new(user, password) - end -end \ No newline at end of file diff --git a/lib/resources/processes.rb b/lib/resources/processes.rb index 380b6d770..8e265573c 100644 --- a/lib/resources/processes.rb +++ b/lib/resources/processes.rb @@ -2,17 +2,19 @@ # copyright: 2015, Vulcano Security GmbH # license: All rights reserved -include Serverspec::Type +class Processes < Vulcano.resource(1) + name 'processes' -class Processes < Serverspec::Type::Base - def initialize grep + attr_reader :list + def initialize(grep) # turn into a regexp if it isn't one yet if grep.class == String grep = '(/[^/]*)*'+grep if grep[0] != '/' grep = Regexp.new('^'+grep+'(\s|$)') end + # get all running processes - cmd = Serverspec::Type::Command.new('ps aux') + cmd = vulcano.run_command('ps aux') all = cmd.stdout.split("\n")[1..-1] all_cmds = all.map do |line| # user 32296 0.0 0.0 42592 7972 pts/15 Ss+ Apr06 0:00 zsh @@ -37,10 +39,5 @@ class Processes < Serverspec::Type::Base hm[:command] =~ grep end end -end -module Serverspec::Type - def processes( grep ) - Processes.new(grep) - end -end \ No newline at end of file +end diff --git a/lib/resources/registry_key.rb b/lib/resources/registry_key.rb index a9ac2aa82..6c64af89f 100644 --- a/lib/resources/registry_key.rb +++ b/lib/resources/registry_key.rb @@ -2,14 +2,20 @@ # copyright: 2015, Vulcano Security GmbH # license: All rights reserved -include Serverspec::Type require 'json' -# Registry Key Helper -class RegistryKey < Serverspec::Type::Base +class RegistryKey < Vulcano.resource(1) + name 'registry_key' attr_accessor :reg_key + def initialize(name, reg_key = nil) + # if we have one parameter, we use it as name + reg_key = name if reg_key == nil + @name = name + @reg_key = reg_key + end + def getRegistryValue(path, key) cmd = "(Get-Item 'Registry::#{path}').GetValue('#{key}')" command_result ||= @runner.run_command(cmd) @@ -42,17 +48,3 @@ class RegistryKey < Serverspec::Type::Base end end - -module Serverspec::Type - def registry_key(name, reg_key=nil) - # if we have one parameter, we use it as name - if reg_key == nil - reg_key = name - end - - # initialize variable - i = RegistryKey.new(name) - i.reg_key = reg_key - i - end -end \ No newline at end of file diff --git a/lib/resources/resources.rb b/lib/resources/resources.rb deleted file mode 100644 index a2f5a2dfc..000000000 --- a/lib/resources/resources.rb +++ /dev/null @@ -1,40 +0,0 @@ -# encoding: utf-8 -# copyright: 2015, Vulcano Security GmbH -# license: All rights reserved - -require 'resources/apache_conf' -require 'resources/audit_policy' -require 'resources/auditd_conf' -require 'resources/auditd_rules' -require 'resources/command' -require 'resources/env' -require 'resources/etc_group' -require 'resources/file' -require 'resources/group_policy' -require 'resources/inetd_conf' -require 'resources/limits_conf' -require 'resources/login_def' -require 'resources/mysql' -require 'resources/mysql_conf' -require 'resources/mysql_session' -require 'resources/ntp_conf' -require 'resources/parse_config' -require 'resources/passwd' -require 'resources/postgres' -require 'resources/postgres_conf' -require 'resources/postgres_session' -require 'resources/processes' -require 'resources/registry_key' -require 'resources/security_policy' -require 'resources/ssh_conf' - -# extend serverspec types -module Serverspec - module Type - - def directory(name) - Directory.new(name) - end - - end -end diff --git a/lib/resources/security_policy.rb b/lib/resources/security_policy.rb index e7cec88a7..8de52cb61 100644 --- a/lib/resources/security_policy.rb +++ b/lib/resources/security_policy.rb @@ -4,14 +4,13 @@ # secedit /export /cfg secpol.cfg # # @link http://www.microsoft.com/en-us/download/details.aspx?id=25250 -# +# # In Windows, some security options are managed differently that the local GPO -# All local GPO parameters can be examined via Registry, but not all security +# All local GPO parameters can be examined via Registry, but not all security # parameters. Therefore we need a combination of Registry and secedit output -include Serverspec::Type - -class SecurityPolicy < Serverspec::Type::Base +class SecurityPolicy < Vulcano.resource(1) + name 'security_policy' # static variable, shared across all instances @@loaded = false @@ -46,7 +45,7 @@ class SecurityPolicy < Serverspec::Type::Base # find line with key key = method.to_s target = "" - @@policy.each_line {|s| + @@policy.each_line {|s| target = s.strip if s.match(/\b#{key}\s*=\s*(.*)\b/) } @@ -57,9 +56,9 @@ class SecurityPolicy < Serverspec::Type::Base val = result[:value] val = val.to_i if val.match(/^\d+$/) else - # TODO we may need to return skip or failure if the + # TODO we may need to return skip or failure if the # requested value is not available - val = nil + val = nil end val @@ -70,9 +69,3 @@ class SecurityPolicy < Serverspec::Type::Base end end - -module Serverspec::Type - def security_policy() - SecurityPolicy.new() - end -end \ No newline at end of file diff --git a/lib/resources/ssh_conf.rb b/lib/resources/ssh_conf.rb index 87d606165..bc1214eff 100644 --- a/lib/resources/ssh_conf.rb +++ b/lib/resources/ssh_conf.rb @@ -4,16 +4,12 @@ require 'utils/simpleconfig' -class SshConf < Vulcano::Resource +class SshConf < Vulcano.resource(1) + name 'ssh_config' - def initialize( conf_path, type = nil ) - @runner = Specinfra::Runner - @conf_path = conf_path - @conf_dir = File.expand_path(File.dirname @conf_path) - @files_contents = {} - @content = nil - @params = nil - typename = ( conf_path.include?('sshd') ? 'Server' : 'Client' ) + def initialize( conf_path = nil, type = nil ) + @conf_path = conf_path || '/etc/ssh/ssh_config' + typename = ( @conf_path.include?('sshd') ? 'Server' : 'Client' ) @type = type || "SSH #{typename} configuration #{conf_path}" read_content end @@ -23,11 +19,10 @@ class SshConf < Vulcano::Resource end def content - @content ||= read_content + @conf.content end def params *opts - @params || read_content res = @params opts.each do |opt| res = res[opt] unless res.nil? @@ -36,40 +31,35 @@ class SshConf < Vulcano::Resource end def method_missing name - @params || read_content @params[name.to_s] end + private + def read_content + @conf = vulcano.file(@conf_path) # read the file - if !@runner.check_file_is_file(@conf_path) + if !@conf.file? return skip_resource "Can't find file \"#{@conf_path}\"" end - @content = read_file(@conf_path) - if @content.empty? && @runner.get_file_size(@conf_path).stdout.strip.to_i > 0 + + if @conf.content.empty? && @conf.size > 0 return skip_resource "Can't read file \"#{@conf_path}\"" end + # parse the file - @params = SimpleConfig.new(@content, + @params = SimpleConfig.new(@conf.content, assignment_re: /^\s*(\S+?)\s+(.*?)\s*$/, multiple_values: false ).params - @content end - def read_file(path) - @files_contents[path] ||= @runner.get_file_content(path).stdout - end end -module Serverspec::Type - def ssh_config( path = nil ) - @ssh_config ||= {} - dpath = path || '/etc/ssh/ssh_config' - @ssh_config[dpath] ||= SshConf.new(dpath) - end +class SshdConf < SshConf + name 'sshd_config' - def sshd_config( path = nil ) - ssh_config( path || '/etc/ssh/sshd_config' ) + def initialize(path = nil) + super(path || '/etc/ssh/sshd_config') end end diff --git a/lib/utils/detect.rb b/lib/utils/detect.rb index 31f9497a1..22e6ecb48 100644 --- a/lib/utils/detect.rb +++ b/lib/utils/detect.rb @@ -45,7 +45,7 @@ if os[:family] == 'windows' release = versions[version] end -# hijack os-detection from serverspec +# print OS detection infos puts JSON.dump({ os_family: os[:family], os_release: release || os[:release], diff --git a/lib/verify/dummy.rb b/lib/verify/dummy.rb index 4103f2a00..cc28acd7a 100644 --- a/lib/verify/dummy.rb +++ b/lib/verify/dummy.rb @@ -1,19 +1,8 @@ # Copyright 2014 Dominik Richter. All rights reserved. # Spec file for Vulcano specs -module Serverspec -end - # Get types -module DummyServerspecTypes - sgem = Gem::Specification.find_by_name("serverspec") - types = Dir[File.join sgem.gem_dir, 'lib', 'serverspec', 'type', '*']. - map{|x| File.basename(x).sub(/\.rb$/,'')} - types.each do |name| - define_method name do |*arg| - end - end - +module DummyTestTypes # a few commands with special handling def describe *args; end def context *args; end diff --git a/lib/vulcano.rb b/lib/vulcano.rb index c3898d9e3..8d96c9d5b 100644 --- a/lib/vulcano.rb +++ b/lib/vulcano.rb @@ -9,12 +9,12 @@ libdir = File.dirname(__FILE__) $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) require 'vulcano/version' +require 'vulcano/backend' require 'vulcano/resource' require 'vulcano/rspec_json_formatter' require 'vulcano/rule' require 'vulcano/runner' -require 'resources/resources' require 'matchers/matchers' # Dummy module for handling additional attributes diff --git a/lib/vulcano/backend.rb b/lib/vulcano/backend.rb index 6f1e062be..6e119fc66 100644 --- a/lib/vulcano/backend.rb +++ b/lib/vulcano/backend.rb @@ -1,6 +1,41 @@ # encoding: utf-8 -require 'vulcano/backend/core' -require 'vulcano/backend/docker' -require 'vulcano/backend/exec' -require 'vulcano/backend/ssh' -require 'vulcano/backend/winrm' +require 'uri' +require 'vulcano/plugins' + +module Vulcano + class Backend + # Expose all registered backends + def self.registry + @registry ||= {} + end + + # Resolve target configuration in URI-scheme into + # all respective fields and merge with existing configuration. + # e.g. ssh://bob@remote => backend: ssh, user: bob, host: remote + def self.target_config( config ) + conf = config.dup + + return conf if conf['target'].to_s.empty? + + uri = URI::parse(conf['target'].to_s) + conf['backend'] = conf['backend'] || uri.scheme + conf['host'] = conf['host'] || uri.host + conf['port'] = conf['port'] || uri.port + conf['user'] = conf['user'] || uri.user + conf['password'] = conf['password'] || uri.password + + # return the updated config + conf + end + end + + def self.backend(version = 1) + if version != 1 + raise "Only backend version 1 is supported!" + end + Vulcano::Plugins::Backend + end +end + +require 'vulcano/backend/mock' +require 'vulcano/backend/specinfra' diff --git a/lib/vulcano/backend/core.rb b/lib/vulcano/backend/core.rb deleted file mode 100644 index b3a8f2ffd..000000000 --- a/lib/vulcano/backend/core.rb +++ /dev/null @@ -1,42 +0,0 @@ -# encoding: utf-8 -require 'utils/modulator' - -module Vulcano - class Backend - extend Modulator - - def initialize(conf) - @conf = conf - end - - def resolve_target_options - return if @conf[:target].to_s.empty? - uri = URI::parse(@conf[:target].to_s) - @conf[:backend] = @conf[:backend] || uri.scheme - @conf[:host] = @conf[:host] || uri.host - @conf[:port] = @conf[:port] || uri.port - @conf[:user] = @conf[:user] || uri.user - @conf[:password] = @conf[:password] || uri.password - end - - def configure_shared_options - Specinfra::Backend::Cmd.send(:include, Specinfra::Helper::Set) - si = Specinfra.configuration - si.os = nil - if @conf['disable_sudo'] - si.disable_sudo = true - else - si.sudo_password = @conf['sudo_password'] - si.sudo_options = @conf['sudo_options'] - end - end - - def configure_target - t = @conf[:backend] || 'exec' - m = Vulcano::Backend.modules[t] - raise "Don't understand backend '#{t}'" if m.nil? - m.configure(@conf) - end - - end -end diff --git a/lib/vulcano/backend/docker.rb b/lib/vulcano/backend/docker.rb deleted file mode 100644 index 741e551ce..000000000 --- a/lib/vulcano/backend/docker.rb +++ /dev/null @@ -1,13 +0,0 @@ -# encoding: utf-8 - -module Vulcano::Backend::Docker - - def self.configure(conf) - host = conf['host'].to_s - Specinfra.configuration.backend = :docker - Specinfra.configuration.docker_container = host - end - -end - -Vulcano::Backend.add_module('docker', Vulcano::Backend::Docker) diff --git a/lib/vulcano/backend/exec.rb b/lib/vulcano/backend/exec.rb deleted file mode 100644 index 0d5fd40d1..000000000 --- a/lib/vulcano/backend/exec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# encoding: utf-8 - -module Vulcano::Backend::Exec - - def self.configure(conf) - Specinfra.configuration.backend = :exec - end - -end - -Vulcano::Backend.add_module('exec', Vulcano::Backend::Exec) diff --git a/lib/vulcano/backend/mock.rb b/lib/vulcano/backend/mock.rb new file mode 100644 index 000000000..d90014d42 --- /dev/null +++ b/lib/vulcano/backend/mock.rb @@ -0,0 +1,75 @@ +# encoding: utf-8 + +module Vulcano::Backends + class Mock < Vulcano.backend(1) + name 'mock' + + def initialize( conf ) + @conf = conf + @files = {} + end + + def file(path) + puts "--> get file #{path}" + @files[path] ||= File.new(self, path) + end + + def run_command(cmd) + Command.new(self, cmd) + end + + def to_s + 'Mock Backend Runner' + end + end + + class Mock + class File + + def initialize(runtime, path) + @path = path + @exists = (rand < 0.8) ? true : false + @is_file = (@exists && rand < 0.7) ? true : false + @size = 0 + @content = '' + if @exists && @is_file + @size = ( rand ** 3 * 1000 ).to_i + @size = 0 if rand < 0.2 + end + if @size > 0 + @content = (0...50).map { ('a'..'z').to_a[rand(26)] }.join + end + end + + def size + puts "----> get file #{@path} size: #{@size}" + @size + end + + def content + puts "----> get file #{@path} content: #{@content}" + @content + end + + def file? + puts "----> is file #{@path} a file? #{@is_file}" + @is_file + end + + def exists? + puts "----> does file #{@path} exist? #{@exists}" + @exists + end + end + + class Command + attr_reader :stdout, :stderr, :exit_status + def initialize(runtime, cmd) + @exit_code = (rand < 0.7) ? 0 : (100 * rand).to_i + @stdout = (0...50).map { ('a'..'z').to_a[rand(26)] }.join + @stderr = (0...50).map { ('a'..'z').to_a[rand(26)] }.join + end + end + + end +end diff --git a/lib/vulcano/backend/specinfra.rb b/lib/vulcano/backend/specinfra.rb new file mode 100644 index 000000000..0b993858d --- /dev/null +++ b/lib/vulcano/backend/specinfra.rb @@ -0,0 +1,250 @@ +# encoding: utf-8 +require 'shellwords' + +module Vulcano::Backends + + class SpecinfraHelper < Vulcano.backend(1) + name 'specinfra' + + def initialize(conf) + @conf = conf + @files = {} + type = @conf['backend'].to_s + + reset_backend(type) + configure_shared_options + + # configure the given backend, if we can handle it + # e.g. backend = exec ==> try to call configure_exec + # if we don't support it, error out + m = "configure_#{type}" + if self.respond_to?(m.to_sym) + self.send(m) + else + raise "Cannot configure Specinfra backend #{type}: it isn't supported yet." + end + end + + def file(path) + @files[path] ||= File.new(path) + end + + def run_command(cmd) + Specinfra::Runner.run_command(cmd) + end + + def to_s + 'SpecInfra Backend Runner' + end + + def reset_backend(type) + # may be less nice, but avoid eval... + case type + when 'exec' + Specinfra::Backend::Exec.instance_variable_set(:@instance, nil) + when 'docker' + Specinfra::Backend::Docker.instance_variable_set(:@instance, nil) + when 'ssh' + Specinfra::Backend::Ssh.instance_variable_set(:@instance, nil) + when 'winrm' + Specinfra::Backend::Winrm.instance_variable_set(:@instance, nil) + end + end + + def configure_shared_options + Specinfra::Backend::Cmd.send(:include, Specinfra::Helper::Set) + si = Specinfra.configuration + si.os = nil + if @conf['disable_sudo'] + si.disable_sudo = true + else + si.sudo_password = @conf['sudo_password'] + si.sudo_options = @conf['sudo_options'] + end + end + + def configure_docker + host = @conf['host'].to_s + Specinfra.configuration.backend = :docker + Specinfra.configuration.docker_container = host + end + + def configure_exec + Specinfra.configuration.backend = :exec + end + + def configure_ssh + si = Specinfra.configuration + si.backend = :ssh + si.request_pty = true + + host = @conf['host'].to_s + RSpec.configuration.host = host + + ssh_opts = { + port: @conf['port'] || 22, + auth_methods: ['none'], + user_known_hosts_file: "/dev/null", + global_known_hosts_file: "/dev/null", + number_of_password_prompts: 0, + user: @conf['user'], + password: @conf['password'], + keys: [@conf['key_file']].compact, + } + + if host.empty? + raise "You must configure a target host." + end + unless ssh_opts[:port] > 0 + raise "Port must be > 0 (not #{ssh_opts[:port]})" + end + if ssh_opts[:user].to_s.empty? + raise "User must not be empty." + end + unless ssh_opts[:keys].empty? + ssh_opts[:auth_methods].push('publickey') + ssh_opts[:keys_only] = true if ssh_opts[:password].nil? + end + unless ssh_opts[:password].nil? + ssh_opts[:auth_methods].push('password') + end + if ssh_opts[:keys].empty? and ssh_opts[:password].nil? + raise "You must configure at least one authentication method" + + ": Password or key." + end + + si.ssh_options = ssh_opts + + end + + def configure_winrm + si = Specinfra.configuration + si.backend = :winrm + si.os = { family: 'windows'} + + # common options + host = conf['host'].to_s + port = conf['port'] + user = conf['user'].to_s + pass = conf['password'].to_s + + # SSL configuration + if conf['winrm_ssl'] + scheme = 'https' + port = port || 5986 + else + scheme = 'http' + port = port || 5985 + end + + # validation + if host.empty? + raise "You must configure a target host." + end + unless port > 0 + raise "Port must be > 0 (not #{port})" + end + if user.empty? + raise "You must configure a WinRM user for login." + end + if pass.empty? + raise "You must configure a WinRM password." + end + + # create the connection + endpoint = "#{scheme}://#{host}:#{port}/wsman" + winrm = ::WinRM::WinRMWebService.new( + endpoint, + :ssl, + user: user, + pass: pass, + basic_auth_only: true, + no_ssl_peer_verification: conf['winrm_self_signed'], + ) + si.winrm = winrm + end + + end + + class SpecinfraHelper + + class File < FileCommon + TYPES = { + socket: 00140000, + symlink: 00120000, + file: 00100000, + block_device: 00060000, + directory: 00040000, + character_device: 00020000, + pipe: 00010000, + } + def initialize(path) + @path = path + end + + def type + path = Shellwords.escape(@path) + raw_type = Specinfra::Runner.run_command("stat -c %f #{path}").stdout + tmask = raw_type.to_i(16) + res = TYPES.find{|x, mask| mask & tmask == mask} + return :unknown if res.nil? + res[0] + end + + def exists? + Specinfra::Runner.check_file_exists(@path) + end + + def mode + Specinfra::Runner.get_file_mode(@path).stdout.to_i(8) + end + + def owner + Specinfra::Runner.get_file_owner_user(@path).stdout.strip + end + + def group + Specinfra::Runner.get_file_owner_group(@path).stdout.strip + end + + def link_target + path = Shellwords.escape(@path) + Specinfra::Runner.run_command("readlink #{path}").stdout.strip + end + + def content + Specinfra::Runner.get_file_content(@path).stdout + end + + def mtime + Specinfra::Runner.get_file_mtime(@path).stdout.strip + end + + def size + Specinfra::Runner.get_file_size(@path).stdout.strip.to_i + end + + def selinux_label + Specinfra::Runner.get_file_selinuxlabel(@path).stdout.strip + end + + def mounted?(opts = {}, only_with = nil) + Specinfra::Runner.check_file_is_mounted(@path, opts, only_with) + end + + def immutable? + Specinfra::Runner.get_file_immutable(@path) + end + + def product_version + Specinfra::Runner.run_command("(Get-Command '#{@path}').FileVersionInfo.ProductVersion").stdout.strip + end + + def file_version + Specinfra::Runner.run_command("(Get-Command '#{@path}').FileVersionInfo.FileVersion").stdout.strip + end + + end + + end +end diff --git a/lib/vulcano/backend/ssh.rb b/lib/vulcano/backend/ssh.rb deleted file mode 100644 index 0e484eff2..000000000 --- a/lib/vulcano/backend/ssh.rb +++ /dev/null @@ -1,51 +0,0 @@ -# encoding: utf-8 - -module Vulcano::Backend::SSH - - def self.configure(conf) - si = Specinfra.configuration - si.backend = :ssh - si.request_pty = true - - host = conf['host'].to_s - RSpec.configuration.host = host - - ssh_opts = { - port: conf['port'] || 22, - auth_methods: ['none'], - user_known_hosts_file: "/dev/null", - global_known_hosts_file: "/dev/null", - number_of_password_prompts: 0, - user: conf['user'], - password: conf['password'], - keys: [conf['key_file']].compact, - } - - if host.empty? - raise "You must configure a target host." - end - unless ssh_opts[:port] > 0 - raise "Port must be > 0 (not #{ssh_opts[:port]})" - end - if ssh_opts[:user].to_s.empty? - raise "User must not be empty." - end - unless ssh_opts[:keys].empty? - ssh_opts[:auth_methods].push('publickey') - ssh_opts[:keys_only] = true if ssh_opts[:password].nil? - end - unless ssh_opts[:password].nil? - ssh_opts[:auth_methods].push('password') - end - if ssh_opts[:keys].empty? and ssh_opts[:password].nil? - raise "You must configure at least one authentication method" + - ": Password or key." - end - - si.ssh_options = ssh_opts - - end - -end - -Vulcano::Backend.add_module('ssh', Vulcano::Backend::SSH) diff --git a/lib/vulcano/backend/winrm.rb b/lib/vulcano/backend/winrm.rb deleted file mode 100644 index 05f2df09c..000000000 --- a/lib/vulcano/backend/winrm.rb +++ /dev/null @@ -1,55 +0,0 @@ -# encoding: utf-8 -require 'winrm' - -module Vulcano::Backend::WinRM - - def self.configure(conf) - si = Specinfra.configuration - si.backend = :winrm - si.os = { family: 'windows'} - - # common options - host = conf['host'].to_s - port = conf['port'] - user = conf['user'].to_s - pass = conf['password'].to_s - - # SSL configuration - if conf['winrm_ssl'] - scheme = 'https' - port = port || 5986 - else - scheme = 'http' - port = port || 5985 - end - - # validation - if host.empty? - raise "You must configure a target host." - end - unless port > 0 - raise "Port must be > 0 (not #{port})" - end - if user.empty? - raise "You must configure a WinRM user for login." - end - if pass.empty? - raise "You must configure a WinRM password." - end - - # create the connection - endpoint = "#{scheme}://#{host}:#{port}/wsman" - winrm = ::WinRM::WinRMWebService.new( - endpoint, - :ssl, - user: user, - pass: pass, - basic_auth_only: true, - no_ssl_peer_verification: conf['winrm_self_signed'], - ) - si.winrm = winrm - end - -end - -Vulcano::Backend.add_module('winrm', Vulcano::Backend::WinRM) diff --git a/lib/vulcano/plugins.rb b/lib/vulcano/plugins.rb new file mode 100644 index 000000000..6bf8793e4 --- /dev/null +++ b/lib/vulcano/plugins.rb @@ -0,0 +1,8 @@ +# encoding: utf-8 + +module Vulcano + module Plugins + autoload :Resource, 'vulcano/plugins/resource' + autoload :Backend, 'vulcano/plugins/backend' + end +end diff --git a/lib/vulcano/plugins/backend.rb b/lib/vulcano/plugins/backend.rb new file mode 100644 index 000000000..4ff2a0dea --- /dev/null +++ b/lib/vulcano/plugins/backend.rb @@ -0,0 +1,130 @@ +# encoding: utf-8 + +require 'digest' + +module Vulcano::Plugins + + class Backend + def self.name( name ) + Vulcano::Plugins::Backend.__register(name, self) + end + + def self.__register(id, obj) + # raise errors for all missing methods + %w{ file run_command os }.each do |m| + next if obj.public_method_defined?(m.to_sym) + obj.send(:define_method, m.to_sym) do |*args| + raise NotImplementedError.new("Backend must implement the #{m}() method.") + end + end + + Vulcano::Backend.registry[id] = obj + end + + class FileCommon + # interface methods: these fields should be implemented by every + # backend File + %w{ + exists? mode owner group link_target content mtime size + selinux_label product_version file_version + } + + def type + :unknown + end + + # The following methods can be overwritten by a derived class + # if desired, to e.g. achieve optimizations. + + def md5sum + res = Digest::MD5.new + res.update(content) + res.hexdigest + end + + def sha256sum + res = Digest::SHA256.new + res.update(content) + res.hexdigest + end + + # Additional methods for convenience + + def file? + type == :file + end + + def block_device? + type == :block_device + end + + def character_device? + type == :character_device + end + + def socket? + type == :socket + end + + def directory? + type == :directory + end + + def symlink? + type == :symlink + end + + def pipe? + type == :pipe? + end + + def mode?(mode) + mode == mode + end + + def owned_by?(owner) + owner == owner + end + + def grouped_into?(group) + group == group + end + + def linked_to?(dst) + link_target == dst + end + + def version?(version) + product_version == version or + file_version == version + end + + # helper methods provided to any implementing class + private + + UNIX_MODE_OWNERS = { + owner: 00700, + group: 00070, + other: 00007, + } + + UNIX_MODE_TYPES = { + r: 00444, + w: 00222, + x: 00111, + } + + def unix_mode_mask(owner, type) + o = UNIX_MODE_OWNERS[owner.to_sym] + return nil if o.nil? + + t = UNIX_MODE_TYPES[type.to_sym] + return nil if t.nil? + + t & o + end + + end + + end +end diff --git a/lib/vulcano/plugins/resource.rb b/lib/vulcano/plugins/resource.rb new file mode 100644 index 000000000..c1ca6bdeb --- /dev/null +++ b/lib/vulcano/plugins/resource.rb @@ -0,0 +1,39 @@ +# encoding: utf-8 + +module Vulcano + module Plugins + + class Resource + def self.name( name ) + Vulcano::Plugins::Resource.__register(name, self) + end + + def self.__register(name, obj) + cl = Class.new(obj) do + # add some common methods + include Vulcano::Plugins::ResourceCommon + def initialize(backend, *args) + # attach the backend to this instance + self.class.send(:define_method, :vulcano){backend} + # call the resource initializer + super(*args) + end + end + + # add the resource to the registry by name + Vulcano::Resource.registry[name] = cl + end + end + + module ResourceCommon + def resource_skipped + @resource_skipped + end + + def skip_resource message + @resource_skipped = message + end + end + + end +end diff --git a/lib/vulcano/profile_context.rb b/lib/vulcano/profile_context.rb new file mode 100644 index 000000000..39132a4a6 --- /dev/null +++ b/lib/vulcano/profile_context.rb @@ -0,0 +1,72 @@ +require 'vulcano/backend' + +module Vulcano + + class ProfileContext + + attr_reader :rules, :only_ifs + def initialize(profile_id, backend, profile_registry: {}, only_ifs: []) + if backend.nil? + raise "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 + __CTX = self + + # 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. + ctx = Class.new do + include Vulcano::DSL + + define_method :__register_rule do |*args| + __CTX.register_rule(*args) + end + define_method :__unregister_rule do |*args| + __CTX.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 + end + @profile_context = ctx.new + + end + + def load(content, source, line) + @profile_context.instance_eval(content, source, line) + end + + def unregister_rule id + full_id = VulcanoBaseRule::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) + if full_id.nil? + # TODO error + return + end + # add the rule to the registry + existing = @rules[full_id] + if existing.nil? + @rules[full_id] = r + else + VulcanoBaseRule::merge(existing, r) + end + end + + end +end diff --git a/lib/vulcano/resource.rb b/lib/vulcano/resource.rb index d0d3d71b1..ecb565928 100644 --- a/lib/vulcano/resource.rb +++ b/lib/vulcano/resource.rb @@ -1,17 +1,46 @@ # encoding: utf-8 # copyright: 2015, Vulcano Security GmbH # license: All rights reserved +require 'vulcano/plugins' module Vulcano class Resource - - def resource_skipped - @resource_skipped + def self.registry + @registry ||= {} end - - def skip_resource message - @resource_skipped = message - end - end -end \ No newline at end of file + + def self.resource(version) + if version != 1 + raise "Only resource version 1 is supported!" + end + Vulcano::Plugins::Resource + end +end + +require 'resources/apache_conf' +require 'resources/audit_policy' +require 'resources/auditd_conf' +require 'resources/auditd_rules' +require 'resources/command' +require 'resources/directory' +require 'resources/env' +require 'resources/etc_group' +require 'resources/file' +require 'resources/group_policy' +require 'resources/inetd_conf' +require 'resources/limits_conf' +require 'resources/login_def' +require 'resources/mysql' +require 'resources/mysql_conf' +require 'resources/mysql_session' +require 'resources/ntp_conf' +require 'resources/parse_config' +require 'resources/passwd' +require 'resources/postgres' +require 'resources/postgres_conf' +require 'resources/postgres_session' +require 'resources/processes' +require 'resources/registry_key' +require 'resources/security_policy' +require 'resources/ssh_conf' diff --git a/lib/vulcano/rule.rb b/lib/vulcano/rule.rb index 1b587107b..57f128a40 100644 --- a/lib/vulcano/rule.rb +++ b/lib/vulcano/rule.rb @@ -2,12 +2,9 @@ # copyright: 2015, Dominik Richter # license: All rights reserved require 'vulcano/base_rule' -require 'serverspec' module Vulcano class Rule < VulcanoBaseRule - include Serverspec::Helper::Type - extend Serverspec::Helper::Type include RSpec::Core::DSL # Override RSpec methods to add @@ -173,43 +170,6 @@ module Vulcano::DSL end -module Vulcano - class ProfileContext - - include Serverspec::Helper::Type - extend Serverspec::Helper::Type - include Vulcano::DSL - - def initialize profile_id, profile_registry, only_ifs - @profile_id = profile_id - @rules = profile_registry - @only_ifs = only_ifs - end - - def __unregister_rule id - full_id = VulcanoBaseRule::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) - if full_id.nil? - # TODO error - return - end - # add the rule to the registry - existing = @rules[full_id] - if existing.nil? - @rules[full_id] = r - else - VulcanoBaseRule::merge(existing, r) - end - end - - end -end - module Vulcano::GlobalDSL def __register_rule r # make sure the profile id is attached to the rule diff --git a/lib/vulcano/runner.rb b/lib/vulcano/runner.rb index 1682822cc..212f6bb8a 100644 --- a/lib/vulcano/runner.rb +++ b/lib/vulcano/runner.rb @@ -5,15 +5,13 @@ require 'uri' require 'vulcano/backend' require 'vulcano/targets' +require 'vulcano/profile_context' # spec requirements require 'rspec' require 'rspec/its' require 'specinfra' require 'specinfra/helper' require 'specinfra/helper/set' -require 'serverspec/helper' -require 'serverspec/matcher' -require 'serverspec/subject' require 'vulcano/rspec_json_formatter' module Vulcano @@ -23,37 +21,67 @@ module Vulcano def initialize(profile_id, conf) @rules = [] @profile_id = profile_id - @conf = conf.dup + @conf = Vulcano::Backend.target_config(normalize_map(conf)) - # RSpec.configuration.output_stream = $stdout - # RSpec.configuration.error_stream = $stderr - RSpec.configuration.add_formatter(:json) + # global reset + RSpec.world.reset - # specinfra - backend = Vulcano::Backend.new(@conf) - backend.resolve_target_options - backend.configure_shared_options - backend.configure_target + configure_output + configure_backend end - def add_resources(resources) - items = resources.map do |resource| - Vulcano::Targets.resolve(resource) + def normalize_map(hm) + res = {} + hm.each{|k,v| + res[k.to_s] = v + } + res + end + + def configure_output + # RSpec.configuration.output_stream = $stdout + # RSpec.configuration.error_stream = $stderr + RSpec.configuration.add_formatter(@conf['format'] || 'progress') + end + + def configure_backend + backend_name = ( @conf['backend'] ||= 'exec' ) + # @TODO all backends except for mock revert to specinfra for now + unless %w{ mock }.include? backend_name + backend_class = Vulcano::Backend.registry['specinfra'] + else + backend_class = Vulcano::Backend.registry[backend_name] end + + # Return on failure + if backend_class.nil? + raise "Can't find command backend '#{backend_name}'." + end + + # create the backend based on the config + @backend = backend_class.new(@conf) + end + + def add_tests(tests) + # retrieve the raw ruby code of all tests + items = tests.map do |test| + Vulcano::Targets.resolve(test) + end + + # add all tests (raw) to the runtime items.flatten.each do |item| add_content(item[:content], item[:ref], item[:line]) end end def add_content(content, source, line = nil) - ctx = Vulcano::ProfileContext.new(@profile_id, {}, []) + ctx = Vulcano::ProfileContext.new(@profile_id, @backend) # evaluate all tests - ctx.instance_eval(content, source, line || 1) + ctx.load(content, source, line || 1) # process the resulting rules - rules = ctx.instance_variable_get(:@rules) - rules.each do |rule_id, rule| + ctx.rules.each do |rule_id, rule| #::Vulcano::DSL.execute_rule(rule, profile_id) checks = rule.instance_variable_get(:@checks) checks.each do |m,a,b| diff --git a/test/docker.rb b/test/docker.rb new file mode 100644 index 000000000..496f2df17 --- /dev/null +++ b/test/docker.rb @@ -0,0 +1,70 @@ +require 'docker' +require 'yaml' +require_relative '../lib/vulcano' + +tests = ARGV +if tests.empty? + puts 'Nothing to do.' + exit 0 +end + +class DockerTester + def initialize(tests) + @tests = tests + @images = docker_images_by_tag + @conf = tests_conf + end + + def run + # test all images + @conf['images'].each{|n| + test_image(n) + }.all? or raise "Test failures" + end + + def docker_images_by_tag + # get all docker image tags + images = {} + Docker::Image.all.map do |img| + Array(img.info['RepoTags']).each do |tag| + images[tag] = img + end + end + images + end + + def tests_conf + # get the test configuration + conf_path = File::join(File::dirname(__FILE__), '..', '.tests.yaml') + raise "Can't find tests config in #{conf_path}" unless File::file?(conf_path) + conf = YAML.load(File::read(conf_path)) + end + + def test_container(container_id) + opts = { 'target' => "docker://#{container_id}" } + runner = Vulcano::Runner.new(nil, opts) + runner.add_tests(@tests) + runner.run + end + + def test_image(name) + dname = "docker-#{name}:latest" + image = @images[dname] + raise "Can't find docker image #{dname}" if image.nil? + + container = Docker::Container.create( + 'Cmd' => [ '/bin/bash' ], + 'Image' => image.id, + 'OpenStdin' => true, + ) + container.start + + res = test_container(container.id) + + container.kill + container.delete(force: true) + res + end +end + +DockerTester.new(tests).run diff --git a/test/helper.rb b/test/helper.rb new file mode 100644 index 000000000..e7c86f879 --- /dev/null +++ b/test/helper.rb @@ -0,0 +1,4 @@ +require 'minitest/autorun' +require 'minitest/spec' + +require 'vulcano/backend' diff --git a/test/resource/command.rb b/test/resource/command.rb new file mode 100644 index 000000000..5f719f458 --- /dev/null +++ b/test/resource/command.rb @@ -0,0 +1,30 @@ + +describe command('echo hello') do + its(:stdout) { should eq "hello\n" } + its(:stderr) { should eq "" } + its(:exit_status) { should eq 0 } +end + +describe command('>&2 echo error') do + its(:stdout) { should eq "" } + its(:stderr) { should eq "error\n" } + its(:exit_status) { should eq 0 } +end + +describe command('exit 123') do + its(:stdout) { should eq "" } + its(:stderr) { should eq "" } + its(:exit_status) { should eq 123 } +end + +describe command('/bin/sh').exists? do + it { should eq true } +end + +describe command('sh').exists? do + it { should eq true } +end + +describe command('this is not existing').exists? do + it { should eq false } +end diff --git a/test/resource/file.rb b/test/resource/file.rb new file mode 100644 index 000000000..865832b7e --- /dev/null +++ b/test/resource/file.rb @@ -0,0 +1,126 @@ + +describe file('/tmp') do + it { should exist } +end + +describe file('/tmpest') do + it { should_not exist } +end + +describe file('/tmp') do + its(:type) { should eq :directory } + it { should be_directory } +end + +describe file('/proc/version') do + its(:type) { should eq :file } + it { should be_file } + it { should_not be_directory } +end + +describe file('/dev/stdout') do + its(:type) { should eq :symlink } + it { should be_symlink } + it { should_not be_file } + it { should_not be_directory } +end + +describe file('/dev/zero') do + its(:type) { should eq :character_device } + it { should be_character_device } + it { should_not be_file } + it { should_not be_directory } +end + +# describe file('...') do +# its(:type) { should eq :block_device } +# it { should be_block_device } +# end + +# describe file('...') do +# its(:type) { should eq :socket } +# it { should be_socket } +# end + +# describe file('...') do +# its(:type) { should eq :pipe } +# it { should be_pipe } +# end + +describe file('/dev') do + its(:mode) { should eq 00755 } +end + +describe file('/dev') do + it { should be_mode 00755 } +end + +describe file('/root') do + its(:owner) { should eq 'root' } +end + +describe file('/dev') do + it { should be_owned_by 'root' } +end + +describe file('/root') do + its(:group) { should eq 'root' } +end + +describe file('/dev') do + it { should be_grouped_into 'root' } +end + +describe file('/dev/kcore') do + its(:link_target) { should eq '/proc/kcore' } +end + +describe file('/dev/kcore') do + it { should be_linked_to '/proc/kcore' } +end + +describe file('/proc/cpuinfo') do + its(:content) { should match /^processor/ } +end + +describe file('/').mtime.to_i do + it { should <= Time.now.to_i } + it { should >= Time.now.to_i - 1000} +end + +describe file('/') do + its(:size) { should be > 64 } + its(:size) { should be < 10240 } +end + +describe file('/proc/cpuinfo') do + its(:size) { should be 0 } +end + +# @TODO selinux_label + +describe file('/proc') do + it { should be_mounted } +end + +describe file('/proc/cpuinfo') do + it { should_not be_mounted } +end + +# @TODO immutable? +# @TODO product_version +# @TODO file_version +# @TODO version? + +require 'digest' +cpuinfo = file('/proc/cpuinfo').content + +md5sum = Digest::MD5.hexdigest(cpuinfo) +describe file('/proc/cpuinfo') do + its(:md5sum) { should eq md5sum } +end + +sha256sum = Digest::SHA256.hexdigest(cpuinfo) +describe file('/proc/cpuinfo') do + its(:sha256sum) { should eq sha256sum } +end diff --git a/test/unit/backend_test.rb b/test/unit/backend_test.rb new file mode 100644 index 000000000..9ab5ba45c --- /dev/null +++ b/test/unit/backend_test.rb @@ -0,0 +1,66 @@ +require 'helper' + +describe 'Vulcano::Backend' do + + it 'should have a populated registry' do + reg = Vulcano::Backend.registry + reg.must_be_kind_of Hash + reg.keys.must_include 'mock' + reg.keys.must_include 'specinfra' + end + + + describe 'target config helper' do + it 'configures resolves target' do + org = { + 'target' => 'ssh://user:pass@host.com:123', + } + res = Vulcano::Backend.target_config(org) + res['backend'].must_equal 'ssh' + res['host'].must_equal 'host.com' + res['user'].must_equal 'user' + res['password'].must_equal 'pass' + res['port'].must_equal 123 + res['target'].must_equal org['target'] + org.keys.must_equal ['target'] + end + + it 'resolves a target while keeping existing fields' do + org = { + 'target' => 'ssh://user:pass@host.com:123', + 'backend' => rand, + 'host' => rand, + 'user' => rand, + 'password' => rand, + 'port' => rand, + 'target' => rand, + } + res = Vulcano::Backend.target_config(org) + res.must_equal org + end + + it 'keeps the configuration when incorrect target is supplied' do + org = { + 'target' => 'wrong', + } + res = Vulcano::Backend.target_config(org) + res['backend'].must_be_nil + res['host'].must_be_nil + res['user'].must_be_nil + res['password'].must_be_nil + res['port'].must_be_nil + res['target'].must_equal org['target'] + end + end + + describe 'helper method for creating backends' do + it 'creates v1 backends by default' do + Vulcano.backend.must_equal Vulcano::Plugins::Backend + end + + it 'creates v1 backends' do + Vulcano.backend(1).must_equal Vulcano::Plugins::Backend + end + end + +end diff --git a/test/unit/plugins_v1_backend_file_test.rb b/test/unit/plugins_v1_backend_file_test.rb new file mode 100644 index 000000000..768623aad --- /dev/null +++ b/test/unit/plugins_v1_backend_file_test.rb @@ -0,0 +1,31 @@ +require 'helper' + +describe 'Vulcano::Plugins::Backend::FileCommon' do + let(:cls) { Vulcano::Plugins::Backend::FileCommon } + let(:backend) { cls.new } + + it 'default type is :unkown' do + backend.type.must_equal :unknown + end + + describe 'with non-empty content' do + let(:backend) { + Class.new(cls) do + def content; 'Hello World'; end + end.new + } + + it 'must return raw content' do + backend.content.must_equal 'Hello World' + end + + it 'must calculate the md5sum of content' do + backend.md5sum.must_equal 'b10a8db164e0754105b7a99be72e3fe5' + end + + it 'must calculate the sha256sum of content' do + backend.sha256sum.must_equal 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e' + end + end + +end diff --git a/test/unit/plugins_v1_backend_test.rb b/test/unit/plugins_v1_backend_test.rb new file mode 100644 index 000000000..979c0fd70 --- /dev/null +++ b/test/unit/plugins_v1_backend_test.rb @@ -0,0 +1,43 @@ +require 'helper' + +describe 'Vulcano::Plugins::Backend' do + let(:cls) { Vulcano::Plugins::Backend } + let(:child) { Class.new(cls) } + + it 'provides a name method for registering' do + child.must_respond_to :name + end + + describe 'when registering a plugin' do + let(:registry) { Vulcano::Backend.registry } + + before do + child.name 'test' + end + + after do + registry.delete('test') + end + + it 'must have the backend registered' do + registry.keys.must_include 'test' + registry['test'].must_equal child + end + + it 'must raise an error if file is not implemented' do + t = registry['test'].new + proc { t.run_command }.must_raise NotImplementedError + end + + it 'must raise an error if run_command is not implemented' do + t = registry['test'].new + proc { t.file }.must_raise NotImplementedError + end + + it 'must raise an error if os is not implemented' do + t = registry['test'].new + proc { t.os }.must_raise NotImplementedError + end + end + +end diff --git a/vulcano.gemspec b/vulcano.gemspec index 364f1265a..98a848cf6 100644 --- a/vulcano.gemspec +++ b/vulcano.gemspec @@ -20,14 +20,13 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.5" spec.add_development_dependency "minitest", "~> 5.5" - spec.add_development_dependency "rspec", "~> 3.2" + spec.add_development_dependency "rspec", "~> 3.3" spec.add_development_dependency "rake", "~> 10" spec.add_development_dependency "pry", "~> 0.10" spec.add_dependency 'thor', '~> 0.19' spec.add_dependency 'json', '~> 1.8' spec.add_dependency 'rainbow', '~> 2' - spec.add_dependency 'serverspec', '~> 2.18' spec.add_dependency 'method_source', '~> 0.8' spec.add_dependency 'rubyzip', '~> 1.1' spec.add_dependency 'rspec', '~> 3.3'