From 56d33fe2520a702f2dc3225fd7fb36c2abfb9b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= Date: Mon, 17 Aug 2026 11:24:00 +0200 Subject: [PATCH] feat(permissions): emit a hello event to verify real-time detection Adds the synthetic hello event from the MVP onboarding flow, so the connection can be verified end to end without changing any real resource. A dedicated rule matches source prowler.simulation and detail-type test_connection, since no CloudTrail event pattern would, and forwards it through the same connection, API destination, API key and endpoint as a real event. In CloudFormation a custom resource emits the event on create and update. Its function only holds events:PutEvents on the default bus plus its own log group, and always answers SUCCESS: a failed verification must never roll back the scan role. Terraform documents the equivalent put-events command instead, because an apply already runs from a shell with credentials. The invoke role trust policy and the dead-letter queue policy now allow both rule ARNs; scoped to a single rule, the hello event would have been dropped with no explanation. --- .../cloudformation/prowler-scan-role.yml | 180 +++++++++++++++++- permissions/templates/terraform/README.md | 12 ++ .../terraform/realtime-detection/main.tf | 40 +++- 3 files changed, 225 insertions(+), 7 deletions(-) diff --git a/permissions/templates/cloudformation/prowler-scan-role.yml b/permissions/templates/cloudformation/prowler-scan-role.yml index 3e4cc54749..29e6482934 100644 --- a/permissions/templates/cloudformation/prowler-scan-role.yml +++ b/permissions/templates/cloudformation/prowler-scan-role.yml @@ -660,8 +660,10 @@ Resources: StringEquals: "aws:SourceAccount": !Ref AWS::AccountId ArnLike: - # 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" + # Built from the rule names to avoid a circular dependency with the rules + "aws:SourceArn": + - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/ProwlerRealtimeDetection" + - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/ProwlerRealtimeDetectionHello" Policies: - PolicyName: InvokeApiDestination PolicyDocument: @@ -716,8 +718,10 @@ Resources: 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" + # Built from the rule names to avoid a circular dependency with the rules + "aws:SourceArn": + - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/ProwlerRealtimeDetection" + - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/ProwlerRealtimeDetectionHello" ProwlerRealtimeRule: Type: AWS::Events::Rule @@ -790,6 +794,174 @@ Resources: - Key: "Name" Value: "ProwlerRealtimeDetection" + # Carries the synthetic hello event, which no CloudTrail pattern would match + ProwlerRealtimeHelloRule: + Type: AWS::Events::Rule + Condition: RealtimeDetectionEnabled + Properties: + Name: ProwlerRealtimeDetectionHello + Description: Forwards the Prowler real-time detection hello event used to verify the connection + State: ENABLED + EventPattern: + source: + - prowler.simulation + detail-type: + - test_connection + Targets: + - Id: ProwlerCloud + Arn: !GetAtt ProwlerRealtimeApiDestination.Arn + RoleArn: !GetAtt ProwlerRealtimeInvokeRole.Arn + RetryPolicy: + MaximumEventAgeInSeconds: 86400 + MaximumRetryAttempts: 185 + DeadLetterConfig: + Arn: !GetAtt ProwlerRealtimeDlq.Arn + Tags: + - Key: "Service" + Value: "https://prowler.com" + - Key: "Support" + Value: "support@prowler.com" + - Key: "CloudFormation" + Value: "true" + - Key: "Name" + Value: "ProwlerRealtimeDetectionHello" + + # Explicit log group so the function does not leave one with unlimited retention behind + ProwlerRealtimeHelloLogGroup: + Type: AWS::Logs::LogGroup + Condition: RealtimeDetectionEnabled + Properties: + LogGroupName: /aws/lambda/ProwlerRealtimeHello + RetentionInDays: 30 + + ProwlerRealtimeHelloRole: + Type: AWS::IAM::Role + Condition: RealtimeDetectionEnabled + Properties: + RoleName: ProwlerRealtimeHello + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: "sts:AssumeRole" + Policies: + - PolicyName: PutHelloEvent + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: "events:PutEvents" + Resource: !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default" + - Effect: Allow + Action: + - "logs:CreateLogStream" + - "logs:PutLogEvents" + Resource: !GetAtt ProwlerRealtimeHelloLogGroup.Arn + Tags: + - Key: "Service" + Value: "https://prowler.com" + - Key: "Support" + Value: "support@prowler.com" + - Key: "CloudFormation" + Value: "true" + - Key: "Name" + Value: "ProwlerRealtimeHello" + + ProwlerRealtimeHelloFunction: + Type: AWS::Lambda::Function + Condition: RealtimeDetectionEnabled + Properties: + FunctionName: ProwlerRealtimeHello + Description: Emits the Prowler real-time detection hello event when the stack is created or updated + Runtime: python3.13 + Handler: index.handler + Timeout: 30 + Role: !GetAtt ProwlerRealtimeHelloRole.Arn + Code: + ZipFile: | + import json + import urllib.request + + import boto3 + + + def respond(event, context, data): + """Answer CloudFormation. Always SUCCESS: see handler().""" + body = json.dumps( + { + "Status": "SUCCESS", + "Reason": f"See CloudWatch log stream {context.log_stream_name}", + "PhysicalResourceId": "ProwlerRealtimeHelloEvent", + "StackId": event["StackId"], + "RequestId": event["RequestId"], + "LogicalResourceId": event["LogicalResourceId"], + "Data": data, + } + ).encode() + request = urllib.request.Request( + event["ResponseURL"], + data=body, + method="PUT", + headers={"content-type": "", "content-length": str(len(body))}, + ) + for attempt in range(3): + try: + urllib.request.urlopen(request, timeout=10) + return + except Exception as error: # noqa: BLE001 + print(f"response attempt {attempt + 1} failed: {error}") + + + def handler(event, context): + """Emit the hello event, then always report SUCCESS. + + A failed verification must never roll back the scan role, so any + error is reported in the response data instead of failing the stack. + """ + data = {"Status": "Skipped"} + try: + if event["RequestType"] in ("Create", "Update"): + _, _, _, region, account = context.invoked_function_arn.split(":")[:5] + entry = boto3.client("events").put_events( + Entries=[ + { + "Source": "prowler.simulation", + "DetailType": "test_connection", + "Detail": json.dumps( + { + "account_id": account, + "region": region, + "stack_id": event["StackId"], + "request_type": event["RequestType"], + } + ), + "EventBusName": "default", + } + ] + )["Entries"][0] + if entry.get("ErrorCode"): + data = {"Status": "Failed", "Error": entry["ErrorCode"]} + else: + data = {"Status": "Sent", "EventId": entry["EventId"]} + except Exception as error: # noqa: BLE001 + data = {"Status": "Failed", "Error": str(error)[:200]} + print(json.dumps(data)) + respond(event, context, data) + + # Emitting the event is the last step: the rule, destination and DLQ must already exist + ProwlerRealtimeHelloTrigger: + Type: Custom::ProwlerHelloEvent + Condition: RealtimeDetectionEnabled + DependsOn: + - ProwlerRealtimeHelloRule + - ProwlerRealtimeDlqPolicy + Properties: + ServiceToken: !GetAtt ProwlerRealtimeHelloFunction.Arn + # Re-emits the event when the endpoint changes; the API key is deliberately not passed in + WebhookUrl: !Ref ProwlerWebhookUrl + Metadata: AWS::CloudFormation::Interface: ParameterGroups: diff --git a/permissions/templates/terraform/README.md b/permissions/templates/terraform/README.md index 19ac623971..da29f68cf9 100644 --- a/permissions/templates/terraform/README.md +++ b/permissions/templates/terraform/README.md @@ -55,6 +55,18 @@ 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. +To verify the connection without touching any real resource, emit the hello event yourself. It travels the same connection, API destination, API key and endpoint as a real event, and Prowler Cloud marks the provider as connected without running a scan: + +```bash +aws events put-events --entries '[{ + "Source": "prowler.simulation", + "DetailType": "test_connection", + "Detail": "{}" +}]' +``` + +The CloudFormation template emits this event by itself on deploy; here it is a manual step, because a Terraform apply already runs from a shell with credentials and adding a Lambda to the account just to send one event is a worse trade. + 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. diff --git a/permissions/templates/terraform/realtime-detection/main.tf b/permissions/templates/terraform/realtime-detection/main.tf index 0a857aeadb..c787bed8d4 100644 --- a/permissions/templates/terraform/realtime-detection/main.tf +++ b/permissions/templates/terraform/realtime-detection/main.tf @@ -40,11 +40,13 @@ data "aws_iam_policy_document" "prowler_realtime_assume_role" { values = [data.aws_caller_identity.current.account_id] } - # Built from the rule name to avoid a circular dependency with the rule condition { test = "ArnLike" variable = "aws:SourceArn" - values = ["arn:${data.aws_partition.current.partition}:events:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:rule/ProwlerRealtimeDetection"] + values = [ + aws_cloudwatch_event_rule.prowler_realtime.arn, + aws_cloudwatch_event_rule.prowler_realtime_hello.arn, + ] } } } @@ -92,7 +94,10 @@ data "aws_iam_policy_document" "prowler_realtime_dlq" { condition { test = "ArnEquals" variable = "aws:SourceArn" - values = [aws_cloudwatch_event_rule.prowler_realtime.arn] + values = [ + aws_cloudwatch_event_rule.prowler_realtime.arn, + aws_cloudwatch_event_rule.prowler_realtime_hello.arn, + ] } } } @@ -174,3 +179,32 @@ resource "aws_cloudwatch_event_target" "prowler_realtime" { arn = aws_sqs_queue.prowler_realtime_dlq.arn } } + +# Rule carrying the synthetic hello event, which no CloudTrail pattern would match +################################### +resource "aws_cloudwatch_event_rule" "prowler_realtime_hello" { + name = "ProwlerRealtimeDetectionHello" + description = "Forwards the Prowler real-time detection hello event used to verify the connection" + state = "ENABLED" + + event_pattern = jsonencode({ + source = ["prowler.simulation"] + "detail-type" = ["test_connection"] + }) +} + +resource "aws_cloudwatch_event_target" "prowler_realtime_hello" { + rule = aws_cloudwatch_event_rule.prowler_realtime_hello.name + target_id = "ProwlerCloud" + arn = aws_cloudwatch_event_api_destination.prowler_realtime.arn + role_arn = aws_iam_role.prowler_realtime_invoke.arn + + retry_policy { + maximum_event_age_in_seconds = 86400 + maximum_retry_attempts = 185 + } + + dead_letter_config { + arn = aws_sqs_queue.prowler_realtime_dlq.arn + } +}