using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using StellaOps.Vexer.Core; namespace StellaOps.Vexer.Storage.Mongo; [BsonIgnoreExtraElements] internal sealed class VexRawDocumentRecord { [BsonId] public string Id { get; set; } = default!; public string ProviderId { get; set; } = default!; public string Format { get; set; } = default!; public string SourceUri { get; set; } = default!; public DateTime RetrievedAt { get; set; } = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc); public string Digest { get; set; } = default!; public byte[] Content { get; set; } = Array.Empty(); [BsonRepresentation(BsonType.ObjectId)] public string? GridFsObjectId { get; set; } = null; public Dictionary Metadata { get; set; } = new(StringComparer.Ordinal); public static VexRawDocumentRecord FromDomain(VexRawDocument document, bool includeContent = true) => new() { Id = document.Digest, ProviderId = document.ProviderId, Format = document.Format.ToString().ToLowerInvariant(), SourceUri = document.SourceUri.ToString(), RetrievedAt = document.RetrievedAt.UtcDateTime, Digest = document.Digest, Content = includeContent ? document.Content.ToArray() : Array.Empty(), Metadata = document.Metadata.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.Ordinal), }; public VexRawDocument ToDomain() => ToDomain(new ReadOnlyMemory(Content ?? Array.Empty())); public VexRawDocument ToDomain(ReadOnlyMemory content) => new( ProviderId, Enum.Parse(Format, ignoreCase: true), new Uri(SourceUri), RetrievedAt, Digest, content, (Metadata ?? new Dictionary(StringComparer.Ordinal)) .ToImmutableDictionary(StringComparer.Ordinal)); } [BsonIgnoreExtraElements] internal sealed class VexExportManifestRecord { [BsonId] public string Id { get; set; } = default!; public string QuerySignature { get; set; } = default!; public string Format { get; set; } = default!; public DateTime CreatedAt { get; set; } = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc); public string ArtifactAlgorithm { get; set; } = default!; public string ArtifactDigest { get; set; } = default!; public int ClaimCount { get; set; } = 0; public bool FromCache { get; set; } = false; public List SourceProviders { get; set; } = new(); public string? ConsensusRevision { get; set; } = null; public string? PredicateType { get; set; } = null; public string? RekorApiVersion { get; set; } = null; public string? RekorLocation { get; set; } = null; public string? RekorLogIndex { get; set; } = null; public string? RekorInclusionProofUri { get; set; } = null; public string? EnvelopeDigest { get; set; } = null; public DateTime? SignedAt { get; set; } = null; public long SizeBytes { get; set; } = 0; public static VexExportManifestRecord FromDomain(VexExportManifest manifest) => new() { Id = CreateId(manifest.QuerySignature, manifest.Format), QuerySignature = manifest.QuerySignature.Value, Format = manifest.Format.ToString().ToLowerInvariant(), CreatedAt = manifest.CreatedAt.UtcDateTime, ArtifactAlgorithm = manifest.Artifact.Algorithm, ArtifactDigest = manifest.Artifact.Digest, ClaimCount = manifest.ClaimCount, FromCache = manifest.FromCache, SourceProviders = manifest.SourceProviders.ToList(), ConsensusRevision = manifest.ConsensusRevision, PredicateType = manifest.Attestation?.PredicateType, RekorApiVersion = manifest.Attestation?.Rekor?.ApiVersion, RekorLocation = manifest.Attestation?.Rekor?.Location, RekorLogIndex = manifest.Attestation?.Rekor?.LogIndex, RekorInclusionProofUri = manifest.Attestation?.Rekor?.InclusionProofUri?.ToString(), EnvelopeDigest = manifest.Attestation?.EnvelopeDigest, SignedAt = manifest.Attestation?.SignedAt?.UtcDateTime, SizeBytes = manifest.SizeBytes, }; public VexExportManifest ToDomain() { var signedAt = SignedAt.HasValue ? new DateTimeOffset(DateTime.SpecifyKind(SignedAt.Value, DateTimeKind.Utc)) : (DateTimeOffset?)null; var attestation = PredicateType is null ? null : new VexAttestationMetadata( PredicateType, RekorApiVersion is null || RekorLocation is null ? null : new VexRekorReference( RekorApiVersion, RekorLocation, RekorLogIndex, RekorInclusionProofUri is null ? null : new Uri(RekorInclusionProofUri)), EnvelopeDigest, signedAt); return new VexExportManifest( Id, new VexQuerySignature(QuerySignature), Enum.Parse(Format, ignoreCase: true), CreatedAt, new VexContentAddress(ArtifactAlgorithm, ArtifactDigest), ClaimCount, SourceProviders, FromCache, ConsensusRevision, attestation, SizeBytes); } public static string CreateId(VexQuerySignature signature, VexExportFormat format) => string.Format(CultureInfo.InvariantCulture, "{0}|{1}", signature.Value, format.ToString().ToLowerInvariant()); } [BsonIgnoreExtraElements] internal sealed class VexProviderRecord { [BsonId] public string Id { get; set; } = default!; public string DisplayName { get; set; } = default!; public string Kind { get; set; } = default!; public List BaseUris { get; set; } = new(); public VexProviderDiscoveryDocument? Discovery { get; set; } = null; public VexProviderTrustDocument? Trust { get; set; } = null; public bool Enabled { get; set; } = true; public static VexProviderRecord FromDomain(VexProvider provider) => new() { Id = provider.Id, DisplayName = provider.DisplayName, Kind = provider.Kind.ToString().ToLowerInvariant(), BaseUris = provider.BaseUris.Select(uri => uri.ToString()).ToList(), Discovery = VexProviderDiscoveryDocument.FromDomain(provider.Discovery), Trust = VexProviderTrustDocument.FromDomain(provider.Trust), Enabled = provider.Enabled, }; public VexProvider ToDomain() { var uris = BaseUris?.Select(uri => new Uri(uri)) ?? Enumerable.Empty(); return new VexProvider( Id, DisplayName, Enum.Parse(Kind, ignoreCase: true), uris, Discovery?.ToDomain(), Trust?.ToDomain(), Enabled); } } [BsonIgnoreExtraElements] internal sealed class VexProviderDiscoveryDocument { public string? WellKnownMetadata { get; set; } = null; public string? RolIeService { get; set; } = null; public static VexProviderDiscoveryDocument? FromDomain(VexProviderDiscovery? discovery) => discovery is null ? null : new VexProviderDiscoveryDocument { WellKnownMetadata = discovery.WellKnownMetadata?.ToString(), RolIeService = discovery.RolIeService?.ToString(), }; public VexProviderDiscovery ToDomain() => new( WellKnownMetadata is null ? null : new Uri(WellKnownMetadata), RolIeService is null ? null : new Uri(RolIeService)); } [BsonIgnoreExtraElements] internal sealed class VexProviderTrustDocument { public double Weight { get; set; } = 1.0; public VexCosignTrustDocument? Cosign { get; set; } = null; public List PgpFingerprints { get; set; } = new(); public static VexProviderTrustDocument? FromDomain(VexProviderTrust? trust) => trust is null ? null : new VexProviderTrustDocument { Weight = trust.Weight, Cosign = trust.Cosign is null ? null : VexCosignTrustDocument.FromDomain(trust.Cosign), PgpFingerprints = trust.PgpFingerprints.ToList(), }; public VexProviderTrust ToDomain() => new( Weight, Cosign?.ToDomain(), PgpFingerprints); } [BsonIgnoreExtraElements] internal sealed class VexCosignTrustDocument { public string Issuer { get; set; } = default!; public string IdentityPattern { get; set; } = default!; public static VexCosignTrustDocument FromDomain(VexCosignTrust trust) => new() { Issuer = trust.Issuer, IdentityPattern = trust.IdentityPattern, }; public VexCosignTrust ToDomain() => new(Issuer, IdentityPattern); } [BsonIgnoreExtraElements] internal sealed class VexConsensusRecord { [BsonId] public string Id { get; set; } = default!; public string VulnerabilityId { get; set; } = default!; public VexProductDocument Product { get; set; } = default!; public string Status { get; set; } = default!; public DateTime CalculatedAt { get; set; } = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc); public List Sources { get; set; } = new(); public List Conflicts { get; set; } = new(); public string? PolicyVersion { get; set; } = null; public string? PolicyRevisionId { get; set; } = null; public string? PolicyDigest { get; set; } = null; public string? Summary { get; set; } = null; public static string CreateId(string vulnerabilityId, string productKey) => string.Format(CultureInfo.InvariantCulture, "{0}|{1}", vulnerabilityId.Trim(), productKey.Trim()); public static VexConsensusRecord FromDomain(VexConsensus consensus) => new() { Id = CreateId(consensus.VulnerabilityId, consensus.Product.Key), VulnerabilityId = consensus.VulnerabilityId, Product = VexProductDocument.FromDomain(consensus.Product), Status = consensus.Status.ToString().ToLowerInvariant(), CalculatedAt = consensus.CalculatedAt.UtcDateTime, Sources = consensus.Sources.Select(VexConsensusSourceDocument.FromDomain).ToList(), Conflicts = consensus.Conflicts.Select(VexConsensusConflictDocument.FromDomain).ToList(), PolicyVersion = consensus.PolicyVersion, PolicyRevisionId = consensus.PolicyRevisionId, PolicyDigest = consensus.PolicyDigest, Summary = consensus.Summary, }; public VexConsensus ToDomain() => new( VulnerabilityId, Product.ToDomain(), Enum.Parse(Status, ignoreCase: true), new DateTimeOffset(CalculatedAt, TimeSpan.Zero), Sources.Select(static source => source.ToDomain()), Conflicts.Select(static conflict => conflict.ToDomain()), PolicyVersion, Summary, PolicyRevisionId, PolicyDigest); } [BsonIgnoreExtraElements] internal sealed class VexProductDocument { public string Key { get; set; } = default!; public string? Name { get; set; } = null; public string? Version { get; set; } = null; public string? Purl { get; set; } = null; public string? Cpe { get; set; } = null; public List ComponentIdentifiers { get; set; } = new(); public static VexProductDocument FromDomain(VexProduct product) => new() { Key = product.Key, Name = product.Name, Version = product.Version, Purl = product.Purl, Cpe = product.Cpe, ComponentIdentifiers = product.ComponentIdentifiers.ToList(), }; public VexProduct ToDomain() => new( Key, Name, Version, Purl, Cpe, ComponentIdentifiers); } [BsonIgnoreExtraElements] internal sealed class VexConsensusSourceDocument { public string ProviderId { get; set; } = default!; public string Status { get; set; } = default!; public string DocumentDigest { get; set; } = default!; public double Weight { get; set; } = 0; public string? Justification { get; set; } = null; public string? Detail { get; set; } = null; public VexConfidenceDocument? Confidence { get; set; } = null; public static VexConsensusSourceDocument FromDomain(VexConsensusSource source) => new() { ProviderId = source.ProviderId, Status = source.Status.ToString().ToLowerInvariant(), DocumentDigest = source.DocumentDigest, Weight = source.Weight, Justification = source.Justification?.ToString().ToLowerInvariant(), Detail = source.Detail, Confidence = source.Confidence is null ? null : VexConfidenceDocument.FromDomain(source.Confidence), }; public VexConsensusSource ToDomain() => new( ProviderId, Enum.Parse(Status, ignoreCase: true), DocumentDigest, Weight, string.IsNullOrWhiteSpace(Justification) ? null : Enum.Parse(Justification, ignoreCase: true), Detail, Confidence?.ToDomain()); } [BsonIgnoreExtraElements] internal sealed class VexConsensusConflictDocument { public string ProviderId { get; set; } = default!; public string Status { get; set; } = default!; public string DocumentDigest { get; set; } = default!; public string? Justification { get; set; } = null; public string? Detail { get; set; } = null; public string? Reason { get; set; } = null; public static VexConsensusConflictDocument FromDomain(VexConsensusConflict conflict) => new() { ProviderId = conflict.ProviderId, Status = conflict.Status.ToString().ToLowerInvariant(), DocumentDigest = conflict.DocumentDigest, Justification = conflict.Justification?.ToString().ToLowerInvariant(), Detail = conflict.Detail, Reason = conflict.Reason, }; public VexConsensusConflict ToDomain() => new( ProviderId, Enum.Parse(Status, ignoreCase: true), DocumentDigest, string.IsNullOrWhiteSpace(Justification) ? null : Enum.Parse(Justification, ignoreCase: true), Detail, Reason); } [BsonIgnoreExtraElements] internal sealed class VexConfidenceDocument { public string Level { get; set; } = default!; public double? Score { get; set; } = null; public string? Method { get; set; } = null; public static VexConfidenceDocument FromDomain(VexConfidence confidence) => new() { Level = confidence.Level, Score = confidence.Score, Method = confidence.Method, }; public VexConfidence ToDomain() => new(Level, Score, Method); } [BsonIgnoreExtraElements] internal sealed class VexCacheEntryRecord { [BsonId] public string Id { get; set; } = default!; public string QuerySignature { get; set; } = default!; public string Format { get; set; } = default!; public string ArtifactAlgorithm { get; set; } = default!; public string ArtifactDigest { get; set; } = default!; public DateTime CreatedAt { get; set; } = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc); public long SizeBytes { get; set; } = 0; public string? ManifestId { get; set; } = null; [BsonRepresentation(BsonType.ObjectId)] public string? GridFsObjectId { get; set; } = null; public DateTime? ExpiresAt { get; set; } = null; public static string CreateId(VexQuerySignature signature, VexExportFormat format) => string.Format(CultureInfo.InvariantCulture, "{0}|{1}", signature.Value, format.ToString().ToLowerInvariant()); public static VexCacheEntryRecord FromDomain(VexCacheEntry entry) => new() { Id = CreateId(entry.QuerySignature, entry.Format), QuerySignature = entry.QuerySignature.Value, Format = entry.Format.ToString().ToLowerInvariant(), ArtifactAlgorithm = entry.Artifact.Algorithm, ArtifactDigest = entry.Artifact.Digest, CreatedAt = entry.CreatedAt.UtcDateTime, SizeBytes = entry.SizeBytes, ManifestId = entry.ManifestId, GridFsObjectId = entry.GridFsObjectId, ExpiresAt = entry.ExpiresAt?.UtcDateTime, }; public VexCacheEntry ToDomain() { var signature = new VexQuerySignature(QuerySignature); var artifact = new VexContentAddress(ArtifactAlgorithm, ArtifactDigest); var createdAt = new DateTimeOffset(DateTime.SpecifyKind(CreatedAt, DateTimeKind.Utc)); var expires = ExpiresAt.HasValue ? new DateTimeOffset(DateTime.SpecifyKind(ExpiresAt.Value, DateTimeKind.Utc)) : (DateTimeOffset?)null; return new VexCacheEntry( signature, Enum.Parse(Format, ignoreCase: true), artifact, createdAt, SizeBytes, ManifestId, GridFsObjectId, expires); } } [BsonIgnoreExtraElements] internal sealed class VexConnectorStateDocument { [BsonId] public string ConnectorId { get; set; } = default!; public DateTime? LastUpdated { get; set; } = null; public List DocumentDigests { get; set; } = new(); public static VexConnectorStateDocument FromRecord(VexConnectorState state) => new() { ConnectorId = state.ConnectorId, LastUpdated = state.LastUpdated?.UtcDateTime, DocumentDigests = state.DocumentDigests.ToList(), }; public VexConnectorState ToRecord() { var lastUpdated = LastUpdated.HasValue ? new DateTimeOffset(DateTime.SpecifyKind(LastUpdated.Value, DateTimeKind.Utc)) : (DateTimeOffset?)null; return new VexConnectorState( ConnectorId, lastUpdated, DocumentDigests.ToImmutableArray()); } }