From b5a23a2764cbd5a7a7115e08a8f5a90397b0c7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= Date: Mon, 17 Aug 2026 11:55:19 +0200 Subject: [PATCH] feat(permissions): emit the hello event from Terraform too Adds the hello event emitter to the Terraform path, so both onboarding templates verify the connection by themselves and leave the same footprint in the account: a function whose only permissions are events:PutEvents on the default bus and its own log group, invoked once on apply and again whenever the endpoint changes. The apply reports the outcome through the prowler_realtime_hello_status output, and the function never raises, so a failed verification cannot fail the apply that created the scan role. --- .gitignore | 1 + permissions/templates/terraform/README.md | 7 +- permissions/templates/terraform/outputs.tf | 5 ++ .../terraform/realtime-detection/hello.py | 45 ++++++++++ .../terraform/realtime-detection/main.tf | 84 +++++++++++++++++++ .../terraform/realtime-detection/outputs.tf | 5 ++ .../terraform/realtime-detection/versions.tf | 6 ++ 7 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 permissions/templates/terraform/realtime-detection/hello.py diff --git a/.gitignore b/.gitignore index 4545146688..96b6f51bb6 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,7 @@ ai-config.json .terraform* *.tfstate *.tfstate.* +permissions/templates/terraform/**/*.zip # .env ui/.env* diff --git a/permissions/templates/terraform/README.md b/permissions/templates/terraform/README.md index da29f68cf9..5e4a78b995 100644 --- a/permissions/templates/terraform/README.md +++ b/permissions/templates/terraform/README.md @@ -55,7 +55,9 @@ 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: +The apply verifies the connection by emitting a hello event, without touching any real resource. 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. The `prowler_realtime_hello_status` output reports the result, and the event is emitted again whenever `prowler_webhook_url` changes. + +To re-check the connection at any point, emit it yourself: ```bash aws events put-events --entries '[{ @@ -65,8 +67,6 @@ aws events put-events --entries '[{ }]' ``` -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. @@ -93,5 +93,6 @@ After successful deployment, you'll get: - `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) +- `prowler_realtime_hello_status`: result of the hello event emitted on apply, `Sent` or `Failed` with the error > **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 d6b2276dad..425d5b17b6 100644 --- a/permissions/templates/terraform/outputs.tf +++ b/permissions/templates/terraform/outputs.tf @@ -40,3 +40,8 @@ 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) } + +output "prowler_realtime_hello_status" { + description = "Result of the hello event emitted on apply (null if real-time detection is disabled)" + value = try(module.realtime_detection[0].prowler_realtime_hello_status, null) +} diff --git a/permissions/templates/terraform/realtime-detection/hello.py b/permissions/templates/terraform/realtime-detection/hello.py new file mode 100644 index 0000000000..62d3424bb3 --- /dev/null +++ b/permissions/templates/terraform/realtime-detection/hello.py @@ -0,0 +1,45 @@ +"""Emits the Prowler real-time detection hello event. + +Invoked once by Terraform when real-time detection is enabled, mirroring the +CloudFormation custom resource. It never raises: a failed verification must not +fail the apply that created the scan role. +""" + +import json +import time + +import boto3 + +ATTEMPTS = 3 +BACKOFF_SECONDS = 2 + + +def handler(_event, context): + _, _, _, region, account = context.invoked_function_arn.split(":")[:5] + entries = [ + { + "Source": "prowler.simulation", + "DetailType": "test_connection", + "Detail": json.dumps( + {"account_id": account, "region": region, "trigger": "terraform"} + ), + "EventBusName": "default", + } + ] + + client = boto3.client("events") + error = "not attempted" + # The role policy may not be in effect yet on the first apply + for attempt in range(ATTEMPTS): + try: + entry = client.put_events(Entries=entries)["Entries"][0] + if not entry.get("ErrorCode"): + return {"status": "Sent", "event_id": entry["EventId"]} + error = entry["ErrorCode"] + except Exception as failure: # noqa: BLE001 + error = str(failure)[:200] + if attempt + 1 < ATTEMPTS: + time.sleep(BACKOFF_SECONDS) + + print(f"hello event not sent: {error}") + return {"status": "Failed", "error": error} diff --git a/permissions/templates/terraform/realtime-detection/main.tf b/permissions/templates/terraform/realtime-detection/main.tf index c787bed8d4..1bd7ffae1b 100644 --- a/permissions/templates/terraform/realtime-detection/main.tf +++ b/permissions/templates/terraform/realtime-detection/main.tf @@ -208,3 +208,87 @@ resource "aws_cloudwatch_event_target" "prowler_realtime_hello" { arn = aws_sqs_queue.prowler_realtime_dlq.arn } } + +# Hello event emitter, the same function the CloudFormation template runs, so both +# onboarding paths leave the same footprint in the account +################################### +data "archive_file" "prowler_realtime_hello" { + type = "zip" + source_file = "${path.module}/hello.py" + output_path = "${path.module}/hello.zip" +} + +# Declared explicitly so the function does not leave a log group with unlimited retention behind +resource "aws_cloudwatch_log_group" "prowler_realtime_hello" { + name = "/aws/lambda/ProwlerRealtimeHello" + retention_in_days = 30 +} + +data "aws_iam_policy_document" "prowler_realtime_hello_assume_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "prowler_realtime_hello" { + name = "ProwlerRealtimeHello" + assume_role_policy = data.aws_iam_policy_document.prowler_realtime_hello_assume_role.json +} + +resource "aws_iam_role_policy" "prowler_realtime_hello" { + name = "PutHelloEvent" + role = aws_iam_role.prowler_realtime_hello.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "events:PutEvents" + Resource = "arn:${data.aws_partition.current.partition}:events:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:event-bus/default" + }, + { + Effect = "Allow" + Action = ["logs:CreateLogStream", "logs:PutLogEvents"] + Resource = "${aws_cloudwatch_log_group.prowler_realtime_hello.arn}:*" + }, + ] + }) +} + +resource "aws_lambda_function" "prowler_realtime_hello" { + function_name = "ProwlerRealtimeHello" + description = "Emits the Prowler real-time detection hello event" + role = aws_iam_role.prowler_realtime_hello.arn + runtime = "python3.13" + handler = "hello.handler" + timeout = 30 + filename = data.archive_file.prowler_realtime_hello.output_path + source_code_hash = data.archive_file.prowler_realtime_hello.output_base64sha256 + + depends_on = [aws_cloudwatch_log_group.prowler_realtime_hello] +} + +# Emitting the event is the last step: the rule, the target and every policy it +# depends on must already exist, and none of them is referenced here +resource "aws_lambda_invocation" "prowler_realtime_hello" { + function_name = aws_lambda_function.prowler_realtime_hello.function_name + input = "{}" + + # Re-emits the event when the endpoint changes, like the CloudFormation custom resource + triggers = { + webhook_url = var.prowler_webhook_url + } + + depends_on = [ + aws_cloudwatch_event_target.prowler_realtime_hello, + aws_iam_role_policy.prowler_realtime_hello, + aws_iam_role_policy.prowler_realtime_invoke, + aws_sqs_queue_policy.prowler_realtime_dlq, + ] +} diff --git a/permissions/templates/terraform/realtime-detection/outputs.tf b/permissions/templates/terraform/realtime-detection/outputs.tf index 4b45dd7fba..dfa0eb89f4 100644 --- a/permissions/templates/terraform/realtime-detection/outputs.tf +++ b/permissions/templates/terraform/realtime-detection/outputs.tf @@ -18,6 +18,11 @@ output "prowler_realtime_dlq_arn" { value = aws_sqs_queue.prowler_realtime_dlq.arn } +output "prowler_realtime_hello_status" { + description = "Result of the hello event emitted on apply: Sent, or Failed with the error" + value = try(jsondecode(aws_lambda_invocation.prowler_realtime_hello.result), null) +} + 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 diff --git a/permissions/templates/terraform/realtime-detection/versions.tf b/permissions/templates/terraform/realtime-detection/versions.tf index f5a0ad7ccb..46275eced7 100644 --- a/permissions/templates/terraform/realtime-detection/versions.tf +++ b/permissions/templates/terraform/realtime-detection/versions.tf @@ -1,3 +1,9 @@ terraform { required_version = ">= 1.5" + required_providers { + archive = { + source = "hashicorp/archive" + version = "~> 2.4" + } + } }