Which GuardDuty Findings Actually Matter, and How to Auto-Respond
GuardDuty emits hundreds of finding types, but only a handful warrant a pager. A working engineer's guide to separating high-signal compromise indicators from recon noise, writing suppression rules that don't blind you, and wiring EventBridge to Lambda and SSM for automated containment.

Amazon GuardDuty will happily generate thousands of findings in an account that has never been touched by an attacker. Internet background radiation hits your public IPs, your own scanners trip Recon findings, and a developer running kubectl from a coffee shop lights up a PenTest finding. If your team treats every finding as an incident, you will burn out and miss the one that matters. The job is not to look at everything; it is to know which finding types are reliable signals of an active compromise, route those to a response, and aggressively suppress the rest. Severity is a coarse pre-filter calibrated for a generic account, not yours: a High Recon finding against a host you deliberately expose is often noise, while a Medium IAM finding can be the first link in a credential-theft chain. The real signal lives in the finding type string, which encodes ThreatPurpose:ResourceType/ThreatFamilyName.DetectionMechanism!Artifact.
Signal versus noise
The findings that actually matter describe an outcome of compromise rather than an attempt at one. UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS is among the highest-fidelity findings GuardDuty produces: role credentials issued to an EC2 instance are being used from an IP outside AWS, meaning the SSRF or host compromise already happened and the attacker is now wielding stolen credentials from their own infrastructure. Page on it every time. Backdoor:EC2/C&CActivity.B!DNS (an instance querying a known command-and-control domain) and Trojan:EC2/DNSDataExfiltration (data tunneled out over DNS) are strong evidence of an active implant. CryptoCurrency:EC2/BitcoinTool.B!DNS is rarely a false positive outside a developer deliberately running a miner. Stealth:IAMUser/CloudTrailLoggingDisabled is a classic anti-forensics move; if it was not your pipeline, assume compromise. Policy:S3/BucketPublicAccessGranted means a bucket was just opened to the world and needs eyes within minutes. The usually-noisy findings, by contrast, describe attempts that happen millions of times a day. Do not page on them; aggregate, dashboard, and review in bulk:
- Recon:IAMUser/MaliciousIPCaller and Recon:EC2/PortProbeUnprotectedPort - someone on a threat-intel list touched your API or scanned an open port. If the port is meant to be open, this is background radiation: high volume, low value individually.
- UnauthorizedAccess:EC2/SSHBruteForce and RDPBruteForce - public endpoints are brute-forced constantly. The finding matters only when paired with a successful login; on its own it is a reason to close the port, not an incident.
- Recon:IAMUser/TorIPCaller and UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B from an unusual location - frequently a privacy-conscious engineer, a VPN, or a traveling employee. Correlate with your IdP before reacting.
Rule of thumb: findings about credentials being used somewhere they should not be (InstanceCredentialExfiltration, C&CActivity, anomalous AssumeRole) are high-fidelity because they describe an outcome of compromise. Findings about someone trying something (BruteForce, PortProbe, MaliciousIPCaller) describe attempts. Build your alerting around outcomes, not attempts.
Suppression rules without blinding yourself
GuardDuty suppression rules auto-archive matching findings so they never reach EventBridge or your inbox, while preserving them for audit. The trap is writing rules so broad they hide a real attack. Scope every suppression to a specific finding type plus a tight attribute filter, never to severity alone. Good examples: suppress Recon:EC2/PortProbeUnprotectedPort where the local port is 443 on your known public ALB security group, or suppress UnauthorizedAccess:EC2/SSHBruteForce against a bastion whose risk you have formally accepted. Never suppress an entire ThreatPurpose like UnauthorizedAccess wholesale, and always suppress your own vulnerability scanners by source IP, or they will drown the real signal during pentests. Review the rules quarterly, because an instance that was safe to ignore last quarter may now hold sensitive data.
Auto-response with EventBridge
GuardDuty publishes every non-suppressed finding to the default EventBridge bus with a detail-type of GuardDuty Finding. You match on finding type and severity in the event pattern, then target a Lambda (for IAM/S3 containment) or an SSM Automation document (for EC2 isolation). Operationally, enable GuardDuty across every region and account through delegated administration, because attackers will pick the region you forgot, and pipe all findings to a central security account so this one rule governs the whole org. The pattern below fires only on the high-fidelity EC2 compromise types at Medium severity or above, deliberately excluding the noisy Recon and BruteForce families, and the prefix matcher catches every detection-mechanism variant of a family without enumerating each one:
{
"source": ["aws.guardduty"],
"detail-type": ["GuardDuty Finding"],
"detail": {
"severity": [{ "numeric": [">=", 4] }],
"type": [
{ "prefix": "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration" },
{ "prefix": "Backdoor:EC2/C&CActivity" },
{ "prefix": "CryptoCurrency:EC2/BitcoinTool" },
{ "prefix": "Trojan:EC2/DNSDataExfiltration" }
]
}
}Wire that rule to a Lambda target. The handler reads the instance ID from detail.resource.instanceDetails.instanceId, swaps the instance into a locked-down quarantine security group with no egress, tags it for the incident, and snapshots its volumes for forensics before any human touches it. Keep it idempotent: GuardDuty re-emits the same finding as activity continues, and you do not want to re-quarantine or duplicate snapshots on every update. For account-level findings with no instance to isolate, branch the response - on Stealth:IAMUser/CloudTrailLoggingDisabled call StartLogging and notify on-call via SNS; on Policy:S3/BucketPublicAccessGranted target an SSM Automation document that re-applies BlockPublicAccess. Every containment action stays small, reversible, and well-logged, because automation will occasionally fire on a false positive, and a quarantine you can undo in one click is far cheaper than a terminated production instance. Everything outside this high-fidelity set goes to a dashboard your team reviews on a cadence, not a pager, so that every alert reaching a human is worth their time and the machine has already contained the threats that could not wait:
import boto3
ec2 = boto3.client("ec2")
QUARANTINE_SG = "sg-0quarantine0noegress"
def handler(event, context):
detail = event["detail"]
instance_id = detail["resource"]["instanceDetails"]["instanceId"]
finding_type = detail["type"]
# Idempotency: skip if already quarantined
tags = ec2.describe_tags(Filters=[
{"Name": "resource-id", "Values": [instance_id]},
{"Name": "key", "Values": ["gd:quarantined"]}
])["Tags"]
if tags:
return {"status": "already-contained", "instance": instance_id}
ec2.modify_instance_attribute(
InstanceId=instance_id, Groups=[QUARANTINE_SG]
)
ec2.create_tags(Resources=[instance_id], Tags=[
{"Key": "gd:quarantined", "Value": "true"},
{"Key": "gd:finding", "Value": finding_type}
])
for v in ec2.describe_volumes(Filters=[{
"Name": "attachment.instance-id", "Values": [instance_id]
}])["Volumes"]:
ec2.create_snapshot(
VolumeId=v["VolumeId"],
Description=f"forensic-{instance_id}-{finding_type}"
)
return {"status": "contained", "instance": instance_id}