LAB46_AWS_Threat Detection: GuardDuty + Security Hub + Automated Response

Threat Detection: GuardDuty + Security Hub + Automated Response

☁ AWS 🔥 Advanced GuardDuty Security Hub EventBridge Lambda Response SNS Alerting

Scenario

Your security team is alerted to threats via email but by the time a human investigates, attackers have already exfiltrated data. You need an automated threat response system: GuardDuty detects threats, Security Hub aggregates findings across services, and an automated Lambda function immediately isolates any EC2 instance flagged with a high-severity finding (by adding it to an isolation security group) while SNS notifies the team.

Your Objectives
  • Enable GuardDuty (aws_guardduty_detector) and AWS Security Hub (aws_securityhub_account) with the AWS Foundational Security Best Practices standard enabled.
  • Create a Lambda function that, when invoked with a GuardDuty finding, extracts the affected EC2 instance ID and modifies its security group to an isolation SG (allowing only SSH from your bastion).
  • Create an EventBridge rule that matches GuardDuty findings with severity >= 7.0 (High) and invokes the Lambda function.
  • Create a second EventBridge target on the same rule that publishes to an SNS topic (for human notification).
  • Create the isolation security group in the VPC (allows inbound 22 only from a bastion CIDR).
  • Output the GuardDuty detector ID, Security Hub ARN, isolation SG ID, and SNS topic ARN.

Additional Context

GuardDuty findings have a severity score from 0.1 to 8.9. The AWS definition: Low (0.1–3.9), Medium (4.0–6.9), High (7.0–8.9). The EventBridge event pattern must filter on detail.severity using a numeric comparison condition. EventBridge numeric filtering uses numeric conditions like [">=", 7].

The EC2 isolation pattern is a common incident response technique: instead of terminating the instance (which destroys forensic evidence), you replace its security group with one that blocks all inbound/outbound except for a forensics bastion. This allows your team to still connect and collect memory dumps while preventing further lateral movement.

variables.tf

variable "aws_region"      { type = string; default = "us-east-1" }
variable "vpc_id"          { type = string; description = "VPC where workloads run." }
variable "bastion_cidr"    { type = string; default = "10.0.5.0/32"; description = "Bastion CIDR for forensic SSH access." }
variable "alert_email"     { type = string; description = "Security team email for GuardDuty alerts." }
variable "environment"     { type = string; default = "prod" }

main.tf

provider "aws" { region = var.aws_region }
locals { tags = { Environment = var.environment, ManagedBy = "terraform" } }

# Enable GuardDuty.
resource "aws_guardduty_detector" "main" {
  enable = true

  datasources {
    s3_logs            { enable = true }
    kubernetes { audit_logs { enable = true } }
    malware_protection { scan_ec2_instance_with_findings { ebs_volumes { enable = true } } }
  }

  tags = local.tags
}

# Enable Security Hub.
resource "aws_securityhub_account" "main" {
  enable_default_standards = false  # we'll enable specific standards below
  auto_enable_controls     = true
}

# Enable AWS Foundational Security Best Practices standard.
resource "aws_securityhub_standards_subscription" "fsbp" {
  standards_arn = "arn:aws:securityhub:${var.aws_region}::standards/aws-foundational-security-best-practices/v/1.0.0"
  depends_on    = [aws_securityhub_account.main]
}

# Isolation security group — only allows SSH from forensics bastion.
resource "aws_security_group" "isolation" {
  name        = "ec2-isolation-sg-${var.environment}"
  description = "Applied to EC2s flagged by GuardDuty — blocks all traffic except bastion SSH"
  vpc_id      = var.vpc_id

  ingress { description = "Forensic bastion only"; from_port = 22; to_port = 22; protocol = "tcp"; cidr_blocks = [var.bastion_cidr] }
  # No egress — block all outbound to prevent further exfiltration.

  tags = merge(local.tags, { Name = "isolation-sg", Purpose = "incident-response" })
}

# SNS topic for human alerts.
resource "aws_sns_topic" "security_alerts" {
  name = "guardduty-high-severity-${var.environment}"
  tags = local.tags
}
resource "aws_sns_topic_subscription" "email" {
  topic_arn = aws_sns_topic.security_alerts.arn
  protocol  = "email"
  endpoint  = var.alert_email
}

# Lambda IAM role — needs ec2:ModifyInstanceAttribute to change the SG.
data "aws_iam_policy_document" "lambda_assume" {
  statement { effect = "Allow"; actions = ["sts:AssumeRole"]
    principals { type = "Service"; identifiers = ["lambda.amazonaws.com"] } }
}
resource "aws_iam_role" "response_lambda" {
  name               = "guardduty-response-role-${var.environment}"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}
resource "aws_iam_role_policy" "response_lambda" {
  name = "ec2-isolate-permissions"
  role = aws_iam_role.response_lambda.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      { Effect = "Allow"; Action = ["ec2:ModifyInstanceAttribute","ec2:DescribeInstances","ec2:DescribeSecurityGroups"]; Resource = "*" },
      { Effect = "Allow"; Action = ["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"]; Resource = "*" }
    ]
  })
}

resource "aws_cloudwatch_log_group" "response" {
  name              = "/aws/lambda/guardduty-response-${var.environment}"
  retention_in_days = 90  # keep security logs longer for forensics
}

resource "aws_lambda_function" "response" {
  function_name = "guardduty-response-${var.environment}"
  runtime       = "python3.12"
  handler       = "response.lambda_handler"
  role          = aws_iam_role.response_lambda.arn
  filename      = "${path.module}/response.zip"
  timeout       = 30

  environment { variables = {
    ISOLATION_SG_ID = aws_security_group.isolation.id
    ENVIRONMENT     = var.environment
  }}

  depends_on = [aws_cloudwatch_log_group.response]
  tags       = local.tags
}

# EventBridge rule — matches GuardDuty High severity findings (>= 7.0).
resource "aws_cloudwatch_event_rule" "high_severity" {
  name        = "guardduty-high-severity-${var.environment}"
  description = "Route GuardDuty findings with severity >= 7 for auto-response"
  event_pattern = jsonencode({
    source      = ["aws.guardduty"]
    detail-type = ["GuardDuty Finding"]
    detail      = { severity = [{ numeric = [">=", 7] }] }
  })
  tags = local.tags
}

# Target 1: Lambda isolation function.
resource "aws_cloudwatch_event_target" "isolate" {
  rule      = aws_cloudwatch_event_rule.high_severity.name
  target_id = "isolate-ec2"
  arn       = aws_lambda_function.response.arn
}
resource "aws_lambda_permission" "eventbridge" {
  statement_id  = "AllowEventBridgeInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.response.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.high_severity.arn
}

# Target 2: SNS for human notification.
resource "aws_sns_topic_policy" "alerts" {
  arn = aws_sns_topic.security_alerts.arn
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{ Effect = "Allow"; Principal = { Service = "events.amazonaws.com" }
      Action = "sns:Publish"; Resource = aws_sns_topic.security_alerts.arn }]
  })
}
resource "aws_cloudwatch_event_target" "notify" {
  rule      = aws_cloudwatch_event_rule.high_severity.name
  target_id = "notify-security-team"
  arn       = aws_sns_topic.security_alerts.arn
}

outputs.tf

output "guardduty_detector_id" { value = aws_guardduty_detector.main.id }
output "isolation_sg_id"       { value = aws_security_group.isolation.id }
output "sns_topic_arn"         { value = aws_sns_topic.security_alerts.arn }
output "response_lambda_arn"   { value = aws_lambda_function.response.arn }

Workflow Commands

terraform init
terraform apply -var="vpc_id=vpc-xxxx" -var="[email protected]"

# Generate a sample finding for testing
aws guardduty create-sample-findings \
  --detector-id $(terraform output -raw guardduty_detector_id) \
  --finding-types UnauthorizedAccess:EC2/SSHBruteForce

# List high-severity findings
aws guardduty list-findings \
  --detector-id $(terraform output -raw guardduty_detector_id) \
  --finding-criteria '{"Criterion":{"severity":{"Gte":70}}}'
# Note: GuardDuty severity is stored as integer×10 in the API (7.0 = 70)

# Check Security Hub findings
aws securityhub get-findings \
  --filters '{"SeverityLabel":[{"Value":"HIGH","Comparison":"EQUALS"}]}' \
  --max-results 5

terraform destroy -var="vpc_id=..." -var="alert_email=..."
✓ Expected Output After Apply
Apply complete! Resources: 13 added, 0 changed, 0 destroyed.

Outputs:

guardduty_detector_id = "abc1234567890abc1234567890abc123"
isolation_sg_id       = "sg-0xxxxxx..."
response_lambda_arn   = "arn:aws:lambda:us-east-1:123456789012:function:guardduty-response-prod"
sns_topic_arn         = "arn:aws:sns:us-east-1:123456789012:guardduty-high-severity-prod"