Tests
Controls
control 'sshd-8' do
impact 0.6
title 'Server: Configure the service port'
desc 'Always specify which port the SSH server should listen.'
desc 'rationale', 'This ensures that there are no unexpected settings' # Requires InSpec >=2.3.4
tag 'ssh','sshd','openssh-server'
tag cce: 'CCE-27072-8'
ref 'NSA-RH6-STIG - Section 3.5.2.1', url: 'https://www.nsa.gov/ia/_files/os/redhat/rhel5-guide-i731.pdf'
describe sshd_config do
its('Port') { should cmp 22 }
end
end
where
'sshd-8'
is the name of the controlimpact
,title
, anddesc
define metadata that fully describes the importance of the control, its purpose, with a succinct and complete descriptiondesc
when given only one argument it sets the default description. As of InSpec 2.3.4, when given 2 arguments (see:'rationale'
) it will use the first argument as a header when rendering in Automateimpact
is a string, or numeric that measures the importance of the compliance results. Valid strings for impact arenone
,low
,medium
,high
, andcritical
. The values are based off CVSS 3.0. A numeric value must be between0.0
and1.0
. The value ranges are:0.0 to <0.01
these are controls with no impact, they only provide information0.01 to <0.4
these are controls with low impact0.4 to <0.7
these are controls with medium impact0.7 to <0.9
these are controls with high impact0.9 to 1.0
these are critical controls
tag
is optional meta-information with key or key-value pairsref
is a reference to an external documentdescribe
is a block that contains at least one test. Acontrol
block must contain at least onedescribe
block, but may contain as many as requiredsshd_config
is an InSpec resource. For the full list of InSpec resources, see InSpec resource documentationits('Port')
is the matcher;{ should eq '22' }
is the test. Adescribe
block must contain at least one matcher, but may contain as many as required
Resource properties
You can use a string with dots to specify a nested attribute (i.e. an attribute of a property of the resource).
Note:
its
only supports universal matchers
its('last_changes.min') { should be < Date.today - 90 - Date.new(1970,1,1) }
its('min_days.uniq') { should eq [0] }
its('max_days.max') { should be < 90 }
its('warn_days.uniq.count') { should eq 1 }
its('warn_days.uniq.first') { should eq 7 }
its('expiry_dates.compact') { should be_empty }
its('ingest.first.processors.count') { should be >= 1 }
Matchers
The following InSpec-supported universal matchers are available:
- be - make numeric comparisons
- be_in - look for the property value in a list
- cmp - general-use equality (try this first)
- eq - type-specific equality
- include - look for an expected value in a list-valued property
- match - look for patterns in text using regular expressions
Examples:
should be_nil.or eq('None')
should be_nil.or be >= 114688
should eq('sha256').or eq('sha384').or eq('sha512')
should be_nil.or eq('True').or eq('auto')
should be <= MANAGEABLE_CONTAINER_NUMBER
should be_empty
should_not contain_duplicates
should include rule
should_not include('')
should_not include('.')
should match(/^root|syslog$/)
should match(/^INCREMENTAL|INCREMENTAL_ASYNC$/)
should match(/^\s*?SSLHonorCipherOrder\s+?On/i)
should match(/-FollowSymLinks/).or match(/\+SymLinksIfOwnerMatch/)
should cmp 'root'
should be < Date.today - 90 - Date.new(1970,1,1)
Attributes
An attribute is a parameter that InSpec reads from a YAML file provided on the command line. You can use this feature either to change a profile’s behavior by passing different attribute files or to store secrets that should not be directly present in a profile.
# attributes
CLIENT_MAX_BODY_SIZE = attribute(
'client_max_body_size',
description: 'Sets the maximum allowed size of the client request body, specified in the “Content-Length” request header field. If the size in a request exceeds the configured value, the 413 (Request Entity Too Large) error is returned to the client. Please be aware that browsers cannot correctly display this error. Setting size to 0 disables checking of client request body size.',
default: '1k'
)
CLIENT_BODY_BUFFER_SIZE = attribute(
'client_body_buffer_size',
description: 'Sets buffer size for reading client request body. In case the request body is larger than the buffer, the whole body or only its part is written to a temporary file. By default, buffer size is equal to two memory pages. This is 8K on x86, other 32-bit platforms, and x86-64. It is usually 16K on other 64-bit platforms.',
default: '1k'
)
Best Practices
Avoid native ruby IO functions
Avoid:
- File.new(“filename”).read
- File.read(“filename”)
- IO.read(“filename”)
Use:
- file(“filename”)
Edge Cases
Test not implemented
control 'foo-1' do
describe 'bar-test' do
skip 'Not implemented yet'
end
end
only_if
# Avoid putting conditionals outside control blocks
if package('..').installed?
control 'redis-1' do
...
end
end
# Instead, always enclose it in a control (or better in a describe block):
control 'redis1' do
only_if('redis is not installed.') do
command('redis-cli').exist?
end
describe command('redis-cli SET test_inspec "HELLO"') do
its('stdout') { should match /OK/ }
end
end
# ----
# Format
only_if { <something>.exists? }
only_if { <something> <comparison operator> <state> }
# Examples
only_if { HTTP_METHODS_CHECK != false }
only_if { os.redhat? }
only_if { os.name != 'fedora' && !container_execution }
control "package-test1" do
only_if { package('..').installed? }
end
only_if do
# Abort if service not found
command(apache.service).exist?
end
# Only run test if criteria match
control 'foo-1' do
only_if { os[:family] != 'ubuntu' && os[:release] != '16.04' } || only_if { os[:family] != 'debian' && os[:release] != '8' }
describe service(apache.service) do
it { should be_enabled }
end
end
control 'foo-2' do
logging_conf = file("#{keystone_conf_dir}/logging.conf")
if logging_conf.exist?
describe logging_conf do
its('mode') { should cmp '0640' }
end
end
end
# run test if file does not exist
control 'os-10' do
impact 1.0
title 'CIS: Disable unused filesystems'
desc '1.1.1 Ensure mounting of cramfs, freevxfs, jffs2, hfs, hfsplus, squashfs, udf, FAT'
only_if { !container_execution }
efi_dir = inspec.file('/sys/firmware/efi')
describe file('/etc/modprobe.d/dev-sec.conf') do
its(:content) { should match 'install cramfs /bin/true' }
its(:content) { should match 'install freevxfs /bin/true' }
its(:content) { should match 'install jffs2 /bin/true' }
its(:content) { should match 'install hfs /bin/true' }
its(:content) { should match 'install hfsplus /bin/true' }
its(:content) { should match 'install squashfs /bin/true' }
its(:content) { should match 'install udf /bin/true' }
# if efi is active, do not disable vfat. otherwise the system
# won't boot anymore
unless efi_dir.exist?
its(:content) { should match 'install vfat /bin/true' }
end
end
end
Default value
# Set default value
shadow_group = 'root'
shadow_group = 'shadow' if os.debian? || os.suse?
# Single lines can be broken up with a trailing backslash
audit_pkg = os.redhat? \
|| os.suse? \
|| os.name == 'amazon' \
|| os.name == 'fedora' ? 'audit' : 'auditd'
# In a control
control 'foo-1' do
if os.redhat? || os.name == 'fedora'
describe file('/etc/shadow') do
it { should_not be_writable.by('owner') }
it { should_not be_readable.by('owner') }
end
else
describe file('/etc/shadow') do
it { should be_writable.by('owner') }
it { should be_readable.by('owner') }
end
end
end
Hide sensitive output
describe file('/tmp/mysecretfile'), :sensitive do
its('content') { should match /secret_info/ }
end
Pass if one test succeeds
With InSpec it is possible to check if at least one of a collection of checks is true. For example: If a setting is configured in two different locations, you may want to test if either configuration A or configuration B have been set. This is accomplished via describe.one
. It defines a block of tests with at least one valid check.
describe.one do
describe ConfigurationA do
its('setting_1') { should eq true }
end
describe ConfigurationB do
its('setting_2') { should eq true }
end
end
Complex examples
Iterate over whitespace array
hab_env_vars = %w(
HAB_AUTH_TOKEN
HAB_CACHE_KEY_PATH
HAB_DEPOT_URL
HAB_ORG
HAB_ORIGIN
HAB_ORIGIN_KEYS
HAB_RING
HAB_RING_KEY
HAB_STUDIOS_HOME
HAB_STUDIO_ROOT
HAB_USER
)
hab_env_vars.each do |e|
describe os_env(e) do
its('content') { should eq nil }
end
end
Prepare loop and iterate over combined elements
module_path = File.join(apache.conf_dir, '/mods-enabled/')
loaded_modules = command('ls ' << module_path).stdout.split.keep_if { |file_name| /.load/.match(file_name) }
loaded_modules.each do |id|
describe file(File.join(module_path, id)) do
its('content') { should_not match(/^\s*?LoadModule\s+?dav_module/) }
its('content') { should_not match(/^\s*?LoadModule\s+?cgid_module/) }
its('content') { should_not match(/^\s*?LoadModule\s+?cgi_module/) }
its('content') { should_not match(/^\s*?LoadModule\s+?include_module/) }
end
end
# ----
# Use the InSpec resource to enumerate IDs, then test in-depth
docker.containers.running?.ids.each do |id|
describe docker.object(id) do
its('State.Health.Status') { should eq 'healthy' }
end
end
Parse nginx config
# determine all required paths
nginx_path = '/etc/nginx'
nginx_conf = File.join(nginx_path, 'nginx.conf')
nginx_confd = File.join(nginx_path, 'conf.d')
nginx_enabled = File.join(nginx_path, 'sites-enabled')
nginx_parsed_config = command('nginx -T').stdout
options = {
assignment_regex: /^\s*([^:]*?)\s*\ \s*(.*?)\s*;$/
}
options_add_header = {
assignment_regex: /^\s*([^:]*?)\s*\ \s*(.*?)\s*;$/,
multiple_values: true
}
control 'nginx-07' do
describe user(nginx_lib.valid_users) do
it { should exist }
end
describe parse_config_file(nginx_conf, options) do
its('user') { should eq nginx_lib.valid_users }
end
describe parse_config_file(nginx_conf, options) do
its('group') { should_not eq 'root' }
end
end
control 'nginx-08' do
impact 1.0
title 'Prevent clickjacking'
desc 'Do not allow the browser to render the page inside an frame or iframe.'
describe parse_config(nginx_parsed_config, options_add_header) do
its('add_header') { should include 'X-Frame-Options SAMEORIGIN' }
end
end
control 'nginx-09' do
impact 1.0
title 'Enable Cross-site scripting filter'
desc 'This header is used to configure the built in reflective XSS protection. This tells the browser to block the response if it detects an attack rather than sanitising the script.'
describe parse_config(nginx_parsed_config, options_add_header) do
its('add_header') { should include 'X-XSS-Protection "1; mode=block"' }
end
end
control 'nginx-10' do
impact 1.0
title 'Disable content-type sniffing'
desc 'It prevents browser from trying to mime-sniff the content-type of a response away from the one being declared by the server. It reduces exposure to drive-by downloads and the risks of user uploaded content that, with clever naming, could be treated as a different content-type, like an executable.'
describe parse_config(nginx_parsed_config, options_add_header) do
its('add_header') { should include 'X-Content-Type-Options nosniff' }
end
end
Validate ini file
control 'check-identity-04' do
title 'Strong hashing algorithms should be used for PKI tokens'
ref 'http://docs.openstack.org/security-guide/identity/checklist.html#check-identity-04-does-identity-use-strong-hashing-algorithms-for-pki-tokens'
keystone_conf = ini(keystone_conf_file)
only_if do
keystone_conf.value(['token', 'provider']) == 'pki'
end
describe ini(keystone_conf_file) do
its(['token', 'hash_algorithm']) { should eq('sha256').or eq('sha384').or eq('sha512') }
end
end
control 'foo-1' do
describe ini(keystone_conf_file) do
its(['oslo_middleware', 'max_request_body_size']) { should be_nil.or be >= 114688 }
end
describe ini("#{conf_dir}/ceilometer.conf") do
its(['DEFAULT', 'auth_strategy']) { should be_nil.or eq 'keystone' }
its(['keystone_authtoken', 'auth_uri']) { should match(/^https:/) }
end
describe ini(keystone_conf_file) do
its(['DEFAULT', 'nas_secure_file_permissions']) { should be_nil.or eq('True').or eq('auto') }
end
end
Filtering
Terminology
Filter statement
When using a plural resource, a filter statement is used to select individual test subjects using filter criteria. A filter statement almost always is indicated by the keyword where, e.g. .where(...)
and may be repeated using method chaining, e.g. .where(...).where(...)
.
A filter statement may use method call syntax (which allows basic criteria operations, such as equality, regex matching, and ruby === comparison) or block syntax (which allows arbitrary code, e.g. .where {...}
).
Filter criteria
When using a plural resource, a filter criterion is used to select individual test subjects within a filter statement. You may use multiple filter criteria in a single filter statement, .where(color: 'blue', size: 'small')
.
When method-call syntax is used with the filter statement, you provide filter criteria as a Hash, with filter criteria names as keys, and conditions as the Hash values. You may provide test, true/false, or numbers, in which case the comparison is equality; or you may provide a regular expression, in which case a match is performed.
Here, (color: blue)
is a single filter criterion being used with a filter statement in method-call syntax.
# Count only blue cars
describe cars.where(color: 'blue') do
its('count') { should eq 20 }
end
When block-method syntax is used with the filter statement, you provide a block. The block may contain arbitrary code, and each filter criteria will be available as an accessor. The block will be evaluated once per row, and each block that evaluates to a truthy value will pass the filter.
Here, { engine_cylinders >= 6 }
is a block-syntax filter statement referring to one filter criterion.
describe cars.where { engine_cylinders >= 6 } do
its('city_mpg_ratings') { should_not include '4-star' }
end
Examples
# Method form, simple
# Select just the root user (direct equality)
describe shadow.where(user: 'root') do
its ('count') { should eq 1 }
end
# Method form, with a regex
# Select all users whose names begin with smb
describe shadow.where(user: /^smb/) do
its ('count') { should eq 2 }
end
# Block form
# Select users whose passwords have expired
describe shadow.where { expiry_date > 0 } do
# This test directly asserts that there should be 0 such users
its('count') { should eq 0 }
# But if the count test fails, this test outputs the users that are causing the failure.
its('users') { should be_empty }
end
# ----
# For improved readability put longer filter criteria
# on separate lines
describe resource(
property1_name: 'my-filter',
property2_name: 'my-log-group'
) do
it { should exist }
end
# ----
# Ensure everyone except admins has an stale policy of no more than 14 days
describe shadow.where { user !~ /adm$/ } do
its('inactive_days.max') { should be <= 14 }
end
# Find 'locked' accounts and ensure 'nobody' is on the list
describe shadow.where(password: '*LK*') do
its('users') { should include 'nobody' }
end
# Find users who have not changed their password within 90 days
describe shadow.where { last_change > Date.today - 90 - Date.new(1970,1,1) } do
its('users') { should be_empty }
end
# Find users who have a nonzero wait time
describe shadow.where { min_days > 0 } do
its('users') { should be_empty }
end
# All users should have a 30-day policy
describe shadow.where { max_days != 30 } do
its('users') { should be_empty }
end
# Ensure everyone has a stale policy of no more than 14 days.
describe shadow.where { inactive_days.nil? || inactive_days > 14 } do
its('users') { should be_empty }
end
# Ensure no one is disabled due to a old password
describe shadow.where { !expiry_date.nil? } do
its('users') { should be_empty }
end
# Ensure no one is disabled for more than 14 days
describe shadow.where { !expiry_date.nil? && expiry_date - Date.new(1970,1,1) > 14} do
its('users') { should be_empty }
end
describe auditd.syscall('open') do
# or
describe auditd.where(syscall: 'open') do
describe auditd.syscall('chown').where { arch == "b32" } do
# or
describe auditd.where(syscall: 'chown', arch: 'b32' ) do
Common ruby methods
See Ruby Docs
cmd.stdout.chomp
Will remove trailing new lines from the string, i.e. it removes carriage return characters (\n, \r, and \r\n).
cmd.exit_status.to_i.nonzero?
Test if a command was unsuccessful.
array.keep_if
Deletes every element of self for which the given block evaluates to false.
a = %w{ a b c d e f }
a.keep_if { |v| v =~ /[aeiou]/ } # => ["a", "e"]
array.sort
No explanation necessary.
array.uniq
Removes duplicate elements from array. Returns nil if no changes are made (that is, no duplicates are found).
a = [ "a", "a", "b", "b", "c" ]
a.uniq # => ["a", "b", "c"]
b = [ "a", "b", "c" ]
b.uniq # => nil
array.flatten
Recursively turns nested arrays into a one-dimensional flattening of self.
a = [[1, 2, 3], [4, 5, 6, [7, 8]], 9, 10]
a.flatten # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
string.gsub
The gsub string method replaces a substring with another string. It finds all instances of the matched string and replaces it with the new argument. The method takes two arguments. The first is the text you want to replace and the second is the new text.
sentence = "Today is Friday"
sentence.gsub('Friday','Saturday') # => "Today is Saturday"
sentence = "My favourite number is 3"
sentence.gsub(/\d+/,"7") # => "My favourite number is 7"
An entire string, as opposed to a substring, may be replaced using the replace
method:
sentence = "Today is Friday"
sentence.replace("Tomorrow is Saturday") # => "Tomorrow is Saturday"
string.scan
Iterate through str, matching the pattern (which may be a Regexp or a String). For each match, a result is generated and either added to the result array. If the pattern contains no groups, each individual result consists of the matched string, $&. If the pattern contains groups, each individual result is itself an array containing one entry per group.
a = "cruel world"
a.scan(/\w+/) #=> ["cruel", "world"]
a.scan(/.../) #=> ["cru", "el ", "wor"]
a.scan(/(...)/) #=> [["cru"], ["el "], ["wor"]]
a.scan(/(..)(..)/) #=> [["cr", "ue"], ["l ", "wo"]]
string.split
Divides str into substrings based on a delimiter, returning an array of these substrings.
- If pattern is a
String
, then its contents are used as the delimiter when splitting str. - If pattern is a single space, str is split on whitespace, with leading whitespace and runs of contiguous whitespace characters ignored.
- If pattern is a
Regexp
, str is divided where the pattern matches. Whenever the pattern matches a zero-length string, str is split into individual characters. - If pattern contains groups, the respective matches will be returned in the array as well.
- If pattern is
nil
, the value of$;
is used. If$;
isnil
(which is the default), str is split on whitespace as if ‘ ‘ were specified. - If the limit parameter is omitted, trailing null fields are suppressed. If limit is a positive number, at most that number of split substrings will be returned (captured groups will be returned as well, but are not counted towards the limit). If limit is
1
, the entire string is returned as the only entry in an array. If negative, there is no limit to the number of fields returned, and trailing null fields are not suppressed. - When the input
str
is empty an empty Array is returned as the string is considered to have no fields to split.
" now's the time".split #=> ["now's", "the", "time"]
" now's the time".split(' ') #=> ["now's", "the", "time"]
" now's the time".split(/ /) #=> ["", "now's", "", "the", "time"]
"1, 2.34,56, 7".split(%r{,\s*}) #=> ["1", "2.34", "56", "7"]
"hello".split(//) #=> ["h", "e", "l", "l", "o"]
"hello".split(//, 3) #=> ["h", "e", "llo"]
"hi mom".split(%r{\s*}) #=> ["h", "i", "m", "o", "m"]
"mellow yellow".split("ello") #=> ["m", "w y", "w"]
"1,2,,3,4,,".split(',') #=> ["1", "2", "", "3", "4"]
"1,2,,3,4,,".split(',', 4) #=> ["1", "2", "", "3,4,,"]
"1,2,,3,4,,".split(',', -4) #=> ["1", "2", "", "3", "4", "", ""]
"1:2:3".split(/(:)()()/, 2) #=> ["1", ":", "", "", "2:3"]
"".split(',', -1) #=> []
string.tr
tr(from_str, to_str) : Returns a copy of str
with the characters in from_str
replaced by the corresponding characters in to_str
. If to_str
is shorter than from_str
, it is padded with its last character in order to maintain the correspondence.
"hello".tr('el', 'ip') #=> "hippo"
"hello".tr('aeiou', '*') #=> "h*ll*"
"hello".tr('aeiou', 'AA*') #=> "hAll*"
Both strings may use the c1-c2
notation to denote ranges of characters, and from_str
may start with a ^
, which denotes all characters except those listed.
"hello".tr('a-y', 'b-z') #=> "ifmmp"
"hello".tr('^aeiou', '*') #=> "*e**o"
The backslash character </code> can be used to escape <code>^
or -
and is otherwise ignored unless it appears at the end of a range or the end of the from_str
orto_str
:
"hello^world".tr("\\^aeiou", "*") #=> "h*ll**w*rld"
"hello-world".tr("a\\-eo", "*") #=> "h*ll**w*rld"
"hello\r\nworld".tr("\r", "") #=> "hello\nworld"
"hello\r\nworld".tr("\\r", "") #=> "hello\r\nwold"
"hello\r\nworld".tr("\\\r", "") #=> "hello\nworld"
"X['\\b']".tr("X\\", "") #=> "['b']"
"X['\\b']".tr("X-\\]", "") #=> "'b'"
.to_str / .to_i
No explanation necessary.
Examples
<some_resource>.forwarding_rules.flatten.each { |rule|
describe rule do
its('property1') { should eq "https" }
its('property2') { should eq 443 }
end
}
# ----
# 1. build path string
# 2. use file resource on string
# 3. access the file resource's "content" property
# 4. remove commented lines
# 5. detect all virtualhost stanzas (case [i]nsensitive, [m]ultiline)
# 6. store nested virtualhosts as new elements in a one-dimensional array
# Regex are created with `/pat/` and `%r{pat}`
# https://github.com/dev-sec/apache-baseline/blob/master/controls/apache_spec.rb
virtual_host = file(
File.join(sites_enabled_path, id)
)
.content.gsub(/#.*$/, '')
.scan(%r{<virtualhost.*443(.*?)<\/virtualhost>}im).flatten
next if virtual_host.empty?
Profiles
Profile Structure
A profile should have the following structure:
examples/profile
├── README.md
├── controls
│ ├── example.rb
│ └── control_etc.rb
├── libraries
│ └── extension.rb
|── files
│ └── extras.conf
└── inspec.yml
where:
inspec.yml
includes the profile description (required)controls
is the directory in which all tests are located (required)libraries
is the directory in which all InSpec resource extensions are located (optional)files
is the directory with additional files that a profile can access (optional)README.md
should be used to explain the profile, its scope, and usage
Create a Profile
InSpec version: 3.2.6
:
$ inspec init help profile
Usage:
inspec init profile [OPTIONS] NAME
Options:
p, [--platform=PLATFORM] # Which platform to generate a platform for: choose from os, aws, gcp
# Default: os
[--overwrite], [--no-overwrite] # Overwrites existing directory
[--log-level=LOG_LEVEL] # Set the log level: info (default), debug, warn, error
[--log-location=LOG_LOCATION] # Location to send diagnostic log messages to. (default: STDOUT or Inspec::Log.error)
Generate a new profile
InSpec Shell
List resources and their respective help:
$ inspec shell
help resources
help <resource name>
Libraries
mongo example
resource_mongo_command.rb
require 'json'
class MongoCommand < Inspec.resource(1)
name 'mongo_command'
desc 'Runs a MongoDB command using the mongo CLI against a given database (default database: admin)'
example <<-EOL
describe mongo_command('db.showRoles()') do
its('params.length') { should be > 0 }
end
EOL
attr_reader :command, :database, :params
def initialize(command, options = {})
@command = command
@username = options[:username]
@password = options[:password]
@database = options.fetch(:database, 'admin')
@allow_auth_errors = options.fetch(:allow_auth_errors, false)
@verify_ssl = options.fetch(:verify_ssl, true)
check_for_cli_command
@inspec_command = run_mongo_command(command)
@params = parse(@inspec_command.stdout)
end
def stdout
@inspec_command.stdout
end
def stderr
@inspec_command.stderr
end
def to_s
str = "MongoDB Command (#{@command}"
str += ", username: #{@username}" unless @username.nil?
str += ", password: <hidden>" unless @password.nil?
str += ')'
str
end
private
def parse(output)
# return right away if stdout is nil
return [] if output.nil?
# strip any network warnings from the output
# Unfortunately, it appears the --sslAllowInvalidHostnames doesn't actually squelch
# any warnings, even when using --quiet mode
output_lines = output.lines.delete_if { |line| line.include?(' W NETWORK ')}
# if, after removing any network warnings, there are no lines to process,
# we received no command output.
return [] if output_lines.empty?
# put our output back together as a string
output = output_lines.join
# skip the whole resource if we could not run the command at all
return skip_resource "User is not authorized to run command #{command}" if
is_auth_error?(output) && !auth_errors_allowed?
# if the output indicates there's an authorization error, and we allow auth
# errors, we won't throw an exception, just set the params to an empty array.
return [] if is_auth_error?(output) && auth_errors_allowed?
# At this point, we should have parseable JSON we can use and no auth errors.
# Let's read it in.
JSON.parse(output)
rescue JSON::ParserError => e
skip_resource "Unable to parse JSON response from mongo client: #{e.message}" unless @allow_auth_errors
[]
end
def check_for_cli_command
check_command = inspec.command(format_command("db.version()"))
if check_command.exit_status != 0
skip_resource "Unable to run mongo commands: #{check_command.stderr}"
end
end
def run_mongo_command(command)
inspec.command(format_command(command))
end
def ssl_verify_disabled?
['false', false].include?(@verify_ssl)
end
def is_auth_error?(output)
output.include?('Error: not authorized')
end
def auth_errors_allowed?
@allow_auth_errors == true
end
def format_command(command)
command = %{echo "#{command}" | mongo --quiet #{database} --ssl}
command += " --username #{@username}" unless @username.nil?
command += " --password #{@password}" unless @password.nil?
command += " --sslAllowInvalidCertificates" if ssl_verify_disabled?
command += " --sslAllowInvalidHostnames" if ssl_verify_disabled?
command
end
end
nginx example
nginx_lib.rb
class Nginxlib < Inspec.resource(1)
name 'nginx_lib'
def valid_users
# define nginx user for different distros
centos_user = 'nginx'
debian_user = 'www-data'
web_user = debian_user
# adjust the nginx user based on OS
case inspec.os[:family]
when 'ubuntu', 'debian'
web_user
when 'redhat', 'centos'
web_user = centos_user
end
web_user
end
end
Misc. (unsorted/wip)
describe file('/proc/sys/kernel/random/entropy_avail').content.to_i do
it { should >= 1000 }
end
describe file(json('/etc/docker/daemon.json').params['tlscacert']) do
it { should exist }
it { should be_file }
it { should be_owned_by 'root' }
it { should be_grouped_into 'root' }
end
control 'foo-1' do
only_if { os.linux? }
# use custom library to test if a systemd unit for a socket config
# exists and return a message if not
if docker_helper.socket
rule = '-w ' + docker_helper.socket + ' -p rwxa -k docker'
describe auditd do
its(:lines) { should include(rule) }
end
else
describe 'audit docker service' do
skip 'Cannot determine docker socket'
end
end
end
control 'foo-1' do
instantiated_images = command('docker ps -qa | xargs docker inspect -f \'{{.Image}}\'').stdout.split
all_images = command('docker images -q --no-trunc').stdout.split
diff = all_images - instantiated_images
describe diff do
it { should be_empty }
end
end
# ----
control 'foo-2' do
total_on_host = command('docker info').stdout.split[1].to_i
total_running = command('docker ps -q').stdout.split.length
diff = total_on_host - total_running
describe diff do
it { should be <= MANAGEABLE_CONTAINER_NUMBER }
end
end
control 'foo-3' do
total_tasks = command("ps aux | grep #{apache.service} | grep -v grep | grep root | wc -l | tr -d [:space:]").stdout.to_i
describe total_tasks do
it { should eq 1 }
end
end
[5000, 35357].each do |port|
# TODO: workaround until https://github.com/chef/inspec/issues/1205 is fixed
next if os.name.nil? # detect mock backend during inspec check
describe ssl(port: port) do
it { should be_enabled }
end
end
Pass options to resource
options = {
enable_remote_worker: true,
headers: { 'User-Agent' => 'Docker' },
ssl_verify: false
}
describe http('https://localhost', options) do
its('status') { should cmp 200 }
end