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.
This commit is contained in:
César Arroba committed 2026-08-17 10:44:11 +02:00
1 parent 13ce9436b3
commit ae1592dfa8
11 files changed
+445 -1

No files matched your search

@@ -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. Provided during Prowler Cloud onboarding.
Required if EnableRealtimeDetection is true.
Type: String
Default: ""
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,130 @@ 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 name to avoid a circular dependency with the rule
"aws:SourceArn": !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/ProwlerRealtimeDetection"
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"
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:
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
Tags:
- Key: "Service"
Value: "https://prowler.com"
- Key: "Support"
Value: "support@prowler.com"
- Key: "CloudFormation"
Value: "true"
- Key: "Name"
Value: "ProwlerRealtimeDetection"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
@@ -599,11 +761,17 @@ Metadata:
- IAMPrincipal
- EnableOrganizations
- EnableS3Integration
- EnableRealtimeDetection
- Label:
default: Optional S3 Integration
Parameters:
- S3IntegrationBucketName
- S3IntegrationBucketAccountId
- Label:
default: Optional Real-Time Detection (Required if EnableRealtimeDetection is true)
Parameters:
- ProwlerWebhookUrl
- ProwlerApiKey
- Label:
default: StackSet Configuration (Required if DeployStackSet is true)
Parameters:
@@ -624,3 +792,17 @@ 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"
+17
View File
@@ -25,6 +25,9 @@ This Terraform configuration creates the necessary IAM role and policies to allo
- `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` (conditional): Prowler Cloud endpoint that receives the events (required if `enable_realtime_detection` is true)
- `prowler_api_key` (conditional): Prowler Cloud API key used to authenticate the events (required if `enable_realtime_detection` is true)
### Usage Examples
@@ -42,6 +45,17 @@ 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_webhook_url=https://api.prowler.com/api/v1/realtime/events" \
-var="prowler_api_key=your-prowler-api-key-here"
```
> **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)
```bash
cp terraform.tfvars.example terraform.tfvars
@@ -60,5 +74,8 @@ 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)
> **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,18 @@ 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)
}
@@ -0,0 +1,3 @@
data "aws_partition" "current" {}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
@@ -0,0 +1,132 @@
# 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]
}
# 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"]
}
}
}
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
}]
})
}
# 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({
"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
}
@@ -0,0 +1,14 @@
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_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,3 @@
terraform {
required_version = ">= 1.5"
}
@@ -29,3 +29,19 @@ 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 (provided during onboarding)
# 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"
@@ -60,3 +60,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. Provided during Prowler Cloud onboarding. Required if enable_realtime_detection is true."
default = ""
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
}