Smart-Diff scoring configuration guide
Tune how Stella Ops Smart-Diff prioritises material risk changes between two scans — the weights and thresholds that drive each finding’s priority score. This guide is for security and platform engineers calibrating detection to their risk appetite; it covers the two configuration types (Scanner-side and Policy-side), the shipped presets, and how the priority score is actually computed. For how these results are exported to CI, see SARIF integration.
Provenance: SPRINT_3500_0004_0001 · SDIFF-BIN-031.
Reconciliation note (verify against source). This guide was reconciled against the implementation. Two distinct configuration types exist and were previously conflated here:
StellaOps.Scanner.SmartDiff.Detection.SmartDiffScoringConfig— the standalone Scanner-side config with named presets and aValidate()method (src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff/Detection/SmartDiffScoringConfig.cs).StellaOps.Policy.SmartDiffPolicyScoringConfig— an optional record embedded inPolicyScoringConfig(src/Policy/__Libraries/StellaOps.Policy/PolicyScoringConfig.cs).Their field sets and defaults differ. Where this guide states a default, it names the owning type. When the code and this doc disagree, the code wins.
Overview
Smart-Diff uses configurable scoring weights to prioritize material risk changes. This guide explains how to customize scoring for your organization’s risk appetite.
Configuration Surfaces
Smart-Diff scoring weights are defined in code as two configuration types:
SmartDiffScoringConfig(Scanner library) — comprehensive weights + thresholds, named presets (GetPreset), andValidate(). Converts to detector options viaToDetectorOptions()/CreateDetector().SmartDiffPolicyScoringConfig(Policy library) — an optional record carried onPolicyScoringConfig.SmartDiff. Bound from policy YAML (camelCase keys) byPolicyScoringConfigBinder.
Not implemented. There is currently no environment-variable override path and no HTTP configuration endpoint for Smart-Diff scoring. Earlier drafts of this guide documented
STELLAOPS_SMARTDIFF_*variables andGET/PUT /api/v1/config/smart-diff/scoring; neither exists in the codebase. See Configuring weights and Smart-Diff API surface below for what is actually available.
Default Configuration
SmartDiffScoringConfig (Scanner) defaults
Source: src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff/Detection/SmartDiffScoringConfig.cs.
{
"name": "default",
"version": "1.0",
"reachabilityFlipUpWeight": 1.0,
"reachabilityFlipDownWeight": 0.8,
"useLatticeConfidence": true,
"vexFlipToAffectedWeight": 0.9,
"vexFlipToNotAffectedWeight": 0.7,
"vexFlipToFixedWeight": 0.6,
"vexFlipToUnderInvestigationWeight": 0.3,
"rangeEntryWeight": 0.8,
"rangeExitWeight": 0.6,
"kevAddedWeight": 1.0,
"kevRemovedWeight": 0.5,
"epssThresholdWeight": 0.6,
"epssThreshold": 0.5,
"policyFlipWeight": 0.7,
"hardeningRegressionWeight": 0.7,
"hardeningScoreThreshold": 0.2,
"includeHardeningFlags": true,
"kevBoost": 1.5,
"minPriorityScore": 0.1,
"highPriorityThreshold": 0.7,
"criticalPriorityThreshold": 0.9
}
SmartDiffPolicyScoringConfig (Policy) defaults
Source: src/Policy/__Libraries/StellaOps.Policy/PolicyScoringConfig.cs. Note the field set differs from the Scanner config (e.g. it has epssThresholdCrossWeight, hardeningImprovementWeight, hardeningRegressionThreshold, and epssThreshold defaults to 0.1 here vs 0.5 in the Scanner config).
{
"reachabilityFlipUpWeight": 1.0,
"reachabilityFlipDownWeight": 0.8,
"vexFlipToAffectedWeight": 0.9,
"vexFlipToNotAffectedWeight": 0.7,
"rangeEntryWeight": 0.8,
"rangeExitWeight": 0.6,
"kevAddedWeight": 1.0,
"epssThreshold": 0.1,
"epssThresholdCrossWeight": 0.5,
"hardeningRegressionWeight": 0.7,
"hardeningImprovementWeight": 0.3,
"hardeningRegressionThreshold": 0.1
}
Weight Categories (SmartDiffScoringConfig)
The tables below document the Scanner SmartDiffScoringConfigfields. The detector (MaterialRiskChangeDetector) consumes the subset projected by ToDetectorOptions().
Reachability Weights (R1)
Controls scoring for reachability status changes (reachable flips true/false; unknown is skipped).
| Parameter | Default | Description |
|---|---|---|
reachabilityFlipUpWeight | 1.0 | Unreachable → Reachable (risk increase) |
reachabilityFlipDownWeight | 0.8 | Reachable → Unreachable (risk decrease) |
useLatticeConfidence | true | Whether to factor in lattice confidence (config flag; not forwarded to the detector by ToDetectorOptions()) |
Example scenarios:
- Vulnerability becomes reachable after code refactoring → weight = 1.0
- Dependency removed, vulnerability no longer reachable → weight = 0.8
VEX Status Weights (R2)
Controls scoring for VEX statement changes. The detector classifies transitions in ClassifyVexTransition: only a fixed set of transitions are “meaningful”. Increased-risk transitions use vexFlipToAffectedWeight; decreased-risk transitions use vexFlipToNotAffectedWeight.
| Parameter | Default | Description |
|---|---|---|
vexFlipToAffectedWeight | 0.9 | Weight applied to increased-risk flips (e.g. → affected) |
vexFlipToNotAffectedWeight | 0.7 | Weight applied to decreased-risk flips (e.g. → not_affected, → fixed) |
vexFlipToFixedWeight | 0.6 | Config field; not forwarded to the detector by ToDetectorOptions() |
vexFlipToUnderInvestigationWeight | 0.3 | Config field; not forwarded to the detector by ToDetectorOptions() |
Implementation detail.
ToDetectorOptions()only mapsvexFlipToAffectedWeightandvexFlipToNotAffectedWeight. The detector applies the affected-weight to all increased-risk transitions and the not-affected-weight to all decreased-risk transitions; there is no separate “fixed” or “under_investigation” detector weight today. Transitions tounder_investigationare detected asNeutral(noteworthy but unscored).
Rationale:
- Increased-risk flips (to “affected”) carry the highest weight as they confirm exploitability.
- Decreased-risk flips (to “not_affected”/“fixed”) carry a lower weight as they indicate remediation.
Version Range Weights (R3)
Controls scoring for affected version range changes (inAffectedRange flips; unknown is skipped).
| Parameter | Default | Description |
|---|---|---|
rangeEntryWeight | 0.8 | Version entered affected range |
rangeExitWeight | 0.6 | Version exited affected range |
Intelligence Signal Weights (R4)
Controls scoring for external intelligence and policy changes. R4 can emit multiple changes (KEV, EPSS, policy) from a single comparison.
| Parameter | Default | Description |
|---|---|---|
kevAddedWeight | 1.0 | Vulnerability added to CISA KEV |
kevRemovedWeight | 0.5 | Vulnerability removed from CISA KEV |
epssThreshold | 0.5 | EPSS score threshold for crossing detection (Scanner config default; Policy config default is 0.1) |
epssThresholdWeight | 0.6 | Weight applied when the EPSS score crosses epssThreshold in either direction |
policyFlipWeight | 0.7 | Weight for a policy-decision change (Allow/Warn/Block ordering); neutral flips are not emitted |
Binary Hardening Weights (R5)
Controls scoring for binary hardening flag changes. These fields live on SmartDiffScoringConfig but are not forwarded to MaterialRiskChangeDetector via ToDetectorOptions(); they configure hardening handling elsewhere in the Smart-Diff pipeline.
| Parameter | Default | Description |
|---|---|---|
hardeningRegressionWeight | 0.7 | Weight for a hardening regression (security flag disabled) |
hardeningScoreThreshold | 0.2 | Minimum hardening score difference to trigger a finding |
includeHardeningFlags | true | Whether to include hardening flags in diff output |
Note. There is no
hardeningImprovementWeightorhardeningRegressionThresholdon the ScannerSmartDiffScoringConfig. Those fields exist only on the PolicySmartDiffPolicyScoringConfig(defaults0.3and0.1respectively).
Priority / Classification Factors
| Parameter | Default | Description |
|---|---|---|
kevBoost | 1.5 | Multiplier applied when a finding is in KEV |
minPriorityScore | 0.1 | Minimum priority score to emit a finding |
highPriorityThreshold | 0.7 | Threshold for “high priority” classification |
criticalPriorityThreshold | 0.9 | Threshold for “critical priority” classification |
These classification factors are stored on the config but are not part of
ToDetectorOptions(); the detector’sComputePriorityScoredoes not consult them today (see Score Calculation).
Validation
SmartDiffScoringConfig.Validate() enforces bounds and logical ordering, returning a SmartDiffScoringConfigValidation(bool IsValid, string[] Errors):
- All weights (
reachabilityFlipUpWeight,reachabilityFlipDownWeight,vexFlipToAffectedWeight,vexFlipToNotAffectedWeight,rangeEntryWeight,rangeExitWeight,kevAddedWeight,kevRemovedWeight,epssThresholdWeight,policyFlipWeight,hardeningRegressionWeight) must be in[0.0, 2.0]. - All thresholds (
epssThreshold,hardeningScoreThreshold,minPriorityScore,highPriorityThreshold,criticalPriorityThreshold) must be in[0.0, 1.0]. highPriorityThresholdmust be< criticalPriorityThreshold.minPriorityScoreshould be< highPriorityThreshold.
Presets
SmartDiffScoringConfig ships four named presets, retrievable via SmartDiffScoringConfig.GetPreset(name) (case-insensitive; aliases noted). Unknown names throw ArgumentException.
The previously documented “Strict” and “Lenient” presets do not exist on
SmartDiffScoringConfig. (The Policy-sideSmartDiffPolicyScoringConfighasDefaultandStrictonly — see Policy Integration.)
Default ("default")
Balanced detection — all defaults shown in Default Configuration.
SmartDiffScoringConfig.Default
Security-focused ("security-focused", alias "security")
Aggressive detection, lower thresholds. Recommended for security-critical applications.
SmartDiffScoringConfig.SecurityFocused
{
"name": "security-focused",
"reachabilityFlipUpWeight": 1.2,
"vexFlipToAffectedWeight": 1.0,
"kevAddedWeight": 1.5,
"epssThreshold": 0.3,
"epssThresholdWeight": 0.8,
"hardeningRegressionWeight": 0.9,
"hardeningScoreThreshold": 0.15,
"minPriorityScore": 0.05,
"highPriorityThreshold": 0.5,
"criticalPriorityThreshold": 0.8
}
Compliance-focused ("compliance-focused", alias "compliance")
Stricter thresholds for regulated environments.
SmartDiffScoringConfig.ComplianceFocused
{
"name": "compliance-focused",
"reachabilityFlipUpWeight": 1.0,
"vexFlipToAffectedWeight": 1.0,
"vexFlipToNotAffectedWeight": 0.9,
"kevAddedWeight": 2.0,
"epssThreshold": 0.2,
"policyFlipWeight": 1.0,
"hardeningRegressionWeight": 1.0,
"hardeningScoreThreshold": 0.1,
"minPriorityScore": 0.0,
"highPriorityThreshold": 0.4,
"criticalPriorityThreshold": 0.7
}
Developer-friendly ("developer-friendly", alias "developer")
Reduced noise; focus on actionable changes. Suitable for development/staging environments.
SmartDiffScoringConfig.DeveloperFriendly
{
"name": "developer-friendly",
"reachabilityFlipUpWeight": 0.8,
"vexFlipToAffectedWeight": 0.7,
"kevAddedWeight": 1.0,
"epssThreshold": 0.7,
"epssThresholdWeight": 0.4,
"hardeningRegressionWeight": 0.5,
"hardeningScoreThreshold": 0.3,
"minPriorityScore": 0.2,
"highPriorityThreshold": 0.8,
"criticalPriorityThreshold": 0.95
}
Configuring weights
To run the detector with a custom configuration, build a SmartDiffScoringConfig (or pick a preset), then construct a detector:
var config = SmartDiffScoringConfig.GetPreset("security-focused");
var validation = config.Validate();
if (!validation.IsValid)
{
throw new InvalidOperationException(string.Join("; ", validation.Errors));
}
// Either of these:
var detector = config.CreateDetector();
// or
var detector = new MaterialRiskChangeDetector(config.ToDetectorOptions());
The detector defaults to MaterialRiskChangeOptions.Default when no options are supplied.
Policy Integration
Smart-Diff scoring can be embedded in PolicyScoringConfig via the optional SmartDiff parameter. Note this uses the Policy type SmartDiffPolicyScoringConfig, whose field set differs from the Scanner config.
var config = new PolicyScoringConfig(
Version: "1.0",
SeverityWeights: severityWeights,
QuietPenalty: 0.1,
WarnPenalty: 0.5,
IgnorePenalty: 0.0,
TrustOverrides: trustOverrides,
ReachabilityBuckets: reachabilityBuckets,
UnknownConfidence: unknownConfig,
SmartDiff: new SmartDiffPolicyScoringConfig(
ReachabilityFlipUpWeight: 1.0,
VexFlipToAffectedWeight: 0.9,
KevAddedWeight: 1.2));
SmartDiffPolicyScoringConfig provides two static presets:
SmartDiffPolicyScoringConfig.Default // all defaults
SmartDiffPolicyScoringConfig.Strict // reachabilityFlipUpWeight 1.2, vexFlipToAffectedWeight 1.1,
// kevAddedWeight 1.5, hardeningRegressionWeight 1.0,
// hardeningRegressionThreshold 0.05
PolicyScoringConfig is bound from policy YAML (camelCase keys via PolicyScoringConfigBinder), not from environment variables.
Smart-Diff API surface
Correction. There is no
GET/PUT /api/v1/config/smart-diff/scoringendpoint. The Scanner WebService exposes the following Smart-Diff endpoints (src/Scanner/StellaOps.Scanner.WebService/Endpoints/SmartDiffEndpoints.cs), mounted under the API group’s/smart-diffprefix. None of them read or write scoring configuration.
| Method | Path | Auth policy | Purpose |
|---|---|---|---|
| GET | /smart-diff/scans/{scanId}/changes | scanner.scans.read or scanner:read | Material risk changes for a scan (optional minPriority filter) |
| GET | /smart-diff/scans/{scanId}/sarif | scanner.scans.read or scanner:read | Smart-Diff results as SARIF 2.1.0 (optional pretty) |
| GET | /smart-diff/{scanId}/vex-candidates | scanner.scans.read or scanner:read | VEX candidates resolved via scan metadata |
| POST | /smart-diff/{scanId}/vex-candidates/review | scanner.scans.write | Review a scan-scoped VEX candidate |
| GET | /smart-diff/images/{digest}/candidates | scanner.scans.read or scanner:read | VEX candidates for an image digest |
| GET | /smart-diff/candidates/{candidateId} | scanner.scans.read or scanner:read | A specific VEX candidate |
| POST | /smart-diff/candidates/{candidateId}/review | scanner.scans.write | Review a VEX candidate (accept/reject/defer) |
Read endpoints accept either the scanner.scans.read scope or the canonical scanner:read scope (StellaOpsScopes.ScannerRead). Review endpoints require scanner.scans.write and are audited.
GET /smart-diff/scans/{scanId}/changes returns each finding’s priorityScore plus the list of detected changes (rule, change type, direction, reason, before/after value, and per-change weight).
Score Calculation
Correction. The priority score is not computed as
base_severity × Σ(change_weight × rule_match). The per-changeweightvalues configured above are emitted on eachDetectedChange(and surfaced in the API/SARIF output), but the finding-levelpriorityScoreis computed byMaterialRiskChangeDetector.ComputePriorityScoreusing a fixed additive point scheme over the current risk state (per the Smart-Diff advisory A9), independent of the configured weights:
priority_score =
(current.kev ? +1000 : 0)
+ (current.reachable == true ? +500 : 0)
+ (R3 increased AND in affected range ? +200 : 0)
+ (R2 increased AND vex == affected ? +150 : 0)
+ round(clamp(current.epss, 0..1) * 100) // 0..100
+ (current.policyDecision == Block ? +300
: current.policyDecision == Warn ? +100 : 0)
The score is 0 when there is no material change. It is an integer point total (stored as NUMERIC(6,4) in the database), not a 0–1 normalized value, so the previous “capped at 1.0” claim does not apply.
Example Calculation
Given a finding whose current state is kev = true, reachable = true, epss = 0.42, policyDecision = Block, with a detected R2 flip to affected and an R3 flip into the affected range:
priority_score = 1000 (kev)
+ 500 (reachable)
+ 200 (range entry)
+ 150 (vex → affected)
+ 42 (round(0.42 * 100))
+ 300 (policy Block)
= 2192
Tuning Recommendations
The detector’s priorityScore is driven by the fixed point scheme above, so tuning the configured weights changes the per-change weight values (visible in the API/SARIF output and used by consumers that aggregate weights), not the finding-level priorityScore directly. Choose a preset close to your posture, then adjust:
For CI/CD pipelines (security-leaning)
Start from SmartDiffScoringConfig.SecurityFocused, then raise KEV/EPSS/hardening sensitivity:
{
"kevAddedWeight": 1.5,
"hardeningRegressionWeight": 1.2,
"epssThreshold": 0.3
}
Focus on surfacing known-exploited vulnerabilities and hardening regressions.
For alert-fatigue reduction (developer-leaning)
Start from SmartDiffScoringConfig.DeveloperFriendly, then lower weights for positive changes:
{
"reachabilityFlipDownWeight": 0.3,
"vexFlipToNotAffectedWeight": 0.2,
"rangeExitWeight": 0.2
}
For compliance focus
Start from SmartDiffScoringConfig.ComplianceFocused, then raise regulatory-relevant weights:
{
"kevAddedWeight": 2.0,
"vexFlipToAffectedWeight": 1.2,
"hardeningScoreThreshold": 0.05
}
Validate any custom config with
Validate()— weights must stay within[0.0, 2.0]and thresholds within[0.0, 1.0].
Monitoring and Metrics
Smart-Diff changes are persisted to scanner.material_risk_changes (src/Scanner/__Libraries/StellaOps.Scanner.Storage/Postgres/Migrations/005_smart_diff_tables.sql). There is no top-level change_type column — the individual detected changes (and their rule type) live inside the changes JSONB array; the row carries a single priority_score and detected_at timestamp.
-- Average priority score and volume over the last 30 days
SELECT
AVG(priority_score) AS avg_priority_score,
COUNT(*) AS change_count
FROM scanner.material_risk_changes
WHERE has_material_change = TRUE
AND detected_at > now() - interval '30 days';
-- Breakdown by detection rule (unnesting the JSONB changes array)
SELECT
c->>'Rule' AS rule,
COUNT(*) AS occurrences,
AVG(mc.priority_score) AS avg_priority_score
FROM scanner.material_risk_changes mc
CROSS JOIN LATERAL jsonb_array_elements(mc.changes) AS c
WHERE mc.has_material_change = TRUE
AND mc.detected_at > now() - interval '30 days'
GROUP BY c->>'Rule'
ORDER BY occurrences DESC;
Adjust the JSONB key casing (
Rule,ChangeType,Direction,Weight, …) to match the storedDetectedChangeserialization in your deployment.
