Add new resource: aws_ebs_volume (#3381)

* Added support for basic AWS EBS volume testing
* Fix error in exists matcher
* Added EBS resource documentation and requested changes

Signed-off-by: James Massardo <jmassardo@chef.io>
This commit is contained in:
James Massardo 2018-09-21 08:49:28 -07:00 committed by Jared Quick
parent a91892af51
commit 2af1535f7c
7 changed files with 534 additions and 0 deletions

View file

@ -0,0 +1,76 @@
---
title: About the aws_ebs_volume Resource
platform: aws
---
# aws\_ebs\_volume
Use the `aws_ebs_volume` InSpec audit resource to test properties of a single AWS EBS volume.
<br>
## Availability
### Installation
This resource is distributed along with InSpec itself. You can use it automatically.
## Syntax
An `aws_ebs_volume` resource block declares the tests for a single AWS EBS volume by either name or id.
describe aws_ebs_volume('vol-01a2349e94458a507') do
it { should exist }
end
describe aws_ebs_volume(name: 'data-vol') do
it { should be_encrypted }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
### Test that an EBS Volume does not exist
describe aws_ebs_volume(name: 'data_vol') do
it { should_not exist }
end
### Test that an EBS Volume is encrypted
describe aws_ebs_volume(name: 'secure_data_vol') do
it { should be_encrypted }
end
### Test that an EBS Volume the correct size
describe aws_ebs_volume(name: 'data_vol') do
its('size') { should cmp 32 }
end
<br>
## Properties
* `availability_zone`, `encrypted`, `iops`, `kms_key_id`, `size`, `snapshot_id`, `state`, `volume_type`
<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\_encrypted
The `be_encrypted` matcher tests if the described EBS Volume is encrypted.
it { should be_encrypted }
## AWS Permissions
Your [Principal](https://docs.aws.amazon.com/IAM/latest/UserGuide/intro-structure.html#intro-structure-principal) will need the `ec2:DescribeVolumes`, and `iam:GetInstanceProfile` actions set to allow.
You can find detailed documentation at [Actions, Resources, and Condition Keys for Amazon EC2](https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazonec2.html), and [Actions, Resources, and Condition Keys for Identity And Access Management](https://docs.aws.amazon.com/IAM/latest/UserGuide/list_identityandaccessmanagement.html).

View file

@ -0,0 +1,86 @@
---
title: About the aws_ebs_volumes Resource
platform: aws
---
# aws\_ebs\_volumes
Use the `aws_ebs_volumes` InSpec audit resource to test properties of some or all AWS EBS volumes. To audit a single EBS volume, use `aws_ebs_volume` (singular).
EBS volumes are persistent block storage volumes for use with Amazon EC2 instances in the AWS Cloud.
Each EBS volume is uniquely identified by its ID.
<br>
## Availability
### Installation
This resource is distributed along with InSpec itself. You can use it automatically.
## Syntax
An `aws_ebs_volumes` resource block collects a group of EBS volumes and then tests that group.
# Ensure you have exactly 3 volumes
describe aws_ebs_volumes do
its('volume_ids.count') { should cmp 3 }
end
# Use the InSpec resource to enumerate IDs, then test in-depth using `aws_ebs_volume`.
aws_ebs_volumes.volume_ids.each do |volume_id|
describe aws_ebs_volume(volume_id) do
it { should exist }
it { should be_encrypted }
its('size') { should cmp 8 }
its('iops') { should cmp 100 }
end
end
<br>
## Examples
As this is the initial release of `aws_ebs_volumes`, its limited functionality precludes examples.
<br>
## Filter Criteria
This resource currently does not support any filter criteria; it will always fetch all volumes in the region.
## Properties
### entries
Provides access to the raw results of the query, which can be treated as an array of hashes. This can be useful for checking counts and other advanced operations.
# Allow at most 100 EBS volumes on the account
describe aws_ebs_volumes do
its('entries.count') { should be <= 100 }
end
### volume_ids
Provides a list of the volume ids that were found in the query.
describe aws_ebs_volumes do
its('volume_ids') { should include 'vol-12345678' }
its('volume_ids.count') { should cmp 3 }
end
<br>
## Matchers
For a full list of available matchers, please visit our [Universal Matchers page](https://www.inspec.io/docs/reference/matchers/).
### exist
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 EBS volume exists
describe aws_ebs_volumes do
it { should exist }
end

View file

@ -19,6 +19,8 @@ require 'resources/aws/aws_cloudwatch_log_metric_filter'
require 'resources/aws/aws_config_delivery_channel'
require 'resources/aws/aws_config_recorder'
require 'resources/aws/aws_ec2_instance'
require 'resources/aws/aws_ebs_volume'
require 'resources/aws/aws_ebs_volumes'
require 'resources/aws/aws_flow_log'
require 'resources/aws/aws_ec2_instances'
require 'resources/aws/aws_ecs_cluster'

View file

@ -0,0 +1,122 @@
class AwsEbsVolume < Inspec.resource(1)
name 'aws_ebs_volume'
desc 'Verifies settings for an EBS volume'
example <<-EOX
describe aws_ebs_volume('vol-123456') do
it { should be_encrypted }
its('size') { should cmp 8 }
end
describe aws_ebs_volume(name: 'my-volume') do
its('encrypted') { should eq true }
its('iops') { should cmp 100 }
end
EOX
supports platform: 'aws'
# TODO: rewrite to avoid direct injection, match other resources, use AwsSingularResourceMixin
def initialize(opts, conn = nil)
@opts = opts
@display_name = opts.is_a?(Hash) ? @opts[: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, {})
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 @volume_id if defined?(@volume_id)
catch_aws_errors do
if @opts.is_a?(Hash)
first = @ec2_resource.volumes(
{
filters: [{
name: 'tag:Name',
values: [@opts[:name]],
}],
},
).first
# catch case where the volume is not known
@volume_id = first.id unless first.nil?
else
@volume_id = @opts
end
end
end
alias volume_id id
def exists?
!volume.nil?
end
def encrypted?
volume.encrypted
end
# attributes that we want to expose
%w{
availability_zone encrypted iops kms_key_id size snapshot_id state volume_type
}.each do |attribute|
define_method attribute do
catch_aws_errors do
volume.send(attribute) if volume
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 ||= volume.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 ||= volume.security_groups.map(&:group_id)
end
end
def tags
catch_aws_errors do
@tags ||= volume.tags.map { |tag| { key: tag.key, value: tag.value } }
end
end
def to_s
"EBS Volume #{@display_name}"
end
private
def volume
catch_aws_errors { @volume ||= @ec2_resource.volume(id) }
end
end

View file

@ -0,0 +1,63 @@
class AwsEbsVolumes < Inspec.resource(1)
name 'aws_ebs_volumes'
desc 'Verifies settings for AWS EBS Volumes in bulk'
example '
describe aws_ebs_volumes do
it { should exist }
end
'
supports platform: 'aws'
include AwsPluralResourceMixin
def validate_params(resource_params)
unless resource_params.empty?
raise ArgumentError, 'aws_ebs_volumes does not accept resource parameters.'
end
resource_params
end
# Underlying FilterTable implementation.
filter = FilterTable.create
filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
filter.register_column(:volume_ids, field: :volume_id)
filter.install_filter_methods_on_resource(self, :table)
def to_s
'EBS Volumes'
end
def fetch_from_api
backend = BackendFactory.create(inspec_runner)
@table = []
pagination_opts = {}
loop do
api_result = backend.describe_volumes(pagination_opts)
@table += unpack_describe_volumes_response(api_result.volumes)
break unless api_result.next_token
pagination_opts = { next_token: api_result.next_token }
end
end
def unpack_describe_volumes_response(volumes)
volume_rows = []
volumes.each do |res|
volume_rows += res.attachments.map do |volume_struct|
{
volume_id: volume_struct.volume_id,
}
end
end
volume_rows
end
class Backend
class AwsClientApi < AwsBackendBase
BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::EC2::Client
def describe_volumes(query)
aws_service_client.describe_volumes(query)
end
end
end
end

View file

@ -0,0 +1,73 @@
require 'helper'
class TestEbs < Minitest::Test
Id = 'volume-id'.freeze
def setup
@mock_conn = Minitest::Mock.new
@mock_client = Minitest::Mock.new
@mock_resource = Minitest::Mock.new
@mock_conn.expect :ec2_client, @mock_client
@mock_conn.expect :ec2_resource, @mock_resource
end
def test_that_id_returns_id_directly_when_constructed_with_an_id
assert_equal Id, AwsEbsVolume.new(Id, @mock_conn).id
end
def test_that_id_returns_fetched_id_when_constructed_with_a_name
mock_volume = Minitest::Mock.new
mock_volume.expect :nil?, false
mock_volume.expect :id, Id
@mock_resource.expect :volumes, [mock_volume], [Hash]
assert_equal Id, AwsEbsVolume.new({ name: 'cut' }, @mock_conn).id
end
def test_that_volume_returns_volume_when_volume_exists
mock_volume = Object.new
@mock_resource.expect :volume, mock_volume, [Id]
assert_same(
mock_volume,
AwsEbsVolume.new(Id, @mock_conn).send(:volume),
)
end
def test_that_volume_returns_nil_when_volume_does_not_exist
@mock_resource.expect :volume, nil, [Id]
assert AwsEbsVolume.new(Id, @mock_conn).send(:volume).nil?
end
def test_that_exists_returns_true_when_volume_exists
mock_volume = Minitest::Mock.new
mock_volume.expect :nil?, false
mock_volume.expect :exists?, true
@mock_resource.expect :volume, mock_volume, [Id]
assert AwsEbsVolume.new(Id, @mock_conn).exists?
end
def test_that_exists_returns_false_when_volume_does_not_exist
mock_volume = Minitest::Mock.new
mock_volume.expect :nil?, true
mock_volume.expect :exists?, false
@mock_resource.expect :volume, mock_volume, [Id]
refute AwsEbsVolume.new(Id, @mock_conn).exists?
end
def test_that_encrypted_returns_true_when_volume_is_encrypted
mock_volume = Minitest::Mock.new
mock_volume.expect :nil?, false
mock_volume.expect :encrypted, true
@mock_resource.expect :volume, mock_volume, [Id]
assert AwsEbsVolume.new(Id, @mock_conn).encrypted?
end
def test_that_encrypted_returns_false_when_volume_is_not_encrypted
mock_volume = Minitest::Mock.new
mock_volume.expect :nil?, false
mock_volume.expect :encrypted, false
@mock_resource.expect :volume, mock_volume, [Id]
refute AwsEbsVolume.new(Id, @mock_conn).encrypted?
end
end

View file

@ -0,0 +1,112 @@
require 'helper'
# MAEIPB = MockAwsEbsVolumesPluralBackend
# Abbreviation not used outside this file
#=============================================================================#
# Constructor Tests
#=============================================================================#
class AwsEbsVolumesConstructorTest < Minitest::Test
def setup
AwsEbsVolumes::BackendFactory.select(MAEIPB::Empty)
end
def test_empty_params_ok
AwsEbsVolumes.new
end
def test_rejects_unrecognized_params
assert_raises(ArgumentError) { AwsEbsVolumes.new(shoe_size: 9) }
end
end
#=============================================================================#
# Search / Recall
#=============================================================================#
class AwsEbsVolumesRecallEmptyTest < Minitest::Test
def setup
AwsEbsVolumes::BackendFactory.select(MAEIPB::Empty)
end
def test_recall_when_no_volumes_exist
refute AwsEbsVolumes.new.exists?
end
end
class AwsEbsVolumesRecallBasicTest < Minitest::Test
def setup
AwsEbsVolumes::BackendFactory.select(MAEIPB::Basic)
end
def test_recall_when_some_volumes_exist
assert AwsEbsVolumes.new.exists?
end
end
#=============================================================================#
# Properties
#=============================================================================#
class AwsEbsVolumesProperties < Minitest::Test
def setup
AwsEbsVolumes::BackendFactory.select(MAEIPB::Basic)
end
def test_property_volume_ids_when_no_volumes_exist
AwsEbsVolumes::BackendFactory.select(MAEIPB::Empty)
empty = AwsEbsVolumes.new
assert_kind_of(Array, empty.volume_ids)
assert_empty(empty.volume_ids)
end
def test_property_volume_ids_when_volumes_exist
basic = AwsEbsVolumes.new
assert_kind_of(Array, basic.volume_ids)
assert(basic.volume_ids.include?('vol-deadbeef'))
assert_equal(3, basic.volume_ids.length)
assert(basic.volume_ids.include?('vol-11112222'))
refute(basic.volume_ids.include?(nil))
end
end
#=============================================================================#
# Test Fixtures
#=============================================================================#
module MAEIPB
class Empty < AwsBackendBase
def describe_volumes(query = {})
OpenStruct.new( volumes: [] )
end
end
class Basic < AwsBackendBase
def describe_volumes(query = {})
Aws::EC2::Types::DescribeVolumesResult.new(
volumes: [
Aws::EC2::Types::Volume.new(
attachments: [
Aws::EC2::Types::VolumeAttachment.new(
# Many, many other properties available here.
# We're starting with what we support.
volume_id: 'vol-0e8541d718e67e1be'
),
Aws::EC2::Types::VolumeAttachment.new(
volume_id: 'vol-deadbeef'
),
],
),
Aws::EC2::Types::Volume.new(
attachments: [
Aws::EC2::Types::VolumeAttachment.new(
volume_id: 'vol-11112222'
),
],
),
]
)
end
end
end