mirror of
https://github.com/inspec/inspec
synced 2024-11-27 07:00:39 +00:00
Add aws_sns_topic resource (#120)
* Docs first draft, integration tests, and constructor unit tests for SNS topic Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com> * Skeleton of SNS topic Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com> * Constructor arg validation works Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com> * Passing unit tests for recall Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com> * Subscription Count property, works Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com> * Subscription, not subscriber Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com> * Integration tests pass; also wildard ARNs are not allowed Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com> * Rubocop changes Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com> * Doc updates per kagarmoe Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
parent
c99f7e318f
commit
ab2170f717
7 changed files with 360 additions and 0 deletions
58
docs/resources/aws_sns_topic.md
Normal file
58
docs/resources/aws_sns_topic.md
Normal file
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
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 will place events in the SNS topic, while other AWS resources will _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].
|
||||
|
||||
## 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
|
||||
|
||||
## 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
|
|
@ -14,6 +14,10 @@ class AWSConnection
|
|||
Aws.config.update(opts)
|
||||
end
|
||||
|
||||
def sns_client
|
||||
@sns_client ||= Aws::SNS::Client.new
|
||||
end
|
||||
|
||||
def ec2_resource
|
||||
@ec2_resource ||= Aws::EC2::Resource.new
|
||||
end
|
||||
|
|
100
libraries/aws_sns_topic.rb
Normal file
100
libraries/aws_sns_topic.rb
Normal file
|
@ -0,0 +1,100 @@
|
|||
require 'aws_conn'
|
||||
|
||||
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
|
||||
"
|
||||
|
||||
attr_reader :arn, :confirmed_subscription_count
|
||||
|
||||
def initialize(raw_params)
|
||||
validated_params = validate_params(raw_params)
|
||||
@arn = validated_params[:arn]
|
||||
search
|
||||
end
|
||||
|
||||
def exists?
|
||||
@exists
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_params(raw_params)
|
||||
# Allow passing ARN as a scalar string, not in a hash
|
||||
raw_params = { arn: raw_params } if raw_params.is_a?(String)
|
||||
|
||||
# Remove all expected params from the raw param hash
|
||||
validated_params = {}
|
||||
[
|
||||
:arn,
|
||||
].each do |expected_param|
|
||||
validated_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}'"
|
||||
end
|
||||
|
||||
# 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 search
|
||||
aws_response = AwsSnsTopic::Backend.create.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
|
||||
|
||||
class Backend
|
||||
#=====================================================#
|
||||
# API Definition
|
||||
#=====================================================#
|
||||
[
|
||||
:get_topic_attributes,
|
||||
].each do |method|
|
||||
define_method(:method) do |*_args|
|
||||
raise "Unimplemented abstract method #{method} - internal error"
|
||||
end
|
||||
end
|
||||
|
||||
#=====================================================#
|
||||
# Concrete Implementation
|
||||
#=====================================================#
|
||||
# Uses the SDK API to really talk to AWS
|
||||
class AwsClientApi < Backend
|
||||
def get_topic_attributes(criteria)
|
||||
AWSConnection.new.sns_client.get_topic_attributes(criteria)
|
||||
end
|
||||
end
|
||||
|
||||
#=====================================================#
|
||||
# Factory Interface
|
||||
#=====================================================#
|
||||
# TODO: move this to a mix-in
|
||||
DEFAULT_BACKEND = AwsClientApi
|
||||
@selected_backend = DEFAULT_BACKEND
|
||||
|
||||
def self.create
|
||||
@selected_backend.new
|
||||
end
|
||||
|
||||
def self.select(klass)
|
||||
@selected_backend = klass
|
||||
end
|
||||
end
|
||||
end
|
|
@ -96,3 +96,38 @@ output "example_ec2_id" {
|
|||
output "no_roles_ec2_id" {
|
||||
value = "${aws_instance.no_roles_instance.id}"
|
||||
}
|
||||
|
||||
|
||||
#===========================================================================#
|
||||
# SNS
|
||||
#===========================================================================#
|
||||
|
||||
# Test fixture:
|
||||
# sns_test_topic_01 has one confirmed subscription
|
||||
# sns_test_topic_02 has no subscriptions
|
||||
|
||||
resource "aws_sns_topic" "sns_test_topic_01" {
|
||||
name = "${terraform.env}-test-topic-01"
|
||||
}
|
||||
|
||||
output "sns_test_topic_01_arn" {
|
||||
value = "${aws_sns_topic.sns_test_topic_01.arn}"
|
||||
}
|
||||
|
||||
resource "aws_sqs_queue" "sqs_test_queue_01" {
|
||||
name = "${terraform.env}-test-queue-01"
|
||||
}
|
||||
|
||||
resource "aws_sns_topic_subscription" "sqs_test_queue_01_sub" {
|
||||
topic_arn = "${aws_sns_topic.sns_test_topic_01.arn}"
|
||||
protocol = "sqs"
|
||||
endpoint = "${aws_sqs_queue.sqs_test_queue_01.arn}"
|
||||
}
|
||||
|
||||
resource "aws_sns_topic" "sns_test_topic_02" {
|
||||
name = "${terraform.env}-test-topic-02"
|
||||
}
|
||||
|
||||
output "sns_test_topic_02_arn" {
|
||||
value = "${aws_sns_topic.sns_test_topic_02.arn}"
|
||||
}
|
34
test/integration/verify/controls/aws_sns_topic.rb
Normal file
34
test/integration/verify/controls/aws_sns_topic.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
sns_topic_with_subscription_arn = attribute(
|
||||
'sns_test_topic_01_arn',
|
||||
default: 'default.sns_test_topic_01_arn',
|
||||
description: 'ARN of an SNS topic with at least one subscription')
|
||||
|
||||
sns_topic_with_no_subscriptions_arn = attribute(
|
||||
'sns_test_topic_02_arn',
|
||||
default: 'default.sns_test_topic_02_arn',
|
||||
description: 'ARN of an SNS topic with no subscriptions')
|
||||
|
||||
control 'SNS Topics' do
|
||||
# Split the ARNs so we can test things
|
||||
scheme, partition, service, region, account, topic = sns_topic_with_subscription_arn.split(':')
|
||||
arn_prefix = [scheme, partition, service].join(':')
|
||||
|
||||
# Search miss
|
||||
no_such_topic_arn = [arn_prefix, region, account, 'no-such-topic-for-realz'].join(':')
|
||||
describe aws_sns_topic(no_such_topic_arn) do
|
||||
it { should_not exist }
|
||||
end
|
||||
|
||||
# Search hit, fully specified, has subscriptions
|
||||
describe aws_sns_topic(sns_topic_with_subscription_arn) do
|
||||
it { should exist }
|
||||
its('confirmed_subscription_count') { should_not be_zero }
|
||||
end
|
||||
|
||||
# Search hit, fully specified, has no subscriptions
|
||||
describe aws_sns_topic(sns_topic_with_no_subscriptions_arn) do
|
||||
it { should exist }
|
||||
its('confirmed_subscription_count') { should be_zero }
|
||||
end
|
||||
|
||||
end
|
|
@ -3,3 +3,6 @@ require 'minitest/unit'
|
|||
require 'minitest/pride'
|
||||
|
||||
require 'inspec/resource'
|
||||
|
||||
# Needed for exception classes, etc
|
||||
require 'aws-sdk'
|
126
test/unit/resources/aws_sns_topic_test.rb
Normal file
126
test/unit/resources/aws_sns_topic_test.rb
Normal file
|
@ -0,0 +1,126 @@
|
|||
require 'ostruct'
|
||||
require 'helper'
|
||||
require 'aws_sns_topic'
|
||||
|
||||
# MSNB = MockSnsBackend
|
||||
# Abbreviation not used outside this file
|
||||
|
||||
#=============================================================================#
|
||||
# Constructor Tests
|
||||
#=============================================================================#
|
||||
class AwsSnsTopicConstructorTest < Minitest::Test
|
||||
def setup
|
||||
AwsSnsTopic::Backend.select(AwsMSNB::NoSubscriptions)
|
||||
end
|
||||
|
||||
def test_constructor_some_args_required
|
||||
assert_raises(ArgumentError) { AwsSnsTopic.new }
|
||||
end
|
||||
|
||||
def test_constructor_accepts_scalar_arn
|
||||
AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:some-topic')
|
||||
end
|
||||
|
||||
def test_constructor_accepts_arn_as_hash
|
||||
AwsSnsTopic.new(arn: 'arn:aws:sns:us-east-1:123456789012:some-topic')
|
||||
end
|
||||
|
||||
def test_constructor_rejects_unrecognized_resource_params
|
||||
assert_raises(ArgumentError) { AwsSnsTopic.new(beep: 'boop') }
|
||||
end
|
||||
|
||||
def test_constructor_rejects_non_arn_formats
|
||||
[
|
||||
'not-even-like-an-arn',
|
||||
'arn:::::', # Empty
|
||||
'arn::::::', # Too many colons
|
||||
'arn:aws::us-east-1:123456789012:some-topic', # Omits SNS service
|
||||
'arn::sns:us-east-1:123456789012:some-topic', # Omits partition
|
||||
'arn:aws:sns:*:123456789012:some-topic', # All-region - not permitted for lookup
|
||||
'arn:aws:sns:us-east-1::some-topic', # Default account - not permitted for lookup
|
||||
].each do |example|
|
||||
assert_raises(ArgumentError) { AwsSnsTopic.new(arn: example) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#=============================================================================#
|
||||
# Search / Recall
|
||||
#=============================================================================#
|
||||
class AwsSnsTopicRecallTest < Minitest::Test
|
||||
# No setup here - each test needs to explicitly declare
|
||||
# what they want from the backend.
|
||||
|
||||
def test_recall_no_match_is_no_exception
|
||||
AwsSnsTopic::Backend.select(AwsMSNB::Miss)
|
||||
topic = AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:nope')
|
||||
refute topic.exists?
|
||||
end
|
||||
|
||||
def test_recall_match_single_result_works
|
||||
AwsSnsTopic::Backend.select(AwsMSNB::NoSubscriptions)
|
||||
topic = AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:does-not-matter')
|
||||
assert topic.exists?
|
||||
end
|
||||
end
|
||||
|
||||
#=============================================================================#
|
||||
# Properties
|
||||
#=============================================================================#
|
||||
|
||||
class AwsSnsTopicPropertiesTest < Minitest::Test
|
||||
# No setup here - each test needs to explicitly declare
|
||||
# what they want from the backend.
|
||||
|
||||
#---------------------------------------
|
||||
# confirmed_subscription_count
|
||||
#---------------------------------------
|
||||
def test_prop_conf_sub_count_zero
|
||||
AwsSnsTopic::Backend.select(AwsMSNB::NoSubscriptions)
|
||||
topic = AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:does-not-matter')
|
||||
assert_equal(0, topic.confirmed_subscription_count)
|
||||
end
|
||||
|
||||
def test_prop_conf_sub_count_zero
|
||||
AwsSnsTopic::Backend.select(AwsMSNB::OneSubscription)
|
||||
topic = AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:does-not-matter')
|
||||
assert_equal(1, topic.confirmed_subscription_count)
|
||||
end
|
||||
end
|
||||
|
||||
#=============================================================================#
|
||||
# Test Fixtures
|
||||
#=============================================================================#
|
||||
|
||||
module AwsMSNB
|
||||
|
||||
class Miss < AwsSnsTopic::Backend
|
||||
def get_topic_attributes(criteria)
|
||||
raise Aws::SNS::Errors::NotFound.new("No SNS topic for #{criteria[:topic_arn]}", 'Nope')
|
||||
end
|
||||
end
|
||||
|
||||
class NoSubscriptions < AwsSnsTopic::Backend
|
||||
def get_topic_attributes(_criteria)
|
||||
OpenStruct.new({
|
||||
attributes: { # Note that this is a plain hash, odd for AWS SDK
|
||||
# Many other attributes available, see
|
||||
# http://docs.aws.amazon.com/sdkforruby/api/Aws/SNS/Types/GetTopicAttributesResponse.html
|
||||
"SubscriptionsConfirmed" => 0
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
class OneSubscription < AwsSnsTopic::Backend
|
||||
def get_topic_attributes(_criteria)
|
||||
OpenStruct.new({
|
||||
attributes: { # Note that this is a plain hash, odd for AWS SDK
|
||||
# Many other attributes available, see
|
||||
# http://docs.aws.amazon.com/sdkforruby/api/Aws/SNS/Types/GetTopicAttributesResponse.html
|
||||
"SubscriptionsConfirmed" => 1
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue