Merge pull request #2655 from chef/release-2.0

Add in release-2.0 changes to master
This commit is contained in:
Jared Quick 2018-02-15 16:57:25 -05:00 committed by GitHub
commit fc990346f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
210 changed files with 14055 additions and 942 deletions

5
.gitignore vendored
View file

@ -28,3 +28,8 @@ www/Gemfile.lock
meta-profile-0.2.0.tar.gz
profile-1.0.0.tar.gz
.attribute.yml
.terraform/
terraform.tfstate*
terraform.tfstate.backup

View file

@ -2,6 +2,7 @@ sudo: required
branches:
only:
- master
- release-2.0
language: ruby
cache: bundler
dist: trusty
@ -17,6 +18,7 @@ matrix:
include:
- rvm: 2.3.6
- rvm: 2.4.3
- rvm: 2.5.0
- rvm: 2.4.3
script: bundle exec rake $SUITE
env: SUITE="test:functional"
@ -65,6 +67,7 @@ matrix:
script: ./support/ci/deploy_website_to_acceptance.sh
allow_failures:
- rvm: 2.5.0
- env:
- AFFECTED_DIRS="www"
- secure: "jdzXUhP1o7RkfSikZLKgUcCIaKqLjqWa35dnxWnz7qAQ2draRKa7I7cXmUv76BZkW8HBUUH11dOi8YOVxPYPOzaqvcTCfqNqGVxsT9epgWa7rA8aXMXkECp548ry1rYJQpti9zpwsoe2GQyNPr9vNiWMiyj51CaABmZ6JzmFEEqlZc8vqpqWeqJvIqaibQGk7ByLKmi4R44fVwFKIG39RuxV+alc/G4nnQ2zmNTFuy8uFGs5EghQvRytzWY+s2AKtDiZ0YXYOII1Nl1unXNnNoQt9oI209ztlSm1+XOuTPelW6bEIx5i7OZFaSRPgJzWnkGN85C9nBE08L2az9Jz18/rYJF4fdVRttdGskueyYI21lh1FwlAg51ZG0RfLTYk2Pq+k4c+NO1cfmGcaXBwihfD5BWqrILU5HHkYszXCSmgl4hscC7/BS4Kgcq2z32JJwV8B+x4XngM0G4uzIn1Soia3lZXEKdnfVsxFDdMQ7FK60F3uQlq/44LRkZujRhqfAKOiz+0tsLexWzj7wK+DJY9Y00CUfh7xcxRxDxFNpOv1FWYFB9lUlaOt3HDHgUoksqbURiUzhOZZzTE/1MAtF2K6mbpME5CbN08J88L5JBlb+CX79XCzj30lNMeS0I/dCRQEmkygr2eJYxvRO2qsBNuphs4SWk8NZyS/llVZFI="

View file

@ -8,6 +8,7 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.2')
end
gem 'ffi', '>= 1.9.14'
gem 'aws-sdk', '~> 2'
group :test do
gem 'bundler', '~> 1.5'
@ -15,11 +16,12 @@ group :test do
gem 'rake', '>= 10'
gem 'rubocop', '= 0.49.1'
gem 'simplecov', '~> 0.10'
gem 'concurrent-ruby', '~> 0.9'
gem 'concurrent-ruby', '~> 1.0'
gem 'mocha', '~> 1.1'
gem 'ruby-progressbar', '~> 1.8'
gem 'webmock', '~> 2.3.2'
gem 'jsonschema', '~> 2.0.2'
gem 'passgen'
gem 'm'
end

View file

@ -242,6 +242,18 @@ inspec exec test.rb --sudo [--sudo-password ...] [--sudo-options ...] [--sudo_co
# run in a subshell
inspec exec test.rb --shell [--shell-options ...] [--shell-command ...]
# run a profile targeting AWS using env vars
inspec exec test.rb -t aws://
# or store your AWS credentials in your ~/.aws/credentials profiles file
inspec exec test.rb -t aws://us-east-2/my-profile
# run a profile targeting Azure using env vars
inspec exec test.rb -t azure://
# or store your Azure credentials in your ~/.azure/credentials profiles file
inspec exec test.rb -t azure://subscription_id
```
### detect
@ -322,6 +334,7 @@ You may also [browse the Supermarket for shared Compliance Profiles](https://sup
InSpec is inspired by the wonderful [Serverspec](http://serverspec.org) project. Kudos to [mizzy](https://github.com/mizzy) and [all contributors](https://github.com/mizzy/serverspec/graphs/contributors)!
The AWS resources were inspired by [inspec-aws](https://github.com/arothian/inspec-aws) from [arothian](https://github.com/arothian).
## Contribute
@ -339,10 +352,11 @@ The InSpec community and maintainers are very active and helpful. This project b
## Testing InSpec
We perform `unit` and `integration` tests.
We offer `unit`, `integration`, and `aws` tests.
- `unit` tests ensure the intended behaviour of the implementation
- `integration` tests run against Docker-based VMs via test-kitchen and [kitchen-inspec](https://github.com/chef/kitchen-inspec)
- `aws` tests exercise the AWS resources against real AWS accounts
### Unit tests
@ -360,7 +374,6 @@ You may also run a single test within a file by line number:
```bash
bundle exec m test/unit/resources/user_test.rb -l 123
```
### Integration tests
@ -396,14 +409,20 @@ You may test all instances in parallel with:
bundle exec kitchen test -c
```
### AWS Tests
Use the rake task `bundle exec rake test:aws` to test the AWS resources against a pair of real AWS accounts.
Please see TESTING_AGAINST_AWS.md for details on how to setup the needed AWS accounts to perform testing.
## License
| | |
| ------ | --- |
| **Author:** | Dominik Richter (<drichter@chef.io>) |
| **Author:** | Christoph Hartmann (<chartmann@chef.io>) |
| **Copyright:** | Copyright (c) 2015 Chef Software Inc. |
| **Copyright:** | Copyright (c) 2015 Vulcano Security GmbH. |
| **Copyright:** | Copyright (c) 2017 Chef Software Inc. |
| **License:** | Apache License, Version 2.0 |
Licensed under the Apache License, Version 2.0 (the "License");

117
Rakefile
View file

@ -4,9 +4,16 @@
require 'bundler'
require 'bundler/gem_tasks'
require 'rake/testtask'
require 'passgen'
require 'train'
require_relative 'tasks/maintainers'
require_relative 'tasks/spdx'
def prompt(message)
print(message)
STDIN.gets.chomp
end
# The docs tasks rely on ruby-progressbar. If we can't load it, then don't
# load the docs tasks. This is necessary to allow this Rakefile to work
# when the "tests" gem group in the Gemfile has been excluded, such as
@ -84,6 +91,116 @@ namespace :test do
sh('sh', '-c', sh_cmd)
end
project_dir = File.dirname(__FILE__)
namespace :aws do
['default', 'minimal'].each do |account|
integration_dir = File.join(project_dir, 'test', 'aws', account)
attribute_file = File.join(integration_dir, '.attribute.yml')
task :"setup:#{account}", :tf_workspace do |t, args|
tf_workspace = args[:tf_workspace] || ENV['INSPEC_TERRAFORM_ENV']
abort("You must either call the top-level test:aws:#{account} task, or set the INSPEC_TERRAFORM_ENV variable.") unless tf_workspace
puts "----> Setup"
abort("You must set the environment variable AWS_REGION") unless ENV['AWS_REGION']
puts "----> Checking for required AWS profile..."
sh("aws configure get aws_access_key_id --profile inspec-aws-test-#{account} > /dev/null")
sh("cd #{integration_dir}/build/ && terraform init")
sh("cd #{integration_dir}/build/ && terraform workspace new #{tf_workspace}")
sh("cd #{integration_dir}/build/ && AWS_PROFILE=inspec-aws-test-#{account} terraform plan")
sh("cd #{integration_dir}/build/ && AWS_PROFILE=inspec-aws-test-#{account} terraform apply")
Rake::Task["test:aws:dump_attrs:#{account}"].execute
end
task :"dump_attrs:#{account}" do
sh("cd #{integration_dir}/build/ && AWS_PROFILE=inspec-aws-test-#{account} terraform output > #{attribute_file}")
raw_output = File.read(attribute_file)
yaml_output = raw_output.gsub(" = ", " : ")
File.open(attribute_file, "w") {|file| file.puts yaml_output}
end
task :"run:#{account}" do
puts "----> Run"
sh("bundle exec inspec exec #{integration_dir}/verify -t aws://${AWS_REGION}/inspec-aws-test-#{account} --attrs #{attribute_file}")
end
task :"cleanup:#{account}", :tf_workspace do |t, args|
tf_workspace = args[:tf_workspace] || ENV['INSPEC_TERRAFORM_ENV']
abort("You must either call the top-level test:aws:#{account} task, or set the INSPEC_TERRAFORM_ENV variable.") unless tf_workspace
puts "----> Cleanup"
sh("cd #{integration_dir}/build/ && AWS_PROFILE=inspec-aws-test-#{account} terraform destroy -force")
sh("cd #{integration_dir}/build/ && terraform workspace select default")
sh("cd #{integration_dir}/build && terraform workspace delete #{tf_workspace}")
end
task :"#{account}" do
tf_workspace = ENV['INSPEC_TERRAFORM_ENV'] || prompt("Please enter a workspace for your integration tests to run in: ")
begin
Rake::Task["test:aws:setup:#{account}"].execute({:tf_workspace => tf_workspace})
Rake::Task["test:aws:run:#{account}"].execute
rescue
abort("Integration testing has failed for the #{account} account")
ensure
Rake::Task["test:aws:cleanup:#{account}"].execute({:tf_workspace => tf_workspace})
end
end
end
end
desc "Perform AWS Integration Tests"
task aws: [:'aws:default', :'aws:minimal']
namespace :azure do
# Specify the directory for the integration tests
integration_dir = 'test/azure'
task :init_workspace do
# Initialize terraform workspace
sh("cd #{integration_dir}/build/ && terraform init")
end
task :setup_integration_tests do
puts '----> Setup'
creds = Train.create('azure').connection.connect
# Determine the storage account name and the admin password
sa_name = (0...15).map { (65 + rand(26)).chr }.join.downcase
admin_password = Passgen::generate(length: 12, uppercase: true, lowercase: true, symbols: true, digits: true)
# Use the first 4 characters of the storage account to create a suffix
suffix = sa_name[0..3]
# Create the plan that can be applied to Azure
cmd = format("cd %s/build/ && terraform plan -var 'subscription_id=%s' -var 'client_id=%s' -var 'client_secret=%s' -var 'tenant_id=%s' -var 'storage_account_name=%s' -var 'admin_password=%s' -var 'suffix=%s' -out inspec-azure.plan", integration_dir, creds[:subscription_id], creds[:client_id], creds[:client_secret], creds[:tenant_id], sa_name, admin_password, suffix)
sh(cmd)
# Apply the plan on Azure
cmd = format("cd %s/build/ && terraform apply inspec-azure.plan", integration_dir)
sh(cmd)
end
task :run_integration_tests do
puts '----> Run'
sh("bundle exec inspec exec #{integration_dir}/verify -t azure://1e0b427a-d58b-494e-ae4f-ee558463ebbf")
end
task :cleanup_integration_tests do
puts '----> Cleanup'
creds = Train.create('azure').connection.connect
cmd = format("cd %s/build/ && terraform destroy -force -var 'subscription_id=%s' -var 'client_id=%s' -var 'client_secret=%s' -var 'tenant_id=%s' -var 'admin_password=dummy' -var 'storage_account_name=dummy' -var 'suffix=dummy'", integration_dir, creds[:subscription_id], creds[:client_id], creds[:client_secret], creds[:tenant_id])
sh(cmd)
end
end
desc "Perform Azure Integration Tests"
task :azure do
Rake::Task['test:azure:init_workspace'].execute
Rake::Task['test:azure:cleanup_integration_tests'].execute
Rake::Task['test:azure:setup_integration_tests'].execute
Rake::Task['test:azure:run_integration_tests'].execute
Rake::Task['test:azure:cleanup_integration_tests'].execute
end
end
# Print the current version of this gem or update it.

108
TESTING_AGAINST_AWS.md Normal file
View file

@ -0,0 +1,108 @@
# Testing Against AWS - Integration Testing
## Problem Statement
We want to be able to test AWS-related InSpec resources against AWS itself. This means we need to create constructs ("test fixtures") in AWS to examine using InSpec. For cost management, we also want to be able to destroy
## General Approach
We use Terraform to setup test fixtures in AWS, then run a defined set of InSpec controls against these (which should all pass), and finally tear down the test fixtures with Terraform. For fixtures that cannot be managed by Terraform, we manually setup fixtures using instructions below.
We use the AWS CLI credentials system to manage credentials.
### Installing Terraform
Download [Terraform](https://www.terraform.io/downloads.html). We require at least v0.10 . To install and choose from multiple Terraform versions, consider using [tfenv](https://github.com/kamatama41/tfenv).
### Installing AWS CLI
Install the [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html). We will store profiles for testing in the `~/.aws/credentials` file.
## Limitations
There are some things that we can't (or very much shouldn't) do via Terraform - like manipulating the root account MFA settings.
Also, there are some singleton resources (such as the default VPC, or Config status) that we should not manipulate without consequences.
## Current Solution
Our solution is to create two AWS accounts, each dedicated to the task of integration testing inspec-aws.
In the "default" account, we setup all fixtures that can be handled by Terraform. For any remaining fixtures,
such as enabling MFA on the root account, we manually set one value in the "default" account, and manually set the opposing value in the "minimal" account. This allows use to perform testing on any reachable resource or property, regardless of whether or not Terraform can manage it.
All tests (and test fixtures) that do not require special handling are placed in the "default" set. That includes both positive and negative checks.
Note that some tests will fail for the first day or two after you set up the accounts, due to the tests checking properties such as the last usage time of an access key, for example.
Additionally, the first time you run the tests, you will need to accept the user agreement in the AWS marketplace for the linux AMIs we use. You'll need to do it 4 times, once for each of debian and centos on the two accounts.
### Creating the Default account
Follow these instructions carefully. Do not perform any action not specified.
1. Create an AWS account. Make a note of the account email and root password in a secure secret storage system.
2. Create an IAM user named `test-fixture-maker`.
* Enable programmatic access (to generate an access key)
* Direct-attach the policy AdministratorAccess
* Note the access key and secret key ID that are generated.
3. Using the aws command line tool, store the access key and secret key in a profile with a special name:
`aws configure --profile inspec-aws-test-default`
#### Test Fixtures for the Default Account
1. As the root user, enable a virtual MFA device.
2. Create an IAM user named 'test-user-last-key-use'.
* Enable programmatic access (to generate an access key)
* Note the access key and secret key ID that are generated.
* Direct-attach the policy AmazonEC2ReadOnlyAccess
* Using the AWS CLI and the credentials, execute the command `aws ec2 describe-instances`.
* The goal here is to have an access key that was used at one point.
### Creating the Minimal Account
Follow these instructions carefully. Do not perform any action not specified.
1. Create an AWS account. Make a note of the account email and root password in a secure secret storage system.
2. Create an IAM user named `test-fixture-maker`.
* Enable programmatic access (to generate an access key)
* Direct-attach the policy AdministratorAccess
* Note the access key and secret key ID that are generated.
3. Using the aws command line tool, store the access key and secret key in a profile with a special name:
`aws configure --profile inspec-aws-test-minimal`
#### Test Fixtures for the Minimal Account
1. Create an Access Key for the root user. You do not have to save the access key.
## Running the integration tests
To run all AWS integration tests, run:
```
bundle exec rake test:aws
```
To run the tests against one account only:
```
bundle exec rake test:aws:default
```
or
```
bundle exec rake test:aws:minimal
```
Each account has separate tasks for setup, running the tests, and cleanup. You may run them separately:
```
bundle exec rake test:aws:setup:default
bundle exec rake test:aws:run:default
bundle exec rake test:aws:cleanup:default
```

View file

@ -6,13 +6,18 @@ environment:
matrix:
- ruby_version: "23"
- ruby_version: "24"
- ruby_version: "25"
clone_folder: c:\projects\inspec
clone_depth: 1
skip_tags: true
branches:
only:
- master
- release-2.0
matrix:
allow_failures:
- ruby_version: "25"
cache:
- vendor/bundle -> appveyor.yml

117
docs/platforms.md Normal file
View file

@ -0,0 +1,117 @@
# Using InSpec 2.0 with Platforms
New in InSpec 2.0, you may now use certain InSpec resources to audit properties of things that aren't individual machines - for example, an Amazon Web Services S3 bucket.
In the initial release of InSpec 2.0, support for selected AWS and Azure resources is included.
## AWS Platform Support
### Setting up AWS credentials for InSpec
InSpec uses the standard AWS authentication mechanisms. Typically, you will create an IAM user which will be used for auditing activities.
1. Create an IAM user in the AWS console, with your choice of username. Check the box marked "Programmatic Access."
2. On the Permissions screen, choose Direct Attach. Select the AWS-managed IAM Profile named "ReadOnlyAccess." If you wish to restrict the user further, you may do so; see individual InSpec resources to identify which permissions are required.
3. After the key is generated, record the Access Key ID and Secret Key.
#### Using Environment Variables to provide credentials
You may provide the credentials to InSpec by setting the following environment variables: `AWS_REGION`, `AWS_ACCESS_KEY_ID`, and `AWS_SECRET_KEY_ID`. You may also use `AWS_PROFILE`, or if you are using MFA, `AWS_SESSION_TOKEN`. See the [AWS Command Line Interface Docs](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) for details.
Once you have your environment variables set, you can verify your credentials by running:
```bash
you$ inspec detect -t aws://
== Platform Details
Name: aws
Families: cloud, api
```
#### Using the InSpec target option to provide credentials
Look for a file in your home directory named `~/.aws/credentials`. If it does not exist, create it. Choose a name for your profile; here, we're using the name 'auditing'. Add your credentials as a new profile, in INI format:
```
[auditing]
aws_access_key_id = AKIA....
aws_secret_access_key = 1234....abcd
```
You may now run InSpec using the `--target` / `-t` option, using the format `-t aws://region/profile`. For example, to connect to the Ohio region using a profile named 'auditing', use `-t aws://us-east-2/auditing`.
To verify your credentials,
```bash
you$ inspec detect -t aws://
== Platform Details
Name: aws
Families: cloud, api
```
#### Verifying your credentials
To verify your credentials
## Azure Platform Support in InSpec 2.0
### Setting up Azure credentials for InSpec
To use InSpec Azure resources, you will need a Service Principal Name (SPN) to be created in the Azure subscription that is being audited.
This can be done on the command line or from the Azure Portal:
- Azure CLI: https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-authenticate-service-principal-cli
- PowerShell: https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-authenticate-service-principal
- Azure Portal: https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal
The information from the SPN can be specified either in a file `~/.azure/credentials`, as environment variables, or using InSpec target URIs.
#### Using the Azure Credentials File
By default InSpec is configured to look at ~/.azure/credentials, and it should contain:
```
[<SUBSCRIPTION_ID>]
client_id = "<CLIENT_ID>"
client_secret = "<CLIENT_SECRET>"
tenant_id = "<TENANT_ID>"
```
NOTE: In the Azure web portal, these values are labelled differently:
* The client_id is referred to as the 'Application ID'
* The client_secret is referred to as the 'Key (Password Type)'
* The tenant_id is referred to as the 'Directory ID'
With the credentials are in place you may now execute InSpec:
```bash
inspec exec my-inspec-profile -t azure://
```
#### Using Environment variables
You may also set the Azure credentials via environment variables:
- `AZURE_SUBSCRIPTION_ID`
- `AZURE_CLIENT_ID`
- `AZURE_CLIENT_SECRET`
- `AZURE_TENANT_ID`
For example:
```bash
AZURE_SUBSCRIPTION_ID="2fbdbb02-df2e-11e6-bf01-fe55135034f3" \
AZURE_CLIENT_ID="58dc4f6c-df2e-11e6-bf01-fe55135034f3" \
AZURE_CLIENT_SECRET="Jibr4iwwaaZwBb6W" \
AZURE_TENANT_ID="6ad89b58-df2e-11e6-bf01-fe55135034f3" inspec exec my-profile -t azure://
```
#### Using InSpec Target Syntax
If you have created a `~/.azure/credentials` file as above, you may also use the InSpec command line `--target` / `-t` option to select a subscription ID. For example:
```bash
inspec exec my-profile -t azure://2fbdbb02-df2e-11e6-bf01-fe55135034f3
```

View file

@ -4,7 +4,7 @@ title: About the audit_policy Resource
# audit_policy
Use the `audit_policy` Inspec audit resource to test auditing policies on the Windows platform. An auditing policy is a category of security-related events to be audited. Auditing is disabled by default and may be enabled for categories like account management, logon events, policy changes, process tracking, privilege use, system events, or object access. For each enabled auditing category property, the auditing level may be set to `No Auditing`, `Not Specified`, `Success`, `Success and Failure`, or `Failure`.
Use the `audit_policy` InSpec audit resource to test auditing policies on the Windows platform. An auditing policy is a category of security-related events to be audited. Auditing is disabled by default and may be enabled for categories like account management, logon events, policy changes, process tracking, privilege use, system events, or object access. For each enabled auditing category property, the auditing level may be set to `No Auditing`, `Not Specified`, `Success`, `Success and Failure`, or `Failure`.
<br>

View file

@ -1,116 +0,0 @@
---
title: About the auditd_rules Resource
---
# auditd_rules
Use the `auditd_rules` InSpec audit resource to test the rules for logging that exist on the system. The `audit.rules` file is typically located under `/etc/audit/` and contains the list of rules that define what is captured in log files. This resource uses `auditctl` to query the run-time `auditd` rules setup, which may be different from `audit.rules`.
<br>
## Syntax
An `auditd_rules` resource block declares one (or more) rules to be tested, and then what that rule should do. The syntax depends on the version of `audit`:
For `audit` >= 2.3:
describe auditd_rules do
its('lines') { should contain_match(rule) }
end
For `audit` < 2.3:
describe audit_daemon_rules do
its("LIST_RULES") {
rule
}
end
For example:
describe auditd_rules do
its('LIST_RULES') { should eq [
'exit,always syscall=rmdir,unlink',
'exit,always auid=1001 (0x3e9) syscall=open',
'exit,always watch=/etc/group perm=wa',
'exit,always watch=/etc/passwd perm=wa',
'exit,always watch=/etc/shadow perm=wa',
'exit,always watch=/etc/sudoers perm=wa',
'exit,always watch=/etc/secret_directory perm=r',
] }
end
or test that individual rules are defined:
describe auditd_rules do
its('LIST_RULES') {
should contain_match(/^exit,always watch=\/etc\/group perm=wa key=identity/)
}
its('LIST_RULES') {
should contain_match(/^exit,always watch=\/etc\/passwd perm=wa key=identity/)
}
its('LIST_RULES') {
should contain_match(/^exit,always watch=\/etc\/gshadow perm=wa key=identity/)
}
its('LIST_RULES') {
should contain_match(/^exit,always watch=\/etc\/shadow perm=wa key=identity/)
}
its('LIST_RULES') {
should contain_match(/^exit,always watch=\/etc\/security\/opasswd perm=wa key=identity/)
}
end
where each test must declare one (or more) rules to be tested.
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test if a rule contains a matching element that is identified by a regular expression
For `audit` >= 2.3:
describe auditd_rules do
its('lines') { should contain_match(%r{-w /etc/ssh/sshd_config/}) }
end
For `audit` < 2.3:
describe audit_daemon_rules do
its("LIST_RULES") {
should contain_match(/^exit,always arch=.*\
key=time-change\
syscall=adjtimex,settimeofday/)
}
end
### Query the audit daemon status
describe auditd_rules.status('backlog') do
it { should cmp 0 }
end
### Query properties of rules targeting specific syscalls or files
describe auditd_rules.syscall('open').action do
it { should eq(['always']) }
end
describe auditd_rules.key('sshd_config') do
its('permissions') { should contain_match(/x/) }
end
Filters may be chained. For example:
describe auditd_rules.syscall('open').action('always').list do
it { should eq(['exit']) }
end
<br>
## Matchers
For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).

View file

@ -0,0 +1,140 @@
---
title: About the aws_cloudtrail_trail Resource
platform: aws
---
# aws\_cloudtrail\_trail
Use the `aws_cloudtrail_trail` InSpec audit resource to test properties of a single AWS Cloudtrail Trail.
AWS CloudTrail is a service that enables governance, compliance, operational auditing, and risk auditing of your AWS account. With CloudTrail, you can log, continuously monitor, and retain account activity related to actions across your AWS infrastructure. CloudTrail provides event history of your AWS account activity, including actions taken through the AWS Management Console, AWS SDKs, command line tools, and other AWS services. This event history simplifies security analysis, resource change tracking, and troubleshooting.
Each AWS Cloudtrail Trail is uniquely identified by its `trail_name` or `trail_arn`.
<br>
## Syntax
An `aws_cloudtrail_trail` resource block identifies a trail by `trail_name`.
# Find a trail by name
describe aws_cloudtrail_trail('trail-name') do
it { should exist }
end
# Hash syntax for trail name
describe aws_cloudtrail_trail(trail_name: 'trail-name') do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test that the specified trail does exist
describe aws_cloudtrail_trail('trail-name') do
it { should exist }
end
### Test that the specified trail is encrypted using SSE-KMS
describe aws_cloudtrail_trail('trail-name') do
it { should be_encrypted }
end
### Test that the specified trail is a multi region trail
describe aws_cloudtrail_trail('trail-name') do
it { should be_multi_region_trail }
end
<br>
## Properties
* `s3_bucket_name`, `trail_arn`, `cloud_watch_logs_role_arn`, `cloud_watch_logs_log_group_arn`, `kms_key_id`, `home_region`,
<br>
## Property Examples
### s3\_bucket\_name
Specifies the name of the Amazon S3 bucket designated for publishing log files.
describe aws_cloudtrail_trail('trail-name') do
its('s3_bucket_name') { should cmp "s3-bucket-name" }
end
### trail\_arn
The ARN identifier of the specified trail. An ARN uniquely identifies the trail within AWS.
describe aws_cloudtrail_trail('trail-name') do
its('trail_arn') { should cmp "arn:aws:cloudtrail:us-east-1:484747447281:trail/trail-name" }
end
### cloud\_watch\_logs\_role\_arn
Specifies the role for the CloudWatch Logs endpoint to assume to write to a user\'s log group.
describe aws_cloudtrail_trail('trail-name') do
its('cloud_watch_logs_role_arn') { should include "arn:aws:iam:::role/CloudTrail_CloudWatchLogs_Role" }
end
### cloud\_watch\_logs\_log\_group\_arn
Specifies a log group name using an Amazon Resource Name (ARN), a unique identifier that represents the log group to which CloudTrail logs will be delivered.
describe aws_cloudtrail_trail('trail-name') do
its('cloud_watch_logs_log_group_arn') { should include "arn:aws:logs:us-east-1::log-group:test:*" }
end
### kms\_key\_id
Specifies the KMS key ID to used to encrypt the logs delivered by CloudTrail.
describe aws_cloudtrail_trail('trail-name') do
its('kms_key_id') { should include "key-arn" }
end
### home\_region
Specifies the region in which the trail was created.
describe aws_cloudtrail_trail('trail-name') do
its('home_region') { should include "us-east-1" }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### be\_multi\_region\_trail
The test will pass if the identified trail is a multi region trail.
describe aws_cloudtrail_trail('trail-name') do
it { should be_multi_region_trail }
end
### be\_encrypted
The test will pass if the logs delivered by the identified trail is encrypted.
describe aws_cloudtrail_trail('trail-name') do
it { should be_encrypted }
end
### be\_log\_file\_validation\_enabled
The test will pass if the identified trail has log file integrity validation is enabled.
describe aws_cloudtrail_trail('trail-name') do
it { should be_log_file_validation_enabled }
end

View file

@ -0,0 +1,81 @@
---
title: About the aws_cloudtrail_trails Resource
platform: aws
---
# aws\_cloudtrail\_trails
Use the `aws_cloudtrail_trails` InSpec audit resource to test properties of some or all AWS CloudTrail Trails.
AWS CloudTrail is a service that enables governance, compliance, operational auditing, and risk auditing of your AWS account. With CloudTrail, you can log, continuously monitor, and retain account activity related to actions across your AWS infrastructure. CloudTrail provides event history of your AWS account activity, including actions taken through the AWS Management Console, AWS SDKs, command line tools, and other AWS services. This event history simplifies security analysis, resource change tracking, and troubleshooting.
Each AWS CloudTrail Trails is uniquely identified by its trail name or trail arn.
<br>
## Syntax
An `aws_cloudtrail_trails` resource block collects a group of CloudTrail Trails and then tests that group.
# Verify the number of CloudTrail Trails in the AWS account
describe aws_cloudtrail_trails do
its('entries.count') { should cmp 10 }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_cloudtrail_trails`, its limited functionality precludes examples.
<br>
## Properties
* `entries`, `names`, `trail_arns`
<br>
## Property Examples
### entries
Provides access to the raw results of the query. This can be useful for checking counts and other advanced operations.
# Allow at most 100 CloudTrail Trails on the account
describe aws_cloudtrail_trails do
its('entries.count') { should be <= 100}
end
### names
Provides a list of trail names for all CloudTrail Trails in the AWS account.
describe aws_cloudtrail_trails do
its('names') { should include('trail-1') }
end
### trail\_arns
Provides a list of trail arns for all CloudTrail Trails in the AWS account.
describe aws_cloudtrail_trails do
its('trail_arns') { should include('arn:aws:cloudtrail:us-east-1::trail/trail-1') }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
# Verify that at least one CloudTrail Trail exists.
describe aws_cloudtrail_trails
it { should exist }
end

View file

@ -0,0 +1,86 @@
---
title: About the aws_cloudwatch_alarm Resource
platform: aws
---
# aws\_cloudwatch\_alarm
Use the `aws_cloudwatch_alarm` InSpec audit resource to test properties of a single Cloudwatch Alarm.
Cloudwatch Alarms are currently identified using the metric name and metric namespace. Future work may allow other approaches to identifying alarms.
<br>
## Syntax
An `aws_cloudwatch_alarm` resource block searches for a Cloudwatch Alarm, specified by several search options. If more than one Alarm matches, an error occurs.
# Look for a specific alarm
aws_cloudwatch_alarm(
metric: 'my-metric-name',
metric_namespace: 'my-metric-namespace',
) do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Ensure an Alarm has at least one alarm action
describe aws_cloudwatch_alarm(
metric: 'my-metric-name',
metric_namespace: 'my-metric-namespace',
) do
its('alarm_actions') { should_not be_empty }
end
<br>
## Properties
* `alarm_actions`
## Property Examples
### alarm\_actions
`alarm_actions` returns a list of strings. Each string is the ARN of an action that will be taken should the alarm be triggered.
# Ensure that the alarm has at least one action
describe aws_cloudwatch_alarm(
metric: 'bed-metric',
metric_namespace: 'my-metric-namespace',
) do
its('alarm_actions') { should_not be_empty }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if a Cloudwatch Alarm could be found. Use `should_not` if you expect zero matches.
# Expect good metric
describe aws_cloudwatch_alarm(
metric: 'good-metric',
metric_namespace: 'my-metric-namespace',
) do
it { should exist }
end
# Disallow alarms based on bad-metric
describe aws_cloudwatch_alarm(
metric: 'bed-metric',
metric_namespace: 'my-metric-namespace',
) do
it { should_not exist }
end

View file

@ -0,0 +1,151 @@
---
title: About the aws_cloudwatch_log_metric_filter Resource
platform: aws
---
# aws\_cloudwatch\_log\_metric\_filter
Use the `aws_cloudwatch_log_metric_filter` InSpec audit resource to search for and test properties of individual AWS Cloudwatch Log Metric Filters.
A Log Metric Filter (LMF) is an AWS resource that observes log traffic, looks for a specified pattern, and updates a metric about the number times the match occurs. The metric can also be connected to AWS Cloudwatch Alarms, so that actions can be taken when a match occurs.
<br>
## Syntax
An `aws_cloudwatch_log_metric_filter` resource block searches for an LMF, specified by several search options. If more than one log metric filter matches, an error occurs.
# Look for a LMF by its filter name and log group name. This combination
# will always either find at most one LMF - no duplicates.
describe aws_cloudwatch_log_metric_filter(
filter_name: 'my-filter',
log_group_name: 'my-log-group'
) do
it { should exist }
end
# Search for an LMF by pattern and log group.
# This could result in an error if the results are not unique.
describe aws_cloudwatch_log_metric_filter(
log_group_name: 'my-log-group',
pattern: 'my-filter'
) do
it { should exist }
end
<br>
## Filter Attributes
* `filter_name`, `log_group_name`, `pattern`
<br>
## Filter Examples
### filter\_name
This is the identifier of the log metric filter within its log group. To ensure you have a unique result, you must also provide the `log_group_name`.
describe aws_cloudwatch_log_metric_filter(
filter_name: 'my-filter'
) do
it { should exist }
end
### log\_group\_name
The name of the Cloudwatch Log Group that the LMF is watching. Together with `filter_name`, this uniquely identifies an LMF.
describe aws_cloudwatch_log_metric_filter(
log_group_name: 'my-log-group',
) do
it { should exist }
end
### pattern
The filter pattern used to match entries from the logs in the log group.
describe aws_cloudwatch_log_metric_filter(
pattern: '"ERROR" - "Exiting"',
) do
it { should exist }
end
<br>
## Properties
* `filter_name`, `log_group_name`,` metric_name`, `metric_namespace`, `pattern`
<br>
## Property Examples
### filter\_name
The name of the LMF within the `log_group`.
# Check the name of the LMF that has a certain pattern
describe aws_cloudwatch_log_metric_filter(
log_group_name: 'app-log-group',
pattern: 'KERBLEWIE',
) do
its('filter_name') { should cmp 'kaboom_lmf' }
end
### log\_group\_name
The name of the log group that the LMF is watching.
# Check which log group the LMF 'error-watcher' is watching
describe aws_cloudwatch_log_metric_filter(
filter_name: 'error-watcher',
) do
its('log_group_name') { should cmp 'app-log-group' }
end
### metric\_name, metric\_namespace
The name and namespace of the Cloudwatch Metric that will be updated when the LMF matches. You also need the `metric_namespace` to uniquely identify the metric.
# Ensure that the LMF has the right metric name
describe aws_cloudwatch_log_metric_filter(
filter_name: 'my-filter',
log_group_name: 'my-log-group',
) do
its('metric_name') { should cmp 'MyMetric' }
its('metric_namespace') { should cmp 'MyFantasticMetrics' }
end
### pattern
The pattern used to match entries from the logs in the log group.
# Ensure that the LMF is watching for errors
describe aws_cloudwatch_log_metric_filter(
filter_name: 'error-watcher',
log_group_name: 'app-log-group',
) do
its('pattern') { should cmp 'ERROR' }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exist
Matches (i.e., passes the test) if the resource parameters (search criteria) were able to locate exactly one LMF.
describe aws_cloudwatch_log_metric_filter(
log_group_name: 'my-log-group',
) do
it { should exist }
end

View file

@ -0,0 +1,106 @@
---
title: About the aws_ec2_instance Resource
platform: aws
---
# aws\_ec2\_instance
Use the `aws_ec2_instance` InSpec audit resource to test properties of a single AWS EC2 instance.
<br>
## Syntax
An `aws_ec2_instance` resource block declares the tests for a single AWS EC2 instance by either name or id.
describe aws_ec2_instance('i-01a2349e94458a507') do
it { should exist }
end
describe aws_ec2_instance(name: 'my-instance') do
it { should be_running }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test that an EC2 instance does not exist
describe aws_ec2_instance(name: 'dev-server') do
it { should_not exist }
end
### Test that an EC2 instance is running
describe aws_ec2_instance(name: 'prod-database') do
it { should be_running }
end
### Test that an EC2 instance is using the correct image ID
describe aws_iam_instance(name: 'my-instance') do
its('image_id') { should eq 'ami-27a58d5c' }
end
### Test that an EC2 instance has the correct tag
describe aws_ec2_instance('i-090c29e4f4c165b74') do
its('tags') { should include(key: 'Contact', value: 'Gilfoyle') }
end
<br>
## Properties
* `architecture`, `client_token`, `image_id`,`instance_type`, `key_name`, `launch_time`,`private_ip_address`, `private_dns_name`, `public_dns_name`, `public_ip_address`, `root_device_type`, `root_device_name`, `security_group_ids`, `subnet_id`, `tags`,`virtualization_type`, `vpc_id`
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### be\_pending
The `be\_pending` matcher tests if the described EC2 instance state is `pending`. This indicates that an instance is provisioning. This state should be temporary.
it { should be_pending }
### be\_running
The `be_running` matcher tests if the described EC2 instance state is `running`. This indicates the instance is fully operational from AWS's perspective.
it { should be_running }
### be\_shutting\_down
The `be_shutting_down` matcher tests if the described EC2 instance state is `shutting-down`. This indicates the instance has received a termination command and is in the process of being permanently halted and de-provisioned. This state should be temporary.
it { should be_shutting_down }
### be\_stopped
The `be_stopped` matcher tests if the described EC2 instance state is `stopped`. This indicates that the instance is suspended and may be started again.
it { should be_stopped }
### be\_stopping
The `be_stopping` matcher tests if the described EC2 instance state is `stopping`. This indicates that an AWS stop command has been issued, which will suspend the instance in an OS-unaware manner. This state should be temporary.
it { should be_stopping }
### be\_terminated
The `be_terminated` matcher tests if the described EC2 instance state is `terminated`. This indicates the instance is permanently halted and will be removed from the instance listing in a short period. This state should be temporary.
it { should be_terminated }
### be\_unknown
The `be_unknown` matcher tests if the described EC2 instance state is `unknown`. This indicates an error condition in the AWS management system. This state should be temporary.
it { should be_unknown }

View file

@ -0,0 +1,123 @@
---
title: About the aws_iam_access_key Resource
platform: aws
---
# aws\_iam\_access\_key
Use the `aws_iam_access_key` InSpec audit resource to test properties of a single AWS IAM access key.
<br>
## Syntax
An `aws_iam_access_key` resource block declares the tests for a single AWS IAM access key. An access key is uniquely identified by its access key id.
# This is unique - the key will either exist or it won't, but it will never be an error.
describe aws_iam_access_key(access_key_id: 'AKIA12345678ABCD') do
it { should exist }
it { should_not be_active }
its('create_date') { should be > Time.now - 365 * 86400 }
its('last_used_date') { should be > Time.now - 90 * 86400 }
end
# id is an alias for access_key_id
describe aws_iam_access_key(id: 'AKIA12345678ABCD') do
# Same
end
Access keys are associated with IAM users, who may have zero, one or two access keys. You may also lookup an access key by username. If the user has more than one access key, an error occurs (You may use `aws_iam_access_keys` with the `username` resource parameter to access a user's keys when they have multiple keys.)
# This is not unique. If the user has zero or one keys, it is not an error.
# If they have two, it is an error.
describe aws_iam_access_key(username: 'roderick') do
it { should exist }
it { should be_active }
end
You may also use both username and access key id to ensure that a particular key is associated with a particular user.
describe aws_iam_access_key(username: 'roderick', access_key_id: 'AKIA12345678ABCD') do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test that an IAM access key is not active
describe aws_iam_access_key(username: 'username', id: 'access-key-id') do
it { should_not be_active }
end
### Test that an IAM access key is older than one year
describe aws_iam_access_key(username: 'username', id: 'access-key-id') do
its('create_date') { should be > Time.now - 365 * 86400 }
end
### Test that an IAM access key has been used in the past 90 days
describe aws_iam_access_key(username: 'username', id: 'access-key-id') do
its('last_used_date') { should be > Time.now - 90 * 86400 }
end
<br>
## Properties
* `access_key_id`, `create_date`, `last_used_date`, `username`
<br>
## Property Examples
### access\_key\_id
The unique ID of this access key.
describe aws_iam_access_key(username: 'bob')
its('access_key_id') { should cmp 'AKIA12345678ABCD' }
end
### create\_date
The date and time, as a Ruby DateTime, at which the access key was created.
# Is the access key less than a year old?
describe aws_iam_access_key(username: 'bob')
its('create_date') { should be > Time.now - 365 * 86400 }
end
### last\_used\_date
The date and time, as a Ruby DateTime, at which the access key was last_used.
# Has the access key been used in the last year?
describe aws_iam_access_key(username: 'bob')
its('last_used_date') { should be > Time.now - 365 * 86400 }
end
### username
The IAM user that owns this key.
describe aws_iam_access_key(access_key_id: 'AKIA12345678ABCD')
its('username') { should cmp 'bob' }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### be\_active
The `be_active` matcher tests if the described IAM access key is active.
it { should be_active }

View file

@ -0,0 +1,198 @@
---
title: About the aws_iam_access_keys Resource
platform: aws
---
# aws\_iam\_access\_keys
Use the `aws_iam_access_keys` InSpec audit resource to test properties of some or all IAM Access Keys.
To test properties of a single Access Key, use the `aws_iam_access_key` resource instead.
To test properties of an individual user's access keys, use the `aws_iam_user` resource.
Access Keys are closely related to AWS User resources. Use this resource to perform audits of all keys or of keys specified by criteria unrelated to any particular user.
<br>
## Syntax
An `aws_iam_access_keys` resource block uses an optional filter to select a group of access keys and then tests that group.
# Do not allow any access keys
describe aws_iam_access_keys do
it { should_not exist }
end
# Don't let fred have access keys, using filter argument syntax
describe aws_iam_access_keys.where(username: 'fred') do
it { should_not exist }
end
# Don't let fred have access keys, using filter block syntax (most flexible)
describe aws_iam_access_keys.where { username == 'fred' } do
it { should_not exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Disallow access keys created more than 90 days ago
describe aws_iam_access_keys.where { created_age > 90 } do
it { should_not exist }
end
<br>
## Filter Criteria
* `active`, `create_date`, `created_days_ago`, `created_hours_ago`, `created_with_user`, `ever_used`, `inactive`, `last_used_date`, `last_used_hours_ago`, `last_used_days_ago`, `never_used`, `user_created_date`
<br>
## Filter Examples
### active
A true / false value indicating if an Access Key is currently "Active" (the normal state) in the AWS console. See also: `inactive`.
# Check if a particular key is enabled
describe aws_iam_access_keys.where { active } do
its('access_key_ids') { should include('AKIA1234567890ABCDEF')}
end
### create\_date
A DateTime identifying when the Access Key was created. See also `created_days_ago` and `created_hours_ago`.
# Detect keys older than 2017
describe aws_iam_access_keys.where { create_date < DateTime.parse('2017-01-01') } do
it { should_not exist }
end
### created\_days\_ago, created\_hours\_ago
An integer, representing how old the access key is.
# Don't allow keys that are older than 90 days
describe aws_iam_access_keys.where { created_days_ago > 90 } do
it { should_not exist }
end
### created\_with\_user
A true / false value indicating if the Access Key was likely created at the same time as the user, by checking if the difference between created_date and user_created_date is less than 1 hour.
# Do not automatically create keys for users
describe aws_iam_access_keys.where { created_with_user } do
it { should_not exist }
end
### ever\_used
A true / false value indicating if the Access Key has ever been used, based on the last_used_date. See also: `never_used`.
# Check to see if a particular key has ever been used
describe aws_iam_access_keys.where { ever_used } do
its('access_key_ids') { should include('AKIA1234567890ABCDEF')}
end
### inactive
A true / false value indicating if the Access Key has been marked Inactive in the AWS console. See also: `active`.
# Don't leave inactive keys laying around
describe aws_iam_access_keys.where { inactive } do
it { should_not exist }
end
### last\_used\_date
A DateTime identifying when the Access Key was last used. Returns nil if the key has never been used. See also: `ever_used`, `last_used_days_ago`, `last_used_hours_ago`, and `never_used`.
# No one should do anything on Mondays
describe aws_iam_access_keys.where { ever_used and last_used_date.monday? } do
it { should_not exist }
end
### last\_used\_days\_ago, last\_used\_hours\_ago
An integer representing when the key was last used. See also: `ever_used`, `last_used_date`, and `never_used`.
# Don't allow keys that sit unused for more than 90 days
describe aws_iam_access_keys.where { last_used_days_ago > 90 } do
it { should_not exist }
end
### never\_used
A true / false value indicating if the Access Key has never been used, based on the `last_used_date`. See also: `ever_used`.
# Don't allow unused keys to lay around
describe aws_iam_access_keys.where { never_used } do
it { should_not exist }
end
### username
Searches for access keys owned by the named user. Each user may have zero, one, or two access keys.
describe aws_iam_access_keys(username: 'bob') do
it { should exist }
end
### user\_created\_date
The date at which the user was created.
# Users have to be a week old to have a key
describe aws_iam_access_keys.where { user_created_date > Date.now - 7 }
it { should_not exist }
end
<br>
## Properties
* `access_key_ids`, `entries`
## Property Examples
### access\_key\_ids
Provides a list of all access key IDs matched.
describe aws_iam_access_keys do
its('access_key_ids') { should include('AKIA1234567890ABCDEF') }
end
### entries
Provides access to the raw results of the query. This can be useful for checking counts and other advanced operations.
# Allow at most 100 access keys on the account
describe aws_iam_access_keys do
its('entries.count') { should be <= 100}
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
# Sally should have at least one access key
describe aws_iam_access_keys.where(username: 'sally') do
it { should exist }
end
# Don't let fred have access keys
describe aws_iam_access_keys.where(username: 'fred') do
it { should_not exist }
end

View file

@ -0,0 +1,46 @@
---
title: About the aws_iam_group Resource
platform: aws
---
# aws\_iam\_group
Use the `aws_iam_group` InSpec audit resource to test properties of a single IAM group.
To test properties of multiple or all groups, use the `aws_iam_groups` resource.
<br>
## Syntax
An `aws_iam_group` resource block identifies a group by group name.
# Find a group by group name
describe aws_iam_group('mygroup') do
it { should exist }
end
# Hash syntax for group name
describe aws_iam_group(group_name: 'mygroup') do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_iam_group`, its limited functionality precludes examples.
<br>
## Matchers
### exists
The control will pass if a group with the given group name exists.
describe aws_iam_group('mygroup')
it { should exist }
end

View file

@ -0,0 +1,43 @@
---
title: About the aws_iam_groups Resource
platform: aws
---
# aws\_iam\_groups
Use the `aws_iam_groups` InSpec audit resource to test properties of all or multiple groups.
To test properties of a single group, use the `aws_iam_group` resource.
<br>
## Syntax
An `aws_iam_groups` resource block uses an optional filter to select a collection of IAM groups and then tests that collection.
# The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
describe aws_iam_groups do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_iam_groups`, its limited functionality precludes examples.
<br>
## Matchers
For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
describe aws_iam_groups
it { should exist }
end

View file

@ -0,0 +1,76 @@
---
title: About the aws_iam_password_policy Resource
platform: aws
---
# aws\_iam\_password\_policy
Use the `aws_iam_password_policy` InSpec audit resource to test properties of the AWS IAM Password Policy.
<br>
## Syntax
An `aws_iam_password_policy` resource block takes no parameters. Several properties and matchers are available.
describe aws_iam_password_policy do
it { should require_lowercase_characters }
end
<br>
## Properties
* `max_password_age_in_days`, `minimum_password_length`, `number_of_passwords_to_remember`
## Examples
The following examples show how to use this InSpec audit resource.
### Test that the IAM Password Policy requires lowercase characters, uppercase characters, numbers, symbols, and a minimum length greater than eight
describe aws_iam_password_policy do
it { should require_lowercase_characters }
it { should require_uppercase_characters }
it { should require_symbols }
it { should require_numbers }
its('minimum_password_length') { should be > 8 }
end
### Test that the IAM Password Policy allows users to change their password
describe aws_iam_password_policy do
it { should allow_users_to_change_passwords }
end
### Test that the IAM Password Policy expires passwords
describe aws_iam_password_policy do
it { should expire_passwords }
end
### Test that the IAM Password Policy has a max password age
describe aws_iam_password_policy do
its('max_password_age_in_days') { should be 90 }
end
### Test that the IAM Password Policy prevents password reuse
describe aws_iam_password_policy do
it { should prevent_password_reuse }
end
### Test that the IAM Password Policy requires users to remember 3 previous passwords
describe aws_iam_password_policy do
its('number_of_passwords_to_remember') { should eq 3 }
end
<br>
## Matchers
This resource uses the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
* `allows_users_to_change_passwords`, `expire_passwords`, `prevent_password_reuse`, `require_lowercase_characters` , `require_uppercase_characters`, `require_numbers`, `require_symbols`

View file

@ -0,0 +1,82 @@
---
title: About the aws_iam_policies Resource
platform: aws
---
# aws\_iam\_policies
Use the `aws_iam_policies` InSpec audit resource to test properties of some or all AWS IAM Policies.
A policy is an entity in AWS that, when attached to an identity or resource, defines their permissions. AWS evaluates these policies when a principal, such as a user, makes a request. Permissions in the policies determine if the request is allowed or denied.
Each IAM Policy is uniquely identified by either its `policy_name` or `arn`.
<br>
## Syntax
An `aws_iam_policies` resource block collects a group of IAM Policies and then tests that group.
# Verify the policy specified by the policy name is included in IAM Policies in the AWS account.
describe aws_iam_policies do
its('policy_names') { should include('test-policy-1') }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_iam_policies`, its limited functionality precludes examples.
<br>
## Properties
* `arns`, `entries`, `policy_names`
<br>
## Property Examples
### policy\_names
Provides a list of policy names for all IAM Policies in the AWS account.
describe aws_iam_policies do
its('policy_names') { should include('test-policy-1') }
end
### arns
Provides a list of policy arns for all IAM Policies in the AWS account.
describe aws_iam_policies do
its('arns') { should include('arn:aws:iam::aws:policy/test-policy-1') }
end
### entries
Provides access to the raw results of the query. This can be useful for checking counts and other advanced operations.
# Allow at most 100 IAM Policies on the account
describe aws_iam_policies do
its('entries.count') { should be <= 100}
end
<br>
## Matchers
For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
# Verify that at least one IAM Policies exists.
describe aws_iam_policies
it { should exist }
end

View file

@ -0,0 +1,146 @@
---
title: About the aws_iam_policy Resource
platform: aws
---
# aws\_iam\_policy
Use the `aws_iam_policy` InSpec audit resource to test properties of a single managed AWS IAM Policy.
A policy is an entity in AWS that, when attached to an identity or resource, defines their permissions. AWS evaluates these policies when a principal, such as a user, makes a request. Permissions in the policies determine if the request is allowed or denied.
Each IAM Policy is uniquely identified by either its policy_name or arn.
<br>
## Syntax
An `aws_iam_policy` resource block identifies a policy by policy name.
# Find a policy by name
describe aws_iam_policy('AWSSupportAccess') do
it { should exist }
end
# Find a customer-managed by name
describe aws_iam_policy('customer-managed-policy') do
it { should exist }
end
# Hash syntax for policy name
describe aws_iam_policy(policy_name: 'AWSSupportAccess') do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test that a policy does exist
describe aws_iam_policy('AWSSupportAccess') do
it { should exist }
end
### Test that a policy is attached to at least one entity
describe aws_iam_policy('AWSSupportAccess') do
it { should be_attached }
end
<br>
## Properties
* `arn`, `attachment_count`, `attached_groups`, `attached_roles`,`attached_users`, `default_version_id`
## Property Examples
### arn
"The ARN identifier of the specified policy. An ARN uniquely identifies the policy within AWS."
describe aws_iam_policy('AWSSupportAccess') do
its('arn') { should cmp "arn:aws:iam::aws:policy/AWSSupportAccess" }
end
### attachment\_count
The count of attached entities for the specified policy.
describe aws_iam_policy('AWSSupportAccess') do
its('attachment_count') { should cmp 1 }
end
### attached\_groups
The list of groupnames of the groups attached to the policy.
describe aws_iam_policy('AWSSupportAccess') do
its('attached_groups') { should include "test-group" }
end
### attached\_roles
The list of rolenames of the roles attached to the policy.
describe aws_iam_policy('AWSSupportAccess') do
its('attached_roles') { should include "test-role" }
end
### attached\_users
The list of usernames of the users attached to the policy.
describe aws_iam_policy('AWSSupportAccess') do
its('attached_users') { should include "test-user" }
end
### default\_version\_id
The 'default_version_id' value of the specified policy.
describe aws_iam_policy('AWSSupportAccess') do
its('default_version_id') { should cmp "v1" }
end
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### be\_attached
The test will pass if the identified policy is attached to at least one IAM user, group, or role.
describe aws_iam_policy('AWSSupportAccess') do
it { should be_attached }
end
### be\_attached\_to\_group(GROUPNAME)
The test will pass if the identified policy attached the specified group.
describe aws_iam_policy('AWSSupportAccess') do
it { should be_attached_to_group(GROUPNAME) }
end
### be\_attached\_to\_user(USERNAME)
The test will pass if the identified policy attached the specified user.
describe aws_iam_policy('AWSSupportAccess') do
it { should be_attached_to_user(USERNAME) }
end
### be\_attached\_to\_role(ROLENAME)
The test will pass if the identified policy attached the specified role.
describe aws_iam_policy('AWSSupportAccess') do
it { should be_attached_to_role(ROLENAME) }
end

View file

@ -0,0 +1,65 @@
---
title: About the aws_iam_role Resource
platform: aws
---
# aws\_iam\_role
Use the `aws_iam_role` InSpec audit resource to test properties of a single IAM Role. A Role is a collection of permissions that may be temporarily assumed by a user, EC2 Instance, Lambda Function, or certain other resources.
<br>
## Syntax
# Ensure that a certain role exists by name
describe aws_iam_role('my-role') do
it { should exist }
end
<br>
## Resource Parameters
### role\_name
This resource expects a single parameter that uniquely identifies the IAM Role, the Role Name. You may pass it as a string, or as the value in a hash:
describe aws_iam_role('my-role') do
it { should exist }
end
# Same
describe aws_iam_role(role_name: 'my-role') do
it { should exist }
end
<br>
## Properties
### description
A textual description of the IAM Role.
describe aws_iam_role('my-role') do
its('description') { should be('Our most important Role')}
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exist
Indicates that the Role Name provided was found. Use should_not to test for IAM Roles that should not exist.
describe aws_iam_role('should-be-there') do
it { should exist }
end
describe aws_iam_role('should-not-be-there') do
it { should_not exist }
end

View file

@ -0,0 +1,58 @@
---
title: About the aws_iam_root_user Resource
platform: aws
---
# aws\_iam\_root\_user
Use the `aws_iam_root_user` InSpec audit resource to test properties of the root user (owner of the account).
To test properties of all or multiple users, use the `aws_iam_users` resource.
To test properties of a specific AWS user use the `aws_iam_user` resource.
<br>
## Syntax
An `aws_iam_root_user` resource block requires no parameters but has several matchers
describe aws_iam_root_user do
its { should have_mfa_enabled }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test that the AWS root account has at-least one access key
describe aws_iam_root_user do
it { should have_access_key }
end
### Test that the AWS root account has Multi-Factor Authentication enabled
describe aws_iam_root_user do
it { should have_mfa_enabled }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### have\_mfa\_enabled
The `have_mfa_enabled` matcher tests if the AWS root user has Multi-Factor Authentication enabled, requiring them to enter a secondary code when they login to the web console.
it { should have_mfa_enabled }
### have\_access\_key
The `have_access_key` matcher tests if the AWS root user has at least one access key.
it { should have_access_key }

View file

@ -0,0 +1,64 @@
---
title: About the aws_iam_user Resource
platform: aws
---
# aws\_iam\_user
Use the `aws_iam_user` InSpec audit resource to test properties of a single AWS IAM user.
To test properties of more than one user, use the `aws_iam_users` resource.
To test properties of the special AWS root user (which owns the account), use the `aws_iam_root_user` resource.
<br>
## Syntax
An `aws_iam_user` resource block declares a user by name, and then lists tests to be performed.
describe aws_iam_user(name: 'test_user') do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test that a user does not exist
describe aws_iam_user(name: 'gone') do
it { should_not exist }
end
### Test that a user has multi-factor authentication enabled
describe aws_iam_user(name: 'test_user') do
it { should have_mfa_enabled }
end
### Test that a service user does not have a password
describe aws_iam_user(name: 'test_user') do
it { should have_console_password }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### have\_console\_password
The `have_console_password` matcher tests if the user has a password that could be used to log into the AWS web console.
it { should have_console_password }
### have\_mfa\_enabled
The `have_mfa_enabled` matcher tests if the user has Multi-Factor Authentication enabled, requiring them to enter a secondary code when they login to the web console.
it { should have_mfa_enabled }

View file

@ -0,0 +1,90 @@
---
title: About the aws_iam_users Resource
platform: aws
---
# aws\_iam\_users
Use the `aws_iam_users` InSpec audit resource to test properties of a all or multiple users.
To test properties of a single user, use the `aws_iam_user` resource.
To test properties of the special AWS root user (which owns the account), use the `aws_iam_root_user` resource.
<br>
## Syntax
An `aws_iam_users` resource block users a filter to select a group of users and then tests that group
describe aws_iam_users.where(has_mfa_enabled?: false) do
it { should_not exist }
end
<br>
## Filter Criteria
* `has_mfa_enabled`, `has_console_password`, `password_ever_used?`, `password_never_used?`, `password_last_used_days_ago`, `username`
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test that all users have Multi-Factor Authentication enabled
describe aws_iam_users.where(has_mfa_enabled?: false) do
it { should_not exist }
end
### Test that at least one user has a console password to log into the AWS web console
describe aws_iam_users.where(has_console_password?: true) do
it { should exist }
end
### Test that all users who have a console password have Multi-Factor Authentication enabled
console_users_without_mfa = aws_iam_users
.where(has_console_password?: true)
.where(has_mfa_enabled?: false)
describe console_users_without_mfa do
it { should_not exist }
end
### Test that all users who have a console password have used it at least once
console_users_with_unused_password = aws_iam_users
.where(has_console_password?: true)
.where(password_never_used?: false)
describe console_users_with_unused_password do
it { should_not exist }
end
### Test that at least one user exists who has a console password and has used it at least once
console_users_with_used_password = aws_iam_users
.where(has_console_password?: true)
.where(password_ever_used?: false)
describe console_users_with_used_password do
it { should exist }
end
### Test that users with passwords that have not been used for 90 days do not
describe aws_iam_users.where { password_last_used_days_ago > 90 } do
it { should_not exist }
end
<br>
## Matchers
This InSpec audit resource has no specific matchers.
For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).

View file

@ -0,0 +1,84 @@
---
title: About the aws_kms_keys Resource
platform: aws
---
# aws\_kms\_keys
Use the `aws_kms_keys` InSpec audit resource to test properties of some or all AWS KMS Keys.
AWS Key Management Service (KMS) is a managed service that makes creating and controlling your encryption keys for your data easier. KMS uses Hardware Security Modules (HSMs) to protect the security of your keys.
AWS Key Management Service is integrated with several other AWS services to help you protect the data you store with these services.
Each AWS KMS Key is uniquely identified by its key-id or key-arn.
<br>
## Syntax
An `aws_kms_keys` resource block uses an optional filter to select a group of KMS Keys and then tests that group.
# Verify the number of KMS keys in the AWS account
describe aws_kms_keys do
its('entries.count') { should cmp 10 }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_kms_keys`, its limited functionality precludes examples.
<br>
## Properties
* `entries`, `key_arns`, `key_ids`
<br>
## Property Examples
### entries
Provides access to the raw results of a query. This can be useful for checking counts and other advanced operations.
# Allow at most 100 KMS Keys on the account
describe aws_kms_keys do
its('entries.count') { should be <= 100}
end
### key\_arns
Provides a list of key arns for all KMS Keys in the AWS account.
describe aws_kms_keys do
its('key_arns') { should include('arn:aws:kms:us-east-1::key/key-id') }
end
### key\_ids
Provides a list of key ids for all KMS Keys in the AWS account.
describe aws_kms_keys do
its('key_ids') { should include('fd7e608b-f435-4186-b8b5-111111111111') }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
# Verify that at least one KMS Key exists.
describe aws_kms_keys
it { should exist }
end

View file

@ -0,0 +1,47 @@
---
title: About the aws_route_table Resource
platform: aws
---
# aws\_route\_table
Use the `aws_route_table` InSpec audit resource to test properties of a single Route Table. A route table contains a set of rules, called routes, that are used to determine where network traffic is directed.
<br>
## Syntax
# Ensure that a certain route table exists by name
describe aws_route_table('rtb-123abcde') do
it { should exist }
end
## Resource Parameters
### route\_table\_id
This resource expects a single parameter that uniquely identifies the Route Table. You may pass it as a string, or as the value in a hash:
describe aws_route_table('rtb-123abcde') do
it { should exist }
end
# Same
describe aws_route_table(route_table_id: 'rtb-123abcde') do
it { should exist }
end
## Matchers
For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exist
Indicates that the Route Table provided was found. Use should_not to test for Route Tables that should not exist.
describe aws_route_table('should-be-there') do
it { should exist }
end
describe aws_route_table('should-not-be-there') do
it { should_not exist }
end

View file

@ -0,0 +1,134 @@
---
title: About the aws_s3_bucket Resource
platform: aws
---
# aws\_s3\_bucket
Use the `aws_s3_bucket` InSpec audit resource to test properties of a single AWS bucket.
To test properties of a multiple S3 buckets, use the `aws_s3_buckets` resource.
<br>
## Limitations
S3 bucket security is a complex matter. For details on how AWS evaluates requests for access, please see [the AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/how-s3-evaluates-access-control.html). S3 buckets and the objects they contain support three different types of access control: bucket ACLs, bucket policies, and object ACLs.
As of January 2018, this resource supports evaluating bucket ACLs and bucket policies. We do not support evaluating object ACLs because it introduces scalability concerns in the AWS API; we recommend using AWS mechanisms such as CloudTrail and Config to detect insecure object ACLs.
In particular, users of the `be_public` matcher should carefully examine the conditions under which the matcher will detect an insecure bucket. See the `be_public` section under the Matchers section below.
<br>
## Syntax
An `aws_s3_bucket` resource block declares a bucket by name, and then lists tests to be performed.
describe aws_s3_bucket(bucket_name: 'test_bucket') do
it { should exist }
it { should_not be_public }
end
describe aws_s3_bucket('test_bucket') do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test the bucket-level ACL
describe aws_s3_bucket('test_bucket') do
its('bucket_acl.count') { should eq 1 }
end
### Check if a bucket has a bucket policy
describe aws_s3_bucket('test_bucket') do
its('bucket_policy') { should be_empty }
end
### Check if a bucket appears to be exposed to the public
# See Limitations section above
describe aws_s3_bucket('test_bucket') do
it { should_not be_public }
end
<br>
## Properties
### region
The `region` property identifies the AWS Region in which the S3 bucket is located.
describe aws_s3_bucket('test_bucket') do
# Check if the correct region is set
its('region') { should eq 'us-east-1' }
end
## Unsupported Properties
### bucket\_acl
The `bucket_acl` property is a low-level property that lists the individual Bucket ACL grants in effect on the bucket. Other higher-level properties, such as be\_public, are more concise and easier to use. You can use the `bucket_acl` property to investigate which grants are in effect, causing be\_public to fail.
The value of bucket_acl is an array of simple objects. Each object has a `permission` property and a `grantee` property. The `permission` property will be a string such as 'READ', 'WRITE' etc (See the [AWS documentation](https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html#get_bucket_acl-instance_method) for a full list). The `grantee` property contains sub-properties, such as `type` and `uri`.
bucket_acl = aws_s3_bucket('my-bucket')
# Look for grants to "AllUsers" (that is, the public)
all_users_grants = bucket_acl.select do |g|
g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/
end
# Look for grants to "AuthenticatedUsers" (that is, any authenticated AWS user - nearly public)
auth_grants = bucket_acl.select do |g|
g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/
end
### bucket\_policy
The `bucket_policy` is a low-level property that describes the IAM policy document controlling access to the bucket. The `bucket_policy` property returns a Ruby structure that you can probe to check for particular statements. We recommend using a higher-level property, such as `be_public`, which is concise and easier to implement in your policy files.
The `bucket_policy` property returns an array of simple objects, each object being an IAM Policy Statement. See the [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-2) for details about the structure of this data.
If there is no bucket policy, this property returns an empty array.
bucket_policy = aws_s3_bucket('my-bucket')
# Look for statements that allow the general public to do things
# This may be a false positive; it's possible these statements
# could be protected by conditions, such as IP restrictions.
public_statements = bucket_policy.select do |s|
s.effect == 'Allow' && s.principal == '*'
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### be\_public
The `be_public` matcher tests if the bucket has potentially insecure access controls. This high-level matcher detects several insecure conditions, which may be enhanced in the future. Currently, the matcher reports an insecure bucket if any of the following conditions are met:
1. A bucket ACL grant exists for the 'AllUsers' group
2. A bucket ACL grant exists for the 'AuthenticatedUsers' group
3. A bucket policy has an effect 'Allow' and principal '*'
Note: This resource does not detect insecure object ACLs.
it { should_not be_public }
### have\_access\_logging\_enabled
The `have_access_logging_enabled` matcher tests if access logging is enabled for the s3 bucket.
it { should have_access_logging_enabled }

View file

@ -0,0 +1,152 @@
---
title: About the aws_security_group Resource
---
# aws\_security\_group
Use the `aws_security_group` InSpec audit resource to test detailed properties of an individual Security Group (SG).
SGs are a networking construct which contain ingress and egress rules for network communications. SGs may be attached to EC2 instances, as well as certain other AWS resources. Along with Network Access Control Lists, SGs are one of the two main mechanisms of enforcing network-level security.
<br>
## Syntax
An `aws_security_group` resource block uses resource parameters to search for a Security Group and then tests that Security Group. If no SGs match, no error is raised, but the `exists` matcher returns `false` and all properties will be `nil`. If more than one SG matches (due to vague search parameters), an error is raised.
# Ensure you have a security group with a certain ID
# This is "safe" - SG IDs are unique within an account
describe aws_security_group('sg-12345678') do
it { should exist }
end
# Ensure you have a security group with a certain ID
# This uses hash syntax
describe aws_security_group(id: 'sg-12345678') do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_security_group`, its limited functionality precludes examples.
<br>
## Resource Parameters
This InSpec resource accepts the following parameters, which are used to search for the Security Group.
### id, group\_id
The Security Group ID of the Security Group. This is of the format `sg-` followed by 8 hexadecimal characters. The ID is unique within your AWS account; using ID ensures that you will never match more than one SG. The ID is also the default resource parameter, so you may omit the hash syntax.
# Using Hash syntax
describe aws_security_group(id: 'sg-12345678') do
it { should exist }
end
# group_id is an alias for id
describe aws_security_group(group_id: 'sg-12345678') do
it { should exist }
end
# Or omit hash syntax, rely on it being the default parameter
describe aws_security_group('sg-12345678') do
it { should exist }
end
### group\_name
The string name of the Security Group. Every VPC has a security group named 'default'. Names are unique within a VPC, but not within an AWS account.
# Get default security group for a certain VPC
describe aws_security_group(group_name: 'default', vpc_id: vpc_id: 'vpc-12345678') do
it { should exist }
end
# This will throw an error if there is a 'backend' SG in more than one VPC.
describe aws_security_group(group_name: 'backend') do
it { should exist }
end
### vpc\_id
A string identifying the VPC that contains the security group. Since VPCs commonly contain many SGs, you should add additional parameters to ensure you find exactly one SG.
# This will error if there is more than the default SG
describe aws_security_group(vpc_id: 'vpc-12345678') do
it { should exist }
end
<br>
## Properties
* `description`, `group_id', `group_name`, `vpc_id`
<br>
## Property Examples
### description
A String reflecting the human-meaningful description that was given to the SG at creation time.
# Require a description of a particular group
describe aws_security_group('sg-12345678') do
its('description') { should_not be_empty }
end
### group\_id
Provides the Security Group ID.
# Inspect the group ID of the default group
describe aws_security_group(group_name: 'default', vpc_id: vpc_id: 'vpc-12345678') do
its('group_id') { should cmp 'sg-12345678' }
end
# Store the group ID in a Ruby variable for use elsewhere
sg_id = aws_security_group(group_name: 'default', vpc_id: vpc_id: 'vpc-12345678').group_id
### group\_name
A String reflecting the name that was given to the SG at creation time.
# Inspect the group name of a particular group
describe aws_security_group('sg-12345678') do
its('group_name') { should cmp 'my_group' }
end
### vpc\_id
A String in the format 'vpc-' followed by 8 hexadecimal characters reflecting VPC that contains the security group.
# Inspec the VPC ID of a particular group
describe aws_security_group('sg-12345678') do
its('vpc_id') { should cmp 'vpc-12345678' }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if the specified SG was found. Use should_not if you want to verify that the specified SG does not exist.
# You will always have at least one SG, the VPC default SG
describe aws_security_group(group_name: 'default')
it { should exist }
end
# Make sure we don't have any security groups with the name 'nogood'
describe aws_security_group(group_name: 'nogood')
it { should_not exist }
end

View file

@ -0,0 +1,92 @@
---
title: About the aws_security_groups Resource
platform: aws
---
# aws\_security\_groups
Use the `aws_security_groups` InSpec audit resource to test properties of some or all security groups.
Security groups are a networking construct that contain ingress and egress rules for network communications. Security groups may be attached to EC2 instances, as well as certain other AWS resources. Along with Network Access Control Lists, Security Groups are one of the two main mechanisms of enforcing network-level security.
<br>
## Syntax
An `aws_security_groups` resource block uses an optional filter to select a group of security groups and then tests that group.
# Verify you have more than the default security group
describe aws_security_groups do
its('entries.count') { should be > 1 }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_security_groups`, its limited functionality precludes examples.
<br>
## Filter Criteria
### vpc\_id
A string identifying the VPC which contains the security group.
# Look for a particular security group in just one VPC
describe aws_security_groups.where( vpc_id: 'vpc-12345678') do
its('group_ids') { should include('sg-abcdef12')}
end
### group\_name
A string identifying a group. Since groups are contained in VPCs, group names are unique within the AWS account, but not across VPCs.
# Examine the default security group in all VPCs
describe aws_security_groups.where( group_name: 'default') do
it { should exist }
end
<br>
## Properties
* `entries`, `group\_ids`
<br>
## Property Examples
### entries
Provides access to the raw results of the query. This can be useful for checking counts and other advanced operations.
# Allow at most 100 security groups on the account
describe aws_security_groups do
its('entries.count') { should be <= 100}
end
### group\_ids
Provides a list of all security group IDs matched.
describe aws_security_groups do
its('group_ids') { should include('sg-12345678') }
end
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
# You will always have at least one SG, the VPC default SG
describe aws_security_groups
it { should exist }
end

View file

@ -0,0 +1,63 @@
---
title: About the aws_sns_topic Resource
---
# aws\_sns\_topic
Use the `aws_sns_topic` InSpec audit resource to test properties of a single AWS Simple Notification Service Topic. SNS topics are channels for related events. AWS resources place events in the SNS topic, while other AWS resources _subscribe_ to receive notifications when new events have appeared.
<br>
## Syntax
# Ensure that a topic exists and has at least one subscription
describe aws_sns_topic('arn:aws:sns:*::my-topic-name') do
it { should exist }
its('confirmed_subscription_count') { should_not be_zero }
end
# You may also use has syntax to pass the ARN
describe aws_sns_topic(arn: 'arn:aws:sns:*::my-topic-name') do
it { should exist }
end
## Resource Parameters
### ARN
This resource expects a single parameter that uniquely identifes the SNS Topic, an ARN. Amazon Resource Names for SNS topics have the format `arn:aws:sns:region:account-id:topicname`. AWS requires a fully-specified ARN for looking up an SNS topic. The account ID and region are required. Wildcards are not permitted.
See also the (AWS documentation on ARNs)[http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html].
<br>
## Properties
### confirmed\_subscription\_count
An integer indicating the number of currently active subscriptions.
# Make sure someone is listening
describe aws_sns_topic('arn:aws:sns:*::my-topic-name') do
its('confirmed_subscription_count') { should_not be_zero}
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exist
Indicates that the ARN provided was found. Use should_not to test for SNS topics that should not exist.
# Expect good news
describe aws_sns_topic('arn:aws:sns:*::good-news') do
it { should exist }
end
# No bad news allowed
describe aws_sns_topic('arn:aws:sns:*::bad-news') do
it { should_not exist }
end

View file

@ -0,0 +1,134 @@
---
title: About the aws_subnet Resource
platform: aws
---
# aws\_subnet
Use the `aws_subnet` InSpec audit resource to test properties of a vpc subnet.
To test properties of a single VPC subnet, use the `aws_subnet` resource.
To test properties of all or a group of VPC subnets, use the `aws_subnets` resource.
<br>
## Syntax
An `aws_subnet` resource block uses the parameter to select a VPC and a subnet in the VPC.
describe aws_subnet(subnet_id: 'subnet-1234567') do
it { should exist }
its('cidr_block') { should eq '10.0.1.0/24' }
end
<br>
## Resource Parameters
This InSpec resource accepts the following parameters, which are used to search for the VPCs subnet.
### subnet\_id
A string identifying the subnet that the VPC contains.
# This will error if there is more than the default SG
describe aws_subnet(subnet_id: 'subnet-12345678') do
it { should exist }
end
<br>
## Properties
* `availavailability_zone`, `available_ip_address_count`, `cidr_block`, `subnet_id`, `vpc_id`
<br>
## Property Examples
### availability\_zone
Provides the Availability Zone of the subnet.
describe aws_subnet(subnet_id: 'subnet-12345678') do
its('availability_zone') { should eq 'us-east-1c' }
end
### available\_ip\_address\_count
Provides the number of available IPv4 addresses on the subnet.
describe aws_subnet(subnet_id: 'subnet-12345678') do
its('available_ip_address_count') { should eq 251 }
end
### cidr\_block
Provides the block of ip addresses specified to the subnet.
describe aws_subnet(subnet_id: 'subnet-12345678') do
its('cidr_block') { should eq '10.0.1.0/24' }
end
### subnet\_id
Provides the ID of the Subnet.
describe aws_subnet(subnet_id: 'subnet-12345678') do
its('subnet_id') { should eq 'subnet-12345678' }
end
### vpc\_id
Provides the ID of the VPC the subnet is in.
describe aws_subnet(subnet_id: 'subnet-12345678') do
its('vpc_id') { should eq 'vpc-12345678' }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### assigning\_ipv\_6\_address\_on\_creation
Detects if the network interface on the subnet accepts IPv6 addresses.
describe aws_subnet(subnet_id: 'subnet-12345678') do
it { should be_assigning_ipv_6_address_on_creation }
end
### available
Provides the current state of the subnet.
describe aws_subnet(subnet_id: 'subnet-12345678') do
it { should be_available }
end
### default\_for\_az
Detects if the subnet is the default subnet for the Availability Zone.
describe aws_subnet(subnet_id: 'subnet-12345678') do
it { should be_default_for_az }
end
### exist
The `exist` matcher indicates that a subnet exists for the specified vpc.
describe aws_subnet(subnet_id: 'subnet-12345678') do
it { should exist }
end
### mapping\_public\_ip\_on\_launch
Provides the VPC ID for the subnet.
describe aws_subnet(subnet_id: 'subnet-12345678') do
it { should be_mapping_public_ip_on_launch }
end

View file

@ -0,0 +1,126 @@
---
title: About the aws_subnets Resource
platform: aws
---
# aws\_subnets
Use the `aws_subnets` InSpec audit resource to test properties of some or all subnets.
Subnets are networks within a VPC that can have their own block of IP address's and ACL's.
VPCs span across all availability zones in AWS, while a subnet in a VPC can only span a single availability zone.
Separating IP addresses allows for protection if there is a failure in one availability zone.
<br>
## Syntax
An `aws_subnets` resource block uses an optional filter to select a group of subnets and then tests that group.
# Test all subnets within a single vpc
describe aws_subnets.where(vpc_id: 'vpc-12345678') do
its('subnet_ids') { should include 'subnet-12345678' }
its('subnet_ids') { should include 'subnet-98765432' }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_subnets`, its limited functionality precludes examples.
<br>
## Filter Criteria
* `vpc_id`, `subnet_id`
## Filter Examples
### vpc\_id
A string identifying the VPC which may or may not contain subnets.
# Look for all subnts within a vpc.
describe aws_subnets.where( vpc_id: 'vpc-12345678') do
its('subnet_ids') { should include 'subnet-12345678' }
its('subnet_ids') { should include 'subnet-98765432' }
end
### subnet\_id
A string identifying a specific subnet.
# Examine a specific subnet
describe aws_subnets.where(subnet_id: 'subnet-12345678') do
its('cidr_blocks') { should eq ['10.0.1.0/24'] }
end
<br>
## Properties
* `cidr_blocks`, `states`, `subnet_ids`,`vpc_ids`
<br>
## Property Examples
### cidr\_blocks
Provides a string that contains the cidr block of ip addresses that can be given in the subnet.
# Examine a specific subnets cidr_blocks
describe aws_subnets.where( subnet_id: 'subnet-12345678') do
its('cidr_blocks') { should eq ['10.0.1.0/24'] }
end
### states
Provides an array of strings including if the subnets are available.
# Examine a specific vpcs Subnet IDs
describe aws_subnets.where( vpc_id: 'vpc-12345678') do
its('states') { should_not include 'pending' }
end
### subnet\_ids
Provides an array of strings containing the subnet IDs associated with a vpc.
# Examine a specific vpcs Subnet IDs
describe aws_subnets.where( vpc_id: 'vpc-12345678') do
its('subnet_ids') { should include 'subnet-12345678' }
its('subnet_ids') { should include 'subnet-98765432' }
end
### vpc\_ids
Provides an array containing a string of the vpc_id associated with a subnet.
# Examine a specific subnets VPC IDS
describe aws_subnets.where( subnet_id: 'subnet-12345678') do
its('vpc_ids') { should include 'vpc-12345678' }
end
<br>
## Matchers
For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
# You dont always have subnets, so you can test if there are any.
describe aws_subnets
it { should exist }
end
# Test that there are subnets in a vpc
describe aws_subnets.where(vpc_id: 'vpc-12345678')
it { should exist }
end

View file

@ -0,0 +1,120 @@
---
title: About the aws_vpc Resource
platform: aws
---
# aws\_vpc
Use the `aws_vpc` InSpec audit resource to test properties of a single AWS Virtual Private Cloud (VPC).
To test properties of all or multiple VPCs, use the `aws_vpcs` resource.
A VPC is a networking construct that provides an isolated environment. A VPC is contained in a geographic region, but spans availability zones in that region. A VPC may have multiple subnets, internet gateways, and other networking resources. Computing resources--such as EC2 instances--reside on subnets within the VPC.
Each VPC is uniquely identified by its VPC ID. In addition, each VPC has a non-unique CIDR IP Address range (such as 10.0.0.0/16) which it manages.
Every AWS account has at least one VPC, the "default" VPC, in every region.
<br>
## Syntax
An `aws_vpc` resource block identifies a VPC by id. If no VPC ID is provided, the default VPC is used.
# Find the default VPC
describe aws_vpc do
it { should exist }
end
# Find a VPC by ID
describe aws_vpc('vpc-12345678') do
it { should exist }
end
# Hash syntax for ID
describe aws_vpc(vpc_id: 'vpc-12345678') do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test that a VPC does not exist
describe aws_vpc('vpc-87654321') do
it { should_not exist }
end
### Test the CIDR of a named VPC
describe aws_vpc('vpc-87654321') do
its('cidr_block') { should cmp '10.0.0.0/16' }
end
<br>
## Properties
* `cidr_block`, `dhcp_options_id`, `state`, `vpc_id`, `instance_tenancy`
<br>
## Property Examples
### cidr\_block
The IPv4 address range that is managed by the VPC.
describe aws_vpc('vpc-87654321') do
its('cidr_block') { should cmp '10.0.0.0/16' }
end
### dhcp\_options\_id
The ID of the set of DHCP options associated with the VPC (or `default` if the default options are associated with the VPC).
describe aws_vpc do
its ('dhcp_options_id') { should eq 'dopt-a94671d0' }
end
### instance\_tenancy
The allowed tenancy of instances launched into the VPC.
describe aws_vpc do
its ('instance_tenancy') { should eq 'default' }
end
### state
The state of the VPC (`pending` | `available`).
describe aws_vpc do
its ('state') { should eq 'available' }
end
### vpc\_id
The ID of the VPC.
describe aws_vpc do
its('vpc_id') { should eq 'vpc-87654321' }
end
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### be\_default
The test will pass if the identified VPC is the default VPC for the region.
describe aws_vpc('vpc-87654321') do
it { should be_default }
end

View file

@ -0,0 +1,48 @@
---
title: About the aws_vpcs Resource
platform: aws
---
# aws\_vpcs
Use the `aws_vpcs` InSpec audit resource to test properties of some or all AWS Virtual Private Clouds (VPCs).
A VPC is a networking construct that provides an isolated environment. A VPC is contained in a geographic region, but spans availability zones in that region. A VPC may have multiple subnets, internet gateways, and other networking resources. Computing resources--such as EC2 instances--reside on subnets within the VPC.
Each VPC is uniquely identified by its VPC ID. In addition, each VPC has a non-unique CIDR IP Address range (such as 10.0.0.0/16) which it manages.
Every AWS account has at least one VPC, the "default" VPC, in every region.
<br>
## Syntax
An `aws_vpcs` resource block uses an optional filter to select a group of VPCs and then tests that group.
# The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
describe aws_vpcs do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_vpcs`, its limited functionality precludes examples.
<br>
## Matchers
This InSpec audit resource has the following special matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exists
The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches.
# You will always have at least one VPC
describe aws_vpcs
it { should exist }
end

View file

@ -0,0 +1,74 @@
---
title: About the azure_generic_resource Resource
---
# azure\_generic\_resource
Use the `azure_generic_resource` InSpec audit resource to test any valid Azure Resource. This is very useful if you need to test something that we do not yet have a specific Inspec resource for.
## Syntax
describe azure_generic_resource(group_name: 'MyResourceGroup', name: 'MyResource') do
its('property') { should eq 'value' }
end
where
* `MyResourceGroup` is the name of the resource group that contains the Azure Resource to be validated
* `MyResource` is the name of the resource that needs to be checked
* `property` - This generic resource dynamically creates the properties on the fly based on the type of resource that has been targetted.
* `value` is the expected output from the chosen property
The options that can be passed to the resource are as follows.
* `group_name`, the Azure resource group to be tested. Example: 'MyResourceGroup' (required)
* `name`, the name of the Azure resource to test. Example: 'MyVM'
* `type`, the Azure resource type. Example: 'Microsoft.Compute/virtualMachines'
* `apiversion`, the API Version to use when querying the resource. Defaults to the latest version for the resoure type is used. Example: 2017-10-9
These options can also be set using the environment variables:
- `AZURE_RESOURCE_GROUP_NAME`
- `AZURE_RESOURCE_NAME`
- `AZURE_RESOURCE_TYPE`
- `AZURE_RESOURCE_API_VERSION`
When the options have been set as well as the environment variables, the environment variables take priority.
There are _normally_ three standard tests that can be performed on a resource.
* `name`: tests the resource name
* `type`: tests the resource type
* `location`: tests the resource's location within Azure
## Example
describe azure_generic_resource(group_name: 'Inspec-Azure', name: 'Linux-Internal-VM') do
its('location') { should eq 'westeurope' }
end
## Properties
The properties that can be tested are entirely dependent on the Azure Resource that is under scrutiny. That means the properties vary. The best way to see what is available please use the [Azure Resources Portal](https://resources.azure.com) to select the resource you are interested in and see what can be tested.
This resource allows you to test _any_ valid Azure Resource. The trade off for this is that the language to check each item is not as natural as it would be for a native Inspec resource.
Please see the integration tests for in depth examples of how this resource can be used.
- [Generic External VM NIC](../test/integration/verify/controls/generic_external_vm_nic.rb)
- [Generic External VM](../test/integration/verify/controls/generic_external_vm.rb)
- [Generic Internal VM NIC](../test/integration/verify/controls/generic_internal_vm_nic.rb)
- [Generic Internal VM](../test/integration/verify/controls/generic_internal_vm.rb)
- [Generic Linux VM Managed OS Disk](../test/integration/verify/controls/generic_linux_vm_manmaged_osdisk.rb)
- [Generic Network Security Group](../test/integration/verify/controls/generic_network_security_group.rb)
- [Generic Public IP Address](../test/integration/verify/controls/generic_public_ip_address.rb)
- [Generic Resources](../test/integration/verify/controls/generic_resources.rb)
- [Generic Storage Account](../test/integration/verify/controls/generic_storage_account.rb)
- [Generic Virtual Network](../test/integration/verify/controls/generic_virtual_network.rb)
- [Generic Windows Internal VM NIC](../test/integration/verify/controls/generic_windows_internal_vm_nic.rb)
- [Generic Windows Internal VM](../test/integration/verify/controls/generic_windows_internal_vm.rb)
## Matchers
For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).

View file

@ -0,0 +1,296 @@
---
title: About the azure_resource_group_resource_counts Resource
---
# azure\_resource\_group\_resource\_counts
Use the `azure_resource_group_resource_counts` InSpec audit resource to check the number of Azure resources in a resource group
## Syntax
The name of the resource group is specified as a parameter on the resource:
describe azure_resource_group(name: 'MyResourceGroup') do
its('property') { should eq 'value' }
end
where
* `MyResourceGroup` is the name of the resource group being interrogated
* `property` is one a resource property
* `value` is the expected output from the matcher
The options that can be passed to the resource are as follows.
## Examples
The following examples show how to use this InSpec audit resource
Please refer the integration tests for more in depth examples:
- [Resource Group](../../test/integration/verify/controls/resource_group.rb)
### Test Resource Group has the correct number of resources
describe azure_resource_group_resource_counts(name: 'Inspec-Azure') do
its('total') { should eq 7}
end
### Ensure that the Resource Group contains the correct resources
describe azure_resource_group_resource_counts(name: 'Inspec-Azure') do
its('total') { should eq 7 }
its('vm_count') { should eq 2 }
its('nic_count') { should eq 2 }
its('public_ip_count') { should eq 1 }
its('sa_count') { should eq 1 }
its('vnet_count') { should eq 1 }
end
<br>
## Resource Parameters
The options that can be passed to the resource are as follows.
### `group_name` (required)
Use this parameter to define the Azure Resource Group to be tested.
example: MyResourceGroup
### name
Use this parameter to define the name of the Azure resource to test
example: MyVM
If both `group_name` and `name` is set then `name` will take priority
These options can also be set using the environment variables:
- `AZURE_RESOURCE_GROUP_NAME`
- `AZURE_RESOURCE_NAME`
When the options have been set as well as the environment variables, the environment variables take priority.
### Parameter Example
describe azure_resource_group_resource_counts(name: 'ChefAutomate') do
its('total') { should eq 7}
its('nic_count') { should eq 1 }
its('vm_count') { should eq 1 }
end
<br>
## Properties
* `name`, `location` ,`id`, `provisioning_state`, `subscription_id`, `total`, `nic_count`, `vm_count`, `extension_count`, `vnet_count`, `sa_count`, `public_ip_count`,`managed_disk_image_count`, `managed_disk_count`, `tag_count`
<br>
## Property Examples
This InSpec audit resource has the following properties:
### name
Returns the name of the resource group.
its(name) { should cmp 'nugget' }
### location
Returns where in Azure the resource group is located.
its(location) { should cmp 'us-west' }
### id
Returns the full qualified ID of the resource group.
This is in the format `/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>`.
its(id) { should cmp 'FQDN' }
### provisioning_state
The provisioning state of the resource group.
its(provisioning_state) { should cmp '????' }
### subscription_id
Returns the subscription ID which contains the resource group.
This is derived from the `id`.
its(subscription_id) { should cmp '????' }
### total
The total number of resources in the resource group
its(total) { should eq 5 }
### nic_count
The number of network interface cards in the resource group
its(nic_count) { should eq 2 }
### vm_count
The number of virtual machines in the resource group
its(vm_count) { should eq 5 }
### vnet_count
The number of virtual networks in the resource group
its(vnet_count) { should eq 5 }
### sa_count
The number of storage accounts in the resource group
its(sa_count) { should eq 5 }
### public_ip_count
The number of Public IP Addresses in the resource group
its(public_ip_count) { should eq 5 }
### managed_disk_image_count
The number of managed disk images that are in the resource group.
These are the items from which managed disks are created which are attached to machines. Generally the images are created from a base image or a custom image (e.g. Packer)
its(managed_disk_image_count) { should eq 5 }
### managed_disk_count
The number of managed disks in the resource group.
If a resource group contains one virtual machine with an OS disk and 2 data disks that are all Managed Disks, then the count would be 3.
its(managed_disk_count) { should eq 3 }
<br>
## Matchers
This resource has a number of `have_xxxx` matchers that provide a simple way to test of a specific Azure Resoure Type exists in the resource group.
### `have_nics`
Use this resource to test `
Microsoft.Network/networkInterfaces`
### `have_vms`
Use this resource to test `Microsoft.Compute/virtualMachines`
### `have_extensions`
Use this resource to test `Microsoft.Compute/virtualMachines/extensions``
### `have_nsgs`
Use this resource to test `Microsoft.Network/networkSecurityGroups`
### `have_vnets`
Use this resource to test `Microsoft.Network/virtualNetworks`
### `have_managed_disks`
Use this resource to test `Microsoft.Compute/disks`
### `have_managed_disk_images`
Use this resource to test `Microsoft.Compute/images`
### `have_sas`
Use this resource to test `Microsoft.Storage/storageAccounts`
### `have_public_ips`
Use this resource to test `Microsoft.Network/publicIPAddresses`
With these methods the following tests are possible
it { should have_nics }
it { should_not have_extensions }
## Tags
It is possible to test the tags that have been assigned to the resource. There are a number of properties that can be called to check that it has tags, that it has the correct number and that the correct ones are assigned.
### have\_tags
This is a simple test to see if the machine has tags assigned to it or not.
it { should have_tags }
### tag\_count
Returns the number of tags that are assigned to the resource
its ('tag_count') { should eq 2 }
### tags
It is possible to check if a specific tag has been set on the resource.
its('tags') { should include 'Owner' }
### xxx\_tag
To get the value of the tag, a number of preoprties have been created from the tags that are set.
For example, if the following tag is set on a resource:
| Tag Name | Value |
|----------|-------|
| Owner | Russell Seymour |
Then a property is available called `Owner_tag`.
its('Owner_tag') { should cmp 'Russell Seymour' }
Note: The tag name is case sensitive which makes the test case sensitive. E.g. `owner_tag` does not equal `Owner_tag`.
## Examples
The following examples show how to use this InSpec audit resource
Please refer the integration tests for more in depth examples:
- [Resource Group](../../test/integration/verify/controls/resource_group.rb)
### Test Resource Group has the correct number of resources
describe azure_resource_group_resource_counts(name: 'Inspec-Azure') do
its('total') { should eq 7}
end
### Ensure that the Resource Group contains the correct resources
describe azure_resource_group_resource_counts(name: 'Inspec-Azure') do
its('total') { should eq 7 }
its('vm_count') { should eq 2 }
its('nic_count') { should eq 2 }
its('public_ip_count') { should eq 1 }
its('sa_count') { should eq 1 }
its('vnet_count') { should eq 1 }
end
## References
- [Azure Ruby SDK - Resources](https://github.com/Azure/azure-sdk-for-ruby/tree/master/management/azure_mgmt_resources)

View file

@ -0,0 +1,314 @@
---
title: About the azure_virtual_machine Resource
---
# azure\_virtual\_machine
Use the `azure_virtual_machine` InSpec audit resource to ensure that a Virtual Machine has been provisionned correctly.
## Syntax
The name of the machine and the resourece group are required as properties to the resource.
describe azure_virtual_machine(group_name: 'MyResourceGroup', name: 'MyVM') do
its('property') { should eq 'value' }
end
where
* `MyVm` is the name of the virtual machine as seen in Azure. (It is **not** the hostname of the machine)
* `MyResourceGroup` is the name of the resource group that the machine is in.
* `property` is one of
* `value` is the expected output from the matcher
The options that can be passed to the resource are as follows.
| Name | Description | Required | Example |
|-------------|---------------------------------------------------------------------------------------------------------------------|----------|-----------------------------------|
| group_name: | Azure Resource Group to be tested | yes | MyResourceGroup |
| name: | Name of the Azure resource to test | no | MyVM |
| apiversion: | API Version to use when interrogating the resource. If not set then the latest version for the resoure type is used | no | 2017-10-9 |
These options can also be set using the environment variables:
- `AZURE_RESOURCE_GROUP_NAME`
- `AZURE_RESOURCE_NAME`
- `AZURE_RESOURCE_API_VERSION`
When the options have been set as well as the environment variables, the environment variables take priority.
For example:
describe azure_virtual_machine(group_name: 'Inspec-Azure', name: 'Linux-Internal-VM') do
its('os_type') { should eq 'Linux' }
it { should have_boot_diagnostics }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
Please refer the integration tests for more in depth examples:
- [Virtual Machine External VM](../../test/integration/verify/controls/virtual_machine_external_vm.rb)
- [Virtual Machine Internal VM](../../test/integration/verify/controls/virtual_machine_internal_vm.rb)
### Test that the machine was built from a Windows image
describe azure_virtual_machine(name: 'Windows-Internal-VM', group_name: 'Inspec-Azure') do
its('publisher') { should eq 'MicrosoftWindowsServer' }
its('offer') { should eq 'WindowsServer' }
its('sku') { should eq '2012-R2-Datacenter' }
end
### Ensure the machine is in the correct location
describe azure_virtual_machine(name: 'Linux-Internal-VM', resource_group: 'Inspec-Azure') do
its('location') { should eq 'westeurope' }
end
<br>
## Properties
* [`type`](#type), [`location`](#location), [`name`](#name), [`publisher`](#publisher), [`offer`](#offer), [`sku`](#sku), [`os_type`](#"os_type"), [`os_disk_name`](#os_disk_name), [`have_managed_osdisk`](#have_managed_osdisk?), [`caching`](#caching), `create_option`, `disk_size_gb`, `have_data_disks`, `data_disk_count` , `storage_account_type`, `vm_size`, `computer_name`, `admin_username`, `have_nics`, `nic_count`, `connected_nics`, `have_password_authentication`, `password_authentication?`, `have_custom_data`, `custom_data?`, `have_ssh_keys`, `ssh_keys?`, `ssh_key_count`, `ssh_keys`, `have_boot_diagnostics`, `boot_diagnostics_storage_uri`
<br>
## Property Examples
This InSpec audit resource has the following properties that can be tested:
### type
THe Azure Resource type. For a virtual machine this will always return `Microsoft.Compute/virtualMachines`
### location
Where the machine is located
its('location') { should eq 'westeurope' }
### name
Name of the Virtual Machine in Azure. Be aware that this is not the computer name or hostname, rather the name of the machine when seen in the Azure Portal.
### publisher
The publisher of the image from which this machine was built.
This will be `nil` if the machine was created from a custom image.
### offer
The offer from the publisher of the build image.
This will be `nil` if the machine was created from a custom image.
### sku
The item from the publisher that was used to create the image.
This will be `nil` if the machine was created from a custom image.
### os\_type
Test that returns the classification in Azure of the operating system type. Ostensibly this will be either `Linux` or `Windows`.
### os\_disk\_name
Return the name of the operating system disk attached to the machine.
### have\_managed\_osdisk
Determine if the operating system disk is a Managed Disks or not.
This test can be used in the following way:
it { should have_managed_osdisk }
### caching
Returns the type of caching that has been set on the operating system disk.
### create\_option
When the operating system disk is created, how it was created is set as an property. This property returns how the disk was created.
### disk\_size\_gb
Returns the size of the operating system disk.
### have\_data\_disks
Denotes if the machine has data disks attached to it or not.
it { should have_data_disks }
### data\_disk\_count
Return the number of data disks that are attached to the machine
### storage\_account\_type
This provides the storage account type for a machine that is using managed disks for the operating system disk.
### vm\_size
The size of the machine in Azure
its('vm_size') { should eq 'Standard_DS2_v2' }
### computer\_name
The name of the machine. This is what was assigned to the machine during deployment and is what _should_ be returned by the `hostname` command.
### admin\_username
The admin username that was assigned to the machine
NOTE: Azure does not allow the use of `Administrator` as the admin username on a Windows machine
## have\_nics
Returns a boolean to state if the machine has NICs connected or not.
This has can be used in the following way:
it { should have_nics }
### nic\_count
The number of network interface cards that have been attached to the machine
### connected\_nics
This returns an array of the NIC ids that are connected to the machine. This means that it possible to check that the machine has the correct NIC(s) attached and thus on the correct subnet.
its('connected_nics') { should include /Inspec-NIC-1/ }
Note the use of the regular expression here. This is because the NIC id is a long string that contains the subscription id, resource group, machine id as well as other things. By using the regular expression the NIC can be checked withouth breaking this string up. It also means that other tests can be performed.
An example of the id string is `/subscriptions/1e0b427a-d58b-494e-ae4f-ee558463ebbf/resourceGroups/Inspec-Azure/providers/Microsoft.Network/networkInterfaces/Inspec-NIC-1`
### have\_password\_authentication
Returns a boolean to denote if the machine is accessible using a password.
it { should have_password_authentication }
### password\_authentication?
Boolean to state of password authentication is enabled or not for the admin user.
its('password_authentication?') { should be false }
This only applies to Linux machines and will always return `true` on Windows.
### have\_custom\_data
Returns a boolean stating if the machine has custom data assigned to it.
it { should have_custom_data }
### custom\_data?
Boolean to state if the machine has custom data or not
its('custom_data') { should be true }
### have\_ssh\_keys
Boolean to state if the machine has SSH keys assigned to it
it { should have_ssh_keys }
For a Windows machine this will always be false.
### ssh\_keys?
Boolean to state of the machine is accessible using SSH keys
its('ssh_keys?') { should be true }
### ssh\_key\_count
Returns how many SSH keys have been applied to the machine.
This only applies to Linux machines and will always return `0` on Windows.
### ssh\_keys
Returns an array of the keys that are assigned to the machine. This is check if the correct keys are assigned.
Most SSH public keys have a signature at the end of them that can be tested. For example:
its('ssh_keys') { should include /azure@inspec.local/ }
### boot\_diagnostics?
Boolean test to see if boot diagnostics have been enabled on the machine
it { should have_boot_diagnostics }
### boot\_diagnostics\_storage\_uri
If boot diagnostics are enabled for the machine they will be saved in a storage account. This method returns the URI for the storage account.
its('boot_diagnostics_storage_uri') { should match 'ghjgjhgjg' }
<br>
## Matchers
There are a number of built in comparison operrtors that are available to test the result with an expected value.
For information on all that are available please refer to the [Inspec Matchers Reference](https://www.inspec.io/docs/reference/matchers/) page.
<br>
## Tags
It is possible to test the tags that have been assigned to the resource. There are a number of properties that can be called to check that it has tags, that it has the correct number and that the correct ones are assigned.
### have\_tags
This is a simple test to see if the machine has tags assigned to it or not.
it { should have_tags }
### tag\_count
Returns the number of tags that are assigned to the resource
its ('tag_count') { should eq 2 }
### tags
It is possible to check if a specific tag has been set on the resource.
its('tags') { should include 'Owner' }
### xxx\_tag
To get the value of the tag, a number of tests have been craeted from the tags that are set.
For example, if the following tag is set on a resource:
| Tag Name | Value |
|----------|-------|
| Owner | Russell Seymour |
Then a test is available called `Owner_tag`.
its('Owner_tag') { should cmp 'Russell Seymour' }
Note: The tag name is case sensitive which makes the test case sensitive. E.g. `owner_tag` does not equal `Owner_tag`.
## References
- [Azure Ruby SDK - Resources](https://github.com/Azure/azure-sdk-for-ruby/tree/master/management/azure_mgmt_resources)

View file

@ -0,0 +1,182 @@
---
title: About the azure_virtual_machine_datadisk Resource
---
# azure\_virtual\_machine\_datadisk
Use this resource to ensure that a specific data disk attached to a machine has been created properly.
## References
- [Azure Ruby SDK - Compute](https://github.com/Azure/azure-sdk-for-ruby/tree/master/management/azure_mgmt_compute)
## Syntax
The name of the resource group and machine are required to use this resource.
describe azure_virtual_machine_data_disk(group_name: 'MyResourceGroup', name: 'MyVM') do
its('property') { should eq 'value' }
end
where
* `MyVm` is the name of the virtual machine as seen in Azure. (It is **not** the hostname of the machine)
* `MyResourceGroup` is the name of the resouce group that the machine is in.
* `property` is a resource property
* `value` is the expected output fdrom the matcher
The `count`, `have_data_disks` and `have_managed_disks` are catchall tests that give information about the virtual machine. The specific tests need to be used in conjunction with the `where` option as shown below.
| Name | Description | Required | Example |
|-------------|---------------------------------------------------------------------------------------------------------------------|----------|-----------------------------------|
| group_name: | Azure Resource Group to be tested | yes | MyResourceGroup |
| name: | Name of the Azure resource to test | no | MyVM |
| apiversion: | API Version to use when interrogating the resource. If not set then the latest version for the resoure type is used | no | 2017-10-9 |
These options can also be set using the environment variables:
- `AZURE_RESOURCE_GROUP_NAME`
- `AZURE_RESOURCE_NAME`
- `AZURE_RESOURCE_API_VERSION`
When the options have been set as well as the environment variables, the environment variables take priority.
For example:
describe azure_virtual_machine_data_disk(group_name: 'Inspec-Azure', name: 'Linux-Internal-VM') do
its('count') { should cmp > 0 }
it { should have_data_disks }
end
<br>
## Examples
The following examples show to use this InSpec audit resource.
Please refer to the following integration tests for more in depth examples:
- [Linux Internal Data Disks](../../test/integration/verify/controls/virtual_machine_linux_external_vm_datadisk.rb)
- [Windows Internal Data Disk](../../test/integration/verify/controls/virtual_machine_windows_internal_vm_datadisk.rb)
### Check that the first data disk is of the correct size
describe azure_virtual_machine_data_disk(group_name: 'Inspec-Azure', name: 'Linux-Internal-VM').where(number: 1) do
its('size') { should cmp >= 15 }
end
<br>
## Properties
* `count`, `have_data_disks`, `have_managed_disks`, `disk`, `number`, `name`, `size`, `lun`
, `caching`, `create_option`, `is_managed_disk?`, `vhd_uri`, `storage_account_name`, `storage_account_type`, `id`, `subscription_id`, `resource_group`
<br>
## Property Examples
### count
Returns the number of data disks attached to the machine
its('count') { should eq 1 }
### have\_data\_disks
Returns a boolean denoting if any data disks are attached to the machine
it { should have_data_disks }
### have\_managed\_disks
Returns a boolean stating if the machine has Managed Disks for data disks.
it { should have_managed_disks }
**The next set of attributes require the `where` operation to be used on the describe.**
The following code shows an example of how to use the where clause.
describe azure_virtual_machine_data_disk(group_name: 'Inspec-Azure', name: 'Windows-Internal-VM').where(number: 1)
end
### disk
The zero based index of the disk attached to the machine.
Typically used in the `where` clause
### number
The '1' based index of the disk attached to the machine.
Typically used in the `where` clause as showm above.
### name
Returns a string of the name of the disk.
### size
Returns an integer of size of this disk in GB.
### lun
The disk number as reported by Azure. This is a zero based index value.
### caching
String stating the caching that has been set on the disk.
### create\_option
How the disk was created. Typically for data disks this will be the string value 'Empty'.
### is\_managed\_disk?
Boolean stating if the disk is a managed disk or not. If it is not a managed disk then it is one that is stored in a Storage Account.
### vhd\_uri
If this _not_ a managed disk then the `vhd_uri` will be the full URI to the disk in the storage account.
### storage\_account\_name
If this is _not_ a managed disk this will be the storage account name in which the disk is stored.
This derived from the `vhd_uri`.
### storage\_account\_type
If this is a managed disk this is the storage account type, e.g. `Standard_LRS`
### id
If this is a managed disk then this is the fully qualified id to the disk in Azure.
### subscription\_id
If this is a managed disk, this returns the subscription id of where the disk is stored.
This is derived from the `id`.
### resource\_group
If this is a managed disk, this returns the resource group in which the disk is stored.
This is derived from the `id`.
<br>
## Matchers
This InSpec audit resource has the following matchers:
### eq
Use the `eq` matcher to test the equality of two values: `its('Port') { should eq '22' }`.
Using `its('Port') { should eq 22 }` will fail because `22` is not a string value! Use the `cmp` matcher for less restrictive value comparisons.
**The following properties are ones that are applied to the virtual machine itself and not specfic disks**

View file

@ -6,13 +6,6 @@ title: About the http Resource
Use the `http` InSpec audit resource to test an http endpoint.
<p class="warning">In InSpec 1.40 and earlier, this resource always executes on the host on which <code>inspec exec</code> is run, even if you use the <code>--target</code> option to remotely scan a different host.<br>
<br>
Beginning with InSpec 1.41, you can enable remote targeting for the HTTP test, provided <code>curl</code> is available. See the "Local vs. Remote" section below.<br>
<br>
Executing the HTTP test on the remote target will be the default behavior in InSpec 2.0.
</p>
<br>
## Syntax
@ -38,15 +31,6 @@ where
* `ssl_verify` may be specified to enable or disable verification of SSL certificates (default to `true`)
<br>
## Local vs. Remote
Beginning with InSpec 1.41, you can enable the ability to have the HTTP test execute on the remote target:
describe http('http://www.example.com', enable_remote_worker: true) do
its('body') { should cmp 'awesome' }
end
In InSpec 2.0, the HTTP test will automatically execute remotely whenever InSpec is testing a remote node.
<br>
## Properties

View file

@ -4,7 +4,7 @@ title: About the windows_task Resource
# windows_task
Use the `windows_task` Inspec audit resource to test a scheduled tasks configuration on a Windows platform.
Use the `windows_task` InSpec audit resource to test a scheduled tasks configuration on a Windows platform.
Microsoft and application vendors use scheduled tasks to perform a variety of system maintaince tasks but system administrators can schedule their own.
<br>

View file

@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = '>= 2.3'
spec.add_dependency 'train', '~> 0.32'
spec.add_dependency 'train', '~> 1.1'
spec.add_dependency 'thor', '~> 0.19'
spec.add_dependency 'json', '>= 1.8', '< 3.0'
spec.add_dependency 'method_source', '~> 0.8'

View file

@ -46,38 +46,6 @@ module Compliance
Compliance::API.login(options)
end
desc "login_automate https://SERVER --insecure --user='USER' --ent='ENTERPRISE' --usertoken='TOKEN'", 'Log in to a Chef Automate SERVER (DEPRECATED: Please use `login`)'
long_desc <<-LONGDESC
This commmand is deprecated and will be removed, please use `--login`.
`login_automate` allows you to use InSpec with Chef Automate.
You need to a token for communication. More information about token retrieval
is available at:
https://docs.chef.io/api_automate.html#authentication-methods
https://docs.chef.io/api_compliance.html#obtaining-an-api-token
LONGDESC
option :insecure, aliases: :k, type: :boolean,
desc: 'Explicitly allows InSpec to perform "insecure" SSL connections and transfers'
option :user, type: :string, required: true,
desc: 'Username'
option :usertoken, type: :string, required: false,
desc: 'Access token (DEPRECATED: Please use `--token`)'
option :token, type: :string, required: false,
desc: 'Access token'
option :dctoken, type: :string, required: false,
desc: 'Data Collector token'
option :ent, type: :string, required: true,
desc: 'Enterprise for Chef Automate reporting'
def login_automate(server)
warn '[DEPRECATION] `inspec compliance login_automate` is deprecated. Please use `inspec compliance login`'
options['server'] = server
options['token'] = options['usertoken'] if options['usertoken']
Compliance::API.login(options)
end
desc 'profiles', 'list all available profiles in Chef Compliance'
option :owner, type: :string, required: false,
desc: 'owner whose profiles to list'

View file

@ -66,8 +66,6 @@ module Inspec
desc: 'Use colors in output.'
option :attrs, type: :array,
desc: 'Load attributes file (experimental)'
option :cache, type: :string,
desc: '[DEPRECATED] Please use --vendor-cache - this will be removed in InSpec 2.0'
option :vendor_cache, type: :string,
desc: 'Use the given path for caching dependencies. (default: ~/.inspec/cache)'
option :create_lockfile, type: :boolean,

View file

@ -162,12 +162,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI
diagnose(o)
configure_logger(o)
# TODO: REMOVE for inspec 2.0
if o.key?('cache')
o[:vendor_cache] = o[:cache]
o[:logger].warn '[DEPRECATED] The use of `--cache` is being deprecated in InSpec 2.0. Please use `--vendor-cache` instead.'
end
runner = Inspec::Runner.new(o)
targets.each { |target| runner.add_target(target) }
@ -184,17 +178,27 @@ class Inspec::InspecCLI < Inspec::BaseCLI
option :format, type: :string
def detect
o = opts(:detect).dup
o[:command] = 'os.params'
o[:command] = 'platform.params'
(_, res) = run_command(o)
if o['format'] == 'json'
puts res.to_json
else
headline('Operating System Details')
%w{name family release arch}.each { |item|
puts format('%-10s %s', item.to_s.capitalize + ':',
mark_text(res[item.to_sym]))
headline('Platform Details')
%w{name families release arch}.each { |item|
data = res[item.to_sym]
# Format Array for better output if applicable
data = data.join(', ') if data.is_a?(Array)
# Do not output fields of data is missing ('unknown' is fine)
next if data.nil?
puts format('%-10s %s', item.to_s.capitalize + ':', mark_text(data))
}
end
rescue ArgumentError, RuntimeError, Train::UserError => e
$stderr.puts e.message
exit 1
rescue StandardError => e
pretty_handle_exception(e)
end

View file

@ -23,6 +23,7 @@ module Inspec::Formatters
run_data[:version] = Inspec::VERSION
run_data[:statistics] = {
duration: summary.duration,
controls: statistics,
}
end
@ -84,6 +85,47 @@ module Inspec::Formatters
private
def all_unique_controls
unique_controls = Set.new
run_data[:profiles].each do |profile|
profile[:controls].map { |control| unique_controls.add(control) }
end
unique_controls
end
def statistics
failed = 0
skipped = 0
passed = 0
all_unique_controls.each do |control|
next unless control[:results]
if control[:results].any? { |r| r[:status] == 'failed' }
failed += 1
elsif control[:results].any? { |r| r[:status] == 'skipped' }
skipped += 1
else
passed += 1
end
end
total = failed + passed + skipped
{
total: total,
passed: {
total: passed,
},
skipped: {
total: skipped,
},
failed: {
total: failed,
},
}
end
def exception_message(exception)
if exception.is_a?(RSpec::Core::MultipleExceptionError)
exception.all_exceptions.map(&:message).uniq.join("\n\n")
@ -111,7 +153,7 @@ module Inspec::Formatters
status: example.execution_result.status.to_s,
code_desc: code_description,
run_time: example.execution_result.run_time,
start_time: example.execution_result.started_at.to_s,
start_time: example.execution_result.started_at.to_datetime.rfc3339.to_s,
resource_title: example.metadata[:described_class] || example.metadata[:example_group][:description],
expectation_message: format_expectation_message(example),
}

View file

@ -54,15 +54,9 @@ module Inspec
end
def inspec_requirement
inspec_in_supports = params[:supports].find { |x| !x[:inspec].nil? }
if inspec_in_supports
warn '[DEPRECATED] The use of inspec.yml `supports:inspec` is deprecated and will be removed in InSpec 2.0. Please use `inspec_version` instead.'
Gem::Requirement.create(inspec_in_supports[:inspec])
else
# using Gem::Requirement here to allow nil values which
# translate to [">= 0"]
Gem::Requirement.create(params[:inspec_version])
end
# using Gem::Requirement here to allow nil values which
# translate to [">= 0"]
Gem::Requirement.create(params[:inspec_version])
end
def supports_runtime?

View file

@ -43,8 +43,8 @@ module Inspec
Inspec::Resource.registry
end
def __register(name, obj) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
cl = Class.new(obj) do # rubocop:disable Metrics/BlockLength
def __register(name, obj) # rubocop:disable Metrics/MethodLength
cl = Class.new(obj) do
attr_reader :resource_exception_message
def initialize(backend, name, *args)
@ -94,13 +94,6 @@ module Inspec
@resource_skipped
end
def resource_skipped
warn('[DEPRECATION] Use `resource_exception_message` for the resource skipped message. This method will be removed in InSpec 2.0.')
# Returning `nil` here to match previous behavior
return nil if @resource_skipped == false
@resource_exception_message
end
def fail_resource(message)
@resource_failed = true
@resource_exception_message = message

View file

@ -7,9 +7,6 @@ module Inspec::Reporters
# Most currently available Windows terminals have poor support
# for ANSI extended colors
COLORS = {
'critical' => "\033[0;1;31m",
'major' => "\033[0;1;31m",
'minor' => "\033[0;36m",
'failed' => "\033[0;1;31m",
'passed' => "\033[0;1;32m",
'skipped' => "\033[0;37m",
@ -19,9 +16,6 @@ module Inspec::Reporters
# Most currently available Windows terminals have poor support
# for UTF-8 characters so use these boring indicators
INDICATORS = {
'critical' => '[CRIT]',
'major' => '[MAJR]',
'minor' => '[MINR]',
'failed' => '[FAIL]',
'skipped' => '[SKIP]',
'passed' => '[PASS]',
@ -30,9 +24,6 @@ module Inspec::Reporters
else
# Extended colors for everyone else
COLORS = {
'critical' => "\033[38;5;9m",
'major' => "\033[38;5;208m",
'minor' => "\033[0;36m",
'failed' => "\033[38;5;9m",
'passed' => "\033[38;5;41m",
'skipped' => "\033[38;5;247m",
@ -42,9 +33,6 @@ module Inspec::Reporters
# Groovy UTF-8 characters for everyone else...
# ...even though they probably only work on Mac
INDICATORS = {
'critical' => '×',
'major' => '∅',
'minor' => '⊚',
'failed' => '×',
'skipped' => '↺',
'passed' => '✔',
@ -177,27 +165,15 @@ module Inspec::Reporters
end
def profile_summary
return @profile_summary unless @profile_summary.nil?
failed = 0
skipped = 0
passed = 0
critical = 0
major = 0
minor = 0
all_unique_controls.each do |control|
next if control[:id].start_with? '(generated from '
next unless control[:results]
if control[:results].any? { |r| r[:status] == 'failed' }
failed += 1
if control[:impact] >= 0.7
critical += 1
elsif control[:impact] >= 0.4
major += 1
else
minor += 1
end
elsif control[:results].any? { |r| r[:status] == 'skipped' }
skipped += 1
else
@ -207,22 +183,15 @@ module Inspec::Reporters
total = failed + passed + skipped
@profile_summary = {
{
'total' => total,
'failed' => {
'total' => failed,
'critical' => critical,
'major' => major,
'minor' => minor,
},
'failed' => failed,
'skipped' => skipped,
'passed' => passed,
}
end
def tests_summary
return @tests_summary unless @tests_summary.nil?
total = 0
failed = 0
skipped = 0
@ -241,7 +210,12 @@ module Inspec::Reporters
end
end
@tests_summary = { 'total' => total, 'failed' => failed, 'skipped' => skipped, 'passed' => passed }
{
'total' => total,
'failed' => failed,
'skipped' => skipped,
'passed' => passed,
}
end
def print_profile_summary
@ -249,11 +223,11 @@ module Inspec::Reporters
return unless summary['total'] > 0
success_str = summary['passed'] == 1 ? '1 successful control' : "#{summary['passed']} successful controls"
failed_str = summary['failed']['total'] == 1 ? '1 control failure' : "#{summary['failed']['total']} control failures"
failed_str = summary['failed'] == 1 ? '1 control failure' : "#{summary['failed']} control failures"
skipped_str = summary['skipped'] == 1 ? '1 control skipped' : "#{summary['skipped']} controls skipped"
success_color = summary['passed'] > 0 ? 'passed' : 'no_color'
failed_color = summary['failed']['total'] > 0 ? 'failed' : 'no_color'
failed_color = summary['failed'] > 0 ? 'failed' : 'no_color'
skipped_color = summary['skipped'] > 0 ? 'skipped' : 'no_color'
s = format(
@ -301,11 +275,6 @@ module Inspec::Reporters
end
class Control
IMPACT_SCORES = {
critical: 0.7,
major: 0.4,
}.freeze
attr_reader :data
def initialize(control_hash)
@ -358,12 +327,8 @@ module Inspec::Reporters
'skipped'
elsif results.nil? || results.empty? || results.all? { |r| r[:status] == 'passed' }
'passed'
elsif impact >= IMPACT_SCORES[:critical]
'critical'
elsif impact >= IMPACT_SCORES[:major]
'major'
else
'minor'
'failed'
end
end
@ -374,12 +339,8 @@ module Inspec::Reporters
'passed'
elsif impact.nil?
'unknown'
elsif impact >= IMPACT_SCORES[:critical]
'critical'
elsif impact >= IMPACT_SCORES[:major]
'major'
else
'minor'
'failed'
end
end

View file

@ -12,10 +12,10 @@ module Inspec::Reporters
{
platform: platform,
profiles: profiles,
statistics: { duration: run_data[:statistics][:duration] },
statistics: {
duration: run_data[:statistics][:duration],
},
version: run_data[:version],
controls: controls,
other_checks: run_data[:other_checks],
}
end
@ -28,28 +28,6 @@ module Inspec::Reporters
}
end
def controls
controls = []
return controls if run_data[:controls].nil?
run_data[:controls].each do |c|
control = {
status: c[:status],
start_time: c[:start_time],
run_time: c[:run_time],
code_desc: c[:code_desc],
}
control[:resource] = c[:resource] if c[:resource]
control[:skip_message] = c[:skip_message] if c[:skip_message]
control[:message] = c[:message] if c[:message]
control[:exception] = c[:exception] if c[:exception]
control[:backtrace] = c[:backtrace] if c[:backtrace]
controls << control
end
controls
end
def profile_results(control)
results = []
return results if control[:results].nil?

View file

@ -74,6 +74,17 @@ module Inspec
end
end
# Many resources use FilterTable.
require 'utils/filter'
# AWS resources are included via their own file.
require 'resource_support/aws'
require 'resources/azure/azure_backend.rb'
require 'resources/azure/azure_generic_resource.rb'
require 'resources/azure/azure_resource_group.rb'
require 'resources/azure/azure_virtual_machine.rb'
require 'resources/azure/azure_virtual_machine_data_disk.rb'
require 'resources/aide_conf'
require 'resources/apache'
require 'resources/apache_conf'
@ -81,7 +92,6 @@ require 'resources/apt'
require 'resources/audit_policy'
require 'resources/auditd'
require 'resources/auditd_conf'
require 'resources/auditd_rules'
require 'resources/bash'
require 'resources/bond'
require 'resources/bridge'

View file

@ -129,10 +129,9 @@ module Inspec
end
def run_tests(with = nil)
status, @run_data = @test_collector.run(with)
# dont output anything if we want a report
render_output(@run_data) unless @conf['report']
status
run_data = @test_collector.run(with)
render_output(run_data)
@test_collector.exit_code
end
# determine all attributes before the execution, fetch data from secrets backend

View file

@ -74,8 +74,24 @@ module Inspec
# @return [int] 0 if all went well; otherwise nonzero
def run(with = nil)
with ||= RSpec::Core::Runner.new(nil)
status = with.run_specs(tests)
[status, @formatter.run_data]
@rspec_exit_code = with.run_specs(tests)
@formatter.results
end
# Return a proper exit code to the runner
#
# @return [int] exit code
def exit_code
stats = @formatter.results[:statistics][:controls]
if stats[:failed][:total] == 0 && stats[:skipped][:total] == 0
0
elsif stats[:failed][:total] > 0
100
elsif stats[:skipped][:total] > 0
101
else
@rspec_exit_code
end
end
# Empty the list of registered tests.

View file

@ -8,6 +8,31 @@ module Inspec
'additionalProperties' => false,
'properties' => {
'duration' => { 'type' => 'number' },
'controls' => {
'type' => 'object',
'optional' => true,
'properties' => {
'total' => { 'type' => 'number' },
'passed' => {
'type' => 'object',
'properties' => {
'total' => { 'type' => 'number' },
},
},
'skipped' => {
'type' => 'object',
'properties' => {
'total' => { 'type' => 'number' },
},
},
'failed' => {
'type' => 'object',
'properties' => {
'total' => { 'type' => 'number' },
},
},
},
},
},
}.freeze
@ -137,10 +162,6 @@ module Inspec
},
'statistics' => STATISTICS,
'version' => { 'type' => 'string' },
# DEPRECATED PROPERTIES!! These will be removed with the next major version bump
'controls' => 'array',
'other_checks' => 'array',
},
}.freeze

View file

@ -66,25 +66,6 @@ RSpec::Matchers.define :be_executable do
end
end
# matcher to check /etc/passwd, /etc/shadow and /etc/group
RSpec::Matchers.define :contain_legacy_plus do
match do |file|
warn '[DEPRECATION] `contain_legacy_plus` is deprecated and will be removed in the next major version. Please use `describe file(\'/etc/passwd\') do its(\'content\') { should_not match /^\+:/ } end`'
file.content =~ /^\+:/
end
end
# verifies that no entry in an array contains a value
RSpec::Matchers.define :contain_match do |regex|
match do |arr|
warn '[DEPRECATION] `contain_match` is deprecated and will be removed in the next major version. See https://github.com/chef/inspec/issues/738 for more details'
arr.inject { |result, i|
result = i.match(regex)
result || i.match(/$/)
}
end
end
RSpec::Matchers.define :contain_duplicates do
match do |arr|
dup = arr.select { |element| arr.count(element) > 1 }
@ -105,11 +86,6 @@ RSpec::Matchers.define :be_installed do
chain :by do
raise "[UNSUPPORTED] Please use the new resources 'gem', 'npm' or 'pip'."
end
chain :with_version do |version|
warn "[DEPRECATION] `with_version` is deprecated. Please use `its('version') { should eq '1.4.1' }` instead."
@version = version
end
end
# for services
@ -143,32 +119,6 @@ RSpec::Matchers.define :be_running do
end
end
# user resource matcher for serverspec compatibility
# Deprecated: You should not use this matcher anymore
RSpec::Matchers.define :belong_to_group do |compare_group|
match do |user|
warn "[DEPRECATION] `belong_to_group` is deprecated. Please use `its('groups') { should include('root') }` instead."
user.groups.include?(compare_group)
end
failure_message do |group|
"expected that the user belongs to group `#{group}`"
end
end
# user resource matcher for serverspec compatibility
# Deprecated: You should not use this matcher anymore
RSpec::Matchers.define :belong_to_primary_group do |compare_group|
match do |user|
warn "[DEPRECATION] `belong_to_primary_group` is deprecated. Please use `its('group') { should eq 'root' }` instead."
user.group == compare_group
end
failure_message do |group|
"expected that the user belongs to primary group `#{group}`"
end
end
# matcher to check if host is reachable
RSpec::Matchers.define :be_reachable do
match do |host|
@ -214,14 +164,6 @@ RSpec::Matchers.define :have_rule do |rule|
end
end
# deprecated
RSpec::Matchers.define :contain do |rule|
match do |resource|
warn "[DEPRECATION] `contain` matcher. Please use the following syntax `its('content') { should include('value') }`."
expect(resource).to include(rule)
end
end
# `be_in` matcher
# You can use it in the following cases:
# - check if an item or array is included in a given array

View file

@ -0,0 +1,40 @@
# Main AWS loader file. The intent is for this to be
# loaded only if AWS resources are needed.
require 'aws-sdk' # TODO: split once ADK v3 is in use
require 'resource_support/aws/aws_backend_factory_mixin'
require 'resource_support/aws/aws_resource_mixin'
require 'resource_support/aws/aws_singular_resource_mixin'
require 'resource_support/aws/aws_plural_resource_mixin'
require 'resource_support/aws/aws_backend_base'
# Load all AWS resources
# TODO: loop over and load entire directory
# for f in ls lib/resources/aws/*; do t=$(echo $f | cut -c 5- | cut -f1 -d. ); echo "require '${t}'"; done
require 'resources/aws/aws_cloudtrail_trail'
require 'resources/aws/aws_cloudtrail_trails'
require 'resources/aws/aws_cloudwatch_alarm'
require 'resources/aws/aws_cloudwatch_log_metric_filter'
require 'resources/aws/aws_ec2_instance'
require 'resources/aws/aws_iam_access_key'
require 'resources/aws/aws_iam_access_keys'
require 'resources/aws/aws_iam_group'
require 'resources/aws/aws_iam_groups'
require 'resources/aws/aws_iam_password_policy'
require 'resources/aws/aws_iam_policies'
require 'resources/aws/aws_iam_policy'
require 'resources/aws/aws_iam_role'
require 'resources/aws/aws_iam_root_user'
require 'resources/aws/aws_iam_user'
require 'resources/aws/aws_iam_users'
require 'resources/aws/aws_kms_keys'
require 'resources/aws/aws_route_table'
require 'resources/aws/aws_s3_bucket'
require 'resources/aws/aws_security_group'
require 'resources/aws/aws_security_groups'
require 'resources/aws/aws_sns_topic'
require 'resources/aws/aws_subnet'
require 'resources/aws/aws_subnets'
require 'resources/aws/aws_vpc'
require 'resources/aws/aws_vpcs'

View file

@ -0,0 +1,12 @@
class AwsBackendBase
attr_reader :aws_transport
class << self; attr_accessor :aws_client_class end
def initialize(inspec = nil)
@aws_transport = inspec ? inspec.backend : nil
end
def aws_service_client
aws_transport.aws_client(self.class.aws_client_class)
end
end

View file

@ -0,0 +1,12 @@
# Intended to be pulled in via extend, not include
module AwsBackendFactoryMixin
def create(inspec)
@selected_backend.new(inspec)
end
def select(klass)
@selected_backend = klass
end
alias set_default_backend select
end

View file

@ -0,0 +1,21 @@
module AwsPluralResourceMixin
include AwsResourceMixin
attr_reader :table
# This sets up a class, AwsSomeResource::BackendFactory, that
# provides a mechanism to create and use backends without
# having to know which is selected. This is mainly used for
# unit testing.
# TODO: DRY up. This code exists in both the Singular and Plural mixins.
# We'd like to put it in AwsResourceMixin, but included only sees the
# directly-including class - we can't see second-order includers.
def self.included(base)
# Create a new class, whose body is simply to extend the
# backend factory mixin
resource_backend_factory_class = Class.new(Object) do
extend AwsBackendFactoryMixin
end
# Name that class
base.const_set('BackendFactory', resource_backend_factory_class)
end
end

View file

@ -0,0 +1,62 @@
module AwsResourceMixin
def initialize(resource_params = {})
validate_params(resource_params).each do |param, value|
instance_variable_set(:"@#{param}", value)
end
catch_aws_errors do
fetch_from_api
end
end
# Default implementation of validate params accepts everything.
def validate_params(resource_params)
resource_params
end
def check_resource_param_names(raw_params: {}, allowed_params: [], allowed_scalar_name: nil, allowed_scalar_type: nil)
# Some resources allow passing in a single ID value. Check and convert to hash if so.
if allowed_scalar_name && !raw_params.is_a?(Hash)
value_seen = raw_params
if value_seen.is_a?(allowed_scalar_type)
raw_params = { allowed_scalar_name => value_seen }
else
raise ArgumentError, 'If you pass a single value to the resource, it must ' \
"be a #{allowed_scalar_type}, not an #{value_seen.class}."
end
end
# Remove all expected params from the raw param hash
recognized_params = {}
allowed_params.each do |expected_param|
recognized_params[expected_param] = raw_params.delete(expected_param) if raw_params.key?(expected_param)
end
# Any leftovers are unwelcome
unless raw_params.empty?
raise ArgumentError, "Unrecognized resource param '#{raw_params.keys.first}'. Expected parameters: #{allowed_params.join(', ')}"
end
recognized_params
end
def inspec_runner
# When running under inspec-cli, we have an 'inspec' method that
# returns the runner. When running under unit tests, we don't
# have that, but we still have to call this to pass something
# (nil is OK) to the backend.
# TODO: remove with https://github.com/chef/inspec-aws/issues/216
inspec if respond_to?(:inspec)
end
# Intercept AWS exceptions
def catch_aws_errors
yield
rescue Aws::Errors::MissingCredentialsError
# The AWS error here is unhelpful:
# "unable to sign request without credentials set"
Inspec::Log.error "It appears that you have not set your AWS credentials. You may set them using environment variables, or using the 'aws://region/aws_credentials_profile' target. See https://www.inspec.io/docs/reference/platforms for details."
fail_resource('No AWS credentials available')
rescue Aws::Errors::ServiceError => e
fail_resource e.message
end
end

View file

@ -0,0 +1,24 @@
module AwsSingularResourceMixin
include AwsResourceMixin
def exists?
@exists
end
# This sets up a class, AwsSomeResource::BackendFactory, that
# provides a mechanism to create and use backends without
# having to know which is selected. This is mainly used for
# unit testing.
# TODO: DRY up. This code exists in both the Singular and Plural mixins.
# We'd like to put it in AwsResourceMixin, but included only sees the
# directly-including class - we can't see second-order includers.
def self.included(base)
# Create a new class, whose body is simply to extend the
# backend factory mixin
resource_backend_factory_class = Class.new(Object) do
extend AwsBackendFactoryMixin
end
# Name that class
base.const_set('BackendFactory', resource_backend_factory_class)
end
end

View file

@ -1,205 +0,0 @@
# encoding: utf-8
# copyright: 2015, Vulcano Security GmbH
# author: Christoph Hartmann
# author: Dominik Richter
require 'forwardable'
require 'utils/filter_array'
module Inspec::Resources
class AuditdRulesLegacy
def initialize(content)
@content = content
@opts = {
assignment_regex: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: true,
}
end
def params
@params ||= SimpleConfig.new(@content, @opts).params
end
def method_missing(name)
params[name.to_s]
end
def status(name)
@status_opts = {
assignment_regex: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
multiple_values: false,
}
@status_content ||= inspec.command('/sbin/auditctl -s').stdout.chomp
@status_params = SimpleConfig.new(@status_content, @status_opts).params
status = @status_params['AUDIT_STATUS']
return nil if status.nil?
items = Hash[status.scan(/([^=]+)=(\w*)\s*/)]
items[name]
end
def to_s
'Audit Daemon Rules (for auditd version < 2.3)'
end
end
class AuditDaemonRules < Inspec.resource(1)
extend Forwardable
attr_accessor :rules, :lines
name 'auditd_rules'
desc 'Use the auditd_rules InSpec audit resource to test the rules for logging that exist on the system. The audit.rules file is typically located under /etc/audit/ and contains the list of rules that define what is captured in log files.'
example "
# syntax for auditd < 2.3
describe auditd_rules do
its('LIST_RULES') {should contain_match(/^exit,always arch=.* key=time-change syscall=adjtimex,settimeofday/) }
its('LIST_RULES') {should contain_match(/^exit,always arch=.* key=time-change syscall=stime,settimeofday,adjtimex/) }
its('LIST_RULES') {should contain_match(/^exit,always arch=.* key=time-change syscall=clock_settime/)}
its('LIST_RULES') {should contain_match(/^exit,always watch=\/etc\/localtime perm=wa key=time-change/)}
end
# syntax for auditd >= 2.3
describe auditd_rules.syscall('open').action do
it { should eq(['always']) }
end
describe auditd_rules.key('sshd_config') do
its('permissions') { should contain_match(/x/) }
end
describe auditd_rules do
its('lines') { should contain_match(%r{-w /etc/ssh/sshd_config/}) }
end
"
def initialize
@content = inspec.command('/sbin/auditctl -l').stdout.chomp
if @content =~ /^LIST_RULES:/
# do not warn on centos 5
unless inspec.os[:name] == 'centos' && inspec.os[:release].to_i == 5
warn '[WARN] this version of auditd is outdated. Updating it allows for using more precise matchers.'
end
@legacy = AuditdRulesLegacy.new(@content)
else
parse_content
@legacy = nil
end
warn '[DEPRECATION] The `auditd_rules` resource is deprecated and will be removed in InSpec 2.0. Use the `auditd` resource instead.'
end
# non-legacy instances are not asked for `its('LIST_RULES')`
# rubocop:disable Style/MethodName
def LIST_RULES
return @legacy.LIST_RULES if @legacy
raise 'Using legacy auditd_rules LIST_RULES interface with non-legacy audit package. Please use the new syntax.'
end
def status(name = nil)
return @legacy.status(name) if @legacy
@status_content ||= inspec.command('/sbin/auditctl -s').stdout.chomp
@status_params ||= Hash[@status_content.scan(/^([^ ]+) (.*)$/)]
return @status_params[name] if name
@status_params
end
def parse_content
@rules = {
syscalls: [],
files: [],
}
@lines = @content.lines.map(&:chomp)
lines.each do |line|
if is_syscall?(line)
syscalls = get_syscalls line
action, list = get_action_list line
fields, opts = get_fields line
# create a 'flatter' structure because sanity
syscalls.each do |s|
@rules[:syscalls] << { syscall: s, list: list, action: action, fields: fields }.merge(opts)
end
elsif is_file?(line)
file = get_file line
perms = get_permissions line
key = get_key line
@rules[:files] << { file: file, key: key, permissions: perms }
end
end
end
def syscall(name)
select_name(:syscall, name)
end
def file(name)
select_name(:file, name)
end
# both files and syscalls have `key` identifiers
def key(name)
res = rules.values.flatten.find_all { |rule| rule[:key] == name }
FilterArray.new(res)
end
def to_s
'Audit Daemon Rules'
end
private
def select_name(key, name)
plural = "#{key}s".to_sym
res = rules[plural].find_all { |rule| rule[key] == name }
FilterArray.new(res)
end
def is_syscall?(line)
line.match(/\ -S /)
end
def is_file?(line)
line.match(/-w /)
end
def get_syscalls(line)
line.scan(/-S ([^ ]+) /).flatten.first.split(',')
end
def get_action_list(line)
line.scan(/-a ([^,]+),([^ ]+)/).flatten
end
# NB only in file lines
def get_key(line)
line.match(/-k ([^ ]+)/)[1] if line.include?('-k ')
end
# NOTE there are NO precautions wrt. filenames containing spaces in auditctl
# `auditctl -w /foo\ bar` gives the following line: `-w /foo bar -p rwxa`
def get_file(line)
line.match(/-w (.+) -p/)[1]
end
def get_permissions(line)
line.match(/-p ([^ ]+)/)[1]
end
def get_fields(line)
fields = line.gsub(/-[aS] [^ ]+ /, '').split('-F ').map { |l| l.split(' ') }.flatten
opts = {}
fields.find_all { |x| x.match(/[a-z]+=.*/) }.each do |kv|
k, v = kv.split('=')
opts[k.to_sym] = v
end
[fields, opts]
end
end
end

View file

@ -0,0 +1,77 @@
class AwsCloudTrailTrail < Inspec.resource(1)
name 'aws_cloudtrail_trail'
desc 'Verifies settings for an individual AWS CloudTrail Trail'
example "
describe aws_cloudtrail_trail('trail-name') do
it { should exist }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :cloud_watch_logs_log_group_arn, :cloud_watch_logs_role_arn, :home_region,
:kms_key_id, :s3_bucket_name, :trail_arn
def to_s
"CloudTrail #{@trail_name}"
end
def multi_region_trail?
@is_multi_region_trail
end
def log_file_validation_enabled?
@log_file_validation_enabled
end
def encrypted?
!kms_key_id.nil?
end
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:trail_name],
allowed_scalar_name: :trail_name,
allowed_scalar_type: String,
)
if validated_params.empty?
raise ArgumentError, "You must provide the parameter 'trail_name' to aws_cloudtrail_trail."
end
validated_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
query = { trail_name_list: [@trail_name] }
resp = backend.describe_trails(query)
@trail = resp.trail_list[0].to_h
@exists = !@trail.empty?
@s3_bucket_name = @trail[:s3_bucket_name]
@is_multi_region_trail = @trail[:is_multi_region_trail]
@trail_arn = @trail[:trail_arn]
@log_file_validation_enabled = @trail[:log_file_validation_enabled]
@cloud_watch_logs_role_arn = @trail[:cloud_watch_logs_role_arn]
@cloud_watch_logs_log_group_arn = @trail[:cloud_watch_logs_log_group_arn]
@kms_key_id = @trail[:kms_key_id]
@home_region = @trail[:home_region]
end
class Backend
class AwsClientApi < AwsBackendBase
AwsCloudTrailTrail::BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::CloudTrail::Client
def describe_trails(query)
aws_service_client.describe_trails(query)
end
end
end
end

View file

@ -0,0 +1,47 @@
class AwsCloudTrailTrails < Inspec.resource(1)
name 'aws_cloudtrail_trails'
desc 'Verifies settings for AWS CloudTrail Trails in bulk'
example '
describe aws_cloudtrail_trails do
it { should exist }
end
'
supports platform: 'aws'
include AwsPluralResourceMixin
def validate_params(resource_params)
unless resource_params.empty?
raise ArgumentError, 'aws_cloudtrail_trails does not accept resource parameters.'
end
resource_params
end
# Underlying FilterTable implementation.
filter = FilterTable.create
filter.add_accessor(:entries)
.add(:exists?) { |x| !x.entries.empty? }
.add(:names, field: :name)
.add(:trail_arns, field: :trail_arn)
filter.connect(self, :table)
def to_s
'CloudTrail Trails'
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
@table = backend.describe_trails({}).to_h[:trail_list]
end
class Backend
class AwsClientApi < AwsBackendBase
AwsCloudTrailTrails::BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::CloudTrail::Client
def describe_trails(query)
aws_service_client.describe_trails(query)
end
end
end
end

View file

@ -0,0 +1,62 @@
class AwsCloudwatchAlarm < Inspec.resource(1)
name 'aws_cloudwatch_alarm'
desc <<-EOD
# Look for a specific alarm
aws_cloudwatch_alarm(
metric: 'my-metric-name',
metric_namespace: 'my-metric-namespace',
) do
it { should exist }
end
EOD
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :alarm_actions, :alarm_name, :metric_name, :metric_namespace
private
def validate_params(raw_params)
recognized_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:metric_name, :metric_namespace],
)
validated_params = {}
# Currently you must specify exactly metric_name and metric_namespace
[:metric_name, :metric_namespace].each do |param|
raise ArgumentError, "Missing resource param #{param}" unless recognized_params.key?(param)
validated_params[param] = recognized_params.delete(param)
end
validated_params
end
def fetch_from_api
aws_alarms = BackendFactory.create(inspec_runner).describe_alarms_for_metric(
metric_name: @metric_name,
namespace: @metric_namespace,
)
if aws_alarms.metric_alarms.empty?
@exists = false
elsif aws_alarms.metric_alarms.count > 1
alarms = aws_alarms.metric_alarms.map(&:alarm_name)
raise 'More than one Cloudwatch Alarm was matched. Try using ' \
"more specific resource parameters. Alarms matched: #{alarms.join(', ')}"
else
@alarm_actions = aws_alarms.metric_alarms.first.alarm_actions
@alarm_name = aws_alarms.metric_alarms.first.alarm_name
@exists = true
end
end
class Backend
class AwsClientApi < AwsBackendBase
AwsCloudwatchAlarm::BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::CloudWatch::Client
def describe_alarms_for_metric(query)
aws_service_client.describe_alarms_for_metric(query)
end
end
end
end

View file

@ -0,0 +1,100 @@
class AwsCloudwatchLogMetricFilter < Inspec.resource(1)
name 'aws_cloudwatch_log_metric_filter'
desc 'Verifies individual Cloudwatch Log Metric Filters'
example <<-EOX
# Look for a LMF by its filter name and log group name. This combination
# will always either find at most one LMF - no duplicates.
describe aws_cloudwatch_log_metric_filter(
filter_name: 'my-filter',
log_group_name: 'my-log-group'
) do
it { should exist }
end
# Search for an LMF by pattern and log group.
# This could result in an error if the results are not unique.
describe aws_cloudwatch_log_metric_filter(
log_group_name: 'my-log-group',
pattern: 'my-filter'
) do
it { should exist }
end
EOX
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :filter_name, :log_group_name, :metric_name, :metric_namespace, :pattern
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:filter_name, :log_group_name, :pattern],
)
if validated_params.empty?
raise ArgumentError, 'You must provide either filter_name, log_group, or pattern to aws_cloudwatch_log_metric_filter.'
end
validated_params
end
def fetch_from_api
# get a backend
backend = BackendFactory.create(inspec_runner)
# Perform query with remote filtering
aws_search_criteria = {}
aws_search_criteria[:filter_name] = filter_name if filter_name
aws_search_criteria[:log_group_name] = log_group_name if log_group_name
begin
aws_results = backend.describe_metric_filters(aws_search_criteria)
rescue Aws::CloudWatchLogs::Errors::ResourceNotFoundException
@exists = false
return
end
# Then perform local filtering
if pattern
aws_results.select! { |lmf| lmf.filter_pattern == pattern }
end
# Check result count. We're a singular resource and can tolerate
# 0 or 1 results, not multiple.
if aws_results.count > 1
raise 'More than one result was returned, but aws_cloudwatch_log_metric_filter '\
'can only handle a single AWS resource. Consider passing more resource '\
'parameters to narrow down the search.'
elsif aws_results.empty?
@exists = false
else
@exists = true
# Unpack the funny-shaped object we got back from AWS into our instance vars
lmf = aws_results.first
@filter_name = lmf.filter_name
@log_group_name = lmf.log_group_name
@pattern = lmf.filter_pattern # Note inconsistent name
# AWS SDK returns an array of metric transformations
# but only allows one (mandatory) entry, let's flatten that
@metric_name = lmf.metric_transformations.first.metric_name
@metric_namespace = lmf.metric_transformations.first.metric_namespace
end
end
class Backend
# Uses the cloudwatch API to really talk to AWS
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::CloudWatchLogs::Client
def describe_metric_filters(criteria)
query = {}
query[:filter_name_prefix] = criteria[:filter_name] if criteria[:filter_name]
query[:log_group_name] = criteria[:log_group_name] if criteria[:log_group_name]
# 'pattern' is not available as a remote filter,
# we filter it after the fact locally
# TODO: handle pagination? Max 50/page. Maybe you want a plural resource?
aws_response = aws_service_client.describe_metric_filters(query)
aws_response.metric_filters
end
end
end
end

View file

@ -0,0 +1,157 @@
# author: Christoph Hartmann
class AwsEc2Instance < Inspec.resource(1)
name 'aws_ec2_instance'
desc 'Verifies settings for an EC2 instance'
example <<-EOX
describe aws_ec2_instance('i-123456') do
it { should be_running }
it { should have_roles }
end
describe aws_ec2_instance(name: 'my-instance') do
it { should be_running }
it { should have_roles }
end
EOX
supports platform: 'aws'
# TODO: rewrite to avoid direct injection, match other resources, use AwsSingularResourceMixin
def initialize(opts, conn = nil)
@opts = opts
@opts.is_a?(Hash) ? @display_name = @opts[:name] : @display_name = opts
@ec2_client = conn ? conn.ec2_client : inspec_runner.backend.aws_client(Aws::EC2::Client)
@ec2_resource = conn ? conn.ec2_resource : inspec_runner.backend.aws_resource(Aws::EC2::Resource, {})
@iam_resource = conn ? conn.iam_resource : inspec_runner.backend.aws_resource(Aws::IAM::Resource, {})
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_resource_mixin.rb
def catch_aws_errors
yield
rescue Aws::Errors::MissingCredentialsError
# The AWS error here is unhelpful:
# "unable to sign request without credentials set"
Inspec::Log.error "It appears that you have not set your AWS credentials. You may set them using environment variables, or using the 'aws://region/aws_credentials_profile' target. See https://www.inspec.io/docs/reference/platforms for details."
fail_resource('No AWS credentials available')
rescue Aws::Errors::ServiceError => e
fail_resource e.message
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_singular_resource_mixin.rb
def inspec_runner
# When running under inspec-cli, we have an 'inspec' method that
# returns the runner. When running under unit tests, we don't
# have that, but we still have to call this to pass something
# (nil is OK) to the backend.
# TODO: remove with https://github.com/chef/inspec-aws/issues/216
# TODO: remove after rewrite to include AwsSingularResource
inspec if respond_to?(:inspec)
end
def id
return @instance_id if defined?(@instance_id)
catch_aws_errors do
if @opts.is_a?(Hash)
first = @ec2_resource.instances(
{
filters: [{
name: 'tag:Name',
values: [@opts[:name]],
}],
},
).first
# catch case where the instance is not known
@instance_id = first.id unless first.nil?
else
@instance_id = @opts
end
end
end
alias instance_id id
def exists?
return false if instance.nil?
instance.exists?
end
# returns the instance state
def state
catch_aws_errors do
instance&.state&.name
end
end
# helper methods for each state
%w{
pending running shutting-down
terminated stopping stopped unknown
}.each do |state_name|
define_method state_name.tr('-', '_') + '?' do
state == state_name
end
end
# attributes that we want to expose
%w{
public_ip_address private_ip_address key_name private_dns_name
public_dns_name subnet_id architecture root_device_type
root_device_name virtualization_type client_token launch_time
instance_type image_id vpc_id
}.each do |attribute|
define_method attribute do
catch_aws_errors do
instance.send(attribute) if instance
end
end
end
# Don't document this - it's a bit hard to use. Our current doctrine
# is to use dumb things, like arrays of strings - use security_group_ids instead.
def security_groups
catch_aws_errors do
@security_groups ||= instance.security_groups.map { |sg|
{ id: sg.group_id, name: sg.group_name }
}
end
end
def security_group_ids
catch_aws_errors do
@security_group_ids ||= instance.security_groups.map(&:group_id)
end
end
def tags
catch_aws_errors do
@tags ||= instance.tags.map { |tag| { key: tag.key, value: tag.value } }
end
end
def to_s
"EC2 Instance #{@display_name}"
end
def has_roles?
catch_aws_errors do
instance_profile = instance.iam_instance_profile
if instance_profile
roles = @iam_resource.instance_profile(
instance_profile.arn.gsub(%r{^.*\/}, ''),
).roles
else
roles = nil
end
roles && !roles.empty?
end
end
private
def instance
catch_aws_errors { @instance ||= @ec2_resource.instance(id) }
end
end

View file

@ -0,0 +1,106 @@
class AwsIamAccessKey < Inspec.resource(1)
name 'aws_iam_access_key'
desc 'Verifies settings for an individual IAM access key'
example "
describe aws_iam_access_key(username: 'username', id: 'access-key id') do
it { should exist }
it { should_not be_active }
its('create_date') { should be > Time.now - 365 * 86400 }
its('last_used_date') { should be > Time.now - 90 * 86400 }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :access_key_id, :create_date, :status, :username
alias id access_key_id
def validate_params(raw_params)
recognized_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:username, :id, :access_key_id],
allowed_scalar_name: :access_key_id,
allowed_scalar_type: String,
)
# id and access_key_id are aliases; standardize on access_key_id
recognized_params[:access_key_id] = recognized_params.delete(:id) if recognized_params.key?(:id)
# Validate format of access_key_id
if recognized_params[:access_key_id] and
recognized_params[:access_key_id] !~ /^AKIA[0-9A-Z]{16}$/
raise ArgumentError, 'Incorrect format for Access Key ID - expected AKIA followed ' \
'by 16 letters or numbers'
end
# One of username and access_key_id is required
if recognized_params[:username].nil? && recognized_params[:access_key_id].nil?
raise ArgumentError, 'You must provide at lease one of access_key_id or username to aws_iam_access_key'
end
recognized_params
end
def active?
return nil unless exists?
status == 'Active'
end
def to_s
"IAM Access-Key #{access_key_id}"
end
def last_used_date
return nil unless exists?
return @last_used_date if defined? @last_used_date
backend = BackendFactory.create(inspec_runner)
catch_aws_errors do
@last_used_date = backend.get_access_key_last_used({ access_key_id: access_key_id }).access_key_last_used.last_used_date
end
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
query = {}
query[:user_name] = username if username
response = backend.list_access_keys(query)
access_keys = response.access_key_metadata.select do |key|
if access_key_id
key.access_key_id == access_key_id
else
true
end
end
if access_keys.empty?
@exists = false
return
end
if access_keys.count > 1
raise 'More than one access key matched for aws_iam_access_key. Use more specific paramaters, such as access_key_id.'
end
@exists = true
@access_key_id = access_keys[0].access_key_id
@username = access_keys[0].user_name
@create_date = access_keys[0].create_date
@status = access_keys[0].status
# Last used date is lazily loaded, separate API call
rescue Aws::IAM::Errors::NoSuchEntity
@exists = false
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::IAM::Client
def list_access_keys(query)
aws_service_client.list_access_keys(query)
end
end
end
end

View file

@ -0,0 +1,144 @@
class AwsIamAccessKeys < Inspec.resource(1)
name 'aws_iam_access_keys'
desc 'Verifies settings for AWS IAM Access Keys in bulk'
example '
describe aws_iam_access_keys do
it { should_not exist }
end
'
supports platform: 'aws'
include AwsPluralResourceMixin
def validate_params(raw_params)
recognized_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:username, :id, :access_key_id, :created_date],
allowed_scalar_name: :access_key_id,
allowed_scalar_type: String,
)
# id and access_key_id are aliases; standardize on access_key_id
recognized_params[:access_key_id] = recognized_params.delete(:id) if recognized_params.key?(:id)
if recognized_params[:access_key_id] and
recognized_params[:access_key_id] !~ /^AKIA[0-9A-Z]{16}$/
raise 'Incorrect format for Access Key ID - expected AKIA followed ' \
'by 16 letters or numbers'
end
recognized_params
end
def fetch_from_api
# TODO: this interface should be normalized to match the AWS API
criteria = {}
criteria[:username] = @username if defined? @username
@table = BackendFactory.create(inspec_runner).fetch(criteria)
end
# Underlying FilterTable implementation.
filter = FilterTable.create
filter.add_accessor(:where)
.add_accessor(:entries)
.add(:exists?) { |x| !x.entries.empty? }
.add(:access_key_ids, field: :access_key_id)
.add(:created_date, field: :create_date)
.add(:created_days_ago, field: :created_days_ago)
.add(:created_with_user, field: :created_with_user)
.add(:created_hours_ago, field: :created_hours_ago)
.add(:usernames, field: :username)
.add(:active, field: :active)
.add(:inactive, field: :inactive)
.add(:last_used_date, field: :last_used_date)
.add(:last_used_hours_ago, field: :last_used_hours_ago)
.add(:last_used_days_ago, field: :last_used_days_ago)
.add(:ever_used, field: :ever_used)
.add(:never_used, field: :never_used)
.add(:user_created_date, field: :user_created_date)
filter.connect(self, :table)
def to_s
'IAM Access Keys'
end
# Internal support class. This is used to fetch
# the users and access keys. We have an abstract
# class with a concrete AWS implementation provided here;
# a few mock implementations are also provided in the unit tests.
class Backend
# Implementation of AccessKeyProvider which operates by looping over
# all users, then fetching their access keys.
# TODO: An alternate, more scalable implementation could be made
# using the Credential Report.
class AwsUserIterator < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::IAM::Client
def fetch(criteria)
iam_client = aws_service_client
user_details = {}
if criteria.key?(:username)
begin
user_details[criteria[:username]] = iam_client.get_user(user_name: criteria[:username]).user
rescue Aws::IAM::Errors::NoSuchEntity # rubocop:disable Lint/HandleExceptions
# Swallow - a miss on search results should return an empty table
end
else
# TODO: pagination check and resume
iam_client.list_users.users.each do |info|
user_details[info.user_name] = info
end
end
access_key_data = []
user_details.each_key do |username|
begin
user_keys = iam_client.list_access_keys(user_name: username)
.access_key_metadata
user_keys = user_keys.map do |metadata|
{
access_key_id: metadata.access_key_id,
username: username,
status: metadata.status,
create_date: metadata.create_date, # DateTime.parse(metadata.create_date),
}
end
# Copy in from user data
# Synthetics
user_keys.each do |key_info|
add_synthetic_fields(key_info, user_details[username])
end
access_key_data.concat(user_keys)
rescue Aws::IAM::Errors::NoSuchEntity # rubocop:disable Lint/HandleExceptions
# Swallow - a miss on search results should return an empty table
end
end
access_key_data
end
def add_synthetic_fields(key_info, user_details) # rubocop:disable Metrics/AbcSize
key_info[:id] = key_info[:access_key_id]
key_info[:active] = key_info[:status] == 'Active'
key_info[:inactive] = key_info[:status] != 'Active'
key_info[:created_hours_ago] = ((Time.now - key_info[:create_date]) / (60*60)).to_i
key_info[:created_days_ago] = (key_info[:created_hours_ago] / 24).to_i
key_info[:user_created_date] = user_details[:create_date]
key_info[:created_with_user] = (key_info[:create_date] - key_info[:user_created_date]).abs < 1.0/24.0
# Last used is a separate API call
iam_client = aws_service_client
last_used =
iam_client.get_access_key_last_used(access_key_id: key_info[:access_key_id])
.access_key_last_used.last_used_date
key_info[:ever_used] = !last_used.nil?
key_info[:never_used] = last_used.nil?
key_info[:last_used_time] = last_used
return unless last_used
key_info[:last_used_hours_ago] = ((Time.now - last_used) / (60*60)).to_i
key_info[:last_used_days_ago] = (key_info[:last_used_hours_ago]/24).to_i
end
end
end
end

View file

@ -0,0 +1,56 @@
class AwsIamGroup < Inspec.resource(1)
name 'aws_iam_group'
desc 'Verifies settings for AWS IAM Group'
example "
describe aws_iam_group('mygroup') do
it { should exist }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :group_name
def to_s
"IAM Group #{group_name}"
end
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:group_name],
allowed_scalar_name: :group_name,
allowed_scalar_type: String,
)
if validated_params.empty?
raise ArgumentError, 'You must provide a group_name to aws_iam_group.'
end
validated_params
end
def fetch_from_api
backend = AwsIamGroup::BackendFactory.create(inspec_runner)
begin
@aws_group_struct = backend.get_group(group_name: group_name)[:group]
@exists = true
rescue Aws::IAM::Errors::NoSuchEntity
@exists = false
end
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::IAM::Client
def get_group(query)
aws_service_client.get_group(query)
end
end
end
end

View file

@ -0,0 +1,45 @@
class AwsIamGroups < Inspec.resource(1)
name 'aws_iam_groups'
desc 'Verifies settings for AWS IAM groups in bulk'
example '
describe aws_iam_groups do
it { should exist }
end
'
supports platform: 'aws'
include AwsPluralResourceMixin
def validate_params(resource_params)
unless resource_params.empty?
raise ArgumentError, 'aws_iam_groups does not accept resource parameters.'
end
resource_params
end
# Underlying FilterTable implementation.
filter = FilterTable.create
filter.add_accessor(:entries)
.add(:exists?) { |x| !x.entries.empty? }
filter.connect(self, :table)
def to_s
'IAM Groups'
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
@table = backend.list_groups.to_h[:groups]
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::IAM::Client
def list_groups(query = {})
aws_service_client.list_groups(query)
end
end
end
end

View file

@ -0,0 +1,116 @@
# author: Viktor Yakovlyev
class AwsIamPasswordPolicy < Inspec.resource(1)
name 'aws_iam_password_policy'
desc 'Verifies iam password policy'
example <<-EOX
describe aws_iam_password_policy do
its('requires_lowercase_characters?') { should be true }
end
describe aws_iam_password_policy do
its('requires_uppercase_characters?') { should be true }
end
EOX
supports platform: 'aws'
# TODO: rewrite to avoid direct injection, match other resources, use AwsSingularResourceMixin
def initialize(conn = nil)
catch_aws_errors do
begin
if conn
# We're in a mocked unit test.
@policy = conn.iam_resource.account_password_policy
else
# Don't use the resource approach. It's a CRUD operation
# - if the policy does not exist, you get back a blank object to populate and save.
# Using the Client will throw an exception if no policy exists.
@policy = inspec_runner.backend.aws_client(Aws::IAM::Client).get_account_password_policy.password_policy
end
rescue Aws::IAM::Errors::NoSuchEntity
@policy = nil
end
end
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_resource_mixin.rb
def catch_aws_errors
yield
rescue Aws::Errors::MissingCredentialsError
# The AWS error here is unhelpful:
# "unable to sign request without credentials set"
Inspec::Log.error "It appears that you have not set your AWS credentials. You may set them using environment variables, or using the 'aws://region/aws_credentials_profile' target. See https://www.inspec.io/docs/reference/platforms for details."
fail_resource('No AWS credentials available')
rescue Aws::Errors::ServiceError => e
fail_resource e.message
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_singular_resource_mixin.rb
def inspec_runner
# When running under inspec-cli, we have an 'inspec' method that
# returns the runner. When running under unit tests, we don't
# have that, but we still have to call this to pass something
# (nil is OK) to the backend.
# TODO: remove with https://github.com/chef/inspec-aws/issues/216
# TODO: remove after rewrite to include AwsSingularResource
inspec if respond_to?(:inspec)
end
def to_s
'IAM Password-Policy'
end
def exists?
!@policy.nil?
end
#-------------------------- Properties ----------------------------#
def minimum_password_length
@policy.minimum_password_length
end
def max_password_age_in_days
raise 'this policy does not expire passwords' unless expire_passwords?
@policy.max_password_age
end
def number_of_passwords_to_remember
raise 'this policy does not prevent password reuse' \
unless prevent_password_reuse?
@policy.password_reuse_prevention
end
#-------------------------- Matchers ----------------------------#
[
:require_lowercase_characters,
:require_uppercase_characters,
:require_symbols,
:require_numbers,
:expire_passwords,
].each do |matcher_stem|
# Create our predicates (for example, 'require_symbols?')
stem_with_question_mark = (matcher_stem.to_s + '?').to_sym
define_method stem_with_question_mark do
@policy.send(matcher_stem)
end
# RSpec will expose that as (for example) `be_require_symbols`.
# To undo that, we have to make a matcher alias.
stem_with_be = ('be_' + matcher_stem.to_s).to_sym
RSpec::Matchers.alias_matcher matcher_stem, stem_with_be
end
# This one has an awkward name mapping
def allow_users_to_change_passwords?
@policy.allow_users_to_change_password
end
RSpec::Matchers.alias_matcher :allow_users_to_change_passwords, :be_allow_users_to_change_passwords
# This one has custom logic and renaming
def prevent_password_reuse?
!@policy.password_reuse_prevention.nil?
end
RSpec::Matchers.alias_matcher :prevent_password_reuse, :be_prevent_password_reuse
end

View file

@ -0,0 +1,46 @@
class AwsIamPolicies < Inspec.resource(1)
name 'aws_iam_policies'
desc 'Verifies settings for AWS IAM Policies in bulk'
example '
describe aws_iam_policies do
it { should exist }
end
'
supports platform: 'aws'
include AwsPluralResourceMixin
def validate_params(resource_params)
unless resource_params.empty?
raise ArgumentError, 'aws_iam_policies does not accept resource parameters.'
end
resource_params
end
# Underlying FilterTable implementation.
filter = FilterTable.create
filter.add_accessor(:entries)
.add(:exists?) { |x| !x.entries.empty? }
.add(:policy_names, field: :policy_name)
.add(:arns, field: :arn)
filter.connect(self, :table)
def to_s
'IAM Policies'
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
@table = backend.list_policies({}).to_h[:policies]
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::IAM::Client
def list_policies(query)
aws_service_client.list_policies(query)
end
end
end
end

View file

@ -0,0 +1,119 @@
class AwsIamPolicy < Inspec.resource(1)
name 'aws_iam_policy'
desc 'Verifies settings for individual AWS IAM Policy'
example "
describe aws_iam_policy('AWSSupportAccess') do
it { should be_attached }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :arn, :attachment_count, :default_version_id
def to_s
"Policy #{@policy_name}"
end
def attached?
!attachment_count.zero?
end
def attached_users
return @attached_users if defined? @attached_users
fetch_attached_entities
@attached_users
end
def attached_groups
return @attached_groups if defined? @attached_groups
fetch_attached_entities
@attached_groups
end
def attached_roles
return @attached_roles if defined? @attached_roles
fetch_attached_entities
@attached_roles
end
def attached_to_user?(user_name)
attached_users.include?(user_name)
end
def attached_to_group?(group_name)
attached_groups.include?(group_name)
end
def attached_to_role?(role_name)
attached_roles.include?(role_name)
end
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:policy_name],
allowed_scalar_name: :policy_name,
allowed_scalar_type: String,
)
if validated_params.empty?
raise ArgumentError, "You must provide the parameter 'policy_name' to aws_iam_policy."
end
validated_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
criteria = { max_items: 1000 } # maxItems max value is 1000
resp = backend.list_policies(criteria)
@policy = resp.policies.detect do |policy|
policy.policy_name == @policy_name
end
@exists = !@policy.nil?
return unless @exists
@arn = @policy[:arn]
@default_version_id = @policy[:default_version_id]
@attachment_count = @policy[:attachment_count]
end
def fetch_attached_entities
unless @exists
@attached_groups = nil
@attached_users = nil
@attached_roles = nil
return
end
backend = AwsIamPolicy::BackendFactory.create(inspec_runner)
criteria = { policy_arn: arn }
resp = nil
catch_aws_errors do
resp = backend.list_entities_for_policy(criteria)
end
@attached_groups = resp.policy_groups.map(&:group_name)
@attached_users = resp.policy_users.map(&:user_name)
@attached_roles = resp.policy_roles.map(&:role_name)
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::IAM::Client
def list_policies(criteria)
aws_service_client.list_policies(criteria)
end
def list_entities_for_policy(criteria)
aws_service_client.list_entities_for_policy(criteria)
end
end
end
end

View file

@ -0,0 +1,51 @@
class AwsIamRole < Inspec.resource(1)
name 'aws_iam_role'
desc 'Verifies settings for an IAM Role'
example "
describe aws_iam_role('my-role') do
it { should exist }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :description, :role_name
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:role_name],
allowed_scalar_name: :role_name,
allowed_scalar_type: String,
)
if validated_params.empty?
raise ArgumentError, 'You must provide a role_name to aws_iam_role.'
end
validated_params
end
def fetch_from_api
role_info = nil
begin
role_info = BackendFactory.create(inspec_runner).get_role(role_name: role_name)
rescue Aws::IAM::Errors::NoSuchEntity
@exists = false
return
end
@exists = true
@description = role_info.role.description
end
# Uses the SDK API to really talk to AWS
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::IAM::Client
def get_role(query)
aws_service_client.get_role(query)
end
end
end
end

View file

@ -0,0 +1,60 @@
class AwsIamRootUser < Inspec.resource(1)
name 'aws_iam_root_user'
desc 'Verifies settings for AWS root account'
example "
describe aws_iam_root_user do
it { should have_access_key }
end
"
supports platform: 'aws'
# TODO: rewrite to avoid direct injection, match other resources, use AwsSingularResourceMixin
def initialize(conn = nil)
@client = conn ? conn.iam_client : inspec_runner.backend.aws_client(Aws::IAM::Client)
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_resource_mixin.rb
def catch_aws_errors
yield
rescue Aws::Errors::MissingCredentialsError
# The AWS error here is unhelpful:
# "unable to sign request without credentials set"
Inspec::Log.error "It appears that you have not set your AWS credentials. You may set them using environment variables, or using the 'aws://region/aws_credentials_profile' target. See https://www.inspec.io/docs/reference/platforms for details."
fail_resource('No AWS credentials available')
rescue Aws::Errors::ServiceError => e
fail_resource e.message
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_singular_resource_mixin.rb
def inspec_runner
# When running under inspec-cli, we have an 'inspec' method that
# returns the runner. When running under unit tests, we don't
# have that, but we still have to call this to pass something
# (nil is OK) to the backend.
# TODO: remove with https://github.com/chef/inspec-aws/issues/216
# TODO: remove after rewrite to include AwsSingularResource
inspec if respond_to?(:inspec)
end
def has_access_key?
summary_account['AccountAccessKeysPresent'] == 1
end
def has_mfa_enabled?
summary_account['AccountMFAEnabled'] == 1
end
def to_s
'AWS Root-User'
end
private
def summary_account
catch_aws_errors do
@summary_account ||= @client.get_account_summary.summary_map
end
end
end

View file

@ -0,0 +1,111 @@
# author: Alex Bedley
# author: Steffanie Freeman
# author: Simon Varlow
# author: Chris Redekop
class AwsIamUser < Inspec.resource(1)
name 'aws_iam_user'
desc 'Verifies settings for AWS IAM user'
example "
describe aws_iam_user(username: 'test_user') do
it { should have_mfa_enabled }
it { should_not have_console_password }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :access_keys, :has_console_password, :has_mfa_enabled, :username
alias has_mfa_enabled? has_mfa_enabled
alias has_console_password? has_console_password
def name
warn "[DEPRECATION] - Property ':name' is deprecated on the aws_iam_user resource. Use ':username' instead."
username
end
def to_s
"IAM User #{username}"
end
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:username, :aws_user_struct, :name, :user],
allowed_scalar_name: :username,
allowed_scalar_type: String,
)
# If someone passed :name, rename it to :username
if validated_params.key?(:name)
warn "[DEPRECATION] - Resource parameter ':name' is deprecated on the aws_iam_user resource. Use ':username' instead."
validated_params[:username] = validated_params.delete(:name)
end
# If someone passed :user, rename it to :aws_user_struct
if validated_params.key?(:user)
warn "[DEPRECATION] - Resource parameter ':user' is deprecated on the aws_iam_user resource. Use ':aws_user_struct' instead."
validated_params[:aws_user_struct] = validated_params.delete(:user)
end
if validated_params.empty?
raise ArgumentError, 'You must provide a username to aws_iam_user.'
end
validated_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
@aws_user_struct ||= nil # silence unitialized warning
unless @aws_user_struct
begin
@aws_user_struct = backend.get_user(user_name: username)
rescue Aws::IAM::Errors::NoSuchEntity
@exists = false
return
end
end
# TODO: extract properties from aws_user_struct?
@exists = true
begin
_login_profile = backend.get_login_profile(user_name: username)
@has_console_password = true
# Password age also available here
rescue Aws::IAM::Errors::NoSuchEntity
@has_console_password = false
end
mfa_info = backend.list_mfa_devices(user_name: username)
@has_mfa_enabled = !mfa_info.mfa_devices.empty?
# TODO: consider returning Inspec AwsIamAccessKey objects
@access_keys = backend.list_access_keys(user_name: username).access_key_metadata
# If the above call fails, we get nil here; but we promise access_keys will be an array.
@access_keys ||= []
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::IAM::Client
def get_user(criteria)
aws_service_client.get_user(criteria)
end
def get_login_profile(criteria)
aws_service_client.get_login_profile(criteria)
end
def list_mfa_devices(criteria)
aws_service_client.list_mfa_devices(criteria)
end
def list_access_keys(criteria)
aws_service_client.list_access_keys(criteria)
end
end
end
end

View file

@ -0,0 +1,96 @@
# author: Alex Bedley
# author: Steffanie Freeman
# author: Simon Varlow
# author: Chris Redekop
class AwsIamUsers < Inspec.resource(1)
name 'aws_iam_users'
desc 'Verifies settings for AWS IAM users'
example '
describe aws_iam_users.where(has_mfa_enabled?: false) do
it { should_not exist }
end
describe aws_iam_users.where(has_console_password?: true) do
it { should exist }
end
'
supports platform: 'aws'
include AwsPluralResourceMixin
filter = FilterTable.create
filter.add_accessor(:where)
.add_accessor(:entries)
.add(:exists?) { |x| !x.entries.empty? }
.add(:has_mfa_enabled?, field: :has_mfa_enabled)
.add(:has_console_password?, field: :has_console_password)
.add(:password_ever_used?, field: :password_ever_used?)
.add(:password_never_used?, field: :password_never_used?)
.add(:password_last_used_days_ago, field: :password_last_used_days_ago)
.add(:username, field: :user_name)
filter.connect(self, :table)
def validate_params(raw_params)
# No params yet
unless raw_params.empty?
raise ArgumentError, 'aws_iam_users does not accept resource parameters'
end
raw_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
@table = backend.list_users.users.map(&:to_h)
# TODO: lazy columns - https://github.com/chef/inspec-aws/issues/100
@table.each do |user|
begin
_login_profile = backend.get_login_profile(user_name: user[:user_name])
user[:has_console_password] = true
rescue Aws::IAM::Errors::NoSuchEntity
user[:has_console_password] = false
end
user[:has_console_password?] = user[:has_console_password]
begin
aws_mfa_devices = backend.list_mfa_devices(user_name: user[:user_name])
user[:has_mfa_enabled] = !aws_mfa_devices.mfa_devices.empty?
rescue Aws::IAM::Errors::NoSuchEntity
user[:has_mfa_enabled] = false
end
user[:has_mfa_enabled?] = user[:has_mfa_enabled]
password_last_used = user[:password_last_used]
user[:password_ever_used?] = !password_last_used.nil?
user[:password_never_used?] = password_last_used.nil?
next unless user[:password_ever_used?]
user[:password_last_used_days_ago] = ((Time.now - password_last_used) / (24*60*60)).to_i
end
@table
end
def to_s
'IAM Users'
end
#===========================================================================#
# Backend Implementation
#===========================================================================#
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::IAM::Client
# TODO: delegate this out
def list_users(query = {})
aws_service_client.list_users(query)
end
def get_login_profile(query)
aws_service_client.get_login_profile(query)
end
def list_mfa_devices(query)
aws_service_client.list_mfa_devices(query)
end
end
end
end

View file

@ -0,0 +1,46 @@
class AwsKmsKeys < Inspec.resource(1)
name 'aws_kms_keys'
desc 'Verifies settings for AWS KMS Keys in bulk'
example '
describe aws_kms_keys do
it { should exist }
end
'
supports platform: 'aws'
include AwsPluralResourceMixin
def validate_params(resource_params)
unless resource_params.empty?
raise ArgumentError, 'aws_kms_keys does not accept resource parameters.'
end
resource_params
end
# Underlying FilterTable implementation.
filter = FilterTable.create
filter.add_accessor(:entries)
.add(:exists?) { |x| !x.entries.empty? }
.add(:key_arns, field: :key_arn)
.add(:key_ids, field: :key_id)
filter.connect(self, :table)
def to_s
'KMS Keys'
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
@table = backend.list_keys({ limit: 1000 }).to_h[:keys] # max value for limit is 1000
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::KMS::Client
def list_keys(query = {})
aws_service_client.list_keys(query)
end
end
end
end

View file

@ -0,0 +1,61 @@
class AwsRouteTable < Inspec.resource(1)
name 'aws_route_table'
desc 'Verifies settings for an AWS Route Table'
example "
describe aws_route_table do
its('route_table_id') { should cmp 'rtb-2c60ec44' }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
def to_s
"Route Table #{@route_table_id}"
end
attr_reader :route_table_id, :vpc_id
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:route_table_id],
allowed_scalar_name: :route_table_id,
allowed_scalar_type: String,
)
if validated_params.key?(:route_table_id) && validated_params[:route_table_id] !~ /^rtb\-[0-9a-f]{8}/
raise ArgumentError, 'aws_route_table Route Table ID must be in the' \
' format "rtb-" followed by 8 hexadecimal characters.'
end
validated_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
if @route_table_id.nil?
args = nil
else
args = { filters: [{ name: 'route-table-id', values: [@route_table_id] }] }
end
resp = backend.describe_route_tables(args)
routetable = resp.to_h[:route_tables]
@exists = !routetable.empty?
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::EC2::Client
def describe_route_tables(query)
aws_service_client.describe_route_tables(query)
end
end
end
end

View file

@ -0,0 +1,115 @@
# author: Matthew Dromazos
class AwsS3Bucket < Inspec.resource(1)
name 'aws_s3_bucket'
desc 'Verifies settings for a s3 bucket'
example "
describe aws_s3_bucket(bucket_name: 'test_bucket') do
it { should exist }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :bucket_name, :has_access_logging_enabled, :region
def to_s
"S3 Bucket #{@bucket_name}"
end
def bucket_acl
catch_aws_errors do
@bucket_acl ||= BackendFactory.create(inspec_runner).get_bucket_acl(bucket: bucket_name).grants
end
end
def bucket_policy
@bucket_policy ||= fetch_bucket_policy
end
# RSpec will alias this to be_public
def public?
# first line just for formatting
false || \
bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ } || \
bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ } || \
bucket_policy.any? { |s| s.effect == 'Allow' && s.principal == '*' }
end
def has_access_logging_enabled?
return unless @exists
catch_aws_errors do
@has_access_logging_enabled ||= !BackendFactory.create(inspec_runner).get_bucket_logging(bucket: bucket_name).logging_enabled.nil?
end
end
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:bucket_name],
allowed_scalar_name: :bucket_name,
allowed_scalar_type: String,
)
if validated_params.empty? or !validated_params.key?(:bucket_name)
raise ArgumentError, 'You must provide a bucket_name to aws_s3_bucket.'
end
validated_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
# Since there is no basic "get_bucket" API call, use the
# region fetch as the existence check.
begin
@region = backend.get_bucket_location(bucket: bucket_name).location_constraint
rescue Aws::S3::Errors::NoSuchBucket
@exists = false
return
end
@exists = true
end
def fetch_bucket_policy
backend = BackendFactory.create(inspec_runner)
catch_aws_errors do
begin
# AWS SDK returns a StringIO, we have to read()
raw_policy = backend.get_bucket_policy(bucket: bucket_name).policy
return JSON.parse(raw_policy.read)['Statement'].map do |statement|
lowercase_hash = {}
statement.each_key { |k| lowercase_hash[k.downcase] = statement[k] }
@bucket_policy = OpenStruct.new(lowercase_hash)
end
rescue Aws::S3::Errors::NoSuchBucketPolicy
@bucket_policy = []
end
end
end
# Uses the SDK API to really talk to AWS
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::S3::Client
def get_bucket_acl(query)
aws_service_client.get_bucket_acl(query)
end
def get_bucket_location(query)
aws_service_client.get_bucket_location(query)
end
def get_bucket_policy(query)
aws_service_client.get_bucket_policy(query)
end
def get_bucket_logging(query)
aws_service_client.get_bucket_logging(query)
end
end
end
end

View file

@ -0,0 +1,93 @@
class AwsSecurityGroup < Inspec.resource(1)
name 'aws_security_group'
desc 'Verifies settings for an individual AWS Security Group.'
example '
describe aws_security_group("sg-12345678") do
it { should exist }
end
'
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :description, :group_id, :group_name, :vpc_id
def to_s
"EC2 Security Group #{@group_id}"
end
private
def validate_params(raw_params)
recognized_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:id, :group_id, :group_name, :vpc_id],
allowed_scalar_name: :group_id,
allowed_scalar_type: String,
)
# id is an alias for group_id
recognized_params[:group_id] = recognized_params.delete(:id) if recognized_params.key?(:id)
if recognized_params.key?(:group_id) && recognized_params[:group_id] !~ /^sg\-[0-9a-f]{8}/
raise ArgumentError, 'aws_security_group security group ID must be in the format "sg-" followed by 8 hexadecimal characters.'
end
if recognized_params.key?(:vpc_id) && recognized_params[:vpc_id] !~ /^vpc\-[0-9a-f]{8}/
raise ArgumentError, 'aws_security_group VPC ID must be in the format "vpc-" followed by 8 hexadecimal characters.'
end
validated_params = recognized_params
if validated_params.empty?
raise ArgumentError, 'You must provide parameters to aws_security_group, such as group_name, group_id, or vpc_id.g_group.'
end
validated_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
# Transform into filter format expected by AWS
filters = []
[
:description,
:group_id,
:group_name,
:vpc_id,
].each do |criterion_name|
instance_var = "@#{criterion_name}".to_sym
next unless instance_variable_defined?(instance_var)
val = instance_variable_get(instance_var)
next if val.nil?
filters.push(
{
name: criterion_name.to_s.tr('_', '-'),
values: [val],
},
)
end
dsg_response = backend.describe_security_groups(filters: filters)
if dsg_response.security_groups.empty?
@exists = false
return
end
@exists = true
@description = dsg_response.security_groups[0].description
@group_id = dsg_response.security_groups[0].group_id
@group_name = dsg_response.security_groups[0].group_name
@vpc_id = dsg_response.security_groups[0].vpc_id
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend self
self.aws_client_class = Aws::EC2::Client
def describe_security_groups(query)
aws_service_client.describe_security_groups(query)
end
end
end
end

View file

@ -0,0 +1,68 @@
class AwsSecurityGroups < Inspec.resource(1)
name 'aws_security_groups'
desc 'Verifies settings for AWS Security Groups in bulk'
example <<-EOX
# Verify that you have security groups defined
describe aws_security_groups do
it { should exist }
end
# Verify you have more than the default security group
describe aws_security_groups do
its('entries.count') { should be > 1 }
end
EOX
supports platform: 'aws'
include AwsPluralResourceMixin
# Underlying FilterTable implementation.
filter = FilterTable.create
filter.add_accessor(:where)
.add_accessor(:entries)
.add(:exists?) { |x| !x.entries.empty? }
.add(:group_ids, field: :group_id)
filter.connect(self, :table)
def to_s
'EC2 Security Groups'
end
private
def validate_params(raw_criteria)
unless raw_criteria.is_a? Hash
raise 'Unrecognized criteria for fetching Security Groups. ' \
"Use 'criteria: value' format."
end
# No criteria yet
unless raw_criteria.empty?
raise ArgumentError, 'aws_ec2_security_groups does not currently accept resource parameters.'
end
raw_criteria
end
def fetch_from_api
@table = []
backend = BackendFactory.create(inspec_runner)
backend.describe_security_groups({}).security_groups.each do |sg_info|
@table.push({
group_id: sg_info.group_id,
group_name: sg_info.group_name,
vpc_id: sg_info.vpc_id,
})
end
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend self
self.aws_client_class = Aws::EC2::Client
def describe_security_groups(query)
aws_service_client.describe_security_groups(query)
end
end
end
end

View file

@ -0,0 +1,53 @@
class AwsSnsTopic < Inspec.resource(1)
name 'aws_sns_topic'
desc 'Verifies settings for an SNS Topic'
example "
describe aws_sns_topic('arn:aws:sns:us-east-1:123456789012:some-topic') do
it { should exist }
its('confirmed_subscription_count') { should_not be_zero }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :arn, :confirmed_subscription_count
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:arn],
allowed_scalar_name: :arn,
allowed_scalar_type: String,
)
# Validate the ARN
unless validated_params[:arn] =~ /^arn:aws:sns:[\w\-]+:\d{12}:[\S]+$/
raise ArgumentError, 'Malformed ARN for SNS topics. Expected an ARN of the form ' \
"'arn:aws:sns:REGION:ACCOUNT-ID:TOPIC-NAME'"
end
validated_params
end
def fetch_from_api
aws_response = BackendFactory.create(inspec_runner).get_topic_attributes(topic_arn: @arn).attributes
@exists = true
# The response has a plain hash with CamelCase plain string keys and string values
@confirmed_subscription_count = aws_response['SubscriptionsConfirmed'].to_i
rescue Aws::SNS::Errors::NotFound
@exists = false
end
# Uses the SDK API to really talk to AWS
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::SNS::Client
def get_topic_attributes(criteria)
aws_service_client.get_topic_attributes(criteria)
end
end
end
end

View file

@ -0,0 +1,88 @@
class AwsSubnet < Inspec.resource(1)
name 'aws_subnet'
desc 'This resource is used to test the attributes of a VPC subnet'
example "
describe aws_subnet(subnet_id: 'subnet-12345678') do
it { should exist }
its('cidr_block') { should eq '10.0.1.0/24' }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :assigning_ipv_6_address_on_creation, :availability_zone, :available_ip_address_count,
:available, :cidr_block, :default_for_az, :ipv_6_cidr_block_association_set,
:mapping_public_ip_on_launch, :subnet_id, :vpc_id
alias available? available
alias default_for_az? default_for_az
alias mapping_public_ip_on_launch? mapping_public_ip_on_launch
alias assigning_ipv_6_address_on_creation? assigning_ipv_6_address_on_creation
def to_s
"VPC Subnet #{@subnet_id}"
end
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:subnet_id],
allowed_scalar_name: :subnet_id,
allowed_scalar_type: String,
)
# Make sure the subnet_id parameter was specified and in the correct form.
if validated_params.key?(:subnet_id) && validated_params[:subnet_id] !~ /^subnet\-[0-9a-f]{8}/
raise ArgumentError, 'aws_subnet Subnet ID must be in the format "subnet-" followed by 8 hexadecimal characters.'
end
if validated_params.empty?
raise ArgumentError, 'You must provide a subnet_id to aws_subnet.'
end
validated_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
# Transform into filter format expected by AWS
filters = []
filters.push({ name: 'subnet-id', values: [@subnet_id] })
ds_response = backend.describe_subnets(filters: filters)
# If no subnets exist in the VPC, exist is false.
if ds_response.subnets.empty?
@exists = false
return
end
@exists = true
assign_properties(ds_response)
end
def assign_properties(ds_response)
@vpc_id = ds_response.subnets[0].vpc_id
@subnet_id = ds_response.subnets[0].subnet_id
@cidr_block = ds_response.subnets[0].cidr_block
@availability_zone = ds_response.subnets[0].availability_zone
@available_ip_address_count = ds_response.subnets[0].available_ip_address_count
@default_for_az = ds_response.subnets[0].default_for_az
@mapping_public_ip_on_launch = ds_response.subnets[0].map_public_ip_on_launch
@available = ds_response.subnets[0].state == 'available'
@ipv_6_cidr_block_association_set = ds_response.subnets[0].ipv_6_cidr_block_association_set
@assigning_ipv_6_address_on_creation = ds_response.subnets[0].assign_ipv_6_address_on_creation
end
# Uses the SDK API to really talk to AWS
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::EC2::Client
def describe_subnets(query)
aws_service_client.describe_subnets(query)
end
end
end
end

View file

@ -0,0 +1,53 @@
class AwsSubnets < Inspec.resource(1)
name 'aws_subnets'
desc 'Verifies settings for VPC Subnets in bulk'
example "
# you should be able to test the cidr_block of a subnet
describe aws_subnets.where(vpc_id: 'vpc-123456789') do
its('subnet_ids') { should eq ['subnet-12345678', 'subnet-87654321'] }
its('cidr_blocks') { should eq ['172.31.96.0/20'] }
its('states') { should_not include 'pending' }
end
"
supports platform: 'aws'
include AwsPluralResourceMixin
def validate_params(resource_params)
unless resource_params.empty?
raise ArgumentError, 'aws_vpc_subnets does not accept resource parameters.'
end
resource_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
@table = backend.describe_subnets.subnets.map(&:to_h)
end
# Underlying FilterTable implementation.
filter = FilterTable.create
filter.add_accessor(:where)
.add_accessor(:entries)
.add(:exists?) { |x| !x.entries.empty? }
.add(:vpc_ids, field: :vpc_id)
.add(:subnet_ids, field: :subnet_id)
.add(:cidr_blocks, field: :cidr_block)
.add(:states, field: :state)
filter.connect(self, :table)
def to_s
'EC2 VPC Subnets'
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend self
self.aws_client_class = Aws::EC2::Client
def describe_subnets(query = {})
aws_service_client.describe_subnets(query)
end
end
end
end

View file

@ -0,0 +1,69 @@
class AwsVpc < Inspec.resource(1)
name 'aws_vpc'
desc 'Verifies settings for AWS VPC'
example "
describe aws_vpc do
it { should be_default }
its('cidr_block') { should cmp '10.0.0.0/16' }
end
"
supports platform: 'aws'
include AwsSingularResourceMixin
def to_s
"VPC #{vpc_id}"
end
[:cidr_block, :dhcp_options_id, :state, :vpc_id, :instance_tenancy, :is_default].each do |property|
define_method(property) do
@vpc[property]
end
end
alias default? is_default
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:vpc_id],
allowed_scalar_name: :vpc_id,
allowed_scalar_type: String,
)
if validated_params.key?(:vpc_id) && validated_params[:vpc_id] !~ /^vpc\-[0-9a-f]{8}/
raise ArgumentError, 'aws_vpc VPC ID must be in the format "vpc-" followed by 8 hexadecimal characters.'
end
validated_params
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
if @vpc_id.nil?
filter = { name: 'isDefault', values: ['true'] }
else
filter = { name: 'vpc-id', values: [@vpc_id] }
end
resp = backend.describe_vpcs({ filters: [filter] })
@vpc = resp.vpcs[0].to_h
@vpc_id = @vpc[:vpc_id]
@exists = !@vpc.empty?
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::EC2::Client
def describe_vpcs(query)
aws_service_client.describe_vpcs(query)
end
end
end
end

View file

@ -0,0 +1,45 @@
class AwsVpcs < Inspec.resource(1)
name 'aws_vpcs'
desc 'Verifies settings for AWS VPCs in bulk'
example '
describe aws_vpcs do
it { should exist }
end
'
supports platform: 'aws'
include AwsPluralResourceMixin
# Underlying FilterTable implementation.
filter = FilterTable.create
filter.add_accessor(:entries)
.add(:exists?) { |x| !x.entries.empty? }
filter.connect(self, :table)
def validate_params(raw_params)
# No params yet
unless raw_params.empty?
raise ArgumentError, 'aws_vpcs does not accept resource parameters'
end
raw_params
end
def to_s
'VPCs'
end
def fetch_from_api
@table = BackendFactory.create(inspec_runner).describe_vpcs.to_h[:vpcs]
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::EC2::Client
def describe_vpcs(query = {})
aws_service_client.describe_vpcs(query)
end
end
end
end

View file

@ -0,0 +1,377 @@
# Base class for Azure Resources. This allows the generic class to work
# as well as the specific target resources for Azure Resources
#
# @author Russell Seymour
module Inspec::Resources
class AzureResourceBase < Inspec.resource(1)
attr_reader :opts, :client, :azure
# Constructor that retreives the specified resource
#
# The opts hash should contain the following
# :group_name - name of the resource group in which to look for items
# :type - the type of Azure resource to look for
# :apiversion - API version to use when looking for a specific resource
# :name - name of the resource to find
#
# @author Russell Seymour
#
# @param [Hash] opts Hashtable of options as highlighted above
# rubocop:disable Metrics/AbcSize
def initialize(opts)
# declare the hashtable of counts
@counts = {}
@total = 0
@opts = opts
# Determine if the environment variables for the options have been set
option_var_names = {
group_name: 'AZURE_RESOURCE_GROUP_NAME',
name: 'AZURE_RESOURCE_NAME',
type: 'AZURE_RESOURCE_TYPE',
apiversion: 'AZURE_RESOURCE_API_VERSION',
}
option_var_names.each do |option_name, env_var_name|
opts[option_name] = ENV[env_var_name] unless ENV[env_var_name].nil?
end
@azure = inspec.backend
@client = azure.azure_client
@failed_resource = false
end
def failed_resource?
@failed_resource
end
def catch_azure_errors
yield
rescue MsRestAzure::AzureOperationError => e
# e.message is actually a massive stringified JSON, which might be useful in the future.
# You want error_message here.
fail_resource e.error_message
@failed_resource = true
nil
end
# Return information about the resource group
def resource_group
catch_azure_errors do
resource_group = client.resource_groups.get(opts[:group_name])
# create the methods for the resource group object
dm = AzureResourceDynamicMethods.new
dm.create_methods(self, resource_group)
end
end
def resources
resources = nil
catch_azure_errors do
resources = client.resources.list_by_resource_group(opts[:group_name])
end
return if failed_resource?
# filter the resources based on the type, and the name if they been specified
resources = filter_resources(resources, opts)
# if there is one resource then define methods on this class
if resources.count == 1
@total = 1
resource = nil
catch_azure_errors do
# get the apiversion for the resource, if one has not been specified
apiversion = azure.get_api_version(resources[0].type, opts)
# get the resource by id so it can be interrogated
resource = client.resources.get_by_id(resources[0].id, apiversion)
end
return if failed_resource?
dm = AzureResourceDynamicMethods.new
dm.create_methods(self, resource)
else
# As there are many resources, parse each one so that it can be
# interrogated by the FilterTable
# @probes = parse_resources(resources, azure)
@probes = resources.each.map do |item|
# update the total
@total += 1
# determine the counts for each type
namespace, type_name = item.type.split(/\./)
counts.key?(namespace) ? false : counts[namespace] = {}
counts[namespace].key?(type_name) ? counts[namespace][type_name] += 1 : counts[namespace][type_name] = 1
# get the detail about the resource
apiversion = azure.get_api_version(item.type, opts)
resource = client.resources.get_by_id(item.id, apiversion)
# parse the resource
parse_resource(resource)
end.compact
# Iterate around the counts and create the necessary classes
counts.each do |namespace, ns_counts|
define_singleton_method namespace do
AzureResourceTypeCounts.new(ns_counts)
end
end
end
end
# Does the resource have any tags?
#
# If it is a Hashtable then it does not, because there was nothing to parse so there is not
# a nested object to work with
#
# @author Russell Seymour
def has_tags?
tags.is_a?(Hash) ? false : true
end
# Returns how many tags have been set on the resource
#
# @author Russell Seymour
def tag_count
tags.count
end
# It is necessary to be able to test the tags of a resource. It is possible to say of the
# resource has tags or not, and it is possible to check that the tags include a specific tag
# However the value is not accessible, this function creates methods for all the tags that
# are available.
#
# The format of the method name is '<TAG_NAME>_tag' and will return the value of that tag
#
# Disabling rubopcop check. If this is set as a normal if..then..end statement there is a
# violation stating it should use a guard. When using a guard it throws this error
#
# @author Russell Seymour
def create_tag_methods
# Iterate around the items of the tags and create the necessary access methods
tags.item.each do |name, value|
method_name = format('%s_tag', name)
define_singleton_method method_name do
value
end
end if defined?(tags.item)
end
private
# Filter the resources that are returned by the options that have been specified
#
def filter_resources(resources, opts)
if opts[:type] && opts[:name]
resources.select { |r| r.type == opts[:type] && r.name == opts[:name] }
elsif opts[:type]
resources.select { |r| r.type == opts[:type] }
elsif opts[:name]
resources.select { |r| r.name == opts[:name] }
else
resources
end
end
end
end
# Class to create methods on the calling object at run time.
# Each of the Azure Resources have different attributes and properties, and they all need
# to be testable. To do this no methods are hardcoded, each on is craeted based on the
# information returned from Azure.
#
# The class is a helper class essentially as it creates the methods on the calling class
# rather than itself. This means that there is less duplication of code and it can be
# reused easily.
#
# @author Russell Seymour
# @since 0.2.0
class AzureResourceDynamicMethods
# Given the calling object and its data, create the methods on the object according
# to the data that has been retrieved. Various types of data can be returned so the method
# checks the type to ensure that the necessary methods are configured correctly
#
# @param AzureResourceProbe|AzureResource object The object on which the methods should be craeted
# @param variant data The data from which the methods should be created
def create_methods(object, data)
# Check the type of data as this affects the setup of the methods
# If it is an Azure Generic Resource then setup methods for each of
# the instance variables
case data.class.to_s
when /^Azure::Resources::Mgmt::.*::Models::GenericResource$/,
/^Azure::Resources::Mgmt::.*::Models::ResourceGroup$/
# iterate around the instance variables
data.instance_variables.each do |var|
create_method(object, var.to_s.delete('@'), data.instance_variable_get(var))
end
# When the data is a Hash object iterate around each of the key value pairs and
# craete a method for each one.
when 'Hash'
data.each do |key, value|
create_method(object, key, value)
end
end
end
private
# Method that is responsible for creating the method on the calling object. This is
# because some nesting maybe required. For example of the value is a Hash then it will
# need to have an AzureResourceProbe create for each key, whereas if it is a simple
# string then the value just needs to be returned
#
# @private
#
# @param AzureResourceProbe|AzureResource object Object on which the methods need to be created
# @param string name The name of the method
# @param variant value The value that needs to be returned by the method
def create_method(object, name, value)
# Create the necessary method based on the var that has been passed
# Test the value for its type so that the method can be setup correctly
case value.class.to_s
when 'String', 'Integer', 'TrueClass', 'FalseClass', 'Fixnum'
object.define_singleton_method name do
value
end
when 'Hash'
value.count.zero? ? return_value = value : return_value = AzureResourceProbe.new(value)
object.define_singleton_method name do
return_value
end
when /^Azure::Resources::Mgmt::.*::Models::ResourceGroupProperties$/
# This is a special case where the properties of the resource group is not a simple JSON model
# This is because the plugin is using the Azure SDK to get this information so it is an SDK object
# that has to be interrogated in a different way. This is the only object type that behaves like this
value.instance_variables.each do |var|
create_method(object, var.to_s.delete('@'), value.instance_variable_get(var))
end
when 'Array'
# Some things are just string or integer arrays
# Check this by seeing if the first element is a string / integer / boolean or
# a hashtable
# This may not be the best methid, but short of testing all elements in the array, this is
# the quickest test
case value[0].class.to_s
when 'String', 'Integer', 'TrueClass', 'FalseClass', 'Fixnum'
probes = value
else
probes = []
value.each do |value_item|
probes << AzureResourceProbe.new(value_item)
end
end
object.define_singleton_method name do
probes
end
end
end
end
# Class object to maintain a count of the Azure Resource types that are found
# when a less specific test is carried out. For example if all the resoures of a resource
# group are called for, there will be variaous types and number of those types.
#
# Each type is namespaced, so for example a virtual machine has the type 'Microsoft.Compute/virtualMachines'
# This is broken down into the 'Microsoft' class with the type 'Compute/virtualMachines'
# This has been done for two reasons:
# 1. Enable the dotted notation to work in the test
# 2. Allow third party resource types ot be catered for if they are ever enabled by Microsoft
#
# @author Russell Seymour
# @since 0.2.0
class AzureResourceTypeCounts
# Constructor to setup a new class for a specific Azure Resource type.
# It should be passed a hashtable with information such as:
# {
# "Compute/virtualMachines" => 2,
# "Network/networkInterfaces" => 3
# }
# This will result in two methods being created on the class:
# - Compute/virtualNetworks
# - Network/networkInterfaces
# Each of which will return the corresponding count value
#
# @param Hash counts Hash table of types and the count of each one
#
# @return AzureResourceTypeCounts
def initialize(counts)
counts.each do |type, count|
define_singleton_method type do
count
end
end
end
end
# Class object that is created for each element that is returned by Azure.
# This is what is interogated by Inspec. If they are nested hashes, then this results
# in nested AzureResourceProbe objects.
#
# For example, if the following was seen in an Azure Resource
# properties -> storageProfile -> imageReference
# Would result in the following nestec classes
# AzureResource -> AzureResourceProbe -> AzureResourceProbe
#
# The methods for each of the classes are dynamically defined at run time and will
# match the items that are retrieved from Azure. See the 'test/integration/verify/controls' for
# examples
#
# This class will not be called externally
#
# @author Russell Seymour
# @since 0.2.0
# @attr_reader string name Name of the Azure resource
# @attr_reader string type Type of the Azure Resource
# @attr_reader string location Location in Azure of the resource
class AzureResourceProbe
attr_reader :name, :type, :location, :item, :count
# Initialize method for the class. Accepts an item, be it a scalar value, hash or Azure object
# It will then create the necessary dynamic methods so that they can be called in the tests
# This is accomplished by call the AzureResourceDynamicMethods
#
# @param varaint The item from which the class will be initialized
#
# @return AzureResourceProbe
def initialize(item)
dm = AzureResourceDynamicMethods.new
dm.create_methods(self, item)
# Set the item as a property on the class
# This is so that it is possible to interrogate what has been added to the class and isolate them from
# the standard methods that a Ruby class has.
# This used for checking Tags on a resource for example
# It also allows direct access if so required
@item = item
# Set how many items have been set
@count = item.length
end
# Allows resources to respond to the include test
# This means that things like tags can be checked for and then their value tested
#
# @author Russell Seymour
#
# @param [String] key Name of the item to look for in the @item property
def include?(key)
@item.key?(key)
end
# Give a sting like `computer_name` return the camelCase version, e.g.
# computerName
#
# @param string data Data that needs to be converted from snake_case to camelCase
#
# @return string
def camel_case(data)
camel_case_data = data.split('_').inject([]) { |buffer, e| buffer.push(buffer.empty? ? e : e.capitalize) }.join
# Ensure that gb (as in gigabytes) is uppercased
camel_case_data.gsub(/[gb]/, &:upcase)
end
end

View file

@ -0,0 +1,59 @@
# encoding: utf-8
require 'resources/azure/azure_backend'
require 'utils/filter'
module Inspec::Resources
class AzureGenericResource < AzureResourceBase
name 'azure_generic_resource'
desc '
Inspec Resource to interrogate any Resource type in Azure
'
supports platform: 'azure'
attr_accessor :filter, :total, :counts, :name, :type, :location, :probes
def initialize(opts = {})
# Call the parent class constructor
super(opts)
# Get the resource group
resource_group
# Get the resources
resources
# Create the tag methods
create_tag_methods
end
# Define the filter table so that it can be interrogated
@filter = FilterTable.create
@filter.add_accessor(:count)
.add_accessor(:entries)
.add_accessor(:where)
.add_accessor(:contains)
.add(:exist?, field: 'exist?')
.add(:type, field: 'type')
.add(:name, field: 'name')
.add(:location, field: 'location')
.add(:properties, field: 'properties')
@filter.connect(self, :probes)
def parse_resource(resource)
# return a hash of information
parsed = {
'location' => resource.location,
'name' => resource.name,
'type' => resource.type,
'exist?' => true,
'properties' => AzureResourceProbe.new(resource.properties),
}
parsed
end
end
end

View file

@ -0,0 +1,152 @@
# encoding: utf-8
require 'resources/azure/azure_backend'
module Inspec::Resources
class AzureResourceGroup < AzureResourceBase
name 'azure_resource_group'
desc '
Inspec Resource to get metadata about a specific Resource Group
'
supports platform: 'azure'
attr_reader :name, :location, :id, :total, :counts, :mapping
# Constructor to get the resource group itself and perform some analysis on the
# resources that in the resource group.
#
# This analysis is defined by the the mapping hashtable which is used to define
# the 'has_xxx?' methods (see AzureResourceGroup#create_has_methods) and return
# the counts for each type
#
# @author Russell Seymour
def initialize(opts)
opts.key?(:name) ? opts[:group_name] = opts[:name] : false
# Ensure that the opts only have the name of the resource group set
opts.select! { |k, _v| k == :group_name }
super(opts)
# set the mapping for the Azure Resources
@mapping = {
nic: 'Microsoft.Network/networkInterfaces',
vm: 'Microsoft.Compute/virtualMachines',
extension: 'Microsoft.Compute/virtualMachines/extensions',
nsg: 'Microsoft.Network/networkSecurityGroups',
vnet: 'Microsoft.Network/virtualNetworks',
managed_disk: 'Microsoft.Compute/disks',
managed_disk_image: 'Microsoft.Compute/images',
sa: 'Microsoft.Storage/storageAccounts',
public_ip: 'Microsoft.Network/publicIPAddresses',
}
# Get information about the resource group itself
resource_group
# Get information about the resources in the resource group
resources
# Call method to create the has_xxxx? methods
create_has_methods
# Call method to allow access to the tag values
create_tag_methods
end
# Return the provisioning state of the resource group
#
# @author Russell Seymour
def provisioning_state
properties.provisioningState
end
# Analyze the fully qualified id of the resource group to return the subscription id
# that this resource group is part of
#
# The format of the id is
# /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>
#
# @author Russell Seymour
def subscription_id
id.split(%r{\/}).reject(&:empty?)[1]
end
# Method to parse the resources that have been returned
# This allows the calculations of the amount of resources to be determined
#
# @author Russell Seymour
#
# @param [Hash] resource A hashtable representing the resource group
def parse_resource(resource)
# return a hash of information
parsed = {
'name' => resource.name,
'type' => resource.type,
}
parsed
end
# This method catches the xxx_count calls that are made on the resource.
#
# The method that is called is stripped of '_count' and then compared with the
# mappings table. If that type exists then the number of those items is returned.
# However if that type is not in the Resource Group then the method will return
# a NoMethodError exception
#
# @author Russell Seymour
#
# @param [Symbol] method_id The name of the method that was called
def method_missing(method_id)
# Determine the mapping_key based on the method_id
mapping_key = method_id.to_s.chomp('_count').to_sym
if mapping.key?(mapping_key)
# based on the method id get the
namespace, type_name = mapping[mapping_key].split(/\./)
# check that the type_name is defined, if not return 0
if send(namespace).methods.include?(type_name.to_sym)
# return the count for the method id
send(namespace).send(type_name)
else
0
end
else
msg = format('undefined method `%s` for %s', method_id, self.class)
raise NoMethodError, msg
end
end
private
# For each of the mappings this method creates the has_xxx? method. This allows the use
# of the following type of test
#
# it { should have_nics }
#
# For example, it will create a has_nics? method that returns a boolean to state of the
# resource group has any nics at all.
#
# @author Russell Seymour
# @private
def create_has_methods
return if failed_resource?
# Create the has methods for each of the mappings
# This is a quick test to show that the resource group has at least one of these things
mapping.each do |name, type|
# Determine the name of the method name
method_name = format('has_%ss?', name)
namespace, type_name = type.split(/\./)
# use the namespace and the type_name to determine if the resource group has this type or not
result = send(namespace).methods.include?(type_name.to_sym) ? true : false
define_singleton_method method_name do
result
end
end
end
end
end

View file

@ -0,0 +1,264 @@
# encoding: utf-8
require 'resources/azure/azure_backend'
module Inspec::Resources
class AzureVirtualMachine < AzureResourceBase
name 'azure_virtual_machine'
desc '
Inspec Resource to test Azure Virtual Machines
'
supports platform: 'azure'
# Constructor for the resource. This calls the parent constructor to
# get the generic resource for the specified machine. This will provide
# static methods that are documented
#
# @author Russell Seymour
def initialize(opts = {})
# The generic resource needs to pass back a Microsoft.Compute/virtualMachines object so force it
opts[:type] = 'Microsoft.Compute/virtualMachines'
super(opts)
# Find the virtual machines
resources
create_tag_methods
end
# Method to catch calls that are not explicitly defined.
# This allows the simple attributes of the virtual machine to be read without having
# to define each one in turn.
#
# rubocop:disable Metrics/AbcSize
#
# @param symobl method_id The symbol of the method that has been called
#
# @return Value of attribute that has been called
def method_missing(method_id)
# Depending on the method that has been called, determine what value should be returned
# These are set as camel case methods to comply with rubocop
image_reference_attrs = %w{sku publisher offer}
osdisk_attrs = %w{os_type caching create_option disk_size_gb}
hardware_profile_attrs = %w{vm_size}
os_profile_attrs = %w{computer_name admin_username}
osdisk_managed_disk_attrs = %w{storage_account_type}
# determine the method name to call by converting the snake_case to camelCase
# method_name = self.camel_case(method_id.to_s)
method_name = method_id.to_s.split('_').inject([]) { |buffer, e| buffer.push(buffer.empty? ? e : e.capitalize) }.join
method_name.end_with?('Gb') ? method_name.gsub!(/Gb/, &:upcase) : false
if image_reference_attrs.include?(method_id.to_s)
properties.storageProfile.imageReference.send(method_name)
elsif osdisk_attrs.include?(method_id.to_s)
properties.storageProfile.osDisk.send(method_name)
elsif hardware_profile_attrs.include?(method_id.to_s)
properties.hardwareProfile.send(method_name)
elsif os_profile_attrs.include?(method_id.to_s)
properties.osProfile.send(method_name)
elsif osdisk_managed_disk_attrs.include?(method_id.to_s)
properties.storageProfile.osDisk.managedDisk.send(method_name)
end
end
# Return the name of the os disk
#
# @return string Name of the OS disk
def os_disk_name
properties.storageProfile.osDisk.name
end
# Determine if the OS disk is a managed disk
#
# @return boolean
def has_managed_osdisk?
defined?(properties.storageProfile.osDisk.managedDisk)
end
# Does the machine have any NICs connected
#
# @return boolean
def has_nics?
properties.networkProfile.networkInterfaces.count != 0
end
# How many NICs are connected to the machine
#
# @return integer
def nic_count
properties.networkProfile.networkInterfaces.count
end
# Return an array of the connected NICs so that it can be tested to ensure
# the machine is connected properly
#
# @return array Array of NIC names connected to the machine
def connected_nics
nic_names = []
properties.networkProfile.networkInterfaces.each do |nic|
nic_names << nic.id.split(%r{/}).last
end
nic_names
end
# Whether the machine has data disks or not
#
# @return boolean
def has_data_disks?
properties.storageProfile.dataDisks.count != 0
end
# How many data disks are connected
#
# @return integer
def data_disk_count
properties.storageProfile.dataDisks.count
end
# Does the machine allow password authentication
#
# This allows the use of
# it { should have_password_authentication }
# within the Inspec profile
#
# @return boolean
def has_password_authentication?
password_authentication?
end
# Deteremine if the machine allows password authentication
#
# @return boolean
def password_authentication?
# if the osProfile property has a linuxConfiguration section then interrogate that
# otherwise it is a Windows machine and that always has password auth
if defined?(properties.osProfile.linuxConfiguration)
!properties.osProfile.linuxConfiguration.disablePasswordAuthentication
else
true
end
end
# Has the machine been given Custom Data at creation
#
# This allows the use of
# it { should have_custom_data }
# within the Inspec Profile
#
# @return boolean
def has_custom_data?
custom_data?
end
# Determine if custom data has been set
#
# @return boolean
def custom_data?
if defined?(properties.osProfile.CustomData)
true
else
false
end
end
# Are any SSH Keys assigned to the machine
#
# This allows the use of
# it { should have_ssh_keys }
# within the Inspec Profile
#
# @return boolean
def has_ssh_keys?
ssh_keys?
end
# Determine if any ssh keys have been asigned to the machine
#
# @return boolean
def ssh_keys?
if defined?(properties.osProfile.linuxConfiguration.ssh)
properties.osProfile.linuxConfiguration.ssh.publicKeys != 0
else
false
end
end
# Return the number of ssh keys that have been assigned to the machine
#
# @return integer
def ssh_key_count
if defined?(properties.osProfile.linuxConfiguration.ssh)
properties.osProfile.linuxConfiguration.ssh.publicKeys.count
else
0
end
end
# Determine is the specified key is in the ssh_keys list
#
# @return array Array of the public keys that are assigned to allow for testing of that key
def ssh_keys
# iterate around the keys
keys = []
properties.osProfile.linuxConfiguration.ssh.publicKeys.each do |key|
keys << key.keyData
end
keys
end
# Does the machine have boot diagnostics enabled
#
# @return boolean
def has_boot_diagnostics?
if defined?(properties.diagnosticsProfile)
properties.diagnosticsProfile.bootDiagnostics.enabled
else
false
end
end
# Return the URI that has been set for the boot diagnostics storage
#
# @return string
def boot_diagnostics_storage_uri
properties.diagnosticsProfile.bootDiagnostics.storageUri
end
# If this is a windows machine, returns whether the agent was provisioned or not
#
# @return boolean
def has_provision_vmagent?
if defined?(properties.osProfile.windowsConfiguration)
properties.osProfile.windowsConfiguration.provisionVMAgent
else
false
end
end
# If a windows machine see if automatic updates for the agent are enabled
#
# @return boolean
def has_automatic_agent_update?
if defined?(properties.osProfile.windowsConfiguration)
properties.osProfile.windowsConfiguration.enableAutomaticUpdates
else
false
end
end
# If this is a windows machine return a boolean to state of the WinRM options
# have been set
#
# @return boolean
def has_winrm_options?
if defined?(properties.osProfile.windowsConfiguration) && defined?(properties.osProfile.windowsConfiguration.winrm)
properties.osProfile.windowsConfiguration.winrm.protocol
else
false
end
end
end
end

View file

@ -0,0 +1,136 @@
# encoding: utf-8
require 'resources/azure/azure_backend'
require 'uri'
module Inspec::Resources
class AzureVirtualMachineDataDisk < AzureResourceBase
name 'azure_virtual_machine_data_disk'
desc '
Inspec Resource to ensure that the data disks attached to a machine are correct
'
supports platform: 'azure'
# Create a filter table so that tests on the disk can be performed
filter = FilterTable.create
filter.add_accessor(:where)
.add_accessor(:entries)
.add_accessor(:has_data_disks?)
.add_accessor(:count)
.add(:exists?) { |x| !x.entries.empty? }
.add(:disk, field: :disk)
.add(:number, field: :number)
.add(:name, field: :name)
.add(:size, field: :size)
.add(:vhd_uri, field: :vhd_uri)
.add(:storage_account_name, field: :storage_account_name)
.add(:lun, field: :lun)
.add(:caching, field: :caching)
.add(:create_option, field: :create_option)
.add(:is_managed_disk?, field: :is_managed_disk?)
.add(:storage_account_type, field: :storage_account_type)
.add(:subscription_id, field: :subscription_id)
.add(:resource_group, field: :resource_group)
filter.connect(self, :datadisk_details)
# Constructor for the resource. This calls the parent constructor to
# get the generic resource for the specified machine. This will provide
# static methods that are documented
#
# @author Russell Seymour
def initialize(opts = {})
# The generic resource needs to pass back a Microsoft.Compute/virtualMachines object so force it
opts[:type] = 'Microsoft.Compute/virtualMachines'
super(opts)
# Get the data disks
resources
end
# Return information about the disks and add to the filter table so that
# assertions can be performed
#
# @author Russell Seymour
def datadisk_details
return if failed_resource?
# Iterate around the data disks on the machine
properties.storageProfile.dataDisks.each_with_index.map do |datadisk, index|
# Call function to parse the data disks and return an object based on the parameters
parse_datadisk(datadisk, index)
end
end
# Return boolean to denote if the machine has data disks attached or not
def has_data_disks?
!entries.empty?
end
# Return an integer stating how many data disks are attached to the machine
def count
entries.count
end
# Return boolean to state if the machine is using managed disks for data disks
def has_managed_disks?
# iterate around the entries
result = entries.each.select { |e| e[:is_managed_disk?] }
result.empty? ? false : true
end
private
# Parse the data disk to determine if these are managed disks or in a storage account
# for example. The disk index, name and size will be returned
#
# params object disk Object containing the details of the disk
# params integer index Index denoting which disk number this is on the machine
#
# return hashtable
def parse_datadisk(disk, index)
# Configure parsed hashtable to hold the information
# Initialise this with common attributes from the different types of disk
parsed = {
disk: index,
number: index + 1,
lun: disk.lun,
name: disk.name,
size: disk.diskSizeGB,
caching: disk.caching,
create_option: disk.createOption,
}
# Determine if the current disk is a managed disk or not
if defined?(disk.vhd)
# As this is in a storage account this is not a managed disk
parsed[:is_managed_disk?] = false
# Set information about the disk
# Parse the uri of the disk URI so that the storage account can be retrieved
uri = URI.parse(disk.vhd.uri)
parsed[:vhd_uri] = disk.vhd.uri
parsed[:storage_account_name] = uri.host.split('.').first
elsif defined?(disk.managedDisk)
# State that this is a managed disk
parsed[:is_managed_disk?] = true
# Get information about the managed disk
parsed[:storage_account_type] = disk.managedDisk.storageAccountType
parsed[:id] = disk.managedDisk.id
# Break up the ID string so that the following information can get retreived
# - subscription_id
# - resource_group
id_parts = parsed[:id].split(%r{/}).reject(&:empty?)
parsed[:subscription_id] = id_parts[1]
parsed[:resource_group] = id_parts[3]
end
# return the parsed object
parsed
end
end
end

View file

@ -22,23 +22,27 @@ module Inspec::Resources
its('Content-Length') { should cmp 258 }
its('Content-Type') { should cmp 'text/html; charset=UTF-8' }
end
# properly execute the HTTP call on the scanned machine instead of the
# machine executing InSpec. This will be the default behavior in InSpec 2.0.
describe http('http://localhost:8080', enable_remote_worker: true) do
its('body') { should cmp 'local web server on target machine' }
end
"
def initialize(url, opts = {})
@url = url
@opts = opts
if use_remote_worker?
return skip_resource 'curl is not available on the target machine' unless inspec.command('curl').exist?
@worker = Worker::Remote.new(inspec, http_method, url, opts)
else
# Prior to InSpec 2.0 the HTTP test had to be instructed to run on the
# remote target machine. This warning will be removed after a few months
# to give users an opportunity to remove the unused option from their
# profiles.
if opts.key?(:enable_remote_worker) && !inspec.local_transport?
warn 'Ignoring `enable_remote_worker` option, the `http` resource ',
'remote worker is enabled by default for remote targets and ',
'cannot be disabled'
end
# Run locally if InSpec is ran locally and remotely if ran remotely
if inspec.local_transport?
@worker = Worker::Local.new(http_method, url, opts)
else
@worker = Worker::Remote.new(inspec, http_method, url, opts)
end
end
@ -62,17 +66,6 @@ module Inspec::Resources
"http #{http_method} on #{@url}"
end
private
def use_remote_worker?
return false if inspec.local_transport?
return true if @opts[:enable_remote_worker]
warn "[DEPRECATION] #{self} will execute locally instead of the target machine. To execute remotely, add `enable_remote_worker: true`."
warn '[DEPRECATION] `enable_remote_worker: true` will be the default behavior in InSpec 2.0.'
false
end
class Worker
class Base
attr_reader :http_method, :opts, :url
@ -154,6 +147,11 @@ module Inspec::Resources
attr_reader :inspec
def initialize(inspec, http_method, url, opts)
unless inspec.command('curl').exist?
raise Inspec::Exceptions::ResourceSkipped,
'curl is not available on the target machine'
end
@inspec = inspec
super(http_method, url, opts)
end

View file

@ -29,16 +29,6 @@ module Inspec::Resources
end
end
# helper to collect a hash object easily
def params
{
name: name,
family: @platform[:family],
release: @platform[:release],
arch: @platform[:arch],
}
end
def to_s
'Operating System Detection'
end

View file

@ -26,7 +26,6 @@ module Inspec::Resources
describe passwd.uids(0) do
its('users') { should cmp 'root' }
its('count') { should eq 1 }
end
describe passwd.shells(/nologin/) do
@ -60,21 +59,6 @@ module Inspec::Resources
.add(:homes, field: 'home')
.add(:shells, field: 'shell')
filter.add(:count) { |t, _|
warn '[DEPRECATION] `passwd.count` is deprecated. Please use `passwd.entries.length` instead. It will be removed in the next major version.'
t.entries.length
}
filter.add(:usernames) { |t, x|
warn '[DEPRECATION] `passwd.usernames` is deprecated. Please use `passwd.users` instead. It will be removed in the next major version.'
t.users(x)
}
filter.add(:username) { |t, x|
warn '[DEPRECATION] `passwd.username` is deprecated. Please use `passwd.users` instead. It will be removed in the next major version.'
t.users(x)[0]
}
# rebuild the passwd line from raw content
filter.add(:content) { |t, _|
t.entries.map do |e|
@ -82,11 +66,6 @@ module Inspec::Resources
end.join("\n")
}
def uid(x)
warn '[DEPRECATION] `passwd.uid(arg)` is deprecated. Please use `passwd.uids(arg)` instead. It will be removed in the next major version.'
uids(x)
end
filter.connect(self, :params)
def to_s

View file

@ -15,32 +15,22 @@ module Inspec::Resources
"
def initialize
@platform = inspec.backend.os
@platform = inspec.backend.platform
end
# add helper methods for easy access of properties
%w{family release arch}.each do |property|
define_method(property.to_sym) do
@platform.send(property)
@platform[property]
end
end
# This is a string override for platform.name.
# TODO: removed in inspec 2.0
class NameCleaned < String
def ==(other)
if other =~ /[A-Z ]/
cleaned = other.downcase.tr(' ', '_')
Inspec::Log.warn "[DEPRECATED] Platform names will become lowercase in InSpec 2.0. Please match on '#{cleaned}' instead of '#{other}'"
super(cleaned)
else
super(other)
end
end
def families
@platform.family_hierarchy
end
def name
NameCleaned.new(@platform.name)
@platform.name
end
def [](key)
@ -60,8 +50,19 @@ module Inspec::Resources
@platform.family_hierarchy.include?(family)
end
def families
@platform.family_hierarchy
def params
h = {
name: name,
families: families,
release: release,
}
# Avoid adding Arch for APIs (not applicable)
unless in_family?('api')
h[:arch] = arch
end
h
end
def supported?(supports)
@ -70,11 +71,7 @@ module Inspec::Resources
status = true
supports.each do |s|
s.each do |k, v|
# ignore the inspec check for supports
# TODO: remove in inspec 2.0
if k == :inspec
next
elsif %i(os_family os-family platform_family platform-family).include?(k)
if %i(os_family os-family platform_family platform-family).include?(k)
status = in_family?(v)
elsif %i(os platform).include?(k)
status = platform?(v)

View file

@ -54,18 +54,6 @@ class SimpleConfig
end
def parse_params_line(line, opts)
# Deprecation handling
if opts.key?(:assignment_re)
warn '[DEPRECATION] `:assignment_re` is deprecated in favor of `:assignment_regex` '\
'and will be removed in the next major version. See: https://github.com/chef/inspec/issues/1709'
opts[:assignment_regex] = opts[:assignment_re]
end
if opts.key?(:key_vals)
warn '[DEPRECATION] `:key_vals` is deprecated in favor of `:key_values` '\
'and will be removed in the next major version. See: https://github.com/chef/inspec/issues/1709'
opts[:key_values] = opts[:key_vals]
end
# now line contains what we are interested in parsing
# check if it is an assignment
m = opts[:assignment_regex].match(line)

View file

@ -227,7 +227,7 @@ namespace :docs do # rubocop:disable Metrics/BlockLength
list = ''
resources.each do |file|
name = File.basename(file).sub(/\.md\.erb$/, '')
list << f.li(f.a(name.sub('_', '\\_'), 'resources/' + name + '.html'))
list << f.li(f.a(name.gsub('_', '\\_'), 'resources/' + name + '.html'))
end
res << f.ul(list)
dst = File.join(src, 'resources.md')

Some files were not shown because too many files have changed in this diff Show more