feat(permissions): dead-letter the real-time events EventBridge cannot deliver

Adds an SQS dead-letter queue to the real-time detection target, in both the
CloudFormation and Terraform templates, so a delivery failure is auditable
instead of silent. EventBridge retries for up to 24 hours and then writes the
event to the queue with the error code and attempt count; responses that are
never retried land there on the first attempt.

EventBridge delivers to a DLQ as a service rather than through the target
invoke role, so the queue carries a resource policy granting sqs:SendMessage
to events.amazonaws.com, scoped to the rule ARN. The queue uses SQS-managed
encryption to avoid granting KMS permissions, and keeps messages for the
14-day maximum. Prowler is granted no access to it.
This commit is contained in:
César Arroba committed 2026-08-17 11:15:45 +02:00
1 parent 0a39c004b1
commit 29102e7b33
5 files changed
+114

No files matched your search

@@ -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"
@@ -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.
@@ -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)
}
@@ -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
}
}
@@ -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