// 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; /// /// Options for delta report generation. /// public sealed record DeltaReportOptions { /// /// Gets or sets the confidence change threshold for triggering ConfidenceUp/Down sections. /// public double ConfidenceChangeThreshold { get; init; } = 0.15; /// /// Gets or sets whether to include damped entries in the report. /// public bool IncludeDamped { get; init; } = false; /// /// Gets or sets whether to include evidence-only changes. /// public bool IncludeEvidenceChanges { get; init; } = true; } /// /// Builder for creating instances. /// public sealed class DeltaReportBuilder { private readonly List _entries = new(); private readonly TimeProvider _timeProvider; private string _fromDigest = string.Empty; private string _toDigest = string.Empty; private DeltaReportOptions _options = new(); /// /// Initializes a new delta report builder. /// public DeltaReportBuilder(TimeProvider? timeProvider = null) { _timeProvider = timeProvider ?? TimeProvider.System; } /// /// Sets the snapshot digests. /// public DeltaReportBuilder WithSnapshots(string fromDigest, string toDigest) { _fromDigest = fromDigest; _toDigest = toDigest; return this; } /// /// Sets the report options. /// public DeltaReportBuilder WithOptions(DeltaReportOptions options) { _options = options; return this; } /// /// Adds a new finding entry. /// public DeltaReportBuilder AddNew( string vulnerabilityId, string productKey, VexStatus status, double confidence, string? rationaleClass = null, VexJustification? justification = null, IEnumerable? 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; } /// /// Adds a resolved finding entry. /// public DeltaReportBuilder AddResolved( string vulnerabilityId, string productKey, VexStatus fromStatus, VexStatus toStatus, double fromConfidence, double toConfidence, VexJustification? justification = null, IEnumerable? 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; } /// /// Adds a confidence change entry. /// public DeltaReportBuilder AddConfidenceChange( string vulnerabilityId, string productKey, VexStatus status, double fromConfidence, double toConfidence, IEnumerable? 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; } /// /// Adds a policy impact entry. /// public DeltaReportBuilder AddPolicyImpact( string vulnerabilityId, string productKey, VexStatus status, double confidence, string impactDescription, IEnumerable? 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; } /// /// Adds a damped (suppressed) entry. /// 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; } /// /// Adds an evidence change entry. /// public DeltaReportBuilder AddEvidenceChange( string vulnerabilityId, string productKey, VexStatus status, double confidence, string fromRationaleClass, string toRationaleClass, IEnumerable? 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; } /// /// Builds the delta report. /// 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? 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]}"; } }