Skip to content

feat(engine): KEV-reachable findings metric with per-entity labels (ADR-0044) - #356

Merged
thejefflarson merged 1 commit into
mainfrom
kev-reachable-findings-metric
Nov 9, 2026
Merged

thejefflarson merged 1 commit into
mainfrom
kev-reachable-findings-metric

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

Do not merge without operator review

This PR reverses a documented invariant (the counts-only / no-per-entity-labels metric convention). Per the task it is an operator decision and must not be merged by an agent.

What

Adds protector.engine.kev_reachable_findings, a per-pass counter that fires once per distinct entry workload that is:

  • reachable — is_breach_relevant() (internet-facing entry) OR foothold.is_some() (a proven exploitable front door), and
  • KEV-backed — its entry image runs a CVE with exploited_in_wild (the CISA KEV flag).

It carries per-finding entity labels:

label source
k8s.namespace.name entry NodeKey
k8s.workload.kind entry NodeKey (workload/<ns>/<kind>/<name>)
k8s.workload.name entry NodeKey
image.reference new SecurityGraph::entry_image_reference (RunsImage walk)
cve.ids entry image's exploited-in-wild CVE ids, sorted + joined into one value

The invariant reversal (ADR-0044)

Every prior instrument was counts-only: entity names were held to be attacker-influenceable, so a per-entity series was treated as a cardinality/DoS vector and the name went to a structured log instead. ADR-0044 records the operator judgment that this is over-cautious for a bounded single-operator homelab — forging workload/node/image names already implies cluster compromise, and routing a KEV hit straight to the workload/image outweighs the guard. The ADR names an explicit revisit trigger: on any large/multi-tenant cluster the bound fails and the removed guard is load-bearing again.

The coverage and narrowing-delta gauges stay counts-only (their docstrings are reworded to say this is because they have no use for an entity label, not because one is forbidden).

Terminology

"KEV" here is exploited_in_wild — not the runtime corroborated signal (ADR-0009) counted by protector.engine.corroborations. The two are unrelated predicates; the new metric does not key off corroborations.

Series shape

One series per distinct entry workload per pass (dedup by entry key — every label is entry-scoped, so an entry's several objective-chains would only inflate one series), and the CVE ids join into one cve.ids value. This is series hygiene, not a reinstated cap or entity-name guard; it does not bound how many distinct workloads may appear. A residual (series count grows with distinct KEV-reachable workloads over the retention window) is noted in the ADR as a consideration, not a control.

Supporting changes

  • SecurityGraph::entry_image_reference — projects the image ref onto the finding (it wasn't before).
  • NodeKey::workload_kind — the k8s-kind-segment accessor.
  • The gate/dedupe lives in record_kev_reachable_findings so it is unit-testable and keeps mod.rs under the 1,000-line cap.

Tests / gates

  • New unit test (in-memory exporter): emits the five entity labels for a KEV + internet-facing finding; emits nothing for a non-KEV internet finding or a KEV internal-only finding; two objective-chains on one KEV entry dedupe to one series of value 1.
  • cargo fmt, cargo check, cargo clippy --all-targets -D warnings, cargo test all clean (1250 lib tests + integration binaries, including the file-size and self-containment guards).

🤖 Generated with Claude Code

https://claude.ai/code/session_013cYVqzH7Xfwea7fAozdQK7

…DR-0044)

Add `protector.engine.kev_reachable_findings`, a per-pass counter that fires
once per distinct entry workload that is reachable (internet-facing OR a proven
foothold) and whose entry image runs a CISA-KEV exploited-in-wild CVE. It
carries per-finding entity labels: `k8s.namespace.name`, `k8s.workload.kind`,
`k8s.workload.name`, `image.reference`, and a joined-and-sorted `cve.ids`.

This reverses the former "no per-entity labels on metrics" invariant. The old
counts-only rule guarded against metric-series cardinality / DoS from
attacker-influenceable workload/node/image names. ADR-0044 records the operator
decision that the rule is over-cautious for this bounded single-operator
homelab: forging those names already implies cluster compromise, and routing a
KEV hit straight to the workload/image outweighs the guard. The guard becomes
load-bearing again on a large/multi-tenant cluster — the ADR names that revisit
trigger explicitly. The coverage/narrowing-delta gauges stay counts-only
because they have no use for an entity label, not because one is forbidden; their
docstrings are reworded to say so.

"KEV" here is `exploited_in_wild`, NOT the runtime `corroborated` signal
(ADR-0009) that `protector.engine.corroborations` counts — the two are
unrelated predicates.

Supporting changes:
- `SecurityGraph::entry_image_reference`: the `RunsImage` image-ref walk for the
  `image.reference` label (the ref wasn't projected onto the finding before).
- `NodeKey::workload_kind`: the k8s-kind segment accessor for the kind label.
- The gate/dedupe lives in `record_kev_reachable_findings` (metrics.rs) so it is
  unit-testable against the in-memory exporter; this also keeps mod.rs under the
  1,000-line cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013cYVqzH7Xfwea7fAozdQK7
@thejefflarson
thejefflarson merged commit 57caeed into main Nov 9, 2026
5 checks passed
@thejefflarson
thejefflarson deleted the kev-reachable-findings-metric branch November 9, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant