using System.Collections.Immutable; using System.Runtime.Serialization; namespace StellaOps.Excititor.Core; public sealed record VexConsensus { public VexConsensus( string vulnerabilityId, VexProduct product, VexConsensusStatus status, DateTimeOffset calculatedAt, IEnumerable sources, IEnumerable? conflicts = null, VexSignalSnapshot? signals = null, string? policyVersion = null, string? summary = null, string? policyRevisionId = null, string? policyDigest = null) { if (string.IsNullOrWhiteSpace(vulnerabilityId)) { throw new ArgumentException("Vulnerability id must be provided.", nameof(vulnerabilityId)); } VulnerabilityId = vulnerabilityId.Trim(); Product = product ?? throw new ArgumentNullException(nameof(product)); Status = status; CalculatedAt = calculatedAt; Sources = NormalizeSources(sources); Conflicts = NormalizeConflicts(conflicts); Signals = signals; PolicyVersion = string.IsNullOrWhiteSpace(policyVersion) ? null : policyVersion.Trim(); Summary = string.IsNullOrWhiteSpace(summary) ? null : summary.Trim(); PolicyRevisionId = string.IsNullOrWhiteSpace(policyRevisionId) ? null : policyRevisionId.Trim(); PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim(); } public string VulnerabilityId { get; } public VexProduct Product { get; } public VexConsensusStatus Status { get; } public DateTimeOffset CalculatedAt { get; } public ImmutableArray Sources { get; } public ImmutableArray Conflicts { get; } public VexSignalSnapshot? Signals { get; } public string? PolicyVersion { get; } public string? Summary { get; } public string? PolicyRevisionId { get; } public string? PolicyDigest { get; } private static ImmutableArray NormalizeSources(IEnumerable sources) { if (sources is null) { throw new ArgumentNullException(nameof(sources)); } var builder = ImmutableArray.CreateBuilder(); builder.AddRange(sources); if (builder.Count == 0) { return ImmutableArray.Empty; } return builder .OrderBy(static x => x.ProviderId, StringComparer.Ordinal) .ThenBy(static x => x.DocumentDigest, StringComparer.Ordinal) .ToImmutableArray(); } private static ImmutableArray NormalizeConflicts(IEnumerable? conflicts) { if (conflicts is null) { return ImmutableArray.Empty; } var items = conflicts.ToArray(); return items.Length == 0 ? ImmutableArray.Empty : items .OrderBy(static x => x.ProviderId, StringComparer.Ordinal) .ThenBy(static x => x.DocumentDigest, StringComparer.Ordinal) .ToImmutableArray(); } } public sealed record VexConsensusSource { public VexConsensusSource( string providerId, VexClaimStatus status, string documentDigest, double weight, VexJustification? justification = null, string? detail = null, VexConfidence? confidence = null) { if (string.IsNullOrWhiteSpace(providerId)) { throw new ArgumentException("Provider id must be provided.", nameof(providerId)); } if (string.IsNullOrWhiteSpace(documentDigest)) { throw new ArgumentException("Document digest must be provided.", nameof(documentDigest)); } if (double.IsNaN(weight) || double.IsInfinity(weight) || weight < 0) { throw new ArgumentOutOfRangeException(nameof(weight), "Weight must be a finite, non-negative number."); } ProviderId = providerId.Trim(); Status = status; DocumentDigest = documentDigest.Trim(); Weight = weight; Justification = justification; Detail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim(); Confidence = confidence; } public string ProviderId { get; } public VexClaimStatus Status { get; } public string DocumentDigest { get; } public double Weight { get; } public VexJustification? Justification { get; } public string? Detail { get; } public VexConfidence? Confidence { get; } } public sealed record VexConsensusConflict { public VexConsensusConflict( string providerId, VexClaimStatus status, string documentDigest, VexJustification? justification = null, string? detail = null, string? reason = null) { if (string.IsNullOrWhiteSpace(providerId)) { throw new ArgumentException("Provider id must be provided.", nameof(providerId)); } if (string.IsNullOrWhiteSpace(documentDigest)) { throw new ArgumentException("Document digest must be provided.", nameof(documentDigest)); } ProviderId = providerId.Trim(); Status = status; DocumentDigest = documentDigest.Trim(); Justification = justification; Detail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim(); Reason = string.IsNullOrWhiteSpace(reason) ? null : reason.Trim(); } public string ProviderId { get; } public VexClaimStatus Status { get; } public string DocumentDigest { get; } public VexJustification? Justification { get; } public string? Detail { get; } public string? Reason { get; } } [DataContract] public enum VexConsensusStatus { [EnumMember(Value = "affected")] Affected, [EnumMember(Value = "not_affected")] NotAffected, [EnumMember(Value = "fixed")] Fixed, [EnumMember(Value = "under_investigation")] UnderInvestigation, [EnumMember(Value = "divergent")] Divergent, }