Compare commits

...
Author SHA1 Message Date
César Arroba 580a0436ae fix(permissions): address review on the real-time detection templates
Constrains the catalog rule to genuine AWS service events with a source prefix,
which PutEvents cannot forge, so a caller inside the account can no longer inject
a CloudTrail-shaped event into the Prowler pipeline.

Makes the Terraform deploy region an input instead of a hardcoded us-east-1,
which contradicted a README that told users to deploy once per region, and adds
the region to the manual put-events command so it reaches the right bus.

Renames the hello event status to Published and reworks the wording around it:
EventBridge accepting an event is not the endpoint receiving it, and Prowler
Cloud is what confirms the connection. Documents that Terraform writes the API
key to state, so the key belongs in a secret manager and the state in an
encrypted remote backend.
2026-08-18 18:02:15 +02:00
César Arroba b5a23a2764 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.
2026-08-17 11:55:19 +02:00
César Arroba 56d33fe252 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.
2026-08-17 11:24:00 +02:00
César Arroba 29102e7b33 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.
2026-08-17 11:15:45 +02:00
César Arroba 0a39c004b1 feat(permissions): default the real-time detection endpoint to Prowler Cloud
ProwlerWebhookUrl and prowler_webhook_url now default to
https://api.prowler.com/api/v1/realtime/events, matching the ingest path
defined in the MVP and the API base already used elsewhere in the repo.
Enabling real-time detection now only requires pasting the API key; the
endpoint is overridden only for self-hosted deployments or testing.
2026-08-17 10:50:27 +02:00
César Arroba ae1592dfa8 feat(permissions): add optional real-time detection to the AWS onboarding templates
Adds an opt-in EventBridge integration to the CloudFormation and Terraform
onboarding templates. When enabled, the tracked CloudTrail management events
are forwarded to Prowler Cloud through an API destination, so a change is
scanned within minutes instead of waiting for the next scheduled scan.

The whole block is gated behind EnableRealtimeDetection / enable_realtime_detection,
both defaulting to false, so the existing onboarding is unchanged unless
explicitly opted in. No new read permissions are added to the ProwlerScan role.
2026-08-17 10:44:11 +02:00
14 changed files with 956 additions and 2 deletions

No files matched your search

+1
View File
@@ -134,6 +134,7 @@ ai-config.json
.terraform*
*.tfstate
*.tfstate.*
permissions/templates/terraform/**/*.zip
# .env
ui/.env*
@@ -66,6 +66,35 @@ Parameters:
Type: String
Default: ""
# Real-Time Detection Parameters
EnableRealtimeDetection:
Description: |
Enable real-time detection. Forwards the tracked CloudTrail management events to Prowler Cloud
through an EventBridge API destination, so a change is scanned within minutes instead of waiting
for the next scheduled scan. Adds no new read permissions to the ProwlerScan role.
Type: String
Default: false
AllowedValues:
- true
- false
ProwlerWebhookUrl:
Description: |
Prowler Cloud endpoint that receives the events. Defaults to the Prowler Cloud ingest endpoint;
change it only for a self-hosted deployment or for testing. Required if EnableRealtimeDetection is true.
Type: String
Default: "https://api.prowler.com/api/v1/realtime/events"
AllowedPattern: '^(https://.+)?$'
ConstraintDescription: "ProwlerWebhookUrl must be an HTTPS URL."
ProwlerApiKey:
Description: |
Prowler Cloud API key used to authenticate the events sent to the endpoint above.
Shown once during Prowler Cloud onboarding. Required if EnableRealtimeDetection is true.
Type: String
Default: ""
NoEcho: true
# Deployment Control Parameters
DeployStackSet:
Description: |
@@ -134,6 +163,7 @@ Conditions:
DeployStackSetEnabled: !Equals [!Ref DeployStackSet, true]
DeployLocalRoleEnabled: !Equals [!Ref DeployLocalRole, true]
UseDelegatedAdmin: !Equals [!Ref DeployFromDelegatedAdmin, true]
RealtimeDetectionEnabled: !Equals [!Ref EnableRealtimeDetection, true]
Rules:
S3IntegrationRequiresParams:
@@ -144,6 +174,14 @@ Rules:
- Assert: !Not [!Equals [!Ref S3IntegrationBucketAccountId, ""]]
AssertDescription: "S3IntegrationBucketAccountId is required when EnableS3Integration is true."
RealtimeDetectionRequiresParams:
RuleCondition: !Equals [!Ref EnableRealtimeDetection, "true"]
Assertions:
- Assert: !Not [!Equals [!Ref ProwlerWebhookUrl, ""]]
AssertDescription: "ProwlerWebhookUrl is required when EnableRealtimeDetection is true."
- Assert: !Not [!Equals [!Ref ProwlerApiKey, ""]]
AssertDescription: "ProwlerApiKey is required when EnableRealtimeDetection is true."
Resources:
# Local ProwlerScan Role (deployed in this account)
ProwlerScan:
@@ -583,6 +621,351 @@ Resources:
Export:
Name: !Sub "${AWS::StackName}-ProwlerScanRoleArn"
# Real-Time Detection: forwards the tracked CloudTrail events to Prowler Cloud
ProwlerRealtimeConnection:
Type: AWS::Events::Connection
Condition: RealtimeDetectionEnabled
Properties:
Name: ProwlerRealtimeDetection
Description: Holds the Prowler Cloud API key used to authenticate forwarded events
AuthorizationType: API_KEY
AuthParameters:
ApiKeyAuthParameters:
ApiKeyName: x-api-key
ApiKeyValue: !Ref ProwlerApiKey
ProwlerRealtimeApiDestination:
Type: AWS::Events::ApiDestination
Condition: RealtimeDetectionEnabled
Properties:
Name: ProwlerRealtimeDetection
Description: Prowler Cloud endpoint that receives the forwarded CloudTrail events
ConnectionArn: !GetAtt ProwlerRealtimeConnection.Arn
InvocationEndpoint: !Ref ProwlerWebhookUrl
HttpMethod: POST
ProwlerRealtimeInvokeRole:
Type: AWS::IAM::Role
Condition: RealtimeDetectionEnabled
Properties:
RoleName: ProwlerRealtimeInvoke
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: "sts:AssumeRole"
Condition:
StringEquals:
"aws:SourceAccount": !Ref AWS::AccountId
ArnLike:
# 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:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: "events:InvokeApiDestination"
Resource: !GetAtt ProwlerRealtimeApiDestination.Arn
Tags:
- Key: "Service"
Value: "https://prowler.com"
- Key: "Support"
Value: "support@prowler.com"
- Key: "CloudFormation"
Value: "true"
- 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 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
Condition: RealtimeDetectionEnabled
Properties:
Name: ProwlerRealtimeDetection
Description: Forwards the CloudTrail management events tracked by Prowler real-time detection to Prowler Cloud
State: ENABLED
EventPattern:
# PutEvents rejects the reserved aws. prefix, so this keeps a caller in the
# account from forging a CloudTrail-shaped event
source:
- prefix: "aws."
detail-type:
- "AWS API Call via CloudTrail"
detail:
eventSource:
- ec2.amazonaws.com
- rds.amazonaws.com
- iam.amazonaws.com
- s3.amazonaws.com
- s3-control.amazonaws.com
- cloudtrail.amazonaws.com
- config.amazonaws.com
- guardduty.amazonaws.com
eventName:
# Security group opened to 0.0.0.0/0
- AuthorizeSecurityGroupIngress
- ModifySecurityGroupRules
# RDS instance made publicly accessible
- CreateDBInstance
- ModifyDBInstance
# Administrator privileges attached to a principal
- AttachUserPolicy
- AttachRolePolicy
- AttachGroupPolicy
- PutUserPolicy
- PutRolePolicy
# S3 bucket policy or ACL grants public access
- PutBucketPolicy
- PutBucketAcl
# S3 Block Public Access weakened
- PutAccountPublicAccessBlock
- DeleteAccountPublicAccessBlock
- PutBucketPublicAccessBlock
- DeleteBucketPublicAccessBlock
# CloudTrail logging stopped or trail deleted
- StopLogging
- DeleteTrail
- UpdateTrail
# AWS Config recorder stopped or deleted
- StopConfigurationRecorder
- DeleteConfigurationRecorder
# GuardDuty detector disabled or deleted
- UpdateDetector
- DeleteDetector
Targets:
- 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"
- Key: "Support"
Value: "support@prowler.com"
- Key: "CloudFormation"
Value: "true"
- 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):
"""Publish 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": "Published", "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:
@@ -599,11 +982,17 @@ Metadata:
- IAMPrincipal
- EnableOrganizations
- EnableS3Integration
- EnableRealtimeDetection
- Label:
default: Optional S3 Integration
Parameters:
- S3IntegrationBucketName
- S3IntegrationBucketAccountId
- Label:
default: Optional Real-Time Detection (ProwlerApiKey required if EnableRealtimeDetection is true)
Parameters:
- ProwlerWebhookUrl
- ProwlerApiKey
- Label:
default: StackSet Configuration (Required if DeployStackSet is true)
Parameters:
@@ -624,3 +1013,24 @@ Outputs:
Condition: DeployStackSetEnabled
Description: "StackSet ID for the ProwlerScan role deployment across accounts"
Value: !Ref ProwlerScanStackSet
ProwlerRealtimeRuleArn:
Condition: RealtimeDetectionEnabled
Description: "ARN of the EventBridge rule forwarding the tracked CloudTrail events to Prowler Cloud"
Value: !GetAtt ProwlerRealtimeRule.Arn
Export:
Name: !Sub "${AWS::StackName}-ProwlerRealtimeRuleArn"
ProwlerRealtimeApiDestinationArn:
Condition: RealtimeDetectionEnabled
Description: "ARN of the EventBridge API destination targeting Prowler Cloud"
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"
+46
View File
@@ -20,11 +20,15 @@ This Terraform configuration creates the necessary IAM role and policies to allo
### Variables
- `external_id` (required): External ID for role assumption security
- `region` (optional): AWS region to deploy to (default: `us-east-1`). The EventBridge rules are regional, so deploy once per region you want covered
- `account_id` (optional): AWS Account ID that will assume the role (defaults to Prowler Cloud: "232136659152")
- `iam_principal` (optional): IAM principal pattern allowed to assume the role (defaults to Prowler Cloud: "role/prowler*")
- `enable_s3_integration` (optional): Enable S3 integration for storing scan reports (default: false)
- `s3_integration_bucket_name` (conditional): S3 bucket name for reports (required if `enable_s3_integration` is true)
- `s3_integration_bucket_account_id` (conditional): S3 bucket owner account ID (required if `enable_s3_integration` is true)
- `enable_realtime_detection` (optional): Forward the tracked CloudTrail management events to Prowler Cloud through an EventBridge API destination (default: false)
- `prowler_webhook_url` (optional): Prowler Cloud endpoint that receives the events (defaults to Prowler Cloud: `https://api.prowler.com/api/v1/realtime/events`; change it only for a self-hosted deployment or for testing)
- `prowler_api_key` (conditional): Prowler Cloud API key used to authenticate the events (required if `enable_realtime_detection` is true)
### Usage Examples
@@ -42,6 +46,34 @@ terraform apply \
-var="s3_integration_bucket_account_id=123456789012"
```
#### With real-time detection enabled
```bash
terraform apply \
-var="external_id=your-external-id-here" \
-var="enable_realtime_detection=true" \
-var="prowler_api_key=your-prowler-api-key-here"
```
`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.
The apply publishes a hello event to verify the connection, 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 once it arrives, without running a scan.
The `prowler_realtime_hello_status` output reports only that EventBridge accepted the event, which is not the same as the endpoint receiving it: delivery is asynchronous. Prowler Cloud is what confirms the connection, and a delivery that fails every retry lands in the dead-letter queue below. The event is published again whenever `prowler_webhook_url` changes.
To re-check the connection at any point, publish it yourself. Use the same region the template deploys to, since the rule only exists on that region's event bus:
```bash
aws events put-events --region us-east-1 --entries '[{
"Source": "prowler.simulation",
"DetailType": "test_connection",
"Detail": "{}"
}]'
```
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 rules are regional. They forward only the events delivered to the default event bus of the region set in `region` (`us-east-1` by default). 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 the region you deploy to. Run the template once per region you want covered, changing `region` each time.
#### Using terraform.tfvars file (Recommended)
```bash
cp terraform.tfvars.example terraform.tfvars
@@ -60,5 +92,19 @@ After successful deployment, you'll get:
- `prowler_role_arn`: The ARN of the created IAM role (use this in Prowler App)
- `prowler_role_name`: The name of the IAM role
- `s3_integration_enabled`: Whether S3 integration is enabled
- `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)
- `prowler_realtime_hello_status`: whether EventBridge accepted the hello event on apply, `Published` or `Failed` with the error
### Handling the API key
Terraform writes every variable it is given to state, including `prowler_api_key`, and marking it `sensitive` only hides it from the CLI output. Before enabling real-time detection:
- Configure an encrypted, access-controlled remote backend (S3 with SSE and a restrictive bucket policy, Terraform Cloud, or equivalent). The default local state is a plaintext file in your working directory.
- Pass the key from your secret manager instead of typing it into a file, for example `export TF_VAR_prowler_api_key="$(your-secret-tool read prowler/api-key)"`.
- Do not commit a populated `terraform.tfvars`, and treat plan files as secrets too.
- Revoking the key in Prowler Cloud stops all ingestion, so rotate it there if a state file is ever exposed.
> **Note:** Terraform will use the AWS credentials of your default profile or AWS_PROFILE environment variable.
+19 -1
View File
@@ -15,6 +15,13 @@ check "s3_integration_requirements" {
}
}
check "realtime_detection_requirements" {
assert {
condition = !var.enable_realtime_detection || (var.prowler_webhook_url != "" && var.prowler_api_key != "")
error_message = "When enable_realtime_detection is true, both prowler_webhook_url and prowler_api_key must be provided and non-empty."
}
}
# IAM Role
###################################
data "aws_iam_policy_document" "prowler_assume_role_policy" {
@@ -113,8 +120,19 @@ module "s3_integration" {
source = "./s3-integration"
s3_integration_bucket_name = var.s3_integration_bucket_name
s3_integration_bucket_name = var.s3_integration_bucket_name
s3_integration_bucket_account_id = var.s3_integration_bucket_account_id
prowler_role_name = aws_iam_role.prowler_scan.name
}
# Real-Time Detection Module
###################################
module "realtime_detection" {
count = var.enable_realtime_detection ? 1 : 0
source = "./realtime-detection"
prowler_webhook_url = var.prowler_webhook_url
prowler_api_key = var.prowler_api_key
}
@@ -20,3 +20,28 @@ output "s3_integration_enabled" {
description = "Whether S3 integration is enabled"
value = var.enable_s3_integration
}
output "realtime_detection_enabled" {
description = "Whether real-time detection is enabled"
value = var.enable_realtime_detection
}
output "prowler_realtime_rule_arn" {
description = "ARN of the EventBridge rule forwarding the tracked CloudTrail events to Prowler Cloud (null if real-time detection is disabled)"
value = try(module.realtime_detection[0].prowler_realtime_rule_arn, null)
}
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)
}
output "prowler_realtime_hello_status" {
description = "Whether the hello event was accepted by EventBridge on apply (null if real-time detection is disabled)"
value = try(module.realtime_detection[0].prowler_realtime_hello_status, null)
}
@@ -0,0 +1,3 @@
data "aws_partition" "current" {}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
@@ -0,0 +1,45 @@
"""Publishes 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": "Published", "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 published: {error}")
return {"status": "Failed", "error": error}
@@ -0,0 +1,297 @@
# EventBridge Connection holding the Prowler Cloud API key
###################################
resource "aws_cloudwatch_event_connection" "prowler_realtime" {
name = "ProwlerRealtimeDetection"
description = "Holds the Prowler Cloud API key used to authenticate forwarded events"
authorization_type = "API_KEY"
auth_parameters {
api_key {
key = "x-api-key"
value = var.prowler_api_key
}
}
}
# EventBridge API Destination pointing at the Prowler Cloud endpoint
###################################
resource "aws_cloudwatch_event_api_destination" "prowler_realtime" {
name = "ProwlerRealtimeDetection"
description = "Prowler Cloud endpoint that receives the forwarded CloudTrail events"
invocation_endpoint = var.prowler_webhook_url
http_method = "POST"
connection_arn = aws_cloudwatch_event_connection.prowler_realtime.arn
}
# IAM Role assumed by EventBridge to invoke the API Destination
###################################
data "aws_iam_policy_document" "prowler_realtime_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["events.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values = [data.aws_caller_identity.current.account_id]
}
condition {
test = "ArnLike"
variable = "aws:SourceArn"
values = [
aws_cloudwatch_event_rule.prowler_realtime.arn,
aws_cloudwatch_event_rule.prowler_realtime_hello.arn,
]
}
}
}
resource "aws_iam_role" "prowler_realtime_invoke" {
name = "ProwlerRealtimeInvoke"
assume_role_policy = data.aws_iam_policy_document.prowler_realtime_assume_role.json
}
resource "aws_iam_role_policy" "prowler_realtime_invoke" {
name = "InvokeApiDestination"
role = aws_iam_role.prowler_realtime_invoke.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "events:InvokeApiDestination"
Resource = aws_cloudwatch_event_api_destination.prowler_realtime.arn
}]
})
}
# 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,
aws_cloudwatch_event_rule.prowler_realtime_hello.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" {
name = "ProwlerRealtimeDetection"
description = "Forwards the CloudTrail management events tracked by Prowler real-time detection to Prowler Cloud"
state = "ENABLED"
event_pattern = jsonencode({
# PutEvents rejects the reserved aws. prefix, so this keeps a caller in the
# account from forging a CloudTrail-shaped event
source = [{ prefix = "aws." }]
"detail-type" = ["AWS API Call via CloudTrail"]
detail = {
eventSource = [
"ec2.amazonaws.com",
"rds.amazonaws.com",
"iam.amazonaws.com",
"s3.amazonaws.com",
"s3-control.amazonaws.com",
"cloudtrail.amazonaws.com",
"config.amazonaws.com",
"guardduty.amazonaws.com",
]
eventName = [
# Security group opened to 0.0.0.0/0
"AuthorizeSecurityGroupIngress",
"ModifySecurityGroupRules",
# RDS instance made publicly accessible
"CreateDBInstance",
"ModifyDBInstance",
# Administrator privileges attached to a principal
"AttachUserPolicy",
"AttachRolePolicy",
"AttachGroupPolicy",
"PutUserPolicy",
"PutRolePolicy",
# S3 bucket policy or ACL grants public access
"PutBucketPolicy",
"PutBucketAcl",
# S3 Block Public Access weakened
"PutAccountPublicAccessBlock",
"DeleteAccountPublicAccessBlock",
"PutBucketPublicAccessBlock",
"DeleteBucketPublicAccessBlock",
# CloudTrail logging stopped or trail deleted
"StopLogging",
"DeleteTrail",
"UpdateTrail",
# AWS Config recorder stopped or deleted
"StopConfigurationRecorder",
"DeleteConfigurationRecorder",
# GuardDuty detector disabled or deleted
"UpdateDetector",
"DeleteDetector",
]
}
})
}
resource "aws_cloudwatch_event_target" "prowler_realtime" {
rule = aws_cloudwatch_event_rule.prowler_realtime.name
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
}
}
# 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
}
}
# 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,
]
}
@@ -0,0 +1,29 @@
output "prowler_realtime_rule_arn" {
description = "ARN of the EventBridge rule forwarding the tracked CloudTrail events to Prowler Cloud"
value = aws_cloudwatch_event_rule.prowler_realtime.arn
}
output "prowler_realtime_api_destination_arn" {
description = "ARN of the EventBridge API destination targeting Prowler Cloud"
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_hello_status" {
description = "Whether the hello event was accepted by EventBridge on apply. Prowler Cloud confirms the connection when the event reaches the endpoint"
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
}
@@ -0,0 +1,20 @@
variable "prowler_webhook_url" {
type = string
description = "Prowler Cloud endpoint that receives the events. Provided during Prowler Cloud onboarding."
validation {
condition = can(regex("^https://", var.prowler_webhook_url))
error_message = "prowler_webhook_url must be an HTTPS URL."
}
}
variable "prowler_api_key" {
type = string
description = "Prowler Cloud API key used to authenticate the events sent to the endpoint above."
sensitive = true
validation {
condition = length(var.prowler_api_key) > 0
error_message = "prowler_api_key must not be empty."
}
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.5"
required_providers {
archive = {
source = "hashicorp/archive"
version = "~> 2.4"
}
}
}
@@ -16,6 +16,10 @@ external_id = "your-unique-external-id-here"
# IAM Principal Pattern (leave default unless using self-hosted)
# iam_principal = "role/prowler*"
# AWS region to deploy to. The real-time detection rules are regional:
# run the template once per region you want covered.
# region = "us-east-1"
# =============================================================================
# S3 Integration Configuration
# =============================================================================
@@ -29,3 +33,20 @@ external_id = "your-unique-external-id-here"
# AWS Account ID that owns the S3 bucket (usually your account)
# s3_integration_bucket_account_id = "123456789012"
# =============================================================================
# Real-Time Detection Configuration
# =============================================================================
# Uncomment the following lines to forward the tracked CloudTrail management
# events to Prowler Cloud through an EventBridge API destination, so a change
# is scanned within minutes instead of waiting for the next scheduled scan.
# Enable real-time detection
# enable_realtime_detection = true
# Prowler Cloud endpoint that receives the events (leave default unless
# self-hosted or testing against another endpoint)
# prowler_webhook_url = "https://api.prowler.com/api/v1/realtime/events"
# Prowler Cloud API key used to authenticate the events (shown once during onboarding)
# prowler_api_key = "your-prowler-api-key-here"
@@ -27,6 +27,12 @@ variable "iam_principal" {
default = "role/prowler*"
}
variable "region" {
type = string
description = "AWS region to deploy to. Only relevant for real-time detection: the EventBridge rules are regional, so deploy once per region you want covered."
default = "us-east-1"
}
variable "enable_organizations" {
type = bool
description = "Enable AWS Organizations discovery permissions. Set to true only when deploying this role in the management account."
@@ -60,3 +66,27 @@ variable "s3_integration_bucket_account_id" {
error_message = "s3_integration_bucket_account_id must be a valid 12-digit AWS Account ID or empty."
}
}
variable "enable_realtime_detection" {
type = bool
description = "Enable real-time detection. Forwards the tracked CloudTrail management events to Prowler Cloud through an EventBridge API destination. Adds no new read permissions to the ProwlerScan role."
default = false
}
variable "prowler_webhook_url" {
type = string
description = "Prowler Cloud endpoint that receives the events. Defaults to the Prowler Cloud ingest endpoint; change it only for a self-hosted deployment or for testing."
default = "https://api.prowler.com/api/v1/realtime/events"
validation {
condition = var.prowler_webhook_url == "" || can(regex("^https://", var.prowler_webhook_url))
error_message = "prowler_webhook_url must be an HTTPS URL or empty."
}
}
variable "prowler_api_key" {
type = string
description = "Prowler Cloud API key used to authenticate the events sent to the endpoint above. Shown once during Prowler Cloud onboarding. Required if enable_realtime_detection is true."
default = ""
sensitive = true
}
+1 -1
View File
@@ -11,7 +11,7 @@ terraform {
}
provider "aws" {
region = "us-east-1"
region = var.region
default_tags {
tags = {
"Name" = "ProwlerScan",