5. Create a Custom Resource - The Git Example
Let's practice creating our own custom resource. Suppose we want to write tests that examine the current state of a local Git repository. We will create a git
resource that can handle all of InSpec's interactions with a Git repo under the hood, allowing us to focus on writing clean and easy-to-read code within a control.
Create a New InSpec Profile
Let's start by creating a new profile:
inspec init profile my_git
βββββββββββββββββββββββββββ InSpec Code Generator βββββββββββββββββββββββββββ
Creating new profile at /workspaces/saf-training-lab-environment/my_git
β’ Creating file inspec.yml
β’ Creating directory /workspaces/saf-training-lab-environment/my_git/controls
β’ Creating file controls/example.rb
β’ Creating file README.md
Develop Example Use-case Tests
To write resources, we first need to know what we are testing! In your Codespaces environment, there is a git repository that we will test under the resources
folder. The git repository will be the test target, similarly to how the docker containers acted as test targets in previous sections. Unzip the target git repository using the following command:
tar xzvf resources/git_test_target.tar.gz
This will generate a git_test_target
repository which we will use for these examples.
Now let's write some tests and confirm that they run. You can put these tests in the example.rb
file generated in the controls
folder of your my_git
InSpec profile. These tests are written using the command
resource which is provided by InSpec. We will write a git
resource in this section to improve this test.
Inputs review
The git_dir
input will be used in our profile. It needs to contain the path of the .git
folder you are assessing. Usually, you'd want to put a default location for that input in your inspec.yml
, such as ./.git
, that the user of your profile could overwrite with an inputfile, but for expediency's sake we're going to hardcode it to the .git
folder inside of your git_test_target
repository. The value in the codeblock below should be correct, but it's always good to doublecheck by using the cd
command to navigate inside of the .git
folder in the git_test_target
repository and then using the pwd
command to get the full path.
name: my_git
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: git_dir
type: String
value: /workspaces/saf-training-lab-environment/git_test_target/.git
git_dir = input('git_dir')
# The following branches should exist
describe command("git --git-dir #{git_dir} branch --format='%(refname:short)'").stdout.lines.map(&:strip) do
it { should include 'main' }
end
describe command("git --git-dir #{git_dir} branch --format='%(refname:short)'").stdout.lines.map(&:strip) do
it { should include 'testBranch' }
end
# What is the current branch
describe command("git --git-dir #{git_dir} branch --show-current").stdout.strip do
it { should eq 'main' }
end
# What is the latest commit
describe command("git --git-dir #{git_dir} log -1 --pretty=format:'%h'") do
its('stdout') { should match /7a748c6/ }
end
# What is the second to last commit
describe command("git --git-dir #{git_dir} log --skip=1 -1 --pretty=format:'%h'") do
its('stdout') { should match /edc207f/ }
end
inspec exec my_git
Redirecting to cinc-auditor...
Profile: InSpec Profile (my_git)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
["main", "testBranch"]
β is expected to include "main"
["main", "testBranch"]
β is expected to include "testBranch"
main
β is expected to eq "main"
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log -1 --pretty=format:'%h'`
β stdout is expected to match /7a748c6/
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log --skip=1 -1 --pretty=format:'%h'`
β stdout is expected to match /edc207f/
Test Summary: 5 successful, 0 failures, 0 skipped
Our tests pass, but they all use the command
resource. It's not best practice to do this -- for one thing, it makes our tests more complicated, and the output too long.
But What If I Don't Care About The Tests Being Complicated And The Output Being Too Long?
Some test writers like to wrap their favorite bash commands in a command
block and call it a day.
However, best practice is to write clean and maintainable InSpec code even if you yourself have no trouble using the command
resource to do everything.
Recall that other developers and assessors need to be able to understand how your tests function. Nobody likes trying to debug someone else's profile that assumes that the operator knows exactly how the profile writer's favorite terminal commands work.
Let's rewrite these tests in a way that abstracts away the complexity of working with the git
command into a resource.
Rewrite a Test
Let's rewrite the first test in our example file to make it more readable by inventing a git
resource that can simplify our test as follows:
# The following branches should exist
describe git(git_dir) do
its('branches') { should include 'main' }
end
Now let's run the profile.
inspec exec my_git
Redirecting to cinc-auditor...
[2023-02-22T03:21:41+00:00] ERROR: Failed to load profile my_git: Failed to load source for controls/example.rb: undefined method `git' for #<Inspec::ControlEvalContext:0x000000000540af38>
Profile: InSpec Profile (my_git)
Version: 0.1.0
Failure Message: Failed to load source for controls/example.rb: undefined method `git' for #<Inspec::ControlEvalContext:0x000000000540af38>
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
No tests executed.
Test Summary: 0 successful, 0 failures, 0 skipped
We should get an error because the git method and resource are not defined yet. We should fix that.
Develop the git resource
Let's start by creating a new file called git.rb
in the libraries
directory. If you do not already have a libraries
directory, you can make one in the my_git
InSpec profile directory. The content of the file should look like this:
class Git < Inspec.resource(1)
name 'git'
end
This is - technically - a valid resource! It was very easy to write, but it is not particularly useful for testing. Let's run our tests again to see why not.
Setting Up a Resource Using InSpec Init
Instead of just creating the git.rb
file in the libraries
directory, you can use InSpec to assist you in creating a resource. Run inspec init resource <your-resource-name>
and follow the prompts to create the foundation and see examples for a resource.
Run the profile again.
inspec exec my_git
[2023-02-22T03:25:57+00:00] ERROR: Failed to load profile my_git: Failed to load source for controls/example.rb: wrong number of arguments (given 1, expected 0)
Profile: InSpec Profile (my_git)
Version: 0.1.0
Failure Message: Failed to load source for controls/example.rb: wrong number of arguments (given 1, expected 0)
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
No tests executed.
Test Summary: 0 successful, 0 failures, 0 skipped
This time we get another error letting us know that we have a resource that has been given the incorrect number of arguments. This means we have given an additional parameter to this resource that we have not yet accepted.
Each resource will require an initialization method.
For our git.rb file let's add that initialization method:
class Git < Inspec.resource(1)
name 'git'
def initialize(path)
@path = path
end
end
This is saving the path we are passing in from the control into an instance method called path
.
Now when we run the profile:
inspec exec my_git
Profile: InSpec Profile (my_git)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
git
Γ branches
undefined method `branches' for #<#<Class:0x00000000041485a8>:0x00000000043620c8>
["main", "testBranch"]
β is expected to include "testBranch"
main
β is expected to eq "main"
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log -1 --pretty=format:'%h'`
β stdout is expected to match /7a748c6/
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log --skip=1 -1 --pretty=format:'%h'`
β stdout is expected to match /edc207f/
Test Summary: 4 successful, 1 failure, 0 skipped
The test will run but we will get an error saying we do not have a branches
method. Remember that the other 4 tests are still passing because they are not yet using the git
resource, but are still relying on InSpec's command
resource.
Let's go back to our git.rb file to fix that by adding a branches
method:
class Git < Inspec.resource(1)
name 'git'
def initialize(path)
@path = path
end
def branches
end
end
We have now defined the branches method. Let's see what the test output shows us.
inspec exec my_git
Profile: InSpec Profile (my_git)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
git
Γ branches is expected to include "main"
expected nil to include "main", but it does not respond to `include?`
["main", "testBranch"]
β is expected to include "testBranch"
main
β is expected to eq "main"
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log -1 --pretty=format:'%h'`
β stdout is expected to match /7a748c6/
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log --skip=1 -1 --pretty=format:'%h'`
β stdout is expected to match /edc207f/
Test Summary: 4 successful, 1 failure, 0 skipped
Now the error message says that the branches
method is returning a null value which is a problem because it actually needs to return something, like an array, that implements the predicate method include?
. A predicate method is one that evaluates a condition, in this case whether a given branch is "included" in the set of branches, and returns true or false accordingly.
To resolve this problem, we can use the inspec
helper method to invoke the built-in command
resource to extract this data as shown below:
class Git < Inspec.resource(1)
name 'git'
def initialize(path)
@path = path
end
def branches
inspec.command("git --git-dir #{@path} branch --format='%(refname:short)'").stdout.lines.map(&:strip)
end
end
You might notice some similarities between this code and what we originally started with in our example.rb
file - this is intentional! Resources are used to encapsulate complicated behaviors, such as the mechanics of dealing with niche git
subcommands, and expose clean interfaces for use by control authors.
Now we see that we get a passing test!
Now let's adjust our test to also check for our second branch that we created earlier as well as check our current branch:
# The following branches should exist
describe git(git_dir) do
its('branches') { should include 'main' }
its('branches') { should include 'testBranch' }
its('current_branch') { should eq 'main' }
end
inspec exec my_git
Profile: InSpec Profile (my_git)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
git
β branches is expected to include "main"
β branches is expected to include "testBranch"
Γ current_branch
undefined method `current_branch' for #<#<Class:0x0000000005400588>:0x00000000053fd0b8>
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log -1 --pretty=format:'%h'`
β stdout is expected to match /7a748c6/
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log --skip=1 -1 --pretty=format:'%h'`
β stdout is expected to match /edc207f/
Test Summary: 6 successful, 1 failure, 0 skipped
Let's head over to the git.rb file to create the current_branch
method we are invoking in the above test:
class Git < Inspec.resource(1)
name 'git'
def initialize(path)
@path = path
end
def branches
inspec.command("git --git-dir #{@path} branch --format='%(refname:short)'").stdout.lines.map(&:strip)
end
def current_branch
inspec.command("git --git-dir #{@path} branch --show-current").stdout.strip
end
end
1337 h4x0rs
While InSpec controls are intended on being easy to read by both software/cybersecurity engineers AND assessors, InSpec resources are an opportunity to flex those programming muscles and use that arcane sysadmin knowledge to do things in the most optimal way.
Here's an alternative implementation for current_branch
:
def current_branch
branch_name = inspec.command("git --git-dir #{@path} branch").stdout.gsub(" ", "").split("\n").find do |name|
name.start_with?('*')
end
branch_name[1..-1]
end
Note how this iterates over the baseline git branch
output which looks like this:
$ git branch
* main
testBranch
And then we do some fancy stuff with string manipulation while searching for the magic string that starts with an asterisk (which marks the current branch). We're able to leverage the full power of Ruby here!
However, if you're a command line expert (or read the docs!), you'd know that you could just pass a single additional flag to the git branch
function that spits out the exact answer that we want - don't get carried away when a simple solution exists!
Now we can run the profile again.
inspec exec my_git
Profile: InSpec Profile (my_git)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
git
β branches is expected to include "main"
β branches is expected to include "testBranch"
β current_branch is expected to eq "main"
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log -1 --pretty=format:'%h'`
β stdout is expected to match /7a748c6/
Command: `git --git-dir /workspaces/saf-training-lab-environment/git_test_target/.git log --skip=1 -1 --pretty=format:'%h'`
β stdout is expected to match /edc207f/
Test Summary: 7 successful, 0 failures, 0 skipped
All the tests should pass!
Exercise!
As a solo exercise, try to create a method in the git.rb file to check what the last commit is.
This is Test-Driven Development!
Did you notice the overall arc of how we wrote this resource? We started with a set of tests before we even wrote any resource code, so we knew we would start out with a failing profile.
However, that failing profile helped us define how we should build our resource. Since we knew what sort of tests we wanted to be able to run, we knew what functions we needed to write to support them in the git
resource. Test-driven development is an excellent method of defining requirements for your code before you even start writing it!
Run the InSpec shell with a custom resource
Invoking the InSpec shell with inspec shell
will give you access to all the core InSpec resources by default, but InSpec does not automatically know about your locally defined resources unless you point them out. If you're testing a local resource, use the --depends
flag and pass in the profile directory that your resource lives in.
inspec shell --depends my_git
Welcome to the interactive InSpec Shell
To find out how to use it, type: help
You are currently running on:
Name: ubuntu
Families: debian, linux, unix, os
Release: 20.04
Arch: x86_64
inspec> git('/workspaces/saf-training-lab-environment/git_test_target/.git').current_branch
=> "main"
Warning
Note that we are passing in the profile directory to the --depends
flag, and not the profile's libraries
directory. In our example, it's inspec shell --depends my_git
and not inspec shell --depends my_git/libraries
.
If you edit the resource class file, you'll need to exit the shell and re-launch it for the updates to be available.
From here, we can examine our custom resource in a sandbox in the same way that we do with core resources.