Cookie Consent by Cookie Consent

Integration and Compliance with InSpec

System Administration 2020-10-11 SunRead Time: 9 min.

Introduction

Chef InSpec is an open-source framework for testing and auditing your applications and infrastructure. Chef InSpec works by comparing the actual state of your system with the desired state that you express in easy-to-read and easy-to-write Chef InSpec code. Chef InSpec detects violations and displays findings in the form of a report, but puts you in control of remediation. [1]

After we got past that well marketed description on what InSpec is, let us be more specific on what you can do with it.

When developing Chef Cookbooks you usually have a default InSpec test file created for you which is also defined in the kitchen.yml manifest, i.e:

t0rrant@testing:~$ chef generate cookbook mycookbook
....

Your cookbook is ready. Type `cd mycookbook` to enter it.

....

Why not start by writing an InSpec test? Tests for the default recipe are stored at:

test/integration/default/default_test.rb

...
t0rrant@testing:~$ cd mycookbook/
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# test/integration/default/default_test.rb
# InSpec test for recipe mycookbook::default

# The InSpec reference, with examples and extensive documentation, can be
# found at https://www.inspec.io/docs/reference/resources/

unless os.windows?
  # This is an example test, replace with your own test.
  describe user('root'), :skip do
    it { should exist }
  end
end

# This is an example test, replace it with your own test.
describe port(80), :skip do
  it { should_not be_listening }
end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# kitchen.yml
...

verifier:
  name: inspec

...
suites:
  - name: default
    verifier:
      inspec_tests:
        - test/integration/default
    attributes:
t0rrant@testing:~/mycookbook$

So you can see that it is fairly simple to start smashing keys and add more tests for your cookbook.

Now imagine that besides that specific cookbook you have a running system, in production, with some of the same integration requirements. Let us assume the following requirement:

  • the machine should be serving MySQL from port 3306

according to those requirements we could define some rules for the system:

For the sake of simplicity, we assume the target system is running a debian-based Linux distribution

  • the package mariadb-server should be installed
  • the user mysql must exist
  • the mysqld process should be listening on port 3306

This can be easily described as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
describe package('mariadb-server') do
  it { should be_installed }
end

describe user('mysql') do
  it { should exist }
end

describe port(3306) do
  it { should be_listening }
  its('processes') { should include 'mysqld' }
end

The result of having that evaluated successfully should be something like:

Package mariadb-server
  ✔  is expected to be installed
User mysql
  ✔  is expected to exist
Port 3306
  ✔  is expected to be listening
  ✔  processes is expected to include "mysqld"

Cool, but wait... what are we actually testing here? Configuration, not functionality. These tests do not tell us if the MySQL server is actually working or that the SSH server is properly accepting Public Keys and not Passwords.

This shows that the system is compliant for our specific rules, but what we probably want is also a way to test for functionality.

As a first step let us organize these tests and create a profile for them. Let us call it database.

Creating InSpec Profiles

Chef InSpec supports the creation of complex test and compliance profiles, which organize controls to support dependency management and code reuse. Each profile is a standalone structure with its own distribution and execution flow. [2]

Here we will only cover the basic aspects of InSpec profiles, you should check the docs [2] for more information.

Profile Structure

A profile can be seen as a directory with the following structure:

t0rrant@testing:~/inspec/profile$ tree
    .
    ├── controls
    │   ├── control1.rb
    │   └── control2.rb
    ├── files
    │   └── example_input.yml
    ├── inspec.yml
    └── README.md

The README.md file should contain information about the profile, I usually leave some examples of a successful execution on the profile.

The inspec.yml file has general information about the profile, including inputs (we will get to that later), the profile name, supported platforms, the profile's version, among others.

The controls directory contains the InSpec tests which will be applied when executing the profile.

The files directory can contain additional files, accessible by the profile, or just arbitrary files useful outside of the testing scope, and it is optional.

We can also have a libraries directory in which we can have InSpec resources extensions and helper functions, and is also optional.

Database Profile

Let us create the previously mentioned structure with two control files (one for compliance and another for integration testing), the main inspec.yml file describing our profile and an input.yml (which we will use to pass custom values to variables we will be using in the integration testing).

t0rrant@testing:~/database$ tree
    .
    ├── controls
    │   ├── compliance.rb
    │   └── integration.rb
    ├── files
    │   └── input.yml
    ├── inspec.yml
    └── README.md

First the inspec.yml file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# inspec.yml
name: database
title: Basic Database
maintainer: Manuel Torrinha
copyright: Manuel torrinha
copyright_email: torrinha@implement.pt
license: MIT
summary: Verify that mariadb Server is configured and working properly
version: 1.0.0
supports:
  - platform-family: linux
inspec_version: "~> 4.0"

inputs:
  - name: db_host
    description: Hostname or IP address for accessing the database
    type: string
    value: '127.0.0.1'
  - name: db_port
    description: Port on which the database should be listening
    type: numeric
    value: '3306'
  - name: db_user
    description: Username used to access the database
    type: string
    required: true
  - name: db_password
    description: Password used to access the database
    type: string
    required: true

Looks good, we defined four input variables so that we can test different settings for the database connection. And also we made the db_user and db_password mandatory in order for any control that depends on either of them fails if they are not defined.

Next let us create the tests for compliance in controls/compliance.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# controls/compliance.rb
control 'mariadb-compliance' do
  title 'Check for proper configuration'

  describe package('mariadb-server') do
    it { should be_installed }
  end

  describe user('mysql') do
    it { should exist }
  end

  describe file('/etc/my.cnf') do
    it { should_not be_more_permissive_than('644') }
  end

  describe port(3306) do
    it { should be_listening }
    its('processes') { should include 'mysqld' }
  end
end

As you can see we just did the same as in the introduction, plus we also check for the main config file's permissions.

Finally, we will create the tests for integration in controls/integration.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# controls/integration.rb
control 'mariadb-integration' do
  title 'Checks for proper DB connection'

  sql = mysql_session(input('db_user'), input('db_password'), input('db_host'), input('db_port'))

  describe sql.query('show databases;') do
    its('exit_status') { should eq(0) }
  end
end

Here we are testing if the connection works, by executing a common query to list the existing database, which should not fail if we can authenticate successfully.

Now let us test this database profile:

t0rrant@testing:~/database$ inspec exec . --log-level=error

Profile: Basic Database (database)
Version: 1.0.0
Target:  local://

  ✔  mariadb-compliance: Check for proper configuration
     ✔  System Package mariadb-server is expected to be installed
     ✔  User mysql is expected to exist
     ✔  File /etc/my.cnf is expected not to be more permissive than "644"
     ✔  Port 3306 is expected to be listening
     ✔  Port 3306 processes is expected to include "mysqld"
  ×  mariadb-integration: Checks for proper DB connection
     ×  Control Source Code Error ./controls/integration.rb:1
     Input 'db_user' is required and does not have a value.


Profile Summary: 1 successful control, 1 control failure, 0 controls skipped
Test Summary: 4 successful, 1 failure, 0 skipped

As we can see the compliance control seems ok, however we can't run the integration tests because we did not define neither db_user or db_password. So, let us create the file files/input.yml and define them there:

1
2
3
# files/input.yml
db_user: 'avaliduser'
db_password: 'a6v8a8l2li0dp01as0s0w6r0d'
t0rrant@testing:~/database$ inspec exec . --input-file=files/input.yml --log-level=error

Profile: Basic Database (database)
Version: 1.0.0
Target:  local://

  ✔  mariadb-compliance: Check for proper configuration
     ✔  System Package mariadb-server is expected to be installed
     ✔  User mysql is expected to exist
     ✔  File /etc/my.cnf is expected not to be more permissive than "644"
     ✔  Port 3306 is expected to be listening
     ✔  Port 3306 processes is expected to include "mysqld"
  ✔  mariadb-integration: Checks for proper DB connection
     ✔  Command: `mysql -uavaliduser -pa6v8a8l2li0dp01as0s0w6r0d -h 127.0.0.1 --port 3306 -s -e "show databases;"` exit_status is expected to eq 0


Profile Summary: 2 successful controls, 0 control failures, 0 controls skipped
Test Summary: 5 successful, 0 failures, 0 skipped

Looks ok, although we may not want the output showing the whole command, including the user and password used to access the database, so let us modify the integration test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# controls/integration.rb
control 'mariadb-integration' do
  title 'Checks for proper DB connection'

  sql = mysql_session(input('db_user'), input('db_password'), input('db_host'), input('db_port'))

  describe 'A simple SQL query' do
    subject do
      sql.query('show databases;')
    end
    it 'should execute successfully' do
      expect(subject.exit_status).to eq(0)
    end
  end
end

Here we refer to RSpec syntax and use the subject block to define our test and the expect method for the assertion, while using the it block to validate the subject and customize the output through the title. Let us run the profile again:

t0rrant@testing:~/database$ inspec exec . --input-file=files/example_input.yml --log-level=error
Profile: Basic Database (database)
Version: 1.0.0
Target:  local://

  ✔  mariadb-compliance: Check for proper configuration
     ✔  System Package mariadb-server is expected to be installed
     ✔  User mysql is expected to exist
     ✔  File /etc/my.cnf is expected not to be more permissive than "644"
     ✔  Port 3306 is expected to be listening
     ✔  Port 3306 processes is expected to include "mysqld"
  ✔  mariadb-integration: Checks for proper DB connection
     ✔  A simple SQL command should execute successfully


Profile Summary: 2 successful controls, 0 control failures, 0 controls skipped
Test Summary: 6 successful, 0 failures, 0 skipped

Nice, now we don't see anything sensitive and can safely show the output of the test.

Conclusion

In this post we saw how to create InSpec tests, and that we are able to test both configuration settings, and functionality using InSpec. We also saw how to customize a test's output so we can hide sensitive information.

The files described in this post are available here

Feel free to leave comments below, any improvement to this and other articles is always welcome.

Thanks for reading!

References

[1]--, An Overview of Chef InSpec - Chef Web Docs [link]
[2](1, 2) --, About Chef InSpec Profiles - Chef Web Docs [link]

Tags: linux devops chef inspec compliance integration testing


avatar
Manuel Torrinha is an information systems engineer, with more than 10 years of experience in managing GNU/Linux environments. Has an MSc in Information Systems and Computer Engineering. Work interests include High Performance Computing, Data Analysis, and IT management and Administration. Knows diverse programming, scripting and markup languages. Speaks Portuguese and English.

Related Content


comments powered by Disqus