consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -9,10 +9,9 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
@@ -23,12 +22,6 @@ namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
/// </summary>
|
||||
internal static class DeltaCompareEndpoints
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps delta compare endpoints.
|
||||
/// </summary>
|
||||
@@ -190,6 +183,20 @@ public interface IDeltaCompareService
|
||||
/// </summary>
|
||||
public sealed class DeltaCompareService : IDeltaCompareService
|
||||
{
|
||||
private static readonly (string Ecosystem, string Name, string License)[] ComponentTemplates =
|
||||
[
|
||||
("npm", "axios", "MIT"),
|
||||
("npm", "lodash", "MIT"),
|
||||
("maven", "org.apache.logging.log4j/log4j-core", "Apache-2.0"),
|
||||
("maven", "org.springframework/spring-core", "Apache-2.0"),
|
||||
("pypi", "requests", "Apache-2.0"),
|
||||
("nuget", "Newtonsoft.Json", "MIT"),
|
||||
("golang", "golang.org/x/net", "BSD-3-Clause"),
|
||||
("cargo", "tokio", "MIT"),
|
||||
];
|
||||
|
||||
private static readonly string[] OrderedSeverities = ["critical", "high", "medium", "low", "unknown"];
|
||||
private readonly ConcurrentDictionary<string, DeltaCompareResponseDto> _comparisons = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public DeltaCompareService(TimeProvider timeProvider)
|
||||
@@ -199,95 +206,552 @@ public sealed class DeltaCompareService : IDeltaCompareService
|
||||
|
||||
public Task<DeltaCompareResponseDto> CompareAsync(DeltaCompareRequestDto request, CancellationToken ct = default)
|
||||
{
|
||||
// Compute deterministic comparison ID
|
||||
ct.ThrowIfCancellationRequested();
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var comparisonId = ComputeComparisonId(request.BaseDigest, request.TargetDigest);
|
||||
|
||||
// In a full implementation, this would:
|
||||
// 1. Load both snapshots from storage
|
||||
// 2. Compare vulnerabilities and components
|
||||
// 3. Compute policy diffs
|
||||
// For now, return a structured response
|
||||
|
||||
var baseSummary = CreateSnapshotSummary(request.BaseDigest, "Block");
|
||||
var targetSummary = CreateSnapshotSummary(request.TargetDigest, "Ship");
|
||||
|
||||
var response = new DeltaCompareResponseDto
|
||||
if (!_comparisons.TryGetValue(comparisonId, out var fullComparison))
|
||||
{
|
||||
Base = baseSummary,
|
||||
Target = targetSummary,
|
||||
Summary = new DeltaChangeSummaryDto
|
||||
{
|
||||
Added = 0,
|
||||
Removed = 0,
|
||||
Modified = 0,
|
||||
Unchanged = 0,
|
||||
NetVulnerabilityChange = 0,
|
||||
NetComponentChange = 0,
|
||||
SeverityChanges = new DeltaSeverityChangesDto(),
|
||||
VerdictChanged = baseSummary.PolicyVerdict != targetSummary.PolicyVerdict,
|
||||
RiskDirection = "unchanged"
|
||||
},
|
||||
Vulnerabilities = request.IncludeVulnerabilities ? [] : null,
|
||||
Components = request.IncludeComponents ? [] : null,
|
||||
PolicyDiff = request.IncludePolicyDiff
|
||||
? new DeltaPolicyDiffDto
|
||||
{
|
||||
BaseVerdict = baseSummary.PolicyVerdict ?? "Unknown",
|
||||
TargetVerdict = targetSummary.PolicyVerdict ?? "Unknown",
|
||||
VerdictChanged = baseSummary.PolicyVerdict != targetSummary.PolicyVerdict,
|
||||
BlockToShipCount = 0,
|
||||
ShipToBlockCount = 0
|
||||
}
|
||||
: null,
|
||||
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||
ComparisonId = comparisonId
|
||||
};
|
||||
fullComparison = BuildComparison(request.BaseDigest.Trim(), request.TargetDigest.Trim(), comparisonId);
|
||||
_comparisons[comparisonId] = fullComparison;
|
||||
}
|
||||
|
||||
return Task.FromResult(response);
|
||||
return Task.FromResult(ProjectComparison(fullComparison, request));
|
||||
}
|
||||
|
||||
public Task<QuickDiffSummaryDto> GetQuickDiffAsync(string baseDigest, string targetDigest, CancellationToken ct = default)
|
||||
public async Task<QuickDiffSummaryDto> GetQuickDiffAsync(string baseDigest, string targetDigest, CancellationToken ct = default)
|
||||
{
|
||||
var summary = new QuickDiffSummaryDto
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var comparisonId = ComputeComparisonId(baseDigest, targetDigest);
|
||||
if (!_comparisons.TryGetValue(comparisonId, out var comparison))
|
||||
{
|
||||
comparison = await CompareAsync(
|
||||
new DeltaCompareRequestDto
|
||||
{
|
||||
BaseDigest = baseDigest,
|
||||
TargetDigest = targetDigest,
|
||||
IncludeComponents = true,
|
||||
IncludePolicyDiff = true,
|
||||
IncludeVulnerabilities = true,
|
||||
IncludeUnchanged = true
|
||||
},
|
||||
ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var netBlockingChange = (comparison.Target.SeverityCounts.Critical + comparison.Target.SeverityCounts.High)
|
||||
- (comparison.Base.SeverityCounts.Critical + comparison.Base.SeverityCounts.High);
|
||||
|
||||
return new QuickDiffSummaryDto
|
||||
{
|
||||
BaseDigest = baseDigest,
|
||||
TargetDigest = targetDigest,
|
||||
CanShip = true,
|
||||
RiskDirection = "unchanged",
|
||||
NetBlockingChange = 0,
|
||||
CriticalAdded = 0,
|
||||
CriticalRemoved = 0,
|
||||
HighAdded = 0,
|
||||
HighRemoved = 0,
|
||||
Summary = "No material changes detected"
|
||||
CanShip = !string.Equals(comparison.Target.PolicyVerdict, "Block", StringComparison.OrdinalIgnoreCase),
|
||||
RiskDirection = comparison.Summary.RiskDirection,
|
||||
NetBlockingChange = netBlockingChange,
|
||||
CriticalAdded = comparison.Summary.SeverityChanges.CriticalAdded,
|
||||
CriticalRemoved = comparison.Summary.SeverityChanges.CriticalRemoved,
|
||||
HighAdded = comparison.Summary.SeverityChanges.HighAdded,
|
||||
HighRemoved = comparison.Summary.SeverityChanges.HighRemoved,
|
||||
Summary = comparison.Summary.RiskDirection switch
|
||||
{
|
||||
"degraded" => "Risk increased between snapshots.",
|
||||
"improved" => "Risk reduced between snapshots.",
|
||||
_ => "Risk profile is unchanged."
|
||||
}
|
||||
};
|
||||
|
||||
return Task.FromResult(summary);
|
||||
}
|
||||
|
||||
public Task<DeltaCompareResponseDto?> GetComparisonAsync(string comparisonId, CancellationToken ct = default)
|
||||
{
|
||||
// In a full implementation, this would retrieve from cache/storage
|
||||
return Task.FromResult<DeltaCompareResponseDto?>(null);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (string.IsNullOrWhiteSpace(comparisonId))
|
||||
{
|
||||
return Task.FromResult<DeltaCompareResponseDto?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult(_comparisons.TryGetValue(comparisonId.Trim(), out var comparison) ? comparison : null);
|
||||
}
|
||||
|
||||
private DeltaSnapshotSummaryDto CreateSnapshotSummary(string digest, string verdict)
|
||||
private DeltaCompareResponseDto BuildComparison(string baseDigest, string targetDigest, string comparisonId)
|
||||
{
|
||||
return new DeltaSnapshotSummaryDto
|
||||
var baseSnapshot = BuildSnapshot(baseDigest);
|
||||
var targetSnapshot = BuildSnapshot(targetDigest);
|
||||
|
||||
var vulnerabilities = BuildVulnerabilityDiffs(baseSnapshot, targetSnapshot, includeUnchanged: true);
|
||||
var components = BuildComponentDiffs(baseSnapshot, targetSnapshot, includeUnchanged: true);
|
||||
var severityChanges = BuildSeverityChanges(vulnerabilities);
|
||||
var policyDiff = BuildPolicyDiff(baseSnapshot, targetSnapshot, vulnerabilities);
|
||||
|
||||
var riskScore =
|
||||
(severityChanges.CriticalAdded - severityChanges.CriticalRemoved) * 4 +
|
||||
(severityChanges.HighAdded - severityChanges.HighRemoved) * 3 +
|
||||
(severityChanges.MediumAdded - severityChanges.MediumRemoved) * 2 +
|
||||
(severityChanges.LowAdded - severityChanges.LowRemoved) +
|
||||
((policyDiff.ShipToBlockCount - policyDiff.BlockToShipCount) * 5);
|
||||
|
||||
return new DeltaCompareResponseDto
|
||||
{
|
||||
Digest = digest,
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
ComponentCount = 0,
|
||||
VulnerabilityCount = 0,
|
||||
SeverityCounts = new DeltaSeverityCountsDto(),
|
||||
PolicyVerdict = verdict
|
||||
Base = BuildSummary(baseSnapshot),
|
||||
Target = BuildSummary(targetSnapshot),
|
||||
Summary = new DeltaChangeSummaryDto
|
||||
{
|
||||
Added = vulnerabilities.Count(v => v.ChangeType.Equals("Added", StringComparison.Ordinal)),
|
||||
Removed = vulnerabilities.Count(v => v.ChangeType.Equals("Removed", StringComparison.Ordinal)),
|
||||
Modified = vulnerabilities.Count(v => v.ChangeType.Equals("Modified", StringComparison.Ordinal)),
|
||||
Unchanged = vulnerabilities.Count(v => v.ChangeType.Equals("Unchanged", StringComparison.Ordinal)),
|
||||
NetVulnerabilityChange = targetSnapshot.Vulnerabilities.Count - baseSnapshot.Vulnerabilities.Count,
|
||||
NetComponentChange = targetSnapshot.Components.Count - baseSnapshot.Components.Count,
|
||||
SeverityChanges = severityChanges,
|
||||
VerdictChanged = !string.Equals(baseSnapshot.PolicyVerdict, targetSnapshot.PolicyVerdict, StringComparison.OrdinalIgnoreCase),
|
||||
RiskDirection = riskScore > 0 ? "degraded" : riskScore < 0 ? "improved" : "unchanged"
|
||||
},
|
||||
Vulnerabilities = vulnerabilities,
|
||||
Components = components,
|
||||
PolicyDiff = policyDiff,
|
||||
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||
ComparisonId = comparisonId
|
||||
};
|
||||
}
|
||||
|
||||
private DeltaCompareResponseDto ProjectComparison(DeltaCompareResponseDto full, DeltaCompareRequestDto request)
|
||||
{
|
||||
var changeTypeFilter = request.ChangeTypes?
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(static value => value.Trim())
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var severityFilter = request.Severities?
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(static value => value.Trim())
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var filteredVulnerabilities = (full.Vulnerabilities ?? [])
|
||||
.Where(v => request.IncludeUnchanged || !v.ChangeType.Equals("Unchanged", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(v => changeTypeFilter is null || changeTypeFilter.Contains(v.ChangeType))
|
||||
.Where(v => severityFilter is null || severityFilter.Contains(EffectiveSeverity(v)))
|
||||
.OrderBy(v => ChangeTypeOrder(v.ChangeType))
|
||||
.ThenBy(v => v.VulnId, StringComparer.Ordinal)
|
||||
.ThenBy(v => v.Purl, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var filteredComponents = (full.Components ?? [])
|
||||
.Where(c => request.IncludeUnchanged || !c.ChangeType.Equals("Unchanged", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(c => ChangeTypeOrder(c.ChangeType))
|
||||
.ThenBy(c => c.Purl, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return full with
|
||||
{
|
||||
Summary = full.Summary with
|
||||
{
|
||||
Added = filteredVulnerabilities.Count(v => v.ChangeType.Equals("Added", StringComparison.OrdinalIgnoreCase)),
|
||||
Removed = filteredVulnerabilities.Count(v => v.ChangeType.Equals("Removed", StringComparison.OrdinalIgnoreCase)),
|
||||
Modified = filteredVulnerabilities.Count(v => v.ChangeType.Equals("Modified", StringComparison.OrdinalIgnoreCase)),
|
||||
Unchanged = filteredVulnerabilities.Count(v => v.ChangeType.Equals("Unchanged", StringComparison.OrdinalIgnoreCase)),
|
||||
SeverityChanges = BuildSeverityChanges(filteredVulnerabilities),
|
||||
},
|
||||
Vulnerabilities = request.IncludeVulnerabilities ? filteredVulnerabilities : null,
|
||||
Components = request.IncludeComponents ? filteredComponents : null,
|
||||
PolicyDiff = request.IncludePolicyDiff ? full.PolicyDiff : null
|
||||
};
|
||||
}
|
||||
|
||||
private Snapshot BuildSnapshot(string digest)
|
||||
{
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(digest));
|
||||
var createdAt = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)
|
||||
.AddSeconds(BitConverter.ToUInt32(hash.AsSpan(0, sizeof(uint))) % (365 * 24 * 60 * 60));
|
||||
var componentCount = 4 + (hash[1] % 3);
|
||||
var components = new Dictionary<string, SnapshotComponent>(StringComparer.Ordinal);
|
||||
|
||||
for (var i = 0; i < componentCount; i++)
|
||||
{
|
||||
var template = ComponentTemplates[(hash[(i * 5 + 7) % hash.Length] + i) % ComponentTemplates.Length];
|
||||
var version = $"{1 + (hash[(i * 3 + 9) % hash.Length] % 3)}.{hash[(i * 7 + 13) % hash.Length] % 10}.{hash[(i * 11 + 17) % hash.Length] % 20}";
|
||||
var purl = template.Ecosystem switch
|
||||
{
|
||||
"rpm" or "deb" => $"pkg:generic/{template.Name}@{version}",
|
||||
_ => $"pkg:{template.Ecosystem}/{template.Name}@{version}"
|
||||
};
|
||||
|
||||
components[purl] = new SnapshotComponent(purl, version, template.License);
|
||||
}
|
||||
|
||||
var vulnerabilities = new List<SnapshotVulnerability>();
|
||||
foreach (var (component, index) in components.Values.OrderBy(v => v.Purl, StringComparer.Ordinal).Select((value, idx) => (value, idx)))
|
||||
{
|
||||
var vulnerabilityCount = 1 + (hash[(index + 19) % hash.Length] % 2);
|
||||
for (var slot = 0; slot < vulnerabilityCount; slot++)
|
||||
{
|
||||
var cve = $"CVE-{2024 + (hash[(index + slot + 3) % hash.Length] % 3)}-{1000 + (((hash[(index * 3 + slot + 5) % hash.Length] << 8) + hash[(index * 3 + slot + 6) % hash.Length]) % 8000):D4}";
|
||||
var severity = OrderedSeverities[hash[(index * 3 + slot + 23) % hash.Length] % 4];
|
||||
var reachability = (hash[(index * 3 + slot + 29) % hash.Length] % 3) switch
|
||||
{
|
||||
0 => "reachable",
|
||||
1 => "likely",
|
||||
_ => "unreachable"
|
||||
};
|
||||
var verdict = severity is "critical" or "high" ? "Block" : severity == "medium" ? "Warn" : "Ship";
|
||||
vulnerabilities.Add(new SnapshotVulnerability(cve, component.Purl, severity, reachability, verdict, IncrementPatch(component.Version)));
|
||||
}
|
||||
}
|
||||
|
||||
var distinctVulnerabilities = vulnerabilities
|
||||
.DistinctBy(v => $"{v.VulnId}|{v.Purl}", StringComparer.Ordinal)
|
||||
.OrderBy(v => v.VulnId, StringComparer.Ordinal)
|
||||
.ThenBy(v => v.Purl, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
var hasBlocking = distinctVulnerabilities.Any(v => v.Severity is "critical" or "high");
|
||||
var hasMedium = distinctVulnerabilities.Any(v => v.Severity == "medium");
|
||||
var policyVerdict = hasBlocking ? "Block" : hasMedium ? "Warn" : "Ship";
|
||||
|
||||
return new Snapshot(digest, createdAt, components.Values.OrderBy(v => v.Purl, StringComparer.Ordinal).ToList(), distinctVulnerabilities, policyVerdict);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<DeltaVulnerabilityDto> BuildVulnerabilityDiffs(Snapshot baseline, Snapshot target, bool includeUnchanged)
|
||||
{
|
||||
var baseIndex = baseline.Vulnerabilities.ToDictionary(v => $"{v.VulnId}|{v.Purl}", StringComparer.Ordinal);
|
||||
var targetIndex = target.Vulnerabilities.ToDictionary(v => $"{v.VulnId}|{v.Purl}", StringComparer.Ordinal);
|
||||
var keys = baseIndex.Keys.Union(targetIndex.Keys, StringComparer.Ordinal).OrderBy(v => v, StringComparer.Ordinal);
|
||||
var results = new List<DeltaVulnerabilityDto>();
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
baseIndex.TryGetValue(key, out var before);
|
||||
targetIndex.TryGetValue(key, out var after);
|
||||
if (before is null && after is not null)
|
||||
{
|
||||
results.Add(new DeltaVulnerabilityDto
|
||||
{
|
||||
VulnId = after.VulnId,
|
||||
Purl = after.Purl,
|
||||
ChangeType = "Added",
|
||||
Severity = after.Severity,
|
||||
Reachability = after.Reachability,
|
||||
Verdict = after.Verdict,
|
||||
FixedVersion = after.FixedVersion
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (before is not null && after is null)
|
||||
{
|
||||
results.Add(new DeltaVulnerabilityDto
|
||||
{
|
||||
VulnId = before.VulnId,
|
||||
Purl = before.Purl,
|
||||
ChangeType = "Removed",
|
||||
Severity = "unknown",
|
||||
PreviousSeverity = before.Severity,
|
||||
PreviousReachability = before.Reachability,
|
||||
PreviousVerdict = before.Verdict,
|
||||
FixedVersion = before.FixedVersion
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (before is null || after is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fields = new List<DeltaFieldChangeDto>();
|
||||
AddFieldChange(fields, "severity", before.Severity, after.Severity);
|
||||
AddFieldChange(fields, "reachability", before.Reachability, after.Reachability);
|
||||
AddFieldChange(fields, "verdict", before.Verdict, after.Verdict);
|
||||
AddFieldChange(fields, "fixedVersion", before.FixedVersion, after.FixedVersion);
|
||||
|
||||
if (fields.Count == 0)
|
||||
{
|
||||
if (!includeUnchanged)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
results.Add(new DeltaVulnerabilityDto
|
||||
{
|
||||
VulnId = after.VulnId,
|
||||
Purl = after.Purl,
|
||||
ChangeType = "Unchanged",
|
||||
Severity = after.Severity,
|
||||
Reachability = after.Reachability,
|
||||
Verdict = after.Verdict,
|
||||
FixedVersion = after.FixedVersion
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
results.Add(new DeltaVulnerabilityDto
|
||||
{
|
||||
VulnId = after.VulnId,
|
||||
Purl = after.Purl,
|
||||
ChangeType = "Modified",
|
||||
Severity = after.Severity,
|
||||
PreviousSeverity = before.Severity,
|
||||
Reachability = after.Reachability,
|
||||
PreviousReachability = before.Reachability,
|
||||
Verdict = after.Verdict,
|
||||
PreviousVerdict = before.Verdict,
|
||||
FixedVersion = after.FixedVersion,
|
||||
FieldChanges = fields
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<DeltaComponentDto> BuildComponentDiffs(Snapshot baseline, Snapshot target, bool includeUnchanged)
|
||||
{
|
||||
var baseIndex = baseline.Components.ToDictionary(v => v.Purl, StringComparer.Ordinal);
|
||||
var targetIndex = target.Components.ToDictionary(v => v.Purl, StringComparer.Ordinal);
|
||||
var baseVulnCount = baseline.Vulnerabilities.GroupBy(v => v.Purl, StringComparer.Ordinal).ToDictionary(g => g.Key, g => g.Count(), StringComparer.Ordinal);
|
||||
var targetVulnCount = target.Vulnerabilities.GroupBy(v => v.Purl, StringComparer.Ordinal).ToDictionary(g => g.Key, g => g.Count(), StringComparer.Ordinal);
|
||||
var keys = baseIndex.Keys.Union(targetIndex.Keys, StringComparer.Ordinal).OrderBy(v => v, StringComparer.Ordinal);
|
||||
var results = new List<DeltaComponentDto>();
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
baseIndex.TryGetValue(key, out var before);
|
||||
targetIndex.TryGetValue(key, out var after);
|
||||
var beforeVuln = baseVulnCount.TryGetValue(key, out var bc) ? bc : 0;
|
||||
var afterVuln = targetVulnCount.TryGetValue(key, out var ac) ? ac : 0;
|
||||
if (before is null && after is not null)
|
||||
{
|
||||
results.Add(new DeltaComponentDto
|
||||
{
|
||||
Purl = key,
|
||||
ChangeType = "Added",
|
||||
CurrentVersion = after.Version,
|
||||
VulnerabilitiesInBase = beforeVuln,
|
||||
VulnerabilitiesInTarget = afterVuln,
|
||||
License = after.License
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (before is not null && after is null)
|
||||
{
|
||||
results.Add(new DeltaComponentDto
|
||||
{
|
||||
Purl = key,
|
||||
ChangeType = "Removed",
|
||||
PreviousVersion = before.Version,
|
||||
VulnerabilitiesInBase = beforeVuln,
|
||||
VulnerabilitiesInTarget = afterVuln,
|
||||
License = before.License
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (before is null || after is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(before.Version, after.Version, StringComparison.Ordinal))
|
||||
{
|
||||
results.Add(new DeltaComponentDto
|
||||
{
|
||||
Purl = key,
|
||||
ChangeType = "VersionChanged",
|
||||
PreviousVersion = before.Version,
|
||||
CurrentVersion = after.Version,
|
||||
VulnerabilitiesInBase = beforeVuln,
|
||||
VulnerabilitiesInTarget = afterVuln,
|
||||
License = after.License
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!includeUnchanged)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
results.Add(new DeltaComponentDto
|
||||
{
|
||||
Purl = key,
|
||||
ChangeType = "Unchanged",
|
||||
PreviousVersion = before.Version,
|
||||
CurrentVersion = after.Version,
|
||||
VulnerabilitiesInBase = beforeVuln,
|
||||
VulnerabilitiesInTarget = afterVuln,
|
||||
License = after.License
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static DeltaPolicyDiffDto BuildPolicyDiff(Snapshot baseline, Snapshot target, IReadOnlyList<DeltaVulnerabilityDto> vulnerabilities)
|
||||
{
|
||||
return new DeltaPolicyDiffDto
|
||||
{
|
||||
BaseVerdict = baseline.PolicyVerdict,
|
||||
TargetVerdict = target.PolicyVerdict,
|
||||
VerdictChanged = !string.Equals(baseline.PolicyVerdict, target.PolicyVerdict, StringComparison.OrdinalIgnoreCase),
|
||||
BlockToShipCount = vulnerabilities.Count(v => string.Equals(v.PreviousVerdict, "Block", StringComparison.OrdinalIgnoreCase) && string.Equals(v.Verdict, "Ship", StringComparison.OrdinalIgnoreCase)),
|
||||
ShipToBlockCount = vulnerabilities.Count(v => string.Equals(v.PreviousVerdict, "Ship", StringComparison.OrdinalIgnoreCase) && string.Equals(v.Verdict, "Block", StringComparison.OrdinalIgnoreCase)),
|
||||
WouldPassIf = vulnerabilities
|
||||
.Where(v => string.Equals(v.Verdict, "Block", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(v => $"Mitigate {v.VulnId} in {v.Purl}")
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(v => v, StringComparer.Ordinal)
|
||||
.Take(3)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static DeltaSeverityChangesDto BuildSeverityChanges(IReadOnlyList<DeltaVulnerabilityDto> vulnerabilities)
|
||||
{
|
||||
var criticalAdded = 0;
|
||||
var criticalRemoved = 0;
|
||||
var highAdded = 0;
|
||||
var highRemoved = 0;
|
||||
var mediumAdded = 0;
|
||||
var mediumRemoved = 0;
|
||||
var lowAdded = 0;
|
||||
var lowRemoved = 0;
|
||||
|
||||
foreach (var vulnerability in vulnerabilities)
|
||||
{
|
||||
var current = NormalizeSeverity(vulnerability.Severity);
|
||||
var previous = NormalizeSeverity(vulnerability.PreviousSeverity);
|
||||
|
||||
if (vulnerability.ChangeType.Equals("Added", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Increment(current, isAdded: true);
|
||||
}
|
||||
else if (vulnerability.ChangeType.Equals("Removed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Increment(previous, isAdded: false);
|
||||
}
|
||||
else if (vulnerability.ChangeType.Equals("Modified", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(current, previous, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Increment(previous, isAdded: false);
|
||||
Increment(current, isAdded: true);
|
||||
}
|
||||
}
|
||||
|
||||
return new DeltaSeverityChangesDto
|
||||
{
|
||||
CriticalAdded = criticalAdded,
|
||||
CriticalRemoved = criticalRemoved,
|
||||
HighAdded = highAdded,
|
||||
HighRemoved = highRemoved,
|
||||
MediumAdded = mediumAdded,
|
||||
MediumRemoved = mediumRemoved,
|
||||
LowAdded = lowAdded,
|
||||
LowRemoved = lowRemoved
|
||||
};
|
||||
|
||||
void Increment(string severity, bool isAdded)
|
||||
{
|
||||
switch (severity)
|
||||
{
|
||||
case "critical":
|
||||
if (isAdded) criticalAdded++; else criticalRemoved++;
|
||||
break;
|
||||
case "high":
|
||||
if (isAdded) highAdded++; else highRemoved++;
|
||||
break;
|
||||
case "medium":
|
||||
if (isAdded) mediumAdded++; else mediumRemoved++;
|
||||
break;
|
||||
case "low":
|
||||
if (isAdded) lowAdded++; else lowRemoved++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DeltaSnapshotSummaryDto BuildSummary(Snapshot snapshot) => new()
|
||||
{
|
||||
Digest = snapshot.Digest,
|
||||
CreatedAt = snapshot.CreatedAt,
|
||||
ComponentCount = snapshot.Components.Count,
|
||||
VulnerabilityCount = snapshot.Vulnerabilities.Count,
|
||||
SeverityCounts = new DeltaSeverityCountsDto
|
||||
{
|
||||
Critical = snapshot.Vulnerabilities.Count(v => v.Severity == "critical"),
|
||||
High = snapshot.Vulnerabilities.Count(v => v.Severity == "high"),
|
||||
Medium = snapshot.Vulnerabilities.Count(v => v.Severity == "medium"),
|
||||
Low = snapshot.Vulnerabilities.Count(v => v.Severity == "low"),
|
||||
Unknown = snapshot.Vulnerabilities.Count(v => v.Severity == "unknown")
|
||||
},
|
||||
PolicyVerdict = snapshot.PolicyVerdict
|
||||
};
|
||||
|
||||
private static void AddFieldChange(List<DeltaFieldChangeDto> changes, string field, string oldValue, string newValue)
|
||||
{
|
||||
if (string.Equals(oldValue, newValue, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
changes.Add(new DeltaFieldChangeDto
|
||||
{
|
||||
Field = field,
|
||||
PreviousValue = oldValue,
|
||||
CurrentValue = newValue
|
||||
});
|
||||
}
|
||||
|
||||
private static string EffectiveSeverity(DeltaVulnerabilityDto vulnerability)
|
||||
=> vulnerability.ChangeType.Equals("Removed", StringComparison.OrdinalIgnoreCase)
|
||||
? NormalizeSeverity(vulnerability.PreviousSeverity)
|
||||
: NormalizeSeverity(vulnerability.Severity);
|
||||
|
||||
private static int ChangeTypeOrder(string value) => value switch
|
||||
{
|
||||
"Added" => 0,
|
||||
"Removed" => 1,
|
||||
"Modified" => 2,
|
||||
"VersionChanged" => 3,
|
||||
"Unchanged" => 4,
|
||||
_ => 5
|
||||
};
|
||||
|
||||
private static string NormalizeSeverity(string? severity)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(severity))
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
var normalized = severity.Trim().ToLowerInvariant();
|
||||
return OrderedSeverities.Contains(normalized, StringComparer.Ordinal) ? normalized : "unknown";
|
||||
}
|
||||
|
||||
private static string IncrementPatch(string version)
|
||||
{
|
||||
var parts = version.Split('.', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 3
|
||||
|| !int.TryParse(parts[0], out var major)
|
||||
|| !int.TryParse(parts[1], out var minor)
|
||||
|| !int.TryParse(parts[2], out var patch))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
|
||||
return $"{major}.{minor}.{patch + 1}";
|
||||
}
|
||||
|
||||
private static string ComputeComparisonId(string baseDigest, string targetDigest)
|
||||
{
|
||||
var input = $"{baseDigest}|{targetDigest}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"cmp-{Convert.ToHexString(hash)[..16].ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private sealed record Snapshot(
|
||||
string Digest,
|
||||
DateTimeOffset CreatedAt,
|
||||
IReadOnlyList<SnapshotComponent> Components,
|
||||
IReadOnlyList<SnapshotVulnerability> Vulnerabilities,
|
||||
string PolicyVerdict);
|
||||
|
||||
private sealed record SnapshotComponent(string Purl, string Version, string License);
|
||||
private sealed record SnapshotVulnerability(string VulnId, string Purl, string Severity, string Reachability, string Verdict, string FixedVersion);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user