mirror of
https://github.com/inspec/inspec
synced 2024-11-22 20:53:11 +00:00
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:
parent
a91892af51
commit
2af1535f7c
7 changed files with 534 additions and 0 deletions
76
docs/resources/aws_ebs_volume.md.erb
Normal file
76
docs/resources/aws_ebs_volume.md.erb
Normal 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).
|
86
docs/resources/aws_ebs_volumes.md.erb
Normal file
86
docs/resources/aws_ebs_volumes.md.erb
Normal 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
|
|
@ -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'
|
||||
|
|
122
lib/resources/aws/aws_ebs_volume.rb
Normal file
122
lib/resources/aws/aws_ebs_volume.rb
Normal 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
|
63
lib/resources/aws/aws_ebs_volumes.rb
Normal file
63
lib/resources/aws/aws_ebs_volumes.rb
Normal 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
|
73
test/unit/resources/aws_ebs_volume_test.rb
Normal file
73
test/unit/resources/aws_ebs_volume_test.rb
Normal 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
|
112
test/unit/resources/aws_ebs_volumes_test.rb
Normal file
112
test/unit/resources/aws_ebs_volumes_test.rb
Normal 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
|
Loading…
Reference in a new issue