SCS Lab 02 - Auto-quarantine an instance on a GuardDuty finding

Scaffold: 3/5 (half-built). The Lambda, its execution role, the EventBridge rule, and the permission that lets events invoke the function are all wired. You write the two things that decide whether the right finding reaches the right code: the rule’s event pattern and the handler’s isolation body.

The scenario

GuardDuty watches your account and emits a finding when it spots trouble on an EC2 instance: a crypto-miner beaconing out, a call from a known-bad IP, an instance reaching for credentials it should not. The finding is an EventBridge event: source aws.guardduty, detail-type GuardDuty Finding, with the offending instance id buried in the finding detail. You want an EC2 instance finding, and only that, to trigger a Lambda that quarantines the instance by moving it into an isolation security group (and tagging it) so it can neither talk out nor be reached.

The tempting shortcut is a broad rule (“anything from aws.guardduty”) with the filtering done in the Lambda. GuardDuty also raises findings for S3 buckets, IAM principals, and access keys, none of which have an instance to isolate. A broad rule fires the function on every one of them, pays for every invocation, and puts the routing decision in code where it is harder to see. The version that earns its place is a rule whose pattern matches the exact kind of finding, so EventBridge drops everything else before the function is ever invoked.

What’s provided

Your task

Gap one, src/event-pattern.json. Narrow the pattern so it matches only an EC2 instance finding: source aws.guardduty, detail-type GuardDuty Finding, and detail.resource.resourceType equal to Instance. EventBridge patterns match a value when the event’s field appears in the list you give, and content filters (like numeric) let you match a range. You can also require the finding to clear a severity threshold:

{
  "source": ["aws.guardduty"],
  "detail-type": ["GuardDuty Finding"],
  "detail": {
    "resource": {
      "resourceType": ["Instance"]
    },
    "severity": [{ "numeric": [">=", 4] }]
  }
}

Gap two, src/handler.py. Fill in handler(): read the instance id from event["detail"]["resource"]["instanceDetails"]["instanceId"], log the isolation (log it before you touch EC2, so it is recorded even if the calls fail), then, if QUARANTINE_SG_ID is set, call ec2.modify_instance_attribute to replace the instance’s security groups with [QUARANTINE_SG_ID]. Wrap the EC2 calls in try/except ClientError so the fake instance id the test sends does not crash the invocation.

Run it

# Defaults: stack scs-lab-02, region ap-southeast-2.
./scripts/deploy.sh          # deploys with your event-pattern.json, uploads your handler.py
./scripts/test.sh            # puts two findings and reads the function's logs
./scripts/teardown.sh        # deletes the stack and the Lambda log group

Before your first lab, do the one-time setup: run the zip’s preflight.sh, then deploy the lab reaper so a forgotten stack cannot bill you. Every deploy here tags its stack for the reaper.

What success looks like

./scripts/test.sh puts two GuardDuty findings on the bus, an EC2 instance finding and an S3-bucket finding, waits, and reads the function’s CloudWatch logs:

Handler isolated i-0lab02quarantine1: true   (expected true)
Handler saw ignored i-0lab02ignored00002:  false  (expected false)

PASS: the rule routed the EC2 instance finding to the Lambda, the handler
isolated it, and the S3-bucket finding was filtered out.

With the starter pattern the handler also acts on the S3 finding, so the test reports it saw i-0lab02ignored00002 and tells you to narrow the pattern. With the skeleton handler nothing is isolated at all, and the test tells you to fill in handler.py.

Reveal the solution

Deploy both reference answers without editing anything:

SRC=solution ./scripts/deploy.sh && ./scripts/test.sh

What you just learned

Next

The rest of the SCS Pro lab track is listed in labs/README-scs.md.