11. Dissecting Resources
June 7, 2022About 11 min
11.1. bridge
require "inspec/resources/file"
# Usage:
# describe bridge('br0') do
# it { should exist }
# it { should have_interface 'eth0' }
# end
module Inspec::Resources
class Bridge < Inspec.resource(1)
name "bridge"
supports platform: "unix"
desc "Use the bridge InSpec audit resource to test basic network bridge properties, such as name, if an interface is defined, and the associations for any defined interface."
example <<~EXAMPLE
describe bridge 'br0' do
it { should exist }
it { should have_interface 'eth0' }
end
EXAMPLE
def initialize(bridge_name)
@bridge_name = bridge_name
@bridge_provider = nil
if inspec.os.linux?
@bridge_provider = LinuxBridge.new(inspec)
elsif inspec.os.windows?
@bridge_provider = WindowsBridge.new(inspec)
else
return skip_resource "The `bridge` resource is not supported on your OS yet."
end
end
def exists?
!bridge_info.nil? && !bridge_info[:name].nil?
end
def has_interface?(interface)
return skip_resource "The `bridge` resource does not provide interface detection for Windows yet" if inspec.os.windows?
bridge_info.nil? ? false : bridge_info[:interfaces].include?(interface)
end
def interfaces
bridge_info.nil? ? nil : bridge_info[:interfaces]
end
def to_s
"Bridge #{@bridge_name}"
end
private
def bridge_info
return @cache if defined?(@cache)
@cache = @bridge_provider.bridge_info(@bridge_name) unless @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
# ensure we have an array of groups
bridges = [bridges] unless 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.empty?
warn "[Possible Error] detected multiple bridges interfaces with the name #{bridge_name}" if bridges.size > 1
bridges[0]
end
end
end
11.2. command
# copyright: 2015, Vulcano Security GmbH
require "inspec/resource"
module Inspec::Resources
class Cmd < Inspec.resource(1)
name "command"
supports platform: "unix"
supports platform: "windows"
desc "Use the command InSpec audit resource to test an arbitrary command that is run on the system."
example <<~EXAMPLE
describe command('ls -al /') do
its('stdout') { should match /bin/ }
its('stderr') { should eq '' }
its('exit_status') { should eq 0 }
end
command('ls -al /').exist? will return false. Existence of command should be checked this way.
describe command('ls') do
it { should exist }
end
EXAMPLE
attr_reader :command
def initialize(cmd, options = {})
if cmd.nil?
raise "InSpec `command` was called with `nil` as the argument. This is not supported. Please provide a valid command instead."
end
@command = cmd
if options[:redact_regex]
unless options[:redact_regex].is_a?(Regexp)
# Make sure command is replaced so sensitive output isn't shown
@command = "ERROR"
raise Inspec::Exceptions::ResourceFailed,
"The `redact_regex` option must be a regular expression"
end
@redact_regex = options[:redact_regex]
end
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? # rubocop:disable Metrics/AbcSize
# silent for mock resources
return false if inspec.os.name.nil? || inspec.os.name == "mock"
if inspec.os.linux?
res = if inspec.platform.name == "alpine"
inspec.backend.run_command("which \"#{@command}\"")
else
inspec.backend.run_command("bash -c 'type \"#{@command}\"'")
end
elsif inspec.os.windows?
res = inspec.backend.run_command("Get-Command \"#{@command}\"")
elsif inspec.os.unix?
res = inspec.backend.run_command("type \"#{@command}\"")
else
warn "`command(#{@command}).exist?` is not supported on your OS: #{inspec.os[:name]}"
return false
end
res.exit_status.to_i == 0
end
def to_s
output = "Command: `#{@command}`"
# Redact output if the `redact_regex` option is passed
# If no capture groups are passed then `\1` and `\2` are ignored
output.gsub!(@redact_regex, '\1REDACTED\2') unless @redact_regex.nil?
output
end
end
end
11.3. NGINX
require "pathname"
require "hashie/mash"
require "inspec/resources/command"
module Inspec::Resources
class Nginx < Inspec.resource(1)
name "nginx"
supports platform: "unix"
desc "Use the nginx InSpec audit resource to test information about your NGINX instance."
example <<~EXAMPLE
describe nginx do
its('conf_path') { should cmp '/etc/nginx/nginx.conf' }
end
describe nginx('/etc/sbin/') do
its('version') { should be >= '1.0.0' }
end
describe nginx do
its('modules') { should include 'my_module' }
end
EXAMPLE
attr_reader :params, :bin_dir
def initialize(nginx_path = "/usr/sbin/nginx")
return skip_resource "The `nginx` resource is not yet available on your OS." if inspec.os.windows?
return skip_resource "The `nginx` binary not found in the path provided." unless inspec.command(nginx_path).exist?
cmd = inspec.command("#{nginx_path} -V 2>&1")
if cmd.exit_status != 0
return skip_resource "Error using the command nginx -V"
end
@data = cmd.stdout
@params = {}
read_content
end
%w{error_log_path http_client_body_temp_path http_fastcgi_temp_path http_log_path http_proxy_temp_path http_scgi_temp_path http_uwsgi_temp_path lock_path modules_path prefix sbin_path service version}.each do |property|
define_method(property.to_sym) do
@params[property.to_sym]
end
end
def openssl_version
result = @data.scan(/built with OpenSSL\s(\S+)\s(\d+\s\S+\s\d{4})/).flatten
Hashie::Mash.new({ "version" => result[0], "date" => result[1] })
end
def compiler_info
result = @data.scan(/built by (\S+)\s(\S+)\s(\S+)/).flatten
Hashie::Mash.new({ "compiler" => result[0], "version" => result[1], "date" => result[2] })
end
def support_info
support_info = @data.scan(/(.*\S+) support enabled/).flatten
support_info.empty? ? nil : support_info.join(" ")
end
def modules
@data.scan(/--with-(\S+)_module/).flatten
end
def to_s
"Nginx Environment"
end
private
def read_content
parse_config
parse_path
parse_http_path
end
def parse_config
@params[:prefix] = @data.scan(/--prefix=(\S+)\s/).flatten.first
@params[:service] = "nginx"
@params[:version] = @data.scan(%r{nginx version: nginx\/(\S+)\s}).flatten.first
end
def parse_path
@params[:sbin_path] = @data.scan(/--sbin-path=(\S+)\s/).flatten.first
@params[:modules_path] = @data.scan(/--modules-path=(\S+)\s/).flatten.first
@params[:error_log_path] = @data.scan(/--error-log-path=(\S+)\s/).flatten.first
@params[:http_log_path] = @data.scan(/--http-log-path=(\S+)\s/).flatten.first
@params[:lock_path] = @data.scan(/--lock-path=(\S+)\s/).flatten.first
end
def parse_http_path
@params[:http_client_body_temp_path] = @data.scan(/--http-client-body-temp-path=(\S+)\s/).flatten.first
@params[:http_proxy_temp_path] = @data.scan(/--http-proxy-temp-path=(\S+)\s/).flatten.first
@params[:http_fastcgi_temp_path] = @data.scan(/--http-fastcgi-temp-path=(\S+)\s/).flatten.first
@params[:http_uwsgi_temp_path] = @data.scan(/--http-uwsgi-temp-path=(\S+)\s/).flatten.first
@params[:http_scgi_temp_path] = @data.scan(/--http-scgi-temp-path=(\S+)\s/).flatten.first
end
end
end
11.4. File
# copyright: 2015, Vulcano Security GmbH
require "shellwords"
require "inspec/utils/parser"
module Inspec::Resources
module FilePermissionsSelector
def select_file_perms_style(os)
if os.unix?
UnixFilePermissions.new(inspec)
elsif os.windows?
WindowsFilePermissions.new(inspec)
end
end
end
# TODO: rename file_resource.rb
class FileResource < Inspec.resource(1)
include FilePermissionsSelector
include LinuxMountParser
name "file"
supports platform: "unix"
supports platform: "windows"
desc "Use the file InSpec audit resource to test all system file types, including files, directories, symbolic links, named pipes, sockets, character devices, block devices, and doors."
example <<~EXAMPLE
describe file('path') do
it { should exist }
it { should be_file }
it { should be_readable }
it { should be_writable }
it { should be_executable.by_user('root') }
it { should be_owned_by 'root' }
its('mode') { should cmp '0644' }
end
EXAMPLE
attr_reader :file, :mount_options
def initialize(path)
# select permissions style
@perms_provider = select_file_perms_style(inspec.os)
@file = inspec.backend.file(path)
end
%w{
type exist? file? block_device? character_device? socket? directory?
symlink? pipe? mode mode? owner owned_by? group grouped_into?
link_path shallow_link_path linked_to? mtime size selinux_label immutable?
product_version file_version version? md5sum sha256sum
path basename source source_path uid gid
}.each do |m|
define_method m do |*args|
file.send(m, *args)
end
end
def content
res = file.content
return nil if res.nil?
res.force_encoding("utf-8")
end
def contain(*_)
raise "Contain is not supported. Please use standard RSpec matchers."
end
def readable?(by_usergroup, by_specific_user)
return false unless exist?
return skip_resource "`readable?` is not supported on your OS yet." if @perms_provider.nil?
file_permission_granted?("read", by_usergroup, by_specific_user)
end
def writable?(by_usergroup, by_specific_user)
return false unless exist?
return skip_resource "`writable?` is not supported on your OS yet." if @perms_provider.nil?
file_permission_granted?("write", by_usergroup, by_specific_user)
end
def executable?(by_usergroup, by_specific_user)
return false unless exist?
return skip_resource "`executable?` is not supported on your OS yet." if @perms_provider.nil?
file_permission_granted?("execute", by_usergroup, by_specific_user)
end
def allowed?(permission, opts = {})
return false unless exist?
return skip_resource "`allowed?` is not supported on your OS yet." if @perms_provider.nil?
file_permission_granted?(permission, opts[:by], opts[:by_user])
end
def mounted?(expected_options = nil, identical = false)
mounted = file.mounted
# return if no additional parameters have been provided
return file.mounted? if expected_options.nil?
# deprecation warning, this functionality will be removed in future version
Inspec.deprecate(:file_resource_be_mounted_matchers, "The file resource `be_mounted.with` and `be_mounted.only_with` matchers are deprecated. Please use the `mount` resource instead")
# we cannot read mount data on non-Linux systems
return nil unless inspec.os.linux?
# parse content if we are on linux
@mount_options ||= parse_mount_options(mounted.stdout, true)
if identical
# check if the options should be identical
@mount_options == expected_options
else
# otherwise compare the selected values
@mount_options.contains(expected_options)
end
end
def suid
(mode & 04000) > 0
end
alias setuid? suid
def sgid
(mode & 02000) > 0
end
alias setgid? sgid
def sticky
(mode & 01000) > 0
end
alias sticky? sticky
def more_permissive_than?(max_mode = nil)
raise Inspec::Exceptions::ResourceFailed, "The file" + file.path + "doesn't seem to exist" unless exist?
raise ArgumentError, "You must proivde a value for the `maximum allowable permission` for the file." if max_mode.nil?
raise ArgumentError, "You must proivde the `maximum permission target` as a `String`, you provided: " + max_mode.class.to_s unless max_mode.is_a?(String)
raise ArgumentError, "The value of the `maximum permission target` should be a valid file mode in 4-ditgit octal format: for example, `0644` or `0777`" unless /(0)?([0-7])([0-7])([0-7])/.match?(max_mode)
# Using the files mode and a few bit-wise calculations we can ensure a
# file is no more permisive than desired.
#
# 1. Calculate the inverse of the desired mode (e.g., 0644) by XOR it with
# 0777 (all 1s). We are interested in the bits that are currently 0 since
# it indicates that the actual mode is more permissive than the desired mode.
# Conversely, we dont care about the bits that are currently 1 because they
# cannot be any more permissive and we can safely ignore them.
#
# 2. Calculate the above result of ANDing the actual mode and the inverse
# mode. This will determine if any of the bits that would indicate a more
# permissive mode are set in the actual mode.
#
# 3. If the result is 0000, the files mode is equal
# to or less permissive than the desired mode (PASS). Otherwise, the files
# mode is more permissive than the desired mode (FAIL).
max_mode = max_mode.to_i(8)
inv_mode = 0777 ^ max_mode
inv_mode & file.mode != 0
end
def to_s
"File #{source_path}"
end
private
def file_permission_granted?(access_type, by_usergroup, by_specific_user)
raise "`file_permission_granted?` is not supported on your OS" if @perms_provider.nil?
if by_specific_user.nil? || by_specific_user.empty?
@perms_provider.check_file_permission_by_mask(file, access_type, by_usergroup, by_specific_user)
else
@perms_provider.check_file_permission_by_user(access_type, by_specific_user, source_path)
end
end
end
class FilePermissions
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
end
class UnixFilePermissions < FilePermissions
def permission_flag(access_type)
case access_type
when "read"
"r"
when "write"
"w"
when "execute"
"x"
else
raise "Invalid access_type provided"
end
end
def usergroup_for(usergroup, specific_user)
if usergroup == "others"
"other"
elsif (usergroup.nil? || usergroup.empty?) && specific_user.nil?
"all"
else
usergroup
end
end
def check_file_permission_by_mask(file, access_type, usergroup, specific_user)
usergroup = usergroup_for(usergroup, specific_user)
flag = permission_flag(access_type)
mask = file.unix_mode_mask(usergroup, flag)
raise "Invalid usergroup/owner provided" if mask.nil?
(file.mode & mask) != 0
end
def check_file_permission_by_user(access_type, user, path)
flag = permission_flag(access_type)
if inspec.os.linux?
perm_cmd = "su -s /bin/sh -c \"test -#{flag} #{path}\" #{user}"
elsif inspec.os.bsd? || inspec.os.solaris?
perm_cmd = "sudo -u #{user} test -#{flag} #{path}"
elsif inspec.os.aix?
perm_cmd = "su #{user} -c test -#{flag} #{path}"
elsif inspec.os.hpux?
perm_cmd = "su #{user} -c \"test -#{flag} #{path}\""
else
return skip_resource "The `file` resource does not support `by_user` on your OS."
end
cmd = inspec.command(perm_cmd)
cmd.exit_status == 0 ? true : false
end
end
class WindowsFilePermissions < FilePermissions
def check_file_permission_by_mask(_file, _access_type, _usergroup, _specific_user)
raise "`check_file_permission_by_mask` is not supported on Windows"
end
def more_permissive_than?(*)
raise Inspec::Exceptions::ResourceSkipped, "The `more_permissive_than?` matcher is not supported on your OS yet."
end
def check_file_permission_by_user(access_type, user, path)
access_rule = translate_perm_names(access_type)
access_rule = convert_to_powershell_array(access_rule)
cmd = inspec.command("@(@((Get-Acl '#{path}').access | Where-Object {$_.AccessControlType -eq 'Allow' -and $_.IdentityReference -eq '#{user}' }) | Where-Object {($_.FileSystemRights.ToString().Split(',') | % {$_.trim()} | ? {#{access_rule} -contains $_}) -ne $null}) | measure | % { $_.Count }")
cmd.stdout.chomp == "0" ? false : true
end
private
def convert_to_powershell_array(arr)
if arr.empty?
"@()"
else
%{@('#{arr.join("', '")}')}
end
end
# Translates a developer-friendly string into a list of acceptable
# FileSystemRights that match it, because Windows has a fun heirarchy
# of permissions that are able to be noted in multiple ways.
#
# See also: https://www.codeproject.com/Reference/871338/AccessControl-FileSystemRights-Permissions-Table
def translate_perm_names(access_type)
names = translate_common_perms(access_type)
names ||= translate_granular_perms(access_type)
names ||= translate_uncommon_perms(access_type)
raise "Invalid access_type provided" unless names
names
end
def translate_common_perms(access_type)
case access_type
when "full-control"
%w{FullControl}
when "modify"
translate_perm_names("full-control") + %w{Modify}
when "read"
translate_perm_names("modify") + %w{ReadAndExecute Read}
when "write"
translate_perm_names("modify") + %w{Write}
when "execute"
translate_perm_names("modify") + %w{ReadAndExecute ExecuteFile Traverse}
when "delete"
translate_perm_names("modify") + %w{Delete}
end
end
def translate_uncommon_perms(access_type)
case access_type
when "delete-subdirectories-and-files"
translate_perm_names("full-control") + %w{DeleteSubdirectoriesAndFiles}
when "change-permissions"
translate_perm_names("full-control") + %w{ChangePermissions}
when "take-ownership"
translate_perm_names("full-control") + %w{TakeOwnership}
when "synchronize"
translate_perm_names("full-control") + %w{Synchronize}
end
end
def translate_granular_perms(access_type)
case access_type
when "write-data", "create-files"
translate_perm_names("write") + %w{WriteData CreateFiles}
when "append-data", "create-directories"
translate_perm_names("write") + %w{CreateDirectories AppendData}
when "write-extended-attributes"
translate_perm_names("write") + %w{WriteExtendedAttributes}
when "write-attributes"
translate_perm_names("write") + %w{WriteAttributes}
when "read-data", "list-directory"
translate_perm_names("read") + %w{ReadData ListDirectory}
when "read-attributes"
translate_perm_names("read") + %w{ReadAttributes}
when "read-extended-attributes"
translate_perm_names("read") + %w{ReadExtendedAttributes}
when "read-permissions"
translate_perm_names("read") + %w{ReadPermissions}
end
end
end
end
11.5. Directory
require "inspec/resources/file"
module Inspec::Resources
class Directory < FileResource
name "directory"
supports platform: "unix"
supports platform: "windows"
desc "Use the directory InSpec audit resource to test if the file type is a directory. This is equivalent to using the file InSpec audit resource and the be_directory matcher, but provides a simpler and more direct way to test directories. All of the matchers available to file may be used with directory."
example <<~EXAMPLE
describe directory('path') do
it { should be_directory }
end
EXAMPLE
def exist?
file.exist? && file.directory?
end
def to_s
"Directory #{source_path}"
end
end
end
11.6. etc_hosts
require "inspec/utils/parser"
require "inspec/utils/file_reader"
class EtcHosts < Inspec.resource(1)
name "etc_hosts"
supports platform: "linux"
supports platform: "bsd"
supports platform: "windows"
desc 'Use the etc_hosts InSpec audit resource to find an
ip_address and its associated hosts'
example <<~EXAMPLE
describe etc_hosts.where { ip_address == '127.0.0.1' } do
its('ip_address') { should cmp '127.0.0.1' }
its('primary_name') { should cmp 'localhost' }
its('all_host_names') { should eq [['localhost', 'localhost.localdomain', 'localhost4', 'localhost4.localdomain4']] }
end
EXAMPLE
attr_reader :params
include CommentParser
include FileReader
DEFAULT_UNIX_PATH = "/etc/hosts".freeze
DEFAULT_WINDOWS_PATH = 'C:\windows\system32\drivers\etc\hosts'.freeze
def initialize(hosts_path = nil)
content = read_file_content(hosts_path || default_hosts_file_path)
@params = parse_conf(content.lines)
end
FilterTable.create
.register_column(:ip_address, field: "ip_address")
.register_column(:primary_name, field: "primary_name")
.register_column(:all_host_names, field: "all_host_names")
.install_filter_methods_on_resource(self, :params)
private
def default_hosts_file_path
inspec.os.windows? ? DEFAULT_WINDOWS_PATH : DEFAULT_UNIX_PATH
end
def parse_conf(lines)
lines.reject(&:empty?).reject(&comment?).map(&parse_data).map(&format_data)
end
def comment?
parse_options = { comment_char: "#", standalone_comments: false }
->(data) { parse_comment_line(data, parse_options).first.empty? }
end
def parse_data
->(data) { [data.split[0], data.split[1], data.split[1..-1]] }
end
def format_data
->(data) { %w{ip_address primary_name all_host_names}.zip(data).to_h }
end
end
11.7. Docker
#
# Copyright 2017, Christoph Hartmann
#
require "inspec/resources/command"
require "inspec/utils/filter"
require "hashie/mash"
module Inspec::Resources
class DockerContainerFilter
# use filtertable for containers
filter = FilterTable.create
filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
filter.register_column(:commands, field: "command")
.register_column(:ids, field: "id")
.register_column(:images, field: "image")
.register_column(:labels, field: "labels", style: :simple)
.register_column(:local_volumes, field: "localvolumes")
.register_column(:mounts, field: "mounts")
.register_column(:names, field: "names")
.register_column(:networks, field: "networks")
.register_column(:ports, field: "ports")
.register_column(:running_for, field: "runningfor")
.register_column(:sizes, field: "size")
.register_column(:status, field: "status")
.register_custom_matcher(:running?) do |x|
x.where { status.downcase.start_with?("up") }
end
filter.install_filter_methods_on_resource(self, :containers)
attr_reader :containers
def initialize(containers)
@containers = containers
end
end
class DockerImageFilter
filter = FilterTable.create
filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
filter.register_column(:ids, field: "id")
.register_column(:repositories, field: "repository")
.register_column(:tags, field: "tag")
.register_column(:sizes, field: "size")
.register_column(:digests, field: "digest")
.register_column(:created, field: "createdat")
.register_column(:created_since, field: "createdsize")
filter.install_filter_methods_on_resource(self, :images)
attr_reader :images
def initialize(images)
@images = images
end
end
class DockerPluginFilter
filter = FilterTable.create
filter.add(:ids, field: "id")
.add(:names, field: "name")
.add(:versions, field: "version")
.add(:enabled, field: "enabled")
filter.connect(self, :plugins)
attr_reader :plugins
def initialize(plugins)
@plugins = plugins
end
end
class DockerServiceFilter
filter = FilterTable.create
filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
filter.register_column(:ids, field: "id")
.register_column(:names, field: "name")
.register_column(:modes, field: "mode")
.register_column(:replicas, field: "replicas")
.register_column(:images, field: "image")
.register_column(:ports, field: "ports")
filter.install_filter_methods_on_resource(self, :services)
attr_reader :services
def initialize(services)
@services = services
end
end
# This resource helps to parse information from the docker host
# For compatability with Serverspec we also offer the following resouses:
# - docker_container
# - docker_image
class Docker < Inspec.resource(1)
name "docker"
supports platform: "unix"
desc "
A resource to retrieve information about docker
"
example <<~EXAMPLE
describe docker.containers do
its('images') { should_not include 'u12:latest' }
end
describe docker.images do
its('repositories') { should_not include 'inssecure_image' }
end
describe docker.plugins.where { name == 'rexray/ebs' } do
it { should exist }
end
describe docker.services do
its('images') { should_not include 'inssecure_image' }
end
describe docker.version do
its('Server.Version') { should cmp >= '1.12'}
its('Client.Version') { should cmp >= '1.12'}
end
describe docker.object(id) do
its('Configuration.Path') { should eq 'value' }
end
docker.containers.ids.each do |id|
# call docker inspect for a specific container id
describe docker.object(id) do
its(%w(HostConfig Privileged)) { should cmp false }
its(%w(HostConfig Privileged)) { should_not cmp true }
end
end
EXAMPLE
def containers
DockerContainerFilter.new(parse_containers)
end
def images
DockerImageFilter.new(parse_images)
end
def plugins
DockerPluginFilter.new(parse_plugins)
end
def services
DockerServiceFilter.new(parse_services)
end
def version
return @version if defined?(@version)
data = {}
cmd = inspec.command("docker version --format '{{ json . }}'")
data = JSON.parse(cmd.stdout) if cmd.exit_status == 0
@version = Hashie::Mash.new(data)
rescue JSON::ParserError => _e
Hashie::Mash.new({})
end
def info
return @info if defined?(@info)
data = {}
# docke info format is only supported for Docker 17.03+
cmd = inspec.command("docker info --format '{{ json . }}'")
data = JSON.parse(cmd.stdout) if cmd.exit_status == 0
@info = Hashie::Mash.new(data)
rescue JSON::ParserError => _e
Hashie::Mash.new({})
end
# returns information about docker objects
def object(id)
return @inspect if defined?(@inspect)
data = JSON.parse(inspec.command("docker inspect #{id}").stdout)
data = data[0] if data.is_a?(Array)
@inspect = Hashie::Mash.new(data)
rescue JSON::ParserError => _e
Hashie::Mash.new({})
end
def to_s
"Docker Host"
end
private
def parse_json_command(labels, subcommand)
# build command
format = labels.map { |label| "\"#{label}\": {{json .#{label}}}" }
raw = inspec.command("docker #{subcommand} --format '{#{format.join(", ")}}'").stdout
output = []
# since docker is not outputting valid json, we need to parse each row
raw.each_line do |entry|
# convert all keys to lower_case to work well with ruby and filter table
row = JSON.parse(entry).map do |key, value|
[key.downcase, value]
end.to_h
# ensure all keys are there
row = ensure_keys(row, labels)
# strip off any linked container names
# Depending on how it was linked, the actual container name may come before
# or after the link information, so we'll just look for the first name that
# does not include a slash since that is not a valid character in a container name
if row["names"]
row["names"] = row["names"].split(",").find { |c| !c.include?("/") }
end
# Split labels on ',' or set to empty array
# Allows for `docker.containers.where { labels.include?('app=redis') }`
row["labels"] = row.key?("labels") ? row["labels"].split(",") : []
output.push(row)
end
output
rescue JSON::ParserError => _e
warn "Could not parse `docker #{subcommand}` output"
[]
end
def parse_containers
# @see https://github.com/moby/moby/issues/20625, works for docker 1.13+
# raw_containers = inspec.command('docker ps -a --no-trunc --format \'{{ json . }}\'').stdout
# therefore we stick with older approach
labels = %w{Command CreatedAt ID Image Labels Mounts Names Ports RunningFor Size Status}
# Networks LocalVolumes work with 1.13+ only
if !version.empty? && Gem::Version.new(version["Client"]["Version"]) >= Gem::Version.new("1.13")
labels.push("Networks")
labels.push("LocalVolumes")
end
parse_json_command(labels, "ps -a --no-trunc")
end
def parse_services
parse_json_command(%w{ID Name Mode Replicas Image Ports}, "service ls")
end
def ensure_keys(entry, labels)
labels.each do |key|
entry[key.downcase] = nil unless entry.key?(key.downcase)
end
entry
end
def parse_images
# docker does not support the `json .` function here, therefore we need to emulate that behavior.
raw_images = inspec.command('docker images -a --no-trunc --format \'{ "id": {{json .ID}}, "repository": {{json .Repository}}, "tag": {{json .Tag}}, "size": {{json .Size}}, "digest": {{json .Digest}}, "createdat": {{json .CreatedAt}}, "createdsize": {{json .CreatedSince}} }\'').stdout
c_images = []
raw_images.each_line do |entry|
c_images.push(JSON.parse(entry))
end
c_images
rescue JSON::ParserError => _e
warn "Could not parse `docker images` output"
[]
end
def parse_plugins
plugins = inspec.command('docker plugin ls --format \'{"id": {{json .ID}}, "name": "{{ with split .Name ":"}}{{index . 0}}{{end}}", "version": "{{ with split .Name ":"}}{{index . 1}}{{end}}", "enabled": {{json .Enabled}} }\'').stdout
c_plugins = []
plugins.each_line do |entry|
c_plugins.push(JSON.parse(entry))
end
c_plugins
rescue JSON::ParserError => _e
warn "Could not parse `docker plugin ls` output"
[]
end
end
end