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
endAlthough 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.0Now 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
endSince 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_sslname: 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_sslYour 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
endLastly, we can edit our inspec.yml file to create a list of admin users:
  - name: admin_users
    type: Array
    value:
      - adminname: 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:
      - adminYour 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
endInput 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
  - Randyadmin_users:
  - root
  - randyname: 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:
      - adminThe 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.ymlIf 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.ymlBest 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.