Skip to main content

Temporal Ghost Detection

A temporal ghost is a configuration reference that was valid in a previous snapshot but is invalid in the current one — the referenced resource was deleted between observations. Unlike single-snapshot ghost detection (which compares references against the current inventory and is vulnerable to extractor-race false positives), temporal ghost detection compares against historical inventory to confirm a resource genuinely existed-then-disappeared.

Why this shape exists​

Stave's broader ghost-reference family (23 controls covering IAM policies, resource policies, event triggers, compute dependencies, network infrastructure, cross-account trust, etc.) catches dangling references against a single snapshot's inventory. That single-snapshot mode has one structural weakness: if the collector races a deletion and a re-creation, the reference can look like a ghost when it isn't.

Temporal ghost detection eliminates the race. A reference is confirmed-ghost only when two independent snapshots witness the disappearance — the referenced resource was present in snapshot N-1, absent in snapshot N, and the reference persists. This is the highest-confidence ghost finding the catalog produces.

The two controls that ship with this shape:

ControlShapeSeverity
CTL.GHOST.TEMPORAL.RESOURCE.001Resource present in snapshot N-1, absent in N, references persist in Nhigh
CTL.GHOST.TEMPORAL.PERMISSION.001Policy Resource pattern broadened from specific ARN → wildcard between snapshots, coinciding with a resource deletion in the same window — a scope expansion disguised as cleanupmedium

CTL.GHOST.TEMPORAL.RESOURCE.001​

Predicate:

unsafe_predicate:
all:
- field: properties.governance.kind
op: eq
value: temporal_analysis
- field: properties.governance.ghost.has_temporal_ghost
op: eq
value: true

The collector emits a synthetic temporal_analysis asset that encodes the cross-snapshot comparison. has_temporal_ghost flips to true only when both observations confirm the deletion — single absence does not fire the control.

Severity inheritance (from the control description):

Reference typeSeverity
Write permissions to reclaimable resourcescritical
Monitoring targets that disappearedcritical
Read permissionshigh
Configuration referencesmedium

CTL.GHOST.TEMPORAL.PERMISSION.001​

Predicate:

unsafe_predicate:
all:
- field: properties.identity.kind
op: eq
value: role
- field: properties.identity.policy.has_broadened_scope
op: eq
value: true

Detects a specific failure mode: someone tries to "fix" a ghost reference by widening the policy's Resource pattern (e.g., arn:aws:s3:::specific-bucket/* → arn:aws:s3:::*). The widening satisfies the ghost-reference detector but expands the blast radius of any future re-creation. The resource that was deleted was specific; the new wildcard pattern admits any future bucket the account creates, including one an attacker could provision under the same role's trust boundary.

Same pattern operators encounter in incident postmortems: "We deleted the bucket but the policy still referenced it. We broadened the pattern to make the policy parse. Six months later we created a new bucket and the role had read access we didn't intend."

How the collector emits the temporal evidence​

Stave's two-snapshot semantics handle this generically — the collector compares snapshot N-1 against snapshot N during projection and emits a governance.temporal_analysis asset carrying the cross-snapshot booleans:

{
"id": "temporal-analysis-2026-05-11",
"type": "aws_temporal_analysis",
"vendor": "aws",
"properties": {
"governance": {
"kind": "temporal_analysis",
"ghost": {
"has_temporal_ghost": true,
"deleted_arn": "arn:aws:lambda:us-east-1:111122223333:function:archived-job",
"snapshots": ["2026-05-04T00:00:00Z", "2026-05-11T00:00:00Z"]
}
}
}
}

The control predicate reads only has_temporal_ghost; the auxiliary fields surface in the finding's evidence block so an auditor can see which ARN disappeared and across which window.

Relationship to the broader ghost family​

Detection modeConfidenceExample controls
Single-snapshot ghost (current-inventory check)Standard — vulnerable to extractor racesCTL.IAM.POLICY.GHOSTREF.001, CTL.CLOUDFRONT.GHOST.ORIGIN.001, CTL.COGNITO.GHOST.PRESIGNUP.001 (and 18 more)
Temporal ghost (cross-snapshot confirmation)Highest — confirmed-deleted by two observationsCTL.GHOST.TEMPORAL.RESOURCE.001, CTL.GHOST.TEMPORAL.PERMISSION.001

The two modes are complementary, not competing:

  • Single-snapshot ghost fires immediately when a reference looks dangling. Useful for real-time CI/CD gating; some false-positive rate during collector races is acceptable when the cost of a false fix is low.
  • Temporal ghost fires only after two snapshots confirm the deletion. Useful for audit-grade evidence and for compound chains that need a low-false-positive primitive.
  • docs/audits/time-features-audit.md — full inventory of time-axis features; this control covers row 13.
  • docs/compliance/owasp-nhi-top10.md NHI1 (Improper Offboarding) — temporal-ghost is the highest-confidence flavor of the offboarding-failure family.
  • E2E fixtures under testdata/e2e/e2e-forge-ghost-temporal-resource-001-{fail,pass}/ and the permission variant exercise the predicates against synthesized two-snapshot observation pairs.
  • The single-snapshot ghost family — 23 controls listed in the project README's Features section.

Commit​

The feature shipped in 28b41bb9b:

feat(ghostref): temporal ghost detection — capstone of ghost family

2 controls using time-series observation model to detect resources that became ghosts between snapshots. Highest-confidence ghost findings — confirmed by two independent observations.