6. Inputs in InSpec
Refactoring the code to use Inputs
Your my_nginx
profile is off to a great start. As your requirements evolve, you can add additional controls. You can also run this profile as often as you need to verify whether your systems remain in compliance.
Let's review the control file, example.rb
.
control 'nginx-version' do
impact 1.0
title 'NGINX version'
desc 'The required version of NGINX should be installed.'
describe nginx do
its('version') { should cmp >= '1.27.0' }
end
end
control 'nginx-modules' do
impact 1.0
title 'NGINX modules'
desc 'The required NGINX modules should be installed.'
describe nginx do
its('modules') { should include 'http_ssl' }
its('modules') { should include 'stream_ssl' }
its('modules') { should include 'mail_ssl' }
end
end
control 'nginx-conf-file' do
impact 1.0
title 'NGINX configuration file'
desc 'The NGINX config file should exist.'
describe file('/etc/nginx/nginx.conf') do
it { should be_file }
end
end
control 'nginx-conf-perms' do
impact 1.0
title 'NGINX configuration'
desc 'The NGINX config file should owned by root, be writable only by owner, and not writeable or and readable by others.'
describe file('/etc/nginx/nginx.conf') do
it { should be_owned_by 'root' }
it { should be_grouped_into 'root' }
it { should_not be_readable.by('others') }
it { should_not be_writable.by('others') }
it { should_not be_executable.by('others') }
end
end
control 'nginx-shell-access' do
impact 1.0
title 'NGINX shell access'
desc 'The NGINX shell access should be restricted to admin users.'
describe users.shells(/bash/).usernames do
it { should be_in ['admin']}
end
end
Although these controls are straightforward, imagine if your controls
directory contained dozens or hundreds of tests, like we might see for a real security validation profile. Many of these tests will be referring to the same test values over and over again -- what happens when we need to change them? Or if I want to test to a different standard and want to adjust the values the tests are looking for? For example, what happens when a new major release of NGINX happens, and we want to test to make sure our deployed webserver is still up to date with the new latest version?
One way to improve these tests is to use inputs
. Inputs enable you to separate the logic of your tests from the data of your tests. Inputs can be provided in-line with your invocation of inspec exec
but they can also be stored in input files
and placed under version control. Input files should be in the YAML
format (files ending in .yaml
or .yml
).
Profile inputs are defined in your profile's main directory within the inspec.yml
. These global inputs
can be used across all the controls in your profile.
Let's add an input to the inspec.yml
file for our profile:
name: my_nginx
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0
supports:
platform: os
inputs:
- name: nginx_version
type: String
value: 1.27.0
Now that we have defined the nginx_version
input, we can access it in any control by using the input
keyword and passing in its name.
For example:
control 'nginx-version' do
impact 1.0
title 'NGINX version'
desc 'The required version of NGINX should be installed.'
describe nginx do
its('version') { should cmp >= input('nginx_version') }
end
end
Since we gave the input a default value of 1.27.0
, that value will be passed down to the control from inspec.yml
at runtime and nothing about the control's returned results will change from before.
We have now swapped out a hard-coded value in the control with a parameter. This is the process of parameterization. It is a common practice during InSpec profile development, where profile authors will write their controls and then read through them to look for values that are hardcoded that could be easily replaced with an input.
Keep in mind that these inputs are constants. Their values should not and do not change during the execution of the program. If you want the input to have a different value, change your inputs file accordingly before your next run of the profile.
Tips
A good rule of thumb is that any check of a numerical value in an InSpec profile should be replaced with an input.
The next control checks whether certain NGINX modules are installed. Let's make an input to represent an array of modules we want to check for.
Example of adding an array object of servers:
- name: servers
type: Array
value:
- server1
- server2
- server3
- name: nginx_modules
type: Array
value:
- http_ssl
- stream_ssl
- mail_ssl
name: my_nginx
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0
supports:
platform: os
inputs:
- name: nginx_version
type: String
value: 1.27.0
- name: nginx_modules
type: Array
value:
- http_ssl
- stream_ssl
- mail_ssl
Your control can now be updated to look like this:
control 'nginx-modules' do
impact 1.0
title 'NGINX modules'
desc 'The required NGINX modules should be installed.'
nginx_modules = input('nginx_modules')
describe nginx do
nginx_modules.each do |current_module|
its('modules') { should include current_module }
end
end
end
Lastly, we can edit our inspec.yml
file to create a list of admin users:
- name: admin_users
type: Array
value:
- admin
name: my_nginx
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0
supports:
platform: os
inputs:
- name: nginx_version
type: String
value: 1.27.0
- name: nginx_modules
type: Array
value:
- http_ssl
- stream_ssl
- mail_ssl
- name: admin_users
type: Array
value:
- admin
Your fifth control can be changed to look like this:
control 'nginx-shell-access' do
impact 1.0
title 'NGINX shell access'
desc 'The NGINX shell access should be restricted to admin users.'
describe users.shells(/bash/).usernames do
it { should be_in input('admin_users')}
end
end
Input File Example
To change your inputs for platform specific cases you can set up multiple input files.
For example, an NGINX web server could be run on a Windows or Linux machine, which may require different admin users for each context. The inputs can be tailored for each system. You can create a inputs-windows.yml
and inputs-linux.yml
file in your home directory and only pass the one you actually need to InSpec at runtime.
Note
Another example is that production and development environments may require different inputs.
admin_users:
- Administrator
- Randy
admin_users:
- root
- randy
name: my_nginx
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0
supports:
platform: os
inputs:
- name: nginx_version
type: String
value: 1.27.0
- name: nginx_modules
type: Array
value:
- http_ssl
- stream_ssl
- mail_ssl
- name: admin_users
type: Array
value:
- admin
The following command runs the tests and applies the inputs specified on the Linux system underlying the nginx application in the container:
inspec exec ./my_nginx -t docker://nginx --input-file inputs-linux.yml
If we'd wanted to do this on a hypothetical Windows system, we would need to create a new inputs file (called inputs-windows.yml
here) and change our target to an nginx instance hosted on a Windows container or virtual machine:
inspec exec ./my_nginx -t docker://windows-nginx --input-file inputs-windows.yml
Best Practice - inputs.yml file
It is best practice to create a separate file for inputs when using the provided profile. The exception to this is when working with an overlay profile, which you will see in Section 10.