api: Complete overhaul of resources and backends

* Created general plugin system
* Add backend plugins: Specinfra and Mock
* Add resource plugins: All Vulcanosec resources
* Add integration testing suite
* Removed Serverspec as a dependency

This will be ready once the remaining resources from Serverspec are integrated.
So far command and file have been largely migrated. See unit and integration tests.
This commit is contained in:
Dominik Richter 2015-09-03 16:18:07 +02:00
commit d769659d48
56 changed files with 1406 additions and 673 deletions

4
.tests.yaml Normal file
View file

@ -0,0 +1,4 @@
images:
- ubuntu-1204-20150612
- ubuntu-1404-20150320
- ubuntu-latest

24
Rakefile Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
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

View file

@ -0,0 +1,9 @@
# encoding: utf-8
require 'resources/file'
module Vulcano::Resources
class Directory < File
name 'directory'
end
end

View file

@ -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

View file

@ -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
end

View file

@ -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
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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],

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

8
lib/vulcano/plugins.rb Normal file
View file

@ -0,0 +1,8 @@
# encoding: utf-8
module Vulcano
module Plugins
autoload :Resource, 'vulcano/plugins/resource'
autoload :Backend, 'vulcano/plugins/backend'
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
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'

View file

@ -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

View file

@ -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|

70
test/docker.rb Normal file
View file

@ -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

4
test/helper.rb Normal file
View file

@ -0,0 +1,4 @@
require 'minitest/autorun'
require 'minitest/spec'
require 'vulcano/backend'

30
test/resource/command.rb Normal file
View file

@ -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

126
test/resource/file.rb Normal file
View file

@ -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

66
test/unit/backend_test.rb Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'