Read-only Tier-2 idle/anomalous-spend + idle-resource posture checker for the R720 agent-team (design D5 / §4 / §6.3 / §7 Phase 3). Mirrors the Tier-1 checker conventions verbatim (flags --canary/--dry-run/--no-api/--targets, mode-600 report under $REPORT_ROOT/aws-posture/<date>/, ALARM-only, finding.schema.json spirit, exit 0/2/3, shared substrate redact/post_slack_alarm). Detectors (complement GuardDuty/SecurityHub/Config, do not replace): - anomalous Cost Explorer deltas (ce get-anomalies, $-impact threshold) - stopped EC2 still paying for attached EBS - unattached EBS volumes - unassociated Elastic IPs - idle NAT gateways (≈0 bytes out) - idle load balancers (0 healthy targets) - idle RDS (0 connections over window) Live AWS calls are PROVISIONING-GATED: they run ONLY when Roles Anywhere creds are available (STS identity probe) AND not --no-api/--canary. With no creds or --no-api/--canary the checker SKIPS live calls and notes them — NEVER alarms on missing data (memory feedback_cloudwatch_alarms). Roles Anywhere/step-ca are not stood up (IAM cross-review PASSED 2026-06-18; see security-review/iam/). Offline canary: fixtures of mocked AWS responses (cost/describe-* JSON) under fixtures/aws-posture/ + EXPECTED_FINDING_COUNT=7, asserted fully offline (no aws, no network). Identical detector code runs online and offline. shellcheck-clean (only accepted SC1091), chmod +x. |
||
|---|---|---|
| .. | ||
| cost-anomalies.json | ||
| describe-addresses.json | ||
| describe-db-instances.json | ||
| describe-instances.json | ||
| describe-load-balancers.json | ||
| describe-nat-gateways.json | ||
| describe-volumes.json | ||
| EXPECTED_FINDING_COUNT | ||
| README.md | ||
aws-posture canary fixtures
Mocked AWS API responses for checkers/aws-posture.sh --canary (offline — no aws calls, no
network, no credentials). The canary feeds these files to the SAME detectors the live path runs
against real aws CLI output, and asserts the total finding count equals EXPECTED_FINDING_COUNT
(anti-complacency floor, design §6.4). If a detector regresses (stops firing), the count drops and
the canary FAILS (exit 3).
These are plain JSON files (not git fixtures — aws-posture scans an AWS account, not a repo tree),
so there is no dotgit/ / .fixture rename trick here; the offline-vs-live seam is the
--canary/--no-api/no-credentials guard inside the checker (mirrors compliance-drift's
API-skip pattern). Each file is shaped like the real aws ... --output json response it stands in
for; a few _Fixture* helper keys carry the per-resource metric the live path derives from
CloudWatch (so the canary stays deterministic and offline).
| Fixture file | Stands in for | Planted finding | Count |
|---|---|---|---|
cost-anomalies.json |
aws ce get-anomalies |
1 anomaly TotalImpact ≥ threshold (the other is below threshold → must NOT fire) | 1 |
describe-instances.json |
aws ec2 describe-instances |
1 stopped instance still paying for its EBS root (the running one must NOT fire) |
1 |
describe-volumes.json |
aws ec2 describe-volumes |
1 available (unattached) volume (the in-use one must NOT fire) |
1 |
describe-addresses.json |
aws ec2 describe-addresses |
1 EIP with no association (the associated one must NOT fire) | 1 |
describe-nat-gateways.json |
aws ec2 describe-nat-gateways |
1 available NAT with ~0 bytes out / 14d (the busy one must NOT fire) |
1 |
describe-load-balancers.json |
aws elbv2 describe-load-balancers |
1 ALB with 0 healthy targets (the one with 3 must NOT fire) | 1 |
describe-db-instances.json |
aws rds describe-db-instances |
1 available RDS with 0 connections / 14d (the busy one must NOT fire) |
1 |
Total = 7 (EXPECTED_FINDING_COUNT).
aws-posture complements GuardDuty / Security Hub / Config (design §4 / Tier-2) — it is an
idle/anomalous-spend + idle-resource posture watch, not a threat detector, and never alarms on
missing data (a skipped/credential-less live call is noted, never counted — memory
feedback_cloudwatch_alarms).
When you add/remove a detector or fixture, update both the fixture and EXPECTED_FINDING_COUNT
in the same commit (the canary edit is itself caught on the next run — design §6.4).