4. Exploring InSpec Resources
Resources
Now that you have learned about making and running InSpec profiles, let's dig deeper into the mechanics of InSpec by learning about resources.
Core Resources
As you saw in the Beginner class, when writing InSpec code, many resources are automatically available because they come "batteries included" with InSpec.
- You can explore the core InSpec resources on Chef's documentation website.
- You can also examine the source code to see what's available. For example, you can see how
file
and other InSpec resources are implemented.
Local Resources
Local resources are those that exist only in the profile in which they are developed. Local resources are put in the libraries
directory:
$ tree examples/profile
examples/profile
...
├── libraries
│ └── custom_resource.rb
Note that the libraries
directory is not created by default within a profile when we use inspec init
. We need to create the directory ourselves.
Once you create and populate a custom resource Ruby file inside the libraries
directory, it can be utilized inside your local profile just like the core resources.
Resource Structure
Like InSpec controls, InSpec resources are written as regular Ruby classes, which means you have the full power of Ruby at your fingertips as you craft this resource.
In addition to the resource name, the following attributes can be configured:
name
- Identifier of the resource (required)desc
- Description of the resource (optional)example
- Example usage of the resource (optional)supports
- (Chef InSpec 2.0+) Platform restrictions of the resource (optional)
The following methods are available to the resource:
inspec
- Contains a registry of all other resources to interact with the operating system or target in general.skip_resource
- A resource may call this method to indicate that requirements aren’t met. All tests that use this resource will be marked as skipped.
The etc_hosts
example
Let's look at a simple default resource to get an idea how these resources are used. We'll take a look at the source code for the InSpec resource that models an operating system's hostfile, which is a simple file where we can map IP addresses (e.g. 198.162.8.1) to domain names (e.g. my-heimdall-deployment.my-domain.dev) without having to add a record to a DNS server somewhere.
require "inspec/utils/parser"
require "inspec/utils/file_reader"
module Inspec::Resources
class EtcHosts < Inspec.resource(1)
name "etc_hosts"
supports platform: "unix"
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 Inspec::Utils::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)
def to_s
"Hosts File"
end
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
end
If you've ever done object-oriented programming, you might be seeing some familiar concepts in this resource file. Let's break down some of the subcomponents of the resource.
require (lines 1 and 2)
The require
statement pulls in code written in other files or Ruby modules that we will use in this resource. In this case we are importing some simple utility functions defined elsewhere in the InSpec codebase.
class
The class
is where a Ruby class definition is given. Classes define the structure and function of an object that we can instantiate to model something.
name
The name
defines what token we can use to invoke this resource within our controls. Remember all those describe
blocks we wrote that invoked the nginx
resource? We used the term nginx
to invoke the resource because that token is the defined name
of the resource in its class definition.
supports
The supports
keyword is used to define what types of platforms should be able to use this resource. The example above only supports the Windows and Unix-based operating systems, but other resources could state that they only support specific cloud providers or specific Linux distro releases.
desc
A simple, human-friendly description of the purpose of this resource. This is also what gets printed when you run <resource> help
in the InSpec shell.
examples
One or more simple sample usages of the resource. It typically consists of a describe
block using the resource, given as a multi-line string via the squiggly heredoc
syntax.
private
This is a keyword that asserts that every function definition that shows up below this line in the class file should be considered private, or not accessible to users who instantiate an object out of this class. For example, when using this resource in a control file, you cannot invoke the parse_data
function, because it is a private function that should really only be invoked by the resource class itself when the object is first created.
initialize method
An initialize
method is required if your resource needs to be able to accept a parameter when called in a test (e.g. the file
resource takes in a string parameter that specifies the location of the file being examined: file('this/path/is/a/parameter')
).
functionality methods
These methods return data from the resource so that you can use it in tests. There can be just a few of them, or there can be a whole bunch. These methods are how we define the custom matchers that can be invoked in an InSpec control file. We'll build some simple examples in the next section.