4. How to Get Started - InSpec Commands & Docs
InSpec Commands and Documentation
Before we test our NGINX configuration, let's take a look at the InSpec commands and documentation we can use to write tests.
How to Run InSpec
Use the InSpec executable with the command inspec exec
to run a profile against a system. The generic command is below, but take a look at our breakdown on How To Run InSpec in the MITRE SAF User course for more information.
inspec exec WHERE_IS_THE_PROFILE -t WHAT_IS_THE_TARGET --more-flags EXTRA_STUFF --reporter WHAT_SHOULD_INSPEC_DO_WITH_THE_RESULTS
Want to try it out?
You can run a simple InSpec command against the nginx target running in the development lab environment using your my_nginx
profile. Remember, this profile only has one sample control right now.
inspec exec my_nginx -t docker://nginx --reporter cli
Redirecting to cinc-auditor...
Profile: InSpec Profile (my_nginx)
Version: 0.1.0
Target: docker://31e4ea1be052a9bcc13700
Target ID: 761efa53-ee0a-5ea0-a459-b2a5d28
✔ tmp-1.0: Create /tmp directory
✔ File /tmp is expected to be directory
File /tmp
✔ is expected to be directory
Profile Summary: 1 successful control, 0 control failures, 0 controls skipped
Test Summary: 2 successful, 0 failures, 0 skipped
InSpec References
The inspec exec
command is used to run or execute InSpec profiles. Now, let's talk about InSpec's existing documentation and features to support writing those InSpec profiles.
Good InSpec tests will use resources, many of which are already built in, to easily describe some part of the system. Remember the file
resource as an example from the previous section. Additionally, tests should use matchers to implement the logic check of an expected result. The previous example used be_directory
as a matcher. There are a number of built-in matchers: be_in
, be_readable
, cmp
, include
, to list a few.
What's up with CINC Auditor?
You may note that every time we invoke InSpec in these classes, the first line of output is the following:
Redirecting to cinc-auditor...
Don't worry - that's just because we are using an open-source community build of the InSpec tool called "CINC Auditor," where CINC stands for "CINC-is-not-Chef." We're doing this because the community build doesn't require a license to run. It is otherwise identical to the Chef-released version of InSpec.
Make Writing InSpec Easier with Built-in Resources
InSpec features dozens of resources and matchers that come predefined in the language. These resources are a core benefit to using InSpec because they allow you to leverage existing stable code to write simple and consistent tests.
When writing profiles, refer back to the documentation for resources. They often give you an example of how to write a test that matches your current problem.
Finding Documentation
- Explore the InSpec documentation to see which resources and matchers are available, including descriptions of their attributes and examples of their use.
- Examine the source code to see what's available. For example, you can see how
file
and other InSpec resources are implemented. - Reference examples, such as profiles provided on Chef Supermarket or developed by the SAF team, as a guide.
- Explore InSpec interactively using the InSpec shell.
The InSpec shell
The InSpec shell enables you to explore InSpec interactively. In this part, you'll use the InSpec shell to discover which resources can be used to test your NGINX configuration.
You are not required to use the InSpec shell to develop your profiles, but some users (including this course's instructors) find the InSpec shell to be a useful way to get immediate feedback and explore what's available. You can also use the InSpec shell to debug your tests; the shell lets you write and execute describe
blocks in-line.
What is the InSpec Shell?
InSpec shell is based on a tool called pry
. pry
is an interactive (or 'runtime') debugging environment for Ruby. There are equivalent tools in most programming languages for examining your code mid-execution.
Entering the InSpec shell
Run inspec shell
to enter the interactive session.
inspec shell
Redirecting to cinc-auditor...
Welcome to the interactive InSpec Shell
To find out how to use it, type: help
You are currently running on:
Name: ubuntu
Families: debian, linux, unix, os
Release: 20.04
Arch: x86_64
inspec>
Run help
to see what commands are available.
inspec> help
inspec> help
You are currently running on:
Name: ubuntu
Families: debian, linux, unix, os
Release: 20.04
Arch: x86_64
Available commands:
`[resource]` - run resource on target machine
`help resources` - show all available resources that can be used as commands
`help [resource]` - information about a specific resource
`help matchers` - show information about common matchers
`exit` - exit the InSpec shell
You can use resources in this environment to test the target machine. For example:
command('uname -a').stdout
file('/proc/cpuinfo').content => "value"
Run help resources
to see which resources are available.
inspec> help resources
inspec> help resources
- aide_conf
- apache
- apache_conf
- apt
- audit_policy
- auditd
- auditd_conf
...
- file
...
- xml
- yaml
- yum
- zfs_dataset
- zfs_pool
You see file
and other resources listed.
Using the InSpec Shell
Earlier, we saw this describe
block:
describe file('/tmp') do
it { should be_directory }
end
The InSpec shell understands the structure of blocks. This enables you to run multiline code from the shell terminal - we can run an entire describe
block by simply copying it and pasting the whole thing into the InSpec shell.
describe file('/tmp') do
it { should be_directory }
end
inspec> describe file('/tmp') do
inspec> it { should be_directory }
inspec> end
Profile: inspec-shell
Version: (not specified)
Target ID:
File /tmp
✔ is expected to be directory
Test Summary: 1 successful, 0 failures, 0 skipped
In practice, you don't typically run controls interactively this way for day to day use, but it is a great way to test out your ideas, find bugs, or validate your approach before running a scan in its entirety on a target of evaluation.
What is the difference between InSpec and Ruby?
Inspec is a Domain Specific Language (DSL) built on top of Ruby. In other words, InSpec is just the Ruby programming language with some extra syntax and keywords tacked on. For example, InSpec matchers are implemented as Ruby methods. This allows you to have the full power of the Ruby programming language at your hands as you write your tests.
Exploring Resources
file
example
You can also use the InSpec shell to explore resources, in addition to referencing the resource documentation. Here, we can use the InSpec shell to see how the file
resource functions.
Run this command to list which methods are available to the file
resource.
file('/tmp').class.superclass.instance_methods(false).sort
inspec> file('/tmp').class.superclass.instance_methods(false).sort
=> [:allowed?,
:basename,
:block_device?,
:character_device?,
:contain,
:content,
:directory?,
...
:sticky,
:sticky?,
:suid,
:symlink?,
:to_s,
:type,
:uid,
:version?,
:writable?]
You can use the arrow keys or Page Up and Page Down keys to scroll through the list. When you're done, press q
.
Exploring Resources in the InSpec Shell
Let's use the InSpec shell to explore some resources in InSpec. We will start with one of the most common elements on the system: a directory. In the InSpec shell, call the file.directory?
method.
file('/tmp').directory?
inspec> file('/tmp').directory?
=> true
This will return true
, since /tmp
is a directory on the system and exists on your workstation container.
To make the tests easier to read, the InSpec language uses "syntactic sugar" to turn methods into English-like phrases. For example, the style conventions for the Ruby language say that methods that return booleans (values that evaluate to true
or false
where nil
is a type of false) ought to have names that end in ?
. InSpec automatically changes the syntax of these methods to include be_
before the method rather than ?
after the method to make it more readable in a describe
block. For example, to check if a directory exists, Ruby would traditionally use directory?
while InSpec uses be_directory
.
Using Ruby Predicate Methods
Given what we have just learned, the best practice in InSpec is to return something that evaluates to true
or false
.
The ?
(or be_
in InSpec) makes your method a Ruby Predicate Method. See Ruby predicate methods to learn more.
nginx
example
For our example use case, we want to be able to run tests against an NGINX webserver.
In the next section, we will start writing controls for my_nginx
profile, but first let's see what resources are available to us to model NGINX itself.
Run help resources
a second time to identify InSpec's provided two built-in resources to support NGINX – nginx
and nginx_conf
.
help resources
inspec> help resources
- aide_conf
- apache
- apache_conf
- apt
...
- nginx
- nginx_conf
...
- xml
- yaml
- yum
- yumrepo
- zfs_dataset
- zfs_pool
Run nginx.methods
. This will list all of the available methods for the nginx
resource. You can see the version
and modules
methods. These will be useful to us in the next section.
nginx.methods
=> [:params,
:bin_dir,
:prefix,
:openssl_version,
:compiler_info,
:support_info,
...
:http_scgi_temp_path,
:version,
:resource_id,
:to_s,
:service,
...
:equal?,
:__id__,
:instance_eval,
:instance_exec]
Run nginx.version
to see what result you get.
nginx.version
inspec> nginx.version
NoMethodError: undefined method `[]' for nil:NilClass
from /opt/inspec/embedded/lib/ruby/gems/2.4.0/gems/inspec-2.0.17/lib/resources/nginx.rb:39:in `block (2 levels) in <class:Nginx>'
Uh-oh. We got an error. Why?
Recall that you're running InSpec from a shell running on your host machine, which does not have NGINX installed (NGINX is installed inside your Docker container, but we did not tell InSpec that we are trying to examine a container). Therefore, when the nginx
resource tries to access information about NGINX, it cannot do so and throws an error when it reads a nil
.
We can verify this is the issue by invoking the package
resource to check if we even have NGINX present on this system:
package('nginx').installed?
inspec> package('nginx').installed?
false
As you can see we get false
- since NGINX is not installed on your runner.
We can instead run InSpec shell commands against the target that does have NGINX installed to see what results we find.
To do so, first start by exiting your InSpec shell session.
exit
Run docker ps
to see the running docker containers in your development lab environment that we can test:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d4bcef5bb9e3 nginx:latest "/docker-entrypoint.…" 23 seconds ago Up 22 seconds 80/tcp nifty_shtern
4f0ceb9b5974 nginx:latest "/docker-entrypoint.…" 2 months ago Up 45 seconds 0.0.0.0:80->80/tcp nginx
We can enter the InSpec shell on the nginx container instead of our lab environment host machine like before.
Run inspec shell
, this time providing the -t
argument to connect the shell session to the target container.
inspec shell -t docker://nginx
inspec shell -t docker://CONTAINER_ID
Welcome to the interactive InSpec Shell
To find out how to use it, type: help
You are currently running on:
Name: debian
Families: debian, linux, unix, os
Release: 12.2
Arch: x86_64
InSpec is agentless
Remember that the target does not have InSpec installed on it. Your shell session exists on the workstation; InSpec routes commands to the target instance over Docker.
Check that the nginx package is installed, this time on the target container.
package('nginx').installed?
inspec> package('nginx').installed?
=> true
Now, let's get the version
of NGINX that is installed on the target, run: nginx.version
. You can see that version 1.23.3 was installed on our container.
nginx.version
inspec> nginx.version
=> "1.25.3"
To complete the example, let's see which modules
are enabled on the nginx container. Run nginx.modules
to list the installed NGINX modules.
nginx.modules
=> ["http_addition",
"http_auth_request",
"http_dav",
"http_flv",
"http_gunzip",
"http_gzip_static",
"http_mp4",
"http_random_index",
"http_realip",
"http_secure_link",
"http_slice",
"http_ssl",
"http_stub_status",
"http_sub",
"http_v2",
"mail_ssl",
"stream_realip",
"stream_ssl",
"stream_ssl_preread"]
Looking at the nginx_conf resource
The nginx_conf resource examines the contents of the NGINX configuration file, /etc/nginx/nginx.conf
.
To check whether the NGINX configuration file exists as a file, we want to test attributes of the file itself, so we use the file
resource.
Use the file
resource to check whether the NGINX configuration file is owned by root
and is not readable, writeable, or executable by others. You saw earlier how the file
resource provides the readable
, writeable
, and executable
methods. You would also see that the file
resource provides the owned_by
and grouped_into
methods.
ls file('/tmp')
Inspec::Resource#methods:
check_supported! check_supports fail_resource inspec inspect ...
Inspec::Resources::FilePermissionsSelector#methods: select_file_perms_style
Inspec::Utils::LinuxMountParser#methods: includes_whitespaces? parse_mount_options
file#methods:
allowed? content_as_json file? inherited?
path setgid? socket? symlink?
...
content file immutable? mode?
owner selinux_label size version?
instance variables:
@__backend_runner__ @__resource_name__ @file @path @perms_provider ...
file('/tmp').class.superclass.instance_methods(false).sort
=> [:allowed?,
:directory?,
:executable?,
:exist?,
:file,
:file?,
:file_version,
:gid,
:group,
:grouped_into?,
...
:owned_by?,
...
:readable?,
...
:to_s,
:type,
:uid,
:version?,
:writable?]
To check whether shell access has been provided to non-admin users, because we want to test attributes of users, you'll use the users
resource.
ls users
Inspec::Resource#methods:
check_supported! fail_resource inspect
...
inspec resource_id skip_resource
Inspec::Resources::UserManagementSelector#methods: select_user_manager
users#methods:
count disabled disabled? enabled? entries exist? exists?
gids groupnames groups homes maxdays mindays raw_data
...
@resource_params @resource_skipped @supports @user_provider
users.class.superclass.instance_methods(false).sort
=> [:count,
:disabled,
:disabled?,
:enabled?,
:entries,
:exist?,
:exists?,
:gids,
:groupnames,
:groups,
:homes,
:maxdays,
:mindays,
:raw_data,
:shells,
:to_s,
:uids,
:usernames,
:warndays,
:where]
Exit the InSpec shell session with the exit
command.
exit
At this point we have some experience playing around with InSpec resources to see what they can tell us about system components. Let's take what we've learned and use it to write more structured, repeatable tests.