33 KiB
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:
- Uncertainty is first-class - Missing signals contribute to entropy, not guesswork
- Graceful degradation - Pipelines continue with guardrails, not hard blocks
- Automatic hardening - Policies tighten as evidence accumulates
- 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:
/// <summary>
/// Observation state for CVE tracking, independent of VEX status.
/// Allows a CVE to be "Affected" (VEX) but "PendingDeterminization" (observation).
/// </summary>
public enum ObservationState
{
/// <summary>
/// Initial state: CVE discovered but evidence incomplete.
/// Triggers guardrail-based policy evaluation.
/// </summary>
PendingDeterminization = 0,
/// <summary>
/// Evidence sufficient for confident determination.
/// Normal policy evaluation applies.
/// </summary>
Determined = 1,
/// <summary>
/// Multiple signals conflict (K4 Conflict state).
/// Requires human review regardless of confidence.
/// </summary>
Disputed = 2,
/// <summary>
/// Evidence decayed below threshold; needs refresh.
/// Auto-triggered when decay > threshold.
/// </summary>
StaleRequiresRefresh = 3,
/// <summary>
/// Manually flagged for review.
/// Bypasses automatic determinization.
/// </summary>
ManualReviewRequired = 4,
/// <summary>
/// CVE suppressed/ignored by policy exception.
/// Evidence tracking continues but decisions skip.
/// </summary>
Suppressed = 5
}
SignalState
Null-aware wrapper distinguishing "not queried" from "queried, value null":
/// <summary>
/// Wraps a signal value with query status metadata.
/// Distinguishes between: not queried, queried with value, queried but absent, query failed.
/// </summary>
public sealed record SignalState<T>
{
/// <summary>Status of the signal query.</summary>
public required SignalQueryStatus Status { get; init; }
/// <summary>Signal value if Status is Queried and value exists.</summary>
public T? Value { get; init; }
/// <summary>When the signal was last queried (UTC).</summary>
public DateTimeOffset? QueriedAt { get; init; }
/// <summary>Reason for failure if Status is Failed.</summary>
public string? FailureReason { get; init; }
/// <summary>Source that provided the value (feed ID, issuer, etc.).</summary>
public string? Source { get; init; }
/// <summary>Whether this signal contributes to uncertainty (true if not queried or failed).</summary>
public bool ContributesToUncertainty =>
Status is SignalQueryStatus.NotQueried or SignalQueryStatus.Failed;
/// <summary>Whether this signal has a usable value.</summary>
public bool HasValue => Status == SignalQueryStatus.Queried && Value is not null;
}
public enum SignalQueryStatus
{
/// <summary>Signal source not yet queried.</summary>
NotQueried = 0,
/// <summary>Signal source queried; value may be present or absent.</summary>
Queried = 1,
/// <summary>Signal query failed (timeout, network, parse error).</summary>
Failed = 2
}
SignalSnapshot
Point-in-time collection of all signals for a CVE observation:
/// <summary>
/// Immutable snapshot of all signals for a CVE observation at a point in time.
/// </summary>
public sealed record SignalSnapshot
{
/// <summary>CVE identifier (e.g., CVE-2026-12345).</summary>
public required string CveId { get; init; }
/// <summary>Subject component (PURL).</summary>
public required string SubjectPurl { get; init; }
/// <summary>Snapshot capture time (UTC).</summary>
public required DateTimeOffset CapturedAt { get; init; }
/// <summary>EPSS score signal.</summary>
public required SignalState<EpssEvidence> Epss { get; init; }
/// <summary>VEX claim signal.</summary>
public required SignalState<VexClaimSummary> Vex { get; init; }
/// <summary>Reachability determination signal.</summary>
public required SignalState<ReachabilityEvidence> Reachability { get; init; }
/// <summary>Runtime observation signal (eBPF, dyld, ETW).</summary>
public required SignalState<RuntimeEvidence> Runtime { get; init; }
/// <summary>Fix backport detection signal.</summary>
public required SignalState<BackportEvidence> Backport { get; init; }
/// <summary>SBOM lineage signal.</summary>
public required SignalState<SbomLineageEvidence> SbomLineage { get; init; }
/// <summary>Known Exploited Vulnerability flag.</summary>
public required SignalState<bool> Kev { get; init; }
/// <summary>CVSS score signal.</summary>
public required SignalState<CvssEvidence> Cvss { get; init; }
}
UncertaintyScore
Knowledge completeness measurement (not code entropy):
/// <summary>
/// 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.
/// </summary>
public sealed record UncertaintyScore
{
/// <summary>Entropy value [0.0-1.0]. Higher = more uncertain.</summary>
public required double Entropy { get; init; }
/// <summary>Completeness value [0.0-1.0]. Higher = more complete. (1 - Entropy)</summary>
public double Completeness => 1.0 - Entropy;
/// <summary>Signals that are missing or failed.</summary>
public required ImmutableArray<SignalGap> MissingSignals { get; init; }
/// <summary>Weighted sum of present signals.</summary>
public required double WeightedEvidenceSum { get; init; }
/// <summary>Maximum possible weighted sum (all signals present).</summary>
public required double MaxPossibleWeight { get; init; }
/// <summary>Tier classification based on entropy.</summary>
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:
/// <summary>
/// Tracks evidence freshness decay for a CVE observation.
/// </summary>
public sealed record ObservationDecay
{
/// <summary>Half-life for confidence decay. Default: 14 days per advisory.</summary>
public required TimeSpan HalfLife { get; init; }
/// <summary>Minimum confidence floor (never decays below). Default: 0.35.</summary>
public required double Floor { get; init; }
/// <summary>Last time any signal was updated (UTC).</summary>
public required DateTimeOffset LastSignalUpdate { get; init; }
/// <summary>Current decayed confidence multiplier [Floor-1.0].</summary>
public required double DecayedMultiplier { get; init; }
/// <summary>When next auto-review is scheduled (UTC).</summary>
public DateTimeOffset? NextReviewAt { get; init; }
/// <summary>Whether decay has triggered stale state.</summary>
public bool IsStale { get; init; }
}
GuardRails
Policy outcome with monitoring requirements:
/// <summary>
/// Guardrails applied when allowing uncertain observations.
/// </summary>
public sealed record GuardRails
{
/// <summary>Enable runtime monitoring for this observation.</summary>
public required bool EnableRuntimeMonitoring { get; init; }
/// <summary>Interval for automatic re-review.</summary>
public required TimeSpan ReviewInterval { get; init; }
/// <summary>EPSS threshold that triggers automatic escalation.</summary>
public required double EpssEscalationThreshold { get; init; }
/// <summary>Reachability status that triggers escalation.</summary>
public required ImmutableArray<string> EscalatingReachabilityStates { get; init; }
/// <summary>Maximum time in guarded state before forced review.</summary>
public required TimeSpan MaxGuardedDuration { get; init; }
/// <summary>Alert channels for this observation.</summary>
public ImmutableArray<string> AlertChannels { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Additional context for audit trail.</summary>
public string? PolicyRationale { get; init; }
}
Scoring Algorithms
Uncertainty Score Calculation
/// <summary>
/// Calculates knowledge completeness entropy from signal snapshot.
/// Formula: entropy = 1 - (sum of weighted present signals / max possible weight)
/// </summary>
public sealed class UncertaintyScoreCalculator : IUncertaintyScoreCalculator
{
private readonly SignalWeights _weights;
public UncertaintyScore Calculate(SignalSnapshot snapshot)
{
var gaps = new List<SignalGap>();
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)
/// <summary>
/// Configurable weights for signal contribution to completeness.
/// Weights should sum to 1.0 for normalized entropy.
/// </summary>
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
/// <summary>
/// Applies exponential decay to confidence based on evidence staleness.
/// Formula: decayed = max(floor, exp(-ln(2) * age_days / half_life_days))
/// </summary>
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
/// <summary>
/// Implements allow/quarantine/escalate logic per advisory specification.
/// </summary>
public sealed class DeterminizationPolicy : IDeterminizationPolicy
{
private readonly DeterminizationOptions _options;
private readonly ILogger<DeterminizationPolicy> _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
/// <summary>
/// Per-environment threshold configuration.
/// </summary>
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<T> to CVE observations:
// In Feedser: EpssSignalAttacher
public async Task<SignalState<EpssEvidence>> AttachEpssAsync(string cveId, CancellationToken ct)
{
try
{
var evidence = await _epssClient.GetScoreAsync(cveId, ct);
return new SignalState<EpssEvidence>
{
Status = SignalQueryStatus.Queried,
Value = evidence,
QueriedAt = _timeProvider.GetUtcNow(),
Source = "first.org"
};
}
catch (EpssNotFoundException)
{
return new SignalState<EpssEvidence>
{
Status = SignalQueryStatus.Queried,
Value = null,
QueriedAt = _timeProvider.GetUtcNow(),
Source = "first.org"
};
}
catch (Exception ex)
{
return new SignalState<EpssEvidence>
{
Status = SignalQueryStatus.Failed,
Value = null,
FailureReason = ex.Message
};
}
}
Policy Engine Gate
// In Policy.Engine: DeterminizationGate
public sealed class DeterminizationGate : IPolicyGate
{
private readonly IDeterminizationPolicy _policy;
private readonly IUncertaintyScoreCalculator _uncertaintyCalculator;
private readonly IDecayedConfidenceCalculator _decayCalculator;
public async Task<GateResult> 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<string, object>
{
["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:
// 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:
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
public sealed class DeterminizationOptions
{
/// <summary>EPSS score that triggers quarantine (block). Default: 0.4</summary>
public double EpssQuarantineThreshold { get; set; } = 0.4;
/// <summary>Trust score threshold for guarded allow. Default: 0.5</summary>
public double GuardedAllowScoreThreshold { get; set; } = 0.5;
/// <summary>Entropy threshold for guarded allow. Default: 0.4</summary>
public double GuardedAllowEntropyThreshold { get; set; } = 0.4;
/// <summary>Entropy threshold for production block. Default: 0.3</summary>
public double ProductionBlockEntropyThreshold { get; set; } = 0.3;
/// <summary>Half-life for evidence decay in days. Default: 14</summary>
public int DecayHalfLifeDays { get; set; } = 14;
/// <summary>Minimum confidence floor after decay. Default: 0.35</summary>
public double DecayFloor { get; set; } = 0.35;
/// <summary>Review interval for guarded observations in days. Default: 7</summary>
public int GuardedReviewIntervalDays { get; set; } = 7;
/// <summary>Maximum time in guarded state in days. Default: 30</summary>
public int MaxGuardedDurationDays { get; set; } = 30;
/// <summary>Signal weights for uncertainty calculation.</summary>
public SignalWeights SignalWeights { get; set; } = new();
/// <summary>Per-environment threshold overrides.</summary>
public Dictionary<string, EnvironmentThresholds> EnvironmentThresholds { get; set; } = new();
}
Verdict Status Extension
Extended PolicyVerdictStatus enum:
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
public static class DeterminizationMetrics
{
// Counters
public static readonly Counter<int> ObservationsCreated =
Meter.CreateCounter<int>("stellaops_determinization_observations_created_total");
public static readonly Counter<int> StateTransitions =
Meter.CreateCounter<int>("stellaops_determinization_state_transitions_total");
public static readonly Counter<int> PolicyEvaluations =
Meter.CreateCounter<int>("stellaops_determinization_policy_evaluations_total");
// Histograms
public static readonly Histogram<double> UncertaintyEntropy =
Meter.CreateHistogram<double>("stellaops_determinization_uncertainty_entropy");
public static readonly Histogram<double> DecayMultiplier =
Meter.CreateHistogram<double>("stellaops_determinization_decay_multiplier");
// Gauges
public static readonly ObservableGauge<int> PendingObservations =
Meter.CreateObservableGauge<int>("stellaops_determinization_pending_observations",
() => /* query count */);
public static readonly ObservableGauge<int> StaleObservations =
Meter.CreateObservableGauge<int>("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
- No Guessing: Missing signals use explicit priors, never random values
- Audit Trail: Every state transition logged with evidence snapshot
- Conservative Defaults: Production blocks high entropy; only non-prod allows guardrails
- Escalation Path: Runtime evidence always escalates regardless of other signals
- 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/