using System.Collections.Immutable; using System.Linq; using System.Text; using System.IO; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Excititor.Core; using StellaOps.Excititor.Formats.CSAF; namespace StellaOps.Excititor.Formats.CSAF.Tests; public sealed class CsafNormalizerTests { [Fact] public async Task NormalizeAsync_ProducesClaimsPerProductStatus() { var json = """ { "document": { "tracking": { "id": "RHSA-2025:0001", "version": "3", "revision": "3", "status": "final", "initial_release_date": "2025-10-01T00:00:00Z", "current_release_date": "2025-10-10T00:00:00Z" }, "publisher": { "name": "Red Hat Product Security", "category": "vendor" } }, "product_tree": { "full_product_names": [ { "product_id": "CSAFPID-0001", "name": "Red Hat Enterprise Linux 9", "product_identification_helper": { "cpe": "cpe:/o:redhat:enterprise_linux:9", "purl": "pkg:rpm/redhat/enterprise-linux@9" } } ] }, "vulnerabilities": [ { "cve": "CVE-2025-0001", "title": "Kernel vulnerability", "product_status": { "known_affected": [ "CSAFPID-0001" ] } }, { "id": "VEX-0002", "title": "Library issue", "product_status": { "known_not_affected": [ "CSAFPID-0001" ] } } ] } """; var rawDocument = new VexRawDocument( ProviderId: "excititor:redhat", VexDocumentFormat.Csaf, new Uri("https://example.com/csaf/rhsa-2025-0001.json"), new DateTimeOffset(2025, 10, 11, 0, 0, 0, TimeSpan.Zero), "sha256:dummydigest", Encoding.UTF8.GetBytes(json), ImmutableDictionary.Empty); var provider = new VexProvider("excititor:redhat", "Red Hat CSAF", VexProviderKind.Distro); var normalizer = new CsafNormalizer(NullLogger.Instance); var batch = await normalizer.NormalizeAsync(rawDocument, provider, CancellationToken.None); batch.Claims.Should().HaveCount(2); var affectedClaim = batch.Claims.First(c => c.VulnerabilityId == "CVE-2025-0001"); affectedClaim.Status.Should().Be(VexClaimStatus.Affected); affectedClaim.Product.Key.Should().Be("CSAFPID-0001"); affectedClaim.Product.Purl.Should().Be("pkg:rpm/redhat/enterprise-linux@9"); affectedClaim.Product.Cpe.Should().Be("cpe:/o:redhat:enterprise_linux:9"); affectedClaim.Document.Revision.Should().Be("3"); affectedClaim.FirstSeen.Should().Be(new DateTimeOffset(2025, 10, 1, 0, 0, 0, TimeSpan.Zero)); affectedClaim.LastSeen.Should().Be(new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero)); affectedClaim.AdditionalMetadata.Should().ContainKey("csaf.tracking.id"); affectedClaim.AdditionalMetadata["csaf.publisher.name"].Should().Be("Red Hat Product Security"); var notAffectedClaim = batch.Claims.First(c => c.VulnerabilityId == "VEX-0002"); notAffectedClaim.Status.Should().Be(VexClaimStatus.NotAffected); } [Fact] public async Task NormalizeAsync_PreservesRedHatSpecificMetadata() { var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", "rhsa-sample.json"); var json = await File.ReadAllTextAsync(path); var rawDocument = new VexRawDocument( ProviderId: "excititor:redhat", VexDocumentFormat.Csaf, new Uri("https://security.example.com/rhsa-2025-1001.json"), new DateTimeOffset(2025, 10, 6, 0, 0, 0, TimeSpan.Zero), "sha256:rhdadigest", Encoding.UTF8.GetBytes(json), ImmutableDictionary.Empty); var provider = new VexProvider("excititor:redhat", "Red Hat CSAF", VexProviderKind.Distro); var normalizer = new CsafNormalizer(NullLogger.Instance); var batch = await normalizer.NormalizeAsync(rawDocument, provider, CancellationToken.None); batch.Claims.Should().ContainSingle(); var claim = batch.Claims[0]; claim.VulnerabilityId.Should().Be("CVE-2025-1234"); claim.Status.Should().Be(VexClaimStatus.Affected); claim.Product.Key.Should().Be("rh-enterprise-linux-9"); claim.Product.Name.Should().Be("Red Hat Enterprise Linux 9"); claim.Product.Purl.Should().Be("pkg:rpm/redhat/enterprise-linux@9"); claim.Product.Cpe.Should().Be("cpe:/o:redhat:enterprise_linux:9"); claim.FirstSeen.Should().Be(new DateTimeOffset(2025, 10, 1, 12, 0, 0, TimeSpan.Zero)); claim.LastSeen.Should().Be(new DateTimeOffset(2025, 10, 5, 10, 0, 0, TimeSpan.Zero)); claim.AdditionalMetadata.Should().ContainKey("csaf.tracking.id"); claim.AdditionalMetadata["csaf.tracking.id"].Should().Be("RHSA-2025:1001"); claim.AdditionalMetadata["csaf.tracking.status"].Should().Be("final"); claim.AdditionalMetadata["csaf.publisher.name"].Should().Be("Red Hat Product Security"); } }