save progress
This commit is contained in:
183
src/VexLens/StellaOps.VexLens/Delta/DeltaReport.cs
Normal file
183
src/VexLens/StellaOps.VexLens/Delta/DeltaReport.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
// 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user