Automated Threat Detection with GuardDuty
Build a threat detection pipeline with GuardDuty, EventBridge, and Lambda.
Introduction
30 Second Summary
Every building has a security system that detects intruders and triggers an alarm. Your cloud infrastructure needs the same kind of automated protection.
In this project, you will build an automated threat detection pipeline on AWS. It detects security threats, sends you alert emails, and automatically quarantines compromised credentials.
What You'll Build
A fully automated security pipeline that connects GuardDuty threat detection, EventBridge event routing, SNS email alerts, and Lambda automated remediation, all deployed with Terraform.
By the end of this project, you'll have:
- A GuardDuty detector deployed via Terraform with S3 protection enabled.
- An event-driven alert pipeline that routes high-severity findings through EventBridge to SNS email notifications.
- A Lambda function that automatically quarantines compromised IAM credentials by attaching a deny-all policy.
- Secret Mission: Add severity-based routing so medium findings trigger alerts only, while high-severity findings trigger both alerts and automated remediation.
Are there any prerequisites?
This project assumes familiarity with Terraform basics like providers, resources, and the plan/apply workflow. You should also be comfortable with AWS IAM concepts and Python fundamentals.
Before We Start
Before We Start...
Set Up Your Terraform Project
Every Terraform project starts with a structured directory of configuration files. Each file serves a specific purpose in defining your infrastructure.
In this step, you'll create the project structure, configure the AWS provider, and define the input variables your threat detection pipeline needs.
In this step, get ready to:
- Create the project directory with configuration files and a Lambda function folder.
- Configure the AWS provider and initialize Terraform.
- Define input variables for the AWS region and notification email.
Create the project directory and files
Your pipeline needs a few files to get started. main.tf defines your AWS resources. variables.tf holds configurable inputs like your region and email address.
outputs.tf displays key values after deployment. The lambda/ folder stores your Python remediation function.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir guardduty-threat-pipeline
cd guardduty-threat-pipeline
- Create the project files and the Lambda folder by running these commands:
macOS
touch main.tf variables.tf outputs.tf
mkdir lambda
touch lambda/remediate.py
Windows
New-Item -ItemType File main.tf, variables.tf, outputs.tf
New-Item -ItemType Directory lambda
New-Item -ItemType File lambda\remediate.py
- Confirm the files exist by running:
ls
You should see main.tf, variables.tf, outputs.tf, and the lambda folder in the output.
Don't see all the files?
- Make sure you ran the file creation commands from inside the guardduty-threat-pipeline folder.
- On Windows, if New-Item is not recognized, make sure you're using PowerShell, not Command Prompt.
- Why are my project files not showing up when I run ls?
Configure the AWS provider and initialize Terraform
The provider block tells Terraform which cloud platform to use and how to connect to it. Pinning the version to ~> 6.0 means Terraform accepts any 6.x release but blocks a major version jump.
- Open main.tf in your text editor and add the following code:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
What does this code do?
- The terraform block declares that this project depends on the hashicorp/aws provider. The ~> 6.0 version constraint accepts any 6.x release but blocks version 7.0 and above.
- The provider "aws" block configures how Terraform connects to AWS. The region is set from a variable (var.aws_region) that you'll define in the next substep.
- Save main.tf.
✔️ Awesome, I've got everything!
Great. Make sure you've saved main.tf before moving on.
ⓧ I'd like to double check the full code
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
- Initialize Terraform by running this command in your terminal:
terraform init
You should see output ending with "Terraform has been successfully initialized!" along with a message about installing the hashicorp/aws provider.
Seeing an error during init?
- If you see "Could not load plugin," check that your internet connection is active. Terraform downloads the AWS provider during initialization.
- If you see "No configuration files," make sure you're running terraform init from inside the guardduty-threat-pipeline folder where main.tf lives.
- Why is terraform init failing in my project?
Define your input variables
Variables make your Terraform configuration reusable. Instead of hardcoding your AWS region or email address, you define them as inputs that can change between deployments.
- Open variables.tf in your text editor and add the following code:
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
variable "notification_email" {
description = "Email address to receive GuardDuty security alerts"
type = string
}
What does this code do?
- aws_region has a default value of us-east-1, so Terraform uses that automatically unless you override it.
- notification_email has no default. Terraform prompts you to enter an email address every time you run terraform apply. This is where your security alert notifications get sent.
- Save variables.tf.
- Open outputs.tf in your text editor and add these placeholder comments:
# Outputs are defined inline in main.tf for this project.
# This file is kept as a placeholder for project structure convention.
# You can move outputs here if you prefer separation.
- Save outputs.tf.
Why is outputs.tf mostly empty?
For this project, you'll define outputs directly in main.tf alongside the resources they reference. Keeping outputs.tf as a placeholder preserves the standard project structure convention.
✔️ Awesome, I've got everything!
Great. Make sure you've saved both files before moving on.
ⓧ I'd like to double check the full code
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
variable "notification_email" {
description = "Email address to receive GuardDuty security alerts"
type = string
}
# Outputs are defined inline in main.tf for this project.
# This file is kept as a placeholder for project structure convention.
# You can move outputs here if you prefer separation.
Your project structure is ready and Terraform is initialized. Next up, you'll deploy GuardDuty and set up SNS to start receiving security alerts.
Deploy GuardDuty and SNS Notification
Your Terraform project is initialized with the AWS provider configured and your input variables defined. Time to deploy the first real cloud infrastructure.
A security pipeline needs two foundational pieces: a threat detector watching for suspicious activity, and a notification system to alert you when something is found. In this step, you'll provision both using Terraform.
In this step, get ready to:
- Enable GuardDuty threat detection with S3 protection.
- Create an SNS topic and email subscription for security alerts.
- Deploy both services and verify they're active.
Enable GuardDuty threat detection
GuardDuty is AWS's managed threat detection service. It continuously analyzes data sources like CloudTrail logs, VPC Flow Logs, and DNS queries to identify suspicious activity in your account.
- In main.tf, add the following code below the provider "aws" block:
# --- GuardDuty ---
resource "aws_guardduty_detector" "main" {
enable = true
finding_publishing_frequency = "FIFTEEN_MINUTES"
tags = {
Project = "guardduty-threat-pipeline"
}
}
resource "aws_guardduty_detector_feature" "s3_protection" {
detector_id = aws_guardduty_detector.main.id
name = "S3_DATA_EVENTS"
status = "ENABLED"
}
What does this code do?
- aws_guardduty_detector creates a GuardDuty detector that scans your account for threats. finding_publishing_frequency controls how often new findings are sent to EventBridge for processing.
- aws_guardduty_detector_feature enables S3-specific threat detection as a separate resource. This monitors S3 data access patterns for suspicious behavior like unauthorized downloads or unusual API calls.
Why is S3 protection a separate resource?
Terraform previously used a datasources block directly on the detector, but that was deprecated in March 2023. The current best practice is to use separate aws_guardduty_detector_feature resources for each protection type.
- Save main.tf.
- You won't see a change yet because Terraform needs the full configuration before you deploy. The next substep adds the notification resources.
Seeing red squiggles in your editor?
- Make sure the resource keyword is outside and below the provider "aws" block, not nested inside it.
- Check that every opening { has a matching closing }. The tags block is nested inside the detector, so it has its own pair of braces.
- Still stuck? Help me debug a syntax error in my GuardDuty Terraform resource.
Create the SNS notification channel
SNS (Simple Notification Service) is AWS's pub/sub messaging service. You'll create a topic that acts as a broadcast channel, then subscribe your email address so you receive alerts whenever a threat is detected.
- In main.tf, add the SNS topic and email subscription below the GuardDuty resources:
# --- SNS ---
resource "aws_sns_topic" "security_alerts" {
name = "guardduty-security-alerts"
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.security_alerts.arn
protocol = "email"
endpoint = var.notification_email
}
What does this code do?
- aws_sns_topic creates a named topic called guardduty-security-alerts. Think of a topic as a broadcast channel that can send messages to multiple subscribers.
- aws_sns_topic_subscription subscribes your email address to the topic. The protocol is set to email so alerts arrive in your inbox.
- Save main.tf. You won't see a change yet. The next chunk adds the permissions that let EventBridge publish to this topic.
The SNS topic exists, but EventBridge won't be able to send messages to it without an explicit access policy. You also need to look up your AWS account ID for permissions you'll configure in a later step.
- Below the subscription resource, add the data source and topic policy:
data "aws_caller_identity" "current" {}
resource "aws_sns_topic_policy" "allow_eventbridge" {
arn = aws_sns_topic.security_alerts.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowEventBridgePublish"
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "SNS:Publish"
Resource = aws_sns_topic.security_alerts.arn
}
]
})
}
What does this code do?
- data "aws_caller_identity" looks up your current AWS account ID at plan time. You'll use this value in a later step when configuring Lambda permissions.
- aws_sns_topic_policy grants the EventBridge service (events.amazonaws.com) permission to publish messages to your SNS topic. Without this policy, EventBridge would be blocked from sending alerts even if a rule matched.
- Save main.tf. You won't see a change yet. The next substep deploys everything together with terraform apply.
Seeing mismatched braces or bracket errors?
- The jsonencode() call has multiple levels of nesting. Count your brackets: jsonencode({ opens two levels, and }) closes them at the end.
- The Statement value is a list ([ ]) containing one object ({ }). Make sure you haven't accidentally dropped a bracket.
- Still stuck? Help me fix a bracket or brace mismatch in my SNS topic policy Terraform code.
Before deploying, double check your full main.tf matches the expected code.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.tf before moving on to deploy.
ⓧ I'd like to double check the full code
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# --- GuardDuty ---
resource "aws_guardduty_detector" "main" {
enable = true
finding_publishing_frequency = "FIFTEEN_MINUTES"
tags = {
Project = "guardduty-threat-pipeline"
}
}
resource "aws_guardduty_detector_feature" "s3_protection" {
detector_id = aws_guardduty_detector.main.id
name = "S3_DATA_EVENTS"
status = "ENABLED"
}
# --- SNS ---
resource "aws_sns_topic" "security_alerts" {
name = "guardduty-security-alerts"
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.security_alerts.arn
protocol = "email"
endpoint = var.notification_email
}
data "aws_caller_identity" "current" {}
resource "aws_sns_topic_policy" "allow_eventbridge" {
arn = aws_sns_topic.security_alerts.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowEventBridgePublish"
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "SNS:Publish"
Resource = aws_sns_topic.security_alerts.arn
}
]
})
}
Deploy and verify your resources
All the configuration is in place. Time to deploy both GuardDuty and SNS to your AWS account.
- Deploy your resources by running:
terraform apply
What to expect from this command
- Terraform prompts you for a value for notification_email. Type your real email address and press Enter. You need access to this inbox to confirm your subscription.
- Terraform displays a plan showing the resources it will create (detector, feature, topic, subscription, policy). Review the plan, then type yes and press Enter to apply.
Terraform creates all five resources in your AWS account. You should see Apply complete! Resources: 5 added in your terminal output.
- Check your email inbox for a message from AWS Notifications with the subject AWS Notification - Subscription Confirmation.
- Click the Confirm subscription link in the email. This activates your email as an SNS subscriber.
- Verify the GuardDuty detector is active by running:
aws guardduty list-detectors
What should I see?
You should see a JSON response with a DetectorIds array containing one ID string. This is your GuardDuty detector ID.
{
"DetectorIds": [
"abc123def456..."
]
}
Make a note of this detector ID. You'll need it in Step 5 when you generate sample findings to test the full pipeline.
Terraform apply failed?
- If you see A]detector already exists, GuardDuty is already enabled in this region from a previous project. You can import the existing detector with terraform import aws_guardduty_detector.main <detector-id>, or switch to a different region by changing the aws_region default in variables.tf.
- If you see InvalidParameterValueException for the SNS subscription, double check you entered a valid email address when prompted.
- If you didn't receive the confirmation email, check your spam folder. The email comes from no-reply@sns.amazonaws.com.
- Still stuck? Help me troubleshoot a terraform apply error when deploying GuardDuty and SNS resources.
Does GuardDuty cost money?
GuardDuty offers a 30-day free trial per region for new accounts. If you've already used GuardDuty in this region before, foundational threat detection costs approximately $4/month for a low-traffic personal account.
The Before You Go section covers how to clean up all resources with terraform destroy when you're done.
Your threat detector is live and your notification channel is ready. Next up, you'll build the EventBridge rule that connects them, so GuardDuty findings automatically trigger SNS alerts.
Build the EventBridge to SNS Alert Pipeline
Your GuardDuty detector is running and your SNS topic is ready with a confirmed email subscription.
Now you need a way to connect them. EventBridge is an AWS event routing service that can watch for GuardDuty findings and automatically forward them to targets like SNS topics.
In this step, get ready to:
- Create an EventBridge rule that filters for high-severity GuardDuty findings.
- Add an SNS target that formats and sends alert emails.
- Test the pipeline end-to-end with sample findings.
Create the EventBridge rule for high-severity findings
An EventBridge rule defines a pattern to match against incoming events. GuardDuty publishes every finding as an event, and your rule will filter for only the high-severity and critical ones (severity 7 and above).
What are GuardDuty severity levels?
GuardDuty assigns a numeric severity to each finding. Low is 1 to 3.9. Medium is 4 to 6.9. High is 7 to 8.9. Critical is 9 to 10.
By filtering for severity >= 7, your rule catches both high and critical findings while ignoring the noise from lower-severity events.
- In main.tf, find the closing } of the aws_sns_topic_policy resource block (the last resource in your file).
- Add the EventBridge rule resource below it by copying this code:
# --- EventBridge Rule (High Severity) ---
resource "aws_cloudwatch_event_rule" "guardduty_high_severity" {
name = "guardduty-high-severity-findings"
description = "Capture high and critical severity GuardDuty findings"
event_pattern = jsonencode({
source = ["aws.guardduty"]
detail-type = ["GuardDuty Finding"]
detail = {
severity = [{ numeric = [">=", 7] }]
}
})
}
What does this code do?
- aws_cloudwatch_event_rule creates an EventBridge rule. Terraform uses the cloudwatch_event_rule name because EventBridge was originally part of CloudWatch Events.
- event_pattern tells EventBridge to match only events from GuardDuty (source aws.guardduty) with detail-type GuardDuty Finding.
- The numeric filter [">=", 7] ensures only findings with severity 7 or higher pass through to the targets you will add next.
- Save main.tf.
You won't see a change yet. The next substep adds a target for this rule, then you will deploy everything together.
Add the SNS notification target
A rule on its own just matches events. It needs a target to actually do something with them. Your target sends matched findings to the SNS topic, formatted into a readable alert message.
- In main.tf, add the following code directly below the aws_cloudwatch_event_rule resource you just added:
# Target 1: SNS notification for high severity findings
resource "aws_cloudwatch_event_target" "high_severity_sns" {
rule = aws_cloudwatch_event_rule.guardduty_high_severity.name
target_id = "guardduty-high-to-sns"
arn = aws_sns_topic.security_alerts.arn
input_transformer {
input_paths = {
severity = "$.detail.severity"
type = "$.detail.type"
description = "$.detail.description"
account = "$.detail.accountId"
region = "$.detail.region"
}
input_template = "\"[HIGH SEVERITY] GuardDuty Finding in account <account> (<region>). Severity: <severity>. Type: <type>. Description: <description>\""
}
}
What does this code do?
- aws_cloudwatch_event_target connects the EventBridge rule to your SNS topic. Every event that matches the rule gets forwarded here.
- input_paths extracts specific values from the raw GuardDuty event JSON using JSONPath expressions (like $.detail.severity).
- input_template formats those extracted values into a human-readable alert message. The placeholders like <account> and <severity> get replaced with the real values from the finding.
- Save main.tf.
You won't see a change yet. The next substep deploys this with terraform apply.
✔️ Awesome, I've got everything!
Double check you have saved main.tf before moving on.
ⓧ I'd like to double check the full code
Here is the full main.tf file as it should look right now.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# --- GuardDuty ---
resource "aws_guardduty_detector" "main" {
enable = true
finding_publishing_frequency = "FIFTEEN_MINUTES"
tags = {
Project = "guardduty-threat-pipeline"
}
}
resource "aws_guardduty_detector_feature" "s3_protection" {
detector_id = aws_guardduty_detector.main.id
name = "S3_DATA_EVENTS"
status = "ENABLED"
}
# --- SNS ---
resource "aws_sns_topic" "security_alerts" {
name = "guardduty-security-alerts"
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.security_alerts.arn
protocol = "email"
endpoint = var.notification_email
}
data "aws_caller_identity" "current" {}
resource "aws_sns_topic_policy" "allow_eventbridge" {
arn = aws_sns_topic.security_alerts.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowEventBridgePublish"
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "SNS:Publish"
Resource = aws_sns_topic.security_alerts.arn
}
]
})
}
# --- EventBridge Rule (High Severity) ---
resource "aws_cloudwatch_event_rule" "guardduty_high_severity" {
name = "guardduty-high-severity-findings"
description = "Capture high and critical severity GuardDuty findings"
event_pattern = jsonencode({
source = ["aws.guardduty"]
detail-type = ["GuardDuty Finding"]
detail = {
severity = [{ numeric = [">=", 7] }]
}
})
}
# Target 1: SNS notification for high severity findings
resource "aws_cloudwatch_event_target" "high_severity_sns" {
rule = aws_cloudwatch_event_rule.guardduty_high_severity.name
target_id = "guardduty-high-to-sns"
arn = aws_sns_topic.security_alerts.arn
input_transformer {
input_paths = {
severity = "$.detail.severity"
type = "$.detail.type"
description = "$.detail.description"
account = "$.detail.accountId"
region = "$.detail.region"
}
input_template = "\"[HIGH SEVERITY] GuardDuty Finding in account <account> (<region>). Severity: <severity>. Type: <type>. Description: <description>\""
}
}
Deploy and test with sample findings
Time to deploy the EventBridge rule and target, then verify the pipeline works by generating a sample GuardDuty finding.
- Deploy the new resources by running this command:
terraform apply
What should I see?
Terraform shows a plan to add 2 new resources (the EventBridge rule and target). Type yes when prompted to confirm. You should see Apply complete! Resources: 2 added in the output.
Seeing an error during apply?
- If you see a syntax error, double check the jsonencode block in event_pattern. The brackets and braces must match exactly.
- If you see a reference error for aws_cloudwatch_event_rule.guardduty_high_severity, make sure the rule resource is defined before the target resource in your file.
- help me debug a terraform apply error for my EventBridge rule
Now you need to generate a sample GuardDuty finding to test the pipeline. GuardDuty provides a built-in CLI command that creates realistic test findings without needing an actual security threat.
- First, retrieve your GuardDuty detector ID by running:
aws guardduty list-detectors
You should see a JSON response with a DetectorIds array containing one ID string. Copy that ID.
- Generate a sample IAM finding by running this command with your detector ID:
aws guardduty create-sample-findings --detector-id [[DETECTOR_ID="your GuardDuty detector ID"]] --finding-types "Recon:IAMUser/MaliciousIPCaller"
What does this command do?
create-sample-findings tells GuardDuty to generate a test finding of the specified type. The Recon:IAMUser/MaliciousIPCaller type simulates a reconnaissance attempt from a known malicious IP address.
The command produces no output on success. Sample findings are flagged with "sample": true internally so they can be distinguished from real threats.
- Wait 1 to 2 minutes for the finding to propagate through EventBridge.
- Check the email inbox for the address you provided as notification_email.
You should see an email from AWS Notifications containing your formatted alert message with [HIGH SEVERITY] in the subject, along with the finding type, severity, and region.
Don't see the email?
- Check your spam or junk folder. AWS notification emails sometimes get filtered.
- Make sure you confirmed the SNS email subscription in Step 2. An unconfirmed subscription will not deliver messages.
- Wait a few more minutes. Sample findings can take up to 5 minutes to flow through EventBridge to SNS.
- help me troubleshoot why my GuardDuty alert email is not arriving
Your detection-to-notification pipeline is live. Next up, you will write a Lambda function that automatically quarantines compromised IAM credentials when GuardDuty detects a threat.
Write and Deploy the Lambda Remediation Function
Your EventBridge rule is now routing high-severity GuardDuty findings to SNS for email alerts. Getting a notification is great, but by the time a human reads that email and responds, a compromised credential could have already done serious damage.
In this step, you will write a Lambda function that automatically quarantines a compromised IAM user by attaching a deny-all policy. Then you will deploy the function with Terraform and wire it into your EventBridge pipeline.
In this step, get ready to:
- Write a Python Lambda function that quarantines compromised IAM users.
- Create Terraform resources to package, permission, and deploy the Lambda function.
- Connect the EventBridge rule to trigger Lambda remediation automatically.
Write the Python Lambda remediation function
The Lambda function needs to do three things when it receives a GuardDuty finding. Extract the IAM user from the event, validate that it is a real IAM user finding, and quarantine the user by attaching a deny-all policy.
- Open lambda/remediate.py in your editor and add the module-level setup code:
import json
import logging
import boto3
# Set up logging so remediation actions appear in CloudWatch Logs
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Create an IAM client to attach policies to users
iam_client = boto3.client("iam")
# The policy that blocks ALL actions for a quarantined user
DENY_ALL_POLICY = json.dumps({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*"
}
]
})
What does this code do?
boto3 is the AWS SDK for Python. The iam_client gives you access to IAM API calls like attaching policies to users.
DENY_ALL_POLICY is a JSON string that blocks every possible AWS action. When attached to a user, it immediately neutralizes their credentials without deleting the user or their access keys. This preserves forensic evidence while stopping the threat.
- Save lambda/remediate.py.
You will not see any output yet. The next chunk adds the handler function that uses these imports.
- Below the DENY_ALL_POLICY constant in lambda/remediate.py, add the handler function:
def lambda_handler(event, context):
logger.info("Received GuardDuty finding event: %s", json.dumps(event))
finding_type = event.get("detail", {}).get("type", "Unknown")
severity = event.get("detail", {}).get("severity", 0)
resource = event.get("detail", {}).get("resource", {})
logger.info("Finding type: %s, Severity: %s", finding_type, severity)
# Extract IAM user information from the finding
access_key_details = resource.get("accessKeyDetails", {})
user_name = access_key_details.get("userName")
user_type = access_key_details.get("userType")
if not user_name:
logger.warning("No IAM user name found in finding. Skipping remediation.")
return {
"statusCode": 200,
"body": "No IAM user to remediate"
}
if user_type != "IAMUser":
logger.info("Resource is not an IAM user (type: %s). Skipping remediation.", user_type)
return {
"statusCode": 200,
"body": f"Skipping non-IAM user type: {user_type}"
}
# Check if this is a sample finding
is_sample = event.get("detail", {}).get("service", {}).get("additionalInfo", {}).get("sample", False)
if is_sample:
logger.info("Sample finding detected for user: %s. Logging only, not quarantining.", user_name)
return {
"statusCode": 200,
"body": f"Sample finding for user {user_name}. No action taken."
}
# Quarantine the IAM user by attaching a deny-all policy
logger.info("Quarantining IAM user: %s", user_name)
try:
iam_client.put_user_policy(
UserName=user_name,
PolicyName="quarantine-policy",
PolicyDocument=DENY_ALL_POLICY
)
logger.info("Successfully quarantined user: %s", user_name)
except Exception as e:
logger.error("Failed to quarantine user %s: %s", user_name, str(e))
raise
return {
"statusCode": 200,
"body": f"Quarantined IAM user: {user_name}"
}
What does this handler do?
The function runs through a series of safety checks before taking action:
- It extracts the finding type, severity, and resource details from the GuardDuty event JSON.
- It skips remediation if there is no IAM user name in the finding, or if the resource type is not IAMUser (for example, it could be a role or root account).
- It checks the sample flag. GuardDuty sample findings have this set to true, so the function logs the event without quarantining a real user during testing.
- Only for real findings does it call iam_client.put_user_policy() to attach the deny-all quarantine-policy to the compromised user.
Why quarantine instead of delete?
Deleting a compromised user destroys the access keys and login trail, which are critical for forensic investigation. Attaching a deny-all policy immediately blocks all actions while preserving the evidence.
- Save lambda/remediate.py.
The function is not deployed yet. You will add the Terraform resources for that next.
Seeing a syntax error in your editor?
Make sure the def lambda_handler line is separated from DENY_ALL_POLICY by two blank lines. Python expects two blank lines before a top-level function definition.
Check that all f-strings use the f"...{variable}" format with the f prefix before the opening quote.
Help me debug a syntax error in my Lambda remediation function
✔️ Awesome, I've got everything!
Great. Double check you have saved lambda/remediate.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete lambda/remediate.py file:
import json
import logging
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
iam_client = boto3.client("iam")
DENY_ALL_POLICY = json.dumps({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*"
}
]
})
def lambda_handler(event, context):
logger.info("Received GuardDuty finding event: %s", json.dumps(event))
finding_type = event.get("detail", {}).get("type", "Unknown")
severity = event.get("detail", {}).get("severity", 0)
resource = event.get("detail", {}).get("resource", {})
logger.info("Finding type: %s, Severity: %s", finding_type, severity)
# Extract IAM user information from the finding
access_key_details = resource.get("accessKeyDetails", {})
user_name = access_key_details.get("userName")
user_type = access_key_details.get("userType")
if not user_name:
logger.warning("No IAM user name found in finding. Skipping remediation.")
return {
"statusCode": 200,
"body": "No IAM user to remediate"
}
if user_type != "IAMUser":
logger.info("Resource is not an IAM user (type: %s). Skipping remediation.", user_type)
return {
"statusCode": 200,
"body": f"Skipping non-IAM user type: {user_type}"
}
# Check if this is a sample finding
is_sample = event.get("detail", {}).get("service", {}).get("additionalInfo", {}).get("sample", False)
if is_sample:
logger.info("Sample finding detected for user: %s. Logging only, not quarantining.", user_name)
return {
"statusCode": 200,
"body": f"Sample finding for user {user_name}. No action taken."
}
# Quarantine the IAM user by attaching a deny-all policy
logger.info("Quarantining IAM user: %s", user_name)
try:
iam_client.put_user_policy(
UserName=user_name,
PolicyName="quarantine-policy",
PolicyDocument=DENY_ALL_POLICY
)
logger.info("Successfully quarantined user: %s", user_name)
except Exception as e:
logger.error("Failed to quarantine user %s: %s", user_name, str(e))
raise
return {
"statusCode": 200,
"body": f"Quarantined IAM user: {user_name}"
}
Add the Lambda Terraform resources
The Lambda function code is ready, but AWS needs more than just a Python file. Terraform needs to zip the code, create an IAM execution role with the right permissions, and deploy the function itself.
- At the bottom of main.tf, add the Lambda section header, the archive data source, and the IAM execution role:
# --- Lambda Remediation Function ---
# Zip the Python code so Lambda can consume it
data "archive_file" "lambda_zip" {
type = "zip"
source_file = "${path.module}/lambda/remediate.py"
output_path = "${path.module}/lambda/remediate.zip"
}
# IAM role that the Lambda function assumes when it runs
resource "aws_iam_role" "lambda_role" {
name = "guardduty-remediation-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
What does this code do?
- data "archive_file" tells Terraform to zip lambda/remediate.py into lambda/remediate.zip. Lambda requires code to be uploaded as a zip archive.
- The aws_iam_role creates an execution role that Lambda assumes at runtime. The assume_role_policy (trust policy) allows only the lambda.amazonaws.com service to assume it.
- Save main.tf.
You will not see any changes yet. The next chunk adds the permissions policy and the Lambda function resource.
- Below the aws_iam_role block you just added, add the IAM permissions policy and the Lambda function resource:
# Permissions the Lambda function needs to quarantine users and write logs
resource "aws_iam_role_policy" "lambda_policy" {
name = "guardduty-remediation-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"iam:PutUserPolicy"
]
Resource = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/*"
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:${data.aws_caller_identity.current.account_id}:*"
}
]
})
}
# The Lambda function itself
resource "aws_lambda_function" "remediation" {
filename = data.archive_file.lambda_zip.output_path
function_name = "guardduty-remediation"
role = aws_iam_role.lambda_role.arn
handler = "remediate.lambda_handler"
runtime = "python3.12"
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
timeout = 30
tags = {
Project = "guardduty-threat-pipeline"
}
}
What does this code do?
- The aws_iam_role_policy grants the Lambda two permissions. iam:PutUserPolicy lets it attach inline policies to IAM users (the quarantine action). The logs:* permissions let it write execution logs to CloudWatch Logs.
- The aws_lambda_function deploys the zipped code with the python3.12 runtime. The handler value remediate.lambda_handler tells Lambda to look in the remediate module for the lambda_handler function.
- source_code_hash ensures Terraform redeploys the function whenever the zip contents change.
What is least-privilege IAM?
The policy only grants iam:PutUserPolicy and logging permissions. It cannot delete users, read secrets, or modify other resources. This follows the principle of least privilege: give only the minimum permissions needed to do the job.
- Save main.tf.
You will not deploy yet. The next substep adds the EventBridge wiring, then you will apply everything together.
Connect EventBridge to the Lambda function and deploy
The Lambda function exists in Terraform, but EventBridge does not know about it yet. You need a second target on the existing high-severity rule and a permission that allows EventBridge to invoke the function.
- In main.tf, find the closing } of the high_severity_sns target block (the one with target_id = "guardduty-high-to-sns").
- Add the following code between that block and the # --- Lambda Remediation Function --- section header:
# Target 2: Lambda remediation for high severity findings
resource "aws_cloudwatch_event_target" "high_severity_lambda" {
rule = aws_cloudwatch_event_rule.guardduty_high_severity.name
target_id = "guardduty-high-to-lambda"
arn = aws_lambda_function.remediation.arn
}
# Allow EventBridge to invoke the Lambda function
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.remediation.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.guardduty_high_severity.arn
}
What does this code do?
- The aws_cloudwatch_event_target adds a second target to the same guardduty-high-severity-findings rule. Now every high-severity finding triggers both the SNS notification and the Lambda function.
- The aws_lambda_permission explicitly grants the events.amazonaws.com service permission to invoke the function. Without this, EventBridge would be blocked by Lambda's resource-based policy.
- Save main.tf.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.tf before deploying.
ⓧ I'd like to double check the full code
Here is the complete main.tf file at this point:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# --- GuardDuty ---
resource "aws_guardduty_detector" "main" {
enable = true
finding_publishing_frequency = "FIFTEEN_MINUTES"
tags = {
Project = "guardduty-threat-pipeline"
}
}
resource "aws_guardduty_detector_feature" "s3_protection" {
detector_id = aws_guardduty_detector.main.id
name = "S3_DATA_EVENTS"
status = "ENABLED"
}
# --- SNS ---
resource "aws_sns_topic" "security_alerts" {
name = "guardduty-security-alerts"
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.security_alerts.arn
protocol = "email"
endpoint = var.notification_email
}
data "aws_caller_identity" "current" {}
resource "aws_sns_topic_policy" "allow_eventbridge" {
arn = aws_sns_topic.security_alerts.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowEventBridgePublish"
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "SNS:Publish"
Resource = aws_sns_topic.security_alerts.arn
}
]
})
}
# --- EventBridge Rule (High Severity) ---
resource "aws_cloudwatch_event_rule" "guardduty_high_severity" {
name = "guardduty-high-severity-findings"
description = "Capture high and critical severity GuardDuty findings"
event_pattern = jsonencode({
source = ["aws.guardduty"]
detail-type = ["GuardDuty Finding"]
detail = {
severity = [{ numeric = [">=", 7] }]
}
})
}
# Target 1: SNS notification for high severity findings
resource "aws_cloudwatch_event_target" "high_severity_sns" {
rule = aws_cloudwatch_event_rule.guardduty_high_severity.name
target_id = "guardduty-high-to-sns"
arn = aws_sns_topic.security_alerts.arn
input_transformer {
input_paths = {
severity = "$.detail.severity"
type = "$.detail.type"
description = "$.detail.description"
account = "$.detail.accountId"
region = "$.detail.region"
}
input_template = "\"[HIGH SEVERITY] GuardDuty Finding in account <account> (<region>). Severity: <severity>. Type: <type>. Description: <description>\""
}
}
# Target 2: Lambda remediation for high severity findings
resource "aws_cloudwatch_event_target" "high_severity_lambda" {
rule = aws_cloudwatch_event_rule.guardduty_high_severity.name
target_id = "guardduty-high-to-lambda"
arn = aws_lambda_function.remediation.arn
}
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.remediation.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.guardduty_high_severity.arn
}
# --- Lambda Remediation Function ---
data "archive_file" "lambda_zip" {
type = "zip"
source_file = "${path.module}/lambda/remediate.py"
output_path = "${path.module}/lambda/remediate.zip"
}
resource "aws_iam_role" "lambda_role" {
name = "guardduty-remediation-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "lambda_policy" {
name = "guardduty-remediation-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"iam:PutUserPolicy"
]
Resource = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/*"
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:${data.aws_caller_identity.current.account_id}:*"
}
]
})
}
resource "aws_lambda_function" "remediation" {
filename = data.archive_file.lambda_zip.output_path
function_name = "guardduty-remediation"
role = aws_iam_role.lambda_role.arn
handler = "remediate.lambda_handler"
runtime = "python3.12"
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
timeout = 30
tags = {
Project = "guardduty-threat-pipeline"
}
}
- Deploy the Lambda function and EventBridge wiring by running:
terraform apply
- Terraform will prompt you to confirm. Type yes and press Enter.
You should see Terraform create several new resources including the Lambda function, IAM role, and EventBridge target. The output ends with Apply complete! followed by a count of added resources.
- Verify the Lambda function was created by running:
aws lambda get-function --function-name guardduty-remediation --query "Configuration.[FunctionName,Runtime,Handler]" --output table
You should see a table showing guardduty-remediation, python3.12, and remediate.lambda_handler.
Seeing an error during terraform apply?
- If you see Error: creating Lambda Function, make sure lambda/remediate.py exists and is saved. The archive_file data source needs the Python file to exist before it can create the zip.
- If you see Error: Invalid function argument or a reference error, check that resource names in your main.tf match exactly. For example, aws_lambda_function.remediation must match the resource name in your Lambda function block.
- If you see AccessDeniedException, your AWS CLI credentials may not have permission to create IAM roles or Lambda functions. Verify you are using credentials with admin or sufficient IAM and Lambda permissions.
Help me debug a terraform apply error when deploying my Lambda function
Your automated remediation function is deployed and wired into the pipeline. Next up, you will test the complete end-to-end pipeline with sample findings to prove that detection, notification, and remediation all work together.
Test the Complete Pipeline End-to-End
Your entire pipeline is deployed. GuardDuty is watching for threats. EventBridge is filtering by severity. Lambda is ready to quarantine compromised users. SNS is set up to alert you by email.
The final step is to prove it all works together. You'll generate a simulated threat finding and trace it through every stage of the pipeline.
In this step, get ready to:
- Generate a sample IAM threat finding using the AWS CLI.
- Verify the SNS email notification and Lambda execution in CloudWatch Logs.
- Add Terraform outputs and review all deployed resources.
Generate a sample IAM threat finding
GuardDuty has a built-in mechanism for generating test findings without any real threats. These sample findings flow through EventBridge exactly like real ones, so they're perfect for validating your pipeline.
- First, retrieve your GuardDuty detector ID by running this command:
aws guardduty list-detectors
You should see a JSON response with a DetectorIds array containing one ID string. Copy this ID.
Seeing an empty DetectorIds array?
Make sure you're using the same AWS region you deployed to. If your default region differs from the one in your variables.tf, add --region us-east-1 to the command.
Help me troubleshoot why my GuardDuty detector list is empty
- Generate an IAM-specific sample finding by running this command:
aws guardduty create-sample-findings --detector-id [[DETECTOR_ID="your GuardDuty detector ID"]] --finding-types "UnauthorizedAccess:IAMUser/MaliciousIPCaller"
What does this command do?
This tells GuardDuty to create a simulated finding of type UnauthorizedAccess:IAMUser/MaliciousIPCaller. This finding type represents an API call from a known malicious IP address using IAM user credentials.
Sample findings include a "sample": true flag in their JSON. Your Lambda function checks for this flag and skips the quarantine action, so no real IAM users are affected during testing.
The command produces no output on success. Wait 1-2 minutes for the finding to propagate through EventBridge to your SNS topic and Lambda function.
Verify the SNS notification and Lambda logs
Your pipeline has two outputs to verify. The first is the email alert from SNS. The second is the Lambda execution logged in CloudWatch Logs.
- Check the email inbox for the address you provided as your notification email.
- Look for an email from AWS Notifications with a subject line starting with [HIGH SEVERITY].
The email body contains the formatted alert with the finding severity, type, description, account ID, and region. This confirms that EventBridge matched the finding and routed it to SNS.
No email after 2 minutes?
Check your spam or junk folder. Also confirm you clicked the subscription confirmation link from the earlier step. Unconfirmed SNS subscriptions silently drop messages.
Help me figure out why my SNS notification email is not arriving
Now verify that your Lambda function also fired. CloudWatch Logs captures every Lambda invocation automatically.
- Confirm the Lambda log group exists by running this command:
aws logs describe-log-groups --log-group-name-prefix "/aws/lambda/guardduty-remediation"
You should see a logGroups array with one entry named /aws/lambda/guardduty-remediation. This confirms the Lambda function has executed at least once.
- Retrieve the latest log stream by running this command:
aws logs describe-log-streams --log-group-name "/aws/lambda/guardduty-remediation" --order-by LastEventTime --descending --limit 1
You should see a logStreams array with one entry. Copy the logStreamName value.
- View the actual Lambda log output by running this command:
aws logs get-log-events --log-group-name "/aws/lambda/guardduty-remediation" --log-stream-name [[LOG_STREAM="your log stream name from the previous command"]]
What should I see in the logs?
Look for log messages like Received GuardDuty finding event, the finding type, and severity. Because this is a sample finding, you should also see Sample finding detected followed by No action taken.
This proves the Lambda function received the finding from EventBridge, parsed it correctly, and detected the sample flag before skipping quarantine.
No log group found?
The log group only appears after the Lambda function runs for the first time. Wait another minute and try again. If it still doesn't appear, verify the EventBridge rule has a Lambda target by running terraform state show aws_cloudwatch_event_target.high_severity_lambda.
Help me figure out why my Lambda is not being invoked by EventBridge
Add Terraform outputs and review your deployment
Your pipeline is verified. The last thing to do is add Terraform outputs so you can quickly reference key resource identifiers without digging through the AWS Console.
- In main.tf, scroll to the very bottom of the file (after the aws_lambda_function resource block).
- Add the following output blocks:
# --- Outputs ---
output "guardduty_detector_id" {
description = "The ID of the GuardDuty detector"
value = aws_guardduty_detector.main.id
}
output "sns_topic_arn" {
description = "ARN of the security alerts SNS topic"
value = aws_sns_topic.security_alerts.arn
}
output "lambda_function_arn" {
description = "ARN of the remediation Lambda function"
value = aws_lambda_function.remediation.arn
}
output "eventbridge_rule_arn" {
description = "ARN of the high severity EventBridge rule"
value = aws_cloudwatch_event_rule.guardduty_high_severity.arn
}
What do these outputs do?
Terraform outputs print key values to your terminal after every terraform apply. They also make these values available to other Terraform modules if you expand this project later.
- guardduty_detector_id is the detector you used to generate sample findings.
- sns_topic_arn is the ARN of the topic that sends you email alerts.
- lambda_function_arn is the ARN of the remediation function that quarantines compromised users.
- eventbridge_rule_arn is the ARN of the severity filter rule that routes findings.
- Save main.tf.
- Deploy the outputs by running this command:
terraform apply -auto-approve
You should see four output values printed at the end of the apply. Each shows the real ARN or ID of the resource you deployed.
Terraform apply failed?
Check that the output blocks are placed outside all resource blocks. Each output block should be at the top level of the file, after the last closing } of the Lambda function resource.
Help me fix my Terraform apply error after adding outputs
- Review all the resources Terraform created by running this command:
terraform state list
You should see a list of every resource in your pipeline. This includes the GuardDuty detector, SNS topic and subscription, EventBridge rule and targets, Lambda function, and IAM role and policy.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.tf and that your four output values are printing in the terminal.
ⓧ I'd like to double check the full code
Here is the complete main.tf file with the outputs added at the bottom.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# --- GuardDuty ---
resource "aws_guardduty_detector" "main" {
enable = true
finding_publishing_frequency = "FIFTEEN_MINUTES"
tags = {
Project = "guardduty-threat-pipeline"
}
}
resource "aws_guardduty_detector_feature" "s3_protection" {
detector_id = aws_guardduty_detector.main.id
name = "S3_DATA_EVENTS"
status = "ENABLED"
}
# --- SNS ---
resource "aws_sns_topic" "security_alerts" {
name = "guardduty-security-alerts"
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.security_alerts.arn
protocol = "email"
endpoint = var.notification_email
}
data "aws_caller_identity" "current" {}
resource "aws_sns_topic_policy" "allow_eventbridge" {
arn = aws_sns_topic.security_alerts.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowEventBridgePublish"
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "SNS:Publish"
Resource = aws_sns_topic.security_alerts.arn
}
]
})
}
# --- EventBridge Rule (High Severity) ---
resource "aws_cloudwatch_event_rule" "guardduty_high_severity" {
name = "guardduty-high-severity-findings"
description = "Capture high and critical severity GuardDuty findings"
event_pattern = jsonencode({
source = ["aws.guardduty"]
detail-type = ["GuardDuty Finding"]
detail = {
severity = [{ numeric = [">=", 7] }]
}
})
}
# Target 1: SNS notification for high severity findings
resource "aws_cloudwatch_event_target" "high_severity_sns" {
rule = aws_cloudwatch_event_rule.guardduty_high_severity.name
target_id = "guardduty-high-to-sns"
arn = aws_sns_topic.security_alerts.arn
input_transformer {
input_paths = {
severity = "$.detail.severity"
type = "$.detail.type"
description = "$.detail.description"
account = "$.detail.accountId"
region = "$.detail.region"
}
input_template = "\"[HIGH SEVERITY] GuardDuty Finding in account <account> (<region>). Severity: <severity>. Type: <type>. Description: <description>\""
}
}
# Target 2: Lambda remediation for high severity findings
resource "aws_cloudwatch_event_target" "high_severity_lambda" {
rule = aws_cloudwatch_event_rule.guardduty_high_severity.name
target_id = "guardduty-high-to-lambda"
arn = aws_lambda_function.remediation.arn
}
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.remediation.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.guardduty_high_severity.arn
}
# --- Lambda Remediation Function ---
data "archive_file" "lambda_zip" {
type = "zip"
source_file = "${path.module}/lambda/remediate.py"
output_path = "${path.module}/lambda/remediate.zip"
}
resource "aws_iam_role" "lambda_role" {
name = "guardduty-remediation-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "lambda_policy" {
name = "guardduty-remediation-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"iam:PutUserPolicy"
]
Resource = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/*"
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:${data.aws_caller_identity.current.account_id}:*"
}
]
})
}
resource "aws_lambda_function" "remediation" {
filename = data.archive_file.lambda_zip.output_path
function_name = "guardduty-remediation"
role = aws_iam_role.lambda_role.arn
handler = "remediate.lambda_handler"
runtime = "python3.12"
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
timeout = 30
tags = {
Project = "guardduty-threat-pipeline"
}
}
# --- Outputs ---
output "guardduty_detector_id" {
description = "The ID of the GuardDuty detector"
value = aws_guardduty_detector.main.id
}
output "sns_topic_arn" {
description = "ARN of the security alerts SNS topic"
value = aws_sns_topic.security_alerts.arn
}
output "lambda_function_arn" {
description = "ARN of the remediation Lambda function"
value = aws_lambda_function.remediation.arn
}
output "eventbridge_rule_arn" {
description = "ARN of the high severity EventBridge rule"
value = aws_cloudwatch_event_rule.guardduty_high_severity.arn
}
Secret mission
Add Severity-Based Routing
Your pipeline currently handles all high-severity findings with the same response. Production security teams route different severity levels to different response strategies. Can you upgrade your pipeline with a second tier for medium-severity findings?
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses AWS cloud resources, and some may incur costs after the GuardDuty free trial period ends.
Cost warning
GuardDuty is free for 30 days if this is the first time you enabled it in this region. After the trial, it costs approximately $4/month for a low-traffic personal account.
Lambda, EventBridge, and SNS usage from this project all fall within the AWS Free Tier. If you want to avoid any charges, choose Pause or Delete below.
Resources you used:
- GuardDuty detector with S3 protection enabled.
- EventBridge rule for high-severity GuardDuty findings.
- SNS topic, email subscription, and topic policy.
- Lambda function with IAM role and policy for remediation.
- Lambda permission for EventBridge invocation.
- Local Terraform state files and Lambda zip archive.
Keep everything running
No action needed. Choose this if you want ongoing threat detection in your AWS account or plan to extend this pipeline further.
- Monitor GuardDuty costs by opening the Usage page in the GuardDuty console.
- Your 30-day free trial countdown is visible on the GuardDuty console dashboard.
- After the trial, expect approximately $4/month for a low-traffic personal account.
Pause - I'll come back to this later
Suspend GuardDuty monitoring to stop costs while keeping your Terraform configuration intact. You can re-enable it later with a single apply.
- Open main.tf and change enable = true to enable = false on the aws_guardduty_detector resource.
- Run terraform apply to suspend monitoring.
- This preserves all your Terraform files, state, and resource configuration. To resume, set enable = true and run terraform apply again.
Delete - I don't want to use this again
Remove all AWS resources created by this project. This disables GuardDuty and deletes all findings in the region.
- Run the following command from your project directory to destroy all resources:
terraform destroy
- Type yes when prompted to confirm the destruction.
- Confirm GuardDuty is fully removed by running:
aws guardduty list-detectors
- You should see an empty DetectorIds list, confirming no detectors remain in this region.
- Optionally, delete the local project folder from your Desktop if you no longer need the Terraform files.
Mission accomplished
That's a Wrap!
Nice work! You just built an automated cloud security threat detection and response pipeline from scratch using Terraform.
What you learned:
- Deployed a GuardDuty threat detector with S3 protection and built an event-driven alert pipeline that routes high-severity findings through EventBridge to SNS email notifications.
- Created an automated IAM credential quarantine system using Lambda that attaches a deny-all policy to compromised users in real time.
- Tested the full pipeline end-to-end with sample findings and verified detection, notification, and remediation all work together.
- Added severity-based routing so medium findings go to SNS only, while high and critical findings trigger both alerts and automated Lambda remediation.
Ready to quiz yourself?