184 lines
5.6 KiB
C#
184 lines
5.6 KiB
C#
// Licensed to StellaOps under the AGPL-3.0-or-later license.
|
|
|
|
using System.Collections.Immutable;
|
|
using System.Globalization;
|
|
|
|
namespace StellaOps.VexLens.Delta;
|
|
|
|
/// <summary>
|
|
/// A report summarizing changes between two vulnerability graph snapshots.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// DeltaReport groups changes by section for efficient triage:
|
|
/// - Users can focus on New findings first
|
|
/// - Resolved items can be quickly acknowledged
|
|
/// - Confidence changes help reprioritize existing findings
|
|
/// - Policy impacts highlight workflow-affecting changes
|
|
/// </remarks>
|
|
public sealed record DeltaReport
|
|
{
|
|
/// <summary>
|
|
/// Gets the unique identifier for this report.
|
|
/// </summary>
|
|
public required string ReportId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the digest of the previous snapshot.
|
|
/// </summary>
|
|
public required string FromSnapshotDigest { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the digest of the current snapshot.
|
|
/// </summary>
|
|
public required string ToSnapshotDigest { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the timestamp when the report was generated.
|
|
/// </summary>
|
|
public required DateTimeOffset GeneratedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets all delta entries in this report.
|
|
/// </summary>
|
|
public required ImmutableArray<DeltaEntry> Entries { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the summary counts by section.
|
|
/// </summary>
|
|
public required DeltaSummary Summary { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets entries grouped by section for UI consumption.
|
|
/// </summary>
|
|
public ImmutableDictionary<DeltaSection, ImmutableArray<DeltaEntry>> BySection =>
|
|
Entries
|
|
.GroupBy(e => e.Section)
|
|
.ToImmutableDictionary(
|
|
g => g.Key,
|
|
g => g.ToImmutableArray());
|
|
|
|
/// <summary>
|
|
/// Gets entries for a specific section.
|
|
/// </summary>
|
|
public ImmutableArray<DeltaEntry> GetSection(DeltaSection section) =>
|
|
BySection.TryGetValue(section, out var entries)
|
|
? entries
|
|
: [];
|
|
|
|
/// <summary>
|
|
/// Gets whether this report has any actionable changes.
|
|
/// </summary>
|
|
public bool HasActionableChanges =>
|
|
Summary.NewCount > 0 ||
|
|
Summary.PolicyImpactCount > 0 ||
|
|
Summary.ConfidenceUpCount > 0;
|
|
|
|
/// <summary>
|
|
/// Gets a one-line summary suitable for notifications.
|
|
/// </summary>
|
|
public string ToNotificationSummary()
|
|
{
|
|
var parts = new List<string>();
|
|
|
|
if (Summary.NewCount > 0)
|
|
{
|
|
parts.Add(string.Create(CultureInfo.InvariantCulture,
|
|
$"{Summary.NewCount} new"));
|
|
}
|
|
|
|
if (Summary.ResolvedCount > 0)
|
|
{
|
|
parts.Add(string.Create(CultureInfo.InvariantCulture,
|
|
$"{Summary.ResolvedCount} resolved"));
|
|
}
|
|
|
|
if (Summary.PolicyImpactCount > 0)
|
|
{
|
|
parts.Add(string.Create(CultureInfo.InvariantCulture,
|
|
$"{Summary.PolicyImpactCount} policy impact"));
|
|
}
|
|
|
|
if (Summary.ConfidenceUpCount > 0)
|
|
{
|
|
parts.Add(string.Create(CultureInfo.InvariantCulture,
|
|
$"{Summary.ConfidenceUpCount} confidence up"));
|
|
}
|
|
|
|
if (Summary.ConfidenceDownCount > 0)
|
|
{
|
|
parts.Add(string.Create(CultureInfo.InvariantCulture,
|
|
$"{Summary.ConfidenceDownCount} confidence down"));
|
|
}
|
|
|
|
return parts.Count == 0
|
|
? "No significant changes"
|
|
: string.Join(", ", parts);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Summary counts for a delta report.
|
|
/// </summary>
|
|
public sealed record DeltaSummary
|
|
{
|
|
/// <summary>
|
|
/// Gets the total number of entries.
|
|
/// </summary>
|
|
public required int TotalCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the count of new findings.
|
|
/// </summary>
|
|
public required int NewCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the count of resolved findings.
|
|
/// </summary>
|
|
public required int ResolvedCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the count of confidence increases.
|
|
/// </summary>
|
|
public required int ConfidenceUpCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the count of confidence decreases.
|
|
/// </summary>
|
|
public required int ConfidenceDownCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the count of policy impact changes.
|
|
/// </summary>
|
|
public required int PolicyImpactCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the count of damped (suppressed) changes.
|
|
/// </summary>
|
|
public int DampedCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the count of evidence-only changes.
|
|
/// </summary>
|
|
public int EvidenceChangedCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Creates a summary from a list of entries.
|
|
/// </summary>
|
|
public static DeltaSummary FromEntries(IEnumerable<DeltaEntry> entries)
|
|
{
|
|
var list = entries.ToList();
|
|
|
|
return new DeltaSummary
|
|
{
|
|
TotalCount = list.Count,
|
|
NewCount = list.Count(e => e.Section == DeltaSection.New),
|
|
ResolvedCount = list.Count(e => e.Section == DeltaSection.Resolved),
|
|
ConfidenceUpCount = list.Count(e => e.Section == DeltaSection.ConfidenceUp),
|
|
ConfidenceDownCount = list.Count(e => e.Section == DeltaSection.ConfidenceDown),
|
|
PolicyImpactCount = list.Count(e => e.Section == DeltaSection.PolicyImpact),
|
|
DampedCount = list.Count(e => e.Section == DeltaSection.Damped),
|
|
EvidenceChangedCount = list.Count(e => e.Section == DeltaSection.EvidenceChanged)
|
|
};
|
|
}
|
|
}
|