Skip to main content

7. InSpec Control Enhancements

Aaron LippoldAbout 6 min

InSpec Control Enhancements

You should have the basic idea of how an InSpec test is written and how to leverage InSpec's built-in features to help you write tests quickly and easily. Now let's discuss some of the more powerful features available to you when writing tests to control what output is displayed to the user.

RSpec

The InSpec testing framework is itself built on RSpecopen in new window, a unit testing module for Ruby. InSpec extended rspec by including features like the resources library and more user-friendly syntax for writing tests, but we can still write our tests using RSpec syntax and they will work just fine.

Most of the time you won't need to use RSpec syntax to write a good test. But we want to show you a few neat tricks you can accomplish with RSpec.

RSpec Explicit Subject

We will write a few tests in this section to demonstrate the difference between InSpec's default syntax and RSpec syntax.

Let's pretend we have a new requirement for NGINX:

6. NGINX's /etc/nginx directory should not be empty.

(It's a bit of an odd requirement, but bear with us for the sake of this example.)

First, we'll try a test that does not use RSpec syntax to illustrate the problem we want to solve:

Code
control 'Requirement 6' do
  impact 1.0
  title 'Checking that /etc/nginx does not return empty'
  desc 'Let\'s do this the ugly way.'
  describe command('ls -al').stdout.strip do
    it { should_not be_empty }
  end
end

Well. . . it sort of works.

Notice how much output InSpec printed here to answer the simple question of "did this command return empty?" Imagine if we had done this on a directory with many files in it. We'd just be cluttering up the screen (and our report files).

Wait, couldn't we have just used the directory resource for this?

Correct. That would have been a much better way of doing this, and illustrates the general principle of "don't use raw shell commands with the command resource unless you have to."

We're just doing it this way for the example.

If we would like to have a more detailed and cleaner feedback to our user, we can override the standard title of our describe block with a specific message that describes the intent of the test and use the Explicit Subjectopen in new window to tell inspec what the "subject" is for the test, then, we could refactor the code like this:

Code
control 'Requirement 6' do
  impact 1.0
  title 'Checking that /etc/nginx does not return empty'
  desc 'Let\'s do this the concise way.'
  describe "The /etc/nginx directory" do
    subject { command('ls -al').stdout.strip }
    it { should_not be_empty }
  end
end

Much better, right? We can override InSpec's default output to print a message that is actually useful.

Info

Another benefit to using subject is preventing command output from being stored in the report.

The should vs. expect syntax

Users familiar with the RSpec testing framework may know that there are two ways to write test statements: should and expect. The RSpec community decided that expect is the preferred syntax.

InSpec recommends the should syntax as it tends to read more easily. However, there are times when the expect syntax will communicate much more clearly to the end-user. InSpec will continue to support both methods of writing tests.

Let's copy the describe shown below directly into our example.rb file (we don't need to wrap them in a control block for this section). Consider this describe block from your my_nginx profile:

Code
describe file('/etc/nginx/nginx.conf') do
  it { should be_file }
end

This can be re-written with expect syntax.

Code
describe file('/etc/nginx/nginx.conf') do
  it 'should be a file' do
    expect(subject).to(be_file)
  end
end

Notice that the output is the same for both the examples above.

In addition, you can make use of the subject keyword to further control your output if you choose:

Code
describe 'I can make this any string I want!' do
  subject { file('/etc/nginx/nginx.conf') }
  it 'should be a file' do
    expect(subject).to(be_file)
  end
end

Info

Note that all three of the above code examples are running the same test that will pass or fail in the same circumstances — the difference is that the second two examples give more control over the message output that appears to the test performer. If you write tests that will later be executed by other people, you should be sure to write descriptive test output!

Reference: https://docs.chef.io/inspec/profiles/open in new window

expect syntax with a failure message

In addition to using an expect statement, a failure message can be added to provide a meaningful output to the end user.

Consider this shell access test from your nginx profile:

Code
describe users.shells(/bash/).usernames do
  it { should be_in input('admin_users')}
end

Again, this is a valid test, and will return the right pass/fail answer when run, but it is difficult for a person to parse if they were not the author ("The root user is supposed to be 'in' the admin user? What does that mean?!").

The output of a test like this can be refined to provide a cleaner output to the user. This can be done using a custom failure message:

Code
non_admin_users = users.shells(/bash/).usernames
describe "Shell access for non-admin users" do
  it "should be removed." do
    failure_message = "These non-admin should not have shell access: #{non_admin_users.join(", ")}"
    expect(non_admin_users).to eq(input('admin_users')), failure_message
  end
end

The failure_message variable in the above describe block is assigned a value by pure Ruby assignment. Remember how we said that, since InSpec is built on Ruby, any Ruby syntax will work inside an InSpec test? Ruby's string formatting syntax (the #{non_admin_users.join(", ")}) can create a string that lists the users who fail the test by having shell access when they shouldn't.

Writing good failure messages

The trick to writing useful failure messages is to use Ruby to find the subset of all elements we are testing (here, the users) that actually fail the test. We don't need to print a statement for every array element we tested; we only need to print a statement that shows the elements that failed.

Expect syntax and Password Hashes

Here's another example -- we have an InSpec test that checks if passwords are SHA512 hashes.

As we said, when possible, and when there is a high change of a large set only having a few offending items, attempt to find only those items that could be outside our requirements. If there are none -- wonderful! We met our requirement.

bad_users = inspec.shadow.where { password != "*" && password != "!" && password !~ /\$6\$/ }.users # note that SHA12-encrypted passwords are marked by starting with '$6$' in /etc/shadow

describe 'Password hashes in /etc/shadow' do
  it 'should only contain SHA512 hashes' do
    failure_message = "Users without SHA512 hashes: #{bad_users.join(', ')}"
    expect(bad_users).to be_empty, failure_message
  end
end

Using Multiple Resources For One Test

The file resource is perfect for looking at single files and their properties. However, it does not look at groups of files. To do that, we need to use multiple resources in concert.

Take a look at this example from a profile for use in AWS virtual machines. We use the command resource to run the find command and then use the file resource to investigate each result. Using multiple resources together is one of the key values InSpec provides, allowing you to get at just the data you need when you need it.

command('find ~/* -type f -maxdepth 0 -xdev').stdout.split.each do |fname|  # we need to be careful about using 'find' --
                                                                            # there could be a LOT of output if we are not specific enough with the search!
  describe file(fname) do
    its('owner') { should cmp 'ec2-user' }
  end
end

Avoid Large Sets or 'Check Everyone at the Door' Approaches

For IO intensive (full filesystem, or global scans) or large scale processes, try to be as specific as possible with your searches. Think about using 'negative logic' vs 'positive logic' - "Find me all the items outside my target set" vs "Look at each item in the set and ensure it has these properties".

This 'find the outsiders' vs 'check everyone at the door' approach can really speed things along. Again, keep your data set as small as possible, don't inspect more than the requirements require!