# The Detection Layer That Was Quietly Off: GuardDuty Disabled on an AWS Account

> Source: <https://dev.to/bala_paranj_059d338e44e7e/the-detection-layer-that-was-quietly-off-guardduty-disabled-on-an-aws-account-45d8>
> Published: 2026-09-16 12:02:30+00:00

✓ 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).
