Merge pull request #526 from chef/adamleff/resource-namespace

Placing all resources in the Inspec::Resources namespace
This commit is contained in:
Dominik Richter 2016-03-09 10:29:11 +01:00
commit 9cb2bc5dec
52 changed files with 4779 additions and 4677 deletions

View file

@ -4,26 +4,28 @@
# author: Dominik Richter
# license: All rights reserved
class Apache < Inspec.resource(1)
name 'apache'
module Inspec::Resources
class Apache < Inspec.resource(1)
name 'apache'
attr_reader :service, :conf_dir, :conf_path, :user
def initialize
case inspec.os[:family]
when 'ubuntu', 'debian'
@service = 'apache2'
@conf_dir = '/etc/apache2/'
@conf_path = File.join @conf_dir, 'apache2.conf'
@user = 'www-data'
else
@service = 'httpd'
@conf_dir = '/etc/httpd/'
@conf_path = File.join @conf_dir, '/conf/httpd.conf'
@user = 'apache'
attr_reader :service, :conf_dir, :conf_path, :user
def initialize
case inspec.os[:family]
when 'ubuntu', 'debian'
@service = 'apache2'
@conf_dir = '/etc/apache2/'
@conf_path = ::File.join @conf_dir, 'apache2.conf'
@user = 'www-data'
else
@service = 'httpd'
@conf_dir = '/etc/httpd/'
@conf_path = ::File.join @conf_dir, '/conf/httpd.conf'
@user = 'apache'
end
end
def to_s
'Apache Environment'
end
end
def to_s
'Apache Environment'
end
end

View file

@ -7,118 +7,120 @@
require 'utils/simpleconfig'
require 'utils/find_files'
class ApacheConf < Inspec.resource(1)
name 'apache_conf'
desc 'Use the apache_conf InSpec audit resource to test the configuration settings for Apache. This file is typically located under /etc/apache2 on the Debian and Ubuntu platforms and under /etc/httpd on the Fedora, CentOS, Red Hat Enterprise Linux, and Arch Linux platforms. The configuration settings may vary significantly from platform to platform.'
example "
describe apache_conf do
its('setting_name') { should eq 'value' }
end
"
include FindFiles
def initialize(conf_path = nil)
@conf_path = conf_path || inspec.apache.conf_path
@conf_dir = File.dirname(@conf_path)
@files_contents = {}
@content = nil
@params = nil
read_content
end
def content
@content ||= read_content
end
def params(*opts)
@params || read_content
res = @params
opts.each do |opt|
res = res[opt] unless res.nil?
end
res
end
def method_missing(name)
# ensure params are loaded
@params || read_content
# extract values
@params[name.to_s] unless @params.nil?
end
def filter_comments(data)
content = ''
data.each_line do |line|
if !line.match(/^\s*#/)
content << line
module Inspec::Resources
class ApacheConf < Inspec.resource(1)
name 'apache_conf'
desc 'Use the apache_conf InSpec audit resource to test the configuration settings for Apache. This file is typically located under /etc/apache2 on the Debian and Ubuntu platforms and under /etc/httpd on the Fedora, CentOS, Red Hat Enterprise Linux, and Arch Linux platforms. The configuration settings may vary significantly from platform to platform.'
example "
describe apache_conf do
its('setting_name') { should eq 'value' }
end
end
content
end
"
def read_content
@content = ''
@params = {}
include FindFiles
# skip if the main configuration file doesn't exist
file = inspec.file(@conf_path)
if !file.file?
return skip_resource "Can't find file \"#{@conf_path}\""
def initialize(conf_path = nil)
@conf_path = conf_path || inspec.apache.conf_path
@conf_dir = ::File.dirname(@conf_path)
@files_contents = {}
@content = nil
@params = nil
read_content
end
raw_conf = file.content
if raw_conf.empty? && file.size > 0
return skip_resource("Can't read file \"#{@conf_path}\"")
def content
@content ||= read_content
end
to_read = [@conf_path]
until to_read.empty?
raw_conf = read_file(to_read[0])
@content += raw_conf
# parse include file parameters
params = SimpleConfig.new(
raw_conf,
assignment_re: /^\s*(\S+)\s+(.*)\s*$/,
multiple_values: true,
).params
@params.merge!(params)
to_read = to_read.drop(1)
to_read += include_files(params).find_all do |fp|
not @files_contents.key? fp
def params(*opts)
@params || read_content
res = @params
opts.each do |opt|
res = res[opt] unless res.nil?
end
res
end
# fiter comments
@content = filter_comments @content
@content
end
def method_missing(name)
# ensure params are loaded
@params || read_content
def include_files(params)
# see if there is more config files to include
include_files = params['Include'] || []
include_files_optional = params['IncludeOptional'] || []
includes = []
(include_files + include_files_optional).each do |f|
id = File.join(@conf_dir, f)
files = find_files(id, depth: 1, type: 'file')
includes.push(files) if files
# extract values
@params[name.to_s] unless @params.nil?
end
# [].flatten! == nil
includes.flatten! || []
end
def filter_comments(data)
content = ''
data.each_line do |line|
if !line.match(/^\s*#/)
content << line
end
end
content
end
def read_file(path)
@files_contents[path] ||= inspec.file(path).content
end
def read_content
@content = ''
@params = {}
def to_s
"Apache Config #{@conf_path}"
# skip if the main configuration file doesn't exist
file = inspec.file(@conf_path)
if !file.file?
return skip_resource "Can't find file \"#{@conf_path}\""
end
raw_conf = file.content
if raw_conf.empty? && file.size > 0
return skip_resource("Can't read file \"#{@conf_path}\"")
end
to_read = [@conf_path]
until to_read.empty?
raw_conf = read_file(to_read[0])
@content += raw_conf
# parse include file parameters
params = SimpleConfig.new(
raw_conf,
assignment_re: /^\s*(\S+)\s+(.*)\s*$/,
multiple_values: true,
).params
@params.merge!(params)
to_read = to_read.drop(1)
to_read += include_files(params).find_all do |fp|
not @files_contents.key? fp
end
end
# fiter comments
@content = filter_comments @content
@content
end
def include_files(params)
# see if there is more config files to include
include_files = params['Include'] || []
include_files_optional = params['IncludeOptional'] || []
includes = []
(include_files + include_files_optional).each do |f|
id = ::File.join(@conf_dir, f)
files = find_files(id, depth: 1, type: 'file')
includes.push(files) if files
end
# [].flatten! == nil
includes.flatten! || []
end
def read_file(path)
@files_contents[path] ||= inspec.file(path).content
end
def to_s
"Apache Config #{@conf_path}"
end
end
end

View file

@ -28,120 +28,122 @@
require 'uri'
class AptRepository < Inspec.resource(1)
name 'apt'
desc 'Use the apt InSpec audit resource to verify Apt repositories on the Debian and Ubuntu platforms, and also PPA repositories on the Ubuntu platform.'
example "
describe apt('nginx/stable') do
it { should exist }
it { should be_enabled }
end
"
module Inspec::Resources
class AptRepository < Inspec.resource(1)
name 'apt'
desc 'Use the apt InSpec audit resource to verify Apt repositories on the Debian and Ubuntu platforms, and also PPA repositories on the Ubuntu platform.'
example "
describe apt('nginx/stable') do
it { should exist }
it { should be_enabled }
end
"
def initialize(ppa_name)
@deb_url = nil
# check if the os is ubuntu or debian
if inspec.os.debian?
@deb_url = determine_ppa_url(ppa_name)
else
# this resource is only supported on ubuntu and debian
skip_resource 'The `apt` resource is not supported on your OS yet.'
def initialize(ppa_name)
@deb_url = nil
# check if the os is ubuntu or debian
if inspec.os.debian?
@deb_url = determine_ppa_url(ppa_name)
else
# this resource is only supported on ubuntu and debian
skip_resource 'The `apt` resource is not supported on your OS yet.'
end
end
def exists?
find_repo.count > 0
end
def enabled?
return false if find_repo.count == 0
actives = find_repo.map { |repo| repo[:active] }
actives = actives.uniq
actives.size == 1 && actives[0] = true
end
def to_s
"Apt Repository #{@deb_url}"
end
private
def find_repo
read_debs.select { |repo| repo[:url] == @deb_url && repo[:type] == 'deb' }
end
HTTP_URL_RE = /\A#{URI::DEFAULT_PARSER.make_regexp(%w{http https})}\z/
# read
def read_debs
return @repo_cache if defined?(@repo_cache)
# load all lists
cmd = inspec.command("find /etc/apt/ -name \*.list -exec sh -c 'cat {} || echo -n' \\;")
# @see https://help.ubuntu.com/community/Repositories/CommandLine#Explanation_of_the_Repository_Format
@repo_cache = cmd.stdout.chomp.split("\n").each_with_object([]) do |raw_line, lines|
active = true
# detect if the repo is commented out
line = raw_line.gsub(/^(#\s*)*/, '')
active = false if raw_line != line
# eg.: deb http://archive.ubuntu.com/ubuntu/ wily main restricted
parse_repo = /^\s*(\S+)\s+"?([^ "\t\r\n\f]+)"?\s+(\S+)\s+(.*)$/.match(line)
# check if we got any result and the second param is an url
next if parse_repo.nil? || !parse_repo[2] =~ HTTP_URL_RE
# map data
repo = {
type: parse_repo[1],
url: parse_repo[2],
distro: parse_repo[3],
components: parse_repo[4].chomp.split(' '),
active: active,
}
next unless ['deb', 'deb-src'].include? repo[:type]
lines.push(repo)
end
end
# resolves ppa urls
# @see http://bazaar.launchpad.net/~ubuntu-core-dev/software-properties/main/view/head:/softwareproperties/ppa.py
def determine_ppa_url(ppa_url)
# verify if we have the url already, then just return
return ppa_url if ppa_url =~ HTTP_URL_RE
# otherwise start generating the ppa url
# special care if the name stats with :
ppa_url = ppa_url.split(':')[1] if ppa_url.start_with?('ppa:')
# parse ppa owner and repo
ppa_owner, ppa_repo = ppa_url.split('/')
ppa_repo = 'ppa' if ppa_repo.nil?
# construct new ppa url and return it
format('http://ppa.launchpad.net/%s/%s/ubuntu', ppa_owner, ppa_repo)
end
end
def exists?
find_repo.count > 0
end
# for compatability with serverspec
# this is deprecated syntax and will be removed in future versions
class PpaRepository < AptRepository
name 'ppa'
def enabled?
return false if find_repo.count == 0
actives = find_repo.map { |repo| repo[:active] }
actives = actives.uniq
actives.size == 1 && actives[0] = true
end
def to_s
"Apt Repository #{@deb_url}"
end
private
def find_repo
read_debs.select { |repo| repo[:url] == @deb_url && repo[:type] == 'deb' }
end
HTTP_URL_RE = /\A#{URI::DEFAULT_PARSER.make_regexp(%w{http https})}\z/
# read
def read_debs
return @repo_cache if defined?(@repo_cache)
# load all lists
cmd = inspec.command("find /etc/apt/ -name \*.list -exec sh -c 'cat {} || echo -n' \\;")
# @see https://help.ubuntu.com/community/Repositories/CommandLine#Explanation_of_the_Repository_Format
@repo_cache = cmd.stdout.chomp.split("\n").each_with_object([]) do |raw_line, lines|
active = true
# detect if the repo is commented out
line = raw_line.gsub(/^(#\s*)*/, '')
active = false if raw_line != line
# eg.: deb http://archive.ubuntu.com/ubuntu/ wily main restricted
parse_repo = /^\s*(\S+)\s+"?([^ "\t\r\n\f]+)"?\s+(\S+)\s+(.*)$/.match(line)
# check if we got any result and the second param is an url
next if parse_repo.nil? || !parse_repo[2] =~ HTTP_URL_RE
# map data
repo = {
type: parse_repo[1],
url: parse_repo[2],
distro: parse_repo[3],
components: parse_repo[4].chomp.split(' '),
active: active,
}
next unless ['deb', 'deb-src'].include? repo[:type]
lines.push(repo)
def exists?
deprecated
super()
end
end
# resolves ppa urls
# @see http://bazaar.launchpad.net/~ubuntu-core-dev/software-properties/main/view/head:/softwareproperties/ppa.py
def determine_ppa_url(ppa_url)
# verify if we have the url already, then just return
return ppa_url if ppa_url =~ HTTP_URL_RE
# otherwise start generating the ppa url
def enabled?
deprecated
super()
end
# special care if the name stats with :
ppa_url = ppa_url.split(':')[1] if ppa_url.start_with?('ppa:')
# parse ppa owner and repo
ppa_owner, ppa_repo = ppa_url.split('/')
ppa_repo = 'ppa' if ppa_repo.nil?
# construct new ppa url and return it
format('http://ppa.launchpad.net/%s/%s/ubuntu', ppa_owner, ppa_repo)
end
end
# for compatability with serverspec
# this is deprecated syntax and will be removed in future versions
class PpaRepository < AptRepository
name 'ppa'
def exists?
deprecated
super()
end
def enabled?
deprecated
super()
end
def deprecated
warn '[DEPRECATION] `ppa(reponame)` is deprecated. Please use `apt(reponame)` instead.'
def deprecated
warn '[DEPRECATION] `ppa(reponame)` is deprecated. Please use `apt(reponame)` instead.'
end
end
end

View file

@ -24,40 +24,42 @@
#
# Further information is available at: https://msdn.microsoft.com/en-us/library/dd973859.aspx
class AuditPolicy < Inspec.resource(1)
name 'audit_policy'
desc 'Use the audit_policy InSpec audit resource to test auditing policies on the Microsoft Windows platform. An auditing policy is a category of security-related events to be audited. Auditing is disabled by default and may be enabled for categories like account management, logon events, policy changes, process tracking, privilege use, system events, or object access. For each auditing category property that is enabled, the auditing level may be set to No Auditing, Not Specified, Success, Success and Failure, or Failure.'
example "
describe audit_policy do
its('parameter') { should eq 'value' }
end
"
module Inspec::Resources
class AuditPolicy < Inspec.resource(1)
name 'audit_policy'
desc 'Use the audit_policy InSpec audit resource to test auditing policies on the Microsoft Windows platform. An auditing policy is a category of security-related events to be audited. Auditing is disabled by default and may be enabled for categories like account management, logon events, policy changes, process tracking, privilege use, system events, or object access. For each auditing category property that is enabled, the auditing level may be set to No Auditing, Not Specified, Success, Success and Failure, or Failure.'
example "
describe audit_policy do
its('parameter') { should eq 'value' }
end
"
def method_missing(method)
key = method.to_s
def method_missing(method)
key = method.to_s
# 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,
result ||= inspec.command("Auditpol /get /subcategory:'#{key}' /r").stdout
# 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,
result ||= inspec.command("Auditpol /get /subcategory:'#{key}' /r").stdout
# find line
target = nil
result.each_line {|s|
target = s.strip if s =~ /\b.*#{key}.*\b/
}
# find line
target = nil
result.each_line {|s|
target = s.strip if s =~ /\b.*#{key}.*\b/
}
# extract value
values = nil
unless target.nil?
# split csv values and return value
values = target.split(',')[4]
# extract value
values = nil
unless target.nil?
# split csv values and return value
values = target.split(',')[4]
end
values
end
values
end
def to_s
'Audit Policy'
def to_s
'Audit Policy'
end
end
end

View file

@ -6,50 +6,52 @@
require 'utils/simpleconfig'
class AuditDaemonConf < Inspec.resource(1)
name 'auditd_conf'
desc "Use the auditd_conf InSpec audit resource to test the configuration settings for the audit daemon. This file is typically located under /etc/audit/auditd.conf' on UNIX and Linux platforms."
example "
describe auditd_conf do
its('space_left_action') { should eq 'email' }
end
"
module Inspec::Resources
class AuditDaemonConf < Inspec.resource(1)
name 'auditd_conf'
desc "Use the auditd_conf InSpec audit resource to test the configuration settings for the audit daemon. This file is typically located under /etc/audit/auditd.conf' on UNIX and Linux platforms."
example "
describe auditd_conf do
its('space_left_action') { should eq 'email' }
end
"
def initialize(path = nil)
@conf_path = path || '/etc/audit/auditd.conf'
end
def method_missing(name)
read_params[name.to_s]
end
def to_s
'Audit Daemon Config'
end
private
def read_params
return @params if defined?(@params)
# read the file
file = inspec.file(@conf_path)
if !file.file?
skip_resource "Can't find file '#{@conf_path}'"
return @params = {}
def initialize(path = nil)
@conf_path = path || '/etc/audit/auditd.conf'
end
content = file.content
if content.empty? && file.size > 0
skip_resource "Can't read file '#{@conf_path}'"
return @params = {}
def method_missing(name)
read_params[name.to_s]
end
# parse the file
conf = SimpleConfig.new(
content,
multiple_values: false,
)
@params = conf.params
def to_s
'Audit Daemon Config'
end
private
def read_params
return @params if defined?(@params)
# read the file
file = inspec.file(@conf_path)
if !file.file?
skip_resource "Can't find file '#{@conf_path}'"
return @params = {}
end
content = file.content
if content.empty? && file.size > 0
skip_resource "Can't read file '#{@conf_path}'"
return @params = {}
end
# parse the file
conf = SimpleConfig.new(
content,
multiple_values: false,
)
@params = conf.params
end
end
end

View file

@ -7,197 +7,199 @@
require 'forwardable'
require 'utils/filter_array'
class AuditdRulesLegacy
def initialize(content)
@content = content
@opts = {
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: true,
}
end
def params
@params ||= SimpleConfig.new(@content, @opts).params
end
def method_missing(name)
params[name.to_s]
end
def status(name)
@status_opts = {
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
}
@status_content ||= inspec.command('/sbin/auditctl -s').stdout.chomp
@status_params = SimpleConfig.new(@status_content, @status_opts).params
status = @status_params['AUDIT_STATUS']
return nil if status.nil?
items = Hash[status.scan(/([^=]+)=(\w*)\s*/)]
items[name]
end
def to_s
'Audit Daemon Rules (for auditd version < 2.3)'
end
end
# rubocop:disable Metrics/ClassLength
class AuditDaemonRules < Inspec.resource(1)
extend Forwardable
attr_accessor :rules, :lines
name 'auditd_rules'
desc 'Use the auditd_rules InSpec audit resource to test the rules for logging that exist on the system. The audit.rules file is typically located under /etc/audit/ and contains the list of rules that define what is captured in log files.'
example "
# syntax for auditd < 2.3
describe auditd_rules do
its('LIST_RULES') {should contain_match(/^exit,always arch=.* key=time-change syscall=adjtimex,settimeofday/) }
its('LIST_RULES') {should contain_match(/^exit,always arch=.* key=time-change syscall=stime,settimeofday,adjtimex/) }
its('LIST_RULES') {should contain_match(/^exit,always arch=.* key=time-change syscall=clock_settime/)}
its('LIST_RULES') {should contain_match(/^exit,always watch=\/etc\/localtime perm=wa key=time-change/)}
module Inspec::Resources
class AuditdRulesLegacy
def initialize(content)
@content = content
@opts = {
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: true,
}
end
# syntax for auditd >= 2.3
describe auditd_rules.syscall('open').action do
it { should eq(['always']) }
def params
@params ||= SimpleConfig.new(@content, @opts).params
end
describe auditd_rules.key('sshd_config') do
its(:permissions) { should contain_match(/x/) }
def method_missing(name)
params[name.to_s]
end
describe auditd_rules do
its(:lines) { should contain_match(%r{-w /etc/ssh/sshd_config/}) }
def status(name)
@status_opts = {
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
}
@status_content ||= inspec.command('/sbin/auditctl -s').stdout.chomp
@status_params = SimpleConfig.new(@status_content, @status_opts).params
status = @status_params['AUDIT_STATUS']
return nil if status.nil?
items = Hash[status.scan(/([^=]+)=(\w*)\s*/)]
items[name]
end
"
def initialize
@content = inspec.command('/sbin/auditctl -l').stdout.chomp
def to_s
'Audit Daemon Rules (for auditd version < 2.3)'
end
end
if @content =~ /^LIST_RULES:/
# do not warn on centos 5
unless inspec.os[:family] == 'centos' && inspec.os[:release].to_i == 5
warn '[WARN] this version of auditd is outdated. Updating it allows for using more precise matchers.'
# rubocop:disable Metrics/ClassLength
class AuditDaemonRules < Inspec.resource(1)
extend Forwardable
attr_accessor :rules, :lines
name 'auditd_rules'
desc 'Use the auditd_rules InSpec audit resource to test the rules for logging that exist on the system. The audit.rules file is typically located under /etc/audit/ and contains the list of rules that define what is captured in log files.'
example "
# syntax for auditd < 2.3
describe auditd_rules do
its('LIST_RULES') {should contain_match(/^exit,always arch=.* key=time-change syscall=adjtimex,settimeofday/) }
its('LIST_RULES') {should contain_match(/^exit,always arch=.* key=time-change syscall=stime,settimeofday,adjtimex/) }
its('LIST_RULES') {should contain_match(/^exit,always arch=.* key=time-change syscall=clock_settime/)}
its('LIST_RULES') {should contain_match(/^exit,always watch=\/etc\/localtime perm=wa key=time-change/)}
end
@legacy = AuditdRulesLegacy.new(@content)
else
parse_content
end
end
# non-legacy instances are not asked for `its('LIST_RULES')`
# rubocop:disable Style/MethodName
def LIST_RULES
return @legacy.LIST_RULES if @legacy
fail 'Using legacy auditd_rules LIST_RULES interface with non-legacy audit package. Please use the new syntax.'
end
# syntax for auditd >= 2.3
describe auditd_rules.syscall('open').action do
it { should eq(['always']) }
end
def status(name = nil)
return @legacy.status(name) if @legacy
describe auditd_rules.key('sshd_config') do
its(:permissions) { should contain_match(/x/) }
end
@status_content ||= inspec.command('/sbin/auditctl -s').stdout.chomp
@status_params ||= Hash[@status_content.scan(/^([^ ]+) (.*)$/)]
describe auditd_rules do
its(:lines) { should contain_match(%r{-w /etc/ssh/sshd_config/}) }
end
"
return @status_params[name] if name
@status_params
end
def initialize
@content = inspec.command('/sbin/auditctl -l').stdout.chomp
def parse_content
@rules = {
syscalls: [],
files: [],
}
@lines = @content.lines.map(&:chomp)
lines.each do |line|
if is_syscall?(line)
syscalls = get_syscalls line
action, list = get_action_list line
fields, opts = get_fields line
# create a 'flatter' structure because sanity
syscalls.each do |s|
@rules[:syscalls] << { syscall: s, list: list, action: action, fields: fields }.merge(opts)
if @content =~ /^LIST_RULES:/
# do not warn on centos 5
unless inspec.os[:family] == 'centos' && inspec.os[:release].to_i == 5
warn '[WARN] this version of auditd is outdated. Updating it allows for using more precise matchers.'
end
elsif is_file?(line)
file = get_file line
perms = get_permissions line
key = get_key line
@rules[:files] << { file: file, key: key, permissions: perms }
@legacy = AuditdRulesLegacy.new(@content)
else
parse_content
end
end
end
def syscall(name)
select_name(:syscall, name)
end
def file(name)
select_name(:file, name)
end
# both files and syscalls have `key` identifiers
def key(name)
res = rules.values.flatten.find_all { |rule| rule[:key] == name }
FilterArray.new(res)
end
def to_s
'Audit Daemon Rules'
end
private
def select_name(key, name)
plural = "#{key}s".to_sym
res = rules[plural].find_all { |rule| rule[key] == name }
FilterArray.new(res)
end
def is_syscall?(line)
line.match(/\ -S /)
end
def is_file?(line)
line.match(/-w /)
end
def get_syscalls(line)
line.scan(/-S ([^ ]+) /).flatten.first.split(',')
end
def get_action_list(line)
line.scan(/-a ([^,]+),([^ ]+)/).flatten
end
# NB only in file lines
def get_key(line)
line.match(/-k ([^ ]+)/)[1]
end
# NOTE there are NO precautions wrt. filenames containing spaces in auditctl
# `auditctl -w /foo\ bar` gives the following line: `-w /foo bar -p rwxa`
def get_file(line)
line.match(/-w (.+) -p/)[1]
end
def get_permissions(line)
line.match(/-p ([^ ]+)/)[1]
end
def get_fields(line)
fields = line.gsub(/-[aS] [^ ]+ /, '').split('-F ').map { |l| l.split(' ') }.flatten
opts = {}
fields.find_all { |x| x.match(/[a-z]+=.*/) }.each do |kv|
k, v = kv.split('=')
opts[k.to_sym] = v
# non-legacy instances are not asked for `its('LIST_RULES')`
# rubocop:disable Style/MethodName
def LIST_RULES
return @legacy.LIST_RULES if @legacy
fail 'Using legacy auditd_rules LIST_RULES interface with non-legacy audit package. Please use the new syntax.'
end
[fields, opts]
def status(name = nil)
return @legacy.status(name) if @legacy
@status_content ||= inspec.command('/sbin/auditctl -s').stdout.chomp
@status_params ||= Hash[@status_content.scan(/^([^ ]+) (.*)$/)]
return @status_params[name] if name
@status_params
end
def parse_content
@rules = {
syscalls: [],
files: [],
}
@lines = @content.lines.map(&:chomp)
lines.each do |line|
if is_syscall?(line)
syscalls = get_syscalls line
action, list = get_action_list line
fields, opts = get_fields line
# create a 'flatter' structure because sanity
syscalls.each do |s|
@rules[:syscalls] << { syscall: s, list: list, action: action, fields: fields }.merge(opts)
end
elsif is_file?(line)
file = get_file line
perms = get_permissions line
key = get_key line
@rules[:files] << { file: file, key: key, permissions: perms }
end
end
end
def syscall(name)
select_name(:syscall, name)
end
def file(name)
select_name(:file, name)
end
# both files and syscalls have `key` identifiers
def key(name)
res = rules.values.flatten.find_all { |rule| rule[:key] == name }
FilterArray.new(res)
end
def to_s
'Audit Daemon Rules'
end
private
def select_name(key, name)
plural = "#{key}s".to_sym
res = rules[plural].find_all { |rule| rule[key] == name }
FilterArray.new(res)
end
def is_syscall?(line)
line.match(/\ -S /)
end
def is_file?(line)
line.match(/-w /)
end
def get_syscalls(line)
line.scan(/-S ([^ ]+) /).flatten.first.split(',')
end
def get_action_list(line)
line.scan(/-a ([^,]+),([^ ]+)/).flatten
end
# NB only in file lines
def get_key(line)
line.match(/-k ([^ ]+)/)[1]
end
# NOTE there are NO precautions wrt. filenames containing spaces in auditctl
# `auditctl -w /foo\ bar` gives the following line: `-w /foo bar -p rwxa`
def get_file(line)
line.match(/-w (.+) -p/)[1]
end
def get_permissions(line)
line.match(/-p ([^ ]+)/)[1]
end
def get_fields(line)
fields = line.gsub(/-[aS] [^ ]+ /, '').split('-F ').map { |l| l.split(' ') }.flatten
opts = {}
fields.find_all { |x| x.match(/[a-z]+=.*/) }.each do |kv|
k, v = kv.split('=')
opts[k.to_sym] = v
end
[fields, opts]
end
end
end

View file

@ -8,114 +8,116 @@
# it { should have_interface 'eth0' }
# end
class Bridge < Inspec.resource(1)
name 'bridge'
desc 'Use the bridge InSpec audit resource to test basic network bridge properties, such as name, if an interface is defined, and the associations for any defined interface.'
example "
describe bridge 'br0' do
it { should exist }
it { should have_interface 'eth0' }
module Inspec::Resources
class Bridge < Inspec.resource(1)
name 'bridge'
desc 'Use the bridge InSpec audit resource to test basic network bridge properties, such as name, if an interface is defined, and the associations for any defined interface.'
example "
describe bridge 'br0' do
it { should exist }
it { should have_interface 'eth0' }
end
"
def initialize(bridge_name)
@bridge_name = bridge_name
@bridge_provider = nil
if inspec.os.linux?
@bridge_provider = LinuxBridge.new(inspec)
elsif inspec.os.windows?
@bridge_provider = WindowsBridge.new(inspec)
else
return skip_resource 'The `bridge` resource is not supported on your OS yet.'
end
end
"
def initialize(bridge_name)
@bridge_name = bridge_name
def exists?
!bridge_info.nil? && !bridge_info[:name].nil?
end
@bridge_provider = nil
if inspec.os.linux?
@bridge_provider = LinuxBridge.new(inspec)
elsif inspec.os.windows?
@bridge_provider = WindowsBridge.new(inspec)
else
return skip_resource 'The `bridge` resource is not supported on your OS yet.'
def has_interface?(interface)
return skip_resource 'The `bridge` resource does not provide interface detection for Windows yet' if inspec.os.windows?
bridge_info.nil? ? false : bridge_info[:interfaces].include?(interface)
end
def interfaces
bridge_info.nil? ? nil : bridge_info[:interfaces]
end
def to_s
"Bridge #{@bridge_name}"
end
private
def bridge_info
return @cache if defined?(@cache)
@cache = @bridge_provider.bridge_info(@bridge_name) if !@bridge_provider.nil?
end
end
def exists?
!bridge_info.nil? && !bridge_info[:name].nil?
end
def has_interface?(interface)
return skip_resource 'The `bridge` resource does not provide interface detection for Windows yet' if inspec.os.windows?
bridge_info.nil? ? false : bridge_info[:interfaces].include?(interface)
end
def interfaces
bridge_info.nil? ? nil : bridge_info[:interfaces]
end
def to_s
"Bridge #{@bridge_name}"
end
private
def bridge_info
return @cache if defined?(@cache)
@cache = @bridge_provider.bridge_info(@bridge_name) if !@bridge_provider.nil?
end
end
class BridgeDetection
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
# Linux Bridge
# If /sys/class/net/{interface}/bridge exists then it must be a bridge
# /sys/class/net/{interface}/brif contains the network interfaces
# @see http://www.tldp.org/HOWTO/BRIDGE-STP-HOWTO/set-up-the-bridge.html
# @see http://unix.stackexchange.com/questions/40560/how-to-know-if-a-network-interface-is-tap-tun-bridge-or-physical
class LinuxBridge < BridgeDetection
def bridge_info(bridge_name)
# read bridge information
bridge = inspec.file("/sys/class/net/#{bridge_name}/bridge").directory?
return nil unless bridge
# load interface names
interfaces = inspec.command("ls -1 /sys/class/net/#{bridge_name}/brif/")
interfaces = interfaces.stdout.chomp.split("\n")
{
name: bridge_name,
interfaces: interfaces,
}
end
end
# Windows Bridge
# select netadapter by adapter binding for windows
# Get-NetAdapterBinding -ComponentID ms_bridge | Get-NetAdapter
# @see https://technet.microsoft.com/en-us/library/jj130921(v=wps.630).aspx
# RegKeys: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}
class WindowsBridge < BridgeDetection
def bridge_info(bridge_name)
# find all bridge adapters
cmd = inspec.command('Get-NetAdapterBinding -ComponentID ms_bridge | Get-NetAdapter | Select-Object -Property Name, InterfaceDescription | ConvertTo-Json')
# filter network interface
begin
bridges = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
class BridgeDetection
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
# ensure we have an array of groups
bridges = [bridges] if !bridges.is_a?(Array)
# Linux Bridge
# If /sys/class/net/{interface}/bridge exists then it must be a bridge
# /sys/class/net/{interface}/brif contains the network interfaces
# @see http://www.tldp.org/HOWTO/BRIDGE-STP-HOWTO/set-up-the-bridge.html
# @see http://unix.stackexchange.com/questions/40560/how-to-know-if-a-network-interface-is-tap-tun-bridge-or-physical
class LinuxBridge < BridgeDetection
def bridge_info(bridge_name)
# read bridge information
bridge = inspec.file("/sys/class/net/#{bridge_name}/bridge").directory?
return nil unless bridge
# select the requested interface
bridges = bridges.each_with_object([]) do |adapter, adapter_collection|
# map object
info = {
name: adapter['Name'],
interfaces: nil,
# load interface names
interfaces = inspec.command("ls -1 /sys/class/net/#{bridge_name}/brif/")
interfaces = interfaces.stdout.chomp.split("\n")
{
name: bridge_name,
interfaces: interfaces,
}
adapter_collection.push(info) if info[:name].casecmp(bridge_name) == 0
end
end
return nil if bridges.size == 0
warn "[Possible Error] detected multiple bridges interfaces with the name #{bridge_name}" if bridges.size > 1
bridges[0]
# Windows Bridge
# select netadapter by adapter binding for windows
# Get-NetAdapterBinding -ComponentID ms_bridge | Get-NetAdapter
# @see https://technet.microsoft.com/en-us/library/jj130921(v=wps.630).aspx
# RegKeys: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}
class WindowsBridge < BridgeDetection
def bridge_info(bridge_name)
# find all bridge adapters
cmd = inspec.command('Get-NetAdapterBinding -ComponentID ms_bridge | Get-NetAdapter | Select-Object -Property Name, InterfaceDescription | ConvertTo-Json')
# filter network interface
begin
bridges = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
# ensure we have an array of groups
bridges = [bridges] if !bridges.is_a?(Array)
# select the requested interface
bridges = bridges.each_with_object([]) do |adapter, adapter_collection|
# map object
info = {
name: adapter['Name'],
interfaces: nil,
}
adapter_collection.push(info) if info[:name].casecmp(bridge_name) == 0
end
return nil if bridges.size == 0
warn "[Possible Error] detected multiple bridges interfaces with the name #{bridge_name}" if bridges.size > 1
bridges[0]
end
end
end

View file

@ -4,58 +4,60 @@
# author: Christoph Hartmann
# license: All rights reserved
class Cmd < Inspec.resource(1)
name 'command'
desc 'Use the command InSpec audit resource to test an arbitrary command that is run on the system.'
example "
describe command('ls -al /') do
it { should exist }
its(:stdout) { should match /bin/ }
its('stderr') { should eq '' }
its(:exit_status) { should eq 0 }
module Inspec::Resources
class Cmd < Inspec.resource(1)
name 'command'
desc 'Use the command InSpec audit resource to test an arbitrary command that is run on the system.'
example "
describe command('ls -al /') do
it { should exist }
its(:stdout) { should match /bin/ }
its('stderr') { should eq '' }
its(:exit_status) { should eq 0 }
end
"
attr_reader :command
def initialize(cmd)
@command = cmd
end
"
attr_reader :command
def initialize(cmd)
@command = cmd
end
def result
@result ||= inspec.backend.run_command(@command)
end
def stdout
result.stdout
end
def stderr
result.stderr
end
def exit_status
result.exit_status.to_i
end
def exist?
# silent for mock resources
return false if inspec.os[:family].to_s == 'unknown'
if inspec.os.linux?
res = inspec.backend.run_command("bash -c 'type \"#{@command}\"'")
elsif inspec.os.windows?
res = inspec.backend.run_command("where.exe \"#{@command}\"")
elsif inspec.os.unix?
res = inspec.backend.run_command("type \"#{@command}\"")
else
warn "`command(#{@command}).exist?` is not suported on your OS: #{inspec.os[:family]}"
return false
def result
@result ||= inspec.backend.run_command(@command)
end
res.exit_status.to_i == 0
end
def to_s
"Command #{@command}"
def stdout
result.stdout
end
def stderr
result.stderr
end
def exit_status
result.exit_status.to_i
end
def exist?
# silent for mock resources
return false if inspec.os[:family].to_s == 'unknown'
if inspec.os.linux?
res = inspec.backend.run_command("bash -c 'type \"#{@command}\"'")
elsif inspec.os.windows?
res = inspec.backend.run_command("where.exe \"#{@command}\"")
elsif inspec.os.unix?
res = inspec.backend.run_command("type \"#{@command}\"")
else
warn "`command(#{@command}).exist?` is not suported on your OS: #{inspec.os[:family]}"
return false
end
res.exit_status.to_i == 0
end
def to_s
"Command #{@command}"
end
end
end

View file

@ -5,29 +5,31 @@
# Parses a csv document
# This implementation was inspired by a blog post
# @see http://technicalpickles.com/posts/parsing-csv-with-ruby
class CsvConfig < JsonConfig
name 'csv'
desc 'Use the csv InSpec audit resource to test configuration data in a CSV file.'
example "
describe csv('example.csv') do
its('name') { should eq(['John', 'Alice']) }
end
"
module Inspec::Resources
class CsvConfig < JsonConfig
name 'csv'
desc 'Use the csv InSpec audit resource to test configuration data in a CSV file.'
example "
describe csv('example.csv') do
its('name') { should eq(['John', 'Alice']) }
end
"
# override file load and parse hash from csv
def parse(content)
require 'csv'
# convert empty field to nil
CSV::Converters[:blank_to_nil] = lambda do |field|
field && field.empty? ? nil : field
# override file load and parse hash from csv
def parse(content)
require 'csv'
# convert empty field to nil
CSV::Converters[:blank_to_nil] = lambda do |field|
field && field.empty? ? nil : field
end
# implicit conversion of values
csv = CSV.new(content, headers: true, converters: [:all, :blank_to_nil])
# convert to hash
csv.to_a.map(&:to_hash)
end
# implicit conversion of values
csv = CSV.new(content, headers: true, converters: [:all, :blank_to_nil])
# convert to hash
csv.to_a.map(&:to_hash)
end
def to_s
"Csv #{@path}"
def to_s
"Csv #{@path}"
end
end
end

View file

@ -24,135 +24,137 @@
require 'utils/convert'
require 'utils/parser'
class EtcGroup < Inspec.resource(1)
include Converter
include CommentParser
module Inspec::Resources
class EtcGroup < Inspec.resource(1)
include Converter
include CommentParser
name 'etc_group'
desc 'Use the etc_group InSpec audit resource to test groups that are defined on Linux and UNIX platforms. The /etc/group file stores details about each group---group name, password, group identifier, along with a comma-separate list of users that belong to the group.'
example "
describe etc_group do
its('gids') { should_not contain_duplicates }
its('groups') { should include 'my_user' }
its('users') { should include 'my_user' }
end
"
name 'etc_group'
desc 'Use the etc_group InSpec audit resource to test groups that are defined on Linux and UNIX platforms. The /etc/group file stores details about each group---group name, password, group identifier, along with a comma-separate list of users that belong to the group.'
example "
describe etc_group do
its('gids') { should_not contain_duplicates }
its('groups') { should include 'my_user' }
its('users') { should include 'my_user' }
end
"
attr_accessor :gid, :entries
def initialize(path = nil)
@path = path || '/etc/group'
@entries = parse_group(@path)
attr_accessor :gid, :entries
def initialize(path = nil)
@path = path || '/etc/group'
@entries = parse_group(@path)
# skip resource if it is not supported on current OS
return skip_resource 'The `etc_group` resource is not supported on your OS.' \
unless inspec.os.unix?
end
def groups(filter = nil)
entries = filter || @entries
entries.map { |x| x['name'] } if !entries.nil?
end
def gids(filter = nil)
entries = filter || @entries
entries.map { |x| x['gid'] } if !entries.nil?
end
def users(filter = nil)
entries = filter || @entries
return nil if entries.nil?
# filter the user entry
res = entries.map { |x|
x['members'].split(',') if !x.nil? && !x['members'].nil?
}.flatten
# filter nil elements
res.reject { |x| x.nil? || x.empty? }
end
def where(conditions = {})
return if conditions.empty?
fields = {
name: 'name',
group_name: 'name',
password: 'password',
gid: 'gid',
group_id: 'gid',
users: 'members',
members: 'members',
}
res = entries
conditions.each do |k, v|
idx = fields[k.to_sym]
next if idx.nil?
res = res.select { |x| x[idx] == v.to_s }
# skip resource if it is not supported on current OS
return skip_resource 'The `etc_group` resource is not supported on your OS.' \
unless inspec.os.unix?
end
EtcGroupView.new(self, res)
end
def to_s
'/etc/group'
end
private
def parse_group(path)
@content = inspec.file(path).content
if @content.nil?
skip_resource "Can't access group file in #{path}"
return []
def groups(filter = nil)
entries = filter || @entries
entries.map { |x| x['name'] } if !entries.nil?
end
# iterate over each line and filter comments
@content.split("\n").each_with_object([]) do |line, lines|
grp_info = parse_group_line(line)
lines.push(grp_info) if !grp_info.nil? && grp_info.size > 0
def gids(filter = nil)
entries = filter || @entries
entries.map { |x| x['gid'] } if !entries.nil?
end
def users(filter = nil)
entries = filter || @entries
return nil if entries.nil?
# filter the user entry
res = entries.map { |x|
x['members'].split(',') if !x.nil? && !x['members'].nil?
}.flatten
# filter nil elements
res.reject { |x| x.nil? || x.empty? }
end
def where(conditions = {})
return if conditions.empty?
fields = {
name: 'name',
group_name: 'name',
password: 'password',
gid: 'gid',
group_id: 'gid',
users: 'members',
members: 'members',
}
res = entries
conditions.each do |k, v|
idx = fields[k.to_sym]
next if idx.nil?
res = res.select { |x| x[idx] == v.to_s }
end
EtcGroupView.new(self, res)
end
def to_s
'/etc/group'
end
private
def parse_group(path)
@content = inspec.file(path).content
if @content.nil?
skip_resource "Can't access group file in #{path}"
return []
end
# iterate over each line and filter comments
@content.split("\n").each_with_object([]) do |line, lines|
grp_info = parse_group_line(line)
lines.push(grp_info) if !grp_info.nil? && grp_info.size > 0
end
end
def parse_group_line(line)
opts = {
comment_char: '#',
standalone_comments: false,
}
line, _idx_nl = parse_comment_line(line, opts)
x = line.split(':')
# abort if we have an empty or comment line
return nil if x.size == 0
# map data
{
'name' => x.at(0), # Name of the group.
'password' => x.at(1), # Group's encrypted password.
'gid' => convert_to_i(x.at(2)), # The group's decimal ID.
'members' => x.at(3), # Group members.
}
end
end
def parse_group_line(line)
opts = {
comment_char: '#',
standalone_comments: false,
}
line, _idx_nl = parse_comment_line(line, opts)
x = line.split(':')
# abort if we have an empty or comment line
return nil if x.size == 0
# map data
{
'name' => x.at(0), # Name of the group.
'password' => x.at(1), # Group's encrypted password.
'gid' => convert_to_i(x.at(2)), # The group's decimal ID.
'members' => x.at(3), # Group members.
}
end
end
# object that hold a specifc view on etc group
class EtcGroupView
def initialize(parent, filter)
@parent = parent
@filter = filter
end
# returns the group object
def entries
@filter
end
# only returns group name
def groups
@parent.groups(@filter)
end
# only return gids
def gids
@parent.gids(@filter)
end
# only returns users
def users
@parent.users(@filter)
# object that hold a specifc view on etc group
class EtcGroupView
def initialize(parent, filter)
@parent = parent
@filter = filter
end
# returns the group object
def entries
@filter
end
# only returns group name
def groups
@parent.groups(@filter)
end
# only return gids
def gids
@parent.gids(@filter)
end
# only returns users
def users
@parent.users(@filter)
end
end
end

View file

@ -2,47 +2,49 @@
# author: Christoph Hartmann
# author: Dominik Richter
class GemPackage < Inspec.resource(1)
name 'gem'
desc 'Use the gem InSpec audit resource to test if a global gem package is installed.'
example "
describe gem('rubocop') do
it { should be_installed }
module Inspec::Resources
class GemPackage < Inspec.resource(1)
name 'gem'
desc 'Use the gem InSpec audit resource to test if a global gem package is installed.'
example "
describe gem('rubocop') do
it { should be_installed }
end
"
def initialize(package_name)
@package_name = package_name
end
"
def initialize(package_name)
@package_name = package_name
end
def info
return @info if defined?(@info)
def info
return @info if defined?(@info)
cmd = inspec.command("gem list --local -a -q \^#{@package_name}\$")
@info = {
installed: cmd.exit_status == 0,
type: 'gem',
}
return @info unless @info[:installed]
cmd = inspec.command("gem list --local -a -q \^#{@package_name}\$")
@info = {
installed: cmd.exit_status == 0,
type: 'gem',
}
return @info unless @info[:installed]
# extract package name and version
# parses data like winrm (1.3.4, 1.3.3)
params = /^\s*([^\(]*?)\s*\((.*?)\)\s*$/.match(cmd.stdout.chomp)
versions = params[2].split(',')
@info[:name] = params[1]
@info[:version] = versions[0]
@info
end
# extract package name and version
# parses data like winrm (1.3.4, 1.3.3)
params = /^\s*([^\(]*?)\s*\((.*?)\)\s*$/.match(cmd.stdout.chomp)
versions = params[2].split(',')
@info[:name] = params[1]
@info[:version] = versions[0]
@info
end
def installed?
info[:installed] == true
end
def installed?
info[:installed] == true
end
def version
info[:version]
end
def version
info[:version]
end
def to_s
"gem package #{@package_name}"
def to_s
"gem package #{@package_name}"
end
end
end

View file

@ -13,123 +13,125 @@
# it { should have_gid 0 }
# end
class Group < Inspec.resource(1)
name 'group'
desc 'Use the group InSpec audit resource to test groups on the system.'
example "
describe group('root') do
it { should exist }
its('gid') { should eq 0 }
module Inspec::Resources
class Group < Inspec.resource(1)
name 'group'
desc 'Use the group InSpec audit resource to test groups on the system.'
example "
describe group('root') do
it { should exist }
its('gid') { should eq 0 }
end
"
def initialize(groupname, domain = nil)
@group = groupname.downcase
@domain = domain
@domain = @domain.downcase unless @domain.nil?
@cache = nil
# select group manager
@group_provider = nil
if inspec.os.unix?
@group_provider = UnixGroup.new(inspec)
elsif inspec.os.windows?
@group_provider = WindowsGroup.new(inspec)
else
return skip_resource 'The `group` resource is not supported on your OS yet.'
end
end
"
def initialize(groupname, domain = nil)
@group = groupname.downcase
@domain = domain
@domain = @domain.downcase unless @domain.nil?
# verifies if a group exists
def exists?
# ensure that we found one group
!group_info.nil? && group_info.size > 0
end
@cache = nil
def gid
return nil if group_info.nil? || group_info.size == 0
# select group manager
@group_provider = nil
if inspec.os.unix?
@group_provider = UnixGroup.new(inspec)
elsif inspec.os.windows?
@group_provider = WindowsGroup.new(inspec)
else
return skip_resource 'The `group` resource is not supported on your OS yet.'
# the default case should be one group
return group_info[0][:gid] if group_info.size == 1
# return array if we got multiple gids
group_info.map { |grp| grp[:gid] }
end
# implements rspec has matcher, to be compatible with serverspec
def has_gid?(compare_gid)
gid == compare_gid
end
def local
return nil if group_info.nil? || group_info.size == 0
# the default case should be one group
return group_info[0][:local] if group_info.size == 1
# return array if we got multiple gids
group_info.map { |grp| grp[:local] }
end
def to_s
"Group #{@group}"
end
private
def group_info
return @cache if !@cache.nil?
@cache = @group_provider.group_info(@group, @domain) if !@group_provider.nil?
end
end
# verifies if a group exists
def exists?
# ensure that we found one group
!group_info.nil? && group_info.size > 0
class GroupInfo
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
def gid
return nil if group_info.nil? || group_info.size == 0
# the default case should be one group
return group_info[0][:gid] if group_info.size == 1
# return array if we got multiple gids
group_info.map { |grp| grp[:gid] }
end
# implements rspec has matcher, to be compatible with serverspec
def has_gid?(compare_gid)
gid == compare_gid
end
def local
return nil if group_info.nil? || group_info.size == 0
# the default case should be one group
return group_info[0][:local] if group_info.size == 1
# return array if we got multiple gids
group_info.map { |grp| grp[:local] }
end
def to_s
"Group #{@group}"
end
private
def group_info
return @cache if !@cache.nil?
@cache = @group_provider.group_info(@group, @domain) if !@group_provider.nil?
end
end
class GroupInfo
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
# implements generic unix groups via /etc/group
class UnixGroup < GroupInfo
def group_info(group, _domain = nil)
inspec.etc_group.where(name: group).entries.map { |grp|
{
name: grp['name'],
gid: grp['gid'],
# implements generic unix groups via /etc/group
class UnixGroup < GroupInfo
def group_info(group, _domain = nil)
inspec.etc_group.where(name: group).entries.map { |grp|
{
name: grp['name'],
gid: grp['gid'],
}
}
}
end
end
class WindowsGroup < GroupInfo
def group_info(compare_group, compare_domain = nil)
cmd = inspec.command('Get-WmiObject Win32_Group | Select-Object -Property Caption, Domain, Name, SID, LocalAccount | ConvertTo-Json')
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0, try to parse json
begin
groups = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
end
# ensure we have an array of groups
groups = [groups] if !groups.is_a?(Array)
class WindowsGroup < GroupInfo
def group_info(compare_group, compare_domain = nil)
cmd = inspec.command('Get-WmiObject Win32_Group | Select-Object -Property Caption, Domain, Name, SID, LocalAccount | ConvertTo-Json')
# reduce list
groups.each_with_object([]) do |grp, grp_collection|
# map object
grp_info = {
name: grp['Name'],
domain: grp['Domain'],
caption: grp['Caption'],
gid: nil,
sid: grp['SID'],
local: grp['LocalAccount'],
}
return grp_collection.push(grp_info) if grp_info[:name].casecmp(compare_group) == 0 && (compare_domain.nil? || grp_info[:domain].casecmp(compare_domain) == 0)
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0, try to parse json
begin
groups = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
# ensure we have an array of groups
groups = [groups] if !groups.is_a?(Array)
# reduce list
groups.each_with_object([]) do |grp, grp_collection|
# map object
grp_info = {
name: grp['Name'],
domain: grp['Domain'],
caption: grp['Caption'],
gid: nil,
sid: grp['SID'],
local: grp['LocalAccount'],
}
return grp_collection.push(grp_info) if grp_info[:name].casecmp(compare_group) == 0 && (compare_domain.nil? || grp_info[:domain].casecmp(compare_domain) == 0)
end
end
end
end

View file

@ -24,126 +24,128 @@
# it { should be_resolvable.by('dns') }
# end
class Host < Inspec.resource(1)
name 'host'
desc 'Use the host InSpec audit resource to test the name used to refer to a specific host and its availability, including the Internet protocols and ports over which that host name should be available.'
example "
describe host('example.com') do
it { should be_reachable }
module Inspec::Resources
class Host < Inspec.resource(1)
name 'host'
desc 'Use the host InSpec audit resource to test the name used to refer to a specific host and its availability, including the Internet protocols and ports over which that host name should be available.'
example "
describe host('example.com') do
it { should be_reachable }
end
"
def initialize(hostname, params = {})
@hostname = hostname
@port = params[:port] || nil
@proto = params[:proto] || nil
@host_provider = nil
if inspec.os.linux?
@host_provider = LinuxHostProvider.new(inspec)
elsif inspec.os.windows?
@host_provider = WindowsHostProvider.new(inspec)
else
return skip_resource 'The `host` resource is not supported on your OS yet.'
end
end
"
def initialize(hostname, params = {})
@hostname = hostname
@port = params[:port] || nil
@proto = params[:proto] || nil
# if we get the IP adress, the host is resolvable
def resolvable?(type = nil)
warn "The `host` resource ignores #{type} parameters. Continue to resolve host." if !type.nil?
resolve.nil? || resolve.empty? ? false : true
end
@host_provider = nil
if inspec.os.linux?
@host_provider = LinuxHostProvider.new(inspec)
elsif inspec.os.windows?
@host_provider = WindowsHostProvider.new(inspec)
else
return skip_resource 'The `host` resource is not supported on your OS yet.'
def reachable?(port = nil, proto = nil, timeout = nil)
fail "Use `host` resource with host('#{@hostname}', port: #{port}, proto: '#{proto}') parameters." if !port.nil? || !proto.nil? || !timeout.nil?
ping.nil? ? false : ping
end
# returns all A records of the IP adress, will return an array
def ipaddress
resolve.nil? || resolve.empty? ? nil : resolve
end
def to_s
"Host #{@hostname}"
end
private
def ping
return @ping_cache if defined?(@ping_cache)
@ping_cache = @host_provider.ping(@hostname, @port, @proto) if !@host_provider.nil?
end
def resolve
return @ip_cache if defined?(@ip_cache)
@ip_cache = @host_provider.resolve(@hostname) if !@host_provider.nil?
end
end
# if we get the IP adress, the host is resolvable
def resolvable?(type = nil)
warn "The `host` resource ignores #{type} parameters. Continue to resolve host." if !type.nil?
resolve.nil? || resolve.empty? ? false : true
class HostProvider
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
def reachable?(port = nil, proto = nil, timeout = nil)
fail "Use `host` resource with host('#{@hostname}', port: #{port}, proto: '#{proto}') parameters." if !port.nil? || !proto.nil? || !timeout.nil?
ping.nil? ? false : ping
end
# returns all A records of the IP adress, will return an array
def ipaddress
resolve.nil? || resolve.empty? ? nil : resolve
end
def to_s
"Host #{@hostname}"
end
private
def ping
return @ping_cache if defined?(@ping_cache)
@ping_cache = @host_provider.ping(@hostname, @port, @proto) if !@host_provider.nil?
end
def resolve
return @ip_cache if defined?(@ip_cache)
@ip_cache = @host_provider.resolve(@hostname) if !@host_provider.nil?
end
end
class HostProvider
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
class LinuxHostProvider < HostProvider
# ping is difficult to achieve, since we are not sure
def ping(hostname, _port = nil, _proto = nil)
# fall back to ping, but we can only test ICMP packages with ping
# therefore we have to skip the test, if we do not have everything on the node to run the test
ping = inspec.command("ping -w 1 -c 1 #{hostname}")
ping.exit_status.to_i != 0 ? false : true
end
def resolve(hostname)
# TODO: we rely on getent hosts for now, but it prefers to return IPv6, only then IPv4
cmd = inspec.command("getent hosts #{hostname}")
return nil if cmd.exit_status.to_i != 0
# extract ip adress
resolve = /^\s*(?<ip>\S+)\s+(.*)\s*$/.match(cmd.stdout.chomp)
[resolve[1]] if resolve
end
end
# Windows
# TODO: UDP is not supported yey, we need a custom ps1 script to add udp support
# @see http://blogs.technet.com/b/josebda/archive/2015/04/18/windows-powershell-equivalents-for-common-networking-commands-ipconfig-ping-nslookup.aspx
# @see http://blogs.technet.com/b/heyscriptingguy/archive/2014/03/19/creating-a-port-scanner-with-windows-powershell.aspx
class WindowsHostProvider < HostProvider
def ping(hostname, port = nil, proto = nil)
# TODO: abort if we cannot run it via udp
return nil if proto == 'udp'
# ICMP: Test-NetConnection www.microsoft.com
# TCP and port: Test-NetConnection -ComputerName www.microsoft.com -RemotePort 80
request = "Test-NetConnection -ComputerName #{hostname}"
request += " -RemotePort #{port}" unless port.nil?
request += '| Select-Object -Property ComputerName, RemoteAddress, RemotePort, SourceAddress, PingSucceeded | ConvertTo-Json'
p request
request += '| Select-Object -Property ComputerName, PingSucceeded | ConvertTo-Json'
cmd = inspec.command(request)
begin
ping = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
class LinuxHostProvider < HostProvider
# ping is difficult to achieve, since we are not sure
def ping(hostname, _port = nil, _proto = nil)
# fall back to ping, but we can only test ICMP packages with ping
# therefore we have to skip the test, if we do not have everything on the node to run the test
ping = inspec.command("ping -w 1 -c 1 #{hostname}")
ping.exit_status.to_i != 0 ? false : true
end
ping['PingSucceeded']
def resolve(hostname)
# TODO: we rely on getent hosts for now, but it prefers to return IPv6, only then IPv4
cmd = inspec.command("getent hosts #{hostname}")
return nil if cmd.exit_status.to_i != 0
# extract ip adress
resolve = /^\s*(?<ip>\S+)\s+(.*)\s*$/.match(cmd.stdout.chomp)
[resolve[1]] if resolve
end
end
def resolve(hostname)
cmd = inspec.command("Resolve-DnsName Type A #{hostname} | ConvertTo-Json")
begin
resolv = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
# Windows
# TODO: UDP is not supported yey, we need a custom ps1 script to add udp support
# @see http://blogs.technet.com/b/josebda/archive/2015/04/18/windows-powershell-equivalents-for-common-networking-commands-ipconfig-ping-nslookup.aspx
# @see http://blogs.technet.com/b/heyscriptingguy/archive/2014/03/19/creating-a-port-scanner-with-windows-powershell.aspx
class WindowsHostProvider < HostProvider
def ping(hostname, port = nil, proto = nil)
# TODO: abort if we cannot run it via udp
return nil if proto == 'udp'
# ICMP: Test-NetConnection www.microsoft.com
# TCP and port: Test-NetConnection -ComputerName www.microsoft.com -RemotePort 80
request = "Test-NetConnection -ComputerName #{hostname}"
request += " -RemotePort #{port}" unless port.nil?
request += '| Select-Object -Property ComputerName, RemoteAddress, RemotePort, SourceAddress, PingSucceeded | ConvertTo-Json'
p request
request += '| Select-Object -Property ComputerName, PingSucceeded | ConvertTo-Json'
cmd = inspec.command(request)
begin
ping = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
ping['PingSucceeded']
end
resolv = [resolv] unless resolv.is_a?(Array)
resolv.map { |entry| entry['IPAddress'] }
def resolve(hostname)
cmd = inspec.command("Resolve-DnsName Type A #{hostname} | ConvertTo-Json")
begin
resolv = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
resolv = [resolv] unless resolv.is_a?(Array)
resolv.map { |entry| entry['IPAddress'] }
end
end
end

View file

@ -6,51 +6,53 @@
require 'utils/simpleconfig'
class InetdConf < Inspec.resource(1)
name 'inetd_conf'
desc 'Use the inetd_conf InSpec audit resource to test if a service is enabled in the inetd.conf file on Linux and UNIX platforms. inetd---the Internet service daemon---listens on dedicated ports, and then loads the appropriate program based on a request. The inetd.conf file is typically located at /etc/inetd.conf and contains a list of Internet services associated to the ports on which that service will listen. Only enabled services may handle a request; only services that are required by the system should be enabled.'
example "
describe inetd_conf do
its('shell') { should eq nil }
its('login') { should eq nil }
its('exec') { should eq nil }
end
"
module Inspec::Resources
class InetdConf < Inspec.resource(1)
name 'inetd_conf'
desc 'Use the inetd_conf InSpec audit resource to test if a service is enabled in the inetd.conf file on Linux and UNIX platforms. inetd---the Internet service daemon---listens on dedicated ports, and then loads the appropriate program based on a request. The inetd.conf file is typically located at /etc/inetd.conf and contains a list of Internet services associated to the ports on which that service will listen. Only enabled services may handle a request; only services that are required by the system should be enabled.'
example "
describe inetd_conf do
its('shell') { should eq nil }
its('login') { should eq nil }
its('exec') { should eq nil }
end
"
def initialize(path = nil)
@conf_path = path || '/etc/inetd.conf'
end
def method_missing(name)
read_params[name.to_s]
end
def read_params
return @params if defined?(@params)
# read the file
file = inspec.file(@conf_path)
if !file.file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
def initialize(path = nil)
@conf_path = path || '/etc/inetd.conf'
end
content = file.content
if content.empty? && file.size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
def method_missing(name)
read_params[name.to_s]
end
# parse the file
conf = SimpleConfig.new(
content,
assignment_re: /^\s*(\S+?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$/,
key_vals: 6,
multiple_values: false,
)
@params = conf.params
end
def to_s
'inetd.conf'
def read_params
return @params if defined?(@params)
# read the file
file = inspec.file(@conf_path)
if !file.file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
end
content = file.content
if content.empty? && file.size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
end
# parse the file
conf = SimpleConfig.new(
content,
assignment_re: /^\s*(\S+?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$/,
key_vals: 6,
multiple_values: false,
)
@params = conf.params
end
def to_s
'inetd.conf'
end
end
end

View file

@ -4,20 +4,22 @@
require 'utils/simpleconfig'
class IniConfig < JsonConfig
name 'ini'
desc 'Use the ini InSpec audit resource to test data in a INI file.'
example "
descibe ini do
its('auth_protocol') { should eq 'https' }
module Inspec::Resources
class IniConfig < JsonConfig
name 'ini'
desc 'Use the ini InSpec audit resource to test data in a INI file.'
example "
descibe ini do
its('auth_protocol') { should eq 'https' }
end
"
# override file load and parse hash with simple config
def parse(content)
SimpleConfig.new(content).params
end
"
# override file load and parse hash with simple config
def parse(content)
SimpleConfig.new(content).params
end
def to_s
"INI #{@path}"
def to_s
"INI #{@path}"
end
end
end

View file

@ -4,124 +4,126 @@
require 'utils/convert'
class NetworkInterface < Inspec.resource(1)
name 'interface'
desc 'Use the interface InSpec audit resource to test basic network adapter properties, such as name, status, state, address, and link speed (in MB/sec).'
example "
describe interface('eth0') do
it { should exist }
it { should be_up }
its(:speed) { should eq 1000 }
end
"
def initialize(iface)
@iface = iface
module Inspec::Resources
class NetworkInterface < Inspec.resource(1)
name 'interface'
desc 'Use the interface InSpec audit resource to test basic network adapter properties, such as name, status, state, address, and link speed (in MB/sec).'
example "
describe interface('eth0') do
it { should exist }
it { should be_up }
its(:speed) { should eq 1000 }
end
"
def initialize(iface)
@iface = iface
@interface_provider = nil
if inspec.os.linux?
@interface_provider = LinuxInterface.new(inspec)
elsif inspec.os.windows?
@interface_provider = WindowsInterface.new(inspec)
else
return skip_resource 'The `interface` resource is not supported on your OS yet.'
@interface_provider = nil
if inspec.os.linux?
@interface_provider = LinuxInterface.new(inspec)
elsif inspec.os.windows?
@interface_provider = WindowsInterface.new(inspec)
else
return skip_resource 'The `interface` resource is not supported on your OS yet.'
end
end
def exists?
!interface_info.nil? && !interface_info[:name].nil?
end
def up?
interface_info.nil? ? false : interface_info[:up]
end
# returns link speed in Mbits/sec
def speed
interface_info.nil? ? nil : interface_info[:speed]
end
def to_s
"Interface #{@iface}"
end
private
def interface_info
return @cache if defined?(@cache)
@cache = @interface_provider.interface_info(@iface) if !@interface_provider.nil?
end
end
def exists?
!interface_info.nil? && !interface_info[:name].nil?
end
def up?
interface_info.nil? ? false : interface_info[:up]
end
# returns link speed in Mbits/sec
def speed
interface_info.nil? ? nil : interface_info[:speed]
end
def to_s
"Interface #{@iface}"
end
private
def interface_info
return @cache if defined?(@cache)
@cache = @interface_provider.interface_info(@iface) if !@interface_provider.nil?
end
end
class InterfaceInfo
include Converter
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
class LinuxInterface < InterfaceInfo
def interface_info(iface)
# will return "[mtu]\n1500\n[type]\n1"
cmd = inspec.command("find /sys/class/net/#{iface}/ -type f -maxdepth 1 -exec sh -c 'echo \"[$(basename {})]\"; cat {} || echo -n' \\;")
return nil if cmd.exit_status.to_i != 0
# parse values, we only recieve values, therefore we threat them as keys
params = SimpleConfig.new(cmd.stdout.chomp).params
# abort if we got an empty result-set
return nil if params.empty?
# parse state
state = false
if params.key?('operstate')
operstate, _value = params['operstate'].first
state = operstate == 'up'
class InterfaceInfo
include Converter
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
# parse speed
speed = nil
if params.key?('speed')
speed, _value = params['speed'].first
speed = convert_to_i(speed)
end
{
name: iface,
up: state,
speed: speed,
}
end
end
class WindowsInterface < InterfaceInfo
def interface_info(iface)
# gather all network interfaces
cmd = inspec.command('Get-NetAdapter | Select-Object -Property Name, InterfaceDescription, Status, State, MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json')
class LinuxInterface < InterfaceInfo
def interface_info(iface)
# will return "[mtu]\n1500\n[type]\n1"
cmd = inspec.command("find /sys/class/net/#{iface}/ -type f -maxdepth 1 -exec sh -c 'echo \"[$(basename {})]\"; cat {} || echo -n' \\;")
return nil if cmd.exit_status.to_i != 0
# filter network interface
begin
net_adapter = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
# parse values, we only recieve values, therefore we threat them as keys
params = SimpleConfig.new(cmd.stdout.chomp).params
# ensure we have an array of groups
net_adapter = [net_adapter] if !net_adapter.is_a?(Array)
# abort if we got an empty result-set
return nil if params.empty?
# select the requested interface
adapters = net_adapter.each_with_object([]) do |adapter, adapter_collection|
# map object
info = {
name: adapter['Name'],
up: adapter['State'] == 2,
speed: adapter['ReceiveLinkSpeed'] / 1000,
# parse state
state = false
if params.key?('operstate')
operstate, _value = params['operstate'].first
state = operstate == 'up'
end
# parse speed
speed = nil
if params.key?('speed')
speed, _value = params['speed'].first
speed = convert_to_i(speed)
end
{
name: iface,
up: state,
speed: speed,
}
adapter_collection.push(info) if info[:name].casecmp(iface) == 0
end
end
return nil if adapters.size == 0
warn "[Possible Error] detected multiple network interfaces with the name #{iface}" if adapters.size > 1
adapters[0]
class WindowsInterface < InterfaceInfo
def interface_info(iface)
# gather all network interfaces
cmd = inspec.command('Get-NetAdapter | Select-Object -Property Name, InterfaceDescription, Status, State, MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json')
# filter network interface
begin
net_adapter = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
# ensure we have an array of groups
net_adapter = [net_adapter] if !net_adapter.is_a?(Array)
# select the requested interface
adapters = net_adapter.each_with_object([]) do |adapter, adapter_collection|
# map object
info = {
name: adapter['Name'],
up: adapter['State'] == 2,
speed: adapter['ReceiveLinkSpeed'] / 1000,
}
adapter_collection.push(info) if info[:name].casecmp(iface) == 0
end
return nil if adapters.size == 0
warn "[Possible Error] detected multiple network interfaces with the name #{iface}" if adapters.size > 1
adapters[0]
end
end
end

View file

@ -21,48 +21,50 @@
# @see http://ipset.netfilter.org/iptables.man.html
# @see http://ipset.netfilter.org/iptables.man.html
# @see https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html
class IpTables < Inspec.resource(1)
name 'iptables'
desc 'Use the iptables InSpec audit resource to test rules that are defined in iptables, which maintains tables of IP packet filtering rules. There may be more than one table. Each table contains one (or more) chains (both built-in and custom). A chain is a list of rules that match packets. When the rule matches, the rule defines what target to assign to the packet.'
example "
describe iptables do
it { should have_rule('-P INPUT ACCEPT') }
module Inspec::Resources
class IpTables < Inspec.resource(1)
name 'iptables'
desc 'Use the iptables InSpec audit resource to test rules that are defined in iptables, which maintains tables of IP packet filtering rules. There may be more than one table. Each table contains one (or more) chains (both built-in and custom). A chain is a list of rules that match packets. When the rule matches, the rule defines what target to assign to the packet.'
example "
describe iptables do
it { should have_rule('-P INPUT ACCEPT') }
end
"
def initialize(params = {})
@table = params[:table]
@chain = params[:chain]
# we're done if we are on linux
return if inspec.os.linux?
# ensures, all calls are aborted for non-supported os
@iptables_cache = []
skip_resource 'The `iptables` resource is not supported on your OS yet.'
end
"
def initialize(params = {})
@table = params[:table]
@chain = params[:chain]
def has_rule?(rule = nil, _table = nil, _chain = nil)
# checks if the rule is part of the ruleset
# for now, we expect an exact match
retrieve_rules.any? { |line| line.casecmp(rule) == 0 }
end
# we're done if we are on linux
return if inspec.os.linux?
def retrieve_rules
return @iptables_cache if defined?(@iptables_cache)
# ensures, all calls are aborted for non-supported os
@iptables_cache = []
skip_resource 'The `iptables` resource is not supported on your OS yet.'
end
# construct iptables command to read all rules
table_cmd = "-t #{@table}" if @table
iptables_cmd = format('iptables %s -S %s', table_cmd, @chain).strip
def has_rule?(rule = nil, _table = nil, _chain = nil)
# checks if the rule is part of the ruleset
# for now, we expect an exact match
retrieve_rules.any? { |line| line.casecmp(rule) == 0 }
end
cmd = inspec.command(iptables_cmd)
return [] if cmd.exit_status.to_i != 0
def retrieve_rules
return @iptables_cache if defined?(@iptables_cache)
# split rules, returns array or rules
@iptables_cache = cmd.stdout.split("\n").map(&:strip)
end
# construct iptables command to read all rules
table_cmd = "-t #{@table}" if @table
iptables_cmd = format('iptables %s -S %s', table_cmd, @chain).strip
cmd = inspec.command(iptables_cmd)
return [] if cmd.exit_status.to_i != 0
# split rules, returns array or rules
@iptables_cache = cmd.stdout.split("\n").map(&:strip)
end
def to_s
format('Iptables %s %s', @table && "table: #{@table}", @chain && "chain: #{@chain}").strip
def to_s
format('Iptables %s %s', @table && "table: #{@table}", @chain && "chain: #{@chain}").strip
end
end
end

View file

@ -2,81 +2,83 @@
# author: Christoph Hartmann
# author: Dominik Richter
class JsonConfig < Inspec.resource(1)
name 'json'
desc 'Use the json InSpec audit resource to test data in a JSON file.'
example "
describe json('policyfile.lock.json') do
its('cookbook_locks.omnibus.version') { should eq('2.2.0') }
end
"
module Inspec::Resources
class JsonConfig < Inspec.resource(1)
name 'json'
desc 'Use the json InSpec audit resource to test data in a JSON file.'
example "
describe json('policyfile.lock.json') do
its('cookbook_locks.omnibus.version') { should eq('2.2.0') }
end
"
# make params readable
attr_reader :params
# make params readable
attr_reader :params
def initialize(path)
@path = path
@file = inspec.file(@path)
@file_content = @file.content
def initialize(path)
@path = path
@file = inspec.file(@path)
@file_content = @file.content
# check if file is available
if !@file.file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
# check if file is available
if !@file.file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
end
# check if file is readable
if @file_content.empty? && @file.size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
end
@params = parse(@file_content)
end
# check if file is readable
if @file_content.empty? && @file.size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
def parse(content)
require 'json'
JSON.parse(content)
end
@params = parse(@file_content)
end
def parse(content)
require 'json'
JSON.parse(content)
end
def value(key)
extract_value(key, @params)
end
# Shorthand to retrieve a parameter name via `#its`.
# Example: describe json('file') { its('paramX') { should eq 'Y' } }
#
# @param [String] name name of the field to retrieve
# @return [Object] the value stored at this position
def method_missing(*keys)
# catch bahavior of rspec its implementation
# @see https://github.com/rspec/rspec-its/blob/master/lib/rspec/its.rb#L110
keys.shift if keys.is_a?(Array) && keys[0] == :[]
value(keys)
end
def to_s
"Json #{@path}"
end
private
def extract_value(keys, value)
key = keys.shift
return nil if key.nil?
# if value is an array, iterate over each child
if value.is_a?(Array)
value = value.map { |i|
extract_value([key], i)
}
else
value = value[key.to_s].nil? ? nil : value[key.to_s]
def value(key)
extract_value(key, @params)
end
# if there are no more keys, just return the value
return value if keys.first.nil?
# if there are more keys, extract more
extract_value(keys.clone, value)
# Shorthand to retrieve a parameter name via `#its`.
# Example: describe json('file') { its('paramX') { should eq 'Y' } }
#
# @param [String] name name of the field to retrieve
# @return [Object] the value stored at this position
def method_missing(*keys)
# catch bahavior of rspec its implementation
# @see https://github.com/rspec/rspec-its/blob/master/lib/rspec/its.rb#L110
keys.shift if keys.is_a?(Array) && keys[0] == :[]
value(keys)
end
def to_s
"Json #{@path}"
end
private
def extract_value(keys, value)
key = keys.shift
return nil if key.nil?
# if value is an array, iterate over each child
if value.is_a?(Array)
value = value.map { |i|
extract_value([key], i)
}
else
value = value[key.to_s].nil? ? nil : value[key.to_s]
end
# if there are no more keys, just return the value
return value if keys.first.nil?
# if there are more keys, extract more
extract_value(keys.clone, value)
end
end
end

View file

@ -3,39 +3,41 @@
# author: Dominik Richter
# license: All rights reserved
class KernelModule < Inspec.resource(1)
name 'kernel_module'
desc 'Use the kernel_module InSpec audit resource to test kernel modules on Linux platforms. These parameters are located under /lib/modules. Any submodule may be tested using this resource.'
example "
describe kernel_module('bridge') do
it { should be_loaded }
module Inspec::Resources
class KernelModule < Inspec.resource(1)
name 'kernel_module'
desc 'Use the kernel_module InSpec audit resource to test kernel modules on Linux platforms. These parameters are located under /lib/modules. Any submodule may be tested using this resource.'
example "
describe kernel_module('bridge') do
it { should be_loaded }
end
"
def initialize(modulename = nil)
@module = modulename
# this resource is only supported on Linux
return skip_resource 'The `kernel_parameter` resource is not supported on your OS.' if !inspec.os.linux?
end
"
def initialize(modulename = nil)
@module = modulename
def loaded?
# default lsmod command
lsmod_cmd = 'lsmod'
# special care for CentOS 5 and sudo
lsmod_cmd = '/sbin/lsmod' if inspec.os[:family] == 'centos' && inspec.os[:release].to_i == 5
# this resource is only supported on Linux
return skip_resource 'The `kernel_parameter` resource is not supported on your OS.' if !inspec.os.linux?
end
# get list of all modules
cmd = inspec.command(lsmod_cmd)
return false if cmd.exit_status != 0
def loaded?
# default lsmod command
lsmod_cmd = 'lsmod'
# special care for CentOS 5 and sudo
lsmod_cmd = '/sbin/lsmod' if inspec.os[:family] == 'centos' && inspec.os[:release].to_i == 5
# check if module is loaded
re = Regexp.new('^'+Regexp.quote(@module)+'\s')
found = cmd.stdout.match(re)
!found.nil?
end
# get list of all modules
cmd = inspec.command(lsmod_cmd)
return false if cmd.exit_status != 0
# check if module is loaded
re = Regexp.new('^'+Regexp.quote(@module)+'\s')
found = cmd.stdout.match(re)
!found.nil?
end
def to_s
"Kernel Module #{@module}"
def to_s
"Kernel Module #{@module}"
end
end
end

View file

@ -2,56 +2,58 @@
# author: Christoph Hartmann
# license: All rights reserved
class KernelParameter < Inspec.resource(1)
name 'kernel_parameter'
desc 'Use the kernel_parameter InSpec audit resource to test kernel parameters on Linux platforms.'
example "
describe kernel_parameter('net.ipv4.conf.all.forwarding') do
its(:value) { should eq 0 }
module Inspec::Resources
class KernelParameter < Inspec.resource(1)
name 'kernel_parameter'
desc 'Use the kernel_parameter InSpec audit resource to test kernel parameters on Linux platforms.'
example "
describe kernel_parameter('net.ipv4.conf.all.forwarding') do
its(:value) { should eq 0 }
end
"
def initialize(parameter = nil)
@parameter = parameter
# this resource is only supported on Linux
return skip_resource 'The `kernel_parameter` resource is not supported on your OS.' if !inspec.os.linux?
end
"
def initialize(parameter = nil)
@parameter = parameter
def value
cmd = inspec.command("/sbin/sysctl -q -n #{@parameter}")
return nil if cmd.exit_status != 0
# remove whitespace
cmd = cmd.stdout.chomp.strip
# convert to number if possible
cmd = cmd.to_i if cmd =~ /^\d+$/
cmd
end
# this resource is only supported on Linux
return skip_resource 'The `kernel_parameter` resource is not supported on your OS.' if !inspec.os.linux?
def to_s
"Kernel Parameter #{@parameter}"
end
end
def value
cmd = inspec.command("/sbin/sysctl -q -n #{@parameter}")
return nil if cmd.exit_status != 0
# remove whitespace
cmd = cmd.stdout.chomp.strip
# convert to number if possible
cmd = cmd.to_i if cmd =~ /^\d+$/
cmd
end
# for compatability with serverspec
# this is deprecated syntax and will be removed in future versions
class LinuxKernelParameter < KernelParameter
name 'linux_kernel_parameter'
def to_s
"Kernel Parameter #{@parameter}"
end
end
# for compatability with serverspec
# this is deprecated syntax and will be removed in future versions
class LinuxKernelParameter < KernelParameter
name 'linux_kernel_parameter'
def initialize(parameter)
super(parameter)
end
def value
deprecated
super()
end
def deprecated
warn '[DEPRECATION] `linux_kernel_parameter(parameter)` is deprecated. Please use `kernel_parameter(parameter)` instead.'
end
def to_s
"Kernel Parameter #{@parameter}"
def initialize(parameter)
super(parameter)
end
def value
deprecated
super()
end
def deprecated
warn '[DEPRECATION] `linux_kernel_parameter(parameter)` is deprecated. Please use `kernel_parameter(parameter)` instead.'
end
def to_s
"Kernel Parameter #{@parameter}"
end
end
end

View file

@ -6,50 +6,52 @@
require 'utils/simpleconfig'
class LimitsConf < Inspec.resource(1)
name 'limits_conf'
desc 'Use the limits_conf InSpec audit resource to test configuration settings in the /etc/security/limits.conf file. The limits.conf defines limits for processes (by user and/or group names) and helps ensure that the system on which those processes are running remains stable. Each process may be assigned a hard or soft limit.'
example "
describe limits_conf do
its('*') { should include ['hard','core','0'] }
end
"
module Inspec::Resources
class LimitsConf < Inspec.resource(1)
name 'limits_conf'
desc 'Use the limits_conf InSpec audit resource to test configuration settings in the /etc/security/limits.conf file. The limits.conf defines limits for processes (by user and/or group names) and helps ensure that the system on which those processes are running remains stable. Each process may be assigned a hard or soft limit.'
example "
describe limits_conf do
its('*') { should include ['hard','core','0'] }
end
"
def initialize(path = nil)
@conf_path = path || '/etc/security/limits.conf'
end
def method_missing(name)
read_params[name.to_s]
end
def read_params
return @params if defined?(@params)
# read the file
file = inspec.file(@conf_path)
if !file.file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
def initialize(path = nil)
@conf_path = path || '/etc/security/limits.conf'
end
content = file.content
if content.empty? && file.size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
def method_missing(name)
read_params[name.to_s]
end
# parse the file
conf = SimpleConfig.new(
content,
assignment_re: /^\s*(\S+?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$/,
key_vals: 3,
multiple_values: true,
)
@params = conf.params
end
def read_params
return @params if defined?(@params)
def to_s
'limits.conf'
# read the file
file = inspec.file(@conf_path)
if !file.file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
end
content = file.content
if content.empty? && file.size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
end
# parse the file
conf = SimpleConfig.new(
content,
assignment_re: /^\s*(\S+?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$/,
key_vals: 3,
multiple_values: true,
)
@params = conf.params
end
def to_s
'limits.conf'
end
end
end

View file

@ -18,49 +18,51 @@ require 'utils/simpleconfig'
# }
# end
class LoginDef < Inspec.resource(1)
name 'login_defs'
desc 'Use the login_defs InSpec audit resource to test configuration settings in the /etc/login.defs file. The logins.defs file defines site-specific configuration for the shadow password suite on Linux and UNIX platforms, such as password expiration ranges, minimum/maximum values for automatic selection of user and group identifiers, or the method with which passwords are encrypted.'
example "
describe login_defs do
its('ENCRYPT_METHOD') { should eq 'SHA512' }
end
"
module Inspec::Resources
class LoginDef < Inspec.resource(1)
name 'login_defs'
desc 'Use the login_defs InSpec audit resource to test configuration settings in the /etc/login.defs file. The logins.defs file defines site-specific configuration for the shadow password suite on Linux and UNIX platforms, such as password expiration ranges, minimum/maximum values for automatic selection of user and group identifiers, or the method with which passwords are encrypted.'
example "
describe login_defs do
its('ENCRYPT_METHOD') { should eq 'SHA512' }
end
"
def initialize(path = nil)
@conf_path = path || '/etc/login.defs'
end
def method_missing(name)
read_params[name.to_s]
end
def read_params
return @params if defined?(@params)
# read the file
file = inspec.file(@conf_path)
if !file.file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
def initialize(path = nil)
@conf_path = path || '/etc/login.defs'
end
content = file.content
if content.empty? && file.size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
def method_missing(name)
read_params[name.to_s]
end
# parse the file
conf = SimpleConfig.new(
content,
assignment_re: /^\s*(\S+)\s+(\S*)\s*$/,
multiple_values: false,
)
@params = conf.params
end
def read_params
return @params if defined?(@params)
def to_s
'login.defs'
# read the file
file = inspec.file(@conf_path)
if !file.file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
end
content = file.content
if content.empty? && file.size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
end
# parse the file
conf = SimpleConfig.new(
content,
assignment_re: /^\s*(\S+)\s+(\S*)\s*$/,
multiple_values: false,
)
@params = conf.params
end
def to_s
'login.defs'
end
end
end

View file

@ -4,54 +4,56 @@
require 'utils/simpleconfig'
class Mount < Inspec.resource(1)
name 'mount'
desc 'Use the mount InSpec audit resource to test if mount points.'
example "
describe mount('/') do
it { should be_mounted }
its(:count) { should eq 1 }
its('device') { should eq '/dev/mapper/VolGroup-lv_root' }
its('type') { should eq 'ext4' }
its('options') { should eq ['rw', 'mode=620'] }
module Inspec::Resources
class Mount < Inspec.resource(1)
name 'mount'
desc 'Use the mount InSpec audit resource to test if mount points.'
example "
describe mount('/') do
it { should be_mounted }
its(:count) { should eq 1 }
its('device') { should eq '/dev/mapper/VolGroup-lv_root' }
its('type') { should eq 'ext4' }
its('options') { should eq ['rw', 'mode=620'] }
end
"
include MountParser
attr_reader :file
def initialize(path)
@path = path
return skip_resource 'The `mount` resource is not supported on your OS yet.' if !inspec.os.linux?
@file = inspec.backend.file(@path)
end
"
include MountParser
attr_reader :file
def mounted?
file.mounted?
end
def initialize(path)
@path = path
return skip_resource 'The `mount` resource is not supported on your OS yet.' if !inspec.os.linux?
@file = inspec.backend.file(@path)
end
def count
mounted = file.mounted
return nil if mounted.nil? || mounted.stdout.nil?
mounted.stdout.lines.count
end
def mounted?
file.mounted?
end
def method_missing(name)
return nil if !file.mounted?
def count
mounted = file.mounted
return nil if mounted.nil? || mounted.stdout.nil?
mounted.stdout.lines.count
end
mounted = file.mounted
return nil if mounted.nil? || mounted.stdout.nil?
def method_missing(name)
return nil if !file.mounted?
line = mounted.stdout
# if we got multiple lines, only use the last entry
line = mounted.stdout.lines.to_a.last if mounted.stdout.lines.count > 1
mounted = file.mounted
return nil if mounted.nil? || mounted.stdout.nil?
# parse content if we are on linux
@mount_options ||= parse_mount_options(line)
@mount_options[name]
end
line = mounted.stdout
# if we got multiple lines, only use the last entry
line = mounted.stdout.lines.to_a.last if mounted.stdout.lines.count > 1
# parse content if we are on linux
@mount_options ||= parse_mount_options(line)
@mount_options[name]
end
def to_s
"Mount #{@path}"
def to_s
"Mount #{@path}"
end
end
end

View file

@ -4,78 +4,80 @@
# author: Christoph Hartmann
# license: All rights reserved
class Mysql < Inspec.resource(1)
name 'mysql'
module Inspec::Resources
class Mysql < Inspec.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
case inspec.os[:family]
when 'ubuntu', 'debian'
init_ubuntu
when 'redhat', 'fedora'
init_redhat
when 'arch'
init_arch
else
# TODO: could not detect
init_default
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
case inspec.os[:family]
when 'ubuntu', 'debian'
init_ubuntu
when 'redhat', 'fedora'
init_redhat
when 'arch'
init_arch
else
# TODO: could not detect
init_default
end
end
end
def init_ubuntu
@package = 'mysql-server'
@service = 'mysql'
@conf_path = '/etc/mysql/my.cnf'
@conf_dir = '/etc/mysql/'
@data_dir = '/var/lib/mysql/'
@log_dir = '/var/log/'
@log_path = '/var/log/mysql.log'
@log_group = 'adm'
case os[:release]
when '14.04'
@log_dir_group = 'syslog'
else
def init_ubuntu
@package = 'mysql-server'
@service = 'mysql'
@conf_path = '/etc/mysql/my.cnf'
@conf_dir = '/etc/mysql/'
@data_dir = '/var/lib/mysql/'
@log_dir = '/var/log/'
@log_path = '/var/log/mysql.log'
@log_group = 'adm'
case os[:release]
when '14.04'
@log_dir_group = 'syslog'
else
@log_dir_group = 'root'
end
end
def init_redhat
@package = 'mysql-server'
@service = 'mysqld'
@conf_path = '/etc/my.cnf'
@conf_dir = '/etc/'
@data_dir = '/var/lib/mysql/'
@log_dir = '/var/log/'
@log_path = '/var/log/mysqld.log'
@log_group = 'mysql'
@log_dir_group = 'root'
end
end
def init_redhat
@package = 'mysql-server'
@service = 'mysqld'
@conf_path = '/etc/my.cnf'
@conf_dir = '/etc/'
@data_dir = '/var/lib/mysql/'
@log_dir = '/var/log/'
@log_path = '/var/log/mysqld.log'
@log_group = 'mysql'
@log_dir_group = 'root'
end
def init_arch
@package = 'mariadb'
@service = 'mysql'
@conf_path = '/etc/mysql/my.cnf'
@conf_dir = '/etc/mysql/'
@data_dir = '/var/lib/mysql/'
@log_dir = '/var/log/'
@log_path = '/var/log/mysql.log'
@log_group = 'mysql'
@log_dir_group = 'root'
end
def init_arch
@package = 'mariadb'
@service = 'mysql'
@conf_path = '/etc/mysql/my.cnf'
@conf_dir = '/etc/mysql/'
@data_dir = '/var/lib/mysql/'
@log_dir = '/var/log/'
@log_path = '/var/log/mysql.log'
@log_group = 'mysql'
@log_dir_group = 'root'
end
def init_default
@service = 'mysqld'
@conf_path = '/etc/my.cnf'
@conf_dir = '/etc/'
@data_dir = '/var/lib/mysql/'
@log_dir = '/var/log/'
@log_path = '/var/log/mysqld.log'
@log_group = 'mysql'
@log_dir_group = 'root'
end
def init_default
@service = 'mysqld'
@conf_path = '/etc/my.cnf'
@conf_dir = '/etc/'
@data_dir = '/var/lib/mysql/'
@log_dir = '/var/log/'
@log_path = '/var/log/mysqld.log'
@log_group = 'mysql'
@log_dir_group = 'root'
end
def to_s
'MySQL'
def to_s
'MySQL'
end
end
end

View file

@ -8,115 +8,117 @@ require 'utils/find_files'
require 'utils/hash'
require 'resources/mysql'
class MysqlConfEntry
def initialize(path, params)
@params = params
@path = path
end
def method_missing(name, *_)
k = name.to_s
res = @params[k]
return true if res.nil? && @params.key?(k)
@params[k]
end
def to_s
"MySQL Config entry [#{@path.join(' ')}]"
end
end
class MysqlConf < Inspec.resource(1)
name 'mysql_conf'
desc 'Use the mysql_conf InSpec audit resource to test the contents of the configuration file for MySQL, typically located at /etc/mysql/my.cnf or /etc/my.cnf.'
example "
describe mysql_conf('path') do
its('setting') { should eq 'value' }
end
"
include FindFiles
def initialize(conf_path = nil)
@conf_path = conf_path || inspec.mysql.conf_path
@files_contents = {}
@content = nil
@params = nil
read_content
end
def content
@content ||= read_content
end
def params(*opts)
@params || read_content
res = @params
opts.each do |opt|
res = res[opt] unless res.nil?
end
MysqlConfEntry.new(opts, res)
end
def method_missing(name)
@params || read_content
@params[name.to_s]
end
def read_content
@content = ''
@params = {}
# skip if the main configuration file doesn't exist
if !inspec.file(@conf_path).file?
return skip_resource "Can't find file \"#{@conf_path}\""
end
raw_conf = read_file(@conf_path)
if raw_conf.empty? && inspec.file(@conf_path).size > 0
return skip_resource("Can't read file \"#{@conf_path}\"")
module Inspec::Resources
class MysqlConfEntry
def initialize(path, params)
@params = params
@path = path
end
to_read = [@conf_path]
until to_read.empty?
cur_file = to_read[0]
raw_conf = read_file(cur_file)
@content += raw_conf
def method_missing(name, *_)
k = name.to_s
res = @params[k]
return true if res.nil? && @params.key?(k)
@params[k]
end
params = SimpleConfig.new(raw_conf).params
@params = @params.deep_merge(params)
def to_s
"MySQL Config entry [#{@path.join(' ')}]"
end
end
to_read = to_read.drop(1)
# see if there is more stuff to include
dir = File.dirname(cur_file)
to_read += include_files(dir, raw_conf).find_all do |fp|
not @files_contents.key? fp
class MysqlConf < Inspec.resource(1)
name 'mysql_conf'
desc 'Use the mysql_conf InSpec audit resource to test the contents of the configuration file for MySQL, typically located at /etc/mysql/my.cnf or /etc/my.cnf.'
example "
describe mysql_conf('path') do
its('setting') { should eq 'value' }
end
"
include FindFiles
def initialize(conf_path = nil)
@conf_path = conf_path || inspec.mysql.conf_path
@files_contents = {}
@content = nil
@params = nil
read_content
end
#
@content
end
def include_files(reldir, conf)
files = conf.scan(/^!include\s+(.*)\s*/).flatten.compact.map { |x| abs_path(reldir, x) }
dirs = conf.scan(/^!includedir\s+(.*)\s*/).flatten.compact.map { |x| abs_path(reldir, x) }
dirs.map do |dir|
# @TODO: non local glob
files += find_files(dir, depth: 1, type: 'file')
def content
@content ||= read_content
end
files
end
def abs_path(dir, f)
return f if f.start_with? '/'
File.join(dir, f)
end
def params(*opts)
@params || read_content
res = @params
opts.each do |opt|
res = res[opt] unless res.nil?
end
MysqlConfEntry.new(opts, res)
end
def read_file(path)
@files_contents[path] ||= inspec.file(path).content
end
def method_missing(name)
@params || read_content
@params[name.to_s]
end
def to_s
'MySQL Configuration'
def read_content
@content = ''
@params = {}
# skip if the main configuration file doesn't exist
if !inspec.file(@conf_path).file?
return skip_resource "Can't find file \"#{@conf_path}\""
end
raw_conf = read_file(@conf_path)
if raw_conf.empty? && inspec.file(@conf_path).size > 0
return skip_resource("Can't read file \"#{@conf_path}\"")
end
to_read = [@conf_path]
until to_read.empty?
cur_file = to_read[0]
raw_conf = read_file(cur_file)
@content += raw_conf
params = SimpleConfig.new(raw_conf).params
@params = @params.deep_merge(params)
to_read = to_read.drop(1)
# see if there is more stuff to include
dir = ::File.dirname(cur_file)
to_read += include_files(dir, raw_conf).find_all do |fp|
not @files_contents.key? fp
end
end
#
@content
end
def include_files(reldir, conf)
files = conf.scan(/^!include\s+(.*)\s*/).flatten.compact.map { |x| abs_path(reldir, x) }
dirs = conf.scan(/^!includedir\s+(.*)\s*/).flatten.compact.map { |x| abs_path(reldir, x) }
dirs.map do |dir|
# @TODO: non local glob
files += find_files(dir, depth: 1, type: 'file')
end
files
end
def abs_path(dir, f)
return f if f.start_with? '/'
::File.join(dir, f)
end
def read_file(path)
@files_contents[path] ||= inspec.file(path).content
end
def to_s
'MySQL Configuration'
end
end
end

View file

@ -4,56 +4,58 @@
# author: Christoph Hartmann
# license: All rights reserved
class MysqlSession < Inspec.resource(1)
name 'mysql_session'
desc 'Use the mysql_session InSpec audit resource to test SQL commands run against a MySQL database.'
example "
sql = mysql_session('my_user','password')
describe sql.query('show databases like \'test\';') do
its(:stdout) { should_not match(/test/) }
end
"
module Inspec::Resources
class MysqlSession < Inspec.resource(1)
name 'mysql_session'
desc 'Use the mysql_session InSpec audit resource to test SQL commands run against a MySQL database.'
example "
sql = mysql_session('my_user','password')
describe sql.query('show databases like \'test\';') do
its(:stdout) { should_not match(/test/) }
end
"
def initialize(user = nil, pass = nil)
@user = user
@pass = pass
init_fallback if user.nil? or pass.nil?
skip_resource("Can't run MySQL SQL checks without authentication") if @user.nil? or @pass.nil?
end
def query(q, db = '')
# TODO: simple escape, must be handled by a library
# that does this securely
escaped_query = q.gsub(/\\/, '\\\\').gsub(/"/, '\\"').gsub(/\$/, '\\$')
# run the query
cmd = inspec.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/
# skip this test if the server can't run the query
skip_resource("Can't connect to MySQL instance for SQL checks.")
def initialize(user = nil, pass = nil)
@user = user
@pass = pass
init_fallback if user.nil? or pass.nil?
skip_resource("Can't run MySQL SQL checks without authentication") if @user.nil? or @pass.nil?
end
# return the raw command output
cmd
end
def query(q, db = '')
# TODO: simple escape, must be handled by a library
# that does this securely
escaped_query = q.gsub(/\\/, '\\\\').gsub(/"/, '\\"').gsub(/\$/, '\\$')
def to_s
'MySQL Session'
end
# run the query
cmd = inspec.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/
# skip this test if the server can't run the query
skip_resource("Can't connect to MySQL instance for SQL checks.")
end
private
# return the raw command output
cmd
end
def init_fallback
# support debian mysql administration login
debian = inspec.command('test -f /etc/mysql/debian.cnf && cat /etc/mysql/debian.cnf').stdout
return if debian.empty?
def to_s
'MySQL Session'
end
user = debian.match(/^\s*user\s*=\s*([^ ]*)\s*$/)
pass = debian.match(/^\s*password\s*=\s*([^ ]*)\s*$/)
return if user.nil? or pass.nil?
@user = user[1]
@pass = pass[1]
private
def init_fallback
# support debian mysql administration login
debian = inspec.command('test -f /etc/mysql/debian.cnf && cat /etc/mysql/debian.cnf').stdout
return if debian.empty?
user = debian.match(/^\s*user\s*=\s*([^ ]*)\s*$/)
pass = debian.match(/^\s*password\s*=\s*([^ ]*)\s*$/)
return if user.nil? or pass.nil?
@user = user[1]
@pass = pass[1]
end
end
end

View file

@ -2,45 +2,47 @@
# author: Christoph Hartmann
# author: Dominik Richter
class NpmPackage < Inspec.resource(1)
name 'npm'
desc 'Use the npm InSpec audit resource to test if a global npm package is installed. npm is the the package manager for Nodejs packages, such as bower and StatsD.'
example "
describe npm('bower') do
it { should be_installed }
module Inspec::Resources
class NpmPackage < Inspec.resource(1)
name 'npm'
desc 'Use the npm InSpec audit resource to test if a global npm package is installed. npm is the the package manager for Nodejs packages, such as bower and StatsD.'
example "
describe npm('bower') do
it { should be_installed }
end
"
def initialize(package_name)
@package_name = package_name
@cache = nil
end
"
def initialize(package_name)
@package_name = package_name
@cache = nil
end
def info
return @info if defined?(@info)
def info
return @info if defined?(@info)
cmd = inspec.command("npm ls -g --json #{@package_name}")
@info = {
name: @package_name,
type: 'npm',
installed: cmd.exit_status == 0,
}
return @info unless @info[:installed]
cmd = inspec.command("npm ls -g --json #{@package_name}")
@info = {
name: @package_name,
type: 'npm',
installed: cmd.exit_status == 0,
}
return @info unless @info[:installed]
pkgs = JSON.parse(cmd.stdout)
@info[:version] = pkgs['dependencies'][@package_name]['version']
@info
end
pkgs = JSON.parse(cmd.stdout)
@info[:version] = pkgs['dependencies'][@package_name]['version']
@info
end
def installed?
info[:installed] == true
end
def installed?
info[:installed] == true
end
def version
info[:version]
end
def version
info[:version]
end
def to_s
"Npm Package #{@package_name}"
def to_s
"Npm Package #{@package_name}"
end
end
end

View file

@ -6,53 +6,55 @@
require 'utils/simpleconfig'
class NtpConf < Inspec.resource(1)
name 'ntp_conf'
desc 'Use the ntp_conf InSpec audit resource to test the synchronization settings defined in the ntp.conf file. This file is typically located at /etc/ntp.conf.'
example "
describe ntp_conf do
its('server') { should_not eq nil }
its('restrict') { should include '-4 default kod notrap nomodify nopeer noquery'}
end
"
module Inspec::Resources
class NtpConf < Inspec.resource(1)
name 'ntp_conf'
desc 'Use the ntp_conf InSpec audit resource to test the synchronization settings defined in the ntp.conf file. This file is typically located at /etc/ntp.conf.'
example "
describe ntp_conf do
its('server') { should_not eq nil }
its('restrict') { should include '-4 default kod notrap nomodify nopeer noquery'}
end
"
def initialize(path = nil)
@conf_path = path || '/etc/ntp.conf'
end
def method_missing(name)
param = read_params[name.to_s]
# extract first value if we have only one value in array
return param[0] if param.is_a?(Array) and param.length == 1
param
end
def to_s
'ntp.conf'
end
private
def read_params
return @params if defined?(@params)
if !inspec.file(@conf_path).file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
def initialize(path = nil)
@conf_path = path || '/etc/ntp.conf'
end
content = inspec.file(@conf_path).content
if content.empty? && inspec.file(@conf_path).size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
def method_missing(name)
param = read_params[name.to_s]
# extract first value if we have only one value in array
return param[0] if param.is_a?(Array) and param.length == 1
param
end
# parse the file
conf = SimpleConfig.new(
content,
assignment_re: /^\s*(\S+)\s+(.*)\s*$/,
multiple_values: true,
)
@params = conf.params
def to_s
'ntp.conf'
end
private
def read_params
return @params if defined?(@params)
if !inspec.file(@conf_path).file?
skip_resource "Can't find file \"#{@conf_path}\""
return @params = {}
end
content = inspec.file(@conf_path).content
if content.empty? && inspec.file(@conf_path).size > 0
skip_resource "Can't read file \"#{@conf_path}\""
return @params = {}
end
# parse the file
conf = SimpleConfig.new(
content,
assignment_re: /^\s*(\S+)\s+(.*)\s*$/,
multiple_values: true,
)
@params = conf.params
end
end
end

View file

@ -9,61 +9,63 @@
# describe oneget('zoomit') do
# it { should be_installed }
# end
class OneGetPackage < Inspec.resource(1)
name 'oneget'
desc 'Use the oneget InSpec audit resource to test if the named package and/or package version is installed on the system. This resource uses OneGet, which is part of the Windows Management Framework 5.0 and Windows 10. This resource uses the Get-Package cmdlet to return all of the package names in the OneGet repository.'
example "
describe oneget('zoomit') do
it { should be_installed }
end
"
def initialize(package_name)
@package_name = package_name
# verify that this resource is only supported on Windows
return skip_resource 'The `oneget` resource is not supported on your OS.' if inspec.os[:family] != 'windows'
end
def info
return @info if defined?(@info)
@info = {}
@info[:type] = 'oneget'
@info[:installed] = false
cmd = inspec.command("Get-Package -Name '#{@package_name}' | ConvertTo-Json")
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0
# try to parse json
begin
pkgs = JSON.parse(cmd.stdout)
@info[:installed] = true
# sometimes we get multiple values
if pkgs.is_a?(Array)
# select the first entry
pkgs = pkgs.first
module Inspec::Resources
class OneGetPackage < Inspec.resource(1)
name 'oneget'
desc 'Use the oneget InSpec audit resource to test if the named package and/or package version is installed on the system. This resource uses OneGet, which is part of the Windows Management Framework 5.0 and Windows 10. This resource uses the Get-Package cmdlet to return all of the package names in the OneGet repository.'
example "
describe oneget('zoomit') do
it { should be_installed }
end
rescue JSON::ParserError => _e
return @info
"
def initialize(package_name)
@package_name = package_name
# verify that this resource is only supported on Windows
return skip_resource 'The `oneget` resource is not supported on your OS.' if inspec.os[:family] != 'windows'
end
@info[:name] = pkgs['Name'] if pkgs.key?('Name')
@info[:version] = pkgs['Version'] if pkgs.key?('Version')
@info
end
def info
return @info if defined?(@info)
def installed?
info[:installed] == true
end
@info = {}
@info[:type] = 'oneget'
@info[:installed] = false
def version
info[:version]
end
cmd = inspec.command("Get-Package -Name '#{@package_name}' | ConvertTo-Json")
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0
# try to parse json
def to_s
"OneGet Package #{@package_name}"
begin
pkgs = JSON.parse(cmd.stdout)
@info[:installed] = true
# sometimes we get multiple values
if pkgs.is_a?(Array)
# select the first entry
pkgs = pkgs.first
end
rescue JSON::ParserError => _e
return @info
end
@info[:name] = pkgs['Name'] if pkgs.key?('Name')
@info[:version] = pkgs['Version'] if pkgs.key?('Version')
@info
end
def installed?
info[:installed] == true
end
def version
info[:version]
end
def to_s
"OneGet Package #{@package_name}"
end
end
end

View file

@ -2,29 +2,31 @@
# author: Dominik Richter
# author: Christoph Hartmann
class OS < Inspec.resource(1)
name 'os'
desc 'Use the os InSpec audit resource to test the platform on which the system is running.'
example "
describe os[:family] do
it { should eq 'redhat' }
module Inspec::Resources
class OS < Inspec.resource(1)
name 'os'
desc 'Use the os InSpec audit resource to test the platform on which the system is running.'
example "
describe os[:family] do
it { should eq 'redhat' }
end
"
# reuse helper methods from backend
%w{aix? redhat? debian? suse? bsd? solaris? linux? unix? windows?}.each do |os_family|
define_method(os_family.to_sym) do
inspec.backend.os.send(os_family)
end
end
"
# reuse helper methods from backend
%w{aix? redhat? debian? suse? bsd? solaris? linux? unix? windows?}.each do |os_family|
define_method(os_family.to_sym) do
inspec.backend.os.send(os_family)
def [](name)
# convert string to symbol
name = name.to_sym if name.is_a? String
inspec.backend.os[name]
end
end
def [](name)
# convert string to symbol
name = name.to_sym if name.is_a? String
inspec.backend.os[name]
end
def to_s
'Operating System Detection'
def to_s
'Operating System Detection'
end
end
end

View file

@ -13,60 +13,62 @@
require 'utils/simpleconfig'
class OsEnv < Inspec.resource(1)
name 'os_env'
desc 'Use the os_env InSpec audit resource to test the environment variables for the platform on which the system is running.'
example "
describe os_env('VARIABLE') do
its('matcher') { should eq 1 }
end
"
module Inspec::Resources
class OsEnv < Inspec.resource(1)
name 'os_env'
desc 'Use the os_env InSpec audit resource to test the environment variables for the platform on which the system is running.'
example "
describe os_env('VARIABLE') do
its('matcher') { should eq 1 }
end
"
attr_reader :content
def initialize(env = nil)
@osenv = env
@content = nil
@content = value_for(env) unless env.nil?
end
def split
# we can't take advantage of `File::PATH_SEPARATOR` as code is
# evaluated on the host machine
path_separator = inspec.os.windows? ? ';' : ':'
# -1 is required to catch cases like dir1::dir2:
# where we have a trailing :
@content.nil? ? [] : @content.split(path_separator, -1)
end
def to_s
if @osenv.nil?
'Environment variables'
else
"Environment variable #{@osenv}"
end
end
private
def value_for(env)
command = if inspec.os.windows?
"$Env:#{env}"
else
'env'
end
out = inspec.command(command)
unless out.exit_status == 0
skip_resource "Can't read environment variables on #{os[:family]}. "\
"Tried `#{command}` which returned #{out.exit_status}"
attr_reader :content
def initialize(env = nil)
@osenv = env
@content = nil
@content = value_for(env) unless env.nil?
end
if inspec.os.windows?
out.stdout.strip
else
params = SimpleConfig.new(out.stdout).params
params[env]
def split
# we can't take advantage of `File::PATH_SEPARATOR` as code is
# evaluated on the host machine
path_separator = inspec.os.windows? ? ';' : ':'
# -1 is required to catch cases like dir1::dir2:
# where we have a trailing :
@content.nil? ? [] : @content.split(path_separator, -1)
end
def to_s
if @osenv.nil?
'Environment variables'
else
"Environment variable #{@osenv}"
end
end
private
def value_for(env)
command = if inspec.os.windows?
"$Env:#{env}"
else
'env'
end
out = inspec.command(command)
unless out.exit_status == 0
skip_resource "Can't read environment variables on #{os[:family]}. "\
"Tried `#{command}` which returned #{out.exit_status}"
end
if inspec.os.windows?
out.stdout.strip
else
params = SimpleConfig.new(out.stdout).params
params[env]
end
end
end
end

View file

@ -8,253 +8,255 @@
# describe package('nginx') do
# it { should be_installed }
# end
class Package < Inspec.resource(1)
name 'package'
desc 'Use the package InSpec audit resource to test if the named package and/or package version is installed on the system.'
example "
describe package('nginx') do
it { should be_installed }
its('version') { should eq 1.9.5 }
module Inspec::Resources
class Package < Inspec.resource(1)
name 'package'
desc 'Use the package InSpec audit resource to test if the named package and/or package version is installed on the system.'
example "
describe package('nginx') do
it { should be_installed }
its('version') { should eq 1.9.5 }
end
"
def initialize(package_name = nil) # rubocop:disable Metrics/AbcSize
@package_name = package_name
@name = @package_name
@cache = nil
# select package manager
@pkgman = nil
os = inspec.os
if os.debian?
@pkgman = Deb.new(inspec)
elsif os.redhat? || os.suse?
@pkgman = Rpm.new(inspec)
elsif ['arch'].include?(os[:family])
@pkgman = Pacman.new(inspec)
elsif ['darwin'].include?(os[:family])
@pkgman = Brew.new(inspec)
elsif inspec.os.windows?
@pkgman = WindowsPkg.new(inspec)
elsif ['aix'].include?(os[:family])
@pkgman = BffPkg.new(inspec)
elsif os.solaris?
@pkgman = SolarisPkg.new(inspec)
else
return skip_resource 'The `package` resource is not supported on your OS yet.'
end
end
"
def initialize(package_name = nil) # rubocop:disable Metrics/AbcSize
@package_name = package_name
@name = @package_name
@cache = nil
# select package manager
@pkgman = nil
# returns true if the package is installed
def installed?(_provider = nil, _version = nil)
return false if info.nil?
info[:installed] == true
end
os = inspec.os
if os.debian?
@pkgman = Deb.new(inspec)
elsif os.redhat? || os.suse?
@pkgman = Rpm.new(inspec)
elsif ['arch'].include?(os[:family])
@pkgman = Pacman.new(inspec)
elsif ['darwin'].include?(os[:family])
@pkgman = Brew.new(inspec)
elsif inspec.os.windows?
@pkgman = WindowsPkg.new(inspec)
elsif ['aix'].include?(os[:family])
@pkgman = BffPkg.new(inspec)
elsif os.solaris?
@pkgman = SolarisPkg.new(inspec)
else
return skip_resource 'The `package` resource is not supported on your OS yet.'
# returns the package description
def info
return @cache if !@cache.nil?
return nil if @pkgman.nil?
@pkgman.info(@package_name)
end
# return the package version
def version
info = @pkgman.info(@package_name)
return nil if info.nil?
info[:version]
end
def to_s
"System Package #{@package_name}"
end
end
# returns true if the package is installed
def installed?(_provider = nil, _version = nil)
return false if info.nil?
info[:installed] == true
end
# returns the package description
def info
return @cache if !@cache.nil?
return nil if @pkgman.nil?
@pkgman.info(@package_name)
end
# return the package version
def version
info = @pkgman.info(@package_name)
return nil if info.nil?
info[:version]
end
def to_s
"System Package #{@package_name}"
end
end
class PkgManagement
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
# Debian / Ubuntu
class Deb < PkgManagement
def info(package_name)
cmd = inspec.command("dpkg -s #{package_name}")
return nil if cmd.exit_status.to_i != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
{
name: params['Package'],
installed: true,
version: params['Version'],
type: 'deb',
}
end
end
# RHEL family
class Rpm < PkgManagement
def info(package_name)
cmd = inspec.command("rpm -qia #{package_name}")
# CentOS does not return an error code if the package is not installed,
# therefore we need to check for emptyness
return nil if cmd.exit_status.to_i != 0 || cmd.stdout.chomp.empty?
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
# On some (all?) systems, the linebreak before the vendor line is missing
if params['Version'] =~ /\s*Vendor:/
v = params['Version'].split(' ')[0]
else
v = params['Version']
end
# On some (all?) systems, the linebreak before the build line is missing
if params['Release'] =~ /\s*Build Date:/
r = params['Release'].split(' ')[0]
else
r = params['Release']
end
{
name: params['Name'],
installed: true,
version: "#{v}-#{r}",
type: 'rpm',
}
end
end
# MacOS / Darwin implementation
class Brew < PkgManagement
def info(package_name)
cmd = inspec.command("brew info --json=v1 #{package_name}")
return nil if cmd.exit_status.to_i != 0
# parse data
pkg = JSON.parse(cmd.stdout)[0]
{
name: pkg.name.to_s,
installed: true,
version: pkg.installed.version.to_s,
type: 'brew',
}
end
end
# Arch Linux
class Pacman < PkgManagement
def info(package_name)
cmd = inspec.command("pacman -Qi #{package_name}")
return nil if cmd.exit_status.to_i != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
{
name: params['Name'],
installed: true,
version: params['Version'],
type: 'pacman',
}
end
end
# Determines the installed packages on Windows
# Currently we use 'Get-WmiObject -Class Win32_Product' as a detection method
# TODO: evaluate if alternative methods as proposed by Microsoft are still valid:
# @see: http://blogs.technet.com/b/heyscriptingguy/archive/2013/11/15/use-powershell-to-find-installed-software.aspx
class WindowsPkg < PkgManagement
def info(package_name)
# Find the package
cmd = inspec.command("Get-WmiObject -Class Win32_Product | Where-Object {$_.Name -eq '#{package_name}'} | Select-Object -Property Name,Version,Vendor,PackageCode,Caption,Description | ConvertTo-Json")
begin
package = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
{
name: package['Name'],
installed: true,
version: package['Version'],
type: 'windows',
}
end
end
# AIX
class BffPkg < PkgManagement
def info(package_name)
cmd = inspec.command("lslpp -cL #{package_name}")
return nil if cmd.exit_status.to_i != 0
bff_pkg = cmd.stdout.split("\n").last.split(':')
{
name: bff_pkg[1],
installed: true,
version: bff_pkg[2],
type: 'bff',
}
end
end
# Solaris
class SolarisPkg < PkgManagement
def info(package_name)
if inspec.os[:release].to_i <= 10
solaris10_info(package_name)
else
solaris11_info(package_name)
class PkgManagement
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
# solaris 10
def solaris10_info(package_name)
cmd = inspec.command("pkginfo -l #{package_name}")
return nil if cmd.exit_status.to_i != 0
# Debian / Ubuntu
class Deb < PkgManagement
def info(package_name)
cmd = inspec.command("dpkg -s #{package_name}")
return nil if cmd.exit_status.to_i != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
# parse 11.10.0,REV=2006.05.18.01.46
v = params['VERSION'].split(',')
{
name: params['PKGINST'],
installed: true,
version: v[0] + '-' + v[1].split('=')[1],
type: 'pkg',
}
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
{
name: params['Package'],
installed: true,
version: params['Version'],
type: 'deb',
}
end
end
# solaris 11
def solaris11_info(package_name)
cmd = inspec.command("pkg info #{package_name}")
return nil if cmd.exit_status.to_i != 0
# RHEL family
class Rpm < PkgManagement
def info(package_name)
cmd = inspec.command("rpm -qia #{package_name}")
# CentOS does not return an error code if the package is not installed,
# therefore we need to check for emptyness
return nil if cmd.exit_status.to_i != 0 || cmd.stdout.chomp.empty?
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
# On some (all?) systems, the linebreak before the vendor line is missing
if params['Version'] =~ /\s*Vendor:/
v = params['Version'].split(' ')[0]
else
v = params['Version']
end
# On some (all?) systems, the linebreak before the build line is missing
if params['Release'] =~ /\s*Build Date:/
r = params['Release'].split(' ')[0]
else
r = params['Release']
end
{
name: params['Name'],
installed: true,
version: "#{v}-#{r}",
type: 'rpm',
}
end
end
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
# MacOS / Darwin implementation
class Brew < PkgManagement
def info(package_name)
cmd = inspec.command("brew info --json=v1 #{package_name}")
return nil if cmd.exit_status.to_i != 0
# parse data
pkg = JSON.parse(cmd.stdout)[0]
{
name: pkg.name.to_s,
installed: true,
version: pkg.installed.version.to_s,
type: 'brew',
}
end
end
{
name: params['Name'],
installed: true,
# 0.5.11-0.175.3.1.0.5.0
version: "#{params['Version']}-#{params['Branch']}",
type: 'pkg',
}
# Arch Linux
class Pacman < PkgManagement
def info(package_name)
cmd = inspec.command("pacman -Qi #{package_name}")
return nil if cmd.exit_status.to_i != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
{
name: params['Name'],
installed: true,
version: params['Version'],
type: 'pacman',
}
end
end
# Determines the installed packages on Windows
# Currently we use 'Get-WmiObject -Class Win32_Product' as a detection method
# TODO: evaluate if alternative methods as proposed by Microsoft are still valid:
# @see: http://blogs.technet.com/b/heyscriptingguy/archive/2013/11/15/use-powershell-to-find-installed-software.aspx
class WindowsPkg < PkgManagement
def info(package_name)
# Find the package
cmd = inspec.command("Get-WmiObject -Class Win32_Product | Where-Object {$_.Name -eq '#{package_name}'} | Select-Object -Property Name,Version,Vendor,PackageCode,Caption,Description | ConvertTo-Json")
begin
package = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
{
name: package['Name'],
installed: true,
version: package['Version'],
type: 'windows',
}
end
end
# AIX
class BffPkg < PkgManagement
def info(package_name)
cmd = inspec.command("lslpp -cL #{package_name}")
return nil if cmd.exit_status.to_i != 0
bff_pkg = cmd.stdout.split("\n").last.split(':')
{
name: bff_pkg[1],
installed: true,
version: bff_pkg[2],
type: 'bff',
}
end
end
# Solaris
class SolarisPkg < PkgManagement
def info(package_name)
if inspec.os[:release].to_i <= 10
solaris10_info(package_name)
else
solaris11_info(package_name)
end
end
# solaris 10
def solaris10_info(package_name)
cmd = inspec.command("pkginfo -l #{package_name}")
return nil if cmd.exit_status.to_i != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
# parse 11.10.0,REV=2006.05.18.01.46
v = params['VERSION'].split(',')
{
name: params['PKGINST'],
installed: true,
version: v[0] + '-' + v[1].split('=')[1],
type: 'pkg',
}
end
# solaris 11
def solaris11_info(package_name)
cmd = inspec.command("pkg info #{package_name}")
return nil if cmd.exit_status.to_i != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
{
name: params['Name'],
installed: true,
# 0.5.11-0.175.3.1.0.5.0
version: "#{params['Version']}-#{params['Branch']}",
type: 'pkg',
}
end
end
end

View file

@ -13,77 +13,79 @@
# }
# describe parse_config(audit, options ) do
class PConfig < Inspec.resource(1)
name 'parse_config'
desc 'Use the parse_config InSpec audit resource to test arbitrary configuration files.'
example "
output = command('some-command').stdout
module Inspec::Resources
class PConfig < Inspec.resource(1)
name 'parse_config'
desc 'Use the parse_config InSpec audit resource to test arbitrary configuration files.'
example "
output = command('some-command').stdout
describe parse_config(output, { data_config_option: value } ) do
its('setting') { should eq 1 }
end
"
describe parse_config(output, { data_config_option: value } ) do
its('setting') { should eq 1 }
end
"
def initialize(content = nil, useropts = nil)
@opts = {}
@opts = useropts.dup unless useropts.nil?
@files_contents = {}
@params = nil
def initialize(content = nil, useropts = nil)
@opts = {}
@opts = useropts.dup unless useropts.nil?
@files_contents = {}
@params = nil
@content = content
read_content if @content.nil?
end
def method_missing(name)
@params || read_content
@params[name.to_s]
end
def parse_file(conf_path)
@conf_path = conf_path
# read the file
if !inspec.file(conf_path).file?
return skip_resource "Can't find file \"#{conf_path}\""
end
@content = read_file(conf_path)
if @content.empty? && inspec.file(conf_path).size > 0
return skip_resource "Can't read file \"#{conf_path}\""
@content = content
read_content if @content.nil?
end
read_content
def method_missing(name)
@params || read_content
@params[name.to_s]
end
def parse_file(conf_path)
@conf_path = conf_path
# read the file
if !inspec.file(conf_path).file?
return skip_resource "Can't find file \"#{conf_path}\""
end
@content = read_file(conf_path)
if @content.empty? && inspec.file(conf_path).size > 0
return skip_resource "Can't read file \"#{conf_path}\""
end
read_content
end
def read_file(path)
@files_contents[path] ||= inspec.file(path).content
end
def read_content
# parse the file
@params = SimpleConfig.new(@content, @opts).params
@content
end
def to_s
"Parse Config #{@conf_path}"
end
end
def read_file(path)
@files_contents[path] ||= inspec.file(path).content
end
class PConfigFile < PConfig
name 'parse_config_file'
desc 'Use the parse_config_file InSpec audit resource to test arbitrary configuration files. It works identiacal to parse_config. Instead of using a command output, this resource works with files.'
example "
describe parse_config_file('/path/to/file') do
its('setting') { should eq 1 }
end
"
def read_content
# parse the file
@params = SimpleConfig.new(@content, @opts).params
@content
end
def initialize(path, opts = nil)
super(nil, opts)
parse_file(path)
end
def to_s
"Parse Config #{@conf_path}"
end
end
class PConfigFile < PConfig
name 'parse_config_file'
desc 'Use the parse_config_file InSpec audit resource to test arbitrary configuration files. It works identiacal to parse_config. Instead of using a command output, this resource works with files.'
example "
describe parse_config_file('/path/to/file') do
its('setting') { should eq 1 }
end
"
def initialize(path, opts = nil)
super(nil, opts)
parse_file(path)
end
def to_s
"Parse Config File #{@conf_path}"
def to_s
"Parse Config File #{@conf_path}"
end
end
end

View file

@ -15,112 +15,114 @@
require 'utils/parser'
class Passwd < Inspec.resource(1)
name 'passwd'
desc 'Use the passwd InSpec audit resource to test the contents of /etc/passwd, which contains the following information for users that may log into the system and/or as users that own running processes.'
example "
describe passwd do
its('users') { should_not include 'forbidden_user' }
module Inspec::Resources
class Passwd < Inspec.resource(1)
name 'passwd'
desc 'Use the passwd InSpec audit resource to test the contents of /etc/passwd, which contains the following information for users that may log into the system and/or as users that own running processes.'
example "
describe passwd do
its('users') { should_not include 'forbidden_user' }
end
describe passwd.uids(0) do
its('users') { should cmp 'root' }
its('count') { should eq 1 }
end
describe passwd.shells(/nologin/) do
# find all users with a nologin shell
its('users') { should_not include 'my_login_user' }
end
"
include PasswdParser
attr_reader :uid
attr_reader :params
attr_reader :content
attr_reader :lines
def initialize(path = nil, opts = nil)
opts ||= {}
@path = path || '/etc/passwd'
@content = opts[:content] || inspec.file(@path).content
@lines = @content.to_s.split("\n")
@filters = opts[:filters] || ''
@params = parse_passwd(@content)
end
describe passwd.uids(0) do
its('users') { should cmp 'root' }
its('count') { should eq 1 }
end
describe passwd.shells(/nologin/) do
# find all users with a nologin shell
its('users') { should_not include 'my_login_user' }
end
"
include PasswdParser
attr_reader :uid
attr_reader :params
attr_reader :content
attr_reader :lines
def initialize(path = nil, opts = nil)
opts ||= {}
@path = path || '/etc/passwd'
@content = opts[:content] || inspec.file(@path).content
@lines = @content.to_s.split("\n")
@filters = opts[:filters] || ''
@params = parse_passwd(@content)
end
def filter(hm = {})
return self if hm.nil? || hm.empty?
res = @params
filters = ''
hm.each do |attr, condition|
condition = condition.to_s if condition.is_a? Integer
filters += " #{attr} = #{condition.inspect}"
res = res.find_all do |line|
case line[attr.to_s]
when condition
true
else
false
def filter(hm = {})
return self if hm.nil? || hm.empty?
res = @params
filters = ''
hm.each do |attr, condition|
condition = condition.to_s if condition.is_a? Integer
filters += " #{attr} = #{condition.inspect}"
res = res.find_all do |line|
case line[attr.to_s]
when condition
true
else
false
end
end
end
content = res.map { |x| x.values.join(':') }.join("\n")
Passwd.new(@path, content: content, filters: @filters + filters)
end
content = res.map { |x| x.values.join(':') }.join("\n")
Passwd.new(@path, content: content, filters: @filters + filters)
end
def usernames
warn '[DEPRECATION] `passwd.usernames` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
users
end
def usernames
warn '[DEPRECATION] `passwd.usernames` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
users
end
def username
warn '[DEPRECATION] `passwd.user` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
users[0]
end
def username
warn '[DEPRECATION] `passwd.user` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
users[0]
end
def uid(x)
warn '[DEPRECATION] `passwd.uid(arg)` is deprecated. Please use `passwd.uids(arg)` instead. It will be removed in version 1.0.0.'
uids(x)
end
def uid(x)
warn '[DEPRECATION] `passwd.uid(arg)` is deprecated. Please use `passwd.uids(arg)` instead. It will be removed in version 1.0.0.'
uids(x)
end
def users(name = nil)
name.nil? ? map_data('user') : filter(user: name)
end
def users(name = nil)
name.nil? ? map_data('user') : filter(user: name)
end
def passwords(password = nil)
password.nil? ? map_data('password') : filter(password: password)
end
def passwords(password = nil)
password.nil? ? map_data('password') : filter(password: password)
end
def uids(uid = nil)
uid.nil? ? map_data('uid') : filter(uid: uid)
end
def uids(uid = nil)
uid.nil? ? map_data('uid') : filter(uid: uid)
end
def gids(gid = nil)
gid.nil? ? map_data('gid') : filter(gid: gid)
end
def gids(gid = nil)
gid.nil? ? map_data('gid') : filter(gid: gid)
end
def homes(home = nil)
home.nil? ? map_data('home') : filter(home: home)
end
def homes(home = nil)
home.nil? ? map_data('home') : filter(home: home)
end
def shells(shell = nil)
shell.nil? ? map_data('shell') : filter(shell: shell)
end
def shells(shell = nil)
shell.nil? ? map_data('shell') : filter(shell: shell)
end
def to_s
f = @filters.empty? ? '' : ' with'+@filters
"/etc/passwd#{f}"
end
def to_s
f = @filters.empty? ? '' : ' with'+@filters
"/etc/passwd#{f}"
end
def count
@params.length
end
def count
@params.length
end
private
private
def map_data(id)
@params.map { |x| x[id] }
def map_data(id)
@params.map { |x| x[id] }
end
end
end

View file

@ -7,75 +7,77 @@
# it { should be_installed }
# end
#
class PipPackage < Inspec.resource(1)
name 'pip'
desc 'Use the pip InSpec audit resource to test packages that are installed using the pip installer.'
example "
describe pip('Jinja2') do
it { should be_installed }
end
"
def initialize(package_name)
@package_name = package_name
end
def info
return @info if defined?(@info)
@info = {}
@info[:type] = 'pip'
cmd = inspec.command("#{pip_cmd} show #{@package_name}")
return @info if cmd.exit_status != 0
params = SimpleConfig.new(
cmd.stdout,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
@info[:name] = params['Name']
@info[:version] = params['Version']
@info[:installed] = true
@info
end
def installed?
info[:installed] == true
end
def version
info[:version]
end
def to_s
"Pip Package #{@package_name}"
end
private
def pip_cmd
# Pip is not on the default path for Windows, therefore we do some logic
# to find the binary on Windows
family = inspec.os[:family]
case family
when 'windows'
# we need to detect the pip command on Windows
cmd = inspec.command('New-Object -Type PSObject | Add-Member -MemberType NoteProperty -Name Pip -Value (Invoke-Command -ScriptBlock {where.exe pip}) -PassThru | Add-Member -MemberType NoteProperty -Name Python -Value (Invoke-Command -ScriptBlock {where.exe python}) -PassThru | ConvertTo-Json')
begin
paths = JSON.parse(cmd.stdout)
# use pip if it on system path
pipcmd = paths['Pip']
# calculate path on windows
if defined?(paths['Python']) && pipcmd.nil?
pipdir = paths['Python'].split('\\')
# remove python.exe
pipdir.pop
pipcmd = pipdir.push('Scripts').push('pip.exe').join('/')
end
rescue JSON::ParserError => _e
return nil
module Inspec::Resources
class PipPackage < Inspec.resource(1)
name 'pip'
desc 'Use the pip InSpec audit resource to test packages that are installed using the pip installer.'
example "
describe pip('Jinja2') do
it { should be_installed }
end
"
def initialize(package_name)
@package_name = package_name
end
def info
return @info if defined?(@info)
@info = {}
@info[:type] = 'pip'
cmd = inspec.command("#{pip_cmd} show #{@package_name}")
return @info if cmd.exit_status != 0
params = SimpleConfig.new(
cmd.stdout,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
).params
@info[:name] = params['Name']
@info[:version] = params['Version']
@info[:installed] = true
@info
end
def installed?
info[:installed] == true
end
def version
info[:version]
end
def to_s
"Pip Package #{@package_name}"
end
private
def pip_cmd
# Pip is not on the default path for Windows, therefore we do some logic
# to find the binary on Windows
family = inspec.os[:family]
case family
when 'windows'
# we need to detect the pip command on Windows
cmd = inspec.command('New-Object -Type PSObject | Add-Member -MemberType NoteProperty -Name Pip -Value (Invoke-Command -ScriptBlock {where.exe pip}) -PassThru | Add-Member -MemberType NoteProperty -Name Python -Value (Invoke-Command -ScriptBlock {where.exe python}) -PassThru | ConvertTo-Json')
begin
paths = JSON.parse(cmd.stdout)
# use pip if it on system path
pipcmd = paths['Pip']
# calculate path on windows
if defined?(paths['Python']) && pipcmd.nil?
pipdir = paths['Python'].split('\\')
# remove python.exe
pipdir.pop
pipcmd = pipdir.push('Scripts').push('pip.exe').join('/')
end
rescue JSON::ParserError => _e
return nil
end
end
pipcmd || 'pip'
end
pipcmd || 'pip'
end
end

View file

@ -17,413 +17,415 @@ require 'utils/parser'
#
# TODO: currently we return local ip only
# TODO: improve handling of same port on multiple interfaces
class Port < Inspec.resource(1)
name 'port'
desc "Use the port InSpec audit resource to test basic port properties, such as port, process, if it's listening."
example "
describe port(80) do
it { should be_listening }
its('protocols') {should eq ['tcp']}
end
"
module Inspec::Resources
class Port < Inspec.resource(1)
name 'port'
desc "Use the port InSpec audit resource to test basic port properties, such as port, process, if it's listening."
example "
describe port(80) do
it { should be_listening }
its('protocols') {should eq ['tcp']}
end
"
def initialize(ip = nil, port) # rubocop:disable OptionalArguments
@ip = ip
@port = port
@port_manager = nil
@cache = nil
os = inspec.os
if os.linux?
@port_manager = LinuxPorts.new(inspec)
elsif %w{darwin aix}.include?(os[:family])
# AIX: see http://www.ibm.com/developerworks/aix/library/au-lsof.html#resources
# and https://www-01.ibm.com/marketing/iwm/iwm/web/reg/pick.do?source=aixbp
# Darwin: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/lsof.8.html
@port_manager = LsofPorts.new(inspec)
elsif os.windows?
@port_manager = WindowsPorts.new(inspec)
elsif ['freebsd'].include?(os[:family])
@port_manager = FreeBsdPorts.new(inspec)
elsif os.solaris?
@port_manager = SolarisPorts.new(inspec)
else
return skip_resource 'The `port` resource is not supported on your OS yet.'
def initialize(ip = nil, port) # rubocop:disable OptionalArguments
@ip = ip
@port = port
@port_manager = nil
@cache = nil
os = inspec.os
if os.linux?
@port_manager = LinuxPorts.new(inspec)
elsif %w{darwin aix}.include?(os[:family])
# AIX: see http://www.ibm.com/developerworks/aix/library/au-lsof.html#resources
# and https://www-01.ibm.com/marketing/iwm/iwm/web/reg/pick.do?source=aixbp
# Darwin: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/lsof.8.html
@port_manager = LsofPorts.new(inspec)
elsif os.windows?
@port_manager = WindowsPorts.new(inspec)
elsif ['freebsd'].include?(os[:family])
@port_manager = FreeBsdPorts.new(inspec)
elsif os.solaris?
@port_manager = SolarisPorts.new(inspec)
else
return skip_resource 'The `port` resource is not supported on your OS yet.'
end
end
def listening?(_protocol = nil, _local_address = nil)
info.size > 0
end
def protocols
res = info.map { |x| x[:protocol] }.uniq.compact
res.size > 0 ? res : nil
end
def processes
res = info.map { |x| x[:process] }.uniq.compact
res.size > 0 ? res : nil
end
def pids
res = info.map { |x| x[:pid] }.uniq.compact
res.size > 0 ? res : nil
end
def to_s
"Port #{@port}"
end
private
def info
return @cache if !@cache.nil?
# abort if os detection has not worked
return @cache = [] if @port_manager.nil?
# query ports
ports = @port_manager.info || []
@cache = ports.select { |p| p[:port] == @port && (!@ip || p[:address] == @ip) }
end
end
def listening?(_protocol = nil, _local_address = nil)
info.size > 0
end
def protocols
res = info.map { |x| x[:protocol] }.uniq.compact
res.size > 0 ? res : nil
end
def processes
res = info.map { |x| x[:process] }.uniq.compact
res.size > 0 ? res : nil
end
def pids
res = info.map { |x| x[:pid] }.uniq.compact
res.size > 0 ? res : nil
end
def to_s
"Port #{@port}"
end
private
def info
return @cache if !@cache.nil?
# abort if os detection has not worked
return @cache = [] if @port_manager.nil?
# query ports
ports = @port_manager.info || []
@cache = ports.select { |p| p[:port] == @port && (!@ip || p[:address] == @ip) }
end
end
# implements an info method and returns all ip adresses and protocols for
# each port
# [{
# port: 22,
# address: '0.0.0.0'
# protocol: 'tcp'
# },
# {
# port: 22,
# address: '::'
# protocol: 'tcp6'
# }]
class PortsInfo
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
# TODO: Add UDP infromation Get-NetUDPEndpoint
# TODO: currently Windows only supports tcp ports
# TODO: Get-NetTCPConnection does not return PIDs
# TODO: double-check output with 'netstat -ano'
# @see https://connect.microsoft.com/PowerShell/feedback/details/1349420/get-nettcpconnection-does-not-show-processid
class WindowsPorts < PortsInfo
def info
# get all port information
cmd = inspec.command('Get-NetTCPConnection | Select-Object -Property State, Caption, Description, LocalAddress, LocalPort, RemoteAddress, RemotePort, DisplayName, Status | ConvertTo-Json')
begin
ports = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
# implements an info method and returns all ip adresses and protocols for
# each port
# [{
# port: 22,
# address: '0.0.0.0'
# protocol: 'tcp'
# },
# {
# port: 22,
# address: '::'
# protocol: 'tcp6'
# }]
class PortsInfo
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
return nil if ports.nil?
# TODO: Add UDP infromation Get-NetUDPEndpoint
# TODO: currently Windows only supports tcp ports
# TODO: Get-NetTCPConnection does not return PIDs
# TODO: double-check output with 'netstat -ano'
# @see https://connect.microsoft.com/PowerShell/feedback/details/1349420/get-nettcpconnection-does-not-show-processid
class WindowsPorts < PortsInfo
def info
# get all port information
cmd = inspec.command('Get-NetTCPConnection | Select-Object -Property State, Caption, Description, LocalAddress, LocalPort, RemoteAddress, RemotePort, DisplayName, Status | ConvertTo-Json')
ports.map { |x|
{
port: x['LocalPort'],
address: x['LocalAddress'],
protocol: 'tcp',
process: nil,
pid: nil,
begin
ports = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
return nil if ports.nil?
ports.map { |x|
{
port: x['LocalPort'],
address: x['LocalAddress'],
protocol: 'tcp',
process: nil,
pid: nil,
}
}
}
end
end
# extracts udp and tcp ports from the lsof command
class LsofPorts < PortsInfo
attr_reader :lsof
def initialize(inspec, lsofpath = nil)
@lsof = lsofpath || 'lsof'
super(inspec)
end
end
def info
ports = []
# extracts udp and tcp ports from the lsof command
class LsofPorts < PortsInfo
attr_reader :lsof
# check that lsof is available, otherwise fail
fail 'Please ensure `lsof` is available on the machine.' if !inspec.command(@lsof.to_s).exist?
# -F p=pid, c=command, P=protocol name, t=type, n=internet addresses
# see 'OUTPUT FOR OTHER PROGRAMS' in LSOF(8)
lsof_cmd = inspec.command("#{@lsof} -nP -i -FpctPn")
return nil if lsof_cmd.exit_status.to_i != 0
# map to desired return struct
lsof_parser(lsof_cmd).each do |process, port_ids|
pid, cmd = process.split(':')
port_ids.each do |port_str|
# should not break on ipv6 addresses
ipv, proto, port, host = port_str.split(':', 4)
ports.push({ port: port.to_i,
address: host,
protocol: ipv == 'ipv6' ? proto + '6' : proto,
process: cmd,
pid: pid.to_i })
end
def initialize(inspec, lsofpath = nil)
@lsof = lsofpath || 'lsof'
super(inspec)
end
ports
end
def info
ports = []
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
def lsof_parser(lsof_cmd)
procs = {}
# build this with formatted output (-F) from lsof
# procs = {
# '123:sshd' => [
# 'ipv4:tcp:22:127.0.0.1',
# 'ipv6:tcp:22:::1',
# 'ipv4:tcp:*',
# 'ipv6:tcp:*',
# ],
# '456:ntpd' => [
# 'ipv4:udp:123:*',
# 'ipv6:udp:123:*',
# ]
# }
proc_id = port_id = nil
lsof_cmd.stdout.each_line do |line|
line.chomp!
key = line.slice!(0)
case key
when 'p'
proc_id = line
port_id = nil
when 'c'
proc_id += ':' + line
when 't'
port_id = line.downcase
when 'P'
port_id += ':' + line.downcase
when 'n'
src, dst = line.split('->')
# check that lsof is available, otherwise fail
fail 'Please ensure `lsof` is available on the machine.' if !inspec.command(@lsof.to_s).exist?
# skip active comm streams
next if dst
# -F p=pid, c=command, P=protocol name, t=type, n=internet addresses
# see 'OUTPUT FOR OTHER PROGRAMS' in LSOF(8)
lsof_cmd = inspec.command("#{@lsof} -nP -i -FpctPn")
return nil if lsof_cmd.exit_status.to_i != 0
host, port = /^(\S+):(\d+|\*)$/.match(src)[1, 2]
# skip channels from port 0 - what does this mean?
next if port == '*'
# create new array stub if !exist?
procs[proc_id] = [] unless procs.key?(proc_id)
# change address '*' to zero
host = (port_id =~ /^ipv6:/) ? '[::]' : '0.0.0.0' if host == '*'
# entrust URI to scrub the host and port
begin
uri = URI("addr://#{host}:#{port}")
uri.host && uri.port
rescue => e
warn "could not parse URI 'addr://#{host}:#{port}' - #{e}"
next
# map to desired return struct
lsof_parser(lsof_cmd).each do |process, port_ids|
pid, cmd = process.split(':')
port_ids.each do |port_str|
# should not break on ipv6 addresses
ipv, proto, port, host = port_str.split(':', 4)
ports.push({ port: port.to_i,
address: host,
protocol: ipv == 'ipv6' ? proto + '6' : proto,
process: cmd,
pid: pid.to_i })
end
# e.g. 'ipv4:tcp:22:127.0.0.1'
# strip ipv6 squares for inspec
port_id += ':' + port + ':' + host.gsub(/^\[|\]$/, '')
# lsof will give us another port unless it's done
procs[proc_id] << port_id
end
ports
end
procs
end
end
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
def lsof_parser(lsof_cmd)
procs = {}
# build this with formatted output (-F) from lsof
# procs = {
# '123:sshd' => [
# 'ipv4:tcp:22:127.0.0.1',
# 'ipv6:tcp:22:::1',
# 'ipv4:tcp:*',
# 'ipv6:tcp:*',
# ],
# '456:ntpd' => [
# 'ipv4:udp:123:*',
# 'ipv6:udp:123:*',
# ]
# }
proc_id = port_id = nil
lsof_cmd.stdout.each_line do |line|
line.chomp!
key = line.slice!(0)
case key
when 'p'
proc_id = line
port_id = nil
when 'c'
proc_id += ':' + line
when 't'
port_id = line.downcase
when 'P'
port_id += ':' + line.downcase
when 'n'
src, dst = line.split('->')
# extract port information from netstat
class LinuxPorts < PortsInfo
def info
cmd = inspec.command('netstat -tulpen')
return nil if cmd.exit_status.to_i != 0
# skip active comm streams
next if dst
ports = []
# parse all lines
cmd.stdout.each_line do |line|
port_info = parse_netstat_line(line)
host, port = /^(\S+):(\d+|\*)$/.match(src)[1, 2]
# only push protocols we are interested in
next unless %w{tcp tcp6 udp udp6}.include?(port_info[:protocol])
ports.push(port_info)
# skip channels from port 0 - what does this mean?
next if port == '*'
# create new array stub if !exist?
procs[proc_id] = [] unless procs.key?(proc_id)
# change address '*' to zero
host = (port_id =~ /^ipv6:/) ? '[::]' : '0.0.0.0' if host == '*'
# entrust URI to scrub the host and port
begin
uri = URI("addr://#{host}:#{port}")
uri.host && uri.port
rescue => e
warn "could not parse URI 'addr://#{host}:#{port}' - #{e}"
next
end
# e.g. 'ipv4:tcp:22:127.0.0.1'
# strip ipv6 squares for inspec
port_id += ':' + port + ':' + host.gsub(/^\[|\]$/, '')
# lsof will give us another port unless it's done
procs[proc_id] << port_id
end
end
procs
end
ports
end
def parse_net_address(net_addr, protocol)
if protocol.eql?('tcp6') || protocol.eql?('udp6')
# prep for URI parsing, parse ip6 port
ip6 = /^(\S+):(\d+)$/.match(net_addr)
ip6addr = ip6[1]
ip6addr = '::' if ip6addr =~ /^:::$/
# build uri
ip_addr = URI("addr://[#{ip6addr}]:#{ip6[2]}")
# replace []
host = ip_addr.host[1..ip_addr.host.size-2]
else
ip_addr = URI('addr://'+net_addr)
host = ip_addr.host
# extract port information from netstat
class LinuxPorts < PortsInfo
def info
cmd = inspec.command('netstat -tulpen')
return nil if cmd.exit_status.to_i != 0
ports = []
# parse all lines
cmd.stdout.each_line do |line|
port_info = parse_netstat_line(line)
# only push protocols we are interested in
next unless %w{tcp tcp6 udp udp6}.include?(port_info[:protocol])
ports.push(port_info)
end
ports
end
port = ip_addr.port
def parse_net_address(net_addr, protocol)
if protocol.eql?('tcp6') || protocol.eql?('udp6')
# prep for URI parsing, parse ip6 port
ip6 = /^(\S+):(\d+)$/.match(net_addr)
ip6addr = ip6[1]
ip6addr = '::' if ip6addr =~ /^:::$/
# build uri
ip_addr = URI("addr://[#{ip6addr}]:#{ip6[2]}")
# replace []
host = ip_addr.host[1..ip_addr.host.size-2]
else
ip_addr = URI('addr://'+net_addr)
host = ip_addr.host
end
[host, port]
rescue URI::InvalidURIError => e
warn "Could not parse #{net_addr}, #{e}"
nil
end
def parse_netstat_line(line)
# parse each line
# 1 - Proto, 2 - Recv-Q, 3 - Send-Q, 4 - Local Address, 5 - Foreign Address, 6 - State, 7 - Inode, 8 - PID/Program name
parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)?\s+(\S+)\s+(\S+)\s+(\S+)/.match(line)
return {} if parsed.nil? || line.match(/^proto/i)
# parse ip4 and ip6 addresses
protocol = parsed[1].downcase
# detect protocol if not provided
protocol += '6' if parsed[4].count(':') > 1 && %w{tcp udp}.include?(protocol)
# extract host and port information
host, port = parse_net_address(parsed[4], protocol)
# extract PID
process = parsed[9].split('/')
pid = process[0]
pid = pid.to_i if pid =~ /^\d+$/
process = process[1]
# map data
{
port: port,
address: host,
protocol: protocol,
process: process,
pid: pid,
}
end
end
# extracts information from sockstat
class FreeBsdPorts < PortsInfo
def info
cmd = inspec.command('sockstat -46l')
return nil if cmd.exit_status.to_i != 0
ports = []
# split on each newline
cmd.stdout.each_line do |line|
port_info = parse_sockstat_line(line)
# push data, if not headerfile
next unless %w{tcp tcp6 udp udp6}.include?(port_info[:protocol])
ports.push(port_info)
end
ports
end
def parse_net_address(net_addr, protocol)
case protocol
when 'tcp4', 'udp4', 'tcp', 'udp'
# replace * with 0.0.0.0
net_addr = net_addr.gsub(/^\*:/, '0.0.0.0:') if net_addr =~ /^*:(\d+)$/
ip_addr = URI('addr://'+net_addr)
host = ip_addr.host
port = ip_addr.port
when 'tcp6', 'udp6'
return [] if net_addr == '*:*' # abort for now
# replace * with 0:0:0:0:0:0:0:0
net_addr = net_addr.gsub(/^\*:/, '0:0:0:0:0:0:0:0:') if net_addr =~ /^*:(\d+)$/
# extract port
ip6 = /^(\S+):(\d+)$/.match(net_addr)
ip6addr = ip6[1]
ip_addr = URI("addr://[#{ip6addr}]:#{ip6[2]}")
# replace []
host = ip_addr.host[1..ip_addr.host.size-2]
port = ip_addr.port
[host, port]
rescue URI::InvalidURIError => e
warn "Could not parse #{net_addr}, #{e}"
nil
end
[host, port]
rescue URI::InvalidURIError => e
warn "Could not parse #{net_addr}, #{e}"
nil
end
def parse_sockstat_line(line)
# 1 - USER, 2 - COMMAND, 3 - PID, 4 - FD 5 - PROTO, 6 - LOCAL ADDRESS, 7 - FOREIGN ADDRESS
parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/.match(line)
return {} if parsed.nil?
def parse_netstat_line(line)
# parse each line
# 1 - Proto, 2 - Recv-Q, 3 - Send-Q, 4 - Local Address, 5 - Foreign Address, 6 - State, 7 - Inode, 8 - PID/Program name
parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)?\s+(\S+)\s+(\S+)\s+(\S+)/.match(line)
# extract ip information
protocol = parsed[5].downcase
host, port = parse_net_address(parsed[6], protocol)
return {} if host.nil? or port.nil?
return {} if parsed.nil? || line.match(/^proto/i)
# extract process
process = parsed[2]
# parse ip4 and ip6 addresses
protocol = parsed[1].downcase
# extract PID
pid = parsed[3]
pid = pid.to_i if pid =~ /^\d+$/
# detect protocol if not provided
protocol += '6' if parsed[4].count(':') > 1 && %w{tcp udp}.include?(protocol)
# map tcp4 and udp4
protocol = 'tcp' if protocol.eql?('tcp4')
protocol = 'udp' if protocol.eql?('udp4')
# extract host and port information
host, port = parse_net_address(parsed[4], protocol)
# map data
{
port: port,
address: host,
protocol: protocol,
process: process,
pid: pid,
}
end
end
# extract PID
process = parsed[9].split('/')
pid = process[0]
pid = pid.to_i if pid =~ /^\d+$/
process = process[1]
class SolarisPorts < FreeBsdPorts
include SolarisNetstatParser
def info
# extract all port info
cmd = inspec.command('netstat -an -f inet -f inet6')
return nil if cmd.exit_status.to_i != 0
# parse the content
netstat_ports = parse_netstat(cmd.stdout)
# filter all ports, where we listen
listen = netstat_ports.select { |val|
!val['state'].nil? && 'listen'.casecmp(val['state']) == 0
}
# map the data
ports = listen.map { |val|
protocol = val['protocol']
local_addr = val['local-address']
# solaris uses 127.0.0.1.57455 instead 127.0.0.1:57455, lets convert the
# the last . to :
local_addr[local_addr.rindex('.')] = ':'
host, port = parse_net_address(local_addr, protocol)
# map data
{
port: port,
address: host,
protocol: protocol,
process: nil, # we do not have pid on solaris
pid: nil, # we do not have pid on solaris
process: process,
pid: pid,
}
}
ports
end
end
# extracts information from sockstat
class FreeBsdPorts < PortsInfo
def info
cmd = inspec.command('sockstat -46l')
return nil if cmd.exit_status.to_i != 0
ports = []
# split on each newline
cmd.stdout.each_line do |line|
port_info = parse_sockstat_line(line)
# push data, if not headerfile
next unless %w{tcp tcp6 udp udp6}.include?(port_info[:protocol])
ports.push(port_info)
end
ports
end
def parse_net_address(net_addr, protocol)
case protocol
when 'tcp4', 'udp4', 'tcp', 'udp'
# replace * with 0.0.0.0
net_addr = net_addr.gsub(/^\*:/, '0.0.0.0:') if net_addr =~ /^*:(\d+)$/
ip_addr = URI('addr://'+net_addr)
host = ip_addr.host
port = ip_addr.port
when 'tcp6', 'udp6'
return [] if net_addr == '*:*' # abort for now
# replace * with 0:0:0:0:0:0:0:0
net_addr = net_addr.gsub(/^\*:/, '0:0:0:0:0:0:0:0:') if net_addr =~ /^*:(\d+)$/
# extract port
ip6 = /^(\S+):(\d+)$/.match(net_addr)
ip6addr = ip6[1]
ip_addr = URI("addr://[#{ip6addr}]:#{ip6[2]}")
# replace []
host = ip_addr.host[1..ip_addr.host.size-2]
port = ip_addr.port
end
[host, port]
rescue URI::InvalidURIError => e
warn "Could not parse #{net_addr}, #{e}"
nil
end
def parse_sockstat_line(line)
# 1 - USER, 2 - COMMAND, 3 - PID, 4 - FD 5 - PROTO, 6 - LOCAL ADDRESS, 7 - FOREIGN ADDRESS
parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/.match(line)
return {} if parsed.nil?
# extract ip information
protocol = parsed[5].downcase
host, port = parse_net_address(parsed[6], protocol)
return {} if host.nil? or port.nil?
# extract process
process = parsed[2]
# extract PID
pid = parsed[3]
pid = pid.to_i if pid =~ /^\d+$/
# map tcp4 and udp4
protocol = 'tcp' if protocol.eql?('tcp4')
protocol = 'udp' if protocol.eql?('udp4')
# map data
{
port: port,
address: host,
protocol: protocol,
process: process,
pid: pid,
}
end
end
class SolarisPorts < FreeBsdPorts
include SolarisNetstatParser
def info
# extract all port info
cmd = inspec.command('netstat -an -f inet -f inet6')
return nil if cmd.exit_status.to_i != 0
# parse the content
netstat_ports = parse_netstat(cmd.stdout)
# filter all ports, where we listen
listen = netstat_ports.select { |val|
!val['state'].nil? && 'listen'.casecmp(val['state']) == 0
}
# map the data
ports = listen.map { |val|
protocol = val['protocol']
local_addr = val['local-address']
# solaris uses 127.0.0.1.57455 instead 127.0.0.1:57455, lets convert the
# the last . to :
local_addr[local_addr.rindex('.')] = ':'
host, port = parse_net_address(local_addr, protocol)
{
port: port,
address: host,
protocol: protocol,
process: nil, # we do not have pid on solaris
pid: nil, # we do not have pid on solaris
}
}
ports
end
end
end

View file

@ -4,34 +4,36 @@
# author: Christoph Hartmann
# license: All rights reserved
class Postgres < Inspec.resource(1)
name 'postgres'
module Inspec::Resources
class Postgres < Inspec.resource(1)
name 'postgres'
attr_reader :service, :data_dir, :conf_dir, :conf_path
def initialize
case inspec.os[:family]
when 'ubuntu', 'debian'
@service = 'postgresql'
@data_dir = '/var/lib/postgresql'
@version = inspec.command('ls /etc/postgresql/').stdout.chomp
@conf_dir = "/etc/postgresql/#{@version}/main"
@conf_path = File.join @conf_dir, 'postgresql.conf'
attr_reader :service, :data_dir, :conf_dir, :conf_path
def initialize
case inspec.os[:family]
when 'ubuntu', 'debian'
@service = 'postgresql'
@data_dir = '/var/lib/postgresql'
@version = inspec.command('ls /etc/postgresql/').stdout.chomp
@conf_dir = "/etc/postgresql/#{@version}/main"
@conf_path = ::File.join @conf_dir, 'postgresql.conf'
when 'arch'
@service = 'postgresql'
@data_dir = '/var/lib/postgres/data'
@conf_dir = '/var/lib/postgres/data'
@conf_path = File.join @conf_dir, 'postgresql.conf'
when 'arch'
@service = 'postgresql'
@data_dir = '/var/lib/postgres/data'
@conf_dir = '/var/lib/postgres/data'
@conf_path = ::File.join @conf_dir, 'postgresql.conf'
else
@service = 'postgresql'
@data_dir = '/var/lib/postgresql'
@conf_dir = '/var/lib/pgsql/data'
@conf_path = File.join @conf_dir, 'postgresql.conf'
else
@service = 'postgresql'
@data_dir = '/var/lib/postgresql'
@conf_dir = '/var/lib/pgsql/data'
@conf_path = ::File.join @conf_dir, 'postgresql.conf'
end
end
def to_s
'PostgreSQL'
end
end
def to_s
'PostgreSQL'
end
end

View file

@ -8,86 +8,88 @@ require 'utils/simpleconfig'
require 'utils/find_files'
require 'resources/postgres'
class PostgresConf < Inspec.resource(1)
name 'postgres_conf'
desc 'Use the postgres_conf InSpec audit resource to test the contents of the configuration file for PostgreSQL, typically located at /etc/postgresql/<version>/main/postgresql.conf or /var/lib/postgres/data/postgresql.conf, depending on the platform.'
example "
describe postgres_conf do
its('max_connections') { should eq '5' }
end
"
include FindFiles
def initialize(conf_path = nil)
@conf_path = conf_path || inspec.postgres.conf_path
@conf_dir = File.expand_path(File.dirname(@conf_path))
@files_contents = {}
@content = nil
@params = nil
read_content
end
def content
@content ||= read_content
end
def params(*opts)
@params || read_content
res = @params
opts.each do |opt|
res = res[opt] unless res.nil?
end
res
end
def read_content
@content = ''
@params = {}
# skip if the main configuration file doesn't exist
if !inspec.file(@conf_path).file?
return skip_resource "Can't find file \"#{@conf_path}\""
end
raw_conf = read_file(@conf_path)
if raw_conf.empty? && inspec.file(@conf_path).size > 0
return skip_resource("Can't read file \"#{@conf_path}\"")
end
to_read = [@conf_path]
until to_read.empty?
raw_conf = read_file(to_read[0])
@content += raw_conf
params = SimpleConfig.new(raw_conf).params
@params.merge!(params)
to_read = to_read.drop(1)
# see if there is more config files to include
to_read += include_files(params).find_all do |fp|
not @files_contents.key? fp
module Inspec::Resources
class PostgresConf < Inspec.resource(1)
name 'postgres_conf'
desc 'Use the postgres_conf InSpec audit resource to test the contents of the configuration file for PostgreSQL, typically located at /etc/postgresql/<version>/main/postgresql.conf or /var/lib/postgres/data/postgresql.conf, depending on the platform.'
example "
describe postgres_conf do
its('max_connections') { should eq '5' }
end
"
include FindFiles
def initialize(conf_path = nil)
@conf_path = conf_path || inspec.postgres.conf_path
@conf_dir = ::File.expand_path(::File.dirname(@conf_path))
@files_contents = {}
@content = nil
@params = nil
read_content
end
@content
end
def include_files(params)
include_files = params['include'] || []
include_files += params['include_if_exists'] || []
dirs = params['include_dir'] || []
dirs.each do |dir|
dir = File.join(@conf_dir, dir) if dir[0] != '/'
include_files += find_files(dir, depth: 1, type: 'file')
def content
@content ||= read_content
end
include_files
end
def read_file(path)
@files_contents[path] ||= inspec.file(path).content
end
def params(*opts)
@params || read_content
res = @params
opts.each do |opt|
res = res[opt] unless res.nil?
end
res
end
def to_s
'PostgreSQL Configuration'
def read_content
@content = ''
@params = {}
# skip if the main configuration file doesn't exist
if !inspec.file(@conf_path).file?
return skip_resource "Can't find file \"#{@conf_path}\""
end
raw_conf = read_file(@conf_path)
if raw_conf.empty? && inspec.file(@conf_path).size > 0
return skip_resource("Can't read file \"#{@conf_path}\"")
end
to_read = [@conf_path]
until to_read.empty?
raw_conf = read_file(to_read[0])
@content += raw_conf
params = SimpleConfig.new(raw_conf).params
@params.merge!(params)
to_read = to_read.drop(1)
# see if there is more config files to include
to_read += include_files(params).find_all do |fp|
not @files_contents.key? fp
end
end
@content
end
def include_files(params)
include_files = params['include'] || []
include_files += params['include_if_exists'] || []
dirs = params['include_dir'] || []
dirs.each do |dir|
dir = File.join(@conf_dir, dir) if dir[0] != '/'
include_files += find_files(dir, depth: 1, type: 'file')
end
include_files
end
def read_file(path)
@files_contents[path] ||= inspec.file(path).content
end
def to_s
'PostgreSQL Configuration'
end
end
end

View file

@ -4,59 +4,61 @@
# author: Christoph Hartmann
# license: All rights reserved
class Lines
attr_reader :output
module Inspec::Resources
class Lines
attr_reader :output
def initialize(raw, desc)
@output = raw
@desc = desc
end
def lines
output.split("\n")
end
def to_s
@desc
end
end
class PostgresSession < Inspec.resource(1)
name 'postgres_session'
desc 'Use the postgres_session InSpec audit resource to test SQL commands run against a PostgreSQL database.'
example "
sql = postgres_session('username', 'password')
describe sql.query('SELECT * FROM pg_shadow WHERE passwd IS NULL;') do
its('output') { should eq('') }
def initialize(raw, desc)
@output = raw
@desc = desc
end
"
def initialize(user, pass)
@user = user || 'postgres'
@pass = pass
def lines
output.split("\n")
end
def to_s
@desc
end
end
def query(query, db = [])
dbs = db.map { |x| "-d #{x}" }.join(' ')
# TODO: simple escape, must be handled by a library
# that does this securely
escaped_query = query.gsub(/\\/, '\\\\').gsub(/"/, '\\"').gsub(/\$/, '\\$')
# run the query
cmd = inspec.command("PGPASSWORD='#{@pass}' psql -U #{@user} #{dbs} -h localhost -c \"#{escaped_query}\"")
out = cmd.stdout + "\n" + cmd.stderr
if cmd.exit_status != 0 or
out =~ /could not connect to .*/ or
out.downcase =~ /^error/
# skip this test if the server can't run the query
skip_resource "Can't read run query #{query.inspect} on postgres_session: #{out}"
else
# remove the whole header (i.e. up to the first ^-----+------+------$)
# remove the tail
lines = cmd.stdout
.sub(/(.*\n)+([-]+[+])*[-]+\n/, '')
.sub(/\n[^\n]*\n\n$/, '')
Lines.new(lines.strip, "PostgreSQL query: #{query}")
class PostgresSession < Inspec.resource(1)
name 'postgres_session'
desc 'Use the postgres_session InSpec audit resource to test SQL commands run against a PostgreSQL database.'
example "
sql = postgres_session('username', 'password')
describe sql.query('SELECT * FROM pg_shadow WHERE passwd IS NULL;') do
its('output') { should eq('') }
end
"
def initialize(user, pass)
@user = user || 'postgres'
@pass = pass
end
def query(query, db = [])
dbs = db.map { |x| "-d #{x}" }.join(' ')
# TODO: simple escape, must be handled by a library
# that does this securely
escaped_query = query.gsub(/\\/, '\\\\').gsub(/"/, '\\"').gsub(/\$/, '\\$')
# run the query
cmd = inspec.command("PGPASSWORD='#{@pass}' psql -U #{@user} #{dbs} -h localhost -c \"#{escaped_query}\"")
out = cmd.stdout + "\n" + cmd.stderr
if cmd.exit_status != 0 or
out =~ /could not connect to .*/ or
out.downcase =~ /^error/
# skip this test if the server can't run the query
skip_resource "Can't read run query #{query.inspect} on postgres_session: #{out}"
else
# remove the whole header (i.e. up to the first ^-----+------+------$)
# remove the tail
lines = cmd.stdout
.sub(/(.*\n)+([-]+[+])*[-]+\n/, '')
.sub(/\n[^\n]*\n\n$/, '')
Lines.new(lines.strip, "PostgreSQL query: #{query}")
end
end
end
end

View file

@ -4,70 +4,72 @@
# author: Christoph Hartmann
# license: All rights reserved
class Processes < Inspec.resource(1)
name 'processes'
desc 'Use the processes InSpec audit resource to test properties for programs that are running on the system.'
example "
describe processes('mysqld') do
its('list.length') { should eq 1 }
its('users') { should eq ['mysql'] }
its('states') { should include 'S' }
end
"
module Inspec::Resources
class Processes < Inspec.resource(1)
name 'processes'
desc 'Use the processes InSpec audit resource to test properties for programs that are running on the system.'
example "
describe processes('mysqld') do
its('list.length') { should eq 1 }
its('users') { should eq ['mysql'] }
its('states') { should include 'S' }
end
"
attr_reader :list,
:users,
:states
attr_reader :list,
:users,
:states
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|$)')
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
all_cmds = ps_aux
@list = all_cmds.find_all do |hm|
hm[:command] =~ grep
end
{ users: :user,
states: :stat }.each do |var, key|
instance_variable_set("@#{var}", @list.map { |l| l[key] }.uniq)
end
end
all_cmds = ps_aux
@list = all_cmds.find_all do |hm|
hm[:command] =~ grep
def to_s
'Processes'
end
{ users: :user,
states: :stat }.each do |var, key|
instance_variable_set("@#{var}", @list.map { |l| l[key] }.uniq)
end
end
private
def to_s
'Processes'
end
def ps_aux
# get all running processes
cmd = inspec.command('ps aux')
all = cmd.stdout.split("\n")[1..-1]
return [] if all.nil?
private
lines = all.map do |line|
# user 32296 0.0 0.0 42592 7972 pts/15 Ss+ Apr06 0:00 zsh
line.match(/^([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+(.*)$/)
end.compact
def ps_aux
# get all running processes
cmd = inspec.command('ps aux')
all = cmd.stdout.split("\n")[1..-1]
return [] if all.nil?
lines = all.map do |line|
# user 32296 0.0 0.0 42592 7972 pts/15 Ss+ Apr06 0:00 zsh
line.match(/^([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+(.*)$/)
end.compact
lines.map do |m|
{
user: m[1],
pid: m[2],
cpu: m[3],
mem: m[4],
vsz: m[5],
rss: m[6],
tty: m[7],
stat: m[8],
start: m[9],
time: m[10],
command: m[11],
}
lines.map do |m|
{
user: m[1],
pid: m[2],
cpu: m[3],
mem: m[4],
vsz: m[5],
rss: m[6],
tty: m[7],
stat: m[8],
start: m[9],
time: m[10],
command: m[11],
}
end
end
end
end

View file

@ -10,174 +10,176 @@ require 'json'
# its('Start') { should eq 2 }
# end
class RegistryKey < Inspec.resource(1)
name 'registry_key'
desc 'Use the registry_key InSpec audit resource to test key values in the Microsoft Windows registry.'
example "
describe registry_key('path\to\key') do
its('name') { should eq 'value' }
end
"
module Inspec::Resources
class RegistryKey < Inspec.resource(1)
name 'registry_key'
desc 'Use the registry_key InSpec audit resource to test key values in the Microsoft Windows registry.'
example "
describe registry_key('path\to\key') do
its('name') { should eq 'value' }
end
"
attr_accessor :reg_key
attr_accessor :reg_key
def initialize(name, reg_key = nil)
# if we have one parameter, we use it as name
reg_key ||= name
@name = name
@reg_key = reg_key
def initialize(name, reg_key = nil)
# if we have one parameter, we use it as name
reg_key ||= name
@name = name
@reg_key = reg_key
return skip_resource 'The `registry_key` resource is not supported on your OS yet.' if !inspec.os.windows?
end
def exists?
!registry_key(@reg_key).nil?
end
def has_value?(value)
val = registry_key(@reg_key)
!val.nil? && registry_property_value(val, '(default)') == value ? true : false
end
def has_property?(property_name, property_type = nil)
val = registry_key(@reg_key)
!val.nil? && registry_property_exists(val, property_name) && (property_type.nil? || registry_property_type(val, property_name) == map2type(property_type)) ? true : false
end
# deactivate rubocop, because we need to stay compatible with Serverspe
# rubocop:disable Style/OptionalArguments
def has_property_value?(property_name, property_type = nil, value)
# rubocop:enable Style/OptionalArguments
val = registry_key(@reg_key)
# convert value to binary if required
value = value.bytes if !property_type.nil? && map2type(property_type) == 3 && !value.is_a?(Array)
!val.nil? && registry_property_value(val, property_name) == value && (property_type.nil? || registry_property_type(val, property_name) == map2type(property_type)) ? true : false
end
# returns nil, if not existant or value
def method_missing(meth)
# get data
val = registry_key(@reg_key)
registry_property_value(val, meth)
end
def to_s
"Registry Key #{@name}"
end
private
def prep_prop(property)
property.to_s.downcase
end
def registry_property_exists(regkey, property)
return false if regkey.nil? || property.nil?
# always ensure the key is lower case
!regkey[prep_prop(property)].nil?
end
def registry_property_value(regkey, property)
return nil if !registry_property_exists(regkey, property)
# always ensure the key is lower case
regkey[prep_prop(property)]['value']
end
def registry_property_type(regkey, property)
return nil if !registry_property_exists(regkey, property)
# always ensure the key is lower case
regkey[prep_prop(property)]['type']
end
def registry_key(path)
return @registry_cache if defined?(@registry_cache)
# load registry key and all properties
script = <<-EOH
$reg = Get-Item 'Registry::#{path}'
$object = New-Object -Type PSObject
$reg.Property | ForEach-Object {
$key = $_
if ("(default)".Equals($key)) { $key = '' }
$value = New-Object psobject -Property @{
"value" = $reg.GetValue($key);
"type" = $reg.GetValueKind($key);
}
$object | Add-Member MemberType NoteProperty Name $_ Value $value
}
$object | ConvertTo-Json
EOH
cmd = inspec.script(script)
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0, try to parse json
begin
@registry_cache = JSON.parse(cmd.stdout)
# convert keys to lower case
@registry_cache = Hash[@registry_cache.map do |key, value|
[key.downcase, value]
end]
rescue JSON::ParserError => _e
@registry_cache = nil
return skip_resource 'The `registry_key` resource is not supported on your OS yet.' if !inspec.os.windows?
end
@registry_cache
def exists?
!registry_key(@reg_key).nil?
end
def has_value?(value)
val = registry_key(@reg_key)
!val.nil? && registry_property_value(val, '(default)') == value ? true : false
end
def has_property?(property_name, property_type = nil)
val = registry_key(@reg_key)
!val.nil? && registry_property_exists(val, property_name) && (property_type.nil? || registry_property_type(val, property_name) == map2type(property_type)) ? true : false
end
# deactivate rubocop, because we need to stay compatible with Serverspe
# rubocop:disable Style/OptionalArguments
def has_property_value?(property_name, property_type = nil, value)
# rubocop:enable Style/OptionalArguments
val = registry_key(@reg_key)
# convert value to binary if required
value = value.bytes if !property_type.nil? && map2type(property_type) == 3 && !value.is_a?(Array)
!val.nil? && registry_property_value(val, property_name) == value && (property_type.nil? || registry_property_type(val, property_name) == map2type(property_type)) ? true : false
end
# returns nil, if not existant or value
def method_missing(meth)
# get data
val = registry_key(@reg_key)
registry_property_value(val, meth)
end
def to_s
"Registry Key #{@name}"
end
private
def prep_prop(property)
property.to_s.downcase
end
def registry_property_exists(regkey, property)
return false if regkey.nil? || property.nil?
# always ensure the key is lower case
!regkey[prep_prop(property)].nil?
end
def registry_property_value(regkey, property)
return nil if !registry_property_exists(regkey, property)
# always ensure the key is lower case
regkey[prep_prop(property)]['value']
end
def registry_property_type(regkey, property)
return nil if !registry_property_exists(regkey, property)
# always ensure the key is lower case
regkey[prep_prop(property)]['type']
end
def registry_key(path)
return @registry_cache if defined?(@registry_cache)
# load registry key and all properties
script = <<-EOH
$reg = Get-Item 'Registry::#{path}'
$object = New-Object -Type PSObject
$reg.Property | ForEach-Object {
$key = $_
if ("(default)".Equals($key)) { $key = '' }
$value = New-Object psobject -Property @{
"value" = $reg.GetValue($key);
"type" = $reg.GetValueKind($key);
}
$object | Add-Member MemberType NoteProperty Name $_ Value $value
}
$object | ConvertTo-Json
EOH
cmd = inspec.script(script)
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0, try to parse json
begin
@registry_cache = JSON.parse(cmd.stdout)
# convert keys to lower case
@registry_cache = Hash[@registry_cache.map do |key, value|
[key.downcase, value]
end]
rescue JSON::ParserError => _e
@registry_cache = nil
end
@registry_cache
end
# Registry key value types
# @see https://msdn.microsoft.com/en-us/library/windows/desktop/ms724884(v=vs.85).aspx
# REG_NONE 0
# REG_SZ 1
# REG_EXPAND_SZ 2
# REG_BINARY 3
# REG_DWORD 4
# REG_DWORD_LITTLE_ENDIAN 4
# REG_DWORD_BIG_ENDIAN 5
# REG_LINK 6
# REG_MULTI_SZ 7
# REG_RESOURCE_LIST 8
# REG_FULL_RESOURCE_DESCRIPTOR 9
# REG_RESOURCE_REQUIREMENTS_LIST 10
# REG_QWORD 11
# REG_QWORD_LITTLE_ENDIAN 11
def map2type(symbol)
options = {}
# chef symbols, we prefer those
options[:binary] = 3
options[:string] = 1
options[:multi_string] = 7
options[:expand_string] = 2
options[:dword] = 4
options[:dword_big_endian] = 5
options[:qword] = 11
# serverspec symbols
options[:type_string] = 1
options[:type_binary] = 3
options[:type_dword] = 4
options[:type_qword] = 11
options[:type_multistring] = 7
options[:type_expandstring] = 2
options[symbol]
end
end
# Registry key value types
# @see https://msdn.microsoft.com/en-us/library/windows/desktop/ms724884(v=vs.85).aspx
# REG_NONE 0
# REG_SZ 1
# REG_EXPAND_SZ 2
# REG_BINARY 3
# REG_DWORD 4
# REG_DWORD_LITTLE_ENDIAN 4
# REG_DWORD_BIG_ENDIAN 5
# REG_LINK 6
# REG_MULTI_SZ 7
# REG_RESOURCE_LIST 8
# REG_FULL_RESOURCE_DESCRIPTOR 9
# REG_RESOURCE_REQUIREMENTS_LIST 10
# REG_QWORD 11
# REG_QWORD_LITTLE_ENDIAN 11
def map2type(symbol)
options = {}
# for compatability with serverspec
# this is deprecated syntax and will be removed in future versions
class WindowsRegistryKey < RegistryKey
name 'windows_registry_key'
# chef symbols, we prefer those
options[:binary] = 3
options[:string] = 1
options[:multi_string] = 7
options[:expand_string] = 2
options[:dword] = 4
options[:dword_big_endian] = 5
options[:qword] = 11
def initialize(name)
deprecated
super(name)
end
# serverspec symbols
options[:type_string] = 1
options[:type_binary] = 3
options[:type_dword] = 4
options[:type_qword] = 11
options[:type_multistring] = 7
options[:type_expandstring] = 2
options[symbol]
end
end
# for compatability with serverspec
# this is deprecated syntax and will be removed in future versions
class WindowsRegistryKey < RegistryKey
name 'windows_registry_key'
def initialize(name)
deprecated
super(name)
end
def deprecated
warn '[DEPRECATION] `windows_registry_key(reg_key)` is deprecated. Please use `registry_key(\'path\to\key\')` instead.'
def deprecated
warn '[DEPRECATION] `windows_registry_key(reg_key)` is deprecated. Please use `registry_key(\'path\to\key\')` instead.'
end
end
end

View file

@ -4,38 +4,40 @@
# author: Dominik Richter
# license: All rights reserved
class Script < Cmd
name 'script'
desc 'Use the script InSpec audit resource to test a Windows PowerShell script on the Microsoft Windows platform.'
example "
script = <<-EOH
# you powershell script
EOH
module Inspec::Resources
class Script < Cmd
name 'script'
desc 'Use the script InSpec audit resource to test a Windows PowerShell script on the Microsoft Windows platform.'
example "
script = <<-EOH
# you powershell script
EOH
describe script(script) do
its('matcher') { should eq 'output' }
end
"
describe script(script) do
its('matcher') { should eq 'output' }
end
"
def initialize(script)
unless inspec.os.windows?
return skip_resource 'The `script` resource is not supported on your OS yet.'
def initialize(script)
unless inspec.os.windows?
return skip_resource 'The `script` resource is not supported on your OS yet.'
end
# encodes a script as base64 to run as powershell encodedCommand
# this comes with performance issues: @see https://gist.github.com/fnichol/7b20596b950e65fb96f9
require 'winrm'
script = WinRM::PowershellScript.new(script)
cmd = "powershell -encodedCommand #{script.encoded}"
super(cmd)
end
# encodes a script as base64 to run as powershell encodedCommand
# this comes with performance issues: @see https://gist.github.com/fnichol/7b20596b950e65fb96f9
require 'winrm'
script = WinRM::PowershellScript.new(script)
cmd = "powershell -encodedCommand #{script.encoded}"
super(cmd)
end
# we cannot determine if a command exists, because that does not work for scripts
def exist?
nil
end
# we cannot determine if a command exists, because that does not work for scripts
def exist?
nil
end
def to_s
'Script'
def to_s
'Script'
end
end
end

View file

@ -13,70 +13,72 @@
# All local GPO parameters can be examined via Registry, but not all security
# parameters. Therefore we need a combination of Registry and secedit output
class SecurityPolicy < Inspec.resource(1)
name 'security_policy'
desc 'Use the security_policy InSpec audit resource to test security policies on the Microsoft Windows platform.'
example "
describe security_policy do
its('SeNetworkLogonRight') { should eq '*S-1-5-11' }
end
"
def initialize
@loaded = false
@policy = nil
@exit_status = nil
end
# load security content
def load
# export the security policy
cmd = inspec.command('secedit /export /cfg win_secpol.cfg')
return nil if cmd.exit_status.to_i != 0
# store file content
cmd = inspec.command('Get-Content win_secpol.cfg')
@exit_status = cmd.exit_status.to_i
return nil if @exit_status != 0
@policy = cmd.stdout
@loaded = true
# returns self
self
ensure
# delete temp file
inspec.command('Remove-Item win_secpol.cfg').exit_status.to_i
end
def method_missing(method)
# load data if needed
if @loaded == false
load
module Inspec::Resources
class SecurityPolicy < Inspec.resource(1)
name 'security_policy'
desc 'Use the security_policy InSpec audit resource to test security policies on the Microsoft Windows platform.'
example "
describe security_policy do
its('SeNetworkLogonRight') { should eq '*S-1-5-11' }
end
"
def initialize
@loaded = false
@policy = nil
@exit_status = nil
end
# find line with key
key = Regexp.escape(method.to_s)
target = ''
@policy.each_line {|s|
target = s.strip if s =~ /^\s*#{key}\s*=\s*(.*)\b/
}
# load security content
def load
# export the security policy
cmd = inspec.command('secedit /export /cfg win_secpol.cfg')
return nil if cmd.exit_status.to_i != 0
# extract variable value
result = target.match(/[=]{1}\s*(?<value>.*)/)
# store file content
cmd = inspec.command('Get-Content win_secpol.cfg')
@exit_status = cmd.exit_status.to_i
return nil if @exit_status != 0
@policy = cmd.stdout
@loaded = true
if !result.nil?
val = result[:value]
val = val.to_i if val =~ /^\d+$/
else
# TODO: we may need to return skip or failure if the
# requested value is not available
val = nil
# returns self
self
ensure
# delete temp file
inspec.command('Remove-Item win_secpol.cfg').exit_status.to_i
end
val
end
def method_missing(method)
# load data if needed
if @loaded == false
load
end
def to_s
'Security Policy'
# find line with key
key = Regexp.escape(method.to_s)
target = ''
@policy.each_line {|s|
target = s.strip if s =~ /^\s*#{key}\s*=\s*(.*)\b/
}
# extract variable value
result = target.match(/[=]{1}\s*(?<value>.*)/)
if !result.nil?
val = result[:value]
val = val.to_i if val =~ /^\d+$/
else
# TODO: we may need to return skip or failure if the
# requested value is not available
val = nil
end
val
end
def to_s
'Security Policy'
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -15,121 +15,123 @@ require 'forwardable'
# - inactive_days before deactivating the account
# - expiry_date when this account will expire
class Shadow < Inspec.resource(1)
name 'shadow'
desc 'Use the shadow InSpec resource to test the contents of /etc/shadow, '\
'which contains the following information for users that may log into '\
'the system and/or as users that own running processes.'
example "
describe shadow do
its('users') { should_not include 'forbidden_user' }
module Inspec::Resources
class Shadow < Inspec.resource(1)
name 'shadow'
desc 'Use the shadow InSpec resource to test the contents of /etc/shadow, '\
'which contains the following information for users that may log into '\
'the system and/or as users that own running processes.'
example "
describe shadow do
its('users') { should_not include 'forbidden_user' }
end
describe shadow.users('bin') do
its('password') { should cmp 'x' }
its('count') { should eq 1 }
end
"
extend Forwardable
attr_reader :params
attr_reader :content
attr_reader :lines
def initialize(path = '/etc/shadow', opts = nil)
opts ||= {}
@path = path || '/etc/shadow'
@content = opts[:content] || inspec.file(@path).content
@lines = @content.to_s.split("\n")
@filters = opts[:filters] || ''
@params = @lines.map { |l| parse_shadow_line(l) }
end
describe shadow.users('bin') do
its('password') { should cmp 'x' }
its('count') { should eq 1 }
end
"
extend Forwardable
attr_reader :params
attr_reader :content
attr_reader :lines
def initialize(path = '/etc/shadow', opts = nil)
opts ||= {}
@path = path || '/etc/shadow'
@content = opts[:content] || inspec.file(@path).content
@lines = @content.to_s.split("\n")
@filters = opts[:filters] || ''
@params = @lines.map { |l| parse_shadow_line(l) }
end
def filter(hm = {})
return self if hm.nil? || hm.empty?
res = @params
filters = ''
hm.each do |attr, condition|
condition = condition.to_s if condition.is_a? Integer
filters += " #{attr} = #{condition.inspect}"
res = res.find_all do |line|
case line[attr.to_s]
when condition
true
else
false
def filter(hm = {})
return self if hm.nil? || hm.empty?
res = @params
filters = ''
hm.each do |attr, condition|
condition = condition.to_s if condition.is_a? Integer
filters += " #{attr} = #{condition.inspect}"
res = res.find_all do |line|
case line[attr.to_s]
when condition
true
else
false
end
end
end
content = res.map { |x| x.values.join(':') }.join("\n")
Shadow.new(@path, content: content, filters: @filters + filters)
end
content = res.map { |x| x.values.join(':') }.join("\n")
Shadow.new(@path, content: content, filters: @filters + filters)
end
def entries
@lines.map { |line| Shadow.new(@path, content: line, filters: @filters) }
end
def entries
@lines.map { |line| Shadow.new(@path, content: line, filters: @filters) }
end
def users(name = nil)
name.nil? ? map_data('user') : filter(user: name)
end
def users(name = nil)
name.nil? ? map_data('user') : filter(user: name)
end
def passwords(password = nil)
password.nil? ? map_data('password') : filter(password: password)
end
def passwords(password = nil)
password.nil? ? map_data('password') : filter(password: password)
end
def last_changes(filter_by = nil)
filter_by.nil? ? map_data('last_change') : filter(last_change: filter_by)
end
def last_changes(filter_by = nil)
filter_by.nil? ? map_data('last_change') : filter(last_change: filter_by)
end
def min_days(filter_by = nil)
filter_by.nil? ? map_data('min_days') : filter(min_days: filter_by)
end
def min_days(filter_by = nil)
filter_by.nil? ? map_data('min_days') : filter(min_days: filter_by)
end
def max_days(filter_by = nil)
filter_by.nil? ? map_data('max_days') : filter(max_days: filter_by)
end
def max_days(filter_by = nil)
filter_by.nil? ? map_data('max_days') : filter(max_days: filter_by)
end
def warn_days(filter_by = nil)
filter_by.nil? ? map_data('warn_days') : filter(warn_days: filter_by)
end
def warn_days(filter_by = nil)
filter_by.nil? ? map_data('warn_days') : filter(warn_days: filter_by)
end
def inactive_days(filter_by = nil)
filter_by.nil? ? map_data('inactive_days') : filter(inactive_days: filter_by)
end
def inactive_days(filter_by = nil)
filter_by.nil? ? map_data('inactive_days') : filter(inactive_days: filter_by)
end
def expiry_dates(filter_by = nil)
filter_by.nil? ? map_data('expiry_date') : filter(expiry_date: filter_by)
end
def expiry_dates(filter_by = nil)
filter_by.nil? ? map_data('expiry_date') : filter(expiry_date: filter_by)
end
def to_s
f = @filters.empty? ? '' : ' with'+@filters
"/etc/shadow#{f}"
end
def to_s
f = @filters.empty? ? '' : ' with'+@filters
"/etc/shadow#{f}"
end
def_delegator :@params, :length, :count
def_delegator :@params, :length, :count
private
private
def map_data(id)
@params.map { |x| x[id] }
end
def map_data(id)
@params.map { |x| x[id] }
end
# Parse a line of /etc/shadow
#
# @param [String] line a line of /etc/shadow
# @return [Hash] Map of entries in this line
def parse_shadow_line(line)
x = line.split(':')
{
'user' => x.at(0),
'password' => x.at(1),
'last_change' => x.at(2),
'min_days' => x.at(3),
'max_days' => x.at(4),
'warn_days' => x.at(5),
'inactive_days' => x.at(6),
'expiry_date' => x.at(7),
'reserved' => x.at(8),
}
# Parse a line of /etc/shadow
#
# @param [String] line a line of /etc/shadow
# @return [Hash] Map of entries in this line
def parse_shadow_line(line)
x = line.split(':')
{
'user' => x.at(0),
'password' => x.at(1),
'last_change' => x.at(2),
'min_days' => x.at(3),
'max_days' => x.at(4),
'warn_days' => x.at(5),
'inactive_days' => x.at(6),
'expiry_date' => x.at(7),
'reserved' => x.at(8),
}
end
end
end

View file

@ -6,76 +6,78 @@
require 'utils/simpleconfig'
class SshConf < Inspec.resource(1)
name 'ssh_config'
desc 'Use the sshd_config InSpec audit resource to test configuration data for the Open SSH daemon located at /etc/ssh/sshd_config on Linux and UNIX platforms. sshd---the Open SSH daemon---listens on dedicated ports, starts a daemon for each incoming connection, and then handles encryption, authentication, key exchanges, command executation, and data exchanges.'
example "
describe sshd_config do
its('Protocol') { should eq '2' }
module Inspec::Resources
class SshConf < Inspec.resource(1)
name 'ssh_config'
desc 'Use the sshd_config InSpec audit resource to test configuration data for the Open SSH daemon located at /etc/ssh/sshd_config on Linux and UNIX platforms. sshd---the Open SSH daemon---listens on dedicated ports, starts a daemon for each incoming connection, and then handles encryption, authentication, key exchanges, command executation, and data exchanges.'
example "
describe sshd_config do
its('Protocol') { should eq '2' }
end
"
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}"
end
"
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}"
end
def content
read_content
end
def content
read_content
end
def params(*opts)
opts.inject(read_params) do |res, nxt|
res.respond_to?(:key) ? res[nxt] : nil
end
end
def params(*opts)
opts.inject(read_params) do |res, nxt|
res.respond_to?(:key) ? res[nxt] : nil
def method_missing(name)
param = read_params[name.to_s]
return nil if param.nil?
# extract first value if we have only one value in array
return param[0] if param.length == 1
param
end
def to_s
'SSH Configuration'
end
private
def read_content
return @content if defined?(@content)
file = inspec.file(@conf_path)
if !file.file?
return skip_resource "Can't find file \"#{@conf_path}\""
end
@content = file.content
if @content.empty? && file.size > 0
return skip_resource "Can't read file \"#{@conf_path}\""
end
@content
end
def read_params
return @params if defined?(@params)
return @params = {} if read_content.nil?
conf = SimpleConfig.new(
read_content,
assignment_re: /^\s*(\S+?)\s+(.*?)\s*$/,
multiple_values: true,
)
@params = conf.params
end
end
def method_missing(name)
param = read_params[name.to_s]
return nil if param.nil?
# extract first value if we have only one value in array
return param[0] if param.length == 1
param
end
class SshdConf < SshConf
name 'sshd_config'
def to_s
'SSH Configuration'
end
private
def read_content
return @content if defined?(@content)
file = inspec.file(@conf_path)
if !file.file?
return skip_resource "Can't find file \"#{@conf_path}\""
def initialize(path = nil)
super(path || '/etc/ssh/sshd_config')
end
@content = file.content
if @content.empty? && file.size > 0
return skip_resource "Can't read file \"#{@conf_path}\""
end
@content
end
def read_params
return @params if defined?(@params)
return @params = {} if read_content.nil?
conf = SimpleConfig.new(
read_content,
assignment_re: /^\s*(\S+?)\s+(.*?)\s*$/,
multiple_values: true,
)
@params = conf.params
end
end
class SshdConf < SshConf
name 'sshd_config'
def initialize(path = nil)
super(path || '/etc/ssh/sshd_config')
end
end

View file

@ -38,421 +38,423 @@
require 'utils/parser'
require 'utils/convert'
class User < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
name 'user'
desc 'Use the user InSpec audit resource to test user profiles, including the groups to which they belong, the frequency of required password changes, the directory paths to home and shell.'
example "
describe user('root') do
it { should exist }
its('uid') { should eq 1234 }
its('gid') { should eq 1234 }
end
"
def initialize(user)
@user = user
module Inspec::Resources
class User < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
name 'user'
desc 'Use the user InSpec audit resource to test user profiles, including the groups to which they belong, the frequency of required password changes, the directory paths to home and shell.'
example "
describe user('root') do
it { should exist }
its('uid') { should eq 1234 }
its('gid') { should eq 1234 }
end
"
def initialize(user)
@user = user
# select package manager
@user_provider = nil
os = inspec.os
if os.linux?
@user_provider = LinuxUser.new(inspec)
elsif os.windows?
@user_provider = WindowsUser.new(inspec)
elsif ['darwin'].include?(os[:family])
@user_provider = DarwinUser.new(inspec)
elsif ['freebsd'].include?(os[:family])
@user_provider = FreeBSDUser.new(inspec)
elsif ['aix'].include?(os[:family])
@user_provider = AixUser.new(inspec)
elsif os.solaris?
@user_provider = SolarisUser.new(inspec)
else
return skip_resource 'The `user` resource is not supported on your OS yet.'
# select package manager
@user_provider = nil
os = inspec.os
if os.linux?
@user_provider = LinuxUser.new(inspec)
elsif os.windows?
@user_provider = WindowsUser.new(inspec)
elsif ['darwin'].include?(os[:family])
@user_provider = DarwinUser.new(inspec)
elsif ['freebsd'].include?(os[:family])
@user_provider = FreeBSDUser.new(inspec)
elsif ['aix'].include?(os[:family])
@user_provider = AixUser.new(inspec)
elsif os.solaris?
@user_provider = SolarisUser.new(inspec)
else
return skip_resource 'The `user` resource is not supported on your OS yet.'
end
end
def exists?
!identity.nil? && !identity[:user].nil?
end
def uid
identity[:uid] unless identity.nil?
end
def gid
identity[:gid] unless identity.nil?
end
def group
identity[:group] unless identity.nil?
end
def groups
identity[:groups] unless identity.nil?
end
def home
meta_info[:home] unless meta_info.nil?
end
def shell
meta_info[:shell] unless meta_info.nil?
end
# returns the minimum days between password changes
def mindays
credentials[:mindays] unless credentials.nil?
end
# returns the maximum days between password changes
def maxdays
credentials[:maxdays] unless credentials.nil?
end
# returns the days for password change warning
def warndays
credentials[:warndays] unless credentials.nil?
end
# implement 'mindays' method to be compatible with serverspec
def minimum_days_between_password_change
deprecated('minimum_days_between_password_change', "Please use 'its(:mindays)'")
mindays
end
# implement 'maxdays' method to be compatible with serverspec
def maximum_days_between_password_change
deprecated('maximum_days_between_password_change', "Please use 'its(:maxdays)'")
maxdays
end
# implements rspec has matcher, to be compatible with serverspec
# @see: https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/built_in/has.rb
def has_uid?(compare_uid)
deprecated('has_uid?')
uid == compare_uid
end
def has_home_directory?(compare_home)
deprecated('has_home_directory?', "Please use 'its(:home)'")
home == compare_home
end
def has_login_shell?(compare_shell)
deprecated('has_login_shell?', "Please use 'its(:shell)'")
shell == compare_shell
end
def has_authorized_key?(_compare_key)
deprecated('has_authorized_key?')
fail NotImplementedError
end
def deprecated(name, alternative = nil)
warn "[DEPRECATION] #{name} is deprecated. #{alternative}"
end
def to_s
"User #{@user}"
end
def identity
return @id_cache if defined?(@id_cache)
@id_cache = @user_provider.identity(@user) if !@user_provider.nil?
end
private
def meta_info
return @meta_cache if defined?(@meta_cache)
@meta_cache = @user_provider.meta_info(@user) if !@user_provider.nil?
end
def credentials
return @cred_cache if defined?(@cred_cache)
@cred_cache = @user_provider.credentials(@user) if !@user_provider.nil?
end
end
def exists?
!identity.nil? && !identity[:user].nil?
end
class UserInfo
include Converter
def uid
identity[:uid] unless identity.nil?
end
def gid
identity[:gid] unless identity.nil?
end
def group
identity[:group] unless identity.nil?
end
def groups
identity[:groups] unless identity.nil?
end
def home
meta_info[:home] unless meta_info.nil?
end
def shell
meta_info[:shell] unless meta_info.nil?
end
# returns the minimum days between password changes
def mindays
credentials[:mindays] unless credentials.nil?
end
# returns the maximum days between password changes
def maxdays
credentials[:maxdays] unless credentials.nil?
end
# returns the days for password change warning
def warndays
credentials[:warndays] unless credentials.nil?
end
# implement 'mindays' method to be compatible with serverspec
def minimum_days_between_password_change
deprecated('minimum_days_between_password_change', "Please use 'its(:mindays)'")
mindays
end
# implement 'maxdays' method to be compatible with serverspec
def maximum_days_between_password_change
deprecated('maximum_days_between_password_change', "Please use 'its(:maxdays)'")
maxdays
end
# implements rspec has matcher, to be compatible with serverspec
# @see: https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/built_in/has.rb
def has_uid?(compare_uid)
deprecated('has_uid?')
uid == compare_uid
end
def has_home_directory?(compare_home)
deprecated('has_home_directory?', "Please use 'its(:home)'")
home == compare_home
end
def has_login_shell?(compare_shell)
deprecated('has_login_shell?', "Please use 'its(:shell)'")
shell == compare_shell
end
def has_authorized_key?(_compare_key)
deprecated('has_authorized_key?')
fail NotImplementedError
end
def deprecated(name, alternative = nil)
warn "[DEPRECATION] #{name} is deprecated. #{alternative}"
end
def to_s
"User #{@user}"
end
def identity
return @id_cache if defined?(@id_cache)
@id_cache = @user_provider.identity(@user) if !@user_provider.nil?
end
private
def meta_info
return @meta_cache if defined?(@meta_cache)
@meta_cache = @user_provider.meta_info(@user) if !@user_provider.nil?
end
def credentials
return @cred_cache if defined?(@cred_cache)
@cred_cache = @user_provider.credentials(@user) if !@user_provider.nil?
end
end
class UserInfo
include Converter
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
def credentials(_username)
end
end
# implements generic unix id handling
class UnixUser < UserInfo
attr_reader :inspec, :id_cmd
def initialize(inspec)
@inspec = inspec
@id_cmd ||= 'id'
super
end
# parse one id entry like '0(wheel)''
def parse_value(line)
SimpleConfig.new(
line,
line_separator: ',',
assignment_re: /^\s*([^\(]*?)\s*\(\s*(.*?)\)*$/,
group_re: nil,
multiple_values: false,
).params
end
# extracts the identity
def identity(username)
cmd = inspec.command("#{id_cmd} #{username}")
return nil if cmd.exit_status != 0
# parse words
params = SimpleConfig.new(
parse_id_entries(cmd.stdout.chomp),
assignment_re: /^\s*([^=]*?)\s*=\s*(.*?)\s*$/,
group_re: nil,
multiple_values: false,
).params
{
uid: convert_to_i(parse_value(params['uid']).keys[0]),
user: parse_value(params['uid']).values[0],
gid: convert_to_i(parse_value(params['gid']).keys[0]),
group: parse_value(params['gid']).values[0],
groups: parse_value(params['groups']).values,
}
end
# splits the results of id into seperate lines
def parse_id_entries(raw)
data = []
until (index = raw.index(/\)\s{1}/)).nil?
data.push(raw[0, index+1]) # inclue closing )
raw = raw[index+2, raw.length-index-2]
end
data.push(raw) if !raw.nil?
data.join("\n")
end
end
class LinuxUser < UnixUser
include PasswdParser
include CommentParser
def meta_info(username)
cmd = inspec.command("getent passwd #{username}")
return nil if cmd.exit_status != 0
# returns: root:x:0:0:root:/root:/bin/bash
passwd = parse_passwd_line(cmd.stdout.chomp)
{
home: passwd['home'],
shell: passwd['shell'],
}
end
def credentials(username)
cmd = inspec.command("chage -l #{username}")
return nil if cmd.exit_status != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
group_re: nil,
multiple_values: false,
).params
{
mindays: convert_to_i(params['Minimum number of days between password change']),
maxdays: convert_to_i(params['Maximum number of days between password change']),
warndays: convert_to_i(params['Number of days of warning before password expires']),
}
end
end
class SolarisUser < LinuxUser
def initialize(inspec)
@inspec = inspec
@id_cmd ||= 'id -a'
super
end
def credentials(_username)
nil
end
end
class AixUser < UnixUser
def identity(username)
id = super(username)
return nil if id.nil?
# AIX 'id' command doesn't include the primary group in the supplementary
# yet it can be somewhere in the supplementary list if someone added root
# to a groups list in /etc/group
# we rearrange to expected list if that is the case
if id[:groups].first != id[:group]
id[:groups].reject! { |i| i == id[:group] } if id[:groups].include?(id[:group])
id[:groups].unshift(id[:group])
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
id
def credentials(_username)
end
end
def meta_info(username)
lsuser = inspec.command("lsuser -C -a home shell #{username}")
return nil if lsuser.exit_status != 0
user = lsuser.stdout.chomp.split("\n").last.split(':')
{
home: user[1],
shell: user[2],
}
end
def credentials(username)
cmd = inspec.command(
"lssec -c -f /etc/security/user -s #{username} -a minage -a maxage -a pwdwarntime",
)
return nil if cmd.exit_status != 0
user_sec = cmd.stdout.chomp.split("\n").last.split(':')
{
mindays: user_sec[1].to_i * 7,
maxdays: user_sec[2].to_i * 7,
warndays: user_sec[3].to_i,
}
end
end
# we do not use 'finger' for MacOS, because it is harder to parse data with it
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/fingerd.8.html
# instead we use 'dscl' to request user data
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dscl.1.html
# @see http://superuser.com/questions/592921/mac-osx-users-vs-dscl-command-to-list-user
class DarwinUser < UnixUser
def meta_info(username)
cmd = inspec.command("dscl -q . -read /Users/#{username} NFSHomeDirectory PrimaryGroupID RecordName UniqueID UserShell")
return nil if cmd.exit_status != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
group_re: nil,
multiple_values: false,
).params
{
home: params['NFSHomeDirectory'],
shell: params['UserShell'],
}
end
end
# FreeBSD recommends to use the 'pw' command for user management
# @see: https://www.freebsd.org/doc/handbook/users-synopsis.html
# @see: https://www.freebsd.org/cgi/man.cgi?pw(8)
# It offers the following commands:
# - adduser(8) The recommended command-line application for adding new users.
# - rmuser(8) The recommended command-line application for removing users.
# - chpass(1) A flexible tool for changing user database information.
# - passwd(1) The command-line tool to change user passwords.
class FreeBSDUser < UnixUser
include PasswdParser
def meta_info(username)
cmd = inspec.command("pw usershow #{username} -7")
return nil if cmd.exit_status != 0
# returns: root:*:0:0:Charlie &:/root:/bin/csh
passwd = parse_passwd_line(cmd.stdout.chomp)
{
home: passwd['home'],
shell: passwd['shell'],
}
end
end
# For now, we stick with WMI Win32_UserAccount
# @see https://msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx
# @see https://msdn.microsoft.com/en-us/library/aa394153(v=vs.85).aspx
#
# using Get-AdUser would be the best command for domain machines, but it will not be installed
# on client machines by default
# @see https://technet.microsoft.com/en-us/library/ee617241.aspx
# @see https://technet.microsoft.com/en-us/library/hh509016(v=WS.10).aspx
# @see http://woshub.com/get-aduser-getting-active-directory-users-data-via-powershell/
# @see http://stackoverflow.com/questions/17548523/the-term-get-aduser-is-not-recognized-as-the-name-of-a-cmdlet
#
# Just for reference, we could also use ADSI (Active Directory Service Interfaces)
# @see https://mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx
class WindowsUser < UserInfo
# parse windows account name
def parse_windows_account(username)
account = username.split('\\')
name = account.pop
domain = account.pop if account.size > 0
[name, domain]
end
def identity(username)
# extract domain/user information
account, domain = parse_windows_account(username)
# TODO: escape content
if !domain.nil?
filter = "Name = '#{account}' and Domain = '#{domain}'"
else
filter = "Name = '#{account}' and LocalAccount = true"
# implements generic unix id handling
class UnixUser < UserInfo
attr_reader :inspec, :id_cmd
def initialize(inspec)
@inspec = inspec
@id_cmd ||= 'id'
super
end
script = <<-EOH
# find user
$user = Get-WmiObject Win32_UserAccount -filter "#{filter}"
# get related groups
$groups = $user.GetRelated('Win32_Group') | Select-Object -Property Caption, Domain, Name, LocalAccount, SID, SIDType, Status
# filter user information
$user = $user | Select-Object -Property Caption, Description, Domain, Name, LocalAccount, Lockout, PasswordChangeable, PasswordExpires, PasswordRequired, SID, SIDType, Status
# build response object
New-Object -Type PSObject | `
Add-Member -MemberType NoteProperty -Name User -Value ($user) -PassThru | `
Add-Member -MemberType NoteProperty -Name Groups -Value ($groups) -PassThru | `
ConvertTo-Json
EOH
cmd = inspec.script(script)
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0, try to parse json
begin
params = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
# parse one id entry like '0(wheel)''
def parse_value(line)
SimpleConfig.new(
line,
line_separator: ',',
assignment_re: /^\s*([^\(]*?)\s*\(\s*(.*?)\)*$/,
group_re: nil,
multiple_values: false,
).params
end
user = params['User']['Caption'] unless params['User'].nil?
groups = params['Groups']
# if groups is no array, generate one
groups = [groups] if !groups.is_a?(Array)
groups = groups.map { |grp| grp['Caption'] } unless params['Groups'].nil?
# extracts the identity
def identity(username)
cmd = inspec.command("#{id_cmd} #{username}")
return nil if cmd.exit_status != 0
{
uid: nil,
user: user,
gid: nil,
group: nil,
groups: groups,
}
# parse words
params = SimpleConfig.new(
parse_id_entries(cmd.stdout.chomp),
assignment_re: /^\s*([^=]*?)\s*=\s*(.*?)\s*$/,
group_re: nil,
multiple_values: false,
).params
{
uid: convert_to_i(parse_value(params['uid']).keys[0]),
user: parse_value(params['uid']).values[0],
gid: convert_to_i(parse_value(params['gid']).keys[0]),
group: parse_value(params['gid']).values[0],
groups: parse_value(params['groups']).values,
}
end
# splits the results of id into seperate lines
def parse_id_entries(raw)
data = []
until (index = raw.index(/\)\s{1}/)).nil?
data.push(raw[0, index+1]) # inclue closing )
raw = raw[index+2, raw.length-index-2]
end
data.push(raw) if !raw.nil?
data.join("\n")
end
end
# not implemented yet
def meta_info(_username)
{
home: nil,
shell: nil,
}
class LinuxUser < UnixUser
include PasswdParser
include CommentParser
def meta_info(username)
cmd = inspec.command("getent passwd #{username}")
return nil if cmd.exit_status != 0
# returns: root:x:0:0:root:/root:/bin/bash
passwd = parse_passwd_line(cmd.stdout.chomp)
{
home: passwd['home'],
shell: passwd['shell'],
}
end
def credentials(username)
cmd = inspec.command("chage -l #{username}")
return nil if cmd.exit_status != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
group_re: nil,
multiple_values: false,
).params
{
mindays: convert_to_i(params['Minimum number of days between password change']),
maxdays: convert_to_i(params['Maximum number of days between password change']),
warndays: convert_to_i(params['Number of days of warning before password expires']),
}
end
end
class SolarisUser < LinuxUser
def initialize(inspec)
@inspec = inspec
@id_cmd ||= 'id -a'
super
end
def credentials(_username)
nil
end
end
class AixUser < UnixUser
def identity(username)
id = super(username)
return nil if id.nil?
# AIX 'id' command doesn't include the primary group in the supplementary
# yet it can be somewhere in the supplementary list if someone added root
# to a groups list in /etc/group
# we rearrange to expected list if that is the case
if id[:groups].first != id[:group]
id[:groups].reject! { |i| i == id[:group] } if id[:groups].include?(id[:group])
id[:groups].unshift(id[:group])
end
id
end
def meta_info(username)
lsuser = inspec.command("lsuser -C -a home shell #{username}")
return nil if lsuser.exit_status != 0
user = lsuser.stdout.chomp.split("\n").last.split(':')
{
home: user[1],
shell: user[2],
}
end
def credentials(username)
cmd = inspec.command(
"lssec -c -f /etc/security/user -s #{username} -a minage -a maxage -a pwdwarntime",
)
return nil if cmd.exit_status != 0
user_sec = cmd.stdout.chomp.split("\n").last.split(':')
{
mindays: user_sec[1].to_i * 7,
maxdays: user_sec[2].to_i * 7,
warndays: user_sec[3].to_i,
}
end
end
# we do not use 'finger' for MacOS, because it is harder to parse data with it
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/fingerd.8.html
# instead we use 'dscl' to request user data
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dscl.1.html
# @see http://superuser.com/questions/592921/mac-osx-users-vs-dscl-command-to-list-user
class DarwinUser < UnixUser
def meta_info(username)
cmd = inspec.command("dscl -q . -read /Users/#{username} NFSHomeDirectory PrimaryGroupID RecordName UniqueID UserShell")
return nil if cmd.exit_status != 0
params = SimpleConfig.new(
cmd.stdout.chomp,
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
group_re: nil,
multiple_values: false,
).params
{
home: params['NFSHomeDirectory'],
shell: params['UserShell'],
}
end
end
# FreeBSD recommends to use the 'pw' command for user management
# @see: https://www.freebsd.org/doc/handbook/users-synopsis.html
# @see: https://www.freebsd.org/cgi/man.cgi?pw(8)
# It offers the following commands:
# - adduser(8) The recommended command-line application for adding new users.
# - rmuser(8) The recommended command-line application for removing users.
# - chpass(1) A flexible tool for changing user database information.
# - passwd(1) The command-line tool to change user passwords.
class FreeBSDUser < UnixUser
include PasswdParser
def meta_info(username)
cmd = inspec.command("pw usershow #{username} -7")
return nil if cmd.exit_status != 0
# returns: root:*:0:0:Charlie &:/root:/bin/csh
passwd = parse_passwd_line(cmd.stdout.chomp)
{
home: passwd['home'],
shell: passwd['shell'],
}
end
end
# For now, we stick with WMI Win32_UserAccount
# @see https://msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx
# @see https://msdn.microsoft.com/en-us/library/aa394153(v=vs.85).aspx
#
# using Get-AdUser would be the best command for domain machines, but it will not be installed
# on client machines by default
# @see https://technet.microsoft.com/en-us/library/ee617241.aspx
# @see https://technet.microsoft.com/en-us/library/hh509016(v=WS.10).aspx
# @see http://woshub.com/get-aduser-getting-active-directory-users-data-via-powershell/
# @see http://stackoverflow.com/questions/17548523/the-term-get-aduser-is-not-recognized-as-the-name-of-a-cmdlet
#
# Just for reference, we could also use ADSI (Active Directory Service Interfaces)
# @see https://mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx
class WindowsUser < UserInfo
# parse windows account name
def parse_windows_account(username)
account = username.split('\\')
name = account.pop
domain = account.pop if account.size > 0
[name, domain]
end
def identity(username)
# extract domain/user information
account, domain = parse_windows_account(username)
# TODO: escape content
if !domain.nil?
filter = "Name = '#{account}' and Domain = '#{domain}'"
else
filter = "Name = '#{account}' and LocalAccount = true"
end
script = <<-EOH
# find user
$user = Get-WmiObject Win32_UserAccount -filter "#{filter}"
# get related groups
$groups = $user.GetRelated('Win32_Group') | Select-Object -Property Caption, Domain, Name, LocalAccount, SID, SIDType, Status
# filter user information
$user = $user | Select-Object -Property Caption, Description, Domain, Name, LocalAccount, Lockout, PasswordChangeable, PasswordExpires, PasswordRequired, SID, SIDType, Status
# build response object
New-Object -Type PSObject | `
Add-Member -MemberType NoteProperty -Name User -Value ($user) -PassThru | `
Add-Member -MemberType NoteProperty -Name Groups -Value ($groups) -PassThru | `
ConvertTo-Json
EOH
cmd = inspec.script(script)
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0, try to parse json
begin
params = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return nil
end
user = params['User']['Caption'] unless params['User'].nil?
groups = params['Groups']
# if groups is no array, generate one
groups = [groups] if !groups.is_a?(Array)
groups = groups.map { |grp| grp['Caption'] } unless params['Groups'].nil?
{
uid: nil,
user: user,
gid: nil,
group: nil,
groups: groups,
}
end
# not implemented yet
def meta_info(_username)
{
home: nil,
shell: nil,
}
end
end
end

View file

@ -27,57 +27,59 @@
# "Installed": false,
# "InstallState": 0
# }
class WindowsFeature < Inspec.resource(1)
name 'windows_feature'
desc 'Use the windows_feature InSpec audit resource to test features on Microsoft Windows.'
example "
describe windows_feature('dhcp') do
it { should be_installed }
end
"
module Inspec::Resources
class WindowsFeature < Inspec.resource(1)
name 'windows_feature'
desc 'Use the windows_feature InSpec audit resource to test features on Microsoft Windows.'
example "
describe windows_feature('dhcp') do
it { should be_installed }
end
"
def initialize(feature)
@feature = feature
@cache = nil
def initialize(feature)
@feature = feature
@cache = nil
# verify that this resource is only supported on Windows
return skip_resource 'The `windows_feature` resource is not supported on your OS.' if inspec.os[:family] != 'windows'
end
# returns true if the package is installed
def installed?(_provider = nil, _version = nil)
info[:installed] == true
end
# returns the package description
def info
return @cache if !@cache.nil?
features_cmd = "Get-WindowsFeature | Where-Object {$_.Name -eq '#{@feature}' -or $_.DisplayName -eq '#{@feature}'} | Select-Object -Property Name,DisplayName,Description,Installed,InstallState | ConvertTo-Json"
cmd = inspec.command(features_cmd)
@cache = {
name: @feature,
type: 'windows-feature',
}
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0
# try to parse json
begin
params = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return @cache
# verify that this resource is only supported on Windows
return skip_resource 'The `windows_feature` resource is not supported on your OS.' if inspec.os[:family] != 'windows'
end
@cache = {
name: params['Name'],
description: params['Description'],
installed: params['Installed'],
type: 'windows-feature',
}
end
# returns true if the package is installed
def installed?(_provider = nil, _version = nil)
info[:installed] == true
end
def to_s
"Windows Feature '#{@feature}'"
# returns the package description
def info
return @cache if !@cache.nil?
features_cmd = "Get-WindowsFeature | Where-Object {$_.Name -eq '#{@feature}' -or $_.DisplayName -eq '#{@feature}'} | Select-Object -Property Name,DisplayName,Description,Installed,InstallState | ConvertTo-Json"
cmd = inspec.command(features_cmd)
@cache = {
name: @feature,
type: 'windows-feature',
}
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0
# try to parse json
begin
params = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return @cache
end
@cache = {
name: params['Name'],
description: params['Description'],
installed: params['Installed'],
type: 'windows-feature',
}
end
def to_s
"Windows Feature '#{@feature}'"
end
end
end

View file

@ -4,139 +4,141 @@
require 'utils/parser'
class XinetdConf < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
name 'xinetd_conf'
desc 'Xinetd services configuration.'
example "
describe xinetd_conf.services('chargen') do
its('socket_types') { should include 'dgram' }
module Inspec::Resources
class XinetdConf < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
name 'xinetd_conf'
desc 'Xinetd services configuration.'
example "
describe xinetd_conf.services('chargen') do
its('socket_types') { should include 'dgram' }
end
describe xinetd_conf.services('chargen').socket_types('dgram') do
it { should be_disabled }
end
"
include XinetdParser
def initialize(conf_path = '/etc/xinetd.conf', opts = {})
@conf_path = conf_path
@params = opts[:params] unless opts[:params].nil?
@filters = opts[:filters] || ''
@contents = {}
end
describe xinetd_conf.services('chargen').socket_types('dgram') do
it { should be_disabled }
def to_s
"Xinetd config #{@conf_path}"
end
"
include XinetdParser
def services(condition = nil)
condition.nil? ? params['services'].keys : filter(service: condition)
end
def initialize(conf_path = '/etc/xinetd.conf', opts = {})
@conf_path = conf_path
@params = opts[:params] unless opts[:params].nil?
@filters = opts[:filters] || ''
@contents = {}
end
def ids(condition = nil)
condition.nil? ? services_field('id') : filter(id: condition)
end
def to_s
"Xinetd config #{@conf_path}"
end
def socket_types(condition = nil)
condition.nil? ? services_field('socket_type') : filter(socket_type: condition)
end
def services(condition = nil)
condition.nil? ? params['services'].keys : filter(service: condition)
end
def types(condition = nil)
condition.nil? ? services_field('type') : filter(type: condition)
end
def ids(condition = nil)
condition.nil? ? services_field('id') : filter(id: condition)
end
def wait(condition = nil)
condition.nil? ? services_field('wait') : filter(wait: condition)
end
def socket_types(condition = nil)
condition.nil? ? services_field('socket_type') : filter(socket_type: condition)
end
def disabled?
filter(disable: 'no').services.empty?
end
def types(condition = nil)
condition.nil? ? services_field('type') : filter(type: condition)
end
def enabled?
filter(disable: 'yes').services.empty?
end
def wait(condition = nil)
condition.nil? ? services_field('wait') : filter(wait: condition)
end
def params
return @params if defined?(@params)
return @params = {} if read_content.nil?
flat_params = parse_xinetd(read_content)
@params = { 'services' => {} }
flat_params.each do |k, v|
name = k[/^service (.+)$/, 1]
if name.nil?
@params[k] = v
else
@params['services'][name] = v
end
end
@params
end
def disabled?
filter(disable: 'no').services.empty?
end
def filter(conditions = {})
res = params.dup
filters = ''
conditions.each do |k, v|
v = v.to_s if v.is_a? Integer
filters += " #{k} = #{v.inspect}"
res['services'] = filter_by(res['services'], k.to_s, v)
end
XinetdConf.new(@conf_path, params: res, filters: filters)
end
def enabled?
filter(disable: 'yes').services.empty?
end
private
def params
return @params if defined?(@params)
return @params = {} if read_content.nil?
flat_params = parse_xinetd(read_content)
@params = { 'services' => {} }
flat_params.each do |k, v|
name = k[/^service (.+)$/, 1]
if name.nil?
@params[k] = v
# Retrieve the provided field from all configured services.
#
# @param [String] field name, e.g. `socket_type`
# @return [Array[String]] all values of this field across services
def services_field(field)
params['services'].values.compact.flatten
.map { |x| x.params[field] }.flatten.compact
end
def match_condition(sth, condition)
case sth
# this does Regex-matching as well as string comparison
when condition
true
else
@params['services'][name] = v
false
end
end
@params
end
def filter(conditions = {})
res = params.dup
filters = ''
conditions.each do |k, v|
v = v.to_s if v.is_a? Integer
filters += " #{k} = #{v.inspect}"
res['services'] = filter_by(res['services'], k.to_s, v)
end
XinetdConf.new(@conf_path, params: res, filters: filters)
end
private
# Retrieve the provided field from all configured services.
#
# @param [String] field name, e.g. `socket_type`
# @return [Array[String]] all values of this field across services
def services_field(field)
params['services'].values.compact.flatten
.map { |x| x.params[field] }.flatten.compact
end
def match_condition(sth, condition)
case sth
# this does Regex-matching as well as string comparison
when condition
true
else
false
end
end
# Filter services by a criteria. This allows for search queries for
# certain values.
#
# @param [Hash] service collection
# @param [String] search key you want to query
# @param [Any] search value that the key should match
# @return [Hash] filtered service collection
def filter_by(services, k, v)
if k == 'service'
return Hash[services.find_all { |name, _| match_condition(v, name) }]
end
Hash[services.map { |name, service_arr|
found = service_arr.find_all { |service|
match_condition(service.params[k], v)
}
found.empty? ? nil : [name, found]
}.compact]
end
def read_content(path = @conf_path)
return @contents[path] if @contents.key?(path)
file = inspec.file(path)
if !file.file?
return skip_resource "Can't find file \"#{path}\""
# Filter services by a criteria. This allows for search queries for
# certain values.
#
# @param [Hash] service collection
# @param [String] search key you want to query
# @param [Any] search value that the key should match
# @return [Hash] filtered service collection
def filter_by(services, k, v)
if k == 'service'
return Hash[services.find_all { |name, _| match_condition(v, name) }]
end
Hash[services.map { |name, service_arr|
found = service_arr.find_all { |service|
match_condition(service.params[k], v)
}
found.empty? ? nil : [name, found]
}.compact]
end
@contents[path] = file.content
if @contents[path].empty? && file.size > 0
return skip_resource "Can't read file \"#{path}\""
end
def read_content(path = @conf_path)
return @contents[path] if @contents.key?(path)
file = inspec.file(path)
if !file.file?
return skip_resource "Can't find file \"#{path}\""
end
@contents[path]
@contents[path] = file.content
if @contents[path].empty? && file.size > 0
return skip_resource "Can't read file \"#{path}\""
end
@contents[path]
end
end
end

View file

@ -9,21 +9,23 @@ require 'yaml'
# describe yaml('.kitchen.yaml') do
# its('driver.name') { should eq('vagrant') }
# end
class YamlConfig < JsonConfig
name 'yaml'
desc 'Use the yaml InSpec audit resource to test configuration data in a YAML file.'
example "
describe yaml do
its('name') { should eq 'foo' }
module Inspec::Resources
class YamlConfig < JsonConfig
name 'yaml'
desc 'Use the yaml InSpec audit resource to test configuration data in a YAML file.'
example "
describe yaml do
its('name') { should eq 'foo' }
end
"
# override file load and parse hash from yaml
def parse(content)
YAML.load(content)
end
"
# override file load and parse hash from yaml
def parse(content)
YAML.load(content)
end
def to_s
"YAML #{@path}"
def to_s
"YAML #{@path}"
end
end
end

View file

@ -30,132 +30,134 @@ require 'resources/file'
# it { should be_enabled }
# end
class Yum < Inspec.resource(1)
name 'yum'
desc 'Use the yum InSpec audit resource to test packages in the Yum repository.'
example "
describe yum.repo('name') do
it { should exist }
it { should be_enabled }
end
"
# returns all repositories
# works as following:
# search for Repo-id
# parse data in hashmap
# store data in object
# until \n
def repositories
return @cache if defined?(@cache)
# parse the repository data from yum
# we cannot use -C, because this is not reliable and may lead to errors
@command_result = inspec.command('yum -v repolist all')
@content = @command_result.stdout
@cache = []
repo = {}
in_repo = false
@content.each_line do |line|
# detect repo start
in_repo = true if line =~ /^\s*Repo-id\s*:\s*(.*)\b/
# detect repo end
if line == "\n" && in_repo
in_repo = false
@cache.push(repo)
repo = {}
module Inspec::Resources
class Yum < Inspec.resource(1)
name 'yum'
desc 'Use the yum InSpec audit resource to test packages in the Yum repository.'
example "
describe yum.repo('name') do
it { should exist }
it { should be_enabled }
end
# parse repo content
if in_repo == true
val = /^\s*([^:]*?)\s*:\s*(.*?)\s*$/.match(line)
repo[repo_key(strip(val[1]))] = strip(val[2])
"
# returns all repositories
# works as following:
# search for Repo-id
# parse data in hashmap
# store data in object
# until \n
def repositories
return @cache if defined?(@cache)
# parse the repository data from yum
# we cannot use -C, because this is not reliable and may lead to errors
@command_result = inspec.command('yum -v repolist all')
@content = @command_result.stdout
@cache = []
repo = {}
in_repo = false
@content.each_line do |line|
# detect repo start
in_repo = true if line =~ /^\s*Repo-id\s*:\s*(.*)\b/
# detect repo end
if line == "\n" && in_repo
in_repo = false
@cache.push(repo)
repo = {}
end
# parse repo content
if in_repo == true
val = /^\s*([^:]*?)\s*:\s*(.*?)\s*$/.match(line)
repo[repo_key(strip(val[1]))] = strip(val[2])
end
end
@cache
end
def repos
repositories.map { |repo| repo['id'] }
end
def repo(repo)
YumRepo.new(self, repo)
end
# alias for yum.repo('reponame')
def method_missing(name)
repo(name.to_s) if !name.nil?
end
def to_s
'Yum Repository'
end
private
# Removes lefthand and righthand whitespace
def strip(value)
value.strip if !value.nil?
end
# Optimize the key value
def repo_key(key)
return key if key.nil?
key.gsub('Repo-', '').downcase
end
@cache
end
def repos
repositories.map { |repo| repo['id'] }
class YumRepo
def initialize(yum, reponame)
@yum = yum
@reponame = reponame
end
# extracts the shortname from a repo id
# e.g. extras/7/x86_64 -> extras
def shortname(id)
val = %r{^\s*([^/]*?)/(.*?)\s*$}.match(id)
val.nil? ? nil : val[1]
end
def info
return @cache if defined?(@cache)
selection = @yum.repositories.select { |e| e['id'] == @reponame || shortname(e['id']) == @reponame }
@cache = selection[0] if !selection.nil? && selection.length == 1
@cache
end
def exist?
!info.nil?
end
def enabled?
repo = info
return false if repo.nil?
info['status'] == 'enabled'
end
end
def repo(repo)
YumRepo.new(self, repo)
end
# for compatability with serverspec
# this is deprecated syntax and will be removed in future versions
class YumRepoLegacy < Yum
name 'yumrepo'
# alias for yum.repo('reponame')
def method_missing(name)
repo(name.to_s) if !name.nil?
end
def initialize(name)
super()
@repository = repo(name)
end
def to_s
'Yum Repository'
end
def exists?
deprecated
@repository.exist?
end
private
def enabled?
deprecated
@repository.enabled?
end
# Removes lefthand and righthand whitespace
def strip(value)
value.strip if !value.nil?
end
# Optimize the key value
def repo_key(key)
return key if key.nil?
key.gsub('Repo-', '').downcase
end
end
class YumRepo
def initialize(yum, reponame)
@yum = yum
@reponame = reponame
end
# extracts the shortname from a repo id
# e.g. extras/7/x86_64 -> extras
def shortname(id)
val = %r{^\s*([^/]*?)/(.*?)\s*$}.match(id)
val.nil? ? nil : val[1]
end
def info
return @cache if defined?(@cache)
selection = @yum.repositories.select { |e| e['id'] == @reponame || shortname(e['id']) == @reponame }
@cache = selection[0] if !selection.nil? && selection.length == 1
@cache
end
def exist?
!info.nil?
end
def enabled?
repo = info
return false if repo.nil?
info['status'] == 'enabled'
end
end
# for compatability with serverspec
# this is deprecated syntax and will be removed in future versions
class YumRepoLegacy < Yum
name 'yumrepo'
def initialize(name)
super()
@repository = repo(name)
end
def exists?
deprecated
@repository.exist?
end
def enabled?
deprecated
@repository.enabled?
end
def deprecated
warn '[DEPRECATION] `yumrepo(reponame)` is deprecated. Please use `yum.repo(reponame)` instead.'
def deprecated
warn '[DEPRECATION] `yumrepo(reponame)` is deprecated. Please use `yum.repo(reponame)` instead.'
end
end
end

View file

@ -136,7 +136,7 @@ class MockLoader
'$Env:PATH' => cmd.call('$env-PATH'),
# registry key test (winrm 1.6.0, 1.6.1)
'2790db1e88204a073ed7fd3493f5445e5ce531afd0d2724a0e36c17110c535e6' => cmd.call('reg_schedule'),
'b00eb49a98c96a808c469e4894b5123a913e354c9ffea5b785898fe30d288ee0' => cmd.call('reg_schedule'),
'25a1a38fafc289a646d30f7aa966ce0901c267798f47abf2f9440e27d31a5b7d' => cmd.call('reg_schedule'),
'Auditpol /get /subcategory:\'User Account Management\' /r' => cmd.call('auditpol'),
'/sbin/auditctl -l' => cmd.call('auditctl'),
'/sbin/auditctl -s' => cmd.call('auditctl-s'),
@ -196,7 +196,7 @@ class MockLoader
'pw usershow root -7' => cmd.call('pw-usershow-root-7'),
# user info for windows (winrm 1.6.0, 1.6.1)
'650b6b72a66316418b25421a54afe21a230704558082914c54711904bb10e370' => cmd.call('GetUserAccount'),
'272e1d767fe6e28c86cfba1a75c3d458acade1f4a36cfd5e711b97884879de24' => cmd.call('GetUserAccount'),
'174686f0441b8dd387b35cf1cbeed3f98441544351de5d8fb7b54f655e75583f' => cmd.call('GetUserAccount'),
# group info for windows
'Get-WmiObject Win32_Group | Select-Object -Property Caption, Domain, Name, SID, LocalAccount | ConvertTo-Json' => cmd.call('GetWin32Group'),
# network interface