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
control 'nginx-interview' do
impact 1.0
title 'NGINX interview'
desc 'NGINX admins should have documentation on security procedures.'
describe "Manual Review" do
skip "This control must be manually reviewed."
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, we will override them using an inputs file.
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
Overriding Inputs with
We have now defined and parameterized a set of default values for our controls. But what if we want to change those defaults at some point? We can pass in our own values for inputs when we run inspec exec
to ensure that each individual run of InSpec uses the values we want.
Create a file inside your my_nginx
profile:
admin_users:
- admin
- root
Note that we are invoking an input name that we defined back in the inspec.yml
file (admin_users
). We are now overriding the default value by adding the root
account to our list of valid account names.
Remember how our test for requirement 5 was failing because we found an account named root
when we were expecting the name admin
? We can now tell InSpec that we were intending to look for an account named root
, and very importantly, we can do it without having to edit either our control code itself or the inspec.yml
file.
When using InSpec in the real world, we want a strong separation between our underlying test code (the profile itself) and the environment-specific parameters we pass to it (the inputs). Therefore, we never edit the inspec.yml
file to simply erase the default values and put in our own; we will instead use InSpec's ability to override input values via an inputs file.
Let's re-run the profile and pass it our custom inputs.
inspec exec my_nginx -t docker://nginx --input-file
--input-file
flagRedirecting to cinc-auditor...
Profile: InSpec Profile (my_nginx)
Version: 0.1.0
Target: docker://DOCKER_CONTAINER_ID
Target ID: TARGET_ID
β nginx-version: NGINX version
β Nginx Environment version is expected to cmp >= "1.27.0"
β nginx-modules: NGINX modules
β Nginx Environment modules is expected to include "http_ssl"
β Nginx Environment modules is expected to include "stream_ssl"
β Nginx Environment modules is expected to include "mail_ssl"
β nginx-conf-file: NGINX configuration file
β File /etc/nginx/nginx.conf is expected to be file
Γ nginx-conf-perms: NGINX configuration permissions (1 failed)
β File /etc/nginx/nginx.conf is expected to be owned by "root"
β File /etc/nginx/nginx.conf is expected to be grouped into "root"
Γ File /etc/nginx/nginx.conf is expected not to be readable by others
expected File /etc/nginx/nginx.conf not to be readable by others
β File /etc/nginx/nginx.conf is expected not to be writable by others
β File /etc/nginx/nginx.conf is expected not to be executable by others
β nginx-shell-access: NGINX shell access
β ["root"] is expected to be in "admin" and "root"
βΊ nginx-interview: NGINX interview
βΊ This control must be manually reviewed.
Profile Summary: 3 successful controls, 2 control failures, 1 control skipped
Test Summary: 9 successful, 2 failures, 1 skipped
And there we are, requirement 5 is now correctly indicating that the test is a pass.
Notice that all the other tests besides number 5 did not change -- we didn't specifically override the inputs for those ones in the inputs file, so InSpec did not change the default values.
Passing Inputs Via the --input
Flags
There are many ways to pass an input to InSpec at runtime. InSpec prioritizes input values differently depending on where the value comes from.
For example, we can pass an input directly on the terminal line using a different flag:
inspec exec my_nginx -t docker://nginx --input=admin_users=['root']
--input
flagRedirecting to cinc-auditor...
Profile: InSpec Profile (my_nginx)
Version: 0.1.0
Target: docker://DOCKER_CONTAINER_ID
Target ID: TARGET_ID
β nginx-version: NGINX version
β Nginx Environment version is expected to cmp >= "1.27.0"
β nginx-modules: NGINX modules
β Nginx Environment modules is expected to include "http_ssl"
β Nginx Environment modules is expected to include "stream_ssl"
β Nginx Environment modules is expected to include "mail_ssl"
β nginx-conf-file: NGINX configuration file
β File /etc/nginx/nginx.conf is expected to be file
Γ nginx-conf-perms: NGINX configuration permissions (1 failed)
β File /etc/nginx/nginx.conf is expected to be owned by "root"
β File /etc/nginx/nginx.conf is expected to be grouped into "root"
Γ File /etc/nginx/nginx.conf is expected not to be readable by others
expected File /etc/nginx/nginx.conf not to be readable by others
β File /etc/nginx/nginx.conf is expected not to be writable by others
β File /etc/nginx/nginx.conf is expected not to be executable by others
β nginx-shell-access: NGINX shell access
β ["root"] is expected to be in "root"
βΊ nginx-interview: NGINX interview
βΊ This control must be manually reviewed.
Profile Summary: 3 successful controls, 2 control failures, 1 control skipped
Test Summary: 9 successful, 2 failures, 1 skipped
The test still passes.
Keeping Multiple Input Files
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