The Detection Layer That Was Quietly Off: GuardDuty Disabled on an AWS Account A security researcher disclosed a HackerOne report (No. 3022516) finding that an AWS account's threat-detection layer was silently disabled: CloudTrail logging, CloudWatch metric filters, and SNS alarms were all configured, but GuardDuty had no detector enabled. The researcher argues that checklist-based scanners miss this gap because they only evaluate existing resources, not the absence of a required one, and proposes a presence-check control (CTL.GUARDDUTY.ENABLED.001) requiring an AWS account to have GuardDuty enabled with at least one detector. ✓ Human-authored analysis; AI used for formatting and proofreading. A researcher looked at an AWS account and asked a question most don't run as part of a configuration audit: is the threat-detection layer actually on? CloudTrail was logging. CloudWatch metric filters were watching for unauthorised API calls. The SNS topic was wired up. But GuardDuty was disabled. This layer correlates those logs into "this looks like account compromise". This is report HackerOne 3022516 https://hackerone.com/reports/3022516 . The cost of the fix is one CLI call. The cost of operating without it is invisible until the day something goes wrong, at that point the absence of GuardDuty is the difference between an alert that arrives and an alert that never arrives. The misconfiguration is the kind a scanner finds boring because nothing is wrong with the resources you have. The wrong thing is the resource you don't have. { "audit trail": { "kind": "trail", "multi region enabled": true }, "monitoring": { "kind": "account", "metric filters": { "unauthorized api calls": { "exists": true } }, "alarms": { "unauthorized api calls": { "exists": true, "sns topic arn": "arn:aws:sns:us-east-1:123456789012:security-alerts" } } }, "threat detection": { "enabled": false } } CloudTrail: multi-region, ✓ Metric filter: present, ✓ Alarm with SNS topic: configured, ✓ GuardDuty: off The first three lull every checklist scanner into reporting a good account. The fourth is invisible to dashboards that only check the resources you've already created. The detection stack on AWS is composed, not redundant. CloudTrail records what happened. CloudWatch metric filters surface specific patterns in those records. GuardDuty is the one that asks "do the records, together, look like reconnaissance, instance compromise, or credential abuse?" The three layers each do something the others can't: When GuardDuty is off, the first two layers are recording without anyone watching. The audit trail will help you write the post-mortem many months after the breach, when you finally know to look. It will not help you catch the breach in progress. Checklist scanners check resources. They iterate through your S3 buckets, IAM roles, security groups and CloudTrails. Each resource gets evaluated. Each setting on each resource gets flagged. GuardDuty's disabled state is not a setting on a resource. It's the absence of a resource. In AWS terms, you have not created a aws guardduty detector . There is nothing for the scanner to iterate over. The scan completes successfully. The dashboard turns green. The detection layer is off. This is the same shape as the no logging gap and the no backups gap: a scanner that only looks at what exists cannot tell you what should exist but doesn't. The correct question is "does the account have a detector?" A presence check, not an attribute check. Most CSPM tools added detector-presence rules eventually, but they are special-case logic, not the default scanner shape. The invariant: An AWS account must have GuardDuty enabled with at least one detector. This is the entire control. The cost of enforcing it is one boolean check per account per snapshot. The cost of not enforcing it is invisible until something fires that GuardDuty would have caught. Stave projects the account asset with three sub-blocks: audit trail CloudTrail , monitoring CloudWatch metric filters + alarms , and threat detection GuardDuty . Three independent presence checks, three independent controls. The GuardDuty control is small: id: CTL.GUARDDUTY.ENABLED.001 name: Amazon GuardDuty Must Be Enabled severity: high compliance: soc2: "CC7.1" pci dss v4.0: "5.2" nist 800 53 r5: "SI-3" iso 27001 2022: "A.8.16" unsafe predicate: all: - field: properties.threat detection.enabled op: eq value: false attack stage: detection evasion in the params block tells the chain engine that this control's failure is in the "blinding the defender" lane. The same lane as CTL.CLOUDTRAIL.ENABLED.001 and CTL.CLOUDWATCH.MONITOR.UNAUTH.001 . When two or three of these fire together, the chain engine produces a compound finding: the account has multiple detection layers off at the same time, which is the precondition for "we got breached and nobody saw it." Run the fixture: cd stave && make build The fixture ships two snapshots T1 unsafe, T2 remediated . Evaluate the T1 snapshot in isolation to see the finding the report describes; remove T1 from the temp dir below to see the remediated state produce zero findings. tmp=$ mktemp -d cp testdata/e2e/e2e-h1-aws-3022516/observations/2025-03-04T000000Z.json "$tmp/" ./stave apply \ --controls testdata/e2e/e2e-h1-aws-3022516/controls \ --observations "$tmp" \ --max-unsafe 168h \ --eval-time 2025-03-15T00:00:00Z \ --allow-unknown-input \ --format json | jq '.findings 0 ' { "control id": "CTL.GUARDDUTY.ENABLED.001", "control severity": "high", "asset id": "acct-123456789012", "evidence": { "misconfigurations": { "property": "threat detection.enabled", "actual value": false, "operator": "eq", "unsafe value": false } }, "remediation": { "description": "GuardDuty is not enabled. Threats are not being detected.", "action": "Enable GuardDuty: aws guardduty create-detector --enable" } } actual value: false against unsafe value: false — a one-field finding that names the property and the command to flip it. No ambiguity, severity-vs-likelihood debate or human-in-the-loop interpretation required. aws guardduty create-detector --enable For an Organization-wide rollout, delegate GuardDuty administration once and enable for every member account through the delegated admin. The cost of enabling GuardDuty is the cost of the events it processes. Typically tens of dollars per account per month which is also the cost of having visibility into account compromise. Most organisations have already paid more than that in CloudTrail storage they aren't actively reading. The preventive control is a Config Rule or SCP, or AWS Organizations central-policy that asserts every member account in the organisation has at least one active detector. The same rule, expressed as a Stave invariant, runs across every snapshot and produces a deterministic verdict at every snapshot timestamp. After enabling: aws guardduty list-detectors --query 'DetectorIds 0 ' --output text The detector ID is the proof of presence. Snapshot the account again; re-run stave apply ; the finding disappears. The verification is the same shape as the detection. Both run against the same fact base, both produce the same deterministic verdict. Most cloud security tooling is shaped around "what's wrong with the things you have." That bias misses an entire class of findings shaped around "what's missing from what you should have." GuardDuty disabled is one example. CloudWatch metric filter for unauthorised API calls absent is another. CloudTrail trail not multi-region is a third. The unifying invariant is presence: For every detective control your security program assumes, the resource that implements it must exist. Stave checks this with three controls that share the same attack stage: detection evasion tag, so the chain engine surfaces the compound finding when more than one is true. The HackerOne report named one disabled layer. The compound case where two or three disabled at once is the breach scenario that costs $300M and takes nine months to detect. The fixture for this report is testdata/e2e/e2e-h1-aws-3022516/ in the Stave repo. The control library that catches it is at stave/controls/guardduty/ . The cost of running both, on every snapshot, in CI, with deterministic output, is almost zero. Framework-coverage tools like turbot/steampipe-mod-aws-compliance https://hub.powerpipe.io/mods/turbot/aws compliance check GuardDuty's configuration thoroughly when it's enabled such as detector retention, finding-publishing frequency, S3 protection, EKS audit-log protection, all enumerated against CIS and AWS Foundational Security Best Practices benchmarks. What no per-resource framework check models well is the presence invariant: across every member account in the organization, does at least one active detector exist? Stave's three attack stage: detection evasion controls compose via the chain engine into the compound finding the H1 report describes. The moment two or three detection layers go dark together is when the breach window opens. Different shape, different tool; both run in Powerpipe side by side. The comparison is at aws-compliance-mod https://github.com/sufield/stave/blob/main/docs/comparison/aws-compliance-mod.md .