save progress
This commit is contained in:
347
src/VexLens/StellaOps.VexLens/Delta/DeltaReportBuilder.cs
Normal file
347
src/VexLens/StellaOps.VexLens/Delta/DeltaReportBuilder.cs
Normal file
@@ -0,0 +1,347 @@
|
||||
// Licensed to StellaOps under the AGPL-3.0-or-later license.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.VexLens.Models;
|
||||
|
||||
namespace StellaOps.VexLens.Delta;
|
||||
|
||||
/// <summary>
|
||||
/// Options for delta report generation.
|
||||
/// </summary>
|
||||
public sealed record DeltaReportOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the confidence change threshold for triggering ConfidenceUp/Down sections.
|
||||
/// </summary>
|
||||
public double ConfidenceChangeThreshold { get; init; } = 0.15;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to include damped entries in the report.
|
||||
/// </summary>
|
||||
public bool IncludeDamped { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to include evidence-only changes.
|
||||
/// </summary>
|
||||
public bool IncludeEvidenceChanges { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for creating <see cref="DeltaReport"/> instances.
|
||||
/// </summary>
|
||||
public sealed class DeltaReportBuilder
|
||||
{
|
||||
private readonly List<DeltaEntry> _entries = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private string _fromDigest = string.Empty;
|
||||
private string _toDigest = string.Empty;
|
||||
private DeltaReportOptions _options = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new delta report builder.
|
||||
/// </summary>
|
||||
public DeltaReportBuilder(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the snapshot digests.
|
||||
/// </summary>
|
||||
public DeltaReportBuilder WithSnapshots(string fromDigest, string toDigest)
|
||||
{
|
||||
_fromDigest = fromDigest;
|
||||
_toDigest = toDigest;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the report options.
|
||||
/// </summary>
|
||||
public DeltaReportBuilder WithOptions(DeltaReportOptions options)
|
||||
{
|
||||
_options = options;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new finding entry.
|
||||
/// </summary>
|
||||
public DeltaReportBuilder AddNew(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
VexStatus status,
|
||||
double confidence,
|
||||
string? rationaleClass = null,
|
||||
VexJustification? justification = null,
|
||||
IEnumerable<string>? sources = null)
|
||||
{
|
||||
var entry = CreateEntry(
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
DeltaSection.New,
|
||||
null,
|
||||
status,
|
||||
null,
|
||||
confidence,
|
||||
null,
|
||||
rationaleClass,
|
||||
justification,
|
||||
$"New {status} finding",
|
||||
sources);
|
||||
|
||||
_entries.Add(entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a resolved finding entry.
|
||||
/// </summary>
|
||||
public DeltaReportBuilder AddResolved(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
VexStatus fromStatus,
|
||||
VexStatus toStatus,
|
||||
double fromConfidence,
|
||||
double toConfidence,
|
||||
VexJustification? justification = null,
|
||||
IEnumerable<string>? sources = null)
|
||||
{
|
||||
var entry = CreateEntry(
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
DeltaSection.Resolved,
|
||||
fromStatus,
|
||||
toStatus,
|
||||
fromConfidence,
|
||||
toConfidence,
|
||||
null,
|
||||
null,
|
||||
justification,
|
||||
$"Resolved: {fromStatus} -> {toStatus}",
|
||||
sources);
|
||||
|
||||
_entries.Add(entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a confidence change entry.
|
||||
/// </summary>
|
||||
public DeltaReportBuilder AddConfidenceChange(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
VexStatus status,
|
||||
double fromConfidence,
|
||||
double toConfidence,
|
||||
IEnumerable<string>? sources = null)
|
||||
{
|
||||
var delta = toConfidence - fromConfidence;
|
||||
var section = delta > 0 ? DeltaSection.ConfidenceUp : DeltaSection.ConfidenceDown;
|
||||
|
||||
if (Math.Abs(delta) < _options.ConfidenceChangeThreshold)
|
||||
{
|
||||
return this; // Below threshold, don't add
|
||||
}
|
||||
|
||||
var entry = CreateEntry(
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
section,
|
||||
status,
|
||||
status,
|
||||
fromConfidence,
|
||||
toConfidence,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
string.Create(CultureInfo.InvariantCulture,
|
||||
$"Confidence {(delta > 0 ? "increased" : "decreased")}: {fromConfidence:P0} -> {toConfidence:P0}"),
|
||||
sources);
|
||||
|
||||
_entries.Add(entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a policy impact entry.
|
||||
/// </summary>
|
||||
public DeltaReportBuilder AddPolicyImpact(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
VexStatus status,
|
||||
double confidence,
|
||||
string impactDescription,
|
||||
IEnumerable<string>? sources = null)
|
||||
{
|
||||
var entry = CreateEntry(
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
DeltaSection.PolicyImpact,
|
||||
status,
|
||||
status,
|
||||
confidence,
|
||||
confidence,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$"Policy impact: {impactDescription}",
|
||||
sources);
|
||||
|
||||
_entries.Add(entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a damped (suppressed) entry.
|
||||
/// </summary>
|
||||
public DeltaReportBuilder AddDamped(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
VexStatus fromStatus,
|
||||
VexStatus toStatus,
|
||||
double fromConfidence,
|
||||
double toConfidence,
|
||||
string dampReason)
|
||||
{
|
||||
if (!_options.IncludeDamped)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
var entry = CreateEntry(
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
DeltaSection.Damped,
|
||||
fromStatus,
|
||||
toStatus,
|
||||
fromConfidence,
|
||||
toConfidence,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$"Damped: {dampReason}",
|
||||
null);
|
||||
|
||||
_entries.Add(entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an evidence change entry.
|
||||
/// </summary>
|
||||
public DeltaReportBuilder AddEvidenceChange(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
VexStatus status,
|
||||
double confidence,
|
||||
string fromRationaleClass,
|
||||
string toRationaleClass,
|
||||
IEnumerable<string>? sources = null)
|
||||
{
|
||||
if (!_options.IncludeEvidenceChanges)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
var entry = CreateEntry(
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
DeltaSection.EvidenceChanged,
|
||||
status,
|
||||
status,
|
||||
confidence,
|
||||
confidence,
|
||||
fromRationaleClass,
|
||||
toRationaleClass,
|
||||
null,
|
||||
$"Evidence changed: {fromRationaleClass} -> {toRationaleClass}",
|
||||
sources);
|
||||
|
||||
_entries.Add(entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the delta report.
|
||||
/// </summary>
|
||||
public DeltaReport Build()
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var reportId = GenerateReportId(now);
|
||||
|
||||
// Sort entries for deterministic output
|
||||
var sortedEntries = _entries
|
||||
.OrderBy(e => (int)e.Section)
|
||||
.ThenBy(e => e.VulnerabilityId, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.ProductKey, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return new DeltaReport
|
||||
{
|
||||
ReportId = reportId,
|
||||
FromSnapshotDigest = _fromDigest,
|
||||
ToSnapshotDigest = _toDigest,
|
||||
GeneratedAt = now,
|
||||
Entries = sortedEntries,
|
||||
Summary = DeltaSummary.FromEntries(sortedEntries)
|
||||
};
|
||||
}
|
||||
|
||||
private DeltaEntry CreateEntry(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
DeltaSection section,
|
||||
VexStatus? fromStatus,
|
||||
VexStatus toStatus,
|
||||
double? fromConfidence,
|
||||
double toConfidence,
|
||||
string? fromRationaleClass,
|
||||
string? toRationaleClass,
|
||||
VexJustification? justification,
|
||||
string summary,
|
||||
IEnumerable<string>? sources)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var deltaId = ComputeDeltaId(vulnerabilityId, productKey, section, now);
|
||||
|
||||
return new DeltaEntry
|
||||
{
|
||||
DeltaId = deltaId,
|
||||
VulnerabilityId = vulnerabilityId,
|
||||
ProductKey = productKey,
|
||||
Section = section,
|
||||
FromStatus = fromStatus,
|
||||
ToStatus = toStatus,
|
||||
FromConfidence = fromConfidence,
|
||||
ToConfidence = toConfidence,
|
||||
FromRationaleClass = fromRationaleClass,
|
||||
ToRationaleClass = toRationaleClass,
|
||||
Justification = justification,
|
||||
Summary = summary,
|
||||
Timestamp = now,
|
||||
ContributingSources = sources?.ToImmutableArray() ?? []
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeDeltaId(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
DeltaSection section,
|
||||
DateTimeOffset timestamp)
|
||||
{
|
||||
var input = $"{vulnerabilityId}|{productKey}|{section}|{timestamp:O}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexStringLower(hash)[..16];
|
||||
}
|
||||
|
||||
private string GenerateReportId(DateTimeOffset timestamp)
|
||||
{
|
||||
var input = $"{_fromDigest}|{_toDigest}|{timestamp:O}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"delta-{Convert.ToHexStringLower(hash)[..12]}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user