diff --git a/permissions/templates/cloudformation/prowler-scan-role.yml b/permissions/templates/cloudformation/prowler-scan-role.yml index 868486e4f7..3e4cc54749 100644 --- a/permissions/templates/cloudformation/prowler-scan-role.yml +++ b/permissions/templates/cloudformation/prowler-scan-role.yml @@ -680,6 +680,45 @@ Resources: - Key: "Name" Value: "ProwlerRealtimeInvoke" + # Captures the events EventBridge could not deliver, so a delivery failure is auditable + ProwlerRealtimeDlq: + Type: AWS::SQS::Queue + Condition: RealtimeDetectionEnabled + Properties: + QueueName: ProwlerRealtimeDetectionDLQ + MessageRetentionPeriod: 1209600 + SqsManagedSseEnabled: true + Tags: + - Key: "Service" + Value: "https://prowler.com" + - Key: "Support" + Value: "support@prowler.com" + - Key: "CloudFormation" + Value: "true" + - Key: "Name" + Value: "ProwlerRealtimeDetectionDLQ" + + # EventBridge writes to the DLQ as a service, not through the invoke role, so it needs a queue policy + ProwlerRealtimeDlqPolicy: + Type: AWS::SQS::QueuePolicy + Condition: RealtimeDetectionEnabled + Properties: + Queues: + - !Ref ProwlerRealtimeDlq + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: AllowEventBridgeDeadLetterDelivery + Effect: Allow + Principal: + Service: events.amazonaws.com + Action: "sqs:SendMessage" + Resource: !GetAtt ProwlerRealtimeDlq.Arn + Condition: + ArnEquals: + # Built from the rule name to avoid a circular dependency with the rule + "aws:SourceArn": !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/ProwlerRealtimeDetection" + ProwlerRealtimeRule: Type: AWS::Events::Rule Condition: RealtimeDetectionEnabled @@ -735,6 +774,12 @@ Resources: - Id: ProwlerCloud Arn: !GetAtt ProwlerRealtimeApiDestination.Arn RoleArn: !GetAtt ProwlerRealtimeInvokeRole.Arn + # Retries cover an endpoint outage; whatever outlives the window is dead-lettered + RetryPolicy: + MaximumEventAgeInSeconds: 86400 + MaximumRetryAttempts: 185 + DeadLetterConfig: + Arn: !GetAtt ProwlerRealtimeDlq.Arn Tags: - Key: "Service" Value: "https://prowler.com" @@ -806,3 +851,10 @@ Outputs: Value: !GetAtt ProwlerRealtimeApiDestination.Arn Export: Name: !Sub "${AWS::StackName}-ProwlerRealtimeApiDestinationArn" + + ProwlerRealtimeDlqUrl: + Condition: RealtimeDetectionEnabled + Description: "URL of the dead-letter queue holding the events EventBridge could not deliver" + Value: !Ref ProwlerRealtimeDlq + Export: + Name: !Sub "${AWS::StackName}-ProwlerRealtimeDlqUrl" diff --git a/permissions/templates/terraform/README.md b/permissions/templates/terraform/README.md index 240ef458d2..19ac623971 100644 --- a/permissions/templates/terraform/README.md +++ b/permissions/templates/terraform/README.md @@ -55,6 +55,8 @@ terraform apply \ `prowler_webhook_url` already defaults to the Prowler Cloud ingest endpoint, so only the API key is needed. Override it for a self-hosted deployment or for testing. +Failed deliveries are not lost: EventBridge retries for up to 24 hours and then writes the event to the `ProwlerRealtimeDetectionDLQ` queue created in your account, together with the error code and the number of attempts. Responses that are never retried (any 4xx other than 401, 407, 409 and 429) land there on the first attempt. The queue is yours: Prowler has no permission to read it. + > **Note:** the EventBridge rule is regional. It forwards only the events delivered to the default event bus of the region Terraform deploys to (`us-east-1` by default, see `versions.tf`). IAM events are global and always land in `us-east-1`, but regional services (EC2 security groups, RDS, per-region Config and GuardDuty) are only covered in that region. Deploy the module in every region you want covered. #### Using terraform.tfvars file (Recommended) @@ -78,5 +80,6 @@ After successful deployment, you'll get: - `realtime_detection_enabled`: Whether real-time detection is enabled - `prowler_realtime_rule_arn`: ARN of the EventBridge rule (null if real-time detection is disabled) - `prowler_realtime_api_destination_arn`: ARN of the EventBridge API destination (null if real-time detection is disabled) +- `prowler_realtime_dlq_url`: URL of the dead-letter queue (null if real-time detection is disabled) > **Note:** Terraform will use the AWS credentials of your default profile or AWS_PROFILE environment variable. diff --git a/permissions/templates/terraform/outputs.tf b/permissions/templates/terraform/outputs.tf index 6d6aa38649..d6b2276dad 100644 --- a/permissions/templates/terraform/outputs.tf +++ b/permissions/templates/terraform/outputs.tf @@ -35,3 +35,8 @@ output "prowler_realtime_api_destination_arn" { description = "ARN of the EventBridge API destination targeting Prowler Cloud (null if real-time detection is disabled)" value = try(module.realtime_detection[0].prowler_realtime_api_destination_arn, null) } + +output "prowler_realtime_dlq_url" { + description = "URL of the dead-letter queue holding the events EventBridge could not deliver (null if real-time detection is disabled)" + value = try(module.realtime_detection[0].prowler_realtime_dlq_url, null) +} diff --git a/permissions/templates/terraform/realtime-detection/main.tf b/permissions/templates/terraform/realtime-detection/main.tf index acb1fa86da..0a857aeadb 100644 --- a/permissions/templates/terraform/realtime-detection/main.tf +++ b/permissions/templates/terraform/realtime-detection/main.tf @@ -68,6 +68,40 @@ resource "aws_iam_role_policy" "prowler_realtime_invoke" { }) } +# Dead-letter queue for the events EventBridge could not deliver +################################### +resource "aws_sqs_queue" "prowler_realtime_dlq" { + name = "ProwlerRealtimeDetectionDLQ" + message_retention_seconds = 1209600 + sqs_managed_sse_enabled = true +} + +# EventBridge writes to the DLQ as a service, not through the invoke role, so it needs a queue policy +data "aws_iam_policy_document" "prowler_realtime_dlq" { + statement { + sid = "AllowEventBridgeDeadLetterDelivery" + effect = "Allow" + actions = ["sqs:SendMessage"] + resources = [aws_sqs_queue.prowler_realtime_dlq.arn] + + principals { + type = "Service" + identifiers = ["events.amazonaws.com"] + } + + condition { + test = "ArnEquals" + variable = "aws:SourceArn" + values = [aws_cloudwatch_event_rule.prowler_realtime.arn] + } + } +} + +resource "aws_sqs_queue_policy" "prowler_realtime_dlq" { + queue_url = aws_sqs_queue.prowler_realtime_dlq.id + policy = data.aws_iam_policy_document.prowler_realtime_dlq.json +} + # Rule matching the CloudTrail management events tracked by Prowler real-time detection ################################### resource "aws_cloudwatch_event_rule" "prowler_realtime" { @@ -129,4 +163,14 @@ resource "aws_cloudwatch_event_target" "prowler_realtime" { target_id = "ProwlerCloud" arn = aws_cloudwatch_event_api_destination.prowler_realtime.arn role_arn = aws_iam_role.prowler_realtime_invoke.arn + + # Retries cover an endpoint outage; whatever outlives the window is dead-lettered + retry_policy { + maximum_event_age_in_seconds = 86400 + maximum_retry_attempts = 185 + } + + dead_letter_config { + arn = aws_sqs_queue.prowler_realtime_dlq.arn + } } diff --git a/permissions/templates/terraform/realtime-detection/outputs.tf b/permissions/templates/terraform/realtime-detection/outputs.tf index a32d605341..4b45dd7fba 100644 --- a/permissions/templates/terraform/realtime-detection/outputs.tf +++ b/permissions/templates/terraform/realtime-detection/outputs.tf @@ -8,6 +8,16 @@ output "prowler_realtime_api_destination_arn" { value = aws_cloudwatch_event_api_destination.prowler_realtime.arn } +output "prowler_realtime_dlq_url" { + description = "URL of the dead-letter queue holding the events EventBridge could not deliver" + value = aws_sqs_queue.prowler_realtime_dlq.id +} + +output "prowler_realtime_dlq_arn" { + description = "ARN of the dead-letter queue holding the events EventBridge could not deliver" + value = aws_sqs_queue.prowler_realtime_dlq.arn +} + output "prowler_realtime_invoke_role_arn" { description = "ARN of the IAM role assumed by EventBridge to invoke the API destination" value = aws_iam_role.prowler_realtime_invoke.arn