save progress
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user