mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-14 07:24:15 +00:00
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.
This commit is contained in:
1 parent
29102e7b33
commit
56d33fe252
3 files changed
+225
-7
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user