mirror of
https://github.com/inspec/inspec
synced 2024-12-18 00:53:22 +00:00
Merge pull request #526 from chef/adamleff/resource-namespace
Placing all resources in the Inspec::Resources namespace
This commit is contained in:
commit
9cb2bc5dec
52 changed files with 4779 additions and 4677 deletions
|
@ -4,26 +4,28 @@
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class Apache < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'apache'
|
class Apache < Inspec.resource(1)
|
||||||
|
name 'apache'
|
||||||
|
|
||||||
attr_reader :service, :conf_dir, :conf_path, :user
|
attr_reader :service, :conf_dir, :conf_path, :user
|
||||||
def initialize
|
def initialize
|
||||||
case inspec.os[:family]
|
case inspec.os[:family]
|
||||||
when 'ubuntu', 'debian'
|
when 'ubuntu', 'debian'
|
||||||
@service = 'apache2'
|
@service = 'apache2'
|
||||||
@conf_dir = '/etc/apache2/'
|
@conf_dir = '/etc/apache2/'
|
||||||
@conf_path = File.join @conf_dir, 'apache2.conf'
|
@conf_path = ::File.join @conf_dir, 'apache2.conf'
|
||||||
@user = 'www-data'
|
@user = 'www-data'
|
||||||
else
|
else
|
||||||
@service = 'httpd'
|
@service = 'httpd'
|
||||||
@conf_dir = '/etc/httpd/'
|
@conf_dir = '/etc/httpd/'
|
||||||
@conf_path = File.join @conf_dir, '/conf/httpd.conf'
|
@conf_path = ::File.join @conf_dir, '/conf/httpd.conf'
|
||||||
@user = 'apache'
|
@user = 'apache'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
'Apache Environment'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
|
||||||
'Apache Environment'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,118 +7,120 @@
|
||||||
require 'utils/simpleconfig'
|
require 'utils/simpleconfig'
|
||||||
require 'utils/find_files'
|
require 'utils/find_files'
|
||||||
|
|
||||||
class ApacheConf < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'apache_conf'
|
class ApacheConf < Inspec.resource(1)
|
||||||
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.'
|
name 'apache_conf'
|
||||||
example "
|
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.'
|
||||||
describe apache_conf do
|
example "
|
||||||
its('setting_name') { should eq 'value' }
|
describe apache_conf do
|
||||||
end
|
its('setting_name') { should eq 'value' }
|
||||||
"
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
"
|
||||||
content
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_content
|
include FindFiles
|
||||||
@content = ''
|
|
||||||
@params = {}
|
|
||||||
|
|
||||||
# skip if the main configuration file doesn't exist
|
def initialize(conf_path = nil)
|
||||||
file = inspec.file(@conf_path)
|
@conf_path = conf_path || inspec.apache.conf_path
|
||||||
if !file.file?
|
@conf_dir = ::File.dirname(@conf_path)
|
||||||
return skip_resource "Can't find file \"#{@conf_path}\""
|
@files_contents = {}
|
||||||
|
@content = nil
|
||||||
|
@params = nil
|
||||||
|
read_content
|
||||||
end
|
end
|
||||||
|
|
||||||
raw_conf = file.content
|
def content
|
||||||
if raw_conf.empty? && file.size > 0
|
@content ||= read_content
|
||||||
return skip_resource("Can't read file \"#{@conf_path}\"")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
to_read = [@conf_path]
|
def params(*opts)
|
||||||
until to_read.empty?
|
@params || read_content
|
||||||
raw_conf = read_file(to_read[0])
|
res = @params
|
||||||
@content += raw_conf
|
opts.each do |opt|
|
||||||
|
res = res[opt] unless res.nil?
|
||||||
# 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
|
||||||
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
# fiter comments
|
def method_missing(name)
|
||||||
@content = filter_comments @content
|
# ensure params are loaded
|
||||||
@content
|
@params || read_content
|
||||||
end
|
|
||||||
|
|
||||||
def include_files(params)
|
# extract values
|
||||||
# see if there is more config files to include
|
@params[name.to_s] unless @params.nil?
|
||||||
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
|
end
|
||||||
|
|
||||||
# [].flatten! == nil
|
def filter_comments(data)
|
||||||
includes.flatten! || []
|
content = ''
|
||||||
end
|
data.each_line do |line|
|
||||||
|
if !line.match(/^\s*#/)
|
||||||
|
content << line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
content
|
||||||
|
end
|
||||||
|
|
||||||
def read_file(path)
|
def read_content
|
||||||
@files_contents[path] ||= inspec.file(path).content
|
@content = ''
|
||||||
end
|
@params = {}
|
||||||
|
|
||||||
def to_s
|
# skip if the main configuration file doesn't exist
|
||||||
"Apache Config #{@conf_path}"
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,120 +28,122 @@
|
||||||
|
|
||||||
require 'uri'
|
require 'uri'
|
||||||
|
|
||||||
class AptRepository < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'apt'
|
class AptRepository < Inspec.resource(1)
|
||||||
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.'
|
name 'apt'
|
||||||
example "
|
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.'
|
||||||
describe apt('nginx/stable') do
|
example "
|
||||||
it { should exist }
|
describe apt('nginx/stable') do
|
||||||
it { should be_enabled }
|
it { should exist }
|
||||||
end
|
it { should be_enabled }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def initialize(ppa_name)
|
def initialize(ppa_name)
|
||||||
@deb_url = nil
|
@deb_url = nil
|
||||||
# check if the os is ubuntu or debian
|
# check if the os is ubuntu or debian
|
||||||
if inspec.os.debian?
|
if inspec.os.debian?
|
||||||
@deb_url = determine_ppa_url(ppa_name)
|
@deb_url = determine_ppa_url(ppa_name)
|
||||||
else
|
else
|
||||||
# this resource is only supported on ubuntu and debian
|
# this resource is only supported on ubuntu and debian
|
||||||
skip_resource 'The `apt` resource is not supported on your OS yet.'
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def exists?
|
# for compatability with serverspec
|
||||||
find_repo.count > 0
|
# this is deprecated syntax and will be removed in future versions
|
||||||
end
|
class PpaRepository < AptRepository
|
||||||
|
name 'ppa'
|
||||||
|
|
||||||
def enabled?
|
def exists?
|
||||||
return false if find_repo.count == 0
|
deprecated
|
||||||
actives = find_repo.map { |repo| repo[:active] }
|
super()
|
||||||
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
|
||||||
end
|
|
||||||
|
|
||||||
# resolves ppa urls
|
def enabled?
|
||||||
# @see http://bazaar.launchpad.net/~ubuntu-core-dev/software-properties/main/view/head:/softwareproperties/ppa.py
|
deprecated
|
||||||
def determine_ppa_url(ppa_url)
|
super()
|
||||||
# verify if we have the url already, then just return
|
end
|
||||||
return ppa_url if ppa_url =~ HTTP_URL_RE
|
|
||||||
# otherwise start generating the ppa url
|
|
||||||
|
|
||||||
# special care if the name stats with :
|
def deprecated
|
||||||
ppa_url = ppa_url.split(':')[1] if ppa_url.start_with?('ppa:')
|
warn '[DEPRECATION] `ppa(reponame)` is deprecated. Please use `apt(reponame)` instead.'
|
||||||
|
end
|
||||||
# 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.'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,40 +24,42 @@
|
||||||
#
|
#
|
||||||
# Further information is available at: https://msdn.microsoft.com/en-us/library/dd973859.aspx
|
# Further information is available at: https://msdn.microsoft.com/en-us/library/dd973859.aspx
|
||||||
|
|
||||||
class AuditPolicy < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'audit_policy'
|
class AuditPolicy < Inspec.resource(1)
|
||||||
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.'
|
name 'audit_policy'
|
||||||
example "
|
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.'
|
||||||
describe audit_policy do
|
example "
|
||||||
its('parameter') { should eq 'value' }
|
describe audit_policy do
|
||||||
end
|
its('parameter') { should eq 'value' }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def method_missing(method)
|
def method_missing(method)
|
||||||
key = method.to_s
|
key = method.to_s
|
||||||
|
|
||||||
# expected result:
|
# expected result:
|
||||||
# Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting
|
# Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting
|
||||||
# WIN-MB8NINQ388J,System,Kerberos Authentication Service,{0CCE9242-69AE-11D9-BED3-505054503030},No Auditing,
|
# WIN-MB8NINQ388J,System,Kerberos Authentication Service,{0CCE9242-69AE-11D9-BED3-505054503030},No Auditing,
|
||||||
result ||= inspec.command("Auditpol /get /subcategory:'#{key}' /r").stdout
|
result ||= inspec.command("Auditpol /get /subcategory:'#{key}' /r").stdout
|
||||||
|
|
||||||
# find line
|
# find line
|
||||||
target = nil
|
target = nil
|
||||||
result.each_line {|s|
|
result.each_line {|s|
|
||||||
target = s.strip if s =~ /\b.*#{key}.*\b/
|
target = s.strip if s =~ /\b.*#{key}.*\b/
|
||||||
}
|
}
|
||||||
|
|
||||||
# extract value
|
# extract value
|
||||||
values = nil
|
values = nil
|
||||||
unless target.nil?
|
unless target.nil?
|
||||||
# split csv values and return value
|
# split csv values and return value
|
||||||
values = target.split(',')[4]
|
values = target.split(',')[4]
|
||||||
|
end
|
||||||
|
|
||||||
|
values
|
||||||
end
|
end
|
||||||
|
|
||||||
values
|
def to_s
|
||||||
end
|
'Audit Policy'
|
||||||
|
end
|
||||||
def to_s
|
|
||||||
'Audit Policy'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,50 +6,52 @@
|
||||||
|
|
||||||
require 'utils/simpleconfig'
|
require 'utils/simpleconfig'
|
||||||
|
|
||||||
class AuditDaemonConf < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'auditd_conf'
|
class AuditDaemonConf < Inspec.resource(1)
|
||||||
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."
|
name 'auditd_conf'
|
||||||
example "
|
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."
|
||||||
describe auditd_conf do
|
example "
|
||||||
its('space_left_action') { should eq 'email' }
|
describe auditd_conf do
|
||||||
end
|
its('space_left_action') { should eq 'email' }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def initialize(path = nil)
|
def initialize(path = nil)
|
||||||
@conf_path = path || '/etc/audit/auditd.conf'
|
@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 = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
content = file.content
|
def method_missing(name)
|
||||||
if content.empty? && file.size > 0
|
read_params[name.to_s]
|
||||||
skip_resource "Can't read file '#{@conf_path}'"
|
|
||||||
return @params = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# parse the file
|
def to_s
|
||||||
conf = SimpleConfig.new(
|
'Audit Daemon Config'
|
||||||
content,
|
end
|
||||||
multiple_values: false,
|
|
||||||
)
|
private
|
||||||
@params = conf.params
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,197 +7,199 @@
|
||||||
require 'forwardable'
|
require 'forwardable'
|
||||||
require 'utils/filter_array'
|
require 'utils/filter_array'
|
||||||
|
|
||||||
class AuditdRulesLegacy
|
module Inspec::Resources
|
||||||
def initialize(content)
|
class AuditdRulesLegacy
|
||||||
@content = content
|
def initialize(content)
|
||||||
@opts = {
|
@content = content
|
||||||
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
@opts = {
|
||||||
multiple_values: true,
|
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/)}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# syntax for auditd >= 2.3
|
def params
|
||||||
describe auditd_rules.syscall('open').action do
|
@params ||= SimpleConfig.new(@content, @opts).params
|
||||||
it { should eq(['always']) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe auditd_rules.key('sshd_config') do
|
def method_missing(name)
|
||||||
its(:permissions) { should contain_match(/x/) }
|
params[name.to_s]
|
||||||
end
|
end
|
||||||
|
|
||||||
describe auditd_rules do
|
def status(name)
|
||||||
its(:lines) { should contain_match(%r{-w /etc/ssh/sshd_config/}) }
|
@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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize
|
def to_s
|
||||||
@content = inspec.command('/sbin/auditctl -l').stdout.chomp
|
'Audit Daemon Rules (for auditd version < 2.3)'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if @content =~ /^LIST_RULES:/
|
# rubocop:disable Metrics/ClassLength
|
||||||
# do not warn on centos 5
|
class AuditDaemonRules < Inspec.resource(1)
|
||||||
unless inspec.os[:family] == 'centos' && inspec.os[:release].to_i == 5
|
extend Forwardable
|
||||||
warn '[WARN] this version of auditd is outdated. Updating it allows for using more precise matchers.'
|
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
|
end
|
||||||
@legacy = AuditdRulesLegacy.new(@content)
|
|
||||||
else
|
|
||||||
parse_content
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# non-legacy instances are not asked for `its('LIST_RULES')`
|
# syntax for auditd >= 2.3
|
||||||
# rubocop:disable Style/MethodName
|
describe auditd_rules.syscall('open').action do
|
||||||
def LIST_RULES
|
it { should eq(['always']) }
|
||||||
return @legacy.LIST_RULES if @legacy
|
end
|
||||||
fail 'Using legacy auditd_rules LIST_RULES interface with non-legacy audit package. Please use the new syntax.'
|
|
||||||
end
|
|
||||||
|
|
||||||
def status(name = nil)
|
describe auditd_rules.key('sshd_config') do
|
||||||
return @legacy.status(name) if @legacy
|
its(:permissions) { should contain_match(/x/) }
|
||||||
|
end
|
||||||
|
|
||||||
@status_content ||= inspec.command('/sbin/auditctl -s').stdout.chomp
|
describe auditd_rules do
|
||||||
@status_params ||= Hash[@status_content.scan(/^([^ ]+) (.*)$/)]
|
its(:lines) { should contain_match(%r{-w /etc/ssh/sshd_config/}) }
|
||||||
|
end
|
||||||
|
"
|
||||||
|
|
||||||
return @status_params[name] if name
|
def initialize
|
||||||
@status_params
|
@content = inspec.command('/sbin/auditctl -l').stdout.chomp
|
||||||
end
|
|
||||||
|
|
||||||
def parse_content
|
if @content =~ /^LIST_RULES:/
|
||||||
@rules = {
|
# do not warn on centos 5
|
||||||
syscalls: [],
|
unless inspec.os[:family] == 'centos' && inspec.os[:release].to_i == 5
|
||||||
files: [],
|
warn '[WARN] this version of auditd is outdated. Updating it allows for using more precise matchers.'
|
||||||
}
|
|
||||||
@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
|
end
|
||||||
elsif is_file?(line)
|
@legacy = AuditdRulesLegacy.new(@content)
|
||||||
file = get_file line
|
else
|
||||||
perms = get_permissions line
|
parse_content
|
||||||
key = get_key line
|
|
||||||
|
|
||||||
@rules[:files] << { file: file, key: key, permissions: perms }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def syscall(name)
|
# non-legacy instances are not asked for `its('LIST_RULES')`
|
||||||
select_name(:syscall, name)
|
# rubocop:disable Style/MethodName
|
||||||
end
|
def LIST_RULES
|
||||||
|
return @legacy.LIST_RULES if @legacy
|
||||||
def file(name)
|
fail 'Using legacy auditd_rules LIST_RULES interface with non-legacy audit package. Please use the new syntax.'
|
||||||
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
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,114 +8,116 @@
|
||||||
# it { should have_interface 'eth0' }
|
# it { should have_interface 'eth0' }
|
||||||
# end
|
# end
|
||||||
|
|
||||||
class Bridge < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'bridge'
|
class Bridge < Inspec.resource(1)
|
||||||
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.'
|
name 'bridge'
|
||||||
example "
|
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.'
|
||||||
describe bridge 'br0' do
|
example "
|
||||||
it { should exist }
|
describe bridge 'br0' do
|
||||||
it { should have_interface 'eth0' }
|
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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(bridge_name)
|
def exists?
|
||||||
@bridge_name = bridge_name
|
!bridge_info.nil? && !bridge_info[:name].nil?
|
||||||
|
end
|
||||||
|
|
||||||
@bridge_provider = nil
|
def has_interface?(interface)
|
||||||
if inspec.os.linux?
|
return skip_resource 'The `bridge` resource does not provide interface detection for Windows yet' if inspec.os.windows?
|
||||||
@bridge_provider = LinuxBridge.new(inspec)
|
bridge_info.nil? ? false : bridge_info[:interfaces].include?(interface)
|
||||||
elsif inspec.os.windows?
|
end
|
||||||
@bridge_provider = WindowsBridge.new(inspec)
|
|
||||||
else
|
def interfaces
|
||||||
return skip_resource 'The `bridge` resource is not supported on your OS yet.'
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def exists?
|
class BridgeDetection
|
||||||
!bridge_info.nil? && !bridge_info[:name].nil?
|
attr_reader :inspec
|
||||||
end
|
def initialize(inspec)
|
||||||
|
@inspec = inspec
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# ensure we have an array of groups
|
# Linux Bridge
|
||||||
bridges = [bridges] if !bridges.is_a?(Array)
|
# 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
|
# load interface names
|
||||||
bridges = bridges.each_with_object([]) do |adapter, adapter_collection|
|
interfaces = inspec.command("ls -1 /sys/class/net/#{bridge_name}/brif/")
|
||||||
# map object
|
interfaces = interfaces.stdout.chomp.split("\n")
|
||||||
info = {
|
{
|
||||||
name: adapter['Name'],
|
name: bridge_name,
|
||||||
interfaces: nil,
|
interfaces: interfaces,
|
||||||
}
|
}
|
||||||
adapter_collection.push(info) if info[:name].casecmp(bridge_name) == 0
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return nil if bridges.size == 0
|
# Windows Bridge
|
||||||
warn "[Possible Error] detected multiple bridges interfaces with the name #{bridge_name}" if bridges.size > 1
|
# select netadapter by adapter binding for windows
|
||||||
bridges[0]
|
# 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
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,58 +4,60 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class Cmd < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'command'
|
class Cmd < Inspec.resource(1)
|
||||||
desc 'Use the command InSpec audit resource to test an arbitrary command that is run on the system.'
|
name 'command'
|
||||||
example "
|
desc 'Use the command InSpec audit resource to test an arbitrary command that is run on the system.'
|
||||||
describe command('ls -al /') do
|
example "
|
||||||
it { should exist }
|
describe command('ls -al /') do
|
||||||
its(:stdout) { should match /bin/ }
|
it { should exist }
|
||||||
its('stderr') { should eq '' }
|
its(:stdout) { should match /bin/ }
|
||||||
its(:exit_status) { should eq 0 }
|
its('stderr') { should eq '' }
|
||||||
|
its(:exit_status) { should eq 0 }
|
||||||
|
end
|
||||||
|
"
|
||||||
|
|
||||||
|
attr_reader :command
|
||||||
|
|
||||||
|
def initialize(cmd)
|
||||||
|
@command = cmd
|
||||||
end
|
end
|
||||||
"
|
|
||||||
|
|
||||||
attr_reader :command
|
def result
|
||||||
|
@result ||= inspec.backend.run_command(@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
|
|
||||||
end
|
end
|
||||||
res.exit_status.to_i == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
def stdout
|
||||||
"Command #{@command}"
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,29 +5,31 @@
|
||||||
# Parses a csv document
|
# Parses a csv document
|
||||||
# This implementation was inspired by a blog post
|
# This implementation was inspired by a blog post
|
||||||
# @see http://technicalpickles.com/posts/parsing-csv-with-ruby
|
# @see http://technicalpickles.com/posts/parsing-csv-with-ruby
|
||||||
class CsvConfig < JsonConfig
|
module Inspec::Resources
|
||||||
name 'csv'
|
class CsvConfig < JsonConfig
|
||||||
desc 'Use the csv InSpec audit resource to test configuration data in a CSV file.'
|
name 'csv'
|
||||||
example "
|
desc 'Use the csv InSpec audit resource to test configuration data in a CSV file.'
|
||||||
describe csv('example.csv') do
|
example "
|
||||||
its('name') { should eq(['John', 'Alice']) }
|
describe csv('example.csv') do
|
||||||
end
|
its('name') { should eq(['John', 'Alice']) }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
# override file load and parse hash from csv
|
# override file load and parse hash from csv
|
||||||
def parse(content)
|
def parse(content)
|
||||||
require 'csv'
|
require 'csv'
|
||||||
# convert empty field to nil
|
# convert empty field to nil
|
||||||
CSV::Converters[:blank_to_nil] = lambda do |field|
|
CSV::Converters[:blank_to_nil] = lambda do |field|
|
||||||
field && field.empty? ? nil : 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
|
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
|
def to_s
|
||||||
"Csv #{@path}"
|
"Csv #{@path}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,135 +24,137 @@
|
||||||
require 'utils/convert'
|
require 'utils/convert'
|
||||||
require 'utils/parser'
|
require 'utils/parser'
|
||||||
|
|
||||||
class EtcGroup < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
include Converter
|
class EtcGroup < Inspec.resource(1)
|
||||||
include CommentParser
|
include Converter
|
||||||
|
include CommentParser
|
||||||
|
|
||||||
name 'etc_group'
|
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.'
|
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 "
|
example "
|
||||||
describe etc_group do
|
describe etc_group do
|
||||||
its('gids') { should_not contain_duplicates }
|
its('gids') { should_not contain_duplicates }
|
||||||
its('groups') { should include 'my_user' }
|
its('groups') { should include 'my_user' }
|
||||||
its('users') { should include 'my_user' }
|
its('users') { should include 'my_user' }
|
||||||
end
|
end
|
||||||
"
|
"
|
||||||
|
|
||||||
attr_accessor :gid, :entries
|
attr_accessor :gid, :entries
|
||||||
def initialize(path = nil)
|
def initialize(path = nil)
|
||||||
@path = path || '/etc/group'
|
@path = path || '/etc/group'
|
||||||
@entries = parse_group(@path)
|
@entries = parse_group(@path)
|
||||||
|
|
||||||
# skip resource if it is not supported on current OS
|
# skip resource if it is not supported on current OS
|
||||||
return skip_resource 'The `etc_group` resource is not supported on your OS.' \
|
return skip_resource 'The `etc_group` resource is not supported on your OS.' \
|
||||||
unless inspec.os.unix?
|
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 }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
EtcGroupView.new(self, res)
|
def groups(filter = nil)
|
||||||
end
|
entries = filter || @entries
|
||||||
|
entries.map { |x| x['name'] } if !entries.nil?
|
||||||
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
|
end
|
||||||
# iterate over each line and filter comments
|
|
||||||
@content.split("\n").each_with_object([]) do |line, lines|
|
def gids(filter = nil)
|
||||||
grp_info = parse_group_line(line)
|
entries = filter || @entries
|
||||||
lines.push(grp_info) if !grp_info.nil? && grp_info.size > 0
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_group_line(line)
|
# object that hold a specifc view on etc group
|
||||||
opts = {
|
class EtcGroupView
|
||||||
comment_char: '#',
|
def initialize(parent, filter)
|
||||||
standalone_comments: false,
|
@parent = parent
|
||||||
}
|
@filter = filter
|
||||||
line, _idx_nl = parse_comment_line(line, opts)
|
end
|
||||||
x = line.split(':')
|
|
||||||
# abort if we have an empty or comment line
|
# returns the group object
|
||||||
return nil if x.size == 0
|
def entries
|
||||||
# map data
|
@filter
|
||||||
{
|
end
|
||||||
'name' => x.at(0), # Name of the group.
|
|
||||||
'password' => x.at(1), # Group's encrypted password.
|
# only returns group name
|
||||||
'gid' => convert_to_i(x.at(2)), # The group's decimal ID.
|
def groups
|
||||||
'members' => x.at(3), # Group members.
|
@parent.groups(@filter)
|
||||||
}
|
end
|
||||||
end
|
|
||||||
end
|
# only return gids
|
||||||
|
def gids
|
||||||
# object that hold a specifc view on etc group
|
@parent.gids(@filter)
|
||||||
class EtcGroupView
|
end
|
||||||
def initialize(parent, filter)
|
|
||||||
@parent = parent
|
# only returns users
|
||||||
@filter = filter
|
def users
|
||||||
end
|
@parent.users(@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
|
end
|
||||||
|
|
|
@ -2,47 +2,49 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
|
|
||||||
class GemPackage < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'gem'
|
class GemPackage < Inspec.resource(1)
|
||||||
desc 'Use the gem InSpec audit resource to test if a global gem package is installed.'
|
name 'gem'
|
||||||
example "
|
desc 'Use the gem InSpec audit resource to test if a global gem package is installed.'
|
||||||
describe gem('rubocop') do
|
example "
|
||||||
it { should be_installed }
|
describe gem('rubocop') do
|
||||||
|
it { should be_installed }
|
||||||
|
end
|
||||||
|
"
|
||||||
|
|
||||||
|
def initialize(package_name)
|
||||||
|
@package_name = package_name
|
||||||
end
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(package_name)
|
def info
|
||||||
@package_name = package_name
|
return @info if defined?(@info)
|
||||||
end
|
|
||||||
|
|
||||||
def info
|
cmd = inspec.command("gem list --local -a -q \^#{@package_name}\$")
|
||||||
return @info if defined?(@info)
|
@info = {
|
||||||
|
installed: cmd.exit_status == 0,
|
||||||
|
type: 'gem',
|
||||||
|
}
|
||||||
|
return @info unless @info[:installed]
|
||||||
|
|
||||||
cmd = inspec.command("gem list --local -a -q \^#{@package_name}\$")
|
# extract package name and version
|
||||||
@info = {
|
# parses data like winrm (1.3.4, 1.3.3)
|
||||||
installed: cmd.exit_status == 0,
|
params = /^\s*([^\(]*?)\s*\((.*?)\)\s*$/.match(cmd.stdout.chomp)
|
||||||
type: 'gem',
|
versions = params[2].split(',')
|
||||||
}
|
@info[:name] = params[1]
|
||||||
return @info unless @info[:installed]
|
@info[:version] = versions[0]
|
||||||
|
@info
|
||||||
|
end
|
||||||
|
|
||||||
# extract package name and version
|
def installed?
|
||||||
# parses data like winrm (1.3.4, 1.3.3)
|
info[:installed] == true
|
||||||
params = /^\s*([^\(]*?)\s*\((.*?)\)\s*$/.match(cmd.stdout.chomp)
|
end
|
||||||
versions = params[2].split(',')
|
|
||||||
@info[:name] = params[1]
|
|
||||||
@info[:version] = versions[0]
|
|
||||||
@info
|
|
||||||
end
|
|
||||||
|
|
||||||
def installed?
|
def version
|
||||||
info[:installed] == true
|
info[:version]
|
||||||
end
|
end
|
||||||
|
|
||||||
def version
|
def to_s
|
||||||
info[:version]
|
"gem package #{@package_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
|
||||||
"gem package #{@package_name}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,123 +13,125 @@
|
||||||
# it { should have_gid 0 }
|
# it { should have_gid 0 }
|
||||||
# end
|
# end
|
||||||
|
|
||||||
class Group < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'group'
|
class Group < Inspec.resource(1)
|
||||||
desc 'Use the group InSpec audit resource to test groups on the system.'
|
name 'group'
|
||||||
example "
|
desc 'Use the group InSpec audit resource to test groups on the system.'
|
||||||
describe group('root') do
|
example "
|
||||||
it { should exist }
|
describe group('root') do
|
||||||
its('gid') { should eq 0 }
|
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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(groupname, domain = nil)
|
# verifies if a group exists
|
||||||
@group = groupname.downcase
|
def exists?
|
||||||
@domain = domain
|
# ensure that we found one group
|
||||||
@domain = @domain.downcase unless @domain.nil?
|
!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
|
# the default case should be one group
|
||||||
@group_provider = nil
|
return group_info[0][:gid] if group_info.size == 1
|
||||||
if inspec.os.unix?
|
|
||||||
@group_provider = UnixGroup.new(inspec)
|
# return array if we got multiple gids
|
||||||
elsif inspec.os.windows?
|
group_info.map { |grp| grp[:gid] }
|
||||||
@group_provider = WindowsGroup.new(inspec)
|
end
|
||||||
else
|
|
||||||
return skip_resource 'The `group` resource is not supported on your OS yet.'
|
# 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
|
||||||
end
|
end
|
||||||
|
|
||||||
# verifies if a group exists
|
class GroupInfo
|
||||||
def exists?
|
attr_reader :inspec
|
||||||
# ensure that we found one group
|
def initialize(inspec)
|
||||||
!group_info.nil? && group_info.size > 0
|
@inspec = inspec
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def gid
|
# implements generic unix groups via /etc/group
|
||||||
return nil if group_info.nil? || group_info.size == 0
|
class UnixGroup < GroupInfo
|
||||||
|
def group_info(group, _domain = nil)
|
||||||
# the default case should be one group
|
inspec.etc_group.where(name: group).entries.map { |grp|
|
||||||
return group_info[0][:gid] if group_info.size == 1
|
{
|
||||||
|
name: grp['name'],
|
||||||
# return array if we got multiple gids
|
gid: grp['gid'],
|
||||||
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'],
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
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
|
||||||
|
end
|
||||||
|
|
||||||
# ensure we have an array of groups
|
class WindowsGroup < GroupInfo
|
||||||
groups = [groups] if !groups.is_a?(Array)
|
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
|
# cannot rely on exit code for now, successful command returns exit code 1
|
||||||
groups.each_with_object([]) do |grp, grp_collection|
|
# return nil if cmd.exit_status != 0, try to parse json
|
||||||
# map object
|
begin
|
||||||
grp_info = {
|
groups = JSON.parse(cmd.stdout)
|
||||||
name: grp['Name'],
|
rescue JSON::ParserError => _e
|
||||||
domain: grp['Domain'],
|
return nil
|
||||||
caption: grp['Caption'],
|
end
|
||||||
gid: nil,
|
|
||||||
sid: grp['SID'],
|
# ensure we have an array of groups
|
||||||
local: grp['LocalAccount'],
|
groups = [groups] if !groups.is_a?(Array)
|
||||||
}
|
|
||||||
return grp_collection.push(grp_info) if grp_info[:name].casecmp(compare_group) == 0 && (compare_domain.nil? || grp_info[:domain].casecmp(compare_domain) == 0)
|
# 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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,126 +24,128 @@
|
||||||
# it { should be_resolvable.by('dns') }
|
# it { should be_resolvable.by('dns') }
|
||||||
# end
|
# end
|
||||||
|
|
||||||
class Host < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'host'
|
class Host < Inspec.resource(1)
|
||||||
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.'
|
name 'host'
|
||||||
example "
|
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.'
|
||||||
describe host('example.com') do
|
example "
|
||||||
it { should be_reachable }
|
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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(hostname, params = {})
|
# if we get the IP adress, the host is resolvable
|
||||||
@hostname = hostname
|
def resolvable?(type = nil)
|
||||||
@port = params[:port] || nil
|
warn "The `host` resource ignores #{type} parameters. Continue to resolve host." if !type.nil?
|
||||||
@proto = params[:proto] || nil
|
resolve.nil? || resolve.empty? ? false : true
|
||||||
|
end
|
||||||
|
|
||||||
@host_provider = nil
|
def reachable?(port = nil, proto = nil, timeout = nil)
|
||||||
if inspec.os.linux?
|
fail "Use `host` resource with host('#{@hostname}', port: #{port}, proto: '#{proto}') parameters." if !port.nil? || !proto.nil? || !timeout.nil?
|
||||||
@host_provider = LinuxHostProvider.new(inspec)
|
ping.nil? ? false : ping
|
||||||
elsif inspec.os.windows?
|
end
|
||||||
@host_provider = WindowsHostProvider.new(inspec)
|
|
||||||
else
|
# returns all A records of the IP adress, will return an array
|
||||||
return skip_resource 'The `host` resource is not supported on your OS yet.'
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
# if we get the IP adress, the host is resolvable
|
class HostProvider
|
||||||
def resolvable?(type = nil)
|
attr_reader :inspec
|
||||||
warn "The `host` resource ignores #{type} parameters. Continue to resolve host." if !type.nil?
|
def initialize(inspec)
|
||||||
resolve.nil? || resolve.empty? ? false : true
|
@inspec = inspec
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reachable?(port = nil, proto = nil, timeout = nil)
|
class LinuxHostProvider < HostProvider
|
||||||
fail "Use `host` resource with host('#{@hostname}', port: #{port}, proto: '#{proto}') parameters." if !port.nil? || !proto.nil? || !timeout.nil?
|
# ping is difficult to achieve, since we are not sure
|
||||||
ping.nil? ? false : ping
|
def ping(hostname, _port = nil, _proto = nil)
|
||||||
end
|
# 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
|
||||||
# returns all A records of the IP adress, will return an array
|
ping = inspec.command("ping -w 1 -c 1 #{hostname}")
|
||||||
def ipaddress
|
ping.exit_status.to_i != 0 ? false : true
|
||||||
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
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def resolve(hostname)
|
# Windows
|
||||||
cmd = inspec.command("Resolve-DnsName –Type A #{hostname} | ConvertTo-Json")
|
# TODO: UDP is not supported yey, we need a custom ps1 script to add udp support
|
||||||
begin
|
# @see http://blogs.technet.com/b/josebda/archive/2015/04/18/windows-powershell-equivalents-for-common-networking-commands-ipconfig-ping-nslookup.aspx
|
||||||
resolv = JSON.parse(cmd.stdout)
|
# @see http://blogs.technet.com/b/heyscriptingguy/archive/2014/03/19/creating-a-port-scanner-with-windows-powershell.aspx
|
||||||
rescue JSON::ParserError => _e
|
class WindowsHostProvider < HostProvider
|
||||||
return nil
|
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
|
end
|
||||||
|
|
||||||
resolv = [resolv] unless resolv.is_a?(Array)
|
def resolve(hostname)
|
||||||
resolv.map { |entry| entry['IPAddress'] }
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,51 +6,53 @@
|
||||||
|
|
||||||
require 'utils/simpleconfig'
|
require 'utils/simpleconfig'
|
||||||
|
|
||||||
class InetdConf < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'inetd_conf'
|
class InetdConf < Inspec.resource(1)
|
||||||
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.'
|
name 'inetd_conf'
|
||||||
example "
|
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.'
|
||||||
describe inetd_conf do
|
example "
|
||||||
its('shell') { should eq nil }
|
describe inetd_conf do
|
||||||
its('login') { should eq nil }
|
its('shell') { should eq nil }
|
||||||
its('exec') { should eq nil }
|
its('login') { should eq nil }
|
||||||
end
|
its('exec') { should eq nil }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def initialize(path = nil)
|
def initialize(path = nil)
|
||||||
@conf_path = path || '/etc/inetd.conf'
|
@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 = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
content = file.content
|
def method_missing(name)
|
||||||
if content.empty? && file.size > 0
|
read_params[name.to_s]
|
||||||
skip_resource "Can't read file \"#{@conf_path}\""
|
|
||||||
return @params = {}
|
|
||||||
end
|
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
|
def read_params
|
||||||
'inetd.conf'
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,20 +4,22 @@
|
||||||
|
|
||||||
require 'utils/simpleconfig'
|
require 'utils/simpleconfig'
|
||||||
|
|
||||||
class IniConfig < JsonConfig
|
module Inspec::Resources
|
||||||
name 'ini'
|
class IniConfig < JsonConfig
|
||||||
desc 'Use the ini InSpec audit resource to test data in a INI file.'
|
name 'ini'
|
||||||
example "
|
desc 'Use the ini InSpec audit resource to test data in a INI file.'
|
||||||
descibe ini do
|
example "
|
||||||
its('auth_protocol') { should eq 'https' }
|
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
|
end
|
||||||
"
|
|
||||||
# override file load and parse hash with simple config
|
|
||||||
def parse(content)
|
|
||||||
SimpleConfig.new(content).params
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
"INI #{@path}"
|
"INI #{@path}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,124 +4,126 @@
|
||||||
|
|
||||||
require 'utils/convert'
|
require 'utils/convert'
|
||||||
|
|
||||||
class NetworkInterface < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'interface'
|
class NetworkInterface < Inspec.resource(1)
|
||||||
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).'
|
name 'interface'
|
||||||
example "
|
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).'
|
||||||
describe interface('eth0') do
|
example "
|
||||||
it { should exist }
|
describe interface('eth0') do
|
||||||
it { should be_up }
|
it { should exist }
|
||||||
its(:speed) { should eq 1000 }
|
it { should be_up }
|
||||||
end
|
its(:speed) { should eq 1000 }
|
||||||
"
|
end
|
||||||
def initialize(iface)
|
"
|
||||||
@iface = iface
|
def initialize(iface)
|
||||||
|
@iface = iface
|
||||||
|
|
||||||
@interface_provider = nil
|
@interface_provider = nil
|
||||||
if inspec.os.linux?
|
if inspec.os.linux?
|
||||||
@interface_provider = LinuxInterface.new(inspec)
|
@interface_provider = LinuxInterface.new(inspec)
|
||||||
elsif inspec.os.windows?
|
elsif inspec.os.windows?
|
||||||
@interface_provider = WindowsInterface.new(inspec)
|
@interface_provider = WindowsInterface.new(inspec)
|
||||||
else
|
else
|
||||||
return skip_resource 'The `interface` resource is not supported on your OS yet.'
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def exists?
|
class InterfaceInfo
|
||||||
!interface_info.nil? && !interface_info[:name].nil?
|
include Converter
|
||||||
end
|
attr_reader :inspec
|
||||||
|
def initialize(inspec)
|
||||||
def up?
|
@inspec = inspec
|
||||||
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'
|
|
||||||
end
|
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
|
||||||
end
|
|
||||||
|
|
||||||
class WindowsInterface < InterfaceInfo
|
class LinuxInterface < InterfaceInfo
|
||||||
def interface_info(iface)
|
def interface_info(iface)
|
||||||
# gather all network interfaces
|
# will return "[mtu]\n1500\n[type]\n1"
|
||||||
cmd = inspec.command('Get-NetAdapter | Select-Object -Property Name, InterfaceDescription, Status, State, MacAddress, LinkSpeed, ReceiveLinkSpeed, TransmitLinkSpeed, Virtual | ConvertTo-Json')
|
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
|
# parse values, we only recieve values, therefore we threat them as keys
|
||||||
begin
|
params = SimpleConfig.new(cmd.stdout.chomp).params
|
||||||
net_adapter = JSON.parse(cmd.stdout)
|
|
||||||
rescue JSON::ParserError => _e
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# ensure we have an array of groups
|
# abort if we got an empty result-set
|
||||||
net_adapter = [net_adapter] if !net_adapter.is_a?(Array)
|
return nil if params.empty?
|
||||||
|
|
||||||
# select the requested interface
|
# parse state
|
||||||
adapters = net_adapter.each_with_object([]) do |adapter, adapter_collection|
|
state = false
|
||||||
# map object
|
if params.key?('operstate')
|
||||||
info = {
|
operstate, _value = params['operstate'].first
|
||||||
name: adapter['Name'],
|
state = operstate == 'up'
|
||||||
up: adapter['State'] == 2,
|
end
|
||||||
speed: adapter['ReceiveLinkSpeed'] / 1000,
|
|
||||||
|
# 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
|
||||||
|
end
|
||||||
|
|
||||||
return nil if adapters.size == 0
|
class WindowsInterface < InterfaceInfo
|
||||||
warn "[Possible Error] detected multiple network interfaces with the name #{iface}" if adapters.size > 1
|
def interface_info(iface)
|
||||||
adapters[0]
|
# 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
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,48 +21,50 @@
|
||||||
# @see http://ipset.netfilter.org/iptables.man.html
|
# @see http://ipset.netfilter.org/iptables.man.html
|
||||||
# @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
|
# @see https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html
|
||||||
class IpTables < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'iptables'
|
class IpTables < Inspec.resource(1)
|
||||||
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.'
|
name 'iptables'
|
||||||
example "
|
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.'
|
||||||
describe iptables do
|
example "
|
||||||
it { should have_rule('-P INPUT ACCEPT') }
|
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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(params = {})
|
def has_rule?(rule = nil, _table = nil, _chain = nil)
|
||||||
@table = params[:table]
|
# checks if the rule is part of the ruleset
|
||||||
@chain = params[:chain]
|
# for now, we expect an exact match
|
||||||
|
retrieve_rules.any? { |line| line.casecmp(rule) == 0 }
|
||||||
|
end
|
||||||
|
|
||||||
# we're done if we are on linux
|
def retrieve_rules
|
||||||
return if inspec.os.linux?
|
return @iptables_cache if defined?(@iptables_cache)
|
||||||
|
|
||||||
# ensures, all calls are aborted for non-supported os
|
# construct iptables command to read all rules
|
||||||
@iptables_cache = []
|
table_cmd = "-t #{@table}" if @table
|
||||||
skip_resource 'The `iptables` resource is not supported on your OS yet.'
|
iptables_cmd = format('iptables %s -S %s', table_cmd, @chain).strip
|
||||||
end
|
|
||||||
|
|
||||||
def has_rule?(rule = nil, _table = nil, _chain = nil)
|
cmd = inspec.command(iptables_cmd)
|
||||||
# checks if the rule is part of the ruleset
|
return [] if cmd.exit_status.to_i != 0
|
||||||
# for now, we expect an exact match
|
|
||||||
retrieve_rules.any? { |line| line.casecmp(rule) == 0 }
|
|
||||||
end
|
|
||||||
|
|
||||||
def retrieve_rules
|
# split rules, returns array or rules
|
||||||
return @iptables_cache if defined?(@iptables_cache)
|
@iptables_cache = cmd.stdout.split("\n").map(&:strip)
|
||||||
|
end
|
||||||
|
|
||||||
# construct iptables command to read all rules
|
def to_s
|
||||||
table_cmd = "-t #{@table}" if @table
|
format('Iptables %s %s', @table && "table: #{@table}", @chain && "chain: #{@chain}").strip
|
||||||
iptables_cmd = format('iptables %s -S %s', table_cmd, @chain).strip
|
end
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,81 +2,83 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
|
|
||||||
class JsonConfig < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'json'
|
class JsonConfig < Inspec.resource(1)
|
||||||
desc 'Use the json InSpec audit resource to test data in a JSON file.'
|
name 'json'
|
||||||
example "
|
desc 'Use the json InSpec audit resource to test data in a JSON file.'
|
||||||
describe json('policyfile.lock.json') do
|
example "
|
||||||
its('cookbook_locks.omnibus.version') { should eq('2.2.0') }
|
describe json('policyfile.lock.json') do
|
||||||
end
|
its('cookbook_locks.omnibus.version') { should eq('2.2.0') }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
# make params readable
|
# make params readable
|
||||||
attr_reader :params
|
attr_reader :params
|
||||||
|
|
||||||
def initialize(path)
|
def initialize(path)
|
||||||
@path = path
|
@path = path
|
||||||
@file = inspec.file(@path)
|
@file = inspec.file(@path)
|
||||||
@file_content = @file.content
|
@file_content = @file.content
|
||||||
|
|
||||||
# check if file is available
|
# check if file is available
|
||||||
if !@file.file?
|
if !@file.file?
|
||||||
skip_resource "Can't find file \"#{@conf_path}\""
|
skip_resource "Can't find file \"#{@conf_path}\""
|
||||||
return @params = {}
|
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
|
end
|
||||||
|
|
||||||
# check if file is readable
|
def parse(content)
|
||||||
if @file_content.empty? && @file.size > 0
|
require 'json'
|
||||||
skip_resource "Can't read file \"#{@conf_path}\""
|
JSON.parse(content)
|
||||||
return @params = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@params = parse(@file_content)
|
def value(key)
|
||||||
end
|
extract_value(key, @params)
|
||||||
|
|
||||||
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]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# if there are no more keys, just return the value
|
# Shorthand to retrieve a parameter name via `#its`.
|
||||||
return value if keys.first.nil?
|
# Example: describe json('file') { its('paramX') { should eq 'Y' } }
|
||||||
# if there are more keys, extract more
|
#
|
||||||
extract_value(keys.clone, value)
|
# @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
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,39 +3,41 @@
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class KernelModule < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'kernel_module'
|
class KernelModule < Inspec.resource(1)
|
||||||
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.'
|
name 'kernel_module'
|
||||||
example "
|
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.'
|
||||||
describe kernel_module('bridge') do
|
example "
|
||||||
it { should be_loaded }
|
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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(modulename = nil)
|
def loaded?
|
||||||
@module = modulename
|
# 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
|
# get list of all modules
|
||||||
return skip_resource 'The `kernel_parameter` resource is not supported on your OS.' if !inspec.os.linux?
|
cmd = inspec.command(lsmod_cmd)
|
||||||
end
|
return false if cmd.exit_status != 0
|
||||||
|
|
||||||
def loaded?
|
# check if module is loaded
|
||||||
# default lsmod command
|
re = Regexp.new('^'+Regexp.quote(@module)+'\s')
|
||||||
lsmod_cmd = 'lsmod'
|
found = cmd.stdout.match(re)
|
||||||
# special care for CentOS 5 and sudo
|
!found.nil?
|
||||||
lsmod_cmd = '/sbin/lsmod' if inspec.os[:family] == 'centos' && inspec.os[:release].to_i == 5
|
end
|
||||||
|
|
||||||
# get list of all modules
|
def to_s
|
||||||
cmd = inspec.command(lsmod_cmd)
|
"Kernel Module #{@module}"
|
||||||
return false if cmd.exit_status != 0
|
end
|
||||||
|
|
||||||
# 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}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,56 +2,58 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class KernelParameter < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'kernel_parameter'
|
class KernelParameter < Inspec.resource(1)
|
||||||
desc 'Use the kernel_parameter InSpec audit resource to test kernel parameters on Linux platforms.'
|
name 'kernel_parameter'
|
||||||
example "
|
desc 'Use the kernel_parameter InSpec audit resource to test kernel parameters on Linux platforms.'
|
||||||
describe kernel_parameter('net.ipv4.conf.all.forwarding') do
|
example "
|
||||||
its(:value) { should eq 0 }
|
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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(parameter = nil)
|
def value
|
||||||
@parameter = parameter
|
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
|
def to_s
|
||||||
return skip_resource 'The `kernel_parameter` resource is not supported on your OS.' if !inspec.os.linux?
|
"Kernel Parameter #{@parameter}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def value
|
# for compatability with serverspec
|
||||||
cmd = inspec.command("/sbin/sysctl -q -n #{@parameter}")
|
# this is deprecated syntax and will be removed in future versions
|
||||||
return nil if cmd.exit_status != 0
|
class LinuxKernelParameter < KernelParameter
|
||||||
# remove whitespace
|
name 'linux_kernel_parameter'
|
||||||
cmd = cmd.stdout.chomp.strip
|
|
||||||
# convert to number if possible
|
|
||||||
cmd = cmd.to_i if cmd =~ /^\d+$/
|
|
||||||
cmd
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
def initialize(parameter)
|
||||||
"Kernel Parameter #{@parameter}"
|
super(parameter)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
def value
|
||||||
# for compatability with serverspec
|
deprecated
|
||||||
# this is deprecated syntax and will be removed in future versions
|
super()
|
||||||
class LinuxKernelParameter < KernelParameter
|
end
|
||||||
name 'linux_kernel_parameter'
|
|
||||||
|
def deprecated
|
||||||
def initialize(parameter)
|
warn '[DEPRECATION] `linux_kernel_parameter(parameter)` is deprecated. Please use `kernel_parameter(parameter)` instead.'
|
||||||
super(parameter)
|
end
|
||||||
end
|
|
||||||
|
def to_s
|
||||||
def value
|
"Kernel Parameter #{@parameter}"
|
||||||
deprecated
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -6,50 +6,52 @@
|
||||||
|
|
||||||
require 'utils/simpleconfig'
|
require 'utils/simpleconfig'
|
||||||
|
|
||||||
class LimitsConf < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'limits_conf'
|
class LimitsConf < Inspec.resource(1)
|
||||||
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.'
|
name 'limits_conf'
|
||||||
example "
|
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.'
|
||||||
describe limits_conf do
|
example "
|
||||||
its('*') { should include ['hard','core','0'] }
|
describe limits_conf do
|
||||||
end
|
its('*') { should include ['hard','core','0'] }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def initialize(path = nil)
|
def initialize(path = nil)
|
||||||
@conf_path = path || '/etc/security/limits.conf'
|
@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 = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
content = file.content
|
def method_missing(name)
|
||||||
if content.empty? && file.size > 0
|
read_params[name.to_s]
|
||||||
skip_resource "Can't read file \"#{@conf_path}\""
|
|
||||||
return @params = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# parse the file
|
def read_params
|
||||||
conf = SimpleConfig.new(
|
return @params if defined?(@params)
|
||||||
content,
|
|
||||||
assignment_re: /^\s*(\S+?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$/,
|
|
||||||
key_vals: 3,
|
|
||||||
multiple_values: true,
|
|
||||||
)
|
|
||||||
@params = conf.params
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
# read the file
|
||||||
'limits.conf'
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,49 +18,51 @@ require 'utils/simpleconfig'
|
||||||
# }
|
# }
|
||||||
# end
|
# end
|
||||||
|
|
||||||
class LoginDef < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'login_defs'
|
class LoginDef < Inspec.resource(1)
|
||||||
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.'
|
name 'login_defs'
|
||||||
example "
|
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.'
|
||||||
describe login_defs do
|
example "
|
||||||
its('ENCRYPT_METHOD') { should eq 'SHA512' }
|
describe login_defs do
|
||||||
end
|
its('ENCRYPT_METHOD') { should eq 'SHA512' }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def initialize(path = nil)
|
def initialize(path = nil)
|
||||||
@conf_path = path || '/etc/login.defs'
|
@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 = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
content = file.content
|
def method_missing(name)
|
||||||
if content.empty? && file.size > 0
|
read_params[name.to_s]
|
||||||
skip_resource "Can't read file \"#{@conf_path}\""
|
|
||||||
return @params = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# parse the file
|
def read_params
|
||||||
conf = SimpleConfig.new(
|
return @params if defined?(@params)
|
||||||
content,
|
|
||||||
assignment_re: /^\s*(\S+)\s+(\S*)\s*$/,
|
|
||||||
multiple_values: false,
|
|
||||||
)
|
|
||||||
@params = conf.params
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
# read the file
|
||||||
'login.defs'
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,54 +4,56 @@
|
||||||
|
|
||||||
require 'utils/simpleconfig'
|
require 'utils/simpleconfig'
|
||||||
|
|
||||||
class Mount < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'mount'
|
class Mount < Inspec.resource(1)
|
||||||
desc 'Use the mount InSpec audit resource to test if mount points.'
|
name 'mount'
|
||||||
example "
|
desc 'Use the mount InSpec audit resource to test if mount points.'
|
||||||
describe mount('/') do
|
example "
|
||||||
it { should be_mounted }
|
describe mount('/') do
|
||||||
its(:count) { should eq 1 }
|
it { should be_mounted }
|
||||||
its('device') { should eq '/dev/mapper/VolGroup-lv_root' }
|
its(:count) { should eq 1 }
|
||||||
its('type') { should eq 'ext4' }
|
its('device') { should eq '/dev/mapper/VolGroup-lv_root' }
|
||||||
its('options') { should eq ['rw', 'mode=620'] }
|
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
|
end
|
||||||
"
|
|
||||||
include MountParser
|
|
||||||
|
|
||||||
attr_reader :file
|
def mounted?
|
||||||
|
file.mounted?
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(path)
|
def count
|
||||||
@path = path
|
mounted = file.mounted
|
||||||
return skip_resource 'The `mount` resource is not supported on your OS yet.' if !inspec.os.linux?
|
return nil if mounted.nil? || mounted.stdout.nil?
|
||||||
@file = inspec.backend.file(@path)
|
mounted.stdout.lines.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def mounted?
|
def method_missing(name)
|
||||||
file.mounted?
|
return nil if !file.mounted?
|
||||||
end
|
|
||||||
|
|
||||||
def count
|
mounted = file.mounted
|
||||||
mounted = file.mounted
|
return nil if mounted.nil? || mounted.stdout.nil?
|
||||||
return nil if mounted.nil? || mounted.stdout.nil?
|
|
||||||
mounted.stdout.lines.count
|
|
||||||
end
|
|
||||||
|
|
||||||
def method_missing(name)
|
line = mounted.stdout
|
||||||
return nil if !file.mounted?
|
# 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
|
# parse content if we are on linux
|
||||||
return nil if mounted.nil? || mounted.stdout.nil?
|
@mount_options ||= parse_mount_options(line)
|
||||||
|
@mount_options[name]
|
||||||
|
end
|
||||||
|
|
||||||
line = mounted.stdout
|
def to_s
|
||||||
# if we got multiple lines, only use the last entry
|
"Mount #{@path}"
|
||||||
line = mounted.stdout.lines.to_a.last if mounted.stdout.lines.count > 1
|
end
|
||||||
|
|
||||||
# parse content if we are on linux
|
|
||||||
@mount_options ||= parse_mount_options(line)
|
|
||||||
@mount_options[name]
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
"Mount #{@path}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,78 +4,80 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class Mysql < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'mysql'
|
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
|
attr_reader :package, :service, :conf_dir, :conf_path, :data_dir, :log_dir, :log_path, :log_group, :log_dir_group
|
||||||
def initialize
|
def initialize
|
||||||
# set OS-dependent filenames and paths
|
# set OS-dependent filenames and paths
|
||||||
case inspec.os[:family]
|
case inspec.os[:family]
|
||||||
when 'ubuntu', 'debian'
|
when 'ubuntu', 'debian'
|
||||||
init_ubuntu
|
init_ubuntu
|
||||||
when 'redhat', 'fedora'
|
when 'redhat', 'fedora'
|
||||||
init_redhat
|
init_redhat
|
||||||
when 'arch'
|
when 'arch'
|
||||||
init_arch
|
init_arch
|
||||||
else
|
else
|
||||||
# TODO: could not detect
|
# TODO: could not detect
|
||||||
init_default
|
init_default
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def init_ubuntu
|
def init_ubuntu
|
||||||
@package = 'mysql-server'
|
@package = 'mysql-server'
|
||||||
@service = 'mysql'
|
@service = 'mysql'
|
||||||
@conf_path = '/etc/mysql/my.cnf'
|
@conf_path = '/etc/mysql/my.cnf'
|
||||||
@conf_dir = '/etc/mysql/'
|
@conf_dir = '/etc/mysql/'
|
||||||
@data_dir = '/var/lib/mysql/'
|
@data_dir = '/var/lib/mysql/'
|
||||||
@log_dir = '/var/log/'
|
@log_dir = '/var/log/'
|
||||||
@log_path = '/var/log/mysql.log'
|
@log_path = '/var/log/mysql.log'
|
||||||
@log_group = 'adm'
|
@log_group = 'adm'
|
||||||
case os[:release]
|
case os[:release]
|
||||||
when '14.04'
|
when '14.04'
|
||||||
@log_dir_group = 'syslog'
|
@log_dir_group = 'syslog'
|
||||||
else
|
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'
|
@log_dir_group = 'root'
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def init_redhat
|
def init_arch
|
||||||
@package = 'mysql-server'
|
@package = 'mariadb'
|
||||||
@service = 'mysqld'
|
@service = 'mysql'
|
||||||
@conf_path = '/etc/my.cnf'
|
@conf_path = '/etc/mysql/my.cnf'
|
||||||
@conf_dir = '/etc/'
|
@conf_dir = '/etc/mysql/'
|
||||||
@data_dir = '/var/lib/mysql/'
|
@data_dir = '/var/lib/mysql/'
|
||||||
@log_dir = '/var/log/'
|
@log_dir = '/var/log/'
|
||||||
@log_path = '/var/log/mysqld.log'
|
@log_path = '/var/log/mysql.log'
|
||||||
@log_group = 'mysql'
|
@log_group = 'mysql'
|
||||||
@log_dir_group = 'root'
|
@log_dir_group = 'root'
|
||||||
end
|
end
|
||||||
|
|
||||||
def init_arch
|
def init_default
|
||||||
@package = 'mariadb'
|
@service = 'mysqld'
|
||||||
@service = 'mysql'
|
@conf_path = '/etc/my.cnf'
|
||||||
@conf_path = '/etc/mysql/my.cnf'
|
@conf_dir = '/etc/'
|
||||||
@conf_dir = '/etc/mysql/'
|
@data_dir = '/var/lib/mysql/'
|
||||||
@data_dir = '/var/lib/mysql/'
|
@log_dir = '/var/log/'
|
||||||
@log_dir = '/var/log/'
|
@log_path = '/var/log/mysqld.log'
|
||||||
@log_path = '/var/log/mysql.log'
|
@log_group = 'mysql'
|
||||||
@log_group = 'mysql'
|
@log_dir_group = 'root'
|
||||||
@log_dir_group = 'root'
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def init_default
|
def to_s
|
||||||
@service = 'mysqld'
|
'MySQL'
|
||||||
@conf_path = '/etc/my.cnf'
|
end
|
||||||
@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'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,115 +8,117 @@ require 'utils/find_files'
|
||||||
require 'utils/hash'
|
require 'utils/hash'
|
||||||
require 'resources/mysql'
|
require 'resources/mysql'
|
||||||
|
|
||||||
class MysqlConfEntry
|
module Inspec::Resources
|
||||||
def initialize(path, params)
|
class MysqlConfEntry
|
||||||
@params = params
|
def initialize(path, params)
|
||||||
@path = path
|
@params = params
|
||||||
end
|
@path = path
|
||||||
|
|
||||||
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}\"")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
to_read = [@conf_path]
|
def method_missing(name, *_)
|
||||||
until to_read.empty?
|
k = name.to_s
|
||||||
cur_file = to_read[0]
|
res = @params[k]
|
||||||
raw_conf = read_file(cur_file)
|
return true if res.nil? && @params.key?(k)
|
||||||
@content += raw_conf
|
@params[k]
|
||||||
|
end
|
||||||
|
|
||||||
params = SimpleConfig.new(raw_conf).params
|
def to_s
|
||||||
@params = @params.deep_merge(params)
|
"MySQL Config entry [#{@path.join(' ')}]"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
to_read = to_read.drop(1)
|
class MysqlConf < Inspec.resource(1)
|
||||||
# see if there is more stuff to include
|
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.'
|
||||||
dir = File.dirname(cur_file)
|
example "
|
||||||
to_read += include_files(dir, raw_conf).find_all do |fp|
|
describe mysql_conf('path') do
|
||||||
not @files_contents.key? fp
|
its('setting') { should eq 'value' }
|
||||||
end
|
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
|
end
|
||||||
#
|
|
||||||
@content
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_files(reldir, conf)
|
def content
|
||||||
files = conf.scan(/^!include\s+(.*)\s*/).flatten.compact.map { |x| abs_path(reldir, x) }
|
@content ||= read_content
|
||||||
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
|
end
|
||||||
files
|
|
||||||
end
|
|
||||||
|
|
||||||
def abs_path(dir, f)
|
def params(*opts)
|
||||||
return f if f.start_with? '/'
|
@params || read_content
|
||||||
File.join(dir, f)
|
res = @params
|
||||||
end
|
opts.each do |opt|
|
||||||
|
res = res[opt] unless res.nil?
|
||||||
|
end
|
||||||
|
MysqlConfEntry.new(opts, res)
|
||||||
|
end
|
||||||
|
|
||||||
def read_file(path)
|
def method_missing(name)
|
||||||
@files_contents[path] ||= inspec.file(path).content
|
@params || read_content
|
||||||
end
|
@params[name.to_s]
|
||||||
|
end
|
||||||
|
|
||||||
def to_s
|
def read_content
|
||||||
'MySQL Configuration'
|
@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
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,56 +4,58 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class MysqlSession < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'mysql_session'
|
class MysqlSession < Inspec.resource(1)
|
||||||
desc 'Use the mysql_session InSpec audit resource to test SQL commands run against a MySQL database.'
|
name 'mysql_session'
|
||||||
example "
|
desc 'Use the mysql_session InSpec audit resource to test SQL commands run against a MySQL database.'
|
||||||
sql = mysql_session('my_user','password')
|
example "
|
||||||
describe sql.query('show databases like \'test\';') do
|
sql = mysql_session('my_user','password')
|
||||||
its(:stdout) { should_not match(/test/) }
|
describe sql.query('show databases like \'test\';') do
|
||||||
end
|
its(:stdout) { should_not match(/test/) }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def initialize(user = nil, pass = nil)
|
def initialize(user = nil, pass = nil)
|
||||||
@user = user
|
@user = user
|
||||||
@pass = pass
|
@pass = pass
|
||||||
init_fallback if user.nil? or pass.nil?
|
init_fallback if user.nil? or pass.nil?
|
||||||
skip_resource("Can't run MySQL SQL checks without authentication") 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.")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# return the raw command output
|
def query(q, db = '')
|
||||||
cmd
|
# TODO: simple escape, must be handled by a library
|
||||||
end
|
# that does this securely
|
||||||
|
escaped_query = q.gsub(/\\/, '\\\\').gsub(/"/, '\\"').gsub(/\$/, '\\$')
|
||||||
|
|
||||||
def to_s
|
# run the query
|
||||||
'MySQL Session'
|
cmd = inspec.command("mysql -u#{@user} -p#{@pass} #{db} -s -e \"#{escaped_query}\"")
|
||||||
end
|
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
|
def to_s
|
||||||
# support debian mysql administration login
|
'MySQL Session'
|
||||||
debian = inspec.command('test -f /etc/mysql/debian.cnf && cat /etc/mysql/debian.cnf').stdout
|
end
|
||||||
return if debian.empty?
|
|
||||||
|
|
||||||
user = debian.match(/^\s*user\s*=\s*([^ ]*)\s*$/)
|
private
|
||||||
pass = debian.match(/^\s*password\s*=\s*([^ ]*)\s*$/)
|
|
||||||
return if user.nil? or pass.nil?
|
def init_fallback
|
||||||
@user = user[1]
|
# support debian mysql administration login
|
||||||
@pass = pass[1]
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,45 +2,47 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
|
|
||||||
class NpmPackage < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'npm'
|
class NpmPackage < Inspec.resource(1)
|
||||||
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.'
|
name 'npm'
|
||||||
example "
|
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.'
|
||||||
describe npm('bower') do
|
example "
|
||||||
it { should be_installed }
|
describe npm('bower') do
|
||||||
|
it { should be_installed }
|
||||||
|
end
|
||||||
|
"
|
||||||
|
|
||||||
|
def initialize(package_name)
|
||||||
|
@package_name = package_name
|
||||||
|
@cache = nil
|
||||||
end
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(package_name)
|
def info
|
||||||
@package_name = package_name
|
return @info if defined?(@info)
|
||||||
@cache = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def info
|
cmd = inspec.command("npm ls -g --json #{@package_name}")
|
||||||
return @info if defined?(@info)
|
@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}")
|
pkgs = JSON.parse(cmd.stdout)
|
||||||
@info = {
|
@info[:version] = pkgs['dependencies'][@package_name]['version']
|
||||||
name: @package_name,
|
@info
|
||||||
type: 'npm',
|
end
|
||||||
installed: cmd.exit_status == 0,
|
|
||||||
}
|
|
||||||
return @info unless @info[:installed]
|
|
||||||
|
|
||||||
pkgs = JSON.parse(cmd.stdout)
|
def installed?
|
||||||
@info[:version] = pkgs['dependencies'][@package_name]['version']
|
info[:installed] == true
|
||||||
@info
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def installed?
|
def version
|
||||||
info[:installed] == true
|
info[:version]
|
||||||
end
|
end
|
||||||
|
|
||||||
def version
|
def to_s
|
||||||
info[:version]
|
"Npm Package #{@package_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
|
||||||
"Npm Package #{@package_name}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,53 +6,55 @@
|
||||||
|
|
||||||
require 'utils/simpleconfig'
|
require 'utils/simpleconfig'
|
||||||
|
|
||||||
class NtpConf < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'ntp_conf'
|
class NtpConf < Inspec.resource(1)
|
||||||
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.'
|
name 'ntp_conf'
|
||||||
example "
|
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.'
|
||||||
describe ntp_conf do
|
example "
|
||||||
its('server') { should_not eq nil }
|
describe ntp_conf do
|
||||||
its('restrict') { should include '-4 default kod notrap nomodify nopeer noquery'}
|
its('server') { should_not eq nil }
|
||||||
end
|
its('restrict') { should include '-4 default kod notrap nomodify nopeer noquery'}
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def initialize(path = nil)
|
def initialize(path = nil)
|
||||||
@conf_path = path || '/etc/ntp.conf'
|
@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 = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
content = inspec.file(@conf_path).content
|
def method_missing(name)
|
||||||
if content.empty? && inspec.file(@conf_path).size > 0
|
param = read_params[name.to_s]
|
||||||
skip_resource "Can't read file \"#{@conf_path}\""
|
# extract first value if we have only one value in array
|
||||||
return @params = {}
|
return param[0] if param.is_a?(Array) and param.length == 1
|
||||||
|
param
|
||||||
end
|
end
|
||||||
|
|
||||||
# parse the file
|
def to_s
|
||||||
conf = SimpleConfig.new(
|
'ntp.conf'
|
||||||
content,
|
end
|
||||||
assignment_re: /^\s*(\S+)\s+(.*)\s*$/,
|
|
||||||
multiple_values: true,
|
private
|
||||||
)
|
|
||||||
@params = conf.params
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,61 +9,63 @@
|
||||||
# describe oneget('zoomit') do
|
# describe oneget('zoomit') do
|
||||||
# it { should be_installed }
|
# it { should be_installed }
|
||||||
# end
|
# end
|
||||||
class OneGetPackage < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'oneget'
|
class OneGetPackage < Inspec.resource(1)
|
||||||
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.'
|
name 'oneget'
|
||||||
example "
|
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.'
|
||||||
describe oneget('zoomit') do
|
example "
|
||||||
it { should be_installed }
|
describe oneget('zoomit') do
|
||||||
end
|
it { should be_installed }
|
||||||
"
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
@info[:name] = pkgs['Name'] if pkgs.key?('Name')
|
def info
|
||||||
@info[:version] = pkgs['Version'] if pkgs.key?('Version')
|
return @info if defined?(@info)
|
||||||
@info
|
|
||||||
end
|
|
||||||
|
|
||||||
def installed?
|
@info = {}
|
||||||
info[:installed] == true
|
@info[:type] = 'oneget'
|
||||||
end
|
@info[:installed] = false
|
||||||
|
|
||||||
def version
|
cmd = inspec.command("Get-Package -Name '#{@package_name}' | ConvertTo-Json")
|
||||||
info[:version]
|
# cannot rely on exit code for now, successful command returns exit code 1
|
||||||
end
|
# return nil if cmd.exit_status != 0
|
||||||
|
# try to parse json
|
||||||
|
|
||||||
def to_s
|
begin
|
||||||
"OneGet Package #{@package_name}"
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,29 +2,31 @@
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
|
|
||||||
class OS < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'os'
|
class OS < Inspec.resource(1)
|
||||||
desc 'Use the os InSpec audit resource to test the platform on which the system is running.'
|
name 'os'
|
||||||
example "
|
desc 'Use the os InSpec audit resource to test the platform on which the system is running.'
|
||||||
describe os[:family] do
|
example "
|
||||||
it { should eq 'redhat' }
|
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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
# reuse helper methods from backend
|
def [](name)
|
||||||
%w{aix? redhat? debian? suse? bsd? solaris? linux? unix? windows?}.each do |os_family|
|
# convert string to symbol
|
||||||
define_method(os_family.to_sym) do
|
name = name.to_sym if name.is_a? String
|
||||||
inspec.backend.os.send(os_family)
|
inspec.backend.os[name]
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def [](name)
|
def to_s
|
||||||
# convert string to symbol
|
'Operating System Detection'
|
||||||
name = name.to_sym if name.is_a? String
|
end
|
||||||
inspec.backend.os[name]
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
'Operating System Detection'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,60 +13,62 @@
|
||||||
|
|
||||||
require 'utils/simpleconfig'
|
require 'utils/simpleconfig'
|
||||||
|
|
||||||
class OsEnv < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'os_env'
|
class OsEnv < Inspec.resource(1)
|
||||||
desc 'Use the os_env InSpec audit resource to test the environment variables for the platform on which the system is running.'
|
name 'os_env'
|
||||||
example "
|
desc 'Use the os_env InSpec audit resource to test the environment variables for the platform on which the system is running.'
|
||||||
describe os_env('VARIABLE') do
|
example "
|
||||||
its('matcher') { should eq 1 }
|
describe os_env('VARIABLE') do
|
||||||
end
|
its('matcher') { should eq 1 }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
attr_reader :content
|
attr_reader :content
|
||||||
def initialize(env = nil)
|
def initialize(env = nil)
|
||||||
@osenv = env
|
@osenv = env
|
||||||
@content = nil
|
@content = nil
|
||||||
@content = value_for(env) unless env.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}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if inspec.os.windows?
|
def split
|
||||||
out.stdout.strip
|
# we can't take advantage of `File::PATH_SEPARATOR` as code is
|
||||||
else
|
# evaluated on the host machine
|
||||||
params = SimpleConfig.new(out.stdout).params
|
path_separator = inspec.os.windows? ? ';' : ':'
|
||||||
params[env]
|
# -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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,253 +8,255 @@
|
||||||
# describe package('nginx') do
|
# describe package('nginx') do
|
||||||
# it { should be_installed }
|
# it { should be_installed }
|
||||||
# end
|
# end
|
||||||
class Package < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'package'
|
class Package < Inspec.resource(1)
|
||||||
desc 'Use the package InSpec audit resource to test if the named package and/or package version is installed on the system.'
|
name 'package'
|
||||||
example "
|
desc 'Use the package InSpec audit resource to test if the named package and/or package version is installed on the system.'
|
||||||
describe package('nginx') do
|
example "
|
||||||
it { should be_installed }
|
describe package('nginx') do
|
||||||
its('version') { should eq 1.9.5 }
|
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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(package_name = nil) # rubocop:disable Metrics/AbcSize
|
# returns true if the package is installed
|
||||||
@package_name = package_name
|
def installed?(_provider = nil, _version = nil)
|
||||||
@name = @package_name
|
return false if info.nil?
|
||||||
@cache = nil
|
info[:installed] == true
|
||||||
# select package manager
|
end
|
||||||
@pkgman = nil
|
|
||||||
|
|
||||||
os = inspec.os
|
# returns the package description
|
||||||
if os.debian?
|
def info
|
||||||
@pkgman = Deb.new(inspec)
|
return @cache if !@cache.nil?
|
||||||
elsif os.redhat? || os.suse?
|
return nil if @pkgman.nil?
|
||||||
@pkgman = Rpm.new(inspec)
|
@pkgman.info(@package_name)
|
||||||
elsif ['arch'].include?(os[:family])
|
end
|
||||||
@pkgman = Pacman.new(inspec)
|
|
||||||
elsif ['darwin'].include?(os[:family])
|
# return the package version
|
||||||
@pkgman = Brew.new(inspec)
|
def version
|
||||||
elsif inspec.os.windows?
|
info = @pkgman.info(@package_name)
|
||||||
@pkgman = WindowsPkg.new(inspec)
|
return nil if info.nil?
|
||||||
elsif ['aix'].include?(os[:family])
|
info[:version]
|
||||||
@pkgman = BffPkg.new(inspec)
|
end
|
||||||
elsif os.solaris?
|
|
||||||
@pkgman = SolarisPkg.new(inspec)
|
def to_s
|
||||||
else
|
"System Package #{@package_name}"
|
||||||
return skip_resource 'The `package` resource is not supported on your OS yet.'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns true if the package is installed
|
class PkgManagement
|
||||||
def installed?(_provider = nil, _version = nil)
|
attr_reader :inspec
|
||||||
return false if info.nil?
|
def initialize(inspec)
|
||||||
info[:installed] == true
|
@inspec = inspec
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# solaris 10
|
# Debian / Ubuntu
|
||||||
def solaris10_info(package_name)
|
class Deb < PkgManagement
|
||||||
cmd = inspec.command("pkginfo -l #{package_name}")
|
def info(package_name)
|
||||||
return nil if cmd.exit_status.to_i != 0
|
cmd = inspec.command("dpkg -s #{package_name}")
|
||||||
|
return nil if cmd.exit_status.to_i != 0
|
||||||
|
|
||||||
params = SimpleConfig.new(
|
params = SimpleConfig.new(
|
||||||
cmd.stdout.chomp,
|
cmd.stdout.chomp,
|
||||||
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
||||||
multiple_values: false,
|
multiple_values: false,
|
||||||
).params
|
).params
|
||||||
|
{
|
||||||
# parse 11.10.0,REV=2006.05.18.01.46
|
name: params['Package'],
|
||||||
v = params['VERSION'].split(',')
|
installed: true,
|
||||||
{
|
version: params['Version'],
|
||||||
name: params['PKGINST'],
|
type: 'deb',
|
||||||
installed: true,
|
}
|
||||||
version: v[0] + '-' + v[1].split('=')[1],
|
end
|
||||||
type: 'pkg',
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# solaris 11
|
# RHEL family
|
||||||
def solaris11_info(package_name)
|
class Rpm < PkgManagement
|
||||||
cmd = inspec.command("pkg info #{package_name}")
|
def info(package_name)
|
||||||
return nil if cmd.exit_status.to_i != 0
|
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(
|
# MacOS / Darwin implementation
|
||||||
cmd.stdout.chomp,
|
class Brew < PkgManagement
|
||||||
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
def info(package_name)
|
||||||
multiple_values: false,
|
cmd = inspec.command("brew info --json=v1 #{package_name}")
|
||||||
).params
|
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
|
||||||
name: params['Name'],
|
class Pacman < PkgManagement
|
||||||
installed: true,
|
def info(package_name)
|
||||||
# 0.5.11-0.175.3.1.0.5.0
|
cmd = inspec.command("pacman -Qi #{package_name}")
|
||||||
version: "#{params['Version']}-#{params['Branch']}",
|
return nil if cmd.exit_status.to_i != 0
|
||||||
type: 'pkg',
|
|
||||||
}
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,77 +13,79 @@
|
||||||
# }
|
# }
|
||||||
# describe parse_config(audit, options ) do
|
# describe parse_config(audit, options ) do
|
||||||
|
|
||||||
class PConfig < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'parse_config'
|
class PConfig < Inspec.resource(1)
|
||||||
desc 'Use the parse_config InSpec audit resource to test arbitrary configuration files.'
|
name 'parse_config'
|
||||||
example "
|
desc 'Use the parse_config InSpec audit resource to test arbitrary configuration files.'
|
||||||
output = command('some-command').stdout
|
example "
|
||||||
|
output = command('some-command').stdout
|
||||||
|
|
||||||
describe parse_config(output, { data_config_option: value } ) do
|
describe parse_config(output, { data_config_option: value } ) do
|
||||||
its('setting') { should eq 1 }
|
its('setting') { should eq 1 }
|
||||||
end
|
end
|
||||||
"
|
"
|
||||||
|
|
||||||
def initialize(content = nil, useropts = nil)
|
def initialize(content = nil, useropts = nil)
|
||||||
@opts = {}
|
@opts = {}
|
||||||
@opts = useropts.dup unless useropts.nil?
|
@opts = useropts.dup unless useropts.nil?
|
||||||
@files_contents = {}
|
@files_contents = {}
|
||||||
@params = nil
|
@params = nil
|
||||||
|
|
||||||
@content = content
|
@content = content
|
||||||
read_content if @content.nil?
|
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}\""
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def read_file(path)
|
class PConfigFile < PConfig
|
||||||
@files_contents[path] ||= inspec.file(path).content
|
name 'parse_config_file'
|
||||||
end
|
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
|
def initialize(path, opts = nil)
|
||||||
# parse the file
|
super(nil, opts)
|
||||||
@params = SimpleConfig.new(@content, @opts).params
|
parse_file(path)
|
||||||
@content
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
"Parse Config #{@conf_path}"
|
"Parse Config File #{@conf_path}"
|
||||||
end
|
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}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,112 +15,114 @@
|
||||||
|
|
||||||
require 'utils/parser'
|
require 'utils/parser'
|
||||||
|
|
||||||
class Passwd < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'passwd'
|
class Passwd < Inspec.resource(1)
|
||||||
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.'
|
name 'passwd'
|
||||||
example "
|
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.'
|
||||||
describe passwd do
|
example "
|
||||||
its('users') { should_not include 'forbidden_user' }
|
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
|
end
|
||||||
|
|
||||||
describe passwd.uids(0) do
|
def filter(hm = {})
|
||||||
its('users') { should cmp 'root' }
|
return self if hm.nil? || hm.empty?
|
||||||
its('count') { should eq 1 }
|
res = @params
|
||||||
end
|
filters = ''
|
||||||
|
hm.each do |attr, condition|
|
||||||
describe passwd.shells(/nologin/) do
|
condition = condition.to_s if condition.is_a? Integer
|
||||||
# find all users with a nologin shell
|
filters += " #{attr} = #{condition.inspect}"
|
||||||
its('users') { should_not include 'my_login_user' }
|
res = res.find_all do |line|
|
||||||
end
|
case line[attr.to_s]
|
||||||
"
|
when condition
|
||||||
|
true
|
||||||
include PasswdParser
|
else
|
||||||
|
false
|
||||||
attr_reader :uid
|
end
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
content = res.map { |x| x.values.join(':') }.join("\n")
|
||||||
|
Passwd.new(@path, content: content, filters: @filters + filters)
|
||||||
end
|
end
|
||||||
content = res.map { |x| x.values.join(':') }.join("\n")
|
|
||||||
Passwd.new(@path, content: content, filters: @filters + filters)
|
|
||||||
end
|
|
||||||
|
|
||||||
def usernames
|
def usernames
|
||||||
warn '[DEPRECATION] `passwd.usernames` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
|
warn '[DEPRECATION] `passwd.usernames` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
|
||||||
users
|
users
|
||||||
end
|
end
|
||||||
|
|
||||||
def username
|
def username
|
||||||
warn '[DEPRECATION] `passwd.user` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
|
warn '[DEPRECATION] `passwd.user` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
|
||||||
users[0]
|
users[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
def uid(x)
|
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.'
|
warn '[DEPRECATION] `passwd.uid(arg)` is deprecated. Please use `passwd.uids(arg)` instead. It will be removed in version 1.0.0.'
|
||||||
uids(x)
|
uids(x)
|
||||||
end
|
end
|
||||||
|
|
||||||
def users(name = nil)
|
def users(name = nil)
|
||||||
name.nil? ? map_data('user') : filter(user: name)
|
name.nil? ? map_data('user') : filter(user: name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def passwords(password = nil)
|
def passwords(password = nil)
|
||||||
password.nil? ? map_data('password') : filter(password: password)
|
password.nil? ? map_data('password') : filter(password: password)
|
||||||
end
|
end
|
||||||
|
|
||||||
def uids(uid = nil)
|
def uids(uid = nil)
|
||||||
uid.nil? ? map_data('uid') : filter(uid: uid)
|
uid.nil? ? map_data('uid') : filter(uid: uid)
|
||||||
end
|
end
|
||||||
|
|
||||||
def gids(gid = nil)
|
def gids(gid = nil)
|
||||||
gid.nil? ? map_data('gid') : filter(gid: gid)
|
gid.nil? ? map_data('gid') : filter(gid: gid)
|
||||||
end
|
end
|
||||||
|
|
||||||
def homes(home = nil)
|
def homes(home = nil)
|
||||||
home.nil? ? map_data('home') : filter(home: home)
|
home.nil? ? map_data('home') : filter(home: home)
|
||||||
end
|
end
|
||||||
|
|
||||||
def shells(shell = nil)
|
def shells(shell = nil)
|
||||||
shell.nil? ? map_data('shell') : filter(shell: shell)
|
shell.nil? ? map_data('shell') : filter(shell: shell)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
f = @filters.empty? ? '' : ' with'+@filters
|
f = @filters.empty? ? '' : ' with'+@filters
|
||||||
"/etc/passwd#{f}"
|
"/etc/passwd#{f}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def count
|
def count
|
||||||
@params.length
|
@params.length
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def map_data(id)
|
def map_data(id)
|
||||||
@params.map { |x| x[id] }
|
@params.map { |x| x[id] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,75 +7,77 @@
|
||||||
# it { should be_installed }
|
# it { should be_installed }
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
class PipPackage < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'pip'
|
class PipPackage < Inspec.resource(1)
|
||||||
desc 'Use the pip InSpec audit resource to test packages that are installed using the pip installer.'
|
name 'pip'
|
||||||
example "
|
desc 'Use the pip InSpec audit resource to test packages that are installed using the pip installer.'
|
||||||
describe pip('Jinja2') do
|
example "
|
||||||
it { should be_installed }
|
describe pip('Jinja2') do
|
||||||
end
|
it { should be_installed }
|
||||||
"
|
|
||||||
|
|
||||||
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
|
||||||
|
"
|
||||||
|
|
||||||
|
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
|
end
|
||||||
pipcmd || 'pip'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,413 +17,415 @@ require 'utils/parser'
|
||||||
#
|
#
|
||||||
# TODO: currently we return local ip only
|
# TODO: currently we return local ip only
|
||||||
# TODO: improve handling of same port on multiple interfaces
|
# TODO: improve handling of same port on multiple interfaces
|
||||||
class Port < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'port'
|
class Port < Inspec.resource(1)
|
||||||
desc "Use the port InSpec audit resource to test basic port properties, such as port, process, if it's listening."
|
name 'port'
|
||||||
example "
|
desc "Use the port InSpec audit resource to test basic port properties, such as port, process, if it's listening."
|
||||||
describe port(80) do
|
example "
|
||||||
it { should be_listening }
|
describe port(80) do
|
||||||
its('protocols') {should eq ['tcp']}
|
it { should be_listening }
|
||||||
end
|
its('protocols') {should eq ['tcp']}
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def initialize(ip = nil, port) # rubocop:disable OptionalArguments
|
def initialize(ip = nil, port) # rubocop:disable OptionalArguments
|
||||||
@ip = ip
|
@ip = ip
|
||||||
@port = port
|
@port = port
|
||||||
@port_manager = nil
|
@port_manager = nil
|
||||||
@cache = nil
|
@cache = nil
|
||||||
os = inspec.os
|
os = inspec.os
|
||||||
if os.linux?
|
if os.linux?
|
||||||
@port_manager = LinuxPorts.new(inspec)
|
@port_manager = LinuxPorts.new(inspec)
|
||||||
elsif %w{darwin aix}.include?(os[:family])
|
elsif %w{darwin aix}.include?(os[:family])
|
||||||
# AIX: see http://www.ibm.com/developerworks/aix/library/au-lsof.html#resources
|
# 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
|
# 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
|
# Darwin: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/lsof.8.html
|
||||||
@port_manager = LsofPorts.new(inspec)
|
@port_manager = LsofPorts.new(inspec)
|
||||||
elsif os.windows?
|
elsif os.windows?
|
||||||
@port_manager = WindowsPorts.new(inspec)
|
@port_manager = WindowsPorts.new(inspec)
|
||||||
elsif ['freebsd'].include?(os[:family])
|
elsif ['freebsd'].include?(os[:family])
|
||||||
@port_manager = FreeBsdPorts.new(inspec)
|
@port_manager = FreeBsdPorts.new(inspec)
|
||||||
elsif os.solaris?
|
elsif os.solaris?
|
||||||
@port_manager = SolarisPorts.new(inspec)
|
@port_manager = SolarisPorts.new(inspec)
|
||||||
else
|
else
|
||||||
return skip_resource 'The `port` resource is not supported on your OS yet.'
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def listening?(_protocol = nil, _local_address = nil)
|
# implements an info method and returns all ip adresses and protocols for
|
||||||
info.size > 0
|
# each port
|
||||||
end
|
# [{
|
||||||
|
# port: 22,
|
||||||
def protocols
|
# address: '0.0.0.0'
|
||||||
res = info.map { |x| x[:protocol] }.uniq.compact
|
# protocol: 'tcp'
|
||||||
res.size > 0 ? res : nil
|
# },
|
||||||
end
|
# {
|
||||||
|
# port: 22,
|
||||||
def processes
|
# address: '::'
|
||||||
res = info.map { |x| x[:process] }.uniq.compact
|
# protocol: 'tcp6'
|
||||||
res.size > 0 ? res : nil
|
# }]
|
||||||
end
|
class PortsInfo
|
||||||
|
attr_reader :inspec
|
||||||
def pids
|
def initialize(inspec)
|
||||||
res = info.map { |x| x[:pid] }.uniq.compact
|
@inspec = inspec
|
||||||
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
|
|
||||||
end
|
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|
|
begin
|
||||||
{
|
ports = JSON.parse(cmd.stdout)
|
||||||
port: x['LocalPort'],
|
rescue JSON::ParserError => _e
|
||||||
address: x['LocalAddress'],
|
return nil
|
||||||
protocol: 'tcp',
|
end
|
||||||
process: nil,
|
|
||||||
pid: nil,
|
return nil if ports.nil?
|
||||||
|
|
||||||
|
ports.map { |x|
|
||||||
|
{
|
||||||
|
port: x['LocalPort'],
|
||||||
|
address: x['LocalAddress'],
|
||||||
|
protocol: 'tcp',
|
||||||
|
process: nil,
|
||||||
|
pid: nil,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
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
|
# extracts udp and tcp ports from the lsof command
|
||||||
ports = []
|
class LsofPorts < PortsInfo
|
||||||
|
attr_reader :lsof
|
||||||
|
|
||||||
# check that lsof is available, otherwise fail
|
def initialize(inspec, lsofpath = nil)
|
||||||
fail 'Please ensure `lsof` is available on the machine.' if !inspec.command(@lsof.to_s).exist?
|
@lsof = lsofpath || 'lsof'
|
||||||
|
super(inspec)
|
||||||
# -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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
ports
|
def info
|
||||||
end
|
ports = []
|
||||||
|
|
||||||
# rubocop:disable Metrics/CyclomaticComplexity
|
# check that lsof is available, otherwise fail
|
||||||
# rubocop:disable Metrics/AbcSize
|
fail 'Please ensure `lsof` is available on the machine.' if !inspec.command(@lsof.to_s).exist?
|
||||||
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('->')
|
|
||||||
|
|
||||||
# skip active comm streams
|
# -F p=pid, c=command, P=protocol name, t=type, n=internet addresses
|
||||||
next if dst
|
# 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]
|
# map to desired return struct
|
||||||
|
lsof_parser(lsof_cmd).each do |process, port_ids|
|
||||||
# skip channels from port 0 - what does this mean?
|
pid, cmd = process.split(':')
|
||||||
next if port == '*'
|
port_ids.each do |port_str|
|
||||||
|
# should not break on ipv6 addresses
|
||||||
# create new array stub if !exist?
|
ipv, proto, port, host = port_str.split(':', 4)
|
||||||
procs[proc_id] = [] unless procs.key?(proc_id)
|
ports.push({ port: port.to_i,
|
||||||
|
address: host,
|
||||||
# change address '*' to zero
|
protocol: ipv == 'ipv6' ? proto + '6' : proto,
|
||||||
host = (port_id =~ /^ipv6:/) ? '[::]' : '0.0.0.0' if host == '*'
|
process: cmd,
|
||||||
# entrust URI to scrub the host and port
|
pid: pid.to_i })
|
||||||
begin
|
|
||||||
uri = URI("addr://#{host}:#{port}")
|
|
||||||
uri.host && uri.port
|
|
||||||
rescue => e
|
|
||||||
warn "could not parse URI 'addr://#{host}:#{port}' - #{e}"
|
|
||||||
next
|
|
||||||
end
|
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
|
||||||
|
|
||||||
|
ports
|
||||||
end
|
end
|
||||||
|
|
||||||
procs
|
# rubocop:disable Metrics/CyclomaticComplexity
|
||||||
end
|
# rubocop:disable Metrics/AbcSize
|
||||||
end
|
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
|
# skip active comm streams
|
||||||
class LinuxPorts < PortsInfo
|
next if dst
|
||||||
def info
|
|
||||||
cmd = inspec.command('netstat -tulpen')
|
|
||||||
return nil if cmd.exit_status.to_i != 0
|
|
||||||
|
|
||||||
ports = []
|
host, port = /^(\S+):(\d+|\*)$/.match(src)[1, 2]
|
||||||
# parse all lines
|
|
||||||
cmd.stdout.each_line do |line|
|
|
||||||
port_info = parse_netstat_line(line)
|
|
||||||
|
|
||||||
# only push protocols we are interested in
|
# skip channels from port 0 - what does this mean?
|
||||||
next unless %w{tcp tcp6 udp udp6}.include?(port_info[:protocol])
|
next if port == '*'
|
||||||
ports.push(port_info)
|
|
||||||
|
# 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
|
end
|
||||||
ports
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_net_address(net_addr, protocol)
|
# extract port information from netstat
|
||||||
if protocol.eql?('tcp6') || protocol.eql?('udp6')
|
class LinuxPorts < PortsInfo
|
||||||
# prep for URI parsing, parse ip6 port
|
def info
|
||||||
ip6 = /^(\S+):(\d+)$/.match(net_addr)
|
cmd = inspec.command('netstat -tulpen')
|
||||||
ip6addr = ip6[1]
|
return nil if cmd.exit_status.to_i != 0
|
||||||
ip6addr = '::' if ip6addr =~ /^:::$/
|
|
||||||
# build uri
|
ports = []
|
||||||
ip_addr = URI("addr://[#{ip6addr}]:#{ip6[2]}")
|
# parse all lines
|
||||||
# replace []
|
cmd.stdout.each_line do |line|
|
||||||
host = ip_addr.host[1..ip_addr.host.size-2]
|
port_info = parse_netstat_line(line)
|
||||||
else
|
|
||||||
ip_addr = URI('addr://'+net_addr)
|
# only push protocols we are interested in
|
||||||
host = ip_addr.host
|
next unless %w{tcp tcp6 udp udp6}.include?(port_info[:protocol])
|
||||||
|
ports.push(port_info)
|
||||||
|
end
|
||||||
|
ports
|
||||||
end
|
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
|
port = ip_addr.port
|
||||||
|
|
||||||
|
[host, port]
|
||||||
|
rescue URI::InvalidURIError => e
|
||||||
|
warn "Could not parse #{net_addr}, #{e}"
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
[host, port]
|
|
||||||
rescue URI::InvalidURIError => e
|
|
||||||
warn "Could not parse #{net_addr}, #{e}"
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_sockstat_line(line)
|
def parse_netstat_line(line)
|
||||||
# 1 - USER, 2 - COMMAND, 3 - PID, 4 - FD 5 - PROTO, 6 - LOCAL ADDRESS, 7 - FOREIGN ADDRESS
|
# parse each line
|
||||||
parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/.match(line)
|
# 1 - Proto, 2 - Recv-Q, 3 - Send-Q, 4 - Local Address, 5 - Foreign Address, 6 - State, 7 - Inode, 8 - PID/Program name
|
||||||
return {} if parsed.nil?
|
parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)?\s+(\S+)\s+(\S+)\s+(\S+)/.match(line)
|
||||||
|
|
||||||
# extract ip information
|
return {} if parsed.nil? || line.match(/^proto/i)
|
||||||
protocol = parsed[5].downcase
|
|
||||||
host, port = parse_net_address(parsed[6], protocol)
|
|
||||||
return {} if host.nil? or port.nil?
|
|
||||||
|
|
||||||
# extract process
|
# parse ip4 and ip6 addresses
|
||||||
process = parsed[2]
|
protocol = parsed[1].downcase
|
||||||
|
|
||||||
# extract PID
|
# detect protocol if not provided
|
||||||
pid = parsed[3]
|
protocol += '6' if parsed[4].count(':') > 1 && %w{tcp udp}.include?(protocol)
|
||||||
pid = pid.to_i if pid =~ /^\d+$/
|
|
||||||
|
|
||||||
# map tcp4 and udp4
|
# extract host and port information
|
||||||
protocol = 'tcp' if protocol.eql?('tcp4')
|
host, port = parse_net_address(parsed[4], protocol)
|
||||||
protocol = 'udp' if protocol.eql?('udp4')
|
|
||||||
|
|
||||||
# map data
|
# extract PID
|
||||||
{
|
process = parsed[9].split('/')
|
||||||
port: port,
|
pid = process[0]
|
||||||
address: host,
|
pid = pid.to_i if pid =~ /^\d+$/
|
||||||
protocol: protocol,
|
process = process[1]
|
||||||
process: process,
|
|
||||||
pid: pid,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class SolarisPorts < FreeBsdPorts
|
# map data
|
||||||
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,
|
port: port,
|
||||||
address: host,
|
address: host,
|
||||||
protocol: protocol,
|
protocol: protocol,
|
||||||
process: nil, # we do not have pid on solaris
|
process: process,
|
||||||
pid: nil, # we do not have pid on solaris
|
pid: pid,
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
ports
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,34 +4,36 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class Postgres < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'postgres'
|
class Postgres < Inspec.resource(1)
|
||||||
|
name 'postgres'
|
||||||
|
|
||||||
attr_reader :service, :data_dir, :conf_dir, :conf_path
|
attr_reader :service, :data_dir, :conf_dir, :conf_path
|
||||||
def initialize
|
def initialize
|
||||||
case inspec.os[:family]
|
case inspec.os[:family]
|
||||||
when 'ubuntu', 'debian'
|
when 'ubuntu', 'debian'
|
||||||
@service = 'postgresql'
|
@service = 'postgresql'
|
||||||
@data_dir = '/var/lib/postgresql'
|
@data_dir = '/var/lib/postgresql'
|
||||||
@version = inspec.command('ls /etc/postgresql/').stdout.chomp
|
@version = inspec.command('ls /etc/postgresql/').stdout.chomp
|
||||||
@conf_dir = "/etc/postgresql/#{@version}/main"
|
@conf_dir = "/etc/postgresql/#{@version}/main"
|
||||||
@conf_path = File.join @conf_dir, 'postgresql.conf'
|
@conf_path = ::File.join @conf_dir, 'postgresql.conf'
|
||||||
|
|
||||||
when 'arch'
|
when 'arch'
|
||||||
@service = 'postgresql'
|
@service = 'postgresql'
|
||||||
@data_dir = '/var/lib/postgres/data'
|
@data_dir = '/var/lib/postgres/data'
|
||||||
@conf_dir = '/var/lib/postgres/data'
|
@conf_dir = '/var/lib/postgres/data'
|
||||||
@conf_path = File.join @conf_dir, 'postgresql.conf'
|
@conf_path = ::File.join @conf_dir, 'postgresql.conf'
|
||||||
|
|
||||||
else
|
else
|
||||||
@service = 'postgresql'
|
@service = 'postgresql'
|
||||||
@data_dir = '/var/lib/postgresql'
|
@data_dir = '/var/lib/postgresql'
|
||||||
@conf_dir = '/var/lib/pgsql/data'
|
@conf_dir = '/var/lib/pgsql/data'
|
||||||
@conf_path = File.join @conf_dir, 'postgresql.conf'
|
@conf_path = ::File.join @conf_dir, 'postgresql.conf'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
'PostgreSQL'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
|
||||||
'PostgreSQL'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,86 +8,88 @@ require 'utils/simpleconfig'
|
||||||
require 'utils/find_files'
|
require 'utils/find_files'
|
||||||
require 'resources/postgres'
|
require 'resources/postgres'
|
||||||
|
|
||||||
class PostgresConf < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'postgres_conf'
|
class PostgresConf < Inspec.resource(1)
|
||||||
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.'
|
name 'postgres_conf'
|
||||||
example "
|
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.'
|
||||||
describe postgres_conf do
|
example "
|
||||||
its('max_connections') { should eq '5' }
|
describe postgres_conf do
|
||||||
end
|
its('max_connections') { should eq '5' }
|
||||||
"
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
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
|
end
|
||||||
@content
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_files(params)
|
def content
|
||||||
include_files = params['include'] || []
|
@content ||= read_content
|
||||||
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
|
end
|
||||||
include_files
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_file(path)
|
def params(*opts)
|
||||||
@files_contents[path] ||= inspec.file(path).content
|
@params || read_content
|
||||||
end
|
res = @params
|
||||||
|
opts.each do |opt|
|
||||||
|
res = res[opt] unless res.nil?
|
||||||
|
end
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
def to_s
|
def read_content
|
||||||
'PostgreSQL Configuration'
|
@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
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,59 +4,61 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class Lines
|
module Inspec::Resources
|
||||||
attr_reader :output
|
class Lines
|
||||||
|
attr_reader :output
|
||||||
|
|
||||||
def initialize(raw, desc)
|
def initialize(raw, desc)
|
||||||
@output = raw
|
@output = raw
|
||||||
@desc = desc
|
@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('') }
|
|
||||||
end
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(user, pass)
|
def lines
|
||||||
@user = user || 'postgres'
|
output.split("\n")
|
||||||
@pass = pass
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
@desc
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def query(query, db = [])
|
class PostgresSession < Inspec.resource(1)
|
||||||
dbs = db.map { |x| "-d #{x}" }.join(' ')
|
name 'postgres_session'
|
||||||
# TODO: simple escape, must be handled by a library
|
desc 'Use the postgres_session InSpec audit resource to test SQL commands run against a PostgreSQL database.'
|
||||||
# that does this securely
|
example "
|
||||||
escaped_query = query.gsub(/\\/, '\\\\').gsub(/"/, '\\"').gsub(/\$/, '\\$')
|
sql = postgres_session('username', 'password')
|
||||||
# run the query
|
|
||||||
cmd = inspec.command("PGPASSWORD='#{@pass}' psql -U #{@user} #{dbs} -h localhost -c \"#{escaped_query}\"")
|
describe sql.query('SELECT * FROM pg_shadow WHERE passwd IS NULL;') do
|
||||||
out = cmd.stdout + "\n" + cmd.stderr
|
its('output') { should eq('') }
|
||||||
if cmd.exit_status != 0 or
|
end
|
||||||
out =~ /could not connect to .*/ or
|
"
|
||||||
out.downcase =~ /^error/
|
|
||||||
# skip this test if the server can't run the query
|
def initialize(user, pass)
|
||||||
skip_resource "Can't read run query #{query.inspect} on postgres_session: #{out}"
|
@user = user || 'postgres'
|
||||||
else
|
@pass = pass
|
||||||
# remove the whole header (i.e. up to the first ^-----+------+------$)
|
end
|
||||||
# remove the tail
|
|
||||||
lines = cmd.stdout
|
def query(query, db = [])
|
||||||
.sub(/(.*\n)+([-]+[+])*[-]+\n/, '')
|
dbs = db.map { |x| "-d #{x}" }.join(' ')
|
||||||
.sub(/\n[^\n]*\n\n$/, '')
|
# TODO: simple escape, must be handled by a library
|
||||||
Lines.new(lines.strip, "PostgreSQL query: #{query}")
|
# 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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,70 +4,72 @@
|
||||||
# author: Christoph Hartmann
|
# author: Christoph Hartmann
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class Processes < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'processes'
|
class Processes < Inspec.resource(1)
|
||||||
desc 'Use the processes InSpec audit resource to test properties for programs that are running on the system.'
|
name 'processes'
|
||||||
example "
|
desc 'Use the processes InSpec audit resource to test properties for programs that are running on the system.'
|
||||||
describe processes('mysqld') do
|
example "
|
||||||
its('list.length') { should eq 1 }
|
describe processes('mysqld') do
|
||||||
its('users') { should eq ['mysql'] }
|
its('list.length') { should eq 1 }
|
||||||
its('states') { should include 'S' }
|
its('users') { should eq ['mysql'] }
|
||||||
end
|
its('states') { should include 'S' }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
attr_reader :list,
|
attr_reader :list,
|
||||||
:users,
|
:users,
|
||||||
:states
|
:states
|
||||||
|
|
||||||
def initialize(grep)
|
def initialize(grep)
|
||||||
# turn into a regexp if it isn't one yet
|
# turn into a regexp if it isn't one yet
|
||||||
if grep.class == String
|
if grep.class == String
|
||||||
grep = '(/[^/]*)*'+grep if grep[0] != '/'
|
grep = '(/[^/]*)*'+grep if grep[0] != '/'
|
||||||
grep = Regexp.new('^' + grep + '(\s|$)')
|
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
|
end
|
||||||
|
|
||||||
all_cmds = ps_aux
|
def to_s
|
||||||
@list = all_cmds.find_all do |hm|
|
'Processes'
|
||||||
hm[:command] =~ grep
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{ users: :user,
|
private
|
||||||
states: :stat }.each do |var, key|
|
|
||||||
instance_variable_set("@#{var}", @list.map { |l| l[key] }.uniq)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
def ps_aux
|
||||||
'Processes'
|
# get all running processes
|
||||||
end
|
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
|
lines.map do |m|
|
||||||
# get all running processes
|
{
|
||||||
cmd = inspec.command('ps aux')
|
user: m[1],
|
||||||
all = cmd.stdout.split("\n")[1..-1]
|
pid: m[2],
|
||||||
return [] if all.nil?
|
cpu: m[3],
|
||||||
|
mem: m[4],
|
||||||
lines = all.map do |line|
|
vsz: m[5],
|
||||||
# user 32296 0.0 0.0 42592 7972 pts/15 Ss+ Apr06 0:00 zsh
|
rss: m[6],
|
||||||
line.match(/^([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+(.*)$/)
|
tty: m[7],
|
||||||
end.compact
|
stat: m[8],
|
||||||
|
start: m[9],
|
||||||
lines.map do |m|
|
time: m[10],
|
||||||
{
|
command: m[11],
|
||||||
user: m[1],
|
}
|
||||||
pid: m[2],
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,174 +10,176 @@ require 'json'
|
||||||
# its('Start') { should eq 2 }
|
# its('Start') { should eq 2 }
|
||||||
# end
|
# end
|
||||||
|
|
||||||
class RegistryKey < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'registry_key'
|
class RegistryKey < Inspec.resource(1)
|
||||||
desc 'Use the registry_key InSpec audit resource to test key values in the Microsoft Windows registry.'
|
name 'registry_key'
|
||||||
example "
|
desc 'Use the registry_key InSpec audit resource to test key values in the Microsoft Windows registry.'
|
||||||
describe registry_key('path\to\key') do
|
example "
|
||||||
its('name') { should eq 'value' }
|
describe registry_key('path\to\key') do
|
||||||
end
|
its('name') { should eq 'value' }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
attr_accessor :reg_key
|
attr_accessor :reg_key
|
||||||
|
|
||||||
def initialize(name, reg_key = nil)
|
def initialize(name, reg_key = nil)
|
||||||
# if we have one parameter, we use it as name
|
# if we have one parameter, we use it as name
|
||||||
reg_key ||= name
|
reg_key ||= name
|
||||||
@name = name
|
@name = name
|
||||||
@reg_key = reg_key
|
@reg_key = reg_key
|
||||||
|
|
||||||
return skip_resource 'The `registry_key` resource is not supported on your OS yet.' if !inspec.os.windows?
|
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
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# Registry key value types
|
# for compatability with serverspec
|
||||||
# @see https://msdn.microsoft.com/en-us/library/windows/desktop/ms724884(v=vs.85).aspx
|
# this is deprecated syntax and will be removed in future versions
|
||||||
# REG_NONE 0
|
class WindowsRegistryKey < RegistryKey
|
||||||
# REG_SZ 1
|
name 'windows_registry_key'
|
||||||
# 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
|
def initialize(name)
|
||||||
options[:binary] = 3
|
deprecated
|
||||||
options[:string] = 1
|
super(name)
|
||||||
options[:multi_string] = 7
|
end
|
||||||
options[:expand_string] = 2
|
|
||||||
options[:dword] = 4
|
|
||||||
options[:dword_big_endian] = 5
|
|
||||||
options[:qword] = 11
|
|
||||||
|
|
||||||
# serverspec symbols
|
def deprecated
|
||||||
options[:type_string] = 1
|
warn '[DEPRECATION] `windows_registry_key(reg_key)` is deprecated. Please use `registry_key(\'path\to\key\')` instead.'
|
||||||
options[:type_binary] = 3
|
end
|
||||||
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.'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,38 +4,40 @@
|
||||||
# author: Dominik Richter
|
# author: Dominik Richter
|
||||||
# license: All rights reserved
|
# license: All rights reserved
|
||||||
|
|
||||||
class Script < Cmd
|
module Inspec::Resources
|
||||||
name 'script'
|
class Script < Cmd
|
||||||
desc 'Use the script InSpec audit resource to test a Windows PowerShell script on the Microsoft Windows platform.'
|
name 'script'
|
||||||
example "
|
desc 'Use the script InSpec audit resource to test a Windows PowerShell script on the Microsoft Windows platform.'
|
||||||
script = <<-EOH
|
example "
|
||||||
# you powershell script
|
script = <<-EOH
|
||||||
EOH
|
# you powershell script
|
||||||
|
EOH
|
||||||
|
|
||||||
describe script(script) do
|
describe script(script) do
|
||||||
its('matcher') { should eq 'output' }
|
its('matcher') { should eq 'output' }
|
||||||
end
|
end
|
||||||
"
|
"
|
||||||
|
|
||||||
def initialize(script)
|
def initialize(script)
|
||||||
unless inspec.os.windows?
|
unless inspec.os.windows?
|
||||||
return skip_resource 'The `script` resource is not supported on your OS yet.'
|
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
|
end
|
||||||
|
|
||||||
# encodes a script as base64 to run as powershell encodedCommand
|
# we cannot determine if a command exists, because that does not work for scripts
|
||||||
# this comes with performance issues: @see https://gist.github.com/fnichol/7b20596b950e65fb96f9
|
def exist?
|
||||||
require 'winrm'
|
nil
|
||||||
script = WinRM::PowershellScript.new(script)
|
end
|
||||||
cmd = "powershell -encodedCommand #{script.encoded}"
|
|
||||||
super(cmd)
|
|
||||||
end
|
|
||||||
|
|
||||||
# we cannot determine if a command exists, because that does not work for scripts
|
def to_s
|
||||||
def exist?
|
'Script'
|
||||||
nil
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
'Script'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,70 +13,72 @@
|
||||||
# All local GPO parameters can be examined via Registry, but not all security
|
# All local GPO parameters can be examined via Registry, but not all security
|
||||||
# parameters. Therefore we need a combination of Registry and secedit output
|
# parameters. Therefore we need a combination of Registry and secedit output
|
||||||
|
|
||||||
class SecurityPolicy < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'security_policy'
|
class SecurityPolicy < Inspec.resource(1)
|
||||||
desc 'Use the security_policy InSpec audit resource to test security policies on the Microsoft Windows platform.'
|
name 'security_policy'
|
||||||
example "
|
desc 'Use the security_policy InSpec audit resource to test security policies on the Microsoft Windows platform.'
|
||||||
describe security_policy do
|
example "
|
||||||
its('SeNetworkLogonRight') { should eq '*S-1-5-11' }
|
describe security_policy do
|
||||||
end
|
its('SeNetworkLogonRight') { should eq '*S-1-5-11' }
|
||||||
"
|
end
|
||||||
def initialize
|
"
|
||||||
@loaded = false
|
def initialize
|
||||||
@policy = nil
|
@loaded = false
|
||||||
@exit_status = nil
|
@policy = nil
|
||||||
end
|
@exit_status = nil
|
||||||
|
|
||||||
# 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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# find line with key
|
# load security content
|
||||||
key = Regexp.escape(method.to_s)
|
def load
|
||||||
target = ''
|
# export the security policy
|
||||||
@policy.each_line {|s|
|
cmd = inspec.command('secedit /export /cfg win_secpol.cfg')
|
||||||
target = s.strip if s =~ /^\s*#{key}\s*=\s*(.*)\b/
|
return nil if cmd.exit_status.to_i != 0
|
||||||
}
|
|
||||||
|
|
||||||
# extract variable value
|
# store file content
|
||||||
result = target.match(/[=]{1}\s*(?<value>.*)/)
|
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?
|
# returns self
|
||||||
val = result[:value]
|
self
|
||||||
val = val.to_i if val =~ /^\d+$/
|
|
||||||
else
|
ensure
|
||||||
# TODO: we may need to return skip or failure if the
|
# delete temp file
|
||||||
# requested value is not available
|
inspec.command('Remove-Item win_secpol.cfg').exit_status.to_i
|
||||||
val = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
val
|
def method_missing(method)
|
||||||
end
|
# load data if needed
|
||||||
|
if @loaded == false
|
||||||
|
load
|
||||||
|
end
|
||||||
|
|
||||||
def to_s
|
# find line with key
|
||||||
'Security Policy'
|
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
|
||||||
end
|
end
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -15,121 +15,123 @@ require 'forwardable'
|
||||||
# - inactive_days before deactivating the account
|
# - inactive_days before deactivating the account
|
||||||
# - expiry_date when this account will expire
|
# - expiry_date when this account will expire
|
||||||
|
|
||||||
class Shadow < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'shadow'
|
class Shadow < Inspec.resource(1)
|
||||||
desc 'Use the shadow InSpec resource to test the contents of /etc/shadow, '\
|
name 'shadow'
|
||||||
'which contains the following information for users that may log into '\
|
desc 'Use the shadow InSpec resource to test the contents of /etc/shadow, '\
|
||||||
'the system and/or as users that own running processes.'
|
'which contains the following information for users that may log into '\
|
||||||
example "
|
'the system and/or as users that own running processes.'
|
||||||
describe shadow do
|
example "
|
||||||
its('users') { should_not include 'forbidden_user' }
|
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
|
end
|
||||||
|
|
||||||
describe shadow.users('bin') do
|
def filter(hm = {})
|
||||||
its('password') { should cmp 'x' }
|
return self if hm.nil? || hm.empty?
|
||||||
its('count') { should eq 1 }
|
res = @params
|
||||||
end
|
filters = ''
|
||||||
"
|
hm.each do |attr, condition|
|
||||||
|
condition = condition.to_s if condition.is_a? Integer
|
||||||
extend Forwardable
|
filters += " #{attr} = #{condition.inspect}"
|
||||||
attr_reader :params
|
res = res.find_all do |line|
|
||||||
attr_reader :content
|
case line[attr.to_s]
|
||||||
attr_reader :lines
|
when condition
|
||||||
|
true
|
||||||
def initialize(path = '/etc/shadow', opts = nil)
|
else
|
||||||
opts ||= {}
|
false
|
||||||
@path = path || '/etc/shadow'
|
end
|
||||||
@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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
content = res.map { |x| x.values.join(':') }.join("\n")
|
||||||
|
Shadow.new(@path, content: content, filters: @filters + filters)
|
||||||
end
|
end
|
||||||
content = res.map { |x| x.values.join(':') }.join("\n")
|
|
||||||
Shadow.new(@path, content: content, filters: @filters + filters)
|
|
||||||
end
|
|
||||||
|
|
||||||
def entries
|
def entries
|
||||||
@lines.map { |line| Shadow.new(@path, content: line, filters: @filters) }
|
@lines.map { |line| Shadow.new(@path, content: line, filters: @filters) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def users(name = nil)
|
def users(name = nil)
|
||||||
name.nil? ? map_data('user') : filter(user: name)
|
name.nil? ? map_data('user') : filter(user: name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def passwords(password = nil)
|
def passwords(password = nil)
|
||||||
password.nil? ? map_data('password') : filter(password: password)
|
password.nil? ? map_data('password') : filter(password: password)
|
||||||
end
|
end
|
||||||
|
|
||||||
def last_changes(filter_by = nil)
|
def last_changes(filter_by = nil)
|
||||||
filter_by.nil? ? map_data('last_change') : filter(last_change: filter_by)
|
filter_by.nil? ? map_data('last_change') : filter(last_change: filter_by)
|
||||||
end
|
end
|
||||||
|
|
||||||
def min_days(filter_by = nil)
|
def min_days(filter_by = nil)
|
||||||
filter_by.nil? ? map_data('min_days') : filter(min_days: filter_by)
|
filter_by.nil? ? map_data('min_days') : filter(min_days: filter_by)
|
||||||
end
|
end
|
||||||
|
|
||||||
def max_days(filter_by = nil)
|
def max_days(filter_by = nil)
|
||||||
filter_by.nil? ? map_data('max_days') : filter(max_days: filter_by)
|
filter_by.nil? ? map_data('max_days') : filter(max_days: filter_by)
|
||||||
end
|
end
|
||||||
|
|
||||||
def warn_days(filter_by = nil)
|
def warn_days(filter_by = nil)
|
||||||
filter_by.nil? ? map_data('warn_days') : filter(warn_days: filter_by)
|
filter_by.nil? ? map_data('warn_days') : filter(warn_days: filter_by)
|
||||||
end
|
end
|
||||||
|
|
||||||
def inactive_days(filter_by = nil)
|
def inactive_days(filter_by = nil)
|
||||||
filter_by.nil? ? map_data('inactive_days') : filter(inactive_days: filter_by)
|
filter_by.nil? ? map_data('inactive_days') : filter(inactive_days: filter_by)
|
||||||
end
|
end
|
||||||
|
|
||||||
def expiry_dates(filter_by = nil)
|
def expiry_dates(filter_by = nil)
|
||||||
filter_by.nil? ? map_data('expiry_date') : filter(expiry_date: filter_by)
|
filter_by.nil? ? map_data('expiry_date') : filter(expiry_date: filter_by)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
f = @filters.empty? ? '' : ' with'+@filters
|
f = @filters.empty? ? '' : ' with'+@filters
|
||||||
"/etc/shadow#{f}"
|
"/etc/shadow#{f}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def_delegator :@params, :length, :count
|
def_delegator :@params, :length, :count
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def map_data(id)
|
def map_data(id)
|
||||||
@params.map { |x| x[id] }
|
@params.map { |x| x[id] }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Parse a line of /etc/shadow
|
# Parse a line of /etc/shadow
|
||||||
#
|
#
|
||||||
# @param [String] line a line of /etc/shadow
|
# @param [String] line a line of /etc/shadow
|
||||||
# @return [Hash] Map of entries in this line
|
# @return [Hash] Map of entries in this line
|
||||||
def parse_shadow_line(line)
|
def parse_shadow_line(line)
|
||||||
x = line.split(':')
|
x = line.split(':')
|
||||||
{
|
{
|
||||||
'user' => x.at(0),
|
'user' => x.at(0),
|
||||||
'password' => x.at(1),
|
'password' => x.at(1),
|
||||||
'last_change' => x.at(2),
|
'last_change' => x.at(2),
|
||||||
'min_days' => x.at(3),
|
'min_days' => x.at(3),
|
||||||
'max_days' => x.at(4),
|
'max_days' => x.at(4),
|
||||||
'warn_days' => x.at(5),
|
'warn_days' => x.at(5),
|
||||||
'inactive_days' => x.at(6),
|
'inactive_days' => x.at(6),
|
||||||
'expiry_date' => x.at(7),
|
'expiry_date' => x.at(7),
|
||||||
'reserved' => x.at(8),
|
'reserved' => x.at(8),
|
||||||
}
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,76 +6,78 @@
|
||||||
|
|
||||||
require 'utils/simpleconfig'
|
require 'utils/simpleconfig'
|
||||||
|
|
||||||
class SshConf < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'ssh_config'
|
class SshConf < Inspec.resource(1)
|
||||||
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.'
|
name 'ssh_config'
|
||||||
example "
|
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.'
|
||||||
describe sshd_config do
|
example "
|
||||||
its('Protocol') { should eq '2' }
|
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
|
end
|
||||||
"
|
|
||||||
|
|
||||||
def initialize(conf_path = nil, type = nil)
|
def content
|
||||||
@conf_path = conf_path || '/etc/ssh/ssh_config'
|
read_content
|
||||||
typename = (@conf_path.include?('sshd') ? 'Server' : 'Client')
|
end
|
||||||
@type = type || "SSH #{typename} configuration #{conf_path}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def content
|
def params(*opts)
|
||||||
read_content
|
opts.inject(read_params) do |res, nxt|
|
||||||
end
|
res.respond_to?(:key) ? res[nxt] : nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def params(*opts)
|
def method_missing(name)
|
||||||
opts.inject(read_params) do |res, nxt|
|
param = read_params[name.to_s]
|
||||||
res.respond_to?(:key) ? res[nxt] : nil
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def method_missing(name)
|
class SshdConf < SshConf
|
||||||
param = read_params[name.to_s]
|
name 'sshd_config'
|
||||||
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
|
def initialize(path = nil)
|
||||||
'SSH Configuration'
|
super(path || '/etc/ssh/sshd_config')
|
||||||
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
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,421 +38,423 @@
|
||||||
require 'utils/parser'
|
require 'utils/parser'
|
||||||
require 'utils/convert'
|
require 'utils/convert'
|
||||||
|
|
||||||
class User < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
|
module Inspec::Resources
|
||||||
name 'user'
|
class User < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
|
||||||
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.'
|
name 'user'
|
||||||
example "
|
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.'
|
||||||
describe user('root') do
|
example "
|
||||||
it { should exist }
|
describe user('root') do
|
||||||
its('uid') { should eq 1234 }
|
it { should exist }
|
||||||
its('gid') { should eq 1234 }
|
its('uid') { should eq 1234 }
|
||||||
end
|
its('gid') { should eq 1234 }
|
||||||
"
|
end
|
||||||
def initialize(user)
|
"
|
||||||
@user = user
|
def initialize(user)
|
||||||
|
@user = user
|
||||||
|
|
||||||
# select package manager
|
# select package manager
|
||||||
@user_provider = nil
|
@user_provider = nil
|
||||||
os = inspec.os
|
os = inspec.os
|
||||||
if os.linux?
|
if os.linux?
|
||||||
@user_provider = LinuxUser.new(inspec)
|
@user_provider = LinuxUser.new(inspec)
|
||||||
elsif os.windows?
|
elsif os.windows?
|
||||||
@user_provider = WindowsUser.new(inspec)
|
@user_provider = WindowsUser.new(inspec)
|
||||||
elsif ['darwin'].include?(os[:family])
|
elsif ['darwin'].include?(os[:family])
|
||||||
@user_provider = DarwinUser.new(inspec)
|
@user_provider = DarwinUser.new(inspec)
|
||||||
elsif ['freebsd'].include?(os[:family])
|
elsif ['freebsd'].include?(os[:family])
|
||||||
@user_provider = FreeBSDUser.new(inspec)
|
@user_provider = FreeBSDUser.new(inspec)
|
||||||
elsif ['aix'].include?(os[:family])
|
elsif ['aix'].include?(os[:family])
|
||||||
@user_provider = AixUser.new(inspec)
|
@user_provider = AixUser.new(inspec)
|
||||||
elsif os.solaris?
|
elsif os.solaris?
|
||||||
@user_provider = SolarisUser.new(inspec)
|
@user_provider = SolarisUser.new(inspec)
|
||||||
else
|
else
|
||||||
return skip_resource 'The `user` resource is not supported on your OS yet.'
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def exists?
|
class UserInfo
|
||||||
!identity.nil? && !identity[:user].nil?
|
include Converter
|
||||||
end
|
|
||||||
|
|
||||||
def uid
|
attr_reader :inspec
|
||||||
identity[:uid] unless identity.nil?
|
def initialize(inspec)
|
||||||
end
|
@inspec = inspec
|
||||||
|
|
||||||
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])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
id
|
def credentials(_username)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def meta_info(username)
|
# implements generic unix id handling
|
||||||
lsuser = inspec.command("lsuser -C -a home shell #{username}")
|
class UnixUser < UserInfo
|
||||||
return nil if lsuser.exit_status != 0
|
attr_reader :inspec, :id_cmd
|
||||||
|
def initialize(inspec)
|
||||||
user = lsuser.stdout.chomp.split("\n").last.split(':')
|
@inspec = inspec
|
||||||
{
|
@id_cmd ||= 'id'
|
||||||
home: user[1],
|
super
|
||||||
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
|
end
|
||||||
|
|
||||||
script = <<-EOH
|
# parse one id entry like '0(wheel)''
|
||||||
# find user
|
def parse_value(line)
|
||||||
$user = Get-WmiObject Win32_UserAccount -filter "#{filter}"
|
SimpleConfig.new(
|
||||||
# get related groups
|
line,
|
||||||
$groups = $user.GetRelated('Win32_Group') | Select-Object -Property Caption, Domain, Name, LocalAccount, SID, SIDType, Status
|
line_separator: ',',
|
||||||
# filter user information
|
assignment_re: /^\s*([^\(]*?)\s*\(\s*(.*?)\)*$/,
|
||||||
$user = $user | Select-Object -Property Caption, Description, Domain, Name, LocalAccount, Lockout, PasswordChangeable, PasswordExpires, PasswordRequired, SID, SIDType, Status
|
group_re: nil,
|
||||||
# build response object
|
multiple_values: false,
|
||||||
New-Object -Type PSObject | `
|
).params
|
||||||
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
|
end
|
||||||
|
|
||||||
user = params['User']['Caption'] unless params['User'].nil?
|
# extracts the identity
|
||||||
groups = params['Groups']
|
def identity(username)
|
||||||
# if groups is no array, generate one
|
cmd = inspec.command("#{id_cmd} #{username}")
|
||||||
groups = [groups] if !groups.is_a?(Array)
|
return nil if cmd.exit_status != 0
|
||||||
groups = groups.map { |grp| grp['Caption'] } unless params['Groups'].nil?
|
|
||||||
|
|
||||||
{
|
# parse words
|
||||||
uid: nil,
|
params = SimpleConfig.new(
|
||||||
user: user,
|
parse_id_entries(cmd.stdout.chomp),
|
||||||
gid: nil,
|
assignment_re: /^\s*([^=]*?)\s*=\s*(.*?)\s*$/,
|
||||||
group: nil,
|
group_re: nil,
|
||||||
groups: groups,
|
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
|
end
|
||||||
|
|
||||||
# not implemented yet
|
class LinuxUser < UnixUser
|
||||||
def meta_info(_username)
|
include PasswdParser
|
||||||
{
|
include CommentParser
|
||||||
home: nil,
|
|
||||||
shell: nil,
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,57 +27,59 @@
|
||||||
# "Installed": false,
|
# "Installed": false,
|
||||||
# "InstallState": 0
|
# "InstallState": 0
|
||||||
# }
|
# }
|
||||||
class WindowsFeature < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'windows_feature'
|
class WindowsFeature < Inspec.resource(1)
|
||||||
desc 'Use the windows_feature InSpec audit resource to test features on Microsoft Windows.'
|
name 'windows_feature'
|
||||||
example "
|
desc 'Use the windows_feature InSpec audit resource to test features on Microsoft Windows.'
|
||||||
describe windows_feature('dhcp') do
|
example "
|
||||||
it { should be_installed }
|
describe windows_feature('dhcp') do
|
||||||
end
|
it { should be_installed }
|
||||||
"
|
end
|
||||||
|
"
|
||||||
|
|
||||||
def initialize(feature)
|
def initialize(feature)
|
||||||
@feature = feature
|
@feature = feature
|
||||||
@cache = nil
|
@cache = nil
|
||||||
|
|
||||||
# verify that this resource is only supported on Windows
|
# 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'
|
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@cache = {
|
# returns true if the package is installed
|
||||||
name: params['Name'],
|
def installed?(_provider = nil, _version = nil)
|
||||||
description: params['Description'],
|
info[:installed] == true
|
||||||
installed: params['Installed'],
|
end
|
||||||
type: 'windows-feature',
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
# returns the package description
|
||||||
"Windows Feature '#{@feature}'"
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,139 +4,141 @@
|
||||||
|
|
||||||
require 'utils/parser'
|
require 'utils/parser'
|
||||||
|
|
||||||
class XinetdConf < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
|
module Inspec::Resources
|
||||||
name 'xinetd_conf'
|
class XinetdConf < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
|
||||||
desc 'Xinetd services configuration.'
|
name 'xinetd_conf'
|
||||||
example "
|
desc 'Xinetd services configuration.'
|
||||||
describe xinetd_conf.services('chargen') do
|
example "
|
||||||
its('socket_types') { should include 'dgram' }
|
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
|
end
|
||||||
|
|
||||||
describe xinetd_conf.services('chargen').socket_types('dgram') do
|
def to_s
|
||||||
it { should be_disabled }
|
"Xinetd config #{@conf_path}"
|
||||||
end
|
end
|
||||||
"
|
|
||||||
|
|
||||||
include XinetdParser
|
def services(condition = nil)
|
||||||
|
condition.nil? ? params['services'].keys : filter(service: condition)
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(conf_path = '/etc/xinetd.conf', opts = {})
|
def ids(condition = nil)
|
||||||
@conf_path = conf_path
|
condition.nil? ? services_field('id') : filter(id: condition)
|
||||||
@params = opts[:params] unless opts[:params].nil?
|
end
|
||||||
@filters = opts[:filters] || ''
|
|
||||||
@contents = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
def socket_types(condition = nil)
|
||||||
"Xinetd config #{@conf_path}"
|
condition.nil? ? services_field('socket_type') : filter(socket_type: condition)
|
||||||
end
|
end
|
||||||
|
|
||||||
def services(condition = nil)
|
def types(condition = nil)
|
||||||
condition.nil? ? params['services'].keys : filter(service: condition)
|
condition.nil? ? services_field('type') : filter(type: condition)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ids(condition = nil)
|
def wait(condition = nil)
|
||||||
condition.nil? ? services_field('id') : filter(id: condition)
|
condition.nil? ? services_field('wait') : filter(wait: condition)
|
||||||
end
|
end
|
||||||
|
|
||||||
def socket_types(condition = nil)
|
def disabled?
|
||||||
condition.nil? ? services_field('socket_type') : filter(socket_type: condition)
|
filter(disable: 'no').services.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def types(condition = nil)
|
def enabled?
|
||||||
condition.nil? ? services_field('type') : filter(type: condition)
|
filter(disable: 'yes').services.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait(condition = nil)
|
def params
|
||||||
condition.nil? ? services_field('wait') : filter(wait: condition)
|
return @params if defined?(@params)
|
||||||
end
|
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?
|
def filter(conditions = {})
|
||||||
filter(disable: 'no').services.empty?
|
res = params.dup
|
||||||
end
|
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?
|
private
|
||||||
filter(disable: 'yes').services.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
def params
|
# Retrieve the provided field from all configured services.
|
||||||
return @params if defined?(@params)
|
#
|
||||||
return @params = {} if read_content.nil?
|
# @param [String] field name, e.g. `socket_type`
|
||||||
flat_params = parse_xinetd(read_content)
|
# @return [Array[String]] all values of this field across services
|
||||||
@params = { 'services' => {} }
|
def services_field(field)
|
||||||
flat_params.each do |k, v|
|
params['services'].values.compact.flatten
|
||||||
name = k[/^service (.+)$/, 1]
|
.map { |x| x.params[field] }.flatten.compact
|
||||||
if name.nil?
|
end
|
||||||
@params[k] = v
|
|
||||||
|
def match_condition(sth, condition)
|
||||||
|
case sth
|
||||||
|
# this does Regex-matching as well as string comparison
|
||||||
|
when condition
|
||||||
|
true
|
||||||
else
|
else
|
||||||
@params['services'][name] = v
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@params
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter(conditions = {})
|
# Filter services by a criteria. This allows for search queries for
|
||||||
res = params.dup
|
# certain values.
|
||||||
filters = ''
|
#
|
||||||
conditions.each do |k, v|
|
# @param [Hash] service collection
|
||||||
v = v.to_s if v.is_a? Integer
|
# @param [String] search key you want to query
|
||||||
filters += " #{k} = #{v.inspect}"
|
# @param [Any] search value that the key should match
|
||||||
res['services'] = filter_by(res['services'], k.to_s, v)
|
# @return [Hash] filtered service collection
|
||||||
end
|
def filter_by(services, k, v)
|
||||||
XinetdConf.new(@conf_path, params: res, filters: filters)
|
if k == 'service'
|
||||||
end
|
return Hash[services.find_all { |name, _| match_condition(v, name) }]
|
||||||
|
end
|
||||||
private
|
Hash[services.map { |name, service_arr|
|
||||||
|
found = service_arr.find_all { |service|
|
||||||
# Retrieve the provided field from all configured services.
|
match_condition(service.params[k], v)
|
||||||
#
|
}
|
||||||
# @param [String] field name, e.g. `socket_type`
|
found.empty? ? nil : [name, found]
|
||||||
# @return [Array[String]] all values of this field across services
|
}.compact]
|
||||||
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}\""
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@contents[path] = file.content
|
def read_content(path = @conf_path)
|
||||||
if @contents[path].empty? && file.size > 0
|
return @contents[path] if @contents.key?(path)
|
||||||
return skip_resource "Can't read file \"#{path}\""
|
file = inspec.file(path)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,21 +9,23 @@ require 'yaml'
|
||||||
# describe yaml('.kitchen.yaml') do
|
# describe yaml('.kitchen.yaml') do
|
||||||
# its('driver.name') { should eq('vagrant') }
|
# its('driver.name') { should eq('vagrant') }
|
||||||
# end
|
# end
|
||||||
class YamlConfig < JsonConfig
|
module Inspec::Resources
|
||||||
name 'yaml'
|
class YamlConfig < JsonConfig
|
||||||
desc 'Use the yaml InSpec audit resource to test configuration data in a YAML file.'
|
name 'yaml'
|
||||||
example "
|
desc 'Use the yaml InSpec audit resource to test configuration data in a YAML file.'
|
||||||
describe yaml do
|
example "
|
||||||
its('name') { should eq 'foo' }
|
describe yaml do
|
||||||
|
its('name') { should eq 'foo' }
|
||||||
|
end
|
||||||
|
"
|
||||||
|
|
||||||
|
# override file load and parse hash from yaml
|
||||||
|
def parse(content)
|
||||||
|
YAML.load(content)
|
||||||
end
|
end
|
||||||
"
|
|
||||||
|
|
||||||
# override file load and parse hash from yaml
|
def to_s
|
||||||
def parse(content)
|
"YAML #{@path}"
|
||||||
YAML.load(content)
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
"YAML #{@path}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,132 +30,134 @@ require 'resources/file'
|
||||||
# it { should be_enabled }
|
# it { should be_enabled }
|
||||||
# end
|
# end
|
||||||
|
|
||||||
class Yum < Inspec.resource(1)
|
module Inspec::Resources
|
||||||
name 'yum'
|
class Yum < Inspec.resource(1)
|
||||||
desc 'Use the yum InSpec audit resource to test packages in the Yum repository.'
|
name 'yum'
|
||||||
example "
|
desc 'Use the yum InSpec audit resource to test packages in the Yum repository.'
|
||||||
describe yum.repo('name') do
|
example "
|
||||||
it { should exist }
|
describe yum.repo('name') do
|
||||||
it { should be_enabled }
|
it { should exist }
|
||||||
end
|
it { should be_enabled }
|
||||||
"
|
|
||||||
|
|
||||||
# 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
|
end
|
||||||
# parse repo content
|
"
|
||||||
if in_repo == true
|
|
||||||
val = /^\s*([^:]*?)\s*:\s*(.*?)\s*$/.match(line)
|
# returns all repositories
|
||||||
repo[repo_key(strip(val[1]))] = strip(val[2])
|
# 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
|
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
|
end
|
||||||
@cache
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def repos
|
class YumRepo
|
||||||
repositories.map { |repo| repo['id'] }
|
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
|
end
|
||||||
|
|
||||||
def repo(repo)
|
# for compatability with serverspec
|
||||||
YumRepo.new(self, repo)
|
# this is deprecated syntax and will be removed in future versions
|
||||||
end
|
class YumRepoLegacy < Yum
|
||||||
|
name 'yumrepo'
|
||||||
|
|
||||||
# alias for yum.repo('reponame')
|
def initialize(name)
|
||||||
def method_missing(name)
|
super()
|
||||||
repo(name.to_s) if !name.nil?
|
@repository = repo(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def exists?
|
||||||
'Yum Repository'
|
deprecated
|
||||||
end
|
@repository.exist?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
def enabled?
|
||||||
|
deprecated
|
||||||
|
@repository.enabled?
|
||||||
|
end
|
||||||
|
|
||||||
# Removes lefthand and righthand whitespace
|
def deprecated
|
||||||
def strip(value)
|
warn '[DEPRECATION] `yumrepo(reponame)` is deprecated. Please use `yum.repo(reponame)` instead.'
|
||||||
value.strip if !value.nil?
|
end
|
||||||
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.'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -136,7 +136,7 @@ class MockLoader
|
||||||
'$Env:PATH' => cmd.call('$env-PATH'),
|
'$Env:PATH' => cmd.call('$env-PATH'),
|
||||||
# registry key test (winrm 1.6.0, 1.6.1)
|
# registry key test (winrm 1.6.0, 1.6.1)
|
||||||
'2790db1e88204a073ed7fd3493f5445e5ce531afd0d2724a0e36c17110c535e6' => cmd.call('reg_schedule'),
|
'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'),
|
'Auditpol /get /subcategory:\'User Account Management\' /r' => cmd.call('auditpol'),
|
||||||
'/sbin/auditctl -l' => cmd.call('auditctl'),
|
'/sbin/auditctl -l' => cmd.call('auditctl'),
|
||||||
'/sbin/auditctl -s' => cmd.call('auditctl-s'),
|
'/sbin/auditctl -s' => cmd.call('auditctl-s'),
|
||||||
|
@ -196,7 +196,7 @@ class MockLoader
|
||||||
'pw usershow root -7' => cmd.call('pw-usershow-root-7'),
|
'pw usershow root -7' => cmd.call('pw-usershow-root-7'),
|
||||||
# user info for windows (winrm 1.6.0, 1.6.1)
|
# user info for windows (winrm 1.6.0, 1.6.1)
|
||||||
'650b6b72a66316418b25421a54afe21a230704558082914c54711904bb10e370' => cmd.call('GetUserAccount'),
|
'650b6b72a66316418b25421a54afe21a230704558082914c54711904bb10e370' => cmd.call('GetUserAccount'),
|
||||||
'272e1d767fe6e28c86cfba1a75c3d458acade1f4a36cfd5e711b97884879de24' => cmd.call('GetUserAccount'),
|
'174686f0441b8dd387b35cf1cbeed3f98441544351de5d8fb7b54f655e75583f' => cmd.call('GetUserAccount'),
|
||||||
# group info for windows
|
# group info for windows
|
||||||
'Get-WmiObject Win32_Group | Select-Object -Property Caption, Domain, Name, SID, LocalAccount | ConvertTo-Json' => cmd.call('GetWin32Group'),
|
'Get-WmiObject Win32_Group | Select-Object -Property Caption, Domain, Name, SID, LocalAccount | ConvertTo-Json' => cmd.call('GetWin32Group'),
|
||||||
# network interface
|
# network interface
|
||||||
|
|
Loading…
Reference in a new issue