using System.Collections.Immutable; using System.Runtime.Serialization; namespace StellaOps.Excititor.Core; public sealed record VexClaim { public VexClaim( string vulnerabilityId, string providerId, VexProduct product, VexClaimStatus status, VexClaimDocument document, DateTimeOffset firstSeen, DateTimeOffset lastSeen, VexJustification? justification = null, string? detail = null, VexConfidence? confidence = null, VexSignalSnapshot? signals = null, ImmutableDictionary? additionalMetadata = null) { if (string.IsNullOrWhiteSpace(vulnerabilityId)) { throw new ArgumentException("Vulnerability id must be provided.", nameof(vulnerabilityId)); } if (string.IsNullOrWhiteSpace(providerId)) { throw new ArgumentException("Provider id must be provided.", nameof(providerId)); } if (lastSeen < firstSeen) { throw new ArgumentOutOfRangeException(nameof(lastSeen), "Last seen timestamp cannot be earlier than first seen."); } VulnerabilityId = vulnerabilityId.Trim(); ProviderId = providerId.Trim(); Product = product ?? throw new ArgumentNullException(nameof(product)); Status = status; Document = document ?? throw new ArgumentNullException(nameof(document)); FirstSeen = firstSeen; LastSeen = lastSeen; Justification = justification; Detail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim(); Confidence = confidence; Signals = signals; AdditionalMetadata = NormalizeMetadata(additionalMetadata); } public string VulnerabilityId { get; } public string ProviderId { get; } public VexProduct Product { get; } public VexClaimStatus Status { get; } public VexJustification? Justification { get; } public string? Detail { get; } public VexClaimDocument Document { get; } public DateTimeOffset FirstSeen { get; } public DateTimeOffset LastSeen { get; } public VexConfidence? Confidence { get; } public VexSignalSnapshot? Signals { get; } public ImmutableSortedDictionary AdditionalMetadata { get; } private static ImmutableSortedDictionary NormalizeMetadata( ImmutableDictionary? additionalMetadata) { if (additionalMetadata is null || additionalMetadata.Count == 0) { return ImmutableSortedDictionary.Empty; } var builder = ImmutableSortedDictionary.CreateBuilder(StringComparer.Ordinal); foreach (var (key, value) in additionalMetadata) { if (string.IsNullOrWhiteSpace(key)) { continue; } builder[key.Trim()] = value?.Trim() ?? string.Empty; } return builder.ToImmutable(); } } public sealed record VexProduct { public VexProduct( string key, string? name, string? version = null, string? purl = null, string? cpe = null, IEnumerable? componentIdentifiers = null) { if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentException("Product key must be provided.", nameof(key)); } Key = key.Trim(); Name = string.IsNullOrWhiteSpace(name) ? null : name.Trim(); Version = string.IsNullOrWhiteSpace(version) ? null : version.Trim(); Purl = string.IsNullOrWhiteSpace(purl) ? null : purl.Trim(); Cpe = string.IsNullOrWhiteSpace(cpe) ? null : cpe.Trim(); ComponentIdentifiers = NormalizeComponentIdentifiers(componentIdentifiers); } public string Key { get; } public string? Name { get; } public string? Version { get; } public string? Purl { get; } public string? Cpe { get; } public ImmutableArray ComponentIdentifiers { get; } private static ImmutableArray NormalizeComponentIdentifiers(IEnumerable? identifiers) { if (identifiers is null) { return ImmutableArray.Empty; } var set = new SortedSet(StringComparer.Ordinal); foreach (var identifier in identifiers) { if (string.IsNullOrWhiteSpace(identifier)) { continue; } set.Add(identifier.Trim()); } return set.Count == 0 ? ImmutableArray.Empty : set.ToImmutableArray(); } } public sealed record VexClaimDocument { public VexClaimDocument( VexDocumentFormat format, string digest, Uri sourceUri, string? revision = null, VexSignatureMetadata? signature = null) { if (string.IsNullOrWhiteSpace(digest)) { throw new ArgumentException("Document digest must be provided.", nameof(digest)); } Format = format; Digest = digest.Trim(); SourceUri = sourceUri ?? throw new ArgumentNullException(nameof(sourceUri)); Revision = string.IsNullOrWhiteSpace(revision) ? null : revision.Trim(); Signature = signature; } public VexDocumentFormat Format { get; } public string Digest { get; } public Uri SourceUri { get; } public string? Revision { get; } public VexSignatureMetadata? Signature { get; } } public sealed record VexSignatureMetadata { public VexSignatureMetadata( string type, string? subject = null, string? issuer = null, string? keyId = null, DateTimeOffset? verifiedAt = null, string? transparencyLogReference = null) { if (string.IsNullOrWhiteSpace(type)) { throw new ArgumentException("Signature type must be provided.", nameof(type)); } Type = type.Trim(); Subject = string.IsNullOrWhiteSpace(subject) ? null : subject.Trim(); Issuer = string.IsNullOrWhiteSpace(issuer) ? null : issuer.Trim(); KeyId = string.IsNullOrWhiteSpace(keyId) ? null : keyId.Trim(); VerifiedAt = verifiedAt; TransparencyLogReference = string.IsNullOrWhiteSpace(transparencyLogReference) ? null : transparencyLogReference.Trim(); } public string Type { get; } public string? Subject { get; } public string? Issuer { get; } public string? KeyId { get; } public DateTimeOffset? VerifiedAt { get; } public string? TransparencyLogReference { get; } } public sealed record VexConfidence { public VexConfidence(string level, double? score = null, string? method = null) { if (string.IsNullOrWhiteSpace(level)) { throw new ArgumentException("Confidence level must be provided.", nameof(level)); } if (score is not null && (double.IsNaN(score.Value) || double.IsInfinity(score.Value))) { throw new ArgumentOutOfRangeException(nameof(score), "Confidence score must be a finite number."); } Level = level.Trim(); Score = score; Method = string.IsNullOrWhiteSpace(method) ? null : method.Trim(); } public string Level { get; } public double? Score { get; } public string? Method { get; } } [DataContract] public enum VexDocumentFormat { [EnumMember(Value = "csaf")] Csaf, [EnumMember(Value = "cyclonedx")] CycloneDx, [EnumMember(Value = "openvex")] OpenVex, [EnumMember(Value = "oci_attestation")] OciAttestation, } [DataContract] public enum VexClaimStatus { [EnumMember(Value = "affected")] Affected, [EnumMember(Value = "not_affected")] NotAffected, [EnumMember(Value = "fixed")] Fixed, [EnumMember(Value = "under_investigation")] UnderInvestigation, } [DataContract] public enum VexJustification { [EnumMember(Value = "component_not_present")] ComponentNotPresent, [EnumMember(Value = "component_not_configured")] ComponentNotConfigured, [EnumMember(Value = "vulnerable_code_not_present")] VulnerableCodeNotPresent, [EnumMember(Value = "vulnerable_code_not_in_execute_path")] VulnerableCodeNotInExecutePath, [EnumMember(Value = "vulnerable_code_cannot_be_controlled_by_adversary")] VulnerableCodeCannotBeControlledByAdversary, [EnumMember(Value = "inline_mitigations_already_exist")] InlineMitigationsAlreadyExist, [EnumMember(Value = "protected_by_mitigating_control")] ProtectedByMitigatingControl, [EnumMember(Value = "code_not_present")] CodeNotPresent, [EnumMember(Value = "code_not_reachable")] CodeNotReachable, [EnumMember(Value = "requires_configuration")] RequiresConfiguration, [EnumMember(Value = "requires_dependency")] RequiresDependency, [EnumMember(Value = "requires_environment")] RequiresEnvironment, [EnumMember(Value = "protected_by_compensating_control")] ProtectedByCompensatingControl, [EnumMember(Value = "protected_at_perimeter")] ProtectedAtPerimeter, [EnumMember(Value = "protected_at_runtime")] ProtectedAtRuntime, }