{"slug": "the-detection-layer-that-was-quietly-off-guardduty-disabled-on-an-aws-account", "title": "The Detection Layer That Was Quietly Off: GuardDuty Disabled on an AWS Account", "summary": "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.", "body_md": "✓ Human-authored analysis; AI used for formatting and proofreading.\n\nA 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\".\n\nThis 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.\n\nThe 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.\n\n```\n{\n  \"audit_trail\": {\n    \"kind\": \"trail\",\n    \"multi_region_enabled\": true\n  },\n  \"monitoring\": {\n    \"kind\": \"account\",\n    \"metric_filters\": {\n      \"unauthorized_api_calls\": { \"exists\": true }\n    },\n    \"alarms\": {\n      \"unauthorized_api_calls\": {\n        \"exists\": true,\n        \"sns_topic_arn\": \"arn:aws:sns:us-east-1:123456789012:security-alerts\"\n      }\n    }\n  },\n  \"threat_detection\": {\n    \"enabled\": false\n  }\n}\n```\n\nCloudTrail: multi-region, ✓ \n\nMetric filter: present, ✓\n\nAlarm with SNS topic: configured, ✓ \n\nGuardDuty: off\n\nThe 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.\n\nThe 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:\n\nWhen 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.\n\nChecklist 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.\n\nGuardDuty'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.\n\nThis 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.\n\nThe invariant:\n\n**An AWS account must have GuardDuty enabled with at least one detector.**\n\nThis 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.\n\nStave 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.\n\nThe GuardDuty control is small:\n\n```\nid: CTL.GUARDDUTY.ENABLED.001\nname: Amazon GuardDuty Must Be Enabled\nseverity: high\ncompliance:\n  soc2: \"CC7.1\"\n  pci_dss_v4.0: \"5.2\"\n  nist_800_53_r5: \"SI-3\"\n  iso_27001_2022: \"A.8.16\"\nunsafe_predicate:\n  all:\n    - field: properties.threat_detection.enabled\n      op: eq\n      value: false\n```\n\n`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.\"\n\nRun the fixture:\n\n```\ncd stave && make build\n\n# The fixture ships two snapshots (T1 unsafe, T2 remediated). Evaluate the T1\n# snapshot in isolation to see the finding the report describes; remove T1\n# from the temp dir below to see the remediated state produce zero findings.\ntmp=$(mktemp -d)\ncp testdata/e2e/e2e-h1-aws-3022516/observations/2025-03-04T000000Z.json \"$tmp/\"\n\n./stave apply \\\n  --controls testdata/e2e/e2e-h1-aws-3022516/controls \\\n  --observations \"$tmp\" \\\n  --max-unsafe 168h \\\n  --eval-time 2025-03-15T00:00:00Z \\\n  --allow-unknown-input \\\n  --format json | jq '.findings[0]'\n{\n  \"control_id\": \"CTL.GUARDDUTY.ENABLED.001\",\n  \"control_severity\": \"high\",\n  \"asset_id\": \"acct-123456789012\",\n  \"evidence\": {\n    \"misconfigurations\": [\n      {\n        \"property\": \"threat_detection.enabled\",\n        \"actual_value\": false,\n        \"operator\": \"eq\",\n        \"unsafe_value\": false\n      }\n    ]\n  },\n  \"remediation\": {\n    \"description\": \"GuardDuty is not enabled. Threats are not being detected.\",\n    \"action\": \"Enable GuardDuty: aws guardduty create-detector --enable\"\n  }\n}\n```\n\n`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.\n\n```\naws guardduty create-detector --enable\n```\n\nFor 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.\n\nThe 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.\n\nAfter enabling:\n\n```\naws guardduty list-detectors --query 'DetectorIds[0]' --output text\n```\n\nThe 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.\n\nMost 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.\n\nThe unifying invariant is presence:\n\n**For every detective control your security program assumes, the resource that implements it must exist.**\n\nStave 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.\n\nThe 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.\n\nFramework-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\n\n[aws-compliance-mod](https://github.com/sufield/stave/blob/main/docs/comparison/aws-compliance-mod.md).", "url": "https://wpnews.pro/news/the-detection-layer-that-was-quietly-off-guardduty-disabled-on-an-aws-account", "canonical_source": "https://dev.to/bala_paranj_059d338e44e7e/the-detection-layer-that-was-quietly-off-guardduty-disabled-on-an-aws-account-45d8", "published_at": "2026-09-16 12:02:30+00:00", "updated_at": "2026-09-16 12:12:34.271404+00:00", "lang": "en", "topics": ["ai-safety"], "entities": ["AWS", "GuardDuty", "CloudTrail", "CloudWatch", "HackerOne", "Stave"], "alternates": {"html": "https://wpnews.pro/news/the-detection-layer-that-was-quietly-off-guardduty-disabled-on-an-aws-account", "markdown": "https://wpnews.pro/news/the-detection-layer-that-was-quietly-off-guardduty-disabled-on-an-aws-account.md", "text": "https://wpnews.pro/news/the-detection-layer-that-was-quietly-off-guardduty-disabled-on-an-aws-account.txt", "jsonld": "https://wpnews.pro/news/the-detection-layer-that-was-quietly-off-guardduty-disabled-on-an-aws-account.jsonld"}}