* feat(secrev): doc-drift Plane-1 Tier-1 checker (UNGATED)
Third Plane-1 checker on the Phase-0 shared substrate, mirroring
compliance-drift.sh / dependency-cve.sh conventions verbatim (set -euo pipefail,
sourced substrate, --canary/--dry-run/--no-api/--refresh/--targets, mode-600
reports under $REPORT_ROOT/doc-drift/<UTC-date>/, ALARM-only, finding.schema
spirit JSON, exit 0/2/3, dotgit->.git fixture trick).
Detects documentation drift deterministically (design §4 doc-drift row):
- readme-omits-component: README omits an existing major component in the tree
(top-level service dir, SAM/CDK stack, Lambda handler dir, openapi/docs spec)
- readme-stale-vs-code: README last-touch far older than newest code commit
(two-factor: >=DOC_DRIFT_STALE_DAYS AND >=DOC_DRIFT_STALE_COMMITS)
A repo with NO README is SKIPPED (compliance-drift owns readme-present; no
double-flag). Future Gemini large-context judge (§4) is an inert stub (maybe_judge),
off in canary/dry-run/offline.
Planted-drift fixture corpus + EXPECTED_DRIFT_COUNT=4, canary-asserted (exit 3 on
miss). shellcheck -x clean (only accepted SC1091 source-line info).
Does NOT touch checker_coordinator.sh, requirements.txt, or aws-posture.
Wiring/systemd is gated (PROVISIONING footer). Design refs §4, §7 Phase 3.
* feat(secrev): Phase-3 IAM artifacts for cross-review (aws-posture gated)
Authored FILES (not applied to AWS — provisioning gated behind the mandatory
GPT-4.1 IAM cross-review + Adam, design §7 B3) for the aws-posture checker's
read-only AWS identity. Decision D5: box stays read-only, auths via IAM Roles
Anywhere short-lived leaf certs from a new internal step-ca; NO long-lived AWS key.
- aws-posture-readonly-policy.json least-privilege read-only (ce:Get*,
cloudwatch:GetMetric*/DescribeAlarms, ec2/elb/rds:Describe*, lambda list +
GetFunctionConfiguration, s3:ListAllMyBuckets/GetBucketLocation). No write,
no iam:* mutation, no s3:GetObject/secrets/kms/logs data reads, no wildcard
actions. Resource:* only where AWS has no resource-level support.
- aws-posture-readonly-policy.rationale.md per-statement least-privilege rationale.
- aws-posture-trust-policy.json pins Roles Anywhere principal + leaf subject CN +
issuer CN + trust-anchor SourceArn (three conditions, all required).
- roles-anywhere-config.json trust anchor (pins step-ca root) + profile (1h session).
- step-ca-config-sketch.md internal CA config + systemd-timer leaf auto-renewal.
- CROSS-REVIEW-PACKET.md end-to-end trust model, blast radius, EXERCISED rollback,
reviewer scrutiny list.
Does NOT build aws-posture.sh, touch checker_coordinator.sh, or requirements.txt.
* fix(secrev): apply IAM cross-review FIXes
GPT-4.1 IAM cross-review 2026-06-18: APPROVE, no BLOCKs. Applied FIXes:
- trust policy: add aws:SourceAccount=328440206208 (confused-deputy guard)
alongside the existing aws:SourceArn trust-anchor pin
- readonly policy: remove ec2:DescribeImages (data minimization — AMIs are
not an idle-spend signal)
- aws:RequestedRegion NIT: deliberately SKIPPED — ce:* and s3:ListAllMyBuckets
are global-endpoint services a blanket region condition could DENY; rationale
recorded in aws-posture-readonly-policy.rationale.md
- rationale.md + CROSS-REVIEW-PACKET.md: record APPROVE + FIXes + NIT answers
(snapshots=account-owned idle signal; s3 list=names-only; no logs:* needed)
* feat(secrev): aws-posture checker (Tier-2, provisioning-gated)
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.
* fix(secrev): doc-drift fixture py ruff-clean (root CI runs check + format --check)
The repo-root CI lint runs both 'ruff check .' and 'ruff format --check .' over
all fixtures. Fixed E701 one-liners and ruff-formatted the sample-service .py
files (handlers/*, feature_*.py). Fixture content is irrelevant to doc-drift
(keys on file/dir presence + git staleness).
2.5 KiB
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).