# Policy Determinization Architecture ## Overview The **Determinization** subsystem handles CVEs that arrive without complete evidence (EPSS, VEX, reachability). Rather than blocking pipelines or silently ignoring unknowns, it treats them as **probabilistic observations** that can mature as evidence arrives. **Design Principles:** 1. **Uncertainty is first-class** - Missing signals contribute to entropy, not guesswork 2. **Graceful degradation** - Pipelines continue with guardrails, not hard blocks 3. **Automatic hardening** - Policies tighten as evidence accumulates 4. **Full auditability** - Every decision traces back to evidence state ## Problem Statement When a CVE is discovered against a component, several scenarios create uncertainty: | Scenario | Current Behavior | Desired Behavior | |----------|------------------|------------------| | EPSS not yet published | Treat as unknown severity | Explicit `SignalState.NotQueried` with default prior | | VEX statement missing | Assume affected | Explicit uncertainty with configurable policy | | Reachability indeterminate | Conservative block | Allow with guardrails in non-prod | | Conflicting VEX sources | K4 Conflict state | Entropy penalty + human review trigger | | Stale evidence (>14 days) | No special handling | Decay-adjusted confidence + auto-review | ## Architecture ### Component Diagram ``` +------------------------+ | Policy Engine | | (Verdict Evaluation) | +------------------------+ | v +----------------+ +-------------------+ +------------------------+ | Feedser |--->| Signal Aggregator |-->| Determinization Gate | | (EPSS/VEX/KEV) | | (Null-aware) | | (Entropy Thresholds) | +----------------+ +-------------------+ +------------------------+ | | v v +-------------------+ +-------------------+ | Uncertainty Score | | GuardRails Policy | | Calculator | | (Allow/Quarantine)| +-------------------+ +-------------------+ | | v v +-------------------+ +-------------------+ | Decay Calculator | | Observation State | | (Half-life) | | (pending_determ) | +-------------------+ +-------------------+ ``` ### Library Structure ``` src/Policy/__Libraries/StellaOps.Policy.Determinization/ ├── Models/ │ ├── ObservationState.cs # CVE observation lifecycle states │ ├── SignalState.cs # Null-aware signal wrapper │ ├── SignalSnapshot.cs # Point-in-time signal collection │ ├── UncertaintyScore.cs # Knowledge completeness entropy │ ├── ObservationDecay.cs # Per-CVE decay configuration │ ├── GuardRails.cs # Guardrail policy outcomes │ └── DeterminizationContext.cs # Evaluation context container ├── Scoring/ │ ├── IUncertaintyScoreCalculator.cs │ ├── UncertaintyScoreCalculator.cs # entropy = 1 - evidence_sum │ ├── IDecayedConfidenceCalculator.cs │ ├── DecayedConfidenceCalculator.cs # Half-life decay application │ ├── SignalWeights.cs # Configurable signal weights │ └── PriorDistribution.cs # Default priors for missing signals ├── Policies/ │ ├── IDeterminizationPolicy.cs │ ├── DeterminizationPolicy.cs # Allow/quarantine/escalate rules │ ├── GuardRailsPolicy.cs # Guardrails configuration │ ├── DeterminizationRuleSet.cs # Rule definitions │ └── EnvironmentThresholds.cs # Per-environment thresholds ├── Gates/ │ ├── IDeterminizationGate.cs │ ├── DeterminizationGate.cs # Policy engine gate │ └── DeterminizationGateOptions.cs ├── Subscriptions/ │ ├── ISignalUpdateSubscription.cs │ ├── SignalUpdateHandler.cs # Re-evaluation on new signals │ └── DeterminizationEventTypes.cs ├── DeterminizationOptions.cs # Global options └── ServiceCollectionExtensions.cs # DI registration ``` ## Data Models ### ObservationState Represents the lifecycle state of a CVE observation, orthogonal to VEX status: ```csharp /// /// Observation state for CVE tracking, independent of VEX status. /// Allows a CVE to be "Affected" (VEX) but "PendingDeterminization" (observation). /// public enum ObservationState { /// /// Initial state: CVE discovered but evidence incomplete. /// Triggers guardrail-based policy evaluation. /// PendingDeterminization = 0, /// /// Evidence sufficient for confident determination. /// Normal policy evaluation applies. /// Determined = 1, /// /// Multiple signals conflict (K4 Conflict state). /// Requires human review regardless of confidence. /// Disputed = 2, /// /// Evidence decayed below threshold; needs refresh. /// Auto-triggered when decay > threshold. /// StaleRequiresRefresh = 3, /// /// Manually flagged for review. /// Bypasses automatic determinization. /// ManualReviewRequired = 4, /// /// CVE suppressed/ignored by policy exception. /// Evidence tracking continues but decisions skip. /// Suppressed = 5 } ``` ### SignalState Null-aware wrapper distinguishing "not queried" from "queried, value null": ```csharp /// /// Wraps a signal value with query status metadata. /// Distinguishes between: not queried, queried with value, queried but absent, query failed. /// public sealed record SignalState { /// Status of the signal query. public required SignalQueryStatus Status { get; init; } /// Signal value if Status is Queried and value exists. public T? Value { get; init; } /// When the signal was last queried (UTC). public DateTimeOffset? QueriedAt { get; init; } /// Reason for failure if Status is Failed. public string? FailureReason { get; init; } /// Source that provided the value (feed ID, issuer, etc.). public string? Source { get; init; } /// Whether this signal contributes to uncertainty (true if not queried or failed). public bool ContributesToUncertainty => Status is SignalQueryStatus.NotQueried or SignalQueryStatus.Failed; /// Whether this signal has a usable value. public bool HasValue => Status == SignalQueryStatus.Queried && Value is not null; } public enum SignalQueryStatus { /// Signal source not yet queried. NotQueried = 0, /// Signal source queried; value may be present or absent. Queried = 1, /// Signal query failed (timeout, network, parse error). Failed = 2 } ``` ### SignalSnapshot Point-in-time collection of all signals for a CVE observation: ```csharp /// /// Immutable snapshot of all signals for a CVE observation at a point in time. /// public sealed record SignalSnapshot { /// CVE identifier (e.g., CVE-2026-12345). public required string CveId { get; init; } /// Subject component (PURL). public required string SubjectPurl { get; init; } /// Snapshot capture time (UTC). public required DateTimeOffset CapturedAt { get; init; } /// EPSS score signal. public required SignalState Epss { get; init; } /// VEX claim signal. public required SignalState Vex { get; init; } /// Reachability determination signal. public required SignalState Reachability { get; init; } /// Runtime observation signal (eBPF, dyld, ETW). public required SignalState Runtime { get; init; } /// Fix backport detection signal. public required SignalState Backport { get; init; } /// SBOM lineage signal. public required SignalState SbomLineage { get; init; } /// Known Exploited Vulnerability flag. public required SignalState Kev { get; init; } /// CVSS score signal. public required SignalState Cvss { get; init; } } ``` ### UncertaintyScore Knowledge completeness measurement (not code entropy): ```csharp /// /// Measures knowledge completeness for a CVE observation. /// High entropy (close to 1.0) means many signals are missing. /// Low entropy (close to 0.0) means comprehensive evidence. /// public sealed record UncertaintyScore { /// Entropy value [0.0-1.0]. Higher = more uncertain. public required double Entropy { get; init; } /// Completeness value [0.0-1.0]. Higher = more complete. (1 - Entropy) public double Completeness => 1.0 - Entropy; /// Signals that are missing or failed. public required ImmutableArray MissingSignals { get; init; } /// Weighted sum of present signals. public required double WeightedEvidenceSum { get; init; } /// Maximum possible weighted sum (all signals present). public required double MaxPossibleWeight { get; init; } /// Tier classification based on entropy. public UncertaintyTier Tier => Entropy switch { <= 0.2 => UncertaintyTier.VeryLow, // Comprehensive evidence <= 0.4 => UncertaintyTier.Low, // Good evidence coverage <= 0.6 => UncertaintyTier.Medium, // Moderate gaps <= 0.8 => UncertaintyTier.High, // Significant gaps _ => UncertaintyTier.VeryHigh // Minimal evidence }; } public sealed record SignalGap( string SignalName, double Weight, SignalQueryStatus Status, string? Reason); public enum UncertaintyTier { VeryLow = 0, // Entropy <= 0.2 Low = 1, // Entropy <= 0.4 Medium = 2, // Entropy <= 0.6 High = 3, // Entropy <= 0.8 VeryHigh = 4 // Entropy > 0.8 } ``` ### ObservationDecay Time-based confidence decay configuration: ```csharp /// /// Tracks evidence freshness decay for a CVE observation. /// public sealed record ObservationDecay { /// Half-life for confidence decay. Default: 14 days per advisory. public required TimeSpan HalfLife { get; init; } /// Minimum confidence floor (never decays below). Default: 0.35. public required double Floor { get; init; } /// Last time any signal was updated (UTC). public required DateTimeOffset LastSignalUpdate { get; init; } /// Current decayed confidence multiplier [Floor-1.0]. public required double DecayedMultiplier { get; init; } /// When next auto-review is scheduled (UTC). public DateTimeOffset? NextReviewAt { get; init; } /// Whether decay has triggered stale state. public bool IsStale { get; init; } } ``` ### GuardRails Policy outcome with monitoring requirements: ```csharp /// /// Guardrails applied when allowing uncertain observations. /// public sealed record GuardRails { /// Enable runtime monitoring for this observation. public required bool EnableRuntimeMonitoring { get; init; } /// Interval for automatic re-review. public required TimeSpan ReviewInterval { get; init; } /// EPSS threshold that triggers automatic escalation. public required double EpssEscalationThreshold { get; init; } /// Reachability status that triggers escalation. public required ImmutableArray EscalatingReachabilityStates { get; init; } /// Maximum time in guarded state before forced review. public required TimeSpan MaxGuardedDuration { get; init; } /// Alert channels for this observation. public ImmutableArray AlertChannels { get; init; } = ImmutableArray.Empty; /// Additional context for audit trail. public string? PolicyRationale { get; init; } } ``` ## Scoring Algorithms ### Uncertainty Score Calculation ```csharp /// /// Calculates knowledge completeness entropy from signal snapshot. /// Formula: entropy = 1 - (sum of weighted present signals / max possible weight) /// public sealed class UncertaintyScoreCalculator : IUncertaintyScoreCalculator { private readonly SignalWeights _weights; public UncertaintyScore Calculate(SignalSnapshot snapshot) { var gaps = new List(); var weightedSum = 0.0; var maxWeight = _weights.TotalWeight; // EPSS signal if (snapshot.Epss.HasValue) weightedSum += _weights.Epss; else gaps.Add(new SignalGap("EPSS", _weights.Epss, snapshot.Epss.Status, snapshot.Epss.FailureReason)); // VEX signal if (snapshot.Vex.HasValue) weightedSum += _weights.Vex; else gaps.Add(new SignalGap("VEX", _weights.Vex, snapshot.Vex.Status, snapshot.Vex.FailureReason)); // Reachability signal if (snapshot.Reachability.HasValue) weightedSum += _weights.Reachability; else gaps.Add(new SignalGap("Reachability", _weights.Reachability, snapshot.Reachability.Status, snapshot.Reachability.FailureReason)); // Runtime signal if (snapshot.Runtime.HasValue) weightedSum += _weights.Runtime; else gaps.Add(new SignalGap("Runtime", _weights.Runtime, snapshot.Runtime.Status, snapshot.Runtime.FailureReason)); // Backport signal if (snapshot.Backport.HasValue) weightedSum += _weights.Backport; else gaps.Add(new SignalGap("Backport", _weights.Backport, snapshot.Backport.Status, snapshot.Backport.FailureReason)); // SBOM Lineage signal if (snapshot.SbomLineage.HasValue) weightedSum += _weights.SbomLineage; else gaps.Add(new SignalGap("SBOMLineage", _weights.SbomLineage, snapshot.SbomLineage.Status, snapshot.SbomLineage.FailureReason)); var entropy = 1.0 - (weightedSum / maxWeight); return new UncertaintyScore { Entropy = Math.Clamp(entropy, 0.0, 1.0), MissingSignals = gaps.ToImmutableArray(), WeightedEvidenceSum = weightedSum, MaxPossibleWeight = maxWeight }; } } ``` ### Signal Weights (Configurable) ```csharp /// /// Configurable weights for signal contribution to completeness. /// Weights should sum to 1.0 for normalized entropy. /// public sealed record SignalWeights { public double Vex { get; init; } = 0.25; public double Epss { get; init; } = 0.15; public double Reachability { get; init; } = 0.25; public double Runtime { get; init; } = 0.15; public double Backport { get; init; } = 0.10; public double SbomLineage { get; init; } = 0.10; public double TotalWeight => Vex + Epss + Reachability + Runtime + Backport + SbomLineage; public SignalWeights Normalize() { var total = TotalWeight; return new SignalWeights { Vex = Vex / total, Epss = Epss / total, Reachability = Reachability / total, Runtime = Runtime / total, Backport = Backport / total, SbomLineage = SbomLineage / total }; } } ``` ### Decay Calculation ```csharp /// /// Applies exponential decay to confidence based on evidence staleness. /// Formula: decayed = max(floor, exp(-ln(2) * age_days / half_life_days)) /// public sealed class DecayedConfidenceCalculator : IDecayedConfidenceCalculator { private readonly TimeProvider _timeProvider; public ObservationDecay Calculate( DateTimeOffset lastSignalUpdate, TimeSpan halfLife, double floor = 0.35) { var now = _timeProvider.GetUtcNow(); var ageDays = (now - lastSignalUpdate).TotalDays; double decayedMultiplier; if (ageDays <= 0) { decayedMultiplier = 1.0; } else { var rawDecay = Math.Exp(-Math.Log(2) * ageDays / halfLife.TotalDays); decayedMultiplier = Math.Max(rawDecay, floor); } // Calculate next review time (when decay crosses 50% threshold) var daysTo50Percent = halfLife.TotalDays; var nextReviewAt = lastSignalUpdate.AddDays(daysTo50Percent); return new ObservationDecay { HalfLife = halfLife, Floor = floor, LastSignalUpdate = lastSignalUpdate, DecayedMultiplier = decayedMultiplier, NextReviewAt = nextReviewAt, IsStale = decayedMultiplier <= 0.5 }; } } ``` ## Policy Rules ### Determinization Policy ```csharp /// /// Implements allow/quarantine/escalate logic per advisory specification. /// public sealed class DeterminizationPolicy : IDeterminizationPolicy { private readonly DeterminizationOptions _options; private readonly ILogger _logger; public DeterminizationResult Evaluate(DeterminizationContext ctx) { var snapshot = ctx.SignalSnapshot; var uncertainty = ctx.UncertaintyScore; var decay = ctx.Decay; var env = ctx.Environment; // Rule 1: Escalate if runtime evidence shows loaded if (snapshot.Runtime.HasValue && snapshot.Runtime.Value!.ObservedLoaded) { return DeterminizationResult.Escalated( "Runtime evidence shows vulnerable code loaded", PolicyVerdictStatus.Escalated); } // Rule 2: Quarantine if EPSS >= threshold or proven reachable if (snapshot.Epss.HasValue && snapshot.Epss.Value!.Score >= _options.EpssQuarantineThreshold) { return DeterminizationResult.Quarantined( $"EPSS score {snapshot.Epss.Value.Score:P1} exceeds threshold {_options.EpssQuarantineThreshold:P1}", PolicyVerdictStatus.Blocked); } if (snapshot.Reachability.HasValue && snapshot.Reachability.Value!.Status == ReachabilityStatus.Reachable) { return DeterminizationResult.Quarantined( "Vulnerable code is reachable via call graph", PolicyVerdictStatus.Blocked); } // Rule 3: Allow with guardrails if score < threshold AND entropy > threshold AND non-prod var trustScore = ctx.TrustScore; if (trustScore < _options.GuardedAllowScoreThreshold && uncertainty.Entropy > _options.GuardedAllowEntropyThreshold && env != DeploymentEnvironment.Production) { var guardrails = BuildGuardrails(ctx); return DeterminizationResult.GuardedAllow( $"Uncertain observation (entropy={uncertainty.Entropy:F2}) allowed with guardrails in {env}", PolicyVerdictStatus.GuardedPass, guardrails); } // Rule 4: Block in production with high entropy if (env == DeploymentEnvironment.Production && uncertainty.Entropy > _options.ProductionBlockEntropyThreshold) { return DeterminizationResult.Quarantined( $"High uncertainty (entropy={uncertainty.Entropy:F2}) not allowed in production", PolicyVerdictStatus.Blocked); } // Rule 5: Defer if evidence is stale if (decay.IsStale) { return DeterminizationResult.Deferred( $"Evidence stale (last update: {decay.LastSignalUpdate:u}), requires refresh", PolicyVerdictStatus.Deferred); } // Default: Allow (sufficient evidence or acceptable risk) return DeterminizationResult.Allowed( "Evidence sufficient for determination", PolicyVerdictStatus.Pass); } private GuardRails BuildGuardrails(DeterminizationContext ctx) => new GuardRails { EnableRuntimeMonitoring = true, ReviewInterval = TimeSpan.FromDays(_options.GuardedReviewIntervalDays), EpssEscalationThreshold = _options.EpssQuarantineThreshold, EscalatingReachabilityStates = ImmutableArray.Create("Reachable", "ObservedReachable"), MaxGuardedDuration = TimeSpan.FromDays(_options.MaxGuardedDurationDays), PolicyRationale = $"Auto-allowed with entropy={ctx.UncertaintyScore.Entropy:F2}, trust={ctx.TrustScore:F2}" }; } ``` ### Environment Thresholds ```csharp /// /// Per-environment threshold configuration. /// public sealed record EnvironmentThresholds { public DeploymentEnvironment Environment { get; init; } public double MinConfidenceForNotAffected { get; init; } public double MaxEntropyForAllow { get; init; } public double EpssBlockThreshold { get; init; } public bool RequireReachabilityForAllow { get; init; } } public static class DefaultEnvironmentThresholds { public static EnvironmentThresholds Production => new() { Environment = DeploymentEnvironment.Production, MinConfidenceForNotAffected = 0.75, MaxEntropyForAllow = 0.3, EpssBlockThreshold = 0.3, RequireReachabilityForAllow = true }; public static EnvironmentThresholds Staging => new() { Environment = DeploymentEnvironment.Staging, MinConfidenceForNotAffected = 0.60, MaxEntropyForAllow = 0.5, EpssBlockThreshold = 0.4, RequireReachabilityForAllow = true }; public static EnvironmentThresholds Development => new() { Environment = DeploymentEnvironment.Development, MinConfidenceForNotAffected = 0.40, MaxEntropyForAllow = 0.7, EpssBlockThreshold = 0.6, RequireReachabilityForAllow = false }; } ``` ## Integration Points ### Feedser Integration Feedser attaches `SignalState` to CVE observations: ```csharp // In Feedser: EpssSignalAttacher public async Task> AttachEpssAsync(string cveId, CancellationToken ct) { try { var evidence = await _epssClient.GetScoreAsync(cveId, ct); return new SignalState { Status = SignalQueryStatus.Queried, Value = evidence, QueriedAt = _timeProvider.GetUtcNow(), Source = "first.org" }; } catch (EpssNotFoundException) { return new SignalState { Status = SignalQueryStatus.Queried, Value = null, QueriedAt = _timeProvider.GetUtcNow(), Source = "first.org" }; } catch (Exception ex) { return new SignalState { Status = SignalQueryStatus.Failed, Value = null, FailureReason = ex.Message }; } } ``` ### Policy Engine Gate ```csharp // In Policy.Engine: DeterminizationGate public sealed class DeterminizationGate : IPolicyGate { private readonly IDeterminizationPolicy _policy; private readonly IUncertaintyScoreCalculator _uncertaintyCalculator; private readonly IDecayedConfidenceCalculator _decayCalculator; public async Task EvaluateAsync(PolicyEvaluationContext ctx, CancellationToken ct) { var snapshot = await BuildSignalSnapshotAsync(ctx, ct); var uncertainty = _uncertaintyCalculator.Calculate(snapshot); var decay = _decayCalculator.Calculate(snapshot.CapturedAt, ctx.Options.DecayHalfLife); var determCtx = new DeterminizationContext { SignalSnapshot = snapshot, UncertaintyScore = uncertainty, Decay = decay, TrustScore = ctx.TrustScore, Environment = ctx.Environment }; var result = _policy.Evaluate(determCtx); return new GateResult { Passed = result.Status is PolicyVerdictStatus.Pass or PolicyVerdictStatus.GuardedPass, Status = result.Status, Reason = result.Reason, GuardRails = result.GuardRails, Metadata = new Dictionary { ["uncertainty_entropy"] = uncertainty.Entropy, ["uncertainty_tier"] = uncertainty.Tier.ToString(), ["decay_multiplier"] = decay.DecayedMultiplier, ["missing_signals"] = uncertainty.MissingSignals.Select(g => g.SignalName).ToArray() } }; } } ``` ### Graph Integration CVE nodes in the Graph module carry `ObservationState` and `UncertaintyScore`: ```csharp // Extended CVE node for Graph module public sealed record CveObservationNode { public required string CveId { get; init; } public required string SubjectPurl { get; init; } // VEX status (orthogonal to observation state) public required VexClaimStatus? VexStatus { get; init; } // Observation lifecycle state public required ObservationState ObservationState { get; init; } // Knowledge completeness public required UncertaintyScore Uncertainty { get; init; } // Evidence freshness public required ObservationDecay Decay { get; init; } // Trust score (from confidence aggregation) public required double TrustScore { get; init; } // Policy outcome public required PolicyVerdictStatus PolicyHint { get; init; } // Guardrails if GuardedPass public GuardRails? GuardRails { get; init; } } ``` ## Event-Driven Re-evaluation When new signals arrive, the system re-evaluates affected observations: ```csharp public sealed class SignalUpdateHandler : ISignalUpdateSubscription { private readonly IObservationRepository _observations; private readonly IDeterminizationPolicy _policy; private readonly IEventPublisher _events; public async Task HandleAsync(SignalUpdatedEvent evt, CancellationToken ct) { // Find observations affected by this signal var affected = await _observations.FindByCveAndPurlAsync(evt.CveId, evt.Purl, ct); foreach (var obs in affected) { // Rebuild signal snapshot var snapshot = await BuildCurrentSnapshotAsync(obs, ct); // Recalculate uncertainty var uncertainty = _uncertaintyCalculator.Calculate(snapshot); // Re-evaluate policy var result = _policy.Evaluate(new DeterminizationContext { SignalSnapshot = snapshot, UncertaintyScore = uncertainty, // ... other context }); // Transition state if needed var newState = DetermineNewState(obs.ObservationState, result, uncertainty); if (newState != obs.ObservationState) { await _observations.UpdateStateAsync(obs.Id, newState, ct); await _events.PublishAsync(new ObservationStateChangedEvent( obs.Id, obs.ObservationState, newState, result.Reason), ct); } } } private ObservationState DetermineNewState( ObservationState current, DeterminizationResult result, UncertaintyScore uncertainty) { // Transition logic if (result.Status == PolicyVerdictStatus.Escalated) return ObservationState.ManualReviewRequired; if (uncertainty.Tier == UncertaintyTier.VeryLow) return ObservationState.Determined; if (current == ObservationState.PendingDeterminization && uncertainty.Tier <= UncertaintyTier.Low) return ObservationState.Determined; return current; } } ``` ## Configuration ```csharp public sealed class DeterminizationOptions { /// EPSS score that triggers quarantine (block). Default: 0.4 public double EpssQuarantineThreshold { get; set; } = 0.4; /// Trust score threshold for guarded allow. Default: 0.5 public double GuardedAllowScoreThreshold { get; set; } = 0.5; /// Entropy threshold for guarded allow. Default: 0.4 public double GuardedAllowEntropyThreshold { get; set; } = 0.4; /// Entropy threshold for production block. Default: 0.3 public double ProductionBlockEntropyThreshold { get; set; } = 0.3; /// Half-life for evidence decay in days. Default: 14 public int DecayHalfLifeDays { get; set; } = 14; /// Minimum confidence floor after decay. Default: 0.35 public double DecayFloor { get; set; } = 0.35; /// Review interval for guarded observations in days. Default: 7 public int GuardedReviewIntervalDays { get; set; } = 7; /// Maximum time in guarded state in days. Default: 30 public int MaxGuardedDurationDays { get; set; } = 30; /// Signal weights for uncertainty calculation. public SignalWeights SignalWeights { get; set; } = new(); /// Per-environment threshold overrides. public Dictionary EnvironmentThresholds { get; set; } = new(); } ``` ## Verdict Status Extension Extended `PolicyVerdictStatus` enum: ```csharp public enum PolicyVerdictStatus { Pass = 0, // Finding meets policy requirements GuardedPass = 1, // NEW: Allow with runtime monitoring enabled Blocked = 2, // Finding fails policy checks; must be remediated Ignored = 3, // Finding deliberately ignored via exception Warned = 4, // Finding passes but with warnings Deferred = 5, // Decision deferred; needs additional evidence Escalated = 6, // Decision escalated for human review RequiresVex = 7 // VEX statement required to make decision } ``` ## Metrics & Observability ```csharp public static class DeterminizationMetrics { // Counters public static readonly Counter ObservationsCreated = Meter.CreateCounter("stellaops_determinization_observations_created_total"); public static readonly Counter StateTransitions = Meter.CreateCounter("stellaops_determinization_state_transitions_total"); public static readonly Counter PolicyEvaluations = Meter.CreateCounter("stellaops_determinization_policy_evaluations_total"); // Histograms public static readonly Histogram UncertaintyEntropy = Meter.CreateHistogram("stellaops_determinization_uncertainty_entropy"); public static readonly Histogram DecayMultiplier = Meter.CreateHistogram("stellaops_determinization_decay_multiplier"); // Gauges public static readonly ObservableGauge PendingObservations = Meter.CreateObservableGauge("stellaops_determinization_pending_observations", () => /* query count */); public static readonly ObservableGauge StaleObservations = Meter.CreateObservableGauge("stellaops_determinization_stale_observations", () => /* query count */); } ``` ## Testing Strategy | Test Category | Focus Area | Example | |---------------|------------|---------| | Unit | Uncertainty calculation | Missing 2 signals = correct entropy | | Unit | Decay calculation | 14 days = 50% multiplier | | Unit | Policy rules | EPSS 0.5 + dev = guarded allow | | Integration | Signal attachment | Feedser EPSS query → SignalState | | Integration | State transitions | New VEX → PendingDeterminization → Determined | | Determinism | Same input → same output | Canonical snapshot → reproducible entropy | | Property | Entropy bounds | Always [0.0, 1.0] | | Property | Decay monotonicity | Older → lower multiplier | ## Security Considerations 1. **No Guessing:** Missing signals use explicit priors, never random values 2. **Audit Trail:** Every state transition logged with evidence snapshot 3. **Conservative Defaults:** Production blocks high entropy; only non-prod allows guardrails 4. **Escalation Path:** Runtime evidence always escalates regardless of other signals 5. **Tamper Detection:** Signal snapshots hashed for integrity verification ## References - Product Advisory: "Unknown CVEs: graceful placeholders, not blockers" - Existing: `src/Policy/__Libraries/StellaOps.Policy.Unknowns/` - Existing: `src/Policy/__Libraries/StellaOps.Policy/Confidence/` - Existing: `src/Excititor/__Libraries/StellaOps.Excititor.Core/TrustVector/` - OpenVEX Specification: https://openvex.dev/ - EPSS Model: https://www.first.org/epss/