using StellaOps.Concelier.Models; namespace StellaOps.Concelier.Core.Tests; public sealed class CanonicalMergerTests { private static readonly DateTimeOffset BaseTimestamp = new(2025, 10, 10, 0, 0, 0, TimeSpan.Zero); [Fact] public void Merge_PrefersGhsaTitleAndSummaryByPrecedence() { var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(6))); var ghsa = CreateAdvisory( source: "ghsa", advisoryKey: "GHSA-aaaa-bbbb-cccc", title: "GHSA Title", summary: "GHSA Summary", modified: BaseTimestamp.AddHours(1)); var nvd = CreateAdvisory( source: "nvd", advisoryKey: "CVE-2025-0001", title: "NVD Title", summary: "NVD Summary", modified: BaseTimestamp); var result = merger.Merge("CVE-2025-0001", ghsa, nvd, null); Assert.Equal("GHSA Title", result.Advisory.Title); Assert.Equal("GHSA Summary", result.Advisory.Summary); Assert.Contains(result.Decisions, decision => decision.Field == "summary" && string.Equals(decision.SelectedSource, "ghsa", StringComparison.OrdinalIgnoreCase) && string.Equals(decision.DecisionReason, "precedence", StringComparison.OrdinalIgnoreCase)); Assert.Contains(result.Advisory.Provenance, provenance => string.Equals(provenance.Source, "ghsa", StringComparison.OrdinalIgnoreCase) && string.Equals(provenance.Kind, "merge", StringComparison.OrdinalIgnoreCase) && string.Equals(provenance.Value, "summary", StringComparison.OrdinalIgnoreCase) && string.Equals(provenance.DecisionReason, "precedence", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Merge_FreshnessOverrideUsesOsvSummaryWhenNewerByThreshold() { var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(10))); var ghsa = CreateAdvisory( source: "ghsa", advisoryKey: "GHSA-xxxx-yyyy-zzzz", title: "Container Escape Vulnerability", summary: "Initial GHSA summary.", modified: BaseTimestamp); var osv = CreateAdvisory( source: "osv", advisoryKey: "GHSA-xxxx-yyyy-zzzz", title: "Container Escape Vulnerability", summary: "OSV summary with additional mitigation steps.", modified: BaseTimestamp.AddHours(72)); var result = merger.Merge("CVE-2025-9000", ghsa, null, osv); Assert.Equal("OSV summary with additional mitigation steps.", result.Advisory.Summary); Assert.Contains(result.Decisions, decision => decision.Field == "summary" && string.Equals(decision.SelectedSource, "osv", StringComparison.OrdinalIgnoreCase) && string.Equals(decision.DecisionReason, "freshness_override", StringComparison.OrdinalIgnoreCase)); Assert.Contains(result.Advisory.Provenance, provenance => string.Equals(provenance.Source, "osv", StringComparison.OrdinalIgnoreCase) && string.Equals(provenance.Kind, "merge", StringComparison.OrdinalIgnoreCase) && string.Equals(provenance.Value, "summary", StringComparison.OrdinalIgnoreCase) && string.Equals(provenance.DecisionReason, "freshness_override", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Merge_AffectedPackagesPreferOsvPrecedence() { var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(4))); var ghsaPackage = new AffectedPackage( AffectedPackageTypes.SemVer, "pkg:npm/example@1", platform: null, versionRanges: new[] { new AffectedVersionRange( rangeKind: "semver", introducedVersion: null, fixedVersion: "1.2.3", lastAffectedVersion: null, rangeExpression: "<1.2.3", provenance: CreateProvenance("ghsa", ProvenanceFieldMasks.VersionRanges), primitives: null) }, statuses: new[] { new AffectedPackageStatus( "affected", CreateProvenance("ghsa", ProvenanceFieldMasks.PackageStatuses)) }, provenance: new[] { CreateProvenance("ghsa", ProvenanceFieldMasks.AffectedPackages) }, normalizedVersions: Array.Empty()); var nvdPackage = new AffectedPackage( AffectedPackageTypes.SemVer, "pkg:npm/example@1", platform: null, versionRanges: new[] { new AffectedVersionRange( rangeKind: "semver", introducedVersion: null, fixedVersion: "1.2.4", lastAffectedVersion: null, rangeExpression: "<1.2.4", provenance: CreateProvenance("nvd", ProvenanceFieldMasks.VersionRanges), primitives: null) }, statuses: Array.Empty(), provenance: new[] { CreateProvenance("nvd", ProvenanceFieldMasks.AffectedPackages) }, normalizedVersions: Array.Empty()); var osvPackage = new AffectedPackage( AffectedPackageTypes.SemVer, "pkg:npm/example@1", platform: null, versionRanges: new[] { new AffectedVersionRange( rangeKind: "semver", introducedVersion: "1.0.0", fixedVersion: "1.2.5", lastAffectedVersion: null, rangeExpression: ">=1.0.0,<1.2.5", provenance: CreateProvenance("osv", ProvenanceFieldMasks.VersionRanges), primitives: null) }, statuses: Array.Empty(), provenance: new[] { CreateProvenance("osv", ProvenanceFieldMasks.AffectedPackages) }, normalizedVersions: Array.Empty()); var ghsa = CreateAdvisory("ghsa", "GHSA-1234", "GHSA Title", modified: BaseTimestamp.AddHours(1), packages: new[] { ghsaPackage }); var nvd = CreateAdvisory("nvd", "CVE-2025-1111", "NVD Title", modified: BaseTimestamp.AddHours(2), packages: new[] { nvdPackage }); var osv = CreateAdvisory("osv", "OSV-2025-xyz", "OSV Title", modified: BaseTimestamp.AddHours(3), packages: new[] { osvPackage }); var result = merger.Merge("CVE-2025-1111", ghsa, nvd, osv); var package = Assert.Single(result.Advisory.AffectedPackages); Assert.Equal("pkg:npm/example@1", package.Identifier); Assert.Contains(package.Provenance, provenance => string.Equals(provenance.Source, "osv", StringComparison.OrdinalIgnoreCase) && string.Equals(provenance.Kind, "merge", StringComparison.OrdinalIgnoreCase) && string.Equals(provenance.DecisionReason, "precedence", StringComparison.OrdinalIgnoreCase)); Assert.Contains(result.Decisions, decision => decision.Field.StartsWith("affectedPackages", StringComparison.OrdinalIgnoreCase) && string.Equals(decision.SelectedSource, "osv", StringComparison.OrdinalIgnoreCase) && string.Equals(decision.DecisionReason, "precedence", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Merge_CvssMetricsOrderedByPrecedence() { var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(5))); var nvdMetric = new CvssMetric("3.1", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", 9.8, "critical", CreateProvenance("nvd", ProvenanceFieldMasks.CvssMetrics)); var ghsaMetric = new CvssMetric("3.0", "CVSS:3.0/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H", 7.5, "high", CreateProvenance("ghsa", ProvenanceFieldMasks.CvssMetrics)); var nvd = CreateAdvisory("nvd", "CVE-2025-2000", "NVD Title", severity: null, modified: BaseTimestamp, metrics: new[] { nvdMetric }); var ghsa = CreateAdvisory("ghsa", "GHSA-9999", "GHSA Title", severity: null, modified: BaseTimestamp.AddHours(1), metrics: new[] { ghsaMetric }); var result = merger.Merge("CVE-2025-2000", ghsa, nvd, null); Assert.Equal(2, result.Advisory.CvssMetrics.Length); Assert.Equal("nvd", result.Decisions.Single(decision => decision.Field == "cvssMetrics").SelectedSource); Assert.Equal("critical", result.Advisory.Severity); Assert.Contains(result.Advisory.CvssMetrics, metric => metric.Vector == "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"); Assert.Contains(result.Advisory.CvssMetrics, metric => metric.Vector == "CVSS:3.0/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H"); Assert.Equal("3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", result.Advisory.CanonicalMetricId); } [Fact] public void Merge_ReferencesNormalizedAndFreshnessOverrides() { var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(80))); var ghsa = CreateAdvisory( source: "ghsa", advisoryKey: "GHSA-ref", title: "GHSA Title", references: new[] { new AdvisoryReference( "http://Example.COM/path/resource?b=2&a=1#section", kind: "advisory", sourceTag: null, summary: null, CreateProvenance("ghsa", ProvenanceFieldMasks.References)) }, modified: BaseTimestamp); var osv = CreateAdvisory( source: "osv", advisoryKey: "OSV-ref", title: "OSV Title", references: new[] { new AdvisoryReference( "https://example.com/path/resource?a=1&b=2", kind: "advisory", sourceTag: null, summary: null, CreateProvenance("osv", ProvenanceFieldMasks.References)) }, modified: BaseTimestamp.AddHours(80)); var result = merger.Merge("CVE-REF-2025-01", ghsa, null, osv); var reference = Assert.Single(result.Advisory.References); Assert.Equal("https://example.com/path/resource?a=1&b=2", reference.Url); var unionDecision = Assert.Single(result.Decisions.Where(decision => decision.Field == "references")); Assert.Null(unionDecision.SelectedSource); Assert.Equal("union", unionDecision.DecisionReason); var itemDecision = Assert.Single(result.Decisions.Where(decision => decision.Field.StartsWith("references[", StringComparison.OrdinalIgnoreCase))); Assert.Equal("osv", itemDecision.SelectedSource); Assert.Equal("freshness_override", itemDecision.DecisionReason); Assert.Contains("https://example.com/path/resource?a=1&b=2", itemDecision.Field, StringComparison.OrdinalIgnoreCase); } [Fact] public void Merge_DescriptionFreshnessOverride() { var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(12))); var ghsa = CreateAdvisory( source: "ghsa", advisoryKey: "GHSA-desc", title: "GHSA Title", summary: "Summary", description: "Initial GHSA description", modified: BaseTimestamp.AddHours(1)); var nvd = CreateAdvisory( source: "nvd", advisoryKey: "CVE-2025-5555", title: "NVD Title", summary: "Summary", description: "NVD baseline description", modified: BaseTimestamp.AddHours(2)); var osv = CreateAdvisory( source: "osv", advisoryKey: "OSV-2025-desc", title: "OSV Title", summary: "Summary", description: "OSV fresher description", modified: BaseTimestamp.AddHours(72)); var result = merger.Merge("CVE-2025-5555", ghsa, nvd, osv); Assert.Equal("OSV fresher description", result.Advisory.Description); Assert.Contains(result.Decisions, decision => decision.Field == "description" && string.Equals(decision.SelectedSource, "osv", StringComparison.OrdinalIgnoreCase) && string.Equals(decision.DecisionReason, "freshness_override", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Merge_CwesPreferNvdPrecedence() { var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(6))); var ghsaWeakness = CreateWeakness("ghsa", "CWE-79", "cross-site scripting", BaseTimestamp.AddHours(1)); var nvdWeakness = CreateWeakness("nvd", "CWE-79", "Cross-Site Scripting", BaseTimestamp.AddHours(2)); var osvWeakness = CreateWeakness("osv", "CWE-79", "XSS", BaseTimestamp.AddHours(3)); var ghsa = CreateAdvisory("ghsa", "GHSA-weakness", "GHSA Title", weaknesses: new[] { ghsaWeakness }, modified: BaseTimestamp.AddHours(1)); var nvd = CreateAdvisory("nvd", "CVE-2025-7777", "NVD Title", weaknesses: new[] { nvdWeakness }, modified: BaseTimestamp.AddHours(2)); var osv = CreateAdvisory("osv", "OSV-weakness", "OSV Title", weaknesses: new[] { osvWeakness }, modified: BaseTimestamp.AddHours(3)); var result = merger.Merge("CVE-2025-7777", ghsa, nvd, osv); var weakness = Assert.Single(result.Advisory.Cwes); Assert.Equal("CWE-79", weakness.Identifier); Assert.Equal("Cross-Site Scripting", weakness.Name); Assert.Contains(result.Decisions, decision => decision.Field == "cwes[cwe|CWE-79]" && string.Equals(decision.SelectedSource, "nvd", StringComparison.OrdinalIgnoreCase) && string.Equals(decision.DecisionReason, "precedence", StringComparison.OrdinalIgnoreCase)); } private static Advisory CreateAdvisory( string source, string advisoryKey, string title, string? summary = null, string? description = null, DateTimeOffset? modified = null, string? severity = null, IEnumerable? packages = null, IEnumerable? metrics = null, IEnumerable? references = null, IEnumerable? weaknesses = null, string? canonicalMetricId = null) { var provenance = new AdvisoryProvenance( source, kind: "map", value: advisoryKey, recordedAt: modified ?? BaseTimestamp, fieldMask: new[] { ProvenanceFieldMasks.Advisory }); return new Advisory( advisoryKey, title, summary, language: "en", published: modified, modified: modified, severity: severity, exploitKnown: false, aliases: new[] { advisoryKey }, credits: Array.Empty(), references: references ?? Array.Empty(), affectedPackages: packages ?? Array.Empty(), cvssMetrics: metrics ?? Array.Empty(), provenance: new[] { provenance }, description: description, cwes: weaknesses ?? Array.Empty(), canonicalMetricId: canonicalMetricId); } private static AdvisoryProvenance CreateProvenance(string source, string fieldMask) => new( source, kind: "map", value: source, recordedAt: BaseTimestamp, fieldMask: new[] { fieldMask }); private static AdvisoryWeakness CreateWeakness(string source, string identifier, string? name, DateTimeOffset recordedAt) { var provenance = new AdvisoryProvenance( source, kind: "map", value: identifier, recordedAt: recordedAt, fieldMask: new[] { ProvenanceFieldMasks.Weaknesses }); return new AdvisoryWeakness("cwe", identifier, name, uri: null, provenance: new[] { provenance }); } private sealed class FixedTimeProvider : TimeProvider { private readonly DateTimeOffset _utcNow; public FixedTimeProvider(DateTimeOffset utcNow) { _utcNow = utcNow; } public override DateTimeOffset GetUtcNow() => _utcNow; } }