Build an Automated SOC on AWS

Detect threats and auto-quarantine compromised EC2 instances with GuardDuty.

Introduction

30 Second Summary

When a hacker breaks into a company's server, every second of delay in responding gives them more time to steal data or spread to other systems. Most organizations still rely on humans to notice threats and react manually. That gap between detection and response is where the real damage happens.

In this project, you will build an automated Security Operations Center on AWS that detects threats on an EC2 instance and quarantines it in seconds, with zero human intervention.

What You'll Build

A fully automated threat detection and incident response pipeline using five AWS security services working together to isolate compromised instances the moment suspicious activity is detected.

By the end of this project, you'll have:

  • A vulnerable web application (DVWA) running on EC2 as a realistic attack target, with GuardDuty and Security Hub monitoring it for threats.
  • An event-driven automation pipeline where EventBridge triggers a Lambda function that automatically quarantines compromised instances by swapping their security groups.
  • A tested, end-to-end response chain with SNS email alerts confirming every automated action taken.
  • Secret Mission: Add forensic evidence preservation by automatically snapshotting EBS volumes before quarantine.

Are there any prerequisites?

You'll need an AWS account with free tier eligibility and basic experience navigating the AWS console or CLI. This project uses services that offer 30-day free trials (GuardDuty and Security Hub) and free-tier-eligible resources (EC2 t2.micro, Lambda, SNS).

Before We Start

Security incidents can escalate from a minor alert to a full breach in minutes. The faster your response, the less damage an attacker can do.

In this project, you'll build an automated Security Operations Center on AWS that detects threats and quarantines compromised EC2 instances in seconds. Before we dive in, let's make sure your AWS CLI is ready to go.

In this step, get ready to:
  • Verify your AWS CLI installation.
  • Set your working region to us-east-1.
  • Confirm your AWS account identity.
Verify your AWS CLI installation

Every resource you create in this project will be provisioned through the AWS CLI. Let's confirm it's installed and working.

  • Open your terminal by pressing 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.
  • Check your AWS CLI version by running this command:
aws --version

✔️ I see a version number

You should see output that starts with something like aws-cli/2.x.x. Your AWS CLI is ready. Move on to the next substep.

ⓧ Command not found

If your terminal doesn't recognize the aws command, you need to install the AWS CLI first.

  • Follow the official AWS CLI installation guide for your operating system.
  • After installation, close and reopen your terminal.
  • Run aws --version again to confirm it works.

Still not working after install?

Make sure you fully closed your terminal window and opened a fresh one. Some systems need this for the new path to take effect.

Help me troubleshoot my AWS CLI installation

What is the AWS CLI?

The AWS CLI is a command-line tool that lets you create and manage AWS resources directly from your terminal. Instead of clicking through the AWS console, you can script and automate everything.

Set your working region

AWS services run in specific geographic regions. You need to pick one region and stick with it for the whole project so all your resources can talk to each other.

  • Set your default region to us-east-1 by running this command:
aws configure set region us-east-1

Why us-east-1?

The us-east-1 region (N. Virginia) has the broadest service availability across AWS. All five services in this project are fully supported there.

Verify your AWS account identity

Before creating any resources, let's confirm which AWS account and user you're signed in as. This prevents surprises later.

  • Verify your identity by running this command:
aws sts get-caller-identity

You should see a JSON response containing your Account number, UserId, and Arn.

What does this command do?

The aws sts get-caller-identity command asks AWS "who am I?" It confirms your CLI credentials are working and shows which account you're operating in.

Take note of the 12-digit Account number in the output. You'll reference it later in the project.

Seeing an error about credentials?

If you see an error mentioning expired credentials or inability to locate credentials, your AWS CLI is not configured with valid access keys. Run aws configure and provide your Access Key ID and Secret Access Key when prompted.

Help me configure my AWS CLI credentials

Your environment is locked and loaded. Next up, you'll deploy a vulnerable web application on EC2 that will serve as the "attack target" for your automated SOC to protect.

Deploy Your Vulnerable Target

Your automated SOC needs something to protect. In the real world, attackers target web applications running on servers.

In this step, you will deploy a deliberately vulnerable web application called DVWA (Damn Vulnerable Web Application) on an EC2 instance. This gives your security pipeline a realistic target to monitor and protect.

In this step, get ready to:
  • Launch an EC2 instance with a security group that allows web and SSH traffic.
  • Install Docker and run DVWA inside a container.
  • Verify DVWA is accessible in your browser.
Launch an EC2 instance with a security group

Your EC2 instance needs a security group that allows inbound SSH (so you can connect to it) and inbound HTTP (so the web application is reachable). You will create the security group first, then launch the instance.

  • Create a security group for your DVWA instance by running this command:
aws ec2 create-security-group \
  --group-name dvwa-sg \
  --description "Security group for DVWA instance - allows SSH and HTTP" \
  --query 'GroupId' \
  --output text

What does this command do?

This creates a new security group named dvwa-sg in your default VPC. The --query 'GroupId' flag extracts just the security group ID from the response so you can use it in the next commands.

  • Copy the security group ID that appears in your terminal (it looks like sg-0abc123def456). You will need it for the next commands.
  • Add an inbound rule allowing SSH traffic by running this command:
aws ec2 authorize-security-group-ingress \
  --group-id [[SG_ID="your security group ID from above"]] \
  --protocol tcp \
  --port 22 \
  --cidr 0.0.0.0/0
  • Add an inbound rule allowing HTTP traffic by running this command:
aws ec2 authorize-security-group-ingress \
  --group-id [[SG_ID="your security group ID"]] \
  --protocol tcp \
  --port 80 \
  --cidr 0.0.0.0/0

Why open to 0.0.0.0/0?

Opening ports to 0.0.0.0/0 means any IP address can reach those ports. In production, you would restrict SSH to your own IP address.

For this project, the broad access makes testing easier and demonstrates what happens when a server is exposed to the internet.

Now you need an SSH key pair to connect to the instance.

  • Create a key pair and save the private key file by running this command:
aws ec2 create-key-pair \
  --key-name dvwa-key \
  --query 'KeyMaterial' \
  --output text > dvwa-key.pem
  • Set the correct permissions on the key file by running:
chmod 400 dvwa-key.pem

Why chmod 400?

SSH refuses to use a private key file if other users on the system can read it. The chmod 400 command sets the file to read-only for the owner, which is the minimum permission SSH requires.

With the security group and key pair ready, you can now launch the instance.

  • Launch a t2.micro EC2 instance running Amazon Linux 2023 by running this command:
aws ec2 run-instances \
  --image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
  --instance-type t2.micro \
  --key-name dvwa-key \
  --security-group-ids [[SG_ID="your security group ID"]] \
  --associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=DVWA-Target}]' \
  --query 'Instances[0].InstanceId' \
  --output text

What does this command do?

The resolve:ssm: prefix automatically looks up the latest Amazon Linux 2023 AMI ID from AWS Systems Manager Parameter Store. This means you always get a current image without hunting for a specific AMI ID.

The --associate-public-ip-address flag ensures you can reach the instance from the internet. The --tag-specifications flag names the instance DVWA-Target so you can identify it in the console.

  • Copy the instance ID from the output (it looks like i-0abc123def456).
  • Wait for the instance to reach a running state by running:
aws ec2 wait instance-running \
  --instance-ids [[INSTANCE_ID="your instance ID"]]
  • Retrieve the instance's public IP address by running:
aws ec2 describe-instances \
  --instance-ids [[INSTANCE_ID="your instance ID"]] \
  --query 'Reservations[0].Instances[0].PublicIpAddress' \
  --output text
  • Copy the public IP address from the output. You will use this to SSH into the instance and to access DVWA in your browser.

Not seeing a public IP?

If the output shows None instead of an IP address, your default VPC subnet may not auto-assign public IPs. Re-launch the instance with --associate-public-ip-address included (already in the command above). If you already ran the command, wait 30 seconds and try the describe-instances command again.

Help me troubleshoot this

SSH in, install Docker, and run DVWA

Now that your instance is running, you will connect to it, install Docker, and launch the DVWA container. Docker lets you run a complete web application with a single command instead of manually installing PHP, MySQL, and Apache.

  • SSH into your EC2 instance by running this command:
ssh -i dvwa-key.pem ec2-user@[[INSTANCE_IP="your instance public IP address"]]

What should I see?

The first time you connect, SSH will ask you to confirm the host fingerprint. Type yes and press Enter. You should see the Amazon Linux 2023 welcome banner and a command prompt ending in $.

  • Install Docker on the instance by running these commands:
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user

What do these commands do?

  • sudo yum install -y docker installs the Docker engine using the package manager.
  • sudo service docker start starts the Docker daemon so you can run containers.
  • sudo usermod -a -G docker ec2-user adds your user to the docker group so you can run docker commands without sudo.
  • Apply the new group permissions by running:
newgrp docker
  • Verify Docker is working by running:
docker ps

You should see an empty table with column headers (CONTAINER ID, IMAGE, etc.). This confirms Docker is running.

Getting a permission denied error?

If docker ps gives a permission error after running newgrp docker, try disconnecting from the SSH session and reconnecting. The group change sometimes requires a fresh login to take effect.

Help me fix this Docker permission issue

Docker is ready. Time to pull and run the DVWA container.

  • Launch the DVWA container by running:
docker run -d -p 80:80 vulnerables/web-dvwa

What does this command do?

  • -d runs the container in detached mode (in the background).
  • -p 80:80 maps port 80 on the EC2 instance to port 80 inside the container, making the web app accessible.
  • vulnerables/web-dvwa is the official DVWA Docker image containing a PHP/MySQL web application with intentional security vulnerabilities.
  • Verify the container is running by running:
docker ps

You should see one container listed with the image vulnerables/web-dvwa and status Up.

Container not showing as Up?

If the container status shows Exited, port 80 may already be in use. Run sudo lsof -i :80 to check. If something else is listening, stop it and re-run the docker command.

Help me troubleshoot my DVWA container

Verify DVWA is accessible in your browser

The final check is confirming you can reach DVWA from your local machine. This proves the security group, EC2 networking, and Docker container are all working together.

  • Open your web browser.
  • Navigate to http:// followed by your instance's public IP address (the one you copied earlier).
  • You should see the DVWA login page with a red banner and username/password fields.

What is DVWA?

DVWA (Damn Vulnerable Web Application) is a PHP/MySQL application intentionally full of security holes. Security professionals use it to practice penetration testing techniques.

In this project, it serves as a realistic target that attackers might exploit. When your automated SOC detects suspicious activity targeting this instance, it will quarantine it automatically. The DVWA page going down is the visible proof that quarantine worked.

Can't reach the DVWA page?

Make sure you are using http:// (not https://). DVWA does not support HTTPS.

If the page still does not load, verify your security group has port 80 open by running aws ec2 describe-security-groups --group-ids followed by your security group ID. Look for port 80 in the IpPermissions list.

Help me troubleshoot my DVWA connection

Your vulnerable target is live and ready to be monitored. Next up, you will enable the AWS security services that detect threats against this instance.

Enable Threat Detection

Your vulnerable target is live and accessible. Now you need the security layer that watches over it.

In a real Security Operations Center, threat detection runs continuously in the background. You will enable two AWS services that work together to detect and centralize security findings across your account.

In this step, get ready to:
  • Enable Amazon GuardDuty for continuous threat detection.
  • Enable AWS Security Hub as a centralized findings dashboard.
  • Verify both services are active and connected.
Enable Amazon GuardDuty

Amazon GuardDuty is a threat detection service that continuously monitors your AWS account for malicious activity. It analyzes VPC flow logs, DNS logs, and CloudTrail events to spot suspicious behavior on your resources.

  • Enable GuardDuty in your region by running this command:
aws guardduty create-detector --enable

What does this command do?

The create-detector command creates a GuardDuty detector resource in your current region. A detector represents the GuardDuty service itself.

The --enable flag tells AWS to immediately start monitoring. All data sources (VPC flow logs, DNS logs, CloudTrail) are enabled by default.

You should see a JSON response containing a DetectorId value. This confirms GuardDuty is now active.

What about the free trial?

GuardDuty includes a 30-day free trial per AWS account per region. You get the full feature set during this trial with no charges.

  • Copy and save your DetectorId value somewhere accessible. You will need it in a later step.

Seeing a detector already exists error?

You can only have one detector per account per region. If GuardDuty was previously enabled, retrieve your existing detector ID by running:

aws guardduty list-detectors

If the output shows a detector ID, GuardDuty is already active and you can move on.

Help me fix a GuardDuty detector error

Enable AWS Security Hub

AWS Security Hub provides a centralized dashboard where findings from GuardDuty and other security services are aggregated. It gives you a single view of your security posture across your AWS account.

  • Enable Security Hub by running this command:
aws securityhub enable-security-hub

What does this command do?

This enables Security Hub in your current region. It automatically activates default security standards (AWS Foundational Security Best Practices and CIS AWS Foundations Benchmark).

Once enabled, Security Hub begins receiving findings from integrated services like GuardDuty. The integration is automatic and requires no additional configuration.

This command produces no output on success. That is normal.

What about the Security Hub free trial?

Security Hub also includes a 30-day free trial with first enablement. You will not be charged during this project.

Seeing an access denied or resource conflict error?

If you see an access denied error, your IAM user or role may not have the required permissions. Make sure your AWS CLI credentials include the securityhub:EnableSecurityHub permission.

If you see a resource conflict error, Security Hub is already enabled in this region. You can move to the verification step below.

Help me troubleshoot Security Hub enablement

Verify both services are active

Now confirm that both GuardDuty and Security Hub are running and connected.

  • Verify GuardDuty is active by running:
aws guardduty list-detectors

You should see a JSON response with a DetectorIds array containing your detector ID.

  • Verify Security Hub is active by running:
aws securityhub describe-hub

You should see a JSON response containing a HubArn and a SubscribedAt timestamp confirming when Security Hub was enabled.

How do GuardDuty findings flow into Security Hub?

The integration is automatic. When both services are enabled in the same region, GuardDuty findings appear in Security Hub within minutes. No manual linking is required.

Security Hub acts as the central aggregation point. In a production environment, you would also route findings from services like Amazon Inspector and AWS Config for a complete security picture.

  • Open the AWS console in your browser and navigate to the GuardDuty service page.
  • Confirm the dashboard shows GuardDuty is enabled with a green status indicator.
  • Navigate to the Security Hub service page.
  • Confirm the Security Hub summary dashboard loads, showing that the service is active.

Security Hub dashboard not loading?

Make sure you are viewing the correct region (us-east-1) in the top-right corner of the console. Services are region-specific.

If Security Hub shows as not enabled, wait a moment and refresh. Enablement can take up to 30 seconds to propagate.

Help me verify my Security Hub is working

Your threat detection layer is now live. GuardDuty is watching your account for suspicious activity, and Security Hub is ready to receive findings. Next, you will build the Lambda function that automatically responds when a threat is detected.

Build the Lambda Responder

You have threat detection running. GuardDuty is watching for suspicious activity, and Security Hub gives you a centralized dashboard.

Detection alone is not enough. Real Security Operations Centers respond to threats in seconds, not hours. In this step, you will build the automated responder that quarantines compromised instances the moment GuardDuty raises an alert.

In this step, get ready to:
  • Create an SNS topic for security alert emails.
  • Create an IAM role that grants Lambda the permissions it needs.
  • Deploy a Lambda function that isolates compromised instances.
Create an SNS topic and subscribe your email

When your Lambda function quarantines an instance, you need to know about it immediately. Amazon SNS (Simple Notification Service) sends email alerts so you are informed the moment an incident occurs.

  • Create an SNS topic named security-alerts by running this command:
aws sns create-topic --name security-alerts

What does this command do?

This creates a messaging channel called a topic. Any service that publishes to this topic will have its messages forwarded to all subscribers. The output includes a TopicArn value that you will need in the next commands.

  • Copy the TopicArn value from the output. It looks like arn:aws:sns:us-east-1:123456789012:security-alerts.
  • Subscribe your email to this topic by running the following command (replace the placeholders with your actual values):
aws sns subscribe \
    --topic-arn [[SNS_TOPIC_ARN="your SNS topic ARN from the previous command"]] \
    --protocol email \
    --notification-endpoint [[YOUR_EMAIL="your email address"]]
  • Check your email inbox for a confirmation message from AWS Notifications.
  • Click the Confirm subscription link in that email.

Don't see the confirmation email?

Check your spam or junk folder. The email comes from AWS Notifications. If you still do not see it after a few minutes, re-run the subscribe command and double check your email address.

Help me troubleshoot my SNS subscription

Create the IAM role for Lambda

Your Lambda function needs permission to manage EC2 security groups and publish to SNS. You will create an IAM role with two policy documents: a trust policy (who can assume this role) and a permissions policy (what the role can do).

  • Create a file called trust-policy.json by running:
cat > trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

What is a trust policy?

A trust policy defines which AWS service is allowed to assume this role. Here, you are telling AWS that only the Lambda service (lambda.amazonaws.com) can use this role and its permissions.

  • Create the permissions policy file called lambda-policy.json by running:
cat > lambda-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeSecurityGroups",
        "ec2:CreateSecurityGroup",
        "ec2:RevokeSecurityGroupEgress",
        "ec2:ModifyInstanceAttribute"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}
EOF

What does this policy allow?

  • The first statement grants EC2 permissions: describing instances, managing security groups, and swapping the security group on a compromised instance.
  • The second statement lets the function publish alert messages to SNS.
  • The third statement allows writing logs to CloudWatch so you can debug the function later.
  • Create the IAM role by running:
aws iam create-role \
    --role-name lambda-guardduty-responder \
    --assume-role-policy-document file://trust-policy.json
  • Copy the Arn value from the output. It looks like arn:aws:iam::123456789012:role/lambda-guardduty-responder.
  • Attach the permissions policy to the role by running:
aws iam put-role-policy \
    --role-name lambda-guardduty-responder \
    --policy-name lambda-guardduty-policy \
    --policy-document file://lambda-policy.json
  • Confirm both policy files exist by running:
ls

You should see trust-policy.json and lambda-policy.json listed in the output.

Seeing an access denied error?

Your AWS CLI user needs iam:CreateRole and iam:PutRolePolicy permissions. If you are using an admin account or a user with the AdministratorAccess policy, these should already be available.

Help me fix IAM permission errors when creating a role

Deploy the Lambda function

Now for the core of your automated SOC. This Lambda function receives a GuardDuty finding event, looks up the compromised instance, creates an isolation security group with zero traffic rules, and swaps it onto the instance. The instance is immediately cut off from all network access.

  • Create the Lambda function code in a file called lambda_function.py by running:
cat > lambda_function.py << 'EOF'
import boto3
import json
import os

ec2 = boto3.client('ec2')
sns = boto3.client('sns')


def lambda_handler(event, context):
    detail = event['detail']
    instance_id = detail['resource']['instanceDetails']['instanceId']
    finding_type = detail['type']
    severity = detail['severity']

    instance_info = ec2.describe_instances(InstanceIds=[instance_id])
    vpc_id = instance_info['Reservations'][0]['Instances'][0]['VpcId']

    isolation_sg_name = 'guardduty-isolation'
    isolation_sg_id = get_or_create_isolation_sg(vpc_id, isolation_sg_name)

    ec2.modify_instance_attribute(
        InstanceId=instance_id,
        Groups=[isolation_sg_id]
    )

    sns_topic_arn = os.environ['SNS_TOPIC_ARN']
    message = (
        f"AUTOMATED INCIDENT RESPONSE TRIGGERED\n\n"
        f"Instance {instance_id} has been QUARANTINED.\n\n"
        f"Finding Type: {finding_type}\n"
        f"Severity: {severity}\n\n"
        f"Action Taken: All security groups replaced with isolation SG ({isolation_sg_id}).\n"
        f"No inbound or outbound traffic is now permitted.\n\n"
        f"Next Steps: Investigate the finding in the GuardDuty console."
    )

    sns.publish(
        TopicArn=sns_topic_arn,
        Subject=f'[SECURITY] Instance {instance_id} Quarantined',
        Message=message
    )

    return {
        'statusCode': 200,
        'body': json.dumps({
            'instance_id': instance_id,
            'action': 'quarantined',
            'isolation_sg': isolation_sg_id
        })
    }


def get_or_create_isolation_sg(vpc_id, sg_name):
    response = ec2.describe_security_groups(
        Filters=[
            {'Name': 'group-name', 'Values': [sg_name]},
            {'Name': 'vpc-id', 'Values': [vpc_id]}
        ]
    )

    if response['SecurityGroups']:
        return response['SecurityGroups'][0]['GroupId']

    create_response = ec2.create_security_group(
        GroupName=sg_name,
        Description='Isolation SG for compromised instances - no traffic allowed',
        VpcId=vpc_id
    )
    isolation_sg_id = create_response['GroupId']

    ec2.revoke_security_group_egress(
        GroupId=isolation_sg_id,
        IpPermissions=[{
            'IpProtocol': '-1',
            'IpRanges': [{'CidrIp': '0.0.0.0/0'}]
        }]
    )

    return isolation_sg_id
EOF

What does this function do?

  • It extracts the compromised instance ID and finding details from the GuardDuty event.
  • It looks up the instance's VPC, then creates (or reuses) an isolation security group with no inbound or outbound rules.
  • It swaps the instance's security groups with the isolation group, cutting off all network traffic.
  • It sends an email alert via SNS with the finding type, severity, and what action was taken.

Why a security group swap instead of termination?

Security teams need the compromised instance preserved for forensic investigation. Terminating it destroys evidence. Swapping to an isolation security group blocks all traffic while keeping the disk and memory intact for analysis.

  • Package the function code into a zip file by running:
zip function.zip lambda_function.py
  • Wait about 10 seconds for the IAM role to propagate through AWS, then deploy the Lambda function by running:
aws lambda create-function \
    --function-name guardduty-responder \
    --runtime python3.12 \
    --handler lambda_function.lambda_handler \
    --role [[ROLE_ARN="your IAM role ARN from the create-role output"]] \
    --zip-file fileb://function.zip \
    --environment "Variables={SNS_TOPIC_ARN=[[SNS_TOPIC_ARN="your SNS topic ARN"]]}"

What do these flags mean?

  • --function-name names the function guardduty-responder.
  • --runtime python3.12 tells Lambda which language runtime to use.
  • --handler lambda_function.lambda_handler points Lambda to the lambda_handler function inside lambda_function.py.
  • --environment passes the SNS topic ARN as an environment variable so the function knows where to send alerts.

You should see a JSON response containing your function's configuration details, including a FunctionArn and State field.

Seeing an InvalidParameterValueException?

This usually means the IAM role has not finished propagating. Wait 10-15 seconds and re-run the command. If the error mentions the role ARN, double check that you pasted the full ARN from the aws iam create-role output.

Help me fix my Lambda create-function error

✔️ Awesome, I've got everything!

Great. Double check you have confirmed your SNS email subscription and that your Lambda function details appeared in the terminal output.

ⓧ I'd like to double check the full code

Here is the complete lambda_function.py for comparison:

import boto3
import json
import os

ec2 = boto3.client('ec2')
sns = boto3.client('sns')


def lambda_handler(event, context):
    detail = event['detail']
    instance_id = detail['resource']['instanceDetails']['instanceId']
    finding_type = detail['type']
    severity = detail['severity']

    instance_info = ec2.describe_instances(InstanceIds=[instance_id])
    vpc_id = instance_info['Reservations'][0]['Instances'][0]['VpcId']

    isolation_sg_name = 'guardduty-isolation'
    isolation_sg_id = get_or_create_isolation_sg(vpc_id, isolation_sg_name)

    ec2.modify_instance_attribute(
        InstanceId=instance_id,
        Groups=[isolation_sg_id]
    )

    sns_topic_arn = os.environ['SNS_TOPIC_ARN']
    message = (
        f"AUTOMATED INCIDENT RESPONSE TRIGGERED\n\n"
        f"Instance {instance_id} has been QUARANTINED.\n\n"
        f"Finding Type: {finding_type}\n"
        f"Severity: {severity}\n\n"
        f"Action Taken: All security groups replaced with isolation SG ({isolation_sg_id}).\n"
        f"No inbound or outbound traffic is now permitted.\n\n"
        f"Next Steps: Investigate the finding in the GuardDuty console."
    )

    sns.publish(
        TopicArn=sns_topic_arn,
        Subject=f'[SECURITY] Instance {instance_id} Quarantined',
        Message=message
    )

    return {
        'statusCode': 200,
        'body': json.dumps({
            'instance_id': instance_id,
            'action': 'quarantined',
            'isolation_sg': isolation_sg_id
        })
    }


def get_or_create_isolation_sg(vpc_id, sg_name):
    response = ec2.describe_security_groups(
        Filters=[
            {'Name': 'group-name', 'Values': [sg_name]},
            {'Name': 'vpc-id', 'Values': [vpc_id]}
        ]
    )

    if response['SecurityGroups']:
        return response['SecurityGroups'][0]['GroupId']

    create_response = ec2.create_security_group(
        GroupName=sg_name,
        Description='Isolation SG for compromised instances - no traffic allowed',
        VpcId=vpc_id
    )
    isolation_sg_id = create_response['GroupId']

    ec2.revoke_security_group_egress(
        GroupId=isolation_sg_id,
        IpPermissions=[{
            'IpProtocol': '-1',
            'IpRanges': [{'CidrIp': '0.0.0.0/0'}]
        }]
    )

    return isolation_sg_id

And here are the policy files:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeSecurityGroups",
        "ec2:CreateSecurityGroup",
        "ec2:RevokeSecurityGroupEgress",
        "ec2:ModifyInstanceAttribute"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}

Your automated responder is deployed and ready. Next up, you will wire it to GuardDuty via EventBridge so it triggers automatically when a threat is detected.

Connect the Automation Trigger

Your Lambda function can quarantine a compromised instance, but right now nothing triggers it automatically.

In this step, you will wire Amazon EventBridge to your Lambda function so that every GuardDuty finding fires the responder without human intervention.

In this step, get ready to:
  • Create an EventBridge rule that matches GuardDuty findings.
  • Add the Lambda function as the rule's target.
  • Grant EventBridge permission to invoke the Lambda function.
Create the EventBridge rule

EventBridge rules listen for events on the default event bus and route matching events to targets. You need a rule that catches every GuardDuty finding event.

  • Create the event pattern file by running this command:
cat > eventbridge-pattern.json << 'EOF'
{
  "source": ["aws.guardduty"],
  "detail-type": ["GuardDuty Finding"]
}
EOF

What does this pattern do?

The source field filters for events emitted by GuardDuty.

The detail-type field narrows it to finding events specifically. Together, these two fields catch every GuardDuty finding regardless of severity or type.

  • Confirm the file was created by running:
cat eventbridge-pattern.json

You should see the JSON pattern with aws.guardduty as the source and GuardDuty Finding as the detail-type.

  • Create the EventBridge rule by running this command:
aws events put-rule \
  --name guardduty-finding-rule \
  --event-pattern file://eventbridge-pattern.json

What does this command do?

  • aws events put-rule creates a new EventBridge rule on the default event bus.
  • --name guardduty-finding-rule gives the rule a descriptive name you can reference later.
  • --event-pattern file://eventbridge-pattern.json tells EventBridge which events to match, loaded from the JSON file you just created.

You should see a JSON response containing a RuleArn value. Copy this ARN. You will need it in the next substep.

Seeing an error about the event pattern?

Make sure your eventbridge-pattern.json file is valid JSON. Check for missing commas or brackets. You can validate it by running cat eventbridge-pattern.json | python3 -m json.tool.

help me debug my EventBridge put-rule command

Add the Lambda function as the rule's target

A rule without a target does nothing. You need to tell EventBridge to send matched events to your guardduty-responder Lambda function.

  • Get the Lambda function ARN by running:
LAMBDA_ARN=$(aws lambda get-function --function-name guardduty-responder --query 'Configuration.FunctionArn' --output text)
echo $LAMBDA_ARN

You should see an ARN that looks like arn:aws:lambda:us-east-1:<account-id>:function:guardduty-responder.

  • Add the Lambda function as the rule's target by running:
aws events put-targets \
  --rule guardduty-finding-rule \
  --targets "Id"="1","Arn"="$LAMBDA_ARN"

What does this command do?

  • aws events put-targets attaches one or more targets to an existing rule.
  • --rule guardduty-finding-rule specifies which rule to attach the target to.
  • --targets defines the target with an ID (1) and the Lambda function's ARN. EventBridge will pass the full GuardDuty finding event to the function.

You should see a response with "FailedEntryCount": 0 confirming the target was added successfully.

Seeing FailedEntryCount greater than 0?

Double check that $LAMBDA_ARN contains a valid ARN by running echo $LAMBDA_ARN. If it is empty, re-run the aws lambda get-function command above.

help me fix my put-targets command for EventBridge

Grant EventBridge permission to invoke the Lambda function

EventBridge uses resource-based policies to invoke Lambda functions. Without an explicit permission grant, EventBridge cannot call your function even though it is configured as a target.

  • Get the rule ARN by running:
RULE_ARN=$(aws events describe-rule --name guardduty-finding-rule --query 'Arn' --output text)
echo $RULE_ARN

You should see an ARN like arn:aws:events:us-east-1:<account-id>:rule/guardduty-finding-rule.

  • Grant EventBridge permission to invoke your Lambda function by running:
aws lambda add-permission \
  --function-name guardduty-responder \
  --statement-id EventBridgeInvoke \
  --action lambda:InvokeFunction \
  --principal events.amazonaws.com \
  --source-arn $RULE_ARN

What does this command do?

  • aws lambda add-permission adds a statement to the function's resource-based policy.
  • --principal events.amazonaws.com allows the EventBridge service to invoke the function.
  • --source-arn $RULE_ARN restricts the permission so that only your specific rule can trigger the function. Without this, any EventBridge rule in your account could invoke it.

You should see a JSON response showing the new policy statement with events.amazonaws.com as the principal.

Seeing a ResourceConflictException?

This means a statement with the same --statement-id already exists. If you are re-running the command after a previous attempt, this is safe to ignore. The permission is already in place.

help me fix my Lambda add-permission error

  • Verify the full automation chain is wired up by running:
aws events list-targets-by-rule --rule guardduty-finding-rule

You should see your Lambda function ARN listed as a target with "Id": "1".

Your automated pipeline is now fully wired. Next up, you will test the entire system end-to-end to prove that a GuardDuty finding triggers automatic quarantine.

Test Your Automated SOC

Your entire automated pipeline is now wired together. GuardDuty feeds findings to EventBridge, which triggers your Lambda function to quarantine the compromised instance and alert you via email.

But how do you know it actually works? In this step, you will test the full chain in two ways. First, by manually invoking the Lambda with a realistic test event pointing at your real EC2 instance. Second, by generating sample GuardDuty findings to prove the EventBridge trigger fires automatically.

In this step, get ready to:
  • Invoke your Lambda function manually and confirm your EC2 instance gets quarantined.
  • Verify you receive an SNS email alert about the quarantine.
  • Generate sample GuardDuty findings and confirm the EventBridge-to-Lambda trigger works via CloudWatch Logs.
Create a test event file with your instance ID

Your Lambda function expects an event that looks exactly like what EventBridge forwards from GuardDuty. You will create a JSON file that mimics a real GuardDuty finding, but targets your actual EC2 instance.

  • Get your EC2 instance ID by running this command:
aws ec2 describe-instances \
  --filters "Name=instance-state-name,Values=running" \
  --query "Reservations[*].Instances[*].InstanceId" \
  --output text

What does this command do?

This queries your running EC2 instances and returns only the instance ID. If you have multiple running instances, identify the one running DVWA (the t2.micro you launched in Step 2).

  • Create a file called test-event.json by running this command:
cat > test-event.json << 'EOF'
{
  "version": "0",
  "id": "test-event-001",
  "detail-type": "GuardDuty Finding",
  "source": "aws.guardduty",
  "account": "123456789012",
  "time": "2026-06-03T00:00:00Z",
  "region": "us-east-1",
  "resources": [],
  "detail": {
    "type": "UnauthorizedAccess:EC2/MaliciousIPCaller.Custom",
    "severity": 8,
    "resource": {
      "resourceType": "Instance",
      "instanceDetails": {
        "instanceId": "your-instance-id-here"
      }
    }
  }
}
EOF
  • Open test-event.json and replace your-instance-id-here with the actual instance ID you retrieved in the previous command.

What is this test event?

This JSON mimics the exact structure that EventBridge sends when GuardDuty detects a finding. The detail-type field is GuardDuty Finding, and the detail object contains the finding type, severity, and the target instance ID.

By pointing it at your real instance, you will see the quarantine happen live.

Not sure which instance ID to use?

Your instance ID looks like i-0abc1234def56789. If you see multiple IDs, open the EC2 console in your browser and find the one tagged or launched most recently for this project.

Help me find my EC2 instance ID for the test event.

Invoke the Lambda function and confirm quarantine

Now you will send the test event directly to your Lambda function. This simulates what happens when EventBridge forwards a real GuardDuty finding.

  • Before you invoke, confirm DVWA is still accessible. Open your browser and navigate to your EC2 instance's public IP on port 80.
  • You should see the DVWA login page. This proves the instance is reachable before quarantine.
  • Invoke the Lambda function with your test event by running this command:
aws lambda invoke \
  --function-name guardduty-responder \
  --cli-binary-format raw-in-base64-out \
  --payload file://test-event.json \
  response.json

What does this command do?

This invokes your guardduty-responder Lambda function synchronously with the test event as its input payload. The --cli-binary-format raw-in-base64-out flag ensures the CLI reads the JSON file correctly in AWS CLI version 2.

The function's return value is written to response.json.

  • You should see output similar to:
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
  • Check the Lambda response by running:
cat response.json
  • You should see a JSON object confirming the instance was quarantined, including the isolation security group ID.

Now verify the quarantine actually worked.

  • Go back to your browser and refresh the DVWA page at your instance's public IP.
  • The page should fail to load. This confirms the isolation security group replaced your original security group, blocking all inbound traffic.
  • Check your email inbox (the address you subscribed to the SNS topic).
  • You should have received an email with subject line containing [SECURITY] Instance and details about the quarantine action taken.

Why does DVWA become unreachable?

Your Lambda function replaced the instance's security group with the guardduty-isolation group. This isolation group has no inbound rules and its default egress rule was revoked. No traffic can reach the instance from the internet.

Existing TCP connections may linger briefly, but no new connections are permitted.

DVWA still loading after invoke?

Check response.json for errors. If you see a FunctionError field in the CLI output, the Lambda encountered an issue. Common causes:

  • Wrong instance ID in test-event.json. Double-check it matches your running instance.
  • IAM permissions missing. Verify your Lambda role has the ec2:ModifyInstanceAttribute permission.
  • Instance in a different region. Confirm your CLI region matches where your instance is running.

My Lambda invocation returned an error when trying to quarantine my EC2 instance.

Generate sample findings to test the EventBridge trigger

The manual invoke proved your Lambda function works. Now you need to prove the full automated chain works: GuardDuty finding triggers EventBridge, which invokes Lambda automatically.

  • Get your GuardDuty detector ID by running:
aws guardduty list-detectors
  • You should see output containing a DetectorIds array with one ID string. Copy this ID.
  • Generate sample findings by running this command (replace the detector ID with yours):
aws guardduty create-sample-findings \
  --detector-id [[DETECTOR_ID="your GuardDuty detector ID"]] \
  --finding-types UnauthorizedAccess:EC2/TorClient UnauthorizedAccess:EC2/TorRelay

What does this command do?

This tells GuardDuty to generate fake sample findings. These findings have placeholder instance IDs (not your real instance), but they still flow through EventBridge and trigger your Lambda function.

The command produces no output on success. The findings appear in your GuardDuty console marked with the [SAMPLE] prefix.

Why will Lambda fail on sample findings?

Sample findings contain fictitious instance IDs that do not exist in your account. Your Lambda will be invoked by EventBridge (proving the trigger works), but will fail when it tries to describe the non-existent instance. This is expected and visible in CloudWatch Logs.

The manual test you did earlier already proved the quarantine logic works on a real instance. This test proves the automated trigger chain works.

  • Wait about 1-2 minutes for the sample findings to propagate through EventBridge.
  • Check your Lambda's CloudWatch Logs to confirm it was invoked by running:
aws logs filter-log-events \
  --log-group-name /aws/lambda/guardduty-responder \
  --no-paginate
  • You should see log entries showing your Lambda was invoked. Look for START and END entries, plus any error messages about describing a non-existent instance. These errors confirm EventBridge triggered Lambda successfully.

No log entries appearing?

  • Wait a bit longer. It can take 2-5 minutes for sample findings to propagate through EventBridge to Lambda and for logs to appear.
  • Verify the EventBridge rule is enabled. Check in the EventBridge console that guardduty-finding-rule shows a status of Enabled.
  • Confirm the EventBridge permission exists. Lambda needs a resource-based policy allowing EventBridge to invoke it.

My CloudWatch Logs are empty after generating sample GuardDuty findings.

Your automated SOC is fully operational. You have proven that the quarantine logic works on a real instance and that the detection-to-response chain fires automatically when GuardDuty raises a finding.

Secret mission

Add Forensic Evidence Preservation

Real incident responders don't just quarantine a compromised instance. They need disk images to analyze malware and understand the full scope of an attack. Can you modify your Lambda function to automatically snapshot the EBS volume before isolation, preserving forensic evidence for investigation?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. GuardDuty and Security Hub are on 30-day free trials, and the EC2 instance uses free tier hours.

Cost warning

GuardDuty and Security Hub will start charging after their 30-day free trials end. If you do not plan to use them beyond this project, disable both services to avoid unexpected charges.

Resources you used:

  • EC2 instance (t2.micro, Amazon Linux 2023).
  • Amazon GuardDuty (30-day free trial).
  • AWS Security Hub (30-day free trial).
  • Lambda function (guardduty-responder).
  • SNS topic (security-alerts).
  • EventBridge rule (guardduty-finding-rule).
  • IAM role (lambda-guardduty-responder).
  • Isolation security group (guardduty-isolation).
  • EBS forensic snapshot.
  • CloudWatch Log Group (/aws/lambda/guardduty-responder).

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your automated SOC.

  • Your EC2 instance continues using free tier compute hours.
  • GuardDuty and Security Hub remain active under their 30-day free trials.
  • Lambda, EventBridge, and SNS stay within free tier limits for low usage.

Pause - I'll come back to this later

Stop the EC2 instance to avoid compute charges while keeping your automation pipeline intact. You can restart the instance later to resume testing.

  • Stop your EC2 instance by running:
aws ec2 stop-instances --instance-ids [[EC2_INSTANCE_ID="your EC2 instance ID"]]
  • Your Lambda function, EventBridge rule, and SNS topic remain configured at no cost while idle.
  • GuardDuty and Security Hub continue their free trial countdown even while paused.

EBS storage charges continue while the instance is stopped, but the cost is minimal (a few cents per month for an 8 GB volume).

Delete - I don't want to use this again

Remove all project resources to avoid any future charges. Run these commands in order from your terminal.

  • Terminate the EC2 instance:
aws ec2 terminate-instances --instance-ids [[EC2_INSTANCE_ID="your EC2 instance ID"]]
  • Delete the forensic EBS snapshot:
aws ec2 delete-snapshot --snapshot-id [[SNAPSHOT_ID="your forensic snapshot ID"]]
  • Delete the isolation security group (wait a minute after terminating the instance for the network interface to detach):
aws ec2 delete-security-group --group-id [[ISOLATION_SG_ID="your guardduty-isolation security group ID"]]
  • Find the EventBridge rule target ID, then remove it and delete the rule:
aws events list-targets-by-rule --rule guardduty-finding-rule
aws events remove-targets --rule guardduty-finding-rule --ids [[TARGET_ID="your target ID from the output above"]]
aws events delete-rule --name guardduty-finding-rule
  • Delete the Lambda function:
aws lambda delete-function --function-name guardduty-responder
  • Delete the SNS topic:
aws sns delete-topic --topic-arn [[SNS_TOPIC_ARN="your security-alerts topic ARN"]]
  • Delete the IAM role (remove the inline policy first, then delete the role):
aws iam delete-role-policy --role-name lambda-guardduty-responder --policy-name lambda-guardduty-policy
aws iam delete-role --role-name lambda-guardduty-responder
  • Delete the CloudWatch Log Group:
aws logs delete-log-group --log-group-name /aws/lambda/guardduty-responder
  • Disable GuardDuty by deleting the detector. First get your detector ID, then delete it:
aws guardduty list-detectors
aws guardduty delete-detector --detector-id [[DETECTOR_ID="your GuardDuty detector ID"]]
  • Disable Security Hub:
aws securityhub disable-security-hub
  • Delete your local project files:
rm lambda_function.py test-event.json trust-policy.json lambda-policy.json eventbridge-pattern.json

Once all commands complete, your AWS account is back to its pre-project state with no ongoing charges from this project.

Nice Work!

Nice Work!

You just built a fully automated Security Operations Center on AWS. Your system detects threats with GuardDuty and quarantines compromised instances in seconds, without any human intervention.

You've learned how to:

  • Deploy a vulnerable web application (DVWA) on EC2 and enable centralized threat detection with Amazon GuardDuty and AWS Security Hub.
  • Build an automated incident response pipeline using Amazon EventBridge, AWS Lambda, and security group isolation to quarantine compromised instances.
  • Test the full detection-to-response chain end-to-end with automated instance quarantine and SNS email alerting.
  • Add forensic EBS snapshot preservation to capture disk evidence before quarantine, just like real incident response teams.

Ready to quiz yourself?