save progress

This commit is contained in:
StellaOps Bot
2025-12-18 09:10:36 +02:00
parent b4235c134c
commit 28823a8960
169 changed files with 11995 additions and 449 deletions

View File

@@ -23,7 +23,7 @@ public sealed class MaterialRiskChangeDetector
RiskStateSnapshot previous,
RiskStateSnapshot current)
{
if (previous.FindingKey != current.FindingKey)
if (!FindingKeysMatch(previous.FindingKey, current.FindingKey))
throw new ArgumentException("FindingKey mismatch between snapshots");
var changes = new List<DetectedChange>();
@@ -56,6 +56,11 @@ public sealed class MaterialRiskChangeDetector
CurrentStateHash: current.ComputeStateHash());
}
public MaterialRiskChangeResult DetectChanges(
RiskStateSnapshot previous,
RiskStateSnapshot current)
=> Compare(previous, current);
/// <summary>
/// R1: Reachability Flip - reachable changes false→true or true→false
/// </summary>
@@ -286,40 +291,79 @@ public sealed class MaterialRiskChangeDetector
if (changes.Count == 0)
return 0;
// Sum weighted changes
var weightedSum = 0.0;
foreach (var change in changes)
// Priority scoring per Smart-Diff advisory (A9):
// + 1000 if new.kev
// + 500 if new.reachable
// + 200 if RANGE_FLIP to affected
// + 150 if VEX_FLIP to affected
// + 0..100 based on EPSS (epss * 100)
// + policy weight: +300 if BLOCK, +100 if WARN
var score = 0;
if (current.Kev)
score += 1000;
if (current.Reachable == true)
score += 500;
if (changes.Any(c => c.Rule == DetectionRule.R3_RangeBoundary
&& c.Direction == RiskDirection.Increased
&& current.InAffectedRange == true))
{
var directionMultiplier = change.Direction switch
{
RiskDirection.Increased => 1.0,
RiskDirection.Decreased => -0.5,
RiskDirection.Neutral => 0.0,
_ => 0.0
};
weightedSum += change.Weight * directionMultiplier;
score += 200;
}
// Base severity from EPSS or default
var baseSeverity = current.EpssScore ?? 0.5;
// KEV boost
var kevBoost = current.Kev ? 1.5 : 1.0;
// Confidence factor from lattice state
var confidence = current.LatticeState switch
if (changes.Any(c => c.Rule == DetectionRule.R2_VexFlip
&& c.Direction == RiskDirection.Increased
&& current.VexStatus == VexStatusType.Affected))
{
"certain_reachable" => 1.0,
"likely_reachable" => 0.9,
"uncertain" => 0.7,
"likely_unreachable" => 0.5,
"certain_unreachable" => 0.3,
_ => 0.7
score += 150;
}
if (current.EpssScore is not null)
{
var epss = Math.Clamp(current.EpssScore.Value, 0.0, 1.0);
score += (int)Math.Round(epss * 100.0, 0, MidpointRounding.AwayFromZero);
}
score += current.PolicyDecision switch
{
PolicyDecisionType.Block => 300,
PolicyDecisionType.Warn => 100,
_ => 0
};
var score = baseSeverity * weightedSum * kevBoost * confidence;
return score;
}
// Clamp to [-1, 1]
return Math.Clamp(score, -1.0, 1.0);
private static bool FindingKeysMatch(FindingKey previous, FindingKey current)
{
if (!StringComparer.Ordinal.Equals(previous.VulnId, current.VulnId))
{
return false;
}
var prevPurl = NormalizePurlForComparison(previous.ComponentPurl);
var currPurl = NormalizePurlForComparison(current.ComponentPurl);
return StringComparer.Ordinal.Equals(prevPurl, currPurl);
}
private static string NormalizePurlForComparison(string purl)
{
// Strip the version segment (`@<version>`) while preserving qualifiers (`?`) and subpath (`#`).
var atIndex = purl.IndexOf('@');
if (atIndex < 0)
{
return purl;
}
var endIndex = purl.IndexOfAny(['?', '#'], atIndex);
if (endIndex < 0)
{
endIndex = purl.Length;
}
return purl.Remove(atIndex, endIndex - atIndex);
}
}

View File

@@ -147,7 +147,7 @@ public sealed class MaterialRiskChangeOptions
/// <summary>
/// EPSS score threshold for R4 detection.
/// </summary>
public double EpssThreshold { get; init; } = 0.5;
public double EpssThreshold { get; init; } = 0.1;
/// <summary>
/// Weight for policy decision flip.

View File

@@ -46,7 +46,7 @@ public sealed record RiskStateSnapshot(
builder.Append(PolicyDecision?.ToString() ?? "null");
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
return Convert.ToHexString(hash).ToLowerInvariant();
return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
}
}

View File

@@ -98,9 +98,9 @@ public sealed record SarifResult(
[property: JsonPropertyName("level")] SarifLevel Level,
[property: JsonPropertyName("message")] SarifMessage Message,
[property: JsonPropertyName("locations")] ImmutableArray<SarifLocation>? Locations = null,
[property: JsonPropertyName("fingerprints")] ImmutableDictionary<string, string>? Fingerprints = null,
[property: JsonPropertyName("partialFingerprints")] ImmutableDictionary<string, string>? PartialFingerprints = null,
[property: JsonPropertyName("properties")] ImmutableDictionary<string, object>? Properties = null);
[property: JsonPropertyName("fingerprints")] ImmutableSortedDictionary<string, string>? Fingerprints = null,
[property: JsonPropertyName("partialFingerprints")] ImmutableSortedDictionary<string, string>? PartialFingerprints = null,
[property: JsonPropertyName("properties")] ImmutableSortedDictionary<string, object>? Properties = null);
/// <summary>
/// Location of a result.
@@ -157,7 +157,7 @@ public sealed record SarifInvocation(
public sealed record SarifArtifact(
[property: JsonPropertyName("location")] SarifArtifactLocation Location,
[property: JsonPropertyName("mimeType")] string? MimeType = null,
[property: JsonPropertyName("hashes")] ImmutableDictionary<string, string>? Hashes = null);
[property: JsonPropertyName("hashes")] ImmutableSortedDictionary<string, string>? Hashes = null);
/// <summary>
/// Version control information.

View File

@@ -293,10 +293,10 @@ public sealed class SarifOutputGenerator
Level: level,
Message: new SarifMessage(message),
Locations: locations,
Fingerprints: ImmutableDictionary.CreateRange(new[]
Fingerprints: ImmutableSortedDictionary.CreateRange(StringComparer.Ordinal, new[]
{
KeyValuePair.Create("purl", change.ComponentPurl),
KeyValuePair.Create("vulnId", change.VulnId),
KeyValuePair.Create("purl", change.ComponentPurl)
}));
}
@@ -322,10 +322,10 @@ public sealed class SarifOutputGenerator
RuleId: "SDIFF003",
Level: SarifLevel.Note,
Message: new SarifMessage(message),
Fingerprints: ImmutableDictionary.CreateRange(new[]
Fingerprints: ImmutableSortedDictionary.CreateRange(StringComparer.Ordinal, new[]
{
KeyValuePair.Create("purl", candidate.ComponentPurl),
KeyValuePair.Create("vulnId", candidate.VulnId),
KeyValuePair.Create("purl", candidate.ComponentPurl)
}));
}
@@ -338,10 +338,10 @@ public sealed class SarifOutputGenerator
RuleId: "SDIFF004",
Level: SarifLevel.Warning,
Message: new SarifMessage(message),
Fingerprints: ImmutableDictionary.CreateRange(new[]
Fingerprints: ImmutableSortedDictionary.CreateRange(StringComparer.Ordinal, new[]
{
KeyValuePair.Create("purl", change.ComponentPurl),
KeyValuePair.Create("vulnId", change.VulnId),
KeyValuePair.Create("purl", change.ComponentPurl)
}));
}
@@ -350,15 +350,15 @@ public sealed class SarifOutputGenerator
return new SarifInvocation(
ExecutionSuccessful: true,
StartTimeUtc: input.ScanTime,
EndTimeUtc: DateTimeOffset.UtcNow);
EndTimeUtc: null);
}
private static ImmutableArray<SarifArtifact> CreateArtifacts(SmartDiffSarifInput input)
{
var artifacts = new List<SarifArtifact>();
// Collect unique file paths from results
var paths = new HashSet<string>();
// Collect unique file paths from results (sorted for determinism).
var paths = new SortedSet<string>(StringComparer.Ordinal);
foreach (var change in input.MaterialChanges)
{