Add global using for Xunit in test project Enhance ImportValidatorTests with async validation and quarantine checks Implement FileSystemQuarantineServiceTests for quarantine functionality Add integration tests for ImportValidator to check monotonicity Create BundleVersionTests to validate version parsing and comparison logic Implement VersionMonotonicityCheckerTests for monotonicity checks and activation logic
453 lines
10 KiB
C#
453 lines
10 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Findings.Ledger.Domain;
|
|
|
|
/// <summary>
|
|
/// Immutable decision event per advisory §11.
|
|
/// </summary>
|
|
public sealed class DecisionEvent
|
|
{
|
|
/// <summary>
|
|
/// Unique identifier for this decision event.
|
|
/// </summary>
|
|
public string Id { get; init; } = Guid.NewGuid().ToString("N");
|
|
|
|
/// <summary>
|
|
/// Alert identifier.
|
|
/// </summary>
|
|
public required string AlertId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Artifact identifier (image digest/commit hash).
|
|
/// </summary>
|
|
public required string ArtifactId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Actor who made the decision.
|
|
/// </summary>
|
|
public required string ActorId { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the decision was recorded (UTC).
|
|
/// </summary>
|
|
public required DateTimeOffset Timestamp { get; init; }
|
|
|
|
/// <summary>
|
|
/// Decision status: affected, not_affected, under_investigation.
|
|
/// </summary>
|
|
public required string DecisionStatus { get; init; }
|
|
|
|
/// <summary>
|
|
/// Preset reason code.
|
|
/// </summary>
|
|
public required string ReasonCode { get; init; }
|
|
|
|
/// <summary>
|
|
/// Custom reason text.
|
|
/// </summary>
|
|
public string? ReasonText { get; init; }
|
|
|
|
/// <summary>
|
|
/// Content-addressed evidence hashes.
|
|
/// </summary>
|
|
public required List<string> EvidenceHashes { get; init; }
|
|
|
|
/// <summary>
|
|
/// Policy context (ruleset version, policy id).
|
|
/// </summary>
|
|
public string? PolicyContext { get; init; }
|
|
|
|
/// <summary>
|
|
/// Deterministic replay token for reproducibility.
|
|
/// </summary>
|
|
public required string ReplayToken { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Alert entity for triage.
|
|
/// </summary>
|
|
public sealed class Alert
|
|
{
|
|
/// <summary>
|
|
/// Unique alert identifier.
|
|
/// </summary>
|
|
public required string AlertId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Tenant identifier.
|
|
/// </summary>
|
|
public required string TenantId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Artifact identifier (image digest/commit hash).
|
|
/// </summary>
|
|
public required string ArtifactId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Vulnerability identifier.
|
|
/// </summary>
|
|
public required string VulnId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Affected component PURL.
|
|
/// </summary>
|
|
public string? ComponentPurl { get; init; }
|
|
|
|
/// <summary>
|
|
/// Severity level (critical, high, medium, low).
|
|
/// </summary>
|
|
public required string Severity { get; init; }
|
|
|
|
/// <summary>
|
|
/// Triage band (hot, warm, cold).
|
|
/// </summary>
|
|
public required string Band { get; init; }
|
|
|
|
/// <summary>
|
|
/// Alert status (open, in_review, decided, closed).
|
|
/// </summary>
|
|
public required string Status { get; init; }
|
|
|
|
/// <summary>
|
|
/// Composite triage score.
|
|
/// </summary>
|
|
public double Score { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the alert was created.
|
|
/// </summary>
|
|
public required DateTimeOffset CreatedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the alert was last updated.
|
|
/// </summary>
|
|
public DateTimeOffset? UpdatedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of decisions recorded for this alert.
|
|
/// </summary>
|
|
public int DecisionCount { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evidence bundle for an alert.
|
|
/// </summary>
|
|
public sealed class EvidenceBundle
|
|
{
|
|
/// <summary>
|
|
/// Alert identifier.
|
|
/// </summary>
|
|
public required string AlertId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Reachability evidence.
|
|
/// </summary>
|
|
public EvidenceSection? Reachability { get; init; }
|
|
|
|
/// <summary>
|
|
/// Call stack evidence.
|
|
/// </summary>
|
|
public EvidenceSection? CallStack { get; init; }
|
|
|
|
/// <summary>
|
|
/// Provenance evidence.
|
|
/// </summary>
|
|
public EvidenceSection? Provenance { get; init; }
|
|
|
|
/// <summary>
|
|
/// VEX status evidence.
|
|
/// </summary>
|
|
public VexStatusEvidence? VexStatus { get; init; }
|
|
|
|
/// <summary>
|
|
/// Content-addressed hashes for all evidence.
|
|
/// </summary>
|
|
public required EvidenceHashes Hashes { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the bundle was computed.
|
|
/// </summary>
|
|
public required DateTimeOffset ComputedAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evidence section with status and proof.
|
|
/// </summary>
|
|
public sealed class EvidenceSection
|
|
{
|
|
/// <summary>
|
|
/// Status: available, loading, unavailable, error.
|
|
/// </summary>
|
|
public required string Status { get; init; }
|
|
|
|
/// <summary>
|
|
/// Content hash for this evidence.
|
|
/// </summary>
|
|
public string? Hash { get; init; }
|
|
|
|
/// <summary>
|
|
/// Proof data (type-specific).
|
|
/// </summary>
|
|
public object? Proof { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// VEX status evidence with history.
|
|
/// </summary>
|
|
public sealed class VexStatusEvidence
|
|
{
|
|
/// <summary>
|
|
/// Status: available, unavailable.
|
|
/// </summary>
|
|
public required string Status { get; init; }
|
|
|
|
/// <summary>
|
|
/// Current VEX statement.
|
|
/// </summary>
|
|
public VexStatement? Current { get; init; }
|
|
|
|
/// <summary>
|
|
/// Historical VEX statements.
|
|
/// </summary>
|
|
public IReadOnlyList<VexStatement>? History { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// VEX statement summary.
|
|
/// </summary>
|
|
public sealed class VexStatement
|
|
{
|
|
/// <summary>
|
|
/// Statement identifier.
|
|
/// </summary>
|
|
public required string StatementId { get; init; }
|
|
|
|
/// <summary>
|
|
/// VEX status.
|
|
/// </summary>
|
|
public required string Status { get; init; }
|
|
|
|
/// <summary>
|
|
/// Justification code.
|
|
/// </summary>
|
|
public string? Justification { get; init; }
|
|
|
|
/// <summary>
|
|
/// Impact statement.
|
|
/// </summary>
|
|
public string? ImpactStatement { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the statement was issued.
|
|
/// </summary>
|
|
public required DateTimeOffset Timestamp { get; init; }
|
|
|
|
/// <summary>
|
|
/// Statement issuer.
|
|
/// </summary>
|
|
public string? Issuer { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Content-addressed hashes for evidence bundle.
|
|
/// </summary>
|
|
public sealed class EvidenceHashes
|
|
{
|
|
/// <summary>
|
|
/// All hashes for the bundle.
|
|
/// </summary>
|
|
public required IReadOnlyList<string> Hashes { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Audit timeline for an alert.
|
|
/// </summary>
|
|
public sealed class AuditTimeline
|
|
{
|
|
/// <summary>
|
|
/// Alert identifier.
|
|
/// </summary>
|
|
public required string AlertId { get; init; }
|
|
|
|
/// <summary>
|
|
/// List of audit events.
|
|
/// </summary>
|
|
public required IReadOnlyList<AuditEvent> Events { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total count of events.
|
|
/// </summary>
|
|
public int TotalCount { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Single audit event.
|
|
/// </summary>
|
|
public sealed class AuditEvent
|
|
{
|
|
/// <summary>
|
|
/// Event identifier.
|
|
/// </summary>
|
|
public required string EventId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Type of audit event.
|
|
/// </summary>
|
|
public required string EventType { get; init; }
|
|
|
|
/// <summary>
|
|
/// Actor who triggered the event.
|
|
/// </summary>
|
|
public required string ActorId { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the event occurred.
|
|
/// </summary>
|
|
public required DateTimeOffset Timestamp { get; init; }
|
|
|
|
/// <summary>
|
|
/// Event-specific details.
|
|
/// </summary>
|
|
public object? Details { get; init; }
|
|
|
|
/// <summary>
|
|
/// Replay token if applicable.
|
|
/// </summary>
|
|
public string? ReplayToken { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Alert diff result.
|
|
/// </summary>
|
|
public sealed class AlertDiff
|
|
{
|
|
/// <summary>
|
|
/// Alert identifier.
|
|
/// </summary>
|
|
public required string AlertId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Baseline scan identifier.
|
|
/// </summary>
|
|
public string? BaselineScanId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Current scan identifier.
|
|
/// </summary>
|
|
public required string CurrentScanId { get; init; }
|
|
|
|
/// <summary>
|
|
/// SBOM diff summary.
|
|
/// </summary>
|
|
public SbomDiff? SbomDiff { get; init; }
|
|
|
|
/// <summary>
|
|
/// VEX diff summary.
|
|
/// </summary>
|
|
public VexDiff? VexDiff { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the diff was computed.
|
|
/// </summary>
|
|
public required DateTimeOffset ComputedAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// SBOM diff summary.
|
|
/// </summary>
|
|
public sealed class SbomDiff
|
|
{
|
|
/// <summary>
|
|
/// Number of added components.
|
|
/// </summary>
|
|
public int AddedComponents { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of removed components.
|
|
/// </summary>
|
|
public int RemovedComponents { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of changed components.
|
|
/// </summary>
|
|
public int ChangedComponents { get; init; }
|
|
|
|
/// <summary>
|
|
/// Detailed changes.
|
|
/// </summary>
|
|
public IReadOnlyList<ComponentDiff>? Changes { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Single component diff.
|
|
/// </summary>
|
|
public sealed class ComponentDiff
|
|
{
|
|
/// <summary>
|
|
/// Component PURL.
|
|
/// </summary>
|
|
public required string Purl { get; init; }
|
|
|
|
/// <summary>
|
|
/// Type of change: added, removed, changed.
|
|
/// </summary>
|
|
public required string ChangeType { get; init; }
|
|
|
|
/// <summary>
|
|
/// Old version if changed/removed.
|
|
/// </summary>
|
|
public string? OldVersion { get; init; }
|
|
|
|
/// <summary>
|
|
/// New version if changed/added.
|
|
/// </summary>
|
|
public string? NewVersion { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// VEX diff summary.
|
|
/// </summary>
|
|
public sealed class VexDiff
|
|
{
|
|
/// <summary>
|
|
/// Number of status changes.
|
|
/// </summary>
|
|
public int StatusChanges { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of new statements.
|
|
/// </summary>
|
|
public int NewStatements { get; init; }
|
|
|
|
/// <summary>
|
|
/// Detailed changes.
|
|
/// </summary>
|
|
public IReadOnlyList<VexStatusDiff>? Changes { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Single VEX status diff.
|
|
/// </summary>
|
|
public sealed class VexStatusDiff
|
|
{
|
|
/// <summary>
|
|
/// Vulnerability identifier.
|
|
/// </summary>
|
|
public required string VulnId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Old status.
|
|
/// </summary>
|
|
public string? OldStatus { get; init; }
|
|
|
|
/// <summary>
|
|
/// New status.
|
|
/// </summary>
|
|
public required string NewStatus { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the change occurred.
|
|
/// </summary>
|
|
public required DateTimeOffset Timestamp { get; init; }
|
|
}
|