Repository navigation
feat(engine): KEV-reachable findings metric with per-entity labels (ADR-0044) - #356
Merged
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:is_breach_relevant()(internet-facing entry) ORfoothold.is_some()(a proven exploitable front door), andexploited_in_wild(the CISA KEV flag).It carries per-finding entity labels:
k8s.namespace.nameNodeKeyk8s.workload.kindNodeKey(workload/<ns>/<kind>/<name>)k8s.workload.nameNodeKeyimage.referenceSecurityGraph::entry_image_reference(RunsImagewalk)cve.idsThe 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 runtimecorroboratedsignal (ADR-0009) counted byprotector.engine.corroborations. The two are unrelated predicates; the new metric does not key offcorroborations.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.idsvalue. 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.record_kev_reachable_findingsso it is unit-testable and keepsmod.rsunder the 1,000-line cap.Tests / gates
cargo fmt,cargo check,cargo clippy --all-targets -D warnings,cargo testall clean (1250 lib tests + integration binaries, including the file-size and self-containment guards).🤖 Generated with Claude Code
https://claude.ai/code/session_013cYVqzH7Xfwea7fAozdQK7