Skip to content
SAF Advanced InSpec Profile Developer CourseSAF Advanced InSpec Profile Developer Course
MITRE InSpec Advanced Profile Developer Course
Course
Resources
Installation
  • Course

    • 1. Course Overview
      • 2. Review the Fundamentals
        • 3. Practice the Fundamentals
          • 4. Tools for Automation
            • 5. Automate Security Testing
              • 6. Explore InSpec Resources
                • 7. Local vs Built-in Resources
                  • 8. Create a Custom Resource - The Git Example
                    • 9. Create a Custom Resource - The Docker Example
                      • 10. Writing Plural Resources
                        • 11. Dissecting Resources
                          • 11.1. bridge
                            • 11.2. command
                              • 11.3. NGINX
                                • 11.4. File
                                  • 11.5. Directory
                                    • 11.6. etc_hosts
                                      • 11.7. Docker
                                      • 12. Exercise - Develop your own resources
                                        • 13. Add Your Resource to InSpec
                                          • 14. Custom Resource Examples from InSpec

                                          11. Dissecting Resources

                                          June 7, 2022About 11 min

                                          On This Page
                                          • 11.1. bridge
                                          • 11.2. command
                                          • 11.3. NGINX
                                          • 11.4. File
                                          • 11.5. Directory
                                          • 11.6. etc_hosts
                                          • 11.7. Docker

                                          # 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
                                          
                                          Edit this pageopen in new window
                                          Last update: 6/9/2022, 3:47:47 PM
                                          Contributors: Emily Rodriguez
                                          Prev
                                          10. Writing Plural Resources
                                          Next
                                          12. Exercise - Develop your own resources
                                          Apache-2.0 | Copyright © 2022 - The MITRE Corporation
                                          Copyright © 2022 Aaron Lippold