diff --git a/docs/resources/aws_sqs_queue.md.erb b/docs/resources/aws_sqs_queue.md.erb new file mode 100644 index 000000000..a27fc4f23 --- /dev/null +++ b/docs/resources/aws_sqs_queue.md.erb @@ -0,0 +1,126 @@ +--- +title: About the aws_sqs_queue Resource +--- + +# aws\_sqs\_queue + +Use the `aws_sqs_queue` InSpec audit resource to test properties of a single AWS Simple Queue Service queue. + +
+ +## Availability + +### Installation + +This resource is distributed along with InSpec itself. You can use it automatically. + +### Version + +This resource first became available in v3.1.4 of InSpec. + +## Syntax + + # Ensure that a queue exists and has a visibility timeout of 300 seconds + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue') do + it { should exist } + its('visibility_timeout') { should be 300 } + end + + # You may also use hash syntax to pass the URL + describe aws_sqs_queue(url: 'https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue') do + it { should exist } + end + +## Resource Parameters + +### URL + +This resource expects a single parameter, the SQS queue URL that uniquely identifies the SQS queue. + +See also the [AWS documentation on SQS Queue identifiers](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-general-identifiers.html). + +
+ +## Properties + +### visibility\_timeout + +An integer indicating the visibility timeout of the message in seconds + + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue') do + its('visibility_timeout') { should be 300} + end + +### maximum\_message\_size + +An integer indicating the maximum message size in bytes + + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue') do + its('maximum_message_size') { should be 262144 } # 256 KB + end + +### delay\_seconds + +An integer indicating the delay in seconds for the queue + + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue') do + its('delay_seconds') { should be 0 } + end + +### message\_retention\_period + +An integer indicating the maximum retention period for a message in seconds + + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue') do + its('message_retention_period') { should be 345600 } # 4 days + end + +### receive\_message\_wait\_timeout\_seconds + +An integer indicating the number of seconds an attempt to recieve a message will wait before returning + + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue') do + its('receive_message_wait_timeout_seconds') { should be 2 } + end + +### is\_fifo\_queue + +A boolean value indicate if this queue is a FIFO queue + + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue') do + its('is_fifo_queue') { should be false } + end + +### content\_based\_deduplication + +A boolean value indicate if content based dedcuplication is enabled or not + + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue.fifo') do + its('is_fifo_queue') { should be true } + its('content_based_deduplication') { should be true } + 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/). + +### exist + +Indicates that the URL provided was found. Use `should_not` to test for SQS topics that should not exist. + + # Expect good news + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueue') do + it { should exist } + end + + # No bad news allowed + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/1212121/MyQueueWhichDoesntExist') do + it { should_not exist } + end + +## AWS Permissions + +Your [Principal](https://docs.aws.amazon.com/IAM/latest/UserGuide/intro-structure.html#intro-structure-principal) will need the `sqs:GetQueueAttributes` action with Effect set to Allow. +You can find detailed documentation at [Actions, Resources, and Condition Keys for Amazon SQS](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-identity-based-policies.html). \ No newline at end of file diff --git a/lib/resource_support/aws.rb b/lib/resource_support/aws.rb index d057fa161..75eff42c6 100644 --- a/lib/resource_support/aws.rb +++ b/lib/resource_support/aws.rb @@ -51,6 +51,7 @@ require 'resources/aws/aws_security_groups' require 'resources/aws/aws_sns_subscription' require 'resources/aws/aws_sns_topic' require 'resources/aws/aws_sns_topics' +require 'resources/aws/aws_sqs_queue' require 'resources/aws/aws_subnet' require 'resources/aws/aws_subnets' require 'resources/aws/aws_vpc' diff --git a/lib/resources/aws/aws_sqs_queue.rb b/lib/resources/aws/aws_sqs_queue.rb new file mode 100644 index 000000000..2a838bf37 --- /dev/null +++ b/lib/resources/aws/aws_sqs_queue.rb @@ -0,0 +1,62 @@ +require 'uri' + +class AwsSqsQueue < Inspec.resource(1) + name 'aws_sqs_queue' + desc 'Verifies settings for an SQS Queue' + example " + describe aws_sqs_queue('https://sqs.ap-southeast-2.amazonaws.com/519527725796/QueueName') do + it { should exist } + its('visiblity_timeout') { should be 300} + end + " + supports platform: 'aws' + + include AwsSingularResourceMixin + attr_reader :arn, :is_fifo_queue, :visibility_timeout, :maximum_message_size, :message_retention_period, :delay_seconds, :receive_message_wait_timeout_seconds, :content_based_deduplication + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:url], + allowed_scalar_name: :url, + allowed_scalar_type: String, + ) + # Validate the URL + unless validated_params[:url] =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w{https})}\z/ + raise ArgumentError, 'Malformed URL for SQS. Expected an ARN of the form ' \ + "'https://sqs.ap-southeast-2.amazonaws.com/111212121/MyQeueue'" + end + validated_params + end + + def fetch_from_api + aws_response = BackendFactory.create(inspec_runner).get_queue_attributes(queue_url: @url, attribute_names: ['All']).attributes + @exists = true + @visibility_timeout = aws_response['VisibilityTimeout'].to_i + @maximum_message_size = aws_response['MaximumMessageSize'].to_i + @message_retention_period = aws_response['MessageRetentionPeriod'].to_i + @delay_seconds = aws_response['DelaySeconds'].to_i + @receive_message_wait_timeout_seconds = aws_response['ReceiveMessageWaitTimeSeconds'].to_i + + # FIFO queues - these attributes only exist for FIFO queues, their presence indicates a FIFO + # queue + @is_fifo_queue = aws_response['FifoQueue'].nil? ? false: true + @content_based_deduplication = aws_response['ContentBasedDeduplication'].nil? ? false: true + rescue Aws::SQS::Errors::NonExistentQueue + @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::SQS::Client + + def get_queue_attributes(criteria) + aws_service_client.get_queue_attributes(criteria) + end + end + end +end diff --git a/test/integration/aws/default/build/sqs.tf b/test/integration/aws/default/build/sqs.tf new file mode 100644 index 000000000..57d718216 --- /dev/null +++ b/test/integration/aws/default/build/sqs.tf @@ -0,0 +1,33 @@ +#===========================================================================# +# SQS QUeue +#===========================================================================# + +# Test fixture: +# sqs_queue_1 is a non-fifo queue +# sqs_queue_2 is a fifo queue + + + +resource "aws_sqs_queue" "sqs_queue_1" { + name = "sqs_queue_1" + delay_seconds = 0 + max_message_size = 262144 # 256 KB + message_retention_seconds = 345600 # 4 days + receive_wait_time_seconds = 2 + visibility_timeout_seconds = 300 # 5 minutes +} + +output "sqs_queue_1_url" { + value = "${aws_sqs_queue.sqs_queue_1.id}" +} + + +resource "aws_sqs_queue" "sqs_queue_2" { + name = "sqs_queue_2.fifo" + fifo_queue = true + content_based_deduplication = true +} + +output "sqs_queue_2_url" { + value = "${aws_sqs_queue.sqs_queue_2.id}" +} diff --git a/test/integration/aws/default/verify/controls/aws_sqs_queue.rb b/test/integration/aws/default/verify/controls/aws_sqs_queue.rb new file mode 100644 index 000000000..c694889c6 --- /dev/null +++ b/test/integration/aws/default/verify/controls/aws_sqs_queue.rb @@ -0,0 +1,47 @@ +fixtures = {} +[ + 'sqs_queue_1_url', + 'sqs_queue_2_url', +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/sqs.tf', + ) +end + +control 'aws_sqs_queue lookup' do + + sqs_queue1_url = fixtures['sqs_queue_1_url'] + + # Search miss + sqs_queue_url_non_existent = sqs_queue1_url + "random" + + describe aws_sqs_queue(sqs_queue_url_non_existent) do + it { should_not exist } + end + + # Search hit + describe aws_sqs_queue(sqs_queue1_url) do + it { should exist } + end +end + +control "aws_sqs_queue properties" do + describe aws_sqs_queue(fixtures['sqs_queue_1_url']) do + its('delay_seconds') { should be 0 } + its('is_fifo_queue') { should be false } + its('visibility_timeout') { should be 300 } + its('maximum_message_size') { should be 262144 } + its('message_retention_period') { should be 345600 } + its('receive_message_wait_timeout_seconds') { should be 2 } + end +end + + +control "aws_sqs_queue fifo properties" do + describe aws_sqs_queue(fixtures['sqs_queue_2_url']) do + its('is_fifo_queue') { should be true } + its('content_based_deduplication') { should be true } + end +end \ No newline at end of file diff --git a/test/unit/resources/aws_sqs_queue_test.rb b/test/unit/resources/aws_sqs_queue_test.rb new file mode 100644 index 000000000..874949738 --- /dev/null +++ b/test/unit/resources/aws_sqs_queue_test.rb @@ -0,0 +1,126 @@ +require 'helper' + +# MSQB = MockSQsBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsSqsQueueConstructorTest < Minitest::Test + def setup + AwsSqsQueue::BackendFactory.select(AwsMSQB::Hit) + end + + def test_constructor_some_args_required + assert_raises(ArgumentError) { AwsSqsQueue.new } + end + + def test_constructor_accepts_scalar_url + AwsSqsQueue.new('https://sqs.ap-southeast-2.amazonaws.com/5195277125796/MyQueue') + end + + def test_constructor_accepts_url_as_hash + AwsSqsQueue.new(url: 'https://sqs.ap-southeast-2.amazonaws.com/5195277125796/MyQueue') + end + + def test_constructor_rejects_unrecognized_resource_params + assert_raises(ArgumentError) { AwsSqsQueue.new(beep: 'boop') } + end + + def test_constructor_rejects_non_https_url + [ + 'not-even-a-url', + 'http://example.com', # http + ].each do |example| + assert_raises(ArgumentError) { AwsSqsQueue.new(url: example) } + end + end +end + +#=============================================================================# +# Search/Recall +#=============================================================================# +class AwsSqsQueueRecallTest < 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 + AwsSqsQueue::BackendFactory.select(AwsMSQB::Miss) + queue = AwsSqsQueue.new('https://sqs.ap-southeast-2.amazonaws.com/12121/idontexist') + refute queue.exists? + end + + def test_recall_match_single_result_works + AwsSqsQueue::BackendFactory.select(AwsMSQB::Hit) + queue = AwsSqsQueue.new('https://sqs.ap-southeast-2.amazonaws.com/12121/iexist') + assert queue.exists? + end +end + +#=============================================================================# +# Properties +#=============================================================================# + +class AwsSqsQueuePropertiesTest < Minitest::Test + # No setup here - each test needs to explicitly declare + # what they want from the backend. + + #--------------------------------------- + # confirmed_subscription_count + #--------------------------------------- + def test_visibility_timeout + AwsSqsQueue::BackendFactory.select(AwsMSQB::Hit) + queue = AwsSqsQueue.new('https://sqs.ap-southeast-2.amazonaws.com/12121/iexist') + assert_equal(300, queue.visibility_timeout) + end + + def test_not_fifo_queue + AwsSqsQueue::BackendFactory.select(AwsMSQB::Hit) + queue = AwsSqsQueue.new('https://sqs.ap-southeast-2.amazonaws.com/12121/iexist') + refute queue.is_fifo_queue + end + + def test_fifo_queue + AwsSqsQueue::BackendFactory.select(AwsMSQB::FifoQueue) + queue = AwsSqsQueue.new('https://sqs.ap-southeast-2.amazonaws.com/12121/iexist') + assert queue.is_fifo_queue + assert queue.content_based_deduplication + end +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# + +module AwsMSQB + + class Miss < AwsBackendBase + def get_queue_attributes(criteria) + raise Aws::SQS::Errors::NonExistentQueue.new("No SQS queue with URL #{criteria[:url]}", 'Nope') + end + end + + class Hit < AwsBackendBase + def get_queue_attributes(_criteria) + OpenStruct.new({ + attributes: { + "QueueArn" => "arn:aws:sqs:ap-southeast-2:519527721296:MyQueue", + "VisibilityTimeout" => 300 + } + }) + end + end + + class FifoQueue < AwsBackendBase + def get_queue_attributes(_criteria) + OpenStruct.new({ + attributes: { + "QueueArn" => "arn:aws:sqs:ap-southeast-2:519527721296:MyQueue.fifo", + "VisibilityTimeout" => 300, + "FifoQueue" => true, + "ContentBasedDeduplication" => true + } + }) + end + end +end \ No newline at end of file