Files
git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/AdvisoryPrecedenceMergerTests.cs
StellaOps Bot 564df71bfb
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
up
2025-12-13 00:20:26 +02:00

629 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Concelier.Merge.Options;
using StellaOps.Concelier.Merge.Services;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Merge.Tests;
public sealed class AdvisoryPrecedenceMergerTests
{
[Fact]
public void Merge_PrefersVendorPrecedenceOverNvd()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
var merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
using var metrics = new MetricCollector("StellaOps.Concelier.Merge");
var (redHat, nvd) = CreateVendorAndRegistryAdvisories();
var expectedMergeTimestamp = timeProvider.GetUtcNow();
var merged = merger.Merge(new[] { nvd, redHat }).Advisory;
Assert.Equal("CVE-2025-1000", merged.AdvisoryKey);
Assert.Equal("Red Hat Security Advisory", merged.Title);
Assert.Equal("Vendor-confirmed impact on RHEL 9.", merged.Summary);
Assert.Equal("high", merged.Severity);
Assert.Equal(redHat.Published, merged.Published);
Assert.Equal(redHat.Modified, merged.Modified);
Assert.Contains("RHSA-2025:0001", merged.Aliases);
Assert.Contains("CVE-2025-1000", merged.Aliases);
var package = Assert.Single(merged.AffectedPackages);
Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", package.Identifier);
Assert.Empty(package.VersionRanges); // NVD range suppressed by vendor precedence
Assert.Contains(package.Statuses, status => status.Status == "known_affected");
Assert.Contains(package.Provenance, provenance => provenance.Source == "redhat");
Assert.Contains(package.Provenance, provenance => provenance.Source == "nvd");
Assert.Contains(merged.CvssMetrics, metric => metric.Provenance.Source == "redhat");
Assert.Contains(merged.CvssMetrics, metric => metric.Provenance.Source == "nvd");
var mergeProvenance = merged.Provenance.Single(p => p.Source == "merge");
Assert.Equal("precedence", mergeProvenance.Kind);
Assert.Equal(expectedMergeTimestamp, mergeProvenance.RecordedAt);
Assert.Contains("redhat", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);
Assert.Contains("nvd", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);
var rangeMeasurement = Assert.Single(metrics.Measurements, measurement => measurement.Name == "concelier.merge.range_overrides");
Assert.Equal(1, rangeMeasurement.Value);
Assert.Contains(rangeMeasurement.Tags, tag => string.Equals(tag.Key, "suppressed_source", StringComparison.Ordinal) && tag.Value?.ToString()?.Contains("nvd", StringComparison.OrdinalIgnoreCase) == true);
var severityConflict = Assert.Single(metrics.Measurements, measurement => measurement.Name == "concelier.merge.conflicts");
Assert.Equal(1, severityConflict.Value);
Assert.Contains(severityConflict.Tags, tag => string.Equals(tag.Key, "type", StringComparison.Ordinal) && string.Equals(tag.Value?.ToString(), "severity", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Merge_KevOnlyTogglesExploitKnown()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 2, 1, 0, 0, 0, TimeSpan.Zero));
var merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
var nvdProvenance = new AdvisoryProvenance("nvd", "document", "https://nvd", timeProvider.GetUtcNow());
var baseAdvisory = new Advisory(
"CVE-2025-2000",
"CVE-2025-2000",
"Base registry summary",
"en",
new DateTimeOffset(2025, 1, 5, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2025, 1, 6, 0, 0, 0, TimeSpan.Zero),
"medium",
exploitKnown: false,
aliases: new[] { "CVE-2025-2000" },
credits: Array.Empty<AdvisoryCredit>(),
references: Array.Empty<AdvisoryReference>(),
affectedPackages: new[]
{
new AffectedPackage(
AffectedPackageTypes.Cpe,
"cpe:2.3:a:example:product:2.0:*:*:*:*:*:*:*",
null,
new[]
{
new AffectedVersionRange(
"semver",
"2.0.0",
"2.0.5",
null,
"<2.0.5",
new AdvisoryProvenance("nvd", "cpe_match", "product", timeProvider.GetUtcNow()))
},
Array.Empty<AffectedPackageStatus>(),
new[] { nvdProvenance })
},
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { nvdProvenance });
var kevProvenance = new AdvisoryProvenance("kev", "catalog", "CVE-2025-2000", timeProvider.GetUtcNow());
var kevAdvisory = new Advisory(
"CVE-2025-2000",
"Known Exploited Vulnerability",
summary: null,
language: null,
published: null,
modified: null,
severity: null,
exploitKnown: true,
aliases: new[] { "KEV-CVE-2025-2000" },
credits: Array.Empty<AdvisoryCredit>(),
references: Array.Empty<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { kevProvenance });
var merged = merger.Merge(new[] { baseAdvisory, kevAdvisory }).Advisory;
Assert.True(merged.ExploitKnown);
Assert.Equal("medium", merged.Severity); // KEV must not override severity
Assert.Equal("Base registry summary", merged.Summary);
Assert.Contains("CVE-2025-2000", merged.Aliases);
Assert.Contains("KEV-CVE-2025-2000", merged.Aliases);
Assert.Contains(merged.Provenance, provenance => provenance.Source == "kev");
Assert.Contains(merged.Provenance, provenance => provenance.Source == "merge");
}
[Fact]
public void Merge_UnionsCreditsFromSources()
{
var timeProvider = new FakeTimeProvider();
var merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
var ghsaCredits = new[]
{
new AdvisoryCredit(
displayName: "maintainer-team",
role: "remediation_developer",
contacts: new[] { "https://github.com/maintainer-team" },
provenance: new AdvisoryProvenance(
"ghsa",
"credit",
"mantainer-team",
timeProvider.GetUtcNow(),
new[] { ProvenanceFieldMasks.Credits })),
new AdvisoryCredit(
displayName: "security-reporter",
role: "reporter",
contacts: new[] { "https://github.com/security-reporter" },
provenance: new AdvisoryProvenance(
"ghsa",
"credit",
"security-reporter",
timeProvider.GetUtcNow(),
new[] { ProvenanceFieldMasks.Credits })),
};
var ghsa = new Advisory(
"CVE-2025-9000",
"GHSA advisory",
"Reported in GHSA",
"en",
timeProvider.GetUtcNow(),
timeProvider.GetUtcNow(),
"high",
exploitKnown: false,
aliases: new[] { "GHSA-aaaa-bbbb-cccc", "CVE-2025-9000" },
credits: ghsaCredits,
references: Array.Empty<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { new AdvisoryProvenance("ghsa", "document", "https://github.com/advisories/GHSA-aaaa-bbbb-cccc", timeProvider.GetUtcNow(), new[] { ProvenanceFieldMasks.Advisory }) });
var osvCredits = new[]
{
new AdvisoryCredit(
displayName: "osv-researcher",
role: "reporter",
contacts: new[] { "mailto:osv-researcher@example.com" },
provenance: new AdvisoryProvenance(
"osv",
"credit",
"osv-researcher",
timeProvider.GetUtcNow(),
new[] { ProvenanceFieldMasks.Credits })),
new AdvisoryCredit(
displayName: "maintainer-team",
role: "remediation_developer",
contacts: new[] { "https://github.com/maintainer-team" },
provenance: new AdvisoryProvenance(
"osv",
"credit",
"maintainer-team",
timeProvider.GetUtcNow(),
new[] { ProvenanceFieldMasks.Credits })),
};
var osv = new Advisory(
"CVE-2025-9000",
"OSV advisory",
"Reported in OSV.dev",
"en",
timeProvider.GetUtcNow().AddDays(-1),
timeProvider.GetUtcNow().AddHours(-1),
"medium",
exploitKnown: false,
aliases: new[] { "CVE-2025-9000" },
credits: osvCredits,
references: Array.Empty<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { new AdvisoryProvenance("osv", "document", "https://osv.dev/vulnerability/CVE-2025-9000", timeProvider.GetUtcNow(), new[] { ProvenanceFieldMasks.Advisory }) });
var merged = merger.Merge(new[] { ghsa, osv }).Advisory;
Assert.Equal("CVE-2025-9000", merged.AdvisoryKey);
Assert.Contains(merged.Credits, credit =>
string.Equals(credit.DisplayName, "maintainer-team", StringComparison.OrdinalIgnoreCase) &&
string.Equals(credit.Role, "remediation_developer", StringComparison.OrdinalIgnoreCase));
Assert.Contains(merged.Credits, credit =>
string.Equals(credit.DisplayName, "osv-researcher", StringComparison.OrdinalIgnoreCase) &&
string.Equals(credit.Role, "reporter", StringComparison.OrdinalIgnoreCase));
Assert.Contains(merged.Credits, credit =>
string.Equals(credit.DisplayName, "security-reporter", StringComparison.OrdinalIgnoreCase) &&
string.Equals(credit.Role, "reporter", StringComparison.OrdinalIgnoreCase));
Assert.Contains(merged.Credits, credit => credit.Provenance.Source == "ghsa");
Assert.Contains(merged.Credits, credit => credit.Provenance.Source == "osv");
}
[Fact]
public void Merge_AcscActsAsEnrichmentSource()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero));
var merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
var vendorDocumentProvenance = new AdvisoryProvenance(
source: "vndr-cisco",
kind: "document",
value: "https://vendor.example/advisories/router-critical",
recordedAt: timeProvider.GetUtcNow(),
fieldMask: new[] { ProvenanceFieldMasks.Advisory });
var vendorReference = new AdvisoryReference(
"https://vendor.example/advisories/router-critical",
kind: "advisory",
sourceTag: "vendor",
summary: "Vendor advisory",
provenance: new AdvisoryProvenance("vndr-cisco", "reference", "https://vendor.example/advisories/router-critical", timeProvider.GetUtcNow()));
var vendorPackage = new AffectedPackage(
AffectedPackageTypes.Vendor,
"ExampleCo Router X",
platform: null,
versionRanges: Array.Empty<AffectedVersionRange>(),
statuses: Array.Empty<AffectedPackageStatus>(),
normalizedVersions: Array.Empty<NormalizedVersionRule>(),
provenance: new[] { vendorDocumentProvenance });
var vendor = new Advisory(
advisoryKey: "acsc-2025-010",
title: "Vendor Critical Router Advisory",
summary: "Vendor-confirmed exploit.",
language: "en",
published: new DateTimeOffset(2025, 10, 11, 23, 0, 0, TimeSpan.Zero),
modified: new DateTimeOffset(2025, 10, 11, 23, 30, 0, TimeSpan.Zero),
severity: "critical",
exploitKnown: false,
aliases: new[] { "VENDOR-2025-010" },
references: new[] { vendorReference },
affectedPackages: new[] { vendorPackage },
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { vendorDocumentProvenance });
var acscDocumentProvenance = new AdvisoryProvenance(
source: "acsc",
kind: "document",
value: "https://origin.example/feeds/alerts/rss",
recordedAt: timeProvider.GetUtcNow(),
fieldMask: new[] { ProvenanceFieldMasks.Advisory });
var acscReference = new AdvisoryReference(
"https://origin.example/advisories/router-critical",
kind: "advisory",
sourceTag: "acsc",
summary: "ACSC alert",
provenance: new AdvisoryProvenance("acsc", "reference", "https://origin.example/advisories/router-critical", timeProvider.GetUtcNow()));
var acscPackage = new AffectedPackage(
AffectedPackageTypes.Vendor,
"ExampleCo Router X",
platform: null,
versionRanges: Array.Empty<AffectedVersionRange>(),
statuses: Array.Empty<AffectedPackageStatus>(),
normalizedVersions: Array.Empty<NormalizedVersionRule>(),
provenance: new[] { acscDocumentProvenance });
var acsc = new Advisory(
advisoryKey: "acsc-2025-010",
title: "ACSC Router Alert",
summary: "ACSC recommends installing vendor update.",
language: "en",
published: new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero),
modified: null,
severity: "medium",
exploitKnown: false,
aliases: new[] { "ACSC-2025-010" },
references: new[] { acscReference },
affectedPackages: new[] { acscPackage },
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { acscDocumentProvenance });
var merged = merger.Merge(new[] { acsc, vendor }).Advisory;
Assert.Equal("critical", merged.Severity); // ACSC must not override vendor severity
Assert.Equal("Vendor-confirmed exploit.", merged.Summary);
Assert.Contains("ACSC-2025-010", merged.Aliases);
Assert.Contains("VENDOR-2025-010", merged.Aliases);
Assert.Contains(merged.References, reference => reference.SourceTag == "vendor" && reference.Url == vendorReference.Url);
Assert.Contains(merged.References, reference => reference.SourceTag == "acsc" && reference.Url == acscReference.Url);
var enrichedPackage = Assert.Single(merged.AffectedPackages, package => package.Identifier == "ExampleCo Router X");
Assert.Contains(enrichedPackage.Provenance, provenance => provenance.Source == "vndr-cisco");
Assert.Contains(enrichedPackage.Provenance, provenance => provenance.Source == "acsc");
Assert.Contains(merged.Provenance, provenance => provenance.Source == "acsc");
Assert.Contains(merged.Provenance, provenance => provenance.Source == "vndr-cisco");
Assert.Contains(merged.Provenance, provenance => provenance.Source == "merge" && (provenance.Value?.Contains("acsc", StringComparison.OrdinalIgnoreCase) ?? false));
}
[Fact]
public void Merge_RecordsNormalizedRuleMetrics()
{
var now = new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero);
var timeProvider = new FakeTimeProvider(now);
var merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), timeProvider);
using var metrics = new MetricCollector("StellaOps.Concelier.Merge");
var normalizedRule = new NormalizedVersionRule(
NormalizedVersionSchemes.SemVer,
NormalizedVersionRuleTypes.Range,
min: "1.0.0",
minInclusive: true,
max: "2.0.0",
maxInclusive: false,
notes: "ghsa:GHSA-xxxx-yyyy");
var ghsaProvenance = new AdvisoryProvenance("ghsa", "package", "pkg:npm/example", now);
var ghsaPackage = new AffectedPackage(
AffectedPackageTypes.SemVer,
"pkg:npm/example",
platform: null,
versionRanges: new[]
{
new AffectedVersionRange(
NormalizedVersionSchemes.SemVer,
"1.0.0",
"2.0.0",
null,
">= 1.0.0 < 2.0.0",
ghsaProvenance)
},
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[]
{
ghsaProvenance,
},
normalizedVersions: new[] { normalizedRule });
var nvdPackage = new AffectedPackage(
AffectedPackageTypes.SemVer,
"pkg:npm/example",
platform: null,
versionRanges: new[]
{
new AffectedVersionRange(
NormalizedVersionSchemes.SemVer,
"1.0.0",
"2.0.0",
null,
">= 1.0.0 < 2.0.0",
new AdvisoryProvenance("nvd", "cpe_match", "pkg:npm/example", now))
},
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[]
{
new AdvisoryProvenance("nvd", "document", "https://nvd.nist.gov/vuln/detail/CVE-2025-7000", now),
},
normalizedVersions: Array.Empty<NormalizedVersionRule>());
var nvdExclusivePackage = new AffectedPackage(
AffectedPackageTypes.SemVer,
"pkg:npm/another",
platform: null,
versionRanges: new[]
{
new AffectedVersionRange(
NormalizedVersionSchemes.SemVer,
"3.0.0",
null,
null,
">= 3.0.0",
new AdvisoryProvenance("nvd", "cpe_match", "pkg:npm/another", now))
},
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[]
{
new AdvisoryProvenance("nvd", "document", "https://nvd.nist.gov/vuln/detail/CVE-2025-7000", now),
},
normalizedVersions: Array.Empty<NormalizedVersionRule>());
var ghsaAdvisory = new Advisory(
"CVE-2025-7000",
"GHSA advisory",
"GHSA summary",
"en",
now,
now,
"high",
exploitKnown: false,
aliases: new[] { "CVE-2025-7000", "GHSA-xxxx-yyyy" },
credits: Array.Empty<AdvisoryCredit>(),
references: Array.Empty<AdvisoryReference>(),
affectedPackages: new[] { ghsaPackage },
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[]
{
new AdvisoryProvenance("ghsa", "document", "https://github.com/advisories/GHSA-xxxx-yyyy", now),
});
var nvdAdvisory = new Advisory(
"CVE-2025-7000",
"NVD entry",
"NVD summary",
"en",
now,
now,
"high",
exploitKnown: false,
aliases: new[] { "CVE-2025-7000" },
credits: Array.Empty<AdvisoryCredit>(),
references: Array.Empty<AdvisoryReference>(),
affectedPackages: new[] { nvdPackage, nvdExclusivePackage },
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[]
{
new AdvisoryProvenance("nvd", "document", "https://nvd.nist.gov/vuln/detail/CVE-2025-7000", now),
});
var merged = merger.Merge(new[] { nvdAdvisory, ghsaAdvisory }).Advisory;
Assert.Equal(2, merged.AffectedPackages.Length);
var normalizedPackage = Assert.Single(merged.AffectedPackages, pkg => pkg.Identifier == "pkg:npm/example");
Assert.Single(normalizedPackage.NormalizedVersions);
var missingPackage = Assert.Single(merged.AffectedPackages, pkg => pkg.Identifier == "pkg:npm/another");
Assert.Empty(missingPackage.NormalizedVersions);
Assert.NotEmpty(missingPackage.VersionRanges);
var normalizedMeasurements = metrics.Measurements.Where(m => m.Name == "concelier.merge.normalized_rules").ToList();
Assert.Contains(normalizedMeasurements, measurement =>
measurement.Value == 1
&& measurement.Tags.Any(tag => string.Equals(tag.Key, "scheme", StringComparison.Ordinal) && string.Equals(tag.Value?.ToString(), "semver", StringComparison.Ordinal))
&& measurement.Tags.Any(tag => string.Equals(tag.Key, "package_type", StringComparison.Ordinal) && string.Equals(tag.Value?.ToString(), "semver", StringComparison.Ordinal)));
var missingMeasurements = metrics.Measurements.Where(m => m.Name == "concelier.merge.normalized_rules_missing").ToList();
var missingMeasurement = Assert.Single(missingMeasurements);
Assert.Equal(1, missingMeasurement.Value);
Assert.Contains(missingMeasurement.Tags, tag => string.Equals(tag.Key, "package_type", StringComparison.Ordinal) && string.Equals(tag.Value?.ToString(), "semver", StringComparison.Ordinal));
}
[Fact]
public void Merge_RespectsConfiguredPrecedenceOverrides()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero));
var options = new AdvisoryPrecedenceOptions
{
Ranks = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["nvd"] = 0,
["redhat"] = 5,
}
};
var logger = new TestLogger<AdvisoryPrecedenceMerger>();
using var metrics = new MetricCollector("StellaOps.Concelier.Merge");
var merger = new AdvisoryPrecedenceMerger(
new AffectedPackagePrecedenceResolver(),
options,
timeProvider,
logger);
var (redHat, nvd) = CreateVendorAndRegistryAdvisories();
var merged = merger.Merge(new[] { redHat, nvd }).Advisory;
Assert.Equal("CVE-2025-1000", merged.AdvisoryKey);
Assert.Equal("CVE-2025-1000", merged.Title); // NVD preferred
Assert.Equal("NVD summary", merged.Summary);
Assert.Equal("medium", merged.Severity);
var package = Assert.Single(merged.AffectedPackages);
Assert.NotEmpty(package.VersionRanges); // Vendor range no longer overrides
Assert.Contains(package.Provenance, provenance => provenance.Source == "nvd");
Assert.Contains(package.Provenance, provenance => provenance.Source == "redhat");
var overrideMeasurement = Assert.Single(metrics.Measurements, m => m.Name == "concelier.merge.overrides");
Assert.Equal(1, overrideMeasurement.Value);
Assert.Contains(overrideMeasurement.Tags, tag => tag.Key == "primary_source" && string.Equals(tag.Value?.ToString(), "nvd", StringComparison.OrdinalIgnoreCase));
Assert.Contains(overrideMeasurement.Tags, tag => tag.Key == "suppressed_source" && tag.Value?.ToString()?.Contains("redhat", StringComparison.OrdinalIgnoreCase) == true);
Assert.DoesNotContain(metrics.Measurements, measurement => measurement.Name == "concelier.merge.range_overrides");
var conflictMeasurement = Assert.Single(metrics.Measurements, measurement => measurement.Name == "concelier.merge.conflicts");
Assert.Equal(1, conflictMeasurement.Value);
Assert.Contains(conflictMeasurement.Tags, tag => tag.Key == "type" && string.Equals(tag.Value?.ToString(), "severity", StringComparison.OrdinalIgnoreCase));
Assert.Contains(conflictMeasurement.Tags, tag => tag.Key == "reason" && string.Equals(tag.Value?.ToString(), "mismatch", StringComparison.OrdinalIgnoreCase));
var logEntry = Assert.Single(logger.Entries, entry => entry.EventId.Name == "AdvisoryOverride");
Assert.Equal(LogLevel.Information, logEntry.Level);
Assert.NotNull(logEntry.StructuredState);
Assert.Contains(logEntry.StructuredState!, kvp =>
(string.Equals(kvp.Key, "Override", StringComparison.Ordinal) ||
string.Equals(kvp.Key, "@Override", StringComparison.Ordinal)) &&
kvp.Value is not null);
}
private static (Advisory Vendor, Advisory Registry) CreateVendorAndRegistryAdvisories()
{
var redHatPublished = new DateTimeOffset(2025, 1, 10, 0, 0, 0, TimeSpan.Zero);
var redHatModified = redHatPublished.AddDays(1);
var redHatProvenance = new AdvisoryProvenance("redhat", "advisory", "RHSA-2025:0001", redHatModified);
var redHatPackage = new AffectedPackage(
AffectedPackageTypes.Cpe,
"cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
"rhel-9",
Array.Empty<AffectedVersionRange>(),
new[] { new AffectedPackageStatus("known_affected", redHatProvenance) },
new[] { redHatProvenance });
var redHat = new Advisory(
"CVE-2025-1000",
"Red Hat Security Advisory",
"Vendor-confirmed impact on RHEL 9.",
"en",
redHatPublished,
redHatModified,
"high",
exploitKnown: false,
aliases: new[] { "CVE-2025-1000", "RHSA-2025:0001" },
credits: Array.Empty<AdvisoryCredit>(),
references: new[]
{
new AdvisoryReference(
"https://access.redhat.com/errata/RHSA-2025:0001",
"advisory",
"redhat",
"Red Hat errata",
redHatProvenance)
},
affectedPackages: new[] { redHatPackage },
cvssMetrics: new[]
{
new CvssMetric(
"3.1",
"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
9.8,
"critical",
new AdvisoryProvenance("redhat", "cvss", "RHSA-2025:0001", redHatModified))
},
provenance: new[] { redHatProvenance });
var nvdPublished = new DateTimeOffset(2025, 1, 5, 0, 0, 0, TimeSpan.Zero);
var nvdModified = nvdPublished.AddDays(2);
var nvdProvenance = new AdvisoryProvenance("nvd", "document", "https://nvd.nist.gov/vuln/detail/CVE-2025-1000", nvdModified);
var nvdPackage = new AffectedPackage(
AffectedPackageTypes.Cpe,
"cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
"rhel-9",
new[]
{
new AffectedVersionRange(
"cpe",
null,
null,
null,
"<=9.0",
new AdvisoryProvenance("nvd", "cpe_match", "RHEL", nvdModified))
},
Array.Empty<AffectedPackageStatus>(),
new[] { nvdProvenance });
var nvd = new Advisory(
"CVE-2025-1000",
"CVE-2025-1000",
"NVD summary",
"en",
nvdPublished,
nvdModified,
"medium",
exploitKnown: false,
aliases: new[] { "CVE-2025-1000" },
credits: Array.Empty<AdvisoryCredit>(),
references: new[]
{
new AdvisoryReference(
"https://nvd.nist.gov/vuln/detail/CVE-2025-1000",
"advisory",
"nvd",
"NVD advisory",
nvdProvenance)
},
affectedPackages: new[] { nvdPackage },
cvssMetrics: new[]
{
new CvssMetric(
"3.1",
"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N",
6.8,
"medium",
new AdvisoryProvenance("nvd", "cvss", "CVE-2025-1000", nvdModified))
},
provenance: new[] { nvdProvenance });
return (redHat, nvd);
}
}