Files
git.stella-ops.org/src/VexLens/StellaOps.VexLens/Delta/DeltaReportBuilder.cs
2026-02-01 21:37:40 +02:00

349 lines
9.4 KiB
C#

// Licensed to StellaOps under the BUSL-1.1 license.
using StellaOps.VexLens.Models;
using System.Collections.Immutable;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
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]}";
}
}