Files
git.stella-ops.org/src/StellaOps.Excititor.Core/VexConsensus.cs

207 lines
6.3 KiB
C#

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<VexConsensusSource> sources,
IEnumerable<VexConsensusConflict>? 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<VexConsensusSource> Sources { get; }
public ImmutableArray<VexConsensusConflict> 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<VexConsensusSource> NormalizeSources(IEnumerable<VexConsensusSource> sources)
{
if (sources is null)
{
throw new ArgumentNullException(nameof(sources));
}
var builder = ImmutableArray.CreateBuilder<VexConsensusSource>();
builder.AddRange(sources);
if (builder.Count == 0)
{
return ImmutableArray<VexConsensusSource>.Empty;
}
return builder
.OrderBy(static x => x.ProviderId, StringComparer.Ordinal)
.ThenBy(static x => x.DocumentDigest, StringComparer.Ordinal)
.ToImmutableArray();
}
private static ImmutableArray<VexConsensusConflict> NormalizeConflicts(IEnumerable<VexConsensusConflict>? conflicts)
{
if (conflicts is null)
{
return ImmutableArray<VexConsensusConflict>.Empty;
}
var items = conflicts.ToArray();
return items.Length == 0
? ImmutableArray<VexConsensusConflict>.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,
}