mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 23:04:23 +00:00
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.
This commit is contained in:
7 files changed
+150
-3
No files matched your search
@@ -134,6 +134,7 @@ ai-config.json
|
||||
.terraform*
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
permissions/templates/terraform/**/*.zip
|
||||
|
||||
# .env
|
||||
ui/.env*
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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,
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
required_providers {
|
||||
archive = {
|
||||
source = "hashicorp/archive"
|
||||
version = "~> 2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user