Here’s a simple, *actionable* way to keep “unknowns” from piling up in Stella Ops: rank them by **how risky they might be** and **how widely they could spread**—then let Scheduler auto‑recheck or escalate based on that score. --- # Unknowns Triage: a lightweight, high‑leverage scheme **Goal:** decide which “Unknown” findings (no proof yet; inconclusive reachability; unparsed advisory; mismatched version; missing evidence) to re‑scan first or route into VEX escalation—without waiting for perfect certainty. ## 1) Define the score Score each Unknown `U` with a weighted sum (normalize each input to 0–1): * **Component popularity (P):** how many distinct workloads/images depend on this package (direct + transitive). *Proxy:* in‑degree or deployment count across environments. * **CVSS uncertainty (C):** how fuzzy the risk is (e.g., missing vector, version ranges like `<=`, vendor ambiguity). *Proxy:* 1 − certainty; higher = less certain, more dangerous to ignore. * **Graph centrality (G):** how “hub‑like” the component is in your dependency graph. *Proxy:* normalized betweenness/degree centrality in your SBOM DAG. **TriageScore(U) = wP·P + wC·C + wG·G**, with default weights: `wP=0.4, wC=0.35, wG=0.25`. **Thresholds (tuneable):** * `≥ 0.70` → **Hot**: immediate rescan + VEX escalation job * `0.40–0.69` → **Warm**: schedule rescan within 24–48h * `< 0.40` → **Cold**: batch into weekly sweep ## 2) Minimal schema (Postgres or Mongo) to support it * `unknowns(id, pkg_id, version, source, first_seen, last_seen, certainty, evidence_hash, status)` * `deploy_refs(pkg_id, image_id, env, first_seen, last_seen)` → compute **popularity P** * `graph_metrics(pkg_id, degree_c, betweenness_c, last_calc_at)` → compute **centrality G** * `advisory_gaps(pkg_id, missing_fields[], has_range_version, vendor_mismatch)` → compute **uncertainty C** > Store `triage_score`, `triage_band` on write so Scheduler can act without recomputing everything. ## 3) Fast heuristics to fill inputs * **P (popularity):** `P = min(1, log10(1 + deployments)/log10(1 + 100))` * **C (uncertainty):** start at 0; +0.3 if version range, +0.2 if vendor mismatch, +0.2 if missing CVSS vector, +0.2 if evidence stale (>7d), cap at 1.0 * **G (centrality):** precompute on SBOM DAG nightly; normalize to [0,1] ## 4) Scheduler rules (UnknownsRegistry → jobs) * On `unknowns.upsert`: * compute (P,C,G) → `triage_score` * if **Hot** → enqueue: * **Deterministic rescan** (fresh feeds + strict lattice) * **VEX escalation** (Excititor) with context pack (SBOM slice, provenance, last evidence) * if **Warm** → enqueue rescan with jitter (spread load) * if **Cold** → tag for weekly batch * Backoff: if the same Unknown stays **Hot** after N attempts, widen evidence (alternate feeds, secondary matcher, vendor OVAL, NVD mirror) and alert. ## 5) Operator‑visible UX (DevOps‑friendly) * Unknowns list: columns = pkg@ver, deployments, centrality, uncertainty flags, last evidence age, **score badge** (Hot/Warm/Cold), **Next action** chip. * Side panel: show *why* the score is high (P/C/G sub‑scores) + scheduled jobs and last outcomes. * Bulk actions: “Recompute scores”, “Force VEX escalation”, “De‑dupe aliases”. ## 6) Guardrails to keep it deterministic * Record the **inputs + weights + feed hashes** in the scan manifest (your “replay” object). * Any change to weights or heuristics → new policy version in the manifest; old runs remain replayable. ## 7) Reference snippets **SQL (Postgres) — compute and persist score:** ```sql update unknowns u set triage_score = least(1, 0.4*u.popularity_p + 0.35*u.cvss_uncertainty_c + 0.25*u.graph_centrality_g), triage_band = case when (0.4*u.popularity_p + 0.35*u.cvss_uncertainty_c + 0.25*u.graph_centrality_g) >= 0.70 then 'HOT' when (0.4*u.popularity_p + 0.35*u.cvss_uncertainty_c + 0.25*u.graph_centrality_g) >= 0.40 then 'WARM' else 'COLD' end, last_scored_at = now() where u.status = 'OPEN'; ``` **C# (Common) — score helper:** ```csharp public static (double score, string band) Score(double p, double c, double g, double wP=0.4, double wC=0.35, double wG=0.25) { var s = Math.Min(1.0, wP*p + wC*c + wG*g); var band = s >= 0.70 ? "HOT" : s >= 0.40 ? "WARM" : "COLD"; return (s, band); } ``` ## 8) Where this plugs into Stella Ops * **Scanner.WebService**: writes Unknowns with raw flags (range‑version, vector missing, vendor mismatch). * **UnknownsRegistry**: computes P/C/G, persists triage fields, emits `Unknown.Triaged`. * **Scheduler**: listens → enqueues **Rescan** / **VEX Escalation** with jitter/backoff. * **Excititor (VEX)**: builds vendor‑merge proof or raises “Unresolvable” with rationale. * **Authority**: records policy version + weights in replay manifest. --- If you want, I can drop in a ready‑to‑use `UnknownsRegistry` table DDL + EF Core 9 model and a tiny Scheduler job that implements these thresholds. Below is a complete, production-grade **developer guideline for Ranking Unknowns in Reachability Graphs** inside **Stella Ops**. It fits the existing architectural rules (scanner = origin of truth, Concelier/Vexer = prune-preservers, Authority = replay manifest owner, Scheduler = executor). These guidelines give: 1. Definitions 2. Ranking dimensions 3. Deterministic scoring formula 4. Evidence capture 5. Scheduler policies 6. UX and API rules 7. Testing rules and golden fixtures --- # Stella Ops Developer Guidelines # Ranking Unknowns in Reachability Graphs ## 0. Purpose An **Unknown** is any vulnerability-like record where **reachability**, **affectability**, or **evidence linkage** cannot yet be proved true or false. We rank Unknowns to: 1. Prioritize rescans 2. Trigger VEX escalation 3. Guide operators in constrained time windows 4. Maintain deterministic behaviour under replay manifests 5. Avoid non-deterministic or “probabilistic” security decisions Unknown ranking **never declares security state**. It determines **the order of proof acquisition**. --- # 1. Formal Definition of “Unknown” A record is classified as **Unknown** if one or more of the following is true: 1. **Dependency Reachability Unproven** * Graph traversal exists but is not validated by call-graph/rule-graph evidence. * Downstream node is reachable but no execution path has sufficient evidence. 2. **Version Semantics Uncertain** * Advisory reports `<=`, `<`, `>=`, version ranges, or ambiguous pseudo-versions. * Normalized version mapping disagrees between data sources. 3. **Component Provenance Uncertain** * Package cannot be deterministically linked to its SBOM node (name-alias confusion, epoch mismatch, distro backport case). 4. **Missing/Contradictory Evidence** * Feeds disagree; Vendor VEX differs from NVD; OSS index has missing CVSS vector; environment evidence incomplete. 5. **Weak Graph Anchoring** * Node exists but cannot be anchored to a layer digest or artifact hash (common in scratch/base images and badly packaged libs). Unknowns **must be stored with explicit flags**—not as a collapsed bucket. --- # 2. Dimensions for Ranking Unknowns Each Unknown is ranked along **five deterministic axes**: ### 2.1 Popularity Impact (P) How broadly the component is used across workloads. Evidence sources: * SBOM deployment graph * Workload registry * Layer-to-package index Compute: `P = normalized log(deployment_count)`. ### 2.2 Exploit Consequence Potential (E) Not risk. Consequence if the Unknown turns out to be an actual vulnerability. Compute from: * Maximum CVSS across feeds * CWE category weight * Vendor “criticality marker” if present * If CVSS missing → use CWE fallback → mark uncertainty penalty. ### 2.3 Uncertainty Density (U) How much is missing or contradictory. Flags (examples): * version_range → +0.25 * missing_vector → +0.15 * conflicting_feeds → +0.20 * no provenance anchor → +0.30 * unreachable source advisory → +0.10 U ∈ [0, 1]. ### 2.4 Graph Centrality (C) Is this component a structural hub? Use: * In-degree * Out-degree * Betweenness centrality Normalize per artifact type. ### 2.5 Evidence Staleness (S) Age of last successful evidence pull. Decay function: `S = min(1, age_days / 14)`. --- # 3. Deterministic Ranking Score All Unknowns get a reproducible score under replay manifest: ``` Score = clamp01( wP·P + wE·E + wU·U + wC·C + wS·S ) ``` Default recommended weights: ``` wP = 0.25 (deployment impact) wE = 0.25 (potential consequence) wU = 0.25 (uncertainty density) wC = 0.15 (graph centrality) wS = 0.10 (evidence staleness) ``` The manifest must record: * weights * transform functions * normalization rules * feed hashes * evidence hashes Thus the ranking is replayable bit-for-bit. --- # 4. Ranking Bands After computing Score: * **Hot (Score ≥ 0.70)** Immediate rescan, VEX escalation, widen evidence sources. * **Warm (0.40 ≤ Score < 0.70)** Scheduled rescan, no escalation yet. * **Cold (Score < 0.40)** Batch weekly; suppressed from UI noise except on request. Band assignment must be stored explicitly. --- # 5. Evidence Capture Requirements Every Unknown must persist: 1. **UnknownFlags[]** – all uncertainty flags 2. **GraphSliceHash** – deterministic hash of dependents/ancestors 3. **EvidenceSetHash** – hashes of advisories, vendor VEXes, feed extracts 4. **NormalizationTrace** – version normalization decision path 5. **CallGraphAttemptHash** – even if incomplete 6. **PackageMatchTrace** – exact match reasoning (name, epoch, distro backport heuristics) This allows Inspector/Authority to replay everything and prevents “ghost Unknowns” caused by environment drift. --- # 6. Scheduler Policies ### 6.1 On Unknown Created Scheduler receives event: `Unknown.Created`. Decision matrix: | Condition | Action | | --------------- | ------------------------------------- | | Score ≥ 0.70 | Immediate Rescan + VEX Escalation job | | Score 0.40–0.69 | Queue rescan within 12–72h (jitter) | | Score < 0.40 | Add to weekly batch | ### 6.2 On Unknown Unchanged after N rescans If N = 3 consecutive runs with same UnknownFlags: * Force alternate feeds (mirror, vendor direct) * Run VEX excitor with full provenance pack * If still unresolved → emit `Unknown.Unresolvable` event (not an error; a state) ### 6.3 Failure Recovery If fetch/feed errors → Unknown transitions to `Unknown.EvidenceFailed`. This must raise S (staleness) on next compute. --- # 7. Scanner Implementation Guidelines (.NET 10) ### 7.1 Ranking Computation Location Ranking is computed inside **scanner.webservice** immediately after Unknown classification. Concelier/Vexer must **not** touch ranking logic. ### 7.2 Graph Metrics Service Maintain a cached daily calculation of centrality metrics to prevent per-scan recomputation cost explosion. ### 7.3 Compute Path ``` 1. Build evidence set 2. Classify UnknownFlags 3. Compute P, E, U, C, S 4. Compute Score 5. Assign Band 6. Persist UnknownRecord 7. Emit Unknown.Triaged event ``` ### 7.4 Storage Schema (Postgres) Fields required: ``` unknown_id PK pkg_id pkg_version digest_anchor unknown_flags jsonb popularity_p float potential_e float uncertainty_u float centrality_c float staleness_s float score float band enum graph_slice_hash bytea evidence_set_hash bytea normalization_trace jsonb callgraph_attempt_hash bytea created_at, updated_at ``` --- # 8. API and UX Guidelines ### 8.1 Operator UI For every Unknown: * Score badge (Hot/Warm/Cold) * Sub-component contributions (P/E/U/C/S) * Flags list * Evidence age * Scheduled next action * History graph of score evolution ### 8.2 Filters Operators may filter by: * High P (impactful components) * High U (ambiguous advisories) * High S (stale data) * High C (graph hubs) ### 8.3 Reasoning Transparency UI must show *exactly why* the ranking is high. No hidden heuristics. --- # 9. Unit Testing & Golden Fixtures ### 9.1 Golden Unknown Cases Provide frozen fixtures for: * Version range ambiguity * Mismatched epoch/backport * Missing vector * Conflicting severity between vendor/NVD * Unanchored filesystem library Each fixture stores expected: * Flags * P/E/U/C/S * Score * Band ### 9.2 Replay Manifest Tests Given a manifest containing: * feed hashes * rules version * normalization logic * lattice rules (for overall system) Ensure ranking recomputes identically. --- # 10. Developer Checklist (must be followed) 1. Did I persist all traces needed for deterministic replay? 2. Does ranking depend only on manifest-declared parameters (not environment)? 3. Are all uncertainty factors explicit flags, never inferred fuzzily? 4. Is the scoring reproducible under identical inputs? 5. Is Scheduler decision table deterministic and exhaustively tested? 6. Does API expose full reasoning without hiding rules? --- If you want, I can now produce: 1. **A full Postgres DDL** for Unknowns. 2. **A .NET 10 service class** for ranking calculation. 3. **A golden test suite** with 20 fixtures. 4. **UI wireframe** for Unknown triage screen. Which one should I generate? Here is everything in one place: DDL, ranking service, golden fixtures, and a concrete UI wireframe that fits Stella Ops and the rules we already set (ranking happens in `scanner.webservice`, deterministic, replayable). --- ## 1. Postgres DDL for Unknown Ranking You can put this into a dedicated schema, e.g. `scanner` or `stellaops_scanner`. I will assume `scanner` below; rename as needed. ```sql -- Optional: for gen_random_uuid() CREATE EXTENSION IF NOT EXISTS pgcrypto; -- Band enum DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'unknown_band') THEN CREATE TYPE unknown_band AS ENUM ('HOT', 'WARM', 'COLD'); END IF; END$$; CREATE TABLE IF NOT EXISTS scanner_unknowns ( unknown_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- Link to component/artifact component_id uuid NOT NULL, -- FK to your components/packages table component_name text NOT NULL, component_version text NOT NULL, component_type text NOT NULL, -- e.g. "os-pkg", "library", "container-layer" source_image_id uuid NULL, -- FK to images/artifacts table if you have one digest_anchor text NULL, -- e.g. sha256:... layer or image digest -- Uncertainty flags and traces unknown_flags jsonb NOT NULL, -- array or object of flags normalization_trace jsonb NOT NULL, -- version/provenance decisions graph_slice_hash bytea NOT NULL, evidence_set_hash bytea NOT NULL, callgraph_attempt_hash bytea NULL, -- Axes (all normalized to [0, 1]) popularity_p real NOT NULL, potential_e real NOT NULL, uncertainty_u real NOT NULL, centrality_c real NOT NULL, staleness_s real NOT NULL, -- Final ranking score real NOT NULL, band unknown_band NOT NULL, -- Operational metadata created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), last_scored_at timestamptz NOT NULL DEFAULT now() ); -- Keep updated_at and last_scored_at current CREATE OR REPLACE FUNCTION scanner_unknowns_set_timestamps() RETURNS trigger AS $$ BEGIN NEW.updated_at := now(); IF NEW.score IS DISTINCT FROM OLD.score OR NEW.band IS DISTINCT FROM OLD.band OR NEW.popularity_p IS DISTINCT FROM OLD.popularity_p OR NEW.potential_e IS DISTINCT FROM OLD.potential_e OR NEW.uncertainty_u IS DISTINCT FROM OLD.uncertainty_u OR NEW.centrality_c IS DISTINCT FROM OLD.centrality_c OR NEW.staleness_s IS DISTINCT FROM OLD.staleness_s THEN NEW.last_scored_at := now(); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_scanner_unknowns_set_timestamps ON scanner_unknowns; CREATE TRIGGER trg_scanner_unknowns_set_timestamps BEFORE UPDATE ON scanner_unknowns FOR EACH ROW EXECUTE FUNCTION scanner_unknowns_set_timestamps(); -- Useful indexes CREATE INDEX IF NOT EXISTS idx_scanner_unknowns_component ON scanner_unknowns (component_id); CREATE INDEX IF NOT EXISTS idx_scanner_unknowns_source_image_band ON scanner_unknowns (source_image_id, band); CREATE INDEX IF NOT EXISTS idx_scanner_unknowns_score_band ON scanner_unknowns (band, score DESC); CREATE INDEX IF NOT EXISTS idx_scanner_unknowns_unknown_flags_gin ON scanner_unknowns USING gin (unknown_flags jsonb_path_ops); ``` If you prefer a separate table for graph/evidence hashes, they can be factored out, but the above keeps lookup simple. --- ## 2. .NET 10 Ranking Service (Scanner.WebService) This service is **pure**: given normalized axes and weights, it returns Score and Band. Axis computation (deployments → P, etc.) can be in separate helpers or services; this class only enforces the deterministic score. ### 2.1 Domain Models ```csharp namespace StellaOps.Scanner.Domain.Unknowns; public enum UnknownBand { Hot, Warm, Cold } /// /// Normalized ranking axes, all in [0, 1]. /// public sealed record UnknownRankingAxes( double PopularityP, double PotentialE, double UncertaintyU, double CentralityC, double StalenessS ); /// /// Weights for the ranking axes. Stored in the replay manifest. /// public sealed record UnknownRankingWeights( double WeightP, double WeightE, double WeightU, double WeightC, double WeightS ) { public static UnknownRankingWeights Default { get; } = new(WeightP: 0.25, WeightE: 0.25, WeightU: 0.25, WeightC: 0.15, WeightS: 0.10); } /// /// Result of ranking: final score and band, plus the axes used. /// public sealed record UnknownRankingResult( double Score, UnknownBand Band, UnknownRankingAxes Axes, UnknownRankingWeights Weights ); ``` ### 2.2 Ranking Service ```csharp using System; namespace StellaOps.Scanner.Domain.Unknowns; public interface IUnknownRankingService { UnknownRankingResult Rank(UnknownRankingAxes axes, UnknownRankingWeights? weights = null); } public sealed class UnknownRankingService : IUnknownRankingService { public UnknownRankingResult Rank(UnknownRankingAxes axes, UnknownRankingWeights? weights = null) { var w = weights ?? UnknownRankingWeights.Default; ValidateAxis(axes.PopularityP, nameof(axes.PopularityP)); ValidateAxis(axes.PotentialE, nameof(axes.PotentialE)); ValidateAxis(axes.UncertaintyU, nameof(axes.UncertaintyU)); ValidateAxis(axes.CentralityC, nameof(axes.CentralityC)); ValidateAxis(axes.StalenessS, nameof(axes.StalenessS)); var score = w.WeightP * axes.PopularityP + w.WeightE * axes.PotentialE + w.WeightU * axes.UncertaintyU + w.WeightC * axes.CentralityC + w.WeightS * axes.StalenessS; score = Clamp01(score); var band = score switch { >= 0.70 => UnknownBand.Hot, >= 0.40 => UnknownBand.Warm, _ => UnknownBand.Cold }; return new UnknownRankingResult(score, band, axes, w); } private static double Clamp01(double value) => value switch { < 0d => 0d, > 1d => 1d, _ => value }; private static void ValidateAxis(double value, string paramName) { if (value is < 0d or > 1d) { throw new ArgumentOutOfRangeException( paramName, value, "Axis value must be normalized to the [0, 1] range."); } } } ``` ### 2.3 Optional Helpers (Axis Normalization) If you want helpers for P and S (others likely depend on external data like CVSS, CWE, etc.): ```csharp public static class UnknownRankingAxisHelpers { /// /// Normalizes deployment count to popularity P in [0,1]. /// maxDeployments is the effective saturation point (e.g. 1000). /// public static double NormalizeDeploymentsToP(long deployments, long maxDeployments = 1000) { if (deployments <= 0) return 0d; if (maxDeployments <= 0) throw new ArgumentOutOfRangeException(nameof(maxDeployments)); var x = (double)deployments; var max = (double)maxDeployments; var numerator = Math.Log10(1d + x); var denominator = Math.Log10(1d + max); if (denominator <= 0d) return 0d; var value = numerator / denominator; return value < 0d ? 0d : (value > 1d ? 1d : value); } /// /// Normalizes evidence age in days to S in [0,1]. /// Half-lifeWindowDays controls how fast staleness saturates. /// public static double NormalizeEvidenceAgeToS(int ageDays, int saturationDays = 14) { if (ageDays <= 0) return 0d; if (saturationDays <= 0) throw new ArgumentOutOfRangeException(nameof(saturationDays)); var s = (double)ageDays / saturationDays; return s < 0d ? 0d : (s > 1d ? 1d : s); } } ``` You can wire those into the SBOM/graph metric pipeline as needed. --- ## 3. Golden Test Suite with 20 Fixtures ### 3.1 File Layout Proposed test project layout: ``` tests/ Scanner/ StellaOps.Scanner.Domain.Tests/ UnknownRanking/ Fixtures/ unknown-ranking-fixtures.json UnknownRankingTests.cs ``` ### 3.2 JSON Fixtures (20 Cases) `tests/Scanner/StellaOps.Scanner.Domain.Tests/UnknownRanking/Fixtures/unknown-ranking-fixtures.json` Each fixture includes axes and the expected score/band under the **default weights** from above. ```json [ { "id": "U001", "axes": { "p": 0.95, "e": 0.95, "u": 0.90, "c": 0.90, "s": 0.90 }, "expected": { "score": 0.9250, "band": "HOT" } }, { "id": "U002", "axes": { "p": 0.80, "e": 0.90, "u": 0.85, "c": 0.60, "s": 0.70 }, "expected": { "score": 0.7975, "band": "HOT" } }, { "id": "U003", "axes": { "p": 0.70, "e": 0.80, "u": 0.60, "c": 0.70, "s": 0.50 }, "expected": { "score": 0.6800, "band": "WARM" } }, { "id": "U004", "axes": { "p": 0.60, "e": 0.75, "u": 0.80, "c": 0.40, "s": 0.80 }, "expected": { "score": 0.6775, "band": "WARM" } }, { "id": "U005", "axes": { "p": 0.50, "e": 0.70, "u": 0.90, "c": 0.30, "s": 0.60 }, "expected": { "score": 0.6300, "band": "WARM" } }, { "id": "U006", "axes": { "p": 0.60, "e": 0.60, "u": 0.50, "c": 0.40, "s": 0.40 }, "expected": { "score": 0.5250, "band": "WARM" } }, { "id": "U007", "axes": { "p": 0.40, "e": 0.55, "u": 0.55, "c": 0.50, "s": 0.30 }, "expected": { "score": 0.4800, "band": "WARM" } }, { "id": "U008", "axes": { "p": 0.30, "e": 0.65, "u": 0.45, "c": 0.30, "s": 0.50 }, "expected": { "score": 0.4450, "band": "WARM" } }, { "id": "U009", "axes": { "p": 0.45, "e": 0.40, "u": 0.60, "c": 0.35, "s": 0.20 }, "expected": { "score": 0.4350, "band": "WARM" } }, { "id": "U010", "axes": { "p": 0.55, "e": 0.45, "u": 0.35, "c": 0.60, "s": 0.10 }, "expected": { "score": 0.4375, "band": "WARM" } }, { "id": "U011", "axes": { "p": 0.20, "e": 0.30, "u": 0.50, "c": 0.20, "s": 0.40 }, "expected": { "score": 0.3200, "band": "COLD" } }, { "id": "U012", "axes": { "p": 0.10, "e": 0.40, "u": 0.40, "c": 0.10, "s": 0.20 }, "expected": { "score": 0.2600, "band": "COLD" } }, { "id": "U013", "axes": { "p": 0.25, "e": 0.20, "u": 0.60, "c": 0.15, "s": 0.30 }, "expected": { "score": 0.3150, "band": "COLD" } }, { "id": "U014", "axes": { "p": 0.30, "e": 0.35, "u": 0.25, "c": 0.30, "s": 0.30 }, "expected": { "score": 0.3000, "band": "COLD" } }, { "id": "U015", "axes": { "p": 0.35, "e": 0.25, "u": 0.30, "c": 0.10, "s": 0.10 }, "expected": { "score": 0.2500, "band": "COLD" } }, { "id": "U016", "axes": { "p": 0.05, "e": 0.10, "u": 0.20, "c": 0.05, "s": 0.05 }, "expected": { "score": 0.1000, "band": "COLD" } }, { "id": "U017", "axes": { "p": 0.15, "e": 0.05, "u": 0.10, "c": 0.10, "s": 0.10 }, "expected": { "score": 0.1000, "band": "COLD" } }, { "id": "U018", "axes": { "p": 0.05, "e": 0.20, "u": 0.05, "c": 0.05, "s": 0.20 }, "expected": { "score": 0.1025, "band": "COLD" } }, { "id": "U019", "axes": { "p": 0.10, "e": 0.10, "u": 0.15, "c": 0.05, "s": 0.15 }, "expected": { "score": 0.1100, "band": "COLD" } }, { "id": "U020", "axes": { "p": 0.20, "e": 0.05, "u": 0.05, "c": 0.20, "s": 0.05 }, "expected": { "score": 0.1100, "band": "COLD" } } ] ``` ### 3.3 xUnit Test Class `UnknownRankingTests.cs`: ```csharp using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using StellaOps.Scanner.Domain.Unknowns; using Xunit; namespace StellaOps.Scanner.Domain.Tests.UnknownRanking; public sealed class UnknownRankingTests { private sealed class Fixture { public string Id { get; set; } = default!; public AxesFixture Axes { get; set; } = default!; public ExpectedFixture Expected { get; set; } = default!; } private sealed class AxesFixture { public double P { get; set; } public double E { get; set; } public double U { get; set; } public double C { get; set; } public double S { get; set; } } private sealed class ExpectedFixture { public double Score { get; set; } public string Band { get; set; } = default!; } [Fact] public void Ranking_Matches_Golden_Fixtures() { // Arrange var jsonPath = Path.Combine( AppContext.BaseDirectory, "UnknownRanking/Fixtures/unknown-ranking-fixtures.json"); var json = File.ReadAllText(jsonPath); var fixtures = JsonSerializer.Deserialize>(json) ?? throw new InvalidOperationException("Failed to deserialize fixtures."); var service = new UnknownRankingService(); foreach (var f in fixtures) { var axes = new UnknownRankingAxes( PopularityP: f.Axes.P, PotentialE: f.Axes.E, UncertaintyU:f.Axes.U, CentralityC: f.Axes.C, StalenessS: f.Axes.S); // Act var result = service.Rank(axes); // Assert Assert.Equal(f.Expected.Band, result.Band.ToString(), ignoreCase: true); Assert.InRange(result.Score, f.Expected.Score - 1e-6, f.Expected.Score + 1e-6); } } } ``` Note: the tiny epsilon allows for floating-point drift if runtimes change. --- ## 4. UI Wireframe for “Unknowns in Reachability Graphs” This is the operator view in Stella Ops UI (Angular front end). It serves on-call and security engineers; pipeline-first, UI-second. ### 4.1 Layout Overview Two-pane layout: * **Left:** Unknowns list (table) with filters and sorting. * **Right:** Inspector panel with full reasoning for selected Unknown. Rough ASCII layout: ```text +-------------------------------------------------------------------------------------+ | Filters & Summary | | [Image:] [Environment: All v] [Band: HOT/WARM/COLD v] [Component type v] [Search..] | | [Toggle] Show only HOT [Toggle] Show shared components [Toggle] Show stale evidence | +-------------------------------------------------------------------------------------+ | Unknowns (Reachability Graph) | |-------------------------------------------------------------------------------------| | Score | Band | Component | Deployments | Centrality | Uncertainty | Next | | | | | | (P/E/U/C/S)| Flags Count | Action | |-------------------------------------------------------------------------------------| | 0.93 | HOT | openssl 1.1.1k | 134 | 0.88 | 4 | Rescan | | 0.80 | HOT | log4j-core 2.17.0 | 72 | 0.91 | 3 | VEX | | 0.63 | WARM | zlib 1.2.11 | 51 | 0.70 | 2 | Batch | | ... | +---------------------------------------------+---------------------------------------+ | | Inspector: Unknown U001 | | |--------------------------------------| | | [Summary] [Axes] [Uncertainty] [Ev.] | | | | | | Component: openssl 1.1.1k | | | Band: HOT | Score: 0.93 | | | Reason: Broad use + high impact | | | | | | Axes: | | | P (Popularity) [██████████ 0.95] | | | E (Potential) [████████ 0.95] | | | U (Uncertainty) [███████ 0.90] | | | C (Centrality) [███████ 0.90] | | | S (Staleness) [███████ 0.90] | | | | | | Uncertainty flags (4): | | | • version_range | | | • missing_vector | | | • conflicting_feeds | | | • no_provenance_anchor | | | | | | Evidence: | | | • Advisories: NVD, VendorX, OSSIndex| | | • Evidence age: 19 days | | | • EvidenceSetHash: | | | • GraphSliceHash: | | | | | | Scheduler: | | | • Next action: Immediate rescan | | | • Last rescan: 2025-12-02 14:03 UTC | | | • Previous band: WARM (0.68) | +---------------------------------------------+---------------------------------------+ ``` ### 4.2 Left Pane – Unknowns List Columns: 1. **Score** * Numeric (2 decimals). * Coloured bar background by band (without hiding number). 2. **Band** (HOT/WARM/COLD) * Chip style (red/amber/grey). 3. **Component** * `name version` (e.g. `openssl 1.1.1k`). * Tooltip: component_type, package manager, distro. 4. **Deployments** * Number of workloads/images using this component. 5. **Centrality** * Single combined metric or icon-coded: * Hub (C ≥ 0.7), Medium (0.3–0.7), Leaf (≤ 0.3). 6. **Uncertainty** * `n flags` * Hover shows list of flags. 7. **Next Action** * Text: `Rescan`, `VEX`, `Batch`, `None`. * Optional “Run now” button (if operator wants manual override). Filters: * Environment (Prod / Non-prod / All) * Image / Artifact * Band (multi-select) * Component type (OS pkg / library / container layer / unknown) * Quick toggles: * “Hot only” * “Shared components” * “Stale evidence (S ≥ 0.7)” Search: * Free text on component name, version, flags. ### 4.3 Right Pane – Inspector Tabs 1. **Summary tab** * Component identity * Band + Score * Short human-readable reason: generated from top 2 axes + key flags, e.g.: “High use across 134 workloads and critical CVSS; several conflicting advisories.” 2. **Axes tab** * Horizontal bar chart for P/E/U/C/S (0–1). * Each bar labelled and clickable (shows short explanation tooltip: “P: normalized by deployment count; current value driven by 134 workloads”). 3. **Uncertainty tab** * Full list of `UnknownFlags` as discrete chips with explanations: * `version_range` – “Vendor advisory uses <= version ranges; exact boundary unknown.” * `missing_vector` – “CVSS vector missing; consequence estimated from CWE.” * Each flag links to the raw advisory snippet in “Evidence”. 4. **Evidence tab** * Data: * `EvidenceSetHash` * `GraphSliceHash` * `NormalizationTrace` (rendered in readable diff-style, e.g. “deb:2.31-0ubuntu9.2 → normalized to 2.31.0 with epoch 0”) * Advisories list: * NVD ID, Vendor ID, OSS index entries * Severity and CVSS per source * Last fetch times 5. **Scheduler tab (optional or sub-section)** * Current band and Score * Last scoring time * Next scheduled job (rescan / VEX / batch) and ETA * History mini-chart (Score over time for this Unknown) --- If you tell me where in the current Stella Ops repo tree you want to drop these (e.g. `src/Services/Scanner/StellaOps.Scanner.Domain/Unknowns/` and `src/Web/StellaOps.Web/src/app/unknowns/`), I can adapt namespaces/paths and also sketch the Angular components/interfaces to match this backend model.