This commit is contained in:
StellaOps Bot
2025-12-13 02:22:15 +02:00
parent 564df71bfb
commit 999e26a48e
395 changed files with 25045 additions and 2224 deletions

View File

@@ -1,215 +1,254 @@
# Reachability Lattice & Scoring Model
> **Status:** Draft mirrors the December 2025 advisory on confidence-based reachability.
> **Owners:** Scanner Guild · Policy Guild · Signals Guild.
> **Status:** Implemented v0 in Signals; this document describes the current deterministic bucket model and its policy-facing implications.
> **Owners:** Scanner Guild · Signals Guild · Policy Guild.
> Stella Ops isn't just another scanner—it's a different product category: **deterministic, evidence-linked vulnerability decisions** that survive auditors, regulators, and supply-chain propagation.
This document defines the confidence lattice, evidence types, mitigation scoring, and policy gates used to turn static/runtime signals into reproducible reachability decisions and VEX statuses.
StellaOps models reachability as a deterministic, evidence-linked outcome that can safely represent "unknown" without silently producing false safety. Signals produces a `ReachabilityFactDocument` with per-target `states[]` and a top-level `score` that is stable under replays.
---
## 1. Overview
## 1. Current model (Signals v0)
<!-- TODO: Review for separate approval - updated lattice overview -->
**Key differentiator:** Unlike simplistic yes/no reachability approaches, the Stella Ops lattice model explicitly handles an **"Unknown"** (under_investigation) state, ensuring incomplete data doesn't lead to false safety. Every VEX decision is evidence-linked with proof pointers to the underlying reachability evidence.
Signals scoring (`src/Signals/StellaOps.Signals/Services/ReachabilityScoringService.cs`) computes, for each `target` symbol:
Classic "reachable: true/false" answers are too brittle. Stella Ops models reachability as an **ordered lattice** with explicit states and scores. Each analyzer/runtime probe emits `Evidence` documents; mitigations add `Mitigation` entries. The lattice engine joins both inputs into a `ReachDecision`:
- `reachable`: whether there exists a path from the selected `entryPoints[]` to `target`.
- `bucket`: a coarse classification of *why* the target is/was reachable.
- `confidence` (0..1): a bounded confidence value.
- `weight` (0..1): bucket multiplier.
- `score` (0..1): `confidence * weight`.
- `path[]`: the discovered path (if reachable), deterministically ordered.
- `evidence.runtimeHits[]`: runtime hit symbols that appear on the chosen path.
The fact-level `score` is the average of per-target scores, penalized by unknowns pressure (see §4).
---
## 2. Buckets & default weights
Bucket assignment is deterministic and uses this precedence:
1. `unreachable` — no path exists.
2. `entrypoint` — the `target` itself is an entrypoint.
3. `runtime` — at least one runtime hit overlaps the discovered path.
4. `direct` — reachable and the discovered path is length ≤ 2.
5. `unknown` — reachable but none of the above classifications apply.
Default weights (configurable via `SignalsOptions:Scoring:ReachabilityBuckets`):
| Bucket | Default weight |
|--------|----------------|
| `entrypoint` | `1.0` |
| `direct` | `0.85` |
| `runtime` | `0.45` |
| `unknown` | `0.5` |
| `unreachable` | `0.0` |
---
## 3. Confidence (reachable vs unreachable)
Default confidence values (configurable via `SignalsOptions:Scoring:*`):
| Input | Default |
|-------|---------|
| `reachableConfidence` | `0.75` |
| `unreachableConfidence` | `0.25` |
| `runtimeBonus` | `0.15` |
| `minConfidence` | `0.05` |
| `maxConfidence` | `0.99` |
Rules:
- Base confidence is `reachableConfidence` when `reachable=true`, otherwise `unreachableConfidence`.
- When `reachable=true` and runtime evidence overlaps the selected path, add `runtimeBonus` (bounded by `maxConfidence`).
- The final confidence is clamped to `[minConfidence, maxConfidence]`.
---
## 4. Unknowns pressure (missing/ambiguous evidence)
Signals tracks unresolved symbols/edges as **Unknowns** (see `docs/signals/unknowns-registry.md`). The number of unknowns for a subject influences the final score:
```
UNOBSERVED (09)
< POSSIBLE (1029)
< STATIC_PATH (3059)
< DYNAMIC_SEEN (6079)
< DYNAMIC_USER_TAINTED (8099)
< EXPLOIT_CONSTRAINTS_REMOVED (100)
unknownsPressure = unknownsCount / (targetsCount + unknownsCount)
pressurePenalty = min(unknownsPenaltyCeiling, unknownsPressure)
fact.score = avg(states[i].score) * (1 - pressurePenalty)
```
Each state corresponds to increasing confidence that a vulnerability can execute. Mitigations reduce scores; policy gates map scores to VEX statuses (`not_affected`, `under_investigation`, `affected`).
Default `unknownsPenaltyCeiling` is `0.35` (configurable).
This keeps the system deterministic while preventing unknown-heavy subjects from appearing "safe" by omission.
---
## 2. Core types
## 5. Evidence references & determinism anchors
```csharp
public enum ReachState { Unobserved, Possible, StaticPath, DynamicSeen, DynamicUserTainted, ExploitConstraintsRemoved }
Signals produces stable references intended for downstream evidence chains:
public enum EvidenceKind {
StaticCallEdge, StaticEntryPointProximity, StaticPackageDeclaredOnly,
RuntimeMethodHit, RuntimeStackSample, RuntimeHttpRouteHit,
UserInputSource, DataTaintFlow, ConfigFlagOn, ConfigFlagOff,
ContainerNetworkRestricted, ContainerNetworkOpen,
WafRulePresent, PatchLevel, VendorVexNotAffected, VendorVexAffected,
ManualOverride
}
- `metadata.fact.digest` — canonical digest of the reachability fact (`sha256:<hex>`).
- `metadata.fact.version` — monotonically increasing integer for the same `subjectKey`.
- Callgraph ingestion returns a deterministic `graphHash` (sha256) for the normalized callgraph.
public sealed record Evidence(
string Id,
EvidenceKind Kind,
double Weight,
string Source,
DateTimeOffset Timestamp,
string? ArtifactRef,
string? Details);
public enum MitigationKind { WafRule, FeatureFlagDisabled, AuthZEnforced, InputValidation, PatchedVersion, ContainerNetworkIsolation, RuntimeGuard, KillSwitch, Other }
public sealed record Mitigation(
string Id,
MitigationKind Kind,
double Strength,
string Source,
DateTimeOffset Timestamp,
string? ConfigHash,
string? Details);
public sealed record ReachDecision(
string VulnerabilityId,
string ComponentPurl,
ReachState State,
int Score,
string PolicyVersion,
IReadOnlyList<Evidence> Evidence,
IReadOnlyList<Mitigation> Mitigations,
IReadOnlyDictionary<string,string> Metadata);
```
Downstream services (Policy, UI/CLI explainers, replay tooling) should use these fields as stable evidence references.
---
## 3. Scoring policy (default)
## 6. Policy-facing guidance (avoid false "not affected")
| Evidence class | Base score contribution |
|--------------------------|-------------------------|
| Static path (call graph) | ≥ 30 |
| Runtime hit | ≥ 60 |
| User-tainted flow | ≥ 80 |
| "Constraints removed" | = 100 |
| Lockfile-only evidence | 10 (if no other signals)|
Policy should treat `unreachable` (or low fact score) as **insufficient** to claim "not affected" unless:
Mitigations subtract up to 40 points (configurable):
- the reachability evidence is present and referenced (`metadata.fact.digest`), and
- confidence is above a high-confidence threshold.
When evidence is missing or confidence is low, the correct output is **under investigation** rather than "not affected".
---
## 7. Signals API pointers
- `docs/api/signals/reachability-contract.md`
- `docs/api/signals/samples/facts-sample.json`
---
## 8. Roadmap (tracked in Sprint 0401)
- Introduce first-class uncertainty state lists + entropy-derived `riskScore` (see `docs/uncertainty/README.md`).
- Extend evidence refs to include CAS/DSSE pointers for graph-level and edge-bundle attestations.
---
## 9. Formal Lattice Model v1 (design — Sprint 0401)
The v0 bucket model provides coarse classification. The v1 lattice model introduces a formal 7-state lattice with algebraic join/meet operations for monotonic, deterministic reachability analysis across evidence types.
### 9.1 State Definitions
| State | Code | Ordering | Description |
|-------|------|----------|-------------|
| `Unknown` | `U` | ⊥ (bottom) | No evidence available; default state |
| `StaticallyReachable` | `SR` | 1 | Static analysis suggests path exists |
| `StaticallyUnreachable` | `SU` | 1 | Static analysis finds no path |
| `RuntimeObserved` | `RO` | 2 | Runtime probe/hit confirms execution |
| `RuntimeUnobserved` | `RU` | 2 | Runtime probe active but no hit observed |
| `ConfirmedReachable` | `CR` | 3 | Both static + runtime agree reachable |
| `ConfirmedUnreachable` | `CU` | 3 | Both static + runtime agree unreachable |
| `Contested` | `X` | (top) | Static and runtime evidence conflict |
### 9.2 Lattice Ordering (Hasse Diagram)
```
effectiveScore = baseScore - min(sum(m.Strength), 1.0) * MaxMitigationDelta
Contested (X)
/ | \
/ | \
ConfirmedReachable | ConfirmedUnreachable
(CR) | (CU)
| \ / / |
| \ / / |
| \ / / |
RuntimeObserved RuntimeUnobserved
(RO) (RU)
| |
| |
StaticallyReachable StaticallyUnreachable
(SR) (SU)
\ /
\ /
Unknown (U)
```
Clamp final scores to 0100.
### 9.3 Join Rules (⊔ — least upper bound)
---
When combining evidence from multiple sources, use the join operation:
## 4. State & VEX gates
```
U ⊔ S = S (any evidence beats unknown)
SR ⊔ RO = CR (static reachable + runtime hit = confirmed)
SU ⊔ RU = CU (static unreachable + runtime miss = confirmed)
SR ⊔ RU = X (static reachable but runtime miss = contested)
SU ⊔ RO = X (static unreachable but runtime hit = contested)
CR ⊔ CU = X (conflicting confirmations = contested)
X ⊔ * = X (contested absorbs all)
```
Default thresholds (edit in `reachability.policy.yml`):
**Full join table:**
| State | Score range |
|----------------------------|-------------|
| UNOBSERVED | 09 |
| POSSIBLE | 1029 |
| STATIC_PATH | 3059 |
| DYNAMIC_SEEN | 6079 |
| DYNAMIC_USER_TAINTED | 8099 |
| EXPLOIT_CONSTRAINTS_REMOVED| 100 |
| ⊔ | U | SR | SU | RO | RU | CR | CU | X |
|---|---|----|----|----|----|----|----|---|
| **U** | U | SR | SU | RO | RU | CR | CU | X |
| **SR** | SR | SR | X | CR | X | CR | X | X |
| **SU** | SU | X | SU | X | CU | X | CU | X |
| **RO** | RO | CR | X | RO | X | CR | X | X |
| **RU** | RU | X | CU | X | RU | X | CU | X |
| **CR** | CR | CR | X | CR | X | CR | X | X |
| **CU** | CU | X | CU | X | CU | X | CU | X |
| **X** | X | X | X | X | X | X | X | X |
VEX mapping:
### 9.4 Meet Rules (⊓ — greatest lower bound)
* **not_affected**: score ≤ 25 or mitigations dominate (score reduced below threshold).
* **affected**: score ≥ 60 (dynamic evidence without sufficient mitigation).
* **under_investigation**: everything between. **This explicit "Unknown" state is a key differentiator**—incomplete data never leads to false safety.
Used for conservative intersection (e.g., multi-entry-point consensus):
Each decision records `reachability.policy.version`, analyzer versions, policy hash, and config snapshot so downstream verifiers can replay the exact logic. All decisions are sealed in Decision Capsules for audit-grade reproducibility.
```
U ⊓ * = U (unknown is bottom)
CR ⊓ CR = CR (agreement preserved)
X ⊓ S = S (drop contested to either side)
```
---
### 9.5 Monotonicity Properties
## 5. Evidence sources
1. **Evidence accumulation is monotonic:** Once state rises in the lattice, it cannot descend without explicit revocation.
2. **Revocation resets to Unknown:** When evidence is invalidated (e.g., graph invalidation), state resets to `U`.
3. **Contested states require human triage:** `X` state triggers policy flags and UI attention.
| Signal group | Producers | EvidenceKind |
|--------------|-----------|--------------|
| Static call graph | Roslyn/IL walkers, ASP.NET routing models, JVM/JIT analyzers | `StaticCallEdge`, `StaticEntryPointProximity`, `StaticFrameworkRouteEdge` |
| Runtime sampling | .NET EventPipe, JFR, Node inspector, Go/Rust probes | `RuntimeMethodHit`, `RuntimeStackSample`, `RuntimeHttpRouteHit` |
| Data/taint | Taint analyzers, user-input detectors | `UserInputSource`, `DataTaintFlow` |
| Environment | Config snapshot, container args, network policy | `ConfigFlagOn/Off`, `ContainerNetworkRestricted/Open` |
| Mitigations | WAF connectors, patch diff, kill switches | `MitigationKind.*` via `Mitigation` records |
| Trust | Vendor VEX statements, manual overrides | `VendorVexNotAffected/Affected`, `ManualOverride` |
### 9.6 Mapping v0 Buckets to v1 States
Each evidence object **must** log `Source`, timestamps, and references (function IDs, config hashes) so auditors can trace it in the event graph. This enables **evidence-linked VEX decisions** where every assertion includes pointers to the underlying proof.
| v0 Bucket | v1 State(s) | Notes |
|-----------|-------------|-------|
| `unreachable` | `SU`, `CU` | Depends on runtime evidence availability |
| `entrypoint` | `CR` | Entry points are by definition reachable |
| `runtime` | `RO`, `CR` | Depends on static analysis agreement |
| `direct` | `SR`, `CR` | Direct paths with/without runtime confirmation |
| `unknown` | `U` | No evidence available |
---
### 9.7 Policy Decision Matrix
## 6. Event graph schema
| v1 State | VEX "not_affected" | VEX "affected" | VEX "under_investigation" |
|----------|-------------------|----------------|---------------------------|
| `U` | ❌ blocked | ⚠️ needs evidence | ✅ default |
| `SR` | ❌ blocked | ✅ allowed | ✅ allowed |
| `SU` | ⚠️ low confidence | ❌ contested | ✅ allowed |
| `RO` | ❌ blocked | ✅ allowed | ✅ allowed |
| `RU` | ⚠️ medium confidence | ❌ contested | ✅ allowed |
| `CR` | ❌ blocked | ✅ required | ❌ invalid |
| `CU` | ✅ allowed | ❌ blocked | ❌ invalid |
| `X` | ❌ blocked | ❌ blocked | ✅ required |
Persist function-level edges and evidence in Mongo (or your event store) under:
### 9.8 Implementation Notes
* `reach_functions` documents keyed by `FunctionId`.
* `reach_call_sites` `CallSite` edges (`caller`, `callee`, `frameworkEdge`).
* `reach_evidence` array of `Evidence` per `(scanId, vulnId, component)`.
* `reach_mitigations` array of `Mitigation` entries with config hashes.
* `reach_decisions` final `ReachDecision` document; references above IDs.
- **State storage:** `ReachabilityFactDocument.states[].latticeState` field (enum)
- **Join implementation:** `ReachabilityLattice.Join(a, b)` in `src/Signals/StellaOps.Signals/Services/`
- **Backward compatibility:** v0 bucket computed from v1 state for API consumers
All collections are tenant-scoped and include analyzer/policy version metadata.
### 9.9 Evidence Chain Requirements
---
## 7. Policy gates → VEX decisions
VEXer consumes `ReachDecision` and `reachability.policy.yml` to emit:
Each lattice state transition must be accompanied by evidence references:
```json
{
"vulnerability": "CVE-2025-1234",
"products": ["pkg:nuget/Example@1.2.3"],
"status": "not_affected|under_investigation|affected",
"status_notes": "Reachability score 22 (Possible) with WAF rule mitigation.",
"justification": "component_not_present|vulnerable_code_not_present|... or custom reason",
"action_statement": "Monitor config ABC",
"impact_statement": "Runtime probes observed 0 hits; static call graph absent.",
"timestamp": "...",
"custom": {
"reachability": {
"state": "POSSIBLE",
"score": 22,
"policyVersion": "reach-1",
"evidenceRefs": ["evidence:123", "mitigation:456"]
"symbol": "sym:java:...",
"latticeState": "CR",
"previousState": "SR",
"evidence": {
"static": {
"graphHash": "blake3:...",
"pathLength": 3,
"confidence": 0.92
},
"runtime": {
"probeId": "probe:...",
"hitCount": 47,
"observedAt": "2025-12-13T10:00:00Z"
}
}
},
"transitionAt": "2025-12-13T10:00:00Z"
}
```
Justifications cite specific evidence/mitigation IDs so replay bundles (`docs/replay/DETERMINISTIC_REPLAY.md`) can prove the decision.
---
## 8. Runtime probes (overview)
* .NET: EventPipe session watching `Microsoft-Windows-DotNETRuntime/Loader,JIT``RuntimeMethodHit`.
* JVM: JFR recording with `MethodProfilingSample` events.
* Node/TS: Inspector or `--trace-event-categories node.async_hooks,node.perf` sample.
* Go/Rust: `pprof`/probe instrumentation.
All runtime probes write evidence via `IRuntimeEvidenceSink`, which deduplicates hits, enriches them with `FunctionId`, and stores them in `reach_evidence`.
See `src/Scanner/StellaOps.Scanner.WebService/Reachability/Runtime/DotNetRuntimeProbe.cs` (once implemented) for reference.
---
## 9. Hybrid Reachability
<!-- TODO: Review for separate approval - added hybrid reachability section -->
Stella Ops combines **static call-graph analysis** with **runtime process tracing** for true hybrid reachability:
- **Static analysis** provides call-graph edges from IL/bytecode analysis, framework routing models, and entry-point proximity calculations.
- **Runtime analysis** provides observed method hits, stack samples, and HTTP route hits from live or shadow traffic.
- **Hybrid reconciliation** merges both signal types, with each edge type attestable via DSSE. See `docs/reachability/hybrid-attestation.md` for the attestation model.
This hybrid approach ensures that both build-time and run-time context contribute to the same verdict, avoiding the blind spots of purely static or purely runtime analysis.
---
## 10. Roadmap
| Task | Description |
|------|-------------|
| `REACH-LATTICE-401-023` | Initial lattice types + scoring engine + event graph schema. |
| `REACH-RUNTIME-402-024` | Productionize runtime probes (EventPipe/JFR) with opt-in config and telemetry. |
| `REACH-VEX-402-025` | Wire `ReachDecision` into VEX generator; ensure OpenVEX/CSAF cite reachability evidence. |
| `REACH-POLICY-402-026` | Expose reachability gates in Policy DSL & CLI (edit/lint/test). |
Keep this doc updated as the lattice evolves or new signals/mitigations are added.