mirror of
https://github.com/inspec/inspec
synced 2024-12-18 00:53:22 +00:00
5c1467dfe9
Signed-off-by: IanMadd <maddaus@protonmail.com>
240 lines
6.4 KiB
Markdown
240 lines
6.4 KiB
Markdown
# Chef InSpec profile style guide
|
|
|
|
This is a set of recommended Chef InSpec rules you should use when writing controls.
|
|
|
|
# Control Files
|
|
|
|
## Place control files in `controls/` and end them with `.rb`
|
|
|
|
Most syntax highlighters will render Chef InSpec files correctly across a wide list
|
|
of tools.
|
|
|
|
Avoid:
|
|
- `controls/ssh_config`
|
|
- `controls/ssh/config.rb`
|
|
|
|
Use:
|
|
- `controls/ssh_config.rb`
|
|
- `controls/ssh_config.rb`
|
|
|
|
## Avoid `controls`/`control` in your control filenames
|
|
|
|
Using `controls` in the filename creates unnecessary clutter when reading it.
|
|
Keep the names short and concise.
|
|
|
|
Avoid:
|
|
- `controls/ssh_controls.rb`
|
|
|
|
Use:
|
|
- `controls/ssh.rb`
|
|
|
|
# Code Style
|
|
|
|
## Avoid unnecessary parentheses in matchers
|
|
|
|
Adding additional parentheses is not required and provides more readability if
|
|
it is not used:
|
|
|
|
Avoid:
|
|
- `it { should eq(value) }`
|
|
|
|
Use:
|
|
- `it { should eq value }`
|
|
|
|
The exception are matchers that require additional arguments or named arguments.
|
|
|
|
# Controls
|
|
|
|
## Avoid wrapping controls in conditional statements
|
|
|
|
This will create dynamic profiles whose controls depend on the execution. The
|
|
problem here is that we cannot render the profile or provide its information
|
|
before scanning a system. We want to be able to inform users of the contents of
|
|
their profiles before they run them. It is valid to skip controls that are not
|
|
necessary for a system, as long as you do it via `only_if` conditions. Ruby's
|
|
internal conditionals will hide parts of the profile to static analysis and
|
|
should thus be avoided.
|
|
|
|
Avoid:
|
|
|
|
```ruby
|
|
if package('..').installed?
|
|
control "package-test1" do
|
|
..
|
|
end
|
|
end
|
|
```
|
|
|
|
Use:
|
|
|
|
```ruby
|
|
control "package-test1" do
|
|
only_if { package('..').installed? }
|
|
end
|
|
```
|
|
|
|
Avoid:
|
|
|
|
```ruby
|
|
case inspec.platform.name
|
|
when /centos/
|
|
include_controls 'centos-profile'
|
|
...
|
|
```
|
|
|
|
Instead use the `supports` attribute in the `inspec.yml` of the profile you
|
|
want to include:
|
|
|
|
```ruby
|
|
supports:
|
|
- platform-name: centos
|
|
```
|
|
|
|
Now whenever you run the base profile you can just
|
|
`include_controls 'centos-profile'`. It will only run the included profiles is
|
|
the platform matches the supported platform.
|
|
|
|
## Avoid dynamic elements in the control IDs
|
|
|
|
Control IDs are used to map test results to the tests and profiles. Dynamic
|
|
control IDs make it impossible to map results back, since the identifier which
|
|
connects tests and results may change in the process.
|
|
|
|
Avoid:
|
|
|
|
```ruby
|
|
control "test-file-#{name}" do
|
|
..
|
|
end
|
|
```
|
|
|
|
Use:
|
|
|
|
```ruby
|
|
control "test-all-files" do
|
|
..
|
|
end
|
|
```
|
|
|
|
Sometimes you may create controls from a static list of elements. If this list
|
|
stays the same no matter what system is scanned, it may be ok to do so and use
|
|
it as a generator for static controls.
|
|
|
|
## Avoid Ruby system calls
|
|
|
|
Ruby code is executed on the system that runs InSpec. This allows Chef InSpec to work
|
|
without Ruby and RubyGems being required on remote targets (servers or
|
|
containers). System calls are often used to interact with the local OS or remote
|
|
endpoints from a local installation.
|
|
|
|
Chef InSpec tests, however, are designed to be universally executable on all types
|
|
of runtimes, including local and remote execution. We want to give users the
|
|
ability to take an OS profile and execute it remotely or locally.
|
|
|
|
## Avoid shelling out
|
|
|
|
Avoid:
|
|
- `` `ls``\`
|
|
- `system("ls")`
|
|
- `IO.popen("ls")`
|
|
|
|
Use:
|
|
|
|
- `command("ls")` or `powershell("Get-ChildItem")`
|
|
|
|
Ruby's command executors will only run locally. Imagine a test like this:
|
|
|
|
```ruby
|
|
describe `whoami` do
|
|
it { should cmp "bob\n" }
|
|
end
|
|
```
|
|
|
|
If you run this test on your local system and happen to be using Bob's account
|
|
it will succeed. But if you were to run it against
|
|
`--target alice@remote-host.com` it will still report that the user is bob
|
|
instead of alice.
|
|
|
|
Instead, do this:
|
|
|
|
```ruby
|
|
describe command('whoami') do
|
|
its('stdout') { should cmp "bob\n" }
|
|
end
|
|
```
|
|
|
|
If the profile is pointed to a remote endpoint using the `command` resource
|
|
will run it on the remote OS.
|
|
|
|
## Avoid Ruby IO on files
|
|
|
|
Similar to the command interactions these files will only be read locally with
|
|
Ruby's internal calls. If you run this test against a remote target it won't
|
|
read the file from the remote endpoint, but from the local OS instead. Use the
|
|
`file` resource to read files on the target system.
|
|
|
|
Avoid:
|
|
- `File.new("filename").read`
|
|
- `File.read("filename")`
|
|
- `IO.read("filename")`
|
|
|
|
Use:
|
|
- `file("filename")`
|
|
|
|
In general, try to avoid Ruby's IO calls from within Chef InSpec controls and use
|
|
Chef InSpec resources instead.
|
|
|
|
## Avoid Ruby gem dependencies in controls
|
|
|
|
In addition to avoiding system-level gems and modules you should also limit the
|
|
use of external dependencies to resource packs or plugins. Gems need to be
|
|
resolved, installed, vendored, and protected from conflicts. We aim to avoid
|
|
exposing this complexity to users of InSpec, to make it a great tool even if you
|
|
are not a developer.
|
|
|
|
Plugins should declare gem dependencies in their gemspec, and then rely on the
|
|
plugin installation facility to install and manage dependencies.
|
|
|
|
## Avoid debugging calls (in production)
|
|
|
|
One of the best way to develop and explore tests is the interactive debugging
|
|
shell `pry` (see [Interactive Debugging with Pry] (https://www.inspec.io/docs/reference/dsl_inspec/#interactive-debugging-with-pry)
|
|
at the end of this page). However, after you finish your profile make sure you
|
|
have no interactive statements included anymore. Sometimes interactive calls are
|
|
hidden behind conditionals (`if` statements) that are harder to reach. These
|
|
calls can easily cause trouble when an automated profiles runs into an
|
|
interactive `pry` call that stops the execution and waits for user input.
|
|
|
|
Avoid:
|
|
- `binding.pry` in production profiles
|
|
|
|
Use:
|
|
- Use debugging calls during development only
|
|
|
|
Also you may find it helpful to use the Chef InSpec logging interface:
|
|
|
|
```ruby
|
|
Inspec::Log.info('Hi')
|
|
```
|
|
|
|
### 9. Favor `cmp` over `eq`
|
|
|
|
Reason: The `cmp` matcher handles type conversions, case insensitive comparisons, converting strings to versions (e.g. '7.35.0-1ubuntu2.10'), and many other troublesome things. Unless you are wanting an exact match (if so use the `eq` matcher) then the `cmp` matcher should be used.
|
|
|
|
For example, this:
|
|
|
|
```ruby
|
|
describe passwd.uids(0) do
|
|
its('users') { should cmp 'root' }
|
|
end
|
|
```
|
|
|
|
is preferred over:
|
|
|
|
```ruby
|
|
describe passwd.uids(0) do
|
|
its('users') { should eq ['root'] }
|
|
end
|
|
```
|
|
|
|
See the [`cmp` matcher documentation](https://www.inspec.io/docs/reference/matchers/#cmp) for more examples.
|