Rename Feedser to Concelier

This commit is contained in:
master
2025-10-18 20:04:15 +03:00
parent dd66f58b00
commit 89ede53cc3
1208 changed files with 4370 additions and 4370 deletions

View File

@@ -0,0 +1,49 @@
using System;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class AdvisoryProvenanceTests
{
[Fact]
public void FieldMask_NormalizesAndDeduplicates()
{
var timestamp = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var provenance = new AdvisoryProvenance(
source: "nvd",
kind: "map",
value: "CVE-2025-0001",
recordedAt: timestamp,
fieldMask: new[] { " AffectedPackages[] ", "affectedpackages[]", "references[]" });
Assert.Equal(timestamp, provenance.RecordedAt);
Assert.Collection(
provenance.FieldMask,
mask => Assert.Equal("affectedpackages[]", mask),
mask => Assert.Equal("references[]", mask));
Assert.Null(provenance.DecisionReason);
}
[Fact]
public void EmptyProvenance_ExposesEmptyFieldMask()
{
Assert.True(AdvisoryProvenance.Empty.FieldMask.IsEmpty);
Assert.Null(AdvisoryProvenance.Empty.DecisionReason);
}
[Fact]
public void DecisionReason_IsTrimmed()
{
var timestamp = DateTimeOffset.Parse("2025-03-01T00:00:00Z");
var provenance = new AdvisoryProvenance(
source: "merge",
kind: "precedence",
value: "summary",
recordedAt: timestamp,
fieldMask: new[] { ProvenanceFieldMasks.Advisory },
decisionReason: " freshness_override ");
Assert.Equal("freshness_override", provenance.DecisionReason);
}
}

View File

@@ -0,0 +1,62 @@
using System.Linq;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Models.Tests;
public sealed class AdvisoryTests
{
[Fact]
public void CanonicalizesAliasesAndReferences()
{
var advisory = new Advisory(
advisoryKey: "TEST-123",
title: "Sample Advisory",
summary: " summary with spaces ",
language: "EN",
published: DateTimeOffset.Parse("2024-01-01T00:00:00Z"),
modified: DateTimeOffset.Parse("2024-01-02T00:00:00Z"),
severity: "CRITICAL",
exploitKnown: true,
aliases: new[] { " CVE-2024-0001", "GHSA-aaaa", "cve-2024-0001" },
references: new[]
{
new AdvisoryReference("https://example.com/b", "patch", null, null, AdvisoryProvenance.Empty),
new AdvisoryReference("https://example.com/a", null, null, null, AdvisoryProvenance.Empty),
},
affectedPackages: new[]
{
new AffectedPackage(
type: AffectedPackageTypes.SemVer,
identifier: "pkg:npm/sample",
platform: "node",
versionRanges: new[]
{
new AffectedVersionRange("semver", "1.0.0", "1.0.1", null, null, AdvisoryProvenance.Empty),
new AffectedVersionRange("semver", "1.0.0", "1.0.1", null, null, AdvisoryProvenance.Empty),
new AffectedVersionRange("semver", "0.9.0", null, "0.9.9", null, AdvisoryProvenance.Empty),
},
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[]
{
new AdvisoryProvenance("nvd", "map", "", DateTimeOffset.Parse("2024-01-01T00:00:00Z")),
new AdvisoryProvenance("vendor", "map", "", DateTimeOffset.Parse("2024-01-02T00:00:00Z")),
})
},
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[]
{
new AdvisoryProvenance("nvd", "map", "", DateTimeOffset.Parse("2024-01-01T00:00:00Z")),
new AdvisoryProvenance("vendor", "map", "", DateTimeOffset.Parse("2024-01-02T00:00:00Z")),
});
Assert.Equal(new[] { "CVE-2024-0001", "GHSA-aaaa" }, advisory.Aliases);
Assert.Equal(new[] { "https://example.com/a", "https://example.com/b" }, advisory.References.Select(r => r.Url));
Assert.Equal(
new[]
{
"semver|0.9.0||0.9.9|",
"semver|1.0.0|1.0.1||",
},
advisory.AffectedPackages.Single().VersionRanges.Select(r => r.CreateDeterministicKey()));
}
}

View File

@@ -0,0 +1,75 @@
using System;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Models.Tests;
public sealed class AffectedPackageStatusTests
{
[Theory]
[InlineData("Known_Affected", AffectedPackageStatusCatalog.KnownAffected)]
[InlineData("KNOWN-NOT-AFFECTED", AffectedPackageStatusCatalog.KnownNotAffected)]
[InlineData("Under Investigation", AffectedPackageStatusCatalog.UnderInvestigation)]
[InlineData("Fixed", AffectedPackageStatusCatalog.Fixed)]
[InlineData("Known not vulnerable", AffectedPackageStatusCatalog.KnownNotAffected)]
[InlineData("Impacted", AffectedPackageStatusCatalog.Affected)]
[InlineData("Not Vulnerable", AffectedPackageStatusCatalog.NotAffected)]
[InlineData("Analysis in progress", AffectedPackageStatusCatalog.UnderInvestigation)]
[InlineData("Patch Available", AffectedPackageStatusCatalog.Fixed)]
[InlineData("Workaround available", AffectedPackageStatusCatalog.Mitigated)]
[InlineData("Does Not Apply", AffectedPackageStatusCatalog.NotApplicable)]
[InlineData("Awaiting fix", AffectedPackageStatusCatalog.Pending)]
[InlineData("TBD", AffectedPackageStatusCatalog.Unknown)]
public void Constructor_NormalizesStatus(string input, string expected)
{
var provenance = new AdvisoryProvenance("test", "status", "value", DateTimeOffset.UtcNow);
var status = new AffectedPackageStatus(input, provenance);
Assert.Equal(expected, status.Status);
Assert.Equal(provenance, status.Provenance);
}
[Fact]
public void Constructor_ThrowsForUnknownStatus()
{
var provenance = new AdvisoryProvenance("test", "status", "value", DateTimeOffset.UtcNow);
Assert.Throws<ArgumentOutOfRangeException>(() => new AffectedPackageStatus("unsupported", provenance));
}
[Theory]
[InlineData("Not Impacted", AffectedPackageStatusCatalog.NotAffected)]
[InlineData("Resolved", AffectedPackageStatusCatalog.Fixed)]
[InlineData("Mitigation provided", AffectedPackageStatusCatalog.Mitigated)]
[InlineData("Out of scope", AffectedPackageStatusCatalog.NotApplicable)]
public void TryNormalize_ReturnsExpectedValue(string input, string expected)
{
Assert.True(AffectedPackageStatusCatalog.TryNormalize(input, out var normalized));
Assert.Equal(expected, normalized);
}
[Fact]
public void TryNormalize_ReturnsFalseForUnknown()
{
Assert.False(AffectedPackageStatusCatalog.TryNormalize("unsupported", out _));
}
[Fact]
public void Allowed_ReturnsCanonicalStatuses()
{
var expected = new[]
{
AffectedPackageStatusCatalog.KnownAffected,
AffectedPackageStatusCatalog.KnownNotAffected,
AffectedPackageStatusCatalog.UnderInvestigation,
AffectedPackageStatusCatalog.Fixed,
AffectedPackageStatusCatalog.FirstFixed,
AffectedPackageStatusCatalog.Mitigated,
AffectedPackageStatusCatalog.NotApplicable,
AffectedPackageStatusCatalog.Affected,
AffectedPackageStatusCatalog.NotAffected,
AffectedPackageStatusCatalog.Pending,
AffectedPackageStatusCatalog.Unknown,
};
Assert.Equal(expected, AffectedPackageStatusCatalog.Allowed);
}
}

View File

@@ -0,0 +1,74 @@
using System;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class AffectedVersionRangeExtensionsTests
{
[Fact]
public void ToNormalizedVersionRule_UsesNevraPrimitivesWhenAvailable()
{
var range = new AffectedVersionRange(
rangeKind: "nevra",
introducedVersion: "raw-introduced",
fixedVersion: "raw-fixed",
lastAffectedVersion: null,
rangeExpression: null,
provenance: AdvisoryProvenance.Empty,
primitives: new RangePrimitives(
SemVer: null,
Nevra: new NevraPrimitive(
new NevraComponent("pkg", 0, "1.0.0", "1", "x86_64"),
new NevraComponent("pkg", 0, "1.2.0", "2", "x86_64"),
null),
Evr: null,
VendorExtensions: null));
var rule = range.ToNormalizedVersionRule();
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionSchemes.Nevra, rule!.Scheme);
Assert.Equal("pkg-1.0.0-1.x86_64", rule.Min);
Assert.Equal("pkg-1.2.0-2.x86_64", rule.Max);
}
[Fact]
public void ToNormalizedVersionRule_FallsBackForEvrWhenPrimitivesMissing()
{
var range = new AffectedVersionRange(
rangeKind: "EVR",
introducedVersion: "1:1.0.0-1",
fixedVersion: "1:1.2.0-1ubuntu2",
lastAffectedVersion: null,
rangeExpression: null,
provenance: new AdvisoryProvenance("debian", "range", "pkg", DateTimeOffset.UtcNow),
primitives: null);
var rule = range.ToNormalizedVersionRule("fallback");
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionSchemes.Evr, rule!.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.Range, rule.Type);
Assert.Equal("1:1.0.0-1", rule.Min);
Assert.Equal("1:1.2.0-1ubuntu2", rule.Max);
Assert.Equal("fallback", rule.Notes);
}
[Fact]
public void ToNormalizedVersionRule_ReturnsNullForUnknownKind()
{
var range = new AffectedVersionRange(
rangeKind: "vendor",
introducedVersion: null,
fixedVersion: null,
lastAffectedVersion: null,
rangeExpression: null,
provenance: AdvisoryProvenance.Empty,
primitives: null);
var rule = range.ToNormalizedVersionRule();
Assert.Null(rule);
}
}

View File

@@ -0,0 +1,52 @@
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Models.Tests;
public sealed class AliasSchemeRegistryTests
{
[Theory]
[InlineData("cve-2024-1234", AliasSchemes.Cve, "CVE-2024-1234")]
[InlineData("GHSA-xxxx-yyyy-zzzz", AliasSchemes.Ghsa, "GHSA-xxxx-yyyy-zzzz")]
[InlineData("osv-2023-15", AliasSchemes.OsV, "OSV-2023-15")]
[InlineData("jvndb-2023-123456", AliasSchemes.Jvndb, "JVNDB-2023-123456")]
[InlineData("vu#123456", AliasSchemes.Vu, "VU#123456")]
[InlineData("pkg:maven/org.example/app@1.0.0", AliasSchemes.Purl, "pkg:maven/org.example/app@1.0.0")]
[InlineData("cpe:/a:vendor:product:1.0", AliasSchemes.Cpe, "cpe:/a:vendor:product:1.0")]
public void TryNormalize_DetectsSchemeAndCanonicalizes(string input, string expectedScheme, string expectedAlias)
{
var success = AliasSchemeRegistry.TryNormalize(input, out var normalized, out var scheme);
Assert.True(success);
Assert.Equal(expectedScheme, scheme);
Assert.Equal(expectedAlias, normalized);
}
[Fact]
public void TryNormalize_ReturnsFalseForUnknownAlias()
{
var success = AliasSchemeRegistry.TryNormalize("custom-identifier", out var normalized, out var scheme);
Assert.False(success);
Assert.Equal("custom-identifier", normalized);
Assert.Equal(string.Empty, scheme);
}
[Fact]
public void Validation_NormalizesAliasWhenRecognized()
{
var result = Validation.TryNormalizeAlias(" rhsa-2024:0252 ", out var normalized);
Assert.True(result);
Assert.NotNull(normalized);
Assert.Equal("RHSA-2024:0252", normalized);
}
[Fact]
public void Validation_RejectsEmptyAlias()
{
var result = Validation.TryNormalizeAlias(" ", out var normalized);
Assert.False(result);
Assert.Null(normalized);
}
}

View File

@@ -0,0 +1,195 @@
using System.Collections.Generic;
using System.Globalization;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Models.Tests;
internal static class CanonicalExampleFactory
{
public static IEnumerable<(string Name, Advisory Advisory)> GetExamples()
{
yield return ("nvd-basic", CreateNvdExample());
yield return ("psirt-overlay", CreatePsirtOverlay());
yield return ("ghsa-semver", CreateGhsaSemVer());
yield return ("kev-flag", CreateKevFlag());
}
private static Advisory CreateNvdExample()
{
var provenance = Provenance("nvd", "map", "cve-2024-1234", "2024-08-01T12:00:00Z");
return new Advisory(
advisoryKey: "CVE-2024-1234",
title: "Integer overflow in ExampleCMS",
summary: "An integer overflow in ExampleCMS allows remote attackers to escalate privileges.",
language: "en",
published: ParseDate("2024-07-15T00:00:00Z"),
modified: ParseDate("2024-07-16T10:35:00Z"),
severity: "high",
exploitKnown: false,
aliases: new[] { "CVE-2024-1234" },
references: new[]
{
new AdvisoryReference(
"https://nvd.nist.gov/vuln/detail/CVE-2024-1234",
kind: "advisory",
sourceTag: "nvd",
summary: "NVD entry",
provenance: provenance),
new AdvisoryReference(
"https://example.org/security/CVE-2024-1234",
kind: "advisory",
sourceTag: "vendor",
summary: "Vendor bulletin",
provenance: Provenance("example", "fetch", "bulletin", "2024-07-14T15:00:00Z")),
},
affectedPackages: new[]
{
new AffectedPackage(
type: AffectedPackageTypes.Cpe,
identifier: "cpe:/a:examplecms:examplecms:1.0",
platform: null,
versionRanges: new[]
{
new AffectedVersionRange("version", "1.0", "1.0.5", null, null, provenance),
},
statuses: new[]
{
new AffectedPackageStatus("affected", provenance),
},
provenance: new[] { provenance }),
},
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", provenance),
},
provenance: new[] { provenance });
}
private static Advisory CreatePsirtOverlay()
{
var rhsaProv = Provenance("redhat", "map", "rhsa-2024:0252", "2024-05-11T09:00:00Z");
var cveProv = Provenance("redhat", "enrich", "cve-2024-5678", "2024-05-11T09:05:00Z");
return new Advisory(
advisoryKey: "RHSA-2024:0252",
title: "Important: kernel security update",
summary: "Updates the Red Hat Enterprise Linux kernel to address CVE-2024-5678.",
language: "en",
published: ParseDate("2024-05-10T19:28:00Z"),
modified: ParseDate("2024-05-11T08:15:00Z"),
severity: "critical",
exploitKnown: false,
aliases: new[] { "RHSA-2024:0252", "CVE-2024-5678" },
references: new[]
{
new AdvisoryReference(
"https://access.redhat.com/errata/RHSA-2024:0252",
kind: "advisory",
sourceTag: "redhat",
summary: "Red Hat security advisory",
provenance: rhsaProv),
},
affectedPackages: new[]
{
new AffectedPackage(
type: AffectedPackageTypes.Rpm,
identifier: "kernel-0:4.18.0-553.el8.x86_64",
platform: "rhel-8",
versionRanges: new[]
{
new AffectedVersionRange("nevra", "0:4.18.0-553.el8", null, null, null, rhsaProv),
},
statuses: new[]
{
new AffectedPackageStatus("fixed", rhsaProv),
},
provenance: new[] { rhsaProv, cveProv }),
},
cvssMetrics: new[]
{
new CvssMetric("3.1", "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", 6.7, "medium", rhsaProv),
},
provenance: new[] { rhsaProv, cveProv });
}
private static Advisory CreateGhsaSemVer()
{
var provenance = Provenance("ghsa", "map", "ghsa-aaaa-bbbb-cccc", "2024-03-05T10:00:00Z");
return new Advisory(
advisoryKey: "GHSA-aaaa-bbbb-cccc",
title: "Prototype pollution in widget.js",
summary: "A crafted payload can pollute Object.prototype leading to RCE.",
language: "en",
published: ParseDate("2024-03-04T00:00:00Z"),
modified: ParseDate("2024-03-04T12:00:00Z"),
severity: "high",
exploitKnown: false,
aliases: new[] { "GHSA-aaaa-bbbb-cccc", "CVE-2024-2222" },
references: new[]
{
new AdvisoryReference(
"https://github.com/example/widget/security/advisories/GHSA-aaaa-bbbb-cccc",
kind: "advisory",
sourceTag: "ghsa",
summary: "GitHub Security Advisory",
provenance: provenance),
new AdvisoryReference(
"https://github.com/example/widget/commit/abcd1234",
kind: "patch",
sourceTag: "ghsa",
summary: "Patch commit",
provenance: provenance),
},
affectedPackages: new[]
{
new AffectedPackage(
type: AffectedPackageTypes.SemVer,
identifier: "pkg:npm/example-widget",
platform: null,
versionRanges: new[]
{
new AffectedVersionRange("semver", null, "2.5.1", null, ">=0.0.0 <2.5.1", provenance),
new AffectedVersionRange("semver", "3.0.0", "3.2.4", null, null, provenance),
},
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[] { provenance }),
},
cvssMetrics: new[]
{
new CvssMetric("3.1", "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", 8.8, "high", provenance),
},
provenance: new[] { provenance });
}
private static Advisory CreateKevFlag()
{
var provenance = Provenance("cisa-kev", "annotate", "kev", "2024-02-10T09:30:00Z");
return new Advisory(
advisoryKey: "CVE-2023-9999",
title: "Remote code execution in LegacyServer",
summary: "Unauthenticated RCE due to unsafe deserialization.",
language: "en",
published: ParseDate("2023-11-20T00:00:00Z"),
modified: ParseDate("2024-02-09T16:22:00Z"),
severity: "critical",
exploitKnown: true,
aliases: new[] { "CVE-2023-9999" },
references: new[]
{
new AdvisoryReference(
"https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
kind: "kev",
sourceTag: "cisa",
summary: "CISA KEV entry",
provenance: provenance),
},
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { provenance });
}
private static AdvisoryProvenance Provenance(string source, string kind, string value, string recordedAt)
=> new(source, kind, value, ParseDate(recordedAt));
private static DateTimeOffset ParseDate(string value)
=> DateTimeOffset.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToUniversalTime();
}

View File

@@ -0,0 +1,60 @@
using System.Text;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Models.Tests;
public sealed class CanonicalExamplesTests
{
private static readonly string FixtureRoot = Path.Combine(GetProjectRoot(), "Fixtures");
private const string UpdateEnvVar = "UPDATE_GOLDENS";
[Trait("Category", "GoldenSnapshots")]
[Fact]
public void CanonicalExamplesMatchGoldenSnapshots()
{
Directory.CreateDirectory(FixtureRoot);
var envValue = Environment.GetEnvironmentVariable(UpdateEnvVar);
var updateGoldens = string.Equals(envValue, "1", StringComparison.OrdinalIgnoreCase);
var failures = new List<string>();
foreach (var (name, advisory) in CanonicalExampleFactory.GetExamples())
{
var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n");
var fixturePath = Path.Combine(FixtureRoot, $"{name}.json");
if (updateGoldens)
{
File.WriteAllText(fixturePath, snapshot);
continue;
}
if (!File.Exists(fixturePath))
{
failures.Add($"Missing golden fixture: {fixturePath}. Set {UpdateEnvVar}=1 to generate.");
continue;
}
var expected = File.ReadAllText(fixturePath).Replace("\r\n", "\n");
if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(FixtureRoot, $"{name}.actual.json");
File.WriteAllText(actualPath, snapshot);
failures.Add($"Fixture mismatch for {name}. Set {UpdateEnvVar}=1 to regenerate.");
}
}
if (failures.Count > 0)
{
var builder = new StringBuilder();
foreach (var failure in failures)
{
builder.AppendLine(failure);
}
Assert.Fail(builder.ToString());
}
}
private static string GetProjectRoot()
=> Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
}

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Models.Tests;
public sealed class CanonicalJsonSerializerTests
{
[Fact]
public void SerializesPropertiesInDeterministicOrder()
{
var advisory = new Advisory(
advisoryKey: "TEST-321",
title: "Ordering",
summary: null,
language: null,
published: null,
modified: null,
severity: null,
exploitKnown: false,
aliases: new[] { "b", "a" },
references: Array.Empty<AdvisoryReference>(),
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: Array.Empty<AdvisoryProvenance>());
var json = CanonicalJsonSerializer.Serialize(advisory);
using var document = JsonDocument.Parse(json);
var propertyNames = document.RootElement.EnumerateObject().Select(p => p.Name).ToArray();
var sorted = propertyNames.OrderBy(name => name, StringComparer.Ordinal).ToArray();
Assert.Equal(sorted, propertyNames);
}
[Fact]
public void SnapshotSerializerProducesStableOutput()
{
var advisory = new Advisory(
advisoryKey: "TEST-999",
title: "Snapshot",
summary: "Example",
language: "EN",
published: DateTimeOffset.Parse("2024-06-01T00:00:00Z"),
modified: DateTimeOffset.Parse("2024-06-01T01:00:00Z"),
severity: "high",
exploitKnown: false,
aliases: new[] { "ALIAS-1" },
references: new[]
{
new AdvisoryReference("https://example.com/a", "advisory", null, null, AdvisoryProvenance.Empty),
},
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: Array.Empty<AdvisoryProvenance>());
var snap1 = SnapshotSerializer.ToSnapshot(advisory);
var snap2 = SnapshotSerializer.ToSnapshot(advisory);
Assert.Equal(snap1, snap2);
var normalized1 = snap1.Replace("\r\n", "\n");
var normalized2 = snap2.Replace("\r\n", "\n");
Assert.Equal(normalized1, normalized2);
}
[Fact]
public void SerializesRangePrimitivesPayload()
{
var recordedAt = new DateTimeOffset(2025, 2, 1, 0, 0, 0, TimeSpan.Zero);
var provenance = new AdvisoryProvenance("connector-x", "map", "segment-1", recordedAt);
var primitives = new RangePrimitives(
new SemVerPrimitive(
Introduced: "2.0.0",
IntroducedInclusive: true,
Fixed: "2.3.4",
FixedInclusive: false,
LastAffected: "2.3.3",
LastAffectedInclusive: true,
ConstraintExpression: ">=2.0.0 <2.3.4"),
new NevraPrimitive(
Introduced: new NevraComponent("pkg", 0, "2.0.0", "1", "x86_64"),
Fixed: null,
LastAffected: new NevraComponent("pkg", 0, "2.3.3", "3", "x86_64")),
new EvrPrimitive(
Introduced: new EvrComponent(1, "2.0.0", "1"),
Fixed: new EvrComponent(1, "2.3.4", null),
LastAffected: null),
new Dictionary<string, string>(StringComparer.Ordinal)
{
["channel"] = "stable",
});
var range = new AffectedVersionRange(
rangeKind: "semver",
introducedVersion: "2.0.0",
fixedVersion: "2.3.4",
lastAffectedVersion: "2.3.3",
rangeExpression: ">=2.0.0 <2.3.4",
provenance,
primitives);
var package = new AffectedPackage(
type: "semver",
identifier: "pkg@2.x",
platform: "linux",
versionRanges: new[] { range },
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[] { provenance });
var advisory = new Advisory(
advisoryKey: "TEST-PRIM",
title: "Range primitive serialization",
summary: null,
language: null,
published: recordedAt,
modified: recordedAt,
severity: null,
exploitKnown: false,
aliases: Array.Empty<string>(),
references: Array.Empty<AdvisoryReference>(),
affectedPackages: new[] { package },
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { provenance });
var json = CanonicalJsonSerializer.Serialize(advisory);
using var document = JsonDocument.Parse(json);
var rangeElement = document.RootElement
.GetProperty("affectedPackages")[0]
.GetProperty("versionRanges")[0];
Assert.True(rangeElement.TryGetProperty("primitives", out var primitivesElement));
var semver = primitivesElement.GetProperty("semVer");
Assert.Equal("2.0.0", semver.GetProperty("introduced").GetString());
Assert.True(semver.GetProperty("introducedInclusive").GetBoolean());
Assert.Equal("2.3.4", semver.GetProperty("fixed").GetString());
Assert.False(semver.GetProperty("fixedInclusive").GetBoolean());
Assert.Equal("2.3.3", semver.GetProperty("lastAffected").GetString());
var nevra = primitivesElement.GetProperty("nevra");
Assert.Equal("pkg", nevra.GetProperty("introduced").GetProperty("name").GetString());
Assert.Equal(0, nevra.GetProperty("introduced").GetProperty("epoch").GetInt32());
var evr = primitivesElement.GetProperty("evr");
Assert.Equal(1, evr.GetProperty("introduced").GetProperty("epoch").GetInt32());
Assert.Equal("2.3.4", evr.GetProperty("fixed").GetProperty("upstreamVersion").GetString());
var extensions = primitivesElement.GetProperty("vendorExtensions");
Assert.Equal("stable", extensions.GetProperty("channel").GetString());
}
}

View File

@@ -0,0 +1,43 @@
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class EvrPrimitiveExtensionsTests
{
[Fact]
public void ToNormalizedVersionRule_ProducesRangeForIntroducedAndFixed()
{
var primitive = new EvrPrimitive(
Introduced: new EvrComponent(1, "1.2.3", "4"),
Fixed: new EvrComponent(1, "1.2.9", "0ubuntu1"),
LastAffected: null);
var rule = primitive.ToNormalizedVersionRule("ubuntu:focal");
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionSchemes.Evr, rule!.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.Range, rule.Type);
Assert.Equal("1:1.2.3-4", rule.Min);
Assert.True(rule.MinInclusive);
Assert.Equal("1:1.2.9-0ubuntu1", rule.Max);
Assert.False(rule.MaxInclusive);
Assert.Equal("ubuntu:focal", rule.Notes);
}
[Fact]
public void ToNormalizedVersionRule_GreaterThanOrEqualWhenOnlyIntroduced()
{
var primitive = new EvrPrimitive(
Introduced: new EvrComponent(0, "2.0.0", null),
Fixed: null,
LastAffected: null);
var rule = primitive.ToNormalizedVersionRule();
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionRuleTypes.GreaterThanOrEqual, rule!.Type);
Assert.Equal("2.0.0", rule.Min);
Assert.True(rule.MinInclusive);
}
}

View File

@@ -0,0 +1,124 @@
{
"advisoryKey": "GHSA-aaaa-bbbb-cccc",
"affectedPackages": [
{
"type": "semver",
"identifier": "pkg:npm/example-widget",
"platform": null,
"versionRanges": [
{
"fixedVersion": "2.5.1",
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "ghsa-aaaa-bbbb-cccc",
"decisionReason": null,
"recordedAt": "2024-03-05T10:00:00+00:00",
"fieldMask": []
},
"rangeExpression": ">=0.0.0 <2.5.1",
"rangeKind": "semver"
},
{
"fixedVersion": "3.2.4",
"introducedVersion": "3.0.0",
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "ghsa-aaaa-bbbb-cccc",
"decisionReason": null,
"recordedAt": "2024-03-05T10:00:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "semver"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "ghsa",
"kind": "map",
"value": "ghsa-aaaa-bbbb-cccc",
"decisionReason": null,
"recordedAt": "2024-03-05T10:00:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CVE-2024-2222",
"GHSA-aaaa-bbbb-cccc"
],
"credits": [],
"cvssMetrics": [
{
"baseScore": 8.8,
"baseSeverity": "high",
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "ghsa-aaaa-bbbb-cccc",
"decisionReason": null,
"recordedAt": "2024-03-05T10:00:00+00:00",
"fieldMask": []
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"exploitKnown": false,
"language": "en",
"modified": "2024-03-04T12:00:00+00:00",
"provenance": [
{
"source": "ghsa",
"kind": "map",
"value": "ghsa-aaaa-bbbb-cccc",
"decisionReason": null,
"recordedAt": "2024-03-05T10:00:00+00:00",
"fieldMask": []
}
],
"published": "2024-03-04T00:00:00+00:00",
"references": [
{
"kind": "patch",
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "ghsa-aaaa-bbbb-cccc",
"decisionReason": null,
"recordedAt": "2024-03-05T10:00:00+00:00",
"fieldMask": []
},
"sourceTag": "ghsa",
"summary": "Patch commit",
"url": "https://github.com/example/widget/commit/abcd1234"
},
{
"kind": "advisory",
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "ghsa-aaaa-bbbb-cccc",
"decisionReason": null,
"recordedAt": "2024-03-05T10:00:00+00:00",
"fieldMask": []
},
"sourceTag": "ghsa",
"summary": "GitHub Security Advisory",
"url": "https://github.com/example/widget/security/advisories/GHSA-aaaa-bbbb-cccc"
}
],
"severity": "high",
"summary": "A crafted payload can pollute Object.prototype leading to RCE.",
"title": "Prototype pollution in widget.js"
}

View File

@@ -0,0 +1,42 @@
{
"advisoryKey": "CVE-2023-9999",
"affectedPackages": [],
"aliases": [
"CVE-2023-9999"
],
"credits": [],
"cvssMetrics": [],
"exploitKnown": true,
"language": "en",
"modified": "2024-02-09T16:22:00+00:00",
"provenance": [
{
"source": "cisa-kev",
"kind": "annotate",
"value": "kev",
"decisionReason": null,
"recordedAt": "2024-02-10T09:30:00+00:00",
"fieldMask": []
}
],
"published": "2023-11-20T00:00:00+00:00",
"references": [
{
"kind": "kev",
"provenance": {
"source": "cisa-kev",
"kind": "annotate",
"value": "kev",
"decisionReason": null,
"recordedAt": "2024-02-10T09:30:00+00:00",
"fieldMask": []
},
"sourceTag": "cisa",
"summary": "CISA KEV entry",
"url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog"
}
],
"severity": "critical",
"summary": "Unauthenticated RCE due to unsafe deserialization.",
"title": "Remote code execution in LegacyServer"
}

View File

@@ -0,0 +1,119 @@
{
"advisoryKey": "CVE-2024-1234",
"affectedPackages": [
{
"type": "cpe",
"identifier": "cpe:/a:examplecms:examplecms:1.0",
"platform": null,
"versionRanges": [
{
"fixedVersion": "1.0.5",
"introducedVersion": "1.0",
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "nvd",
"kind": "map",
"value": "cve-2024-1234",
"decisionReason": null,
"recordedAt": "2024-08-01T12:00:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "version"
}
],
"normalizedVersions": [],
"statuses": [
{
"provenance": {
"source": "nvd",
"kind": "map",
"value": "cve-2024-1234",
"decisionReason": null,
"recordedAt": "2024-08-01T12:00:00+00:00",
"fieldMask": []
},
"status": "affected"
}
],
"provenance": [
{
"source": "nvd",
"kind": "map",
"value": "cve-2024-1234",
"decisionReason": null,
"recordedAt": "2024-08-01T12:00:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CVE-2024-1234"
],
"credits": [],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "nvd",
"kind": "map",
"value": "cve-2024-1234",
"decisionReason": null,
"recordedAt": "2024-08-01T12:00:00+00:00",
"fieldMask": []
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"exploitKnown": false,
"language": "en",
"modified": "2024-07-16T10:35:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "map",
"value": "cve-2024-1234",
"decisionReason": null,
"recordedAt": "2024-08-01T12:00:00+00:00",
"fieldMask": []
}
],
"published": "2024-07-15T00:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "example",
"kind": "fetch",
"value": "bulletin",
"decisionReason": null,
"recordedAt": "2024-07-14T15:00:00+00:00",
"fieldMask": []
},
"sourceTag": "vendor",
"summary": "Vendor bulletin",
"url": "https://example.org/security/CVE-2024-1234"
},
{
"kind": "advisory",
"provenance": {
"source": "nvd",
"kind": "map",
"value": "cve-2024-1234",
"decisionReason": null,
"recordedAt": "2024-08-01T12:00:00+00:00",
"fieldMask": []
},
"sourceTag": "nvd",
"summary": "NVD entry",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-1234"
}
],
"severity": "high",
"summary": "An integer overflow in ExampleCMS allows remote attackers to escalate privileges.",
"title": "Integer overflow in ExampleCMS"
}

View File

@@ -0,0 +1,122 @@
{
"advisoryKey": "RHSA-2024:0252",
"affectedPackages": [
{
"type": "rpm",
"identifier": "kernel-0:4.18.0-553.el8.x86_64",
"platform": "rhel-8",
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": "0:4.18.0-553.el8",
"lastAffectedVersion": null,
"primitives": null,
"provenance": {
"source": "redhat",
"kind": "map",
"value": "rhsa-2024:0252",
"decisionReason": null,
"recordedAt": "2024-05-11T09:00:00+00:00",
"fieldMask": []
},
"rangeExpression": null,
"rangeKind": "nevra"
}
],
"normalizedVersions": [],
"statuses": [
{
"provenance": {
"source": "redhat",
"kind": "map",
"value": "rhsa-2024:0252",
"decisionReason": null,
"recordedAt": "2024-05-11T09:00:00+00:00",
"fieldMask": []
},
"status": "fixed"
}
],
"provenance": [
{
"source": "redhat",
"kind": "enrich",
"value": "cve-2024-5678",
"decisionReason": null,
"recordedAt": "2024-05-11T09:05:00+00:00",
"fieldMask": []
},
{
"source": "redhat",
"kind": "map",
"value": "rhsa-2024:0252",
"decisionReason": null,
"recordedAt": "2024-05-11T09:00:00+00:00",
"fieldMask": []
}
]
}
],
"aliases": [
"CVE-2024-5678",
"RHSA-2024:0252"
],
"credits": [],
"cvssMetrics": [
{
"baseScore": 6.7,
"baseSeverity": "medium",
"provenance": {
"source": "redhat",
"kind": "map",
"value": "rhsa-2024:0252",
"decisionReason": null,
"recordedAt": "2024-05-11T09:00:00+00:00",
"fieldMask": []
},
"vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"exploitKnown": false,
"language": "en",
"modified": "2024-05-11T08:15:00+00:00",
"provenance": [
{
"source": "redhat",
"kind": "enrich",
"value": "cve-2024-5678",
"decisionReason": null,
"recordedAt": "2024-05-11T09:05:00+00:00",
"fieldMask": []
},
{
"source": "redhat",
"kind": "map",
"value": "rhsa-2024:0252",
"decisionReason": null,
"recordedAt": "2024-05-11T09:00:00+00:00",
"fieldMask": []
}
],
"published": "2024-05-10T19:28:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "redhat",
"kind": "map",
"value": "rhsa-2024:0252",
"decisionReason": null,
"recordedAt": "2024-05-11T09:00:00+00:00",
"fieldMask": []
},
"sourceTag": "redhat",
"summary": "Red Hat security advisory",
"url": "https://access.redhat.com/errata/RHSA-2024:0252"
}
],
"severity": "critical",
"summary": "Updates the Red Hat Enterprise Linux kernel to address CVE-2024-5678.",
"title": "Important: kernel security update"
}

View File

@@ -0,0 +1,44 @@
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class NevraPrimitiveExtensionsTests
{
[Fact]
public void ToNormalizedVersionRule_ProducesRangeWhenBoundsAvailable()
{
var primitive = new NevraPrimitive(
Introduced: new NevraComponent("openssl", 1, "1.1.1k", "4", "x86_64"),
Fixed: new NevraComponent("openssl", 1, "1.1.1n", "5", "x86_64"),
LastAffected: null);
var rule = primitive.ToNormalizedVersionRule("rhel-8");
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionSchemes.Nevra, rule!.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.Range, rule.Type);
Assert.Equal("openssl-1:1.1.1k-4.x86_64", rule.Min);
Assert.True(rule.MinInclusive);
Assert.Equal("openssl-1:1.1.1n-5.x86_64", rule.Max);
Assert.False(rule.MaxInclusive);
Assert.Equal("rhel-8", rule.Notes);
}
[Fact]
public void ToNormalizedVersionRule_UsesLastAffectedAsInclusiveUpperBound()
{
var primitive = new NevraPrimitive(
Introduced: null,
Fixed: null,
LastAffected: new NevraComponent("kernel", 0, "5.15.0", "1024.18", "x86_64"));
var rule = primitive.ToNormalizedVersionRule();
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionSchemes.Nevra, rule!.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThanOrEqual, rule.Type);
Assert.Equal("kernel-5.15.0-1024.18.x86_64", rule.Max);
Assert.True(rule.MaxInclusive);
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Linq;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class NormalizedVersionRuleTests
{
[Fact]
public void NormalizedVersions_AreDeduplicatedAndOrdered()
{
var recordedAt = DateTimeOffset.Parse("2025-01-05T00:00:00Z");
var provenance = new AdvisoryProvenance("ghsa", "map", "GHSA-abc", recordedAt);
var package = new AffectedPackage(
type: AffectedPackageTypes.SemVer,
identifier: "pkg:npm/example",
versionRanges: Array.Empty<AffectedVersionRange>(),
normalizedVersions: new[]
{
new NormalizedVersionRule("SemVer", "Exact", value: "1.0.0 "),
new NormalizedVersionRule("semver", "range", min: "1.2.0", minInclusive: true, max: "2.0.0", maxInclusive: false, notes: "GHSA-abc"),
new NormalizedVersionRule("semver", "range", min: "1.2.0", minInclusive: true, max: "2.0.0", maxInclusive: false, notes: "GHSA-abc"),
new NormalizedVersionRule("semver", "gt", min: "0.9.0", minInclusive: false, notes: " originating nvd "),
},
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[] { provenance });
var normalized = package.NormalizedVersions.ToArray();
Assert.Equal(3, normalized.Length);
Assert.Collection(
normalized,
rule =>
{
Assert.Equal("semver", rule.Scheme);
Assert.Equal("exact", rule.Type);
Assert.Equal("1.0.0", rule.Value);
Assert.Null(rule.Min);
Assert.Null(rule.Max);
},
rule =>
{
Assert.Equal("semver", rule.Scheme);
Assert.Equal("gt", rule.Type);
Assert.Equal("0.9.0", rule.Min);
Assert.False(rule.MinInclusive);
Assert.Equal("originating nvd", rule.Notes);
},
rule =>
{
Assert.Equal("semver", rule.Scheme);
Assert.Equal("range", rule.Type);
Assert.Equal("1.2.0", rule.Min);
Assert.True(rule.MinInclusive);
Assert.Equal("2.0.0", rule.Max);
Assert.False(rule.MaxInclusive);
Assert.Equal("GHSA-abc", rule.Notes);
});
}
[Fact]
public void NormalizedVersionRule_NormalizesTypeSeparators()
{
var rule = new NormalizedVersionRule("semver", "tie_breaker", value: "1.2.3");
Assert.Equal("tie-breaker", rule.Type);
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.Metrics;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class OsvGhsaParityDiagnosticsTests
{
[Fact]
public void RecordReport_EmitsTotalAndIssues()
{
var issues = ImmutableArray.Create(
new OsvGhsaParityIssue(
GhsaId: "GHSA-AAA",
IssueKind: "missing_osv",
Detail: "",
FieldMask: ImmutableArray.Create(ProvenanceFieldMasks.AffectedPackages)),
new OsvGhsaParityIssue(
GhsaId: "GHSA-BBB",
IssueKind: "severity_mismatch",
Detail: "",
FieldMask: ImmutableArray<string>.Empty));
var report = new OsvGhsaParityReport(2, issues);
var measurements = new List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)>();
using var listener = CreateListener(measurements);
OsvGhsaParityDiagnostics.RecordReport(report, "QA");
listener.Dispose();
Assert.Equal(3, measurements.Count);
var total = Assert.Single(measurements, m => m.Instrument == "concelier.osv_ghsa.total");
Assert.Equal(2, total.Value);
Assert.Equal("qa", total.Tags["dataset"]);
var missing = Assert.Single(measurements, m => m.Tags.TryGetValue("issueKind", out var kind) && string.Equals((string)kind!, "missing_osv", StringComparison.Ordinal));
Assert.Equal("affectedpackages[]", missing.Tags["fieldMask"]);
var severity = Assert.Single(measurements, m => m.Tags.TryGetValue("issueKind", out var kind) && string.Equals((string)kind!, "severity_mismatch", StringComparison.Ordinal));
Assert.Equal("none", severity.Tags["fieldMask"]);
}
[Fact]
public void RecordReport_NoIssues_OnlyEmitsTotal()
{
var report = new OsvGhsaParityReport(0, ImmutableArray<OsvGhsaParityIssue>.Empty);
var measurements = new List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)>();
using var listener = CreateListener(measurements);
OsvGhsaParityDiagnostics.RecordReport(report, "");
listener.Dispose();
Assert.Empty(measurements);
}
private static MeterListener CreateListener(List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)> measurements)
{
var listener = new MeterListener
{
InstrumentPublished = (instrument, l) =>
{
if (instrument.Meter.Name.StartsWith("StellaOps.Concelier.Models.OsvGhsaParity", StringComparison.Ordinal))
{
l.EnableMeasurementEvents(instrument);
}
}
};
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var tag in tags)
{
dict[tag.Key] = tag.Value;
}
measurements.Add((instrument.Name, measurement, dict));
});
listener.Start();
return listener;
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class OsvGhsaParityInspectorTests
{
[Fact]
public void Compare_ReturnsNoIssues_WhenDatasetsMatch()
{
var ghsaId = "GHSA-1111";
var osv = CreateOsvAdvisory(ghsaId, severity: "high", includeRanges: true);
var ghsa = CreateGhsaAdvisory(ghsaId, severity: "high", includeRanges: true);
var report = OsvGhsaParityInspector.Compare(new[] { osv }, new[] { ghsa });
Assert.False(report.HasIssues);
Assert.Equal(1, report.TotalGhsaIds);
Assert.Empty(report.Issues);
}
[Fact]
public void Compare_FlagsMissingOsvEntry()
{
var ghsaId = "GHSA-2222";
var ghsa = CreateGhsaAdvisory(ghsaId, severity: "medium", includeRanges: true);
var report = OsvGhsaParityInspector.Compare(Array.Empty<Advisory>(), new[] { ghsa });
var issue = Assert.Single(report.Issues);
Assert.Equal("missing_osv", issue.IssueKind);
Assert.Equal(ghsaId, issue.GhsaId);
Assert.Contains(ProvenanceFieldMasks.AffectedPackages, issue.FieldMask);
}
[Fact]
public void Compare_FlagsMissingGhsaEntry()
{
var ghsaId = "GHSA-2424";
var osv = CreateOsvAdvisory(ghsaId, severity: "medium", includeRanges: true);
var report = OsvGhsaParityInspector.Compare(new[] { osv }, Array.Empty<Advisory>());
var issue = Assert.Single(report.Issues);
Assert.Equal("missing_ghsa", issue.IssueKind);
Assert.Equal(ghsaId, issue.GhsaId);
Assert.Contains(ProvenanceFieldMasks.AffectedPackages, issue.FieldMask);
}
[Fact]
public void Compare_FlagsSeverityMismatch()
{
var ghsaId = "GHSA-3333";
var osv = CreateOsvAdvisory(ghsaId, severity: "low", includeRanges: true);
var ghsa = CreateGhsaAdvisory(ghsaId, severity: "critical", includeRanges: true);
var report = OsvGhsaParityInspector.Compare(new[] { osv }, new[] { ghsa });
var issue = Assert.Single(report.Issues, i => i.IssueKind == "severity_mismatch");
Assert.Equal(ghsaId, issue.GhsaId);
Assert.Contains(ProvenanceFieldMasks.Advisory, issue.FieldMask);
}
[Fact]
public void Compare_FlagsRangeMismatch()
{
var ghsaId = "GHSA-4444";
var osv = CreateOsvAdvisory(ghsaId, severity: "high", includeRanges: false);
var ghsa = CreateGhsaAdvisory(ghsaId, severity: "high", includeRanges: true);
var report = OsvGhsaParityInspector.Compare(new[] { osv }, new[] { ghsa });
var issue = Assert.Single(report.Issues, i => i.IssueKind == "range_mismatch");
Assert.Equal(ghsaId, issue.GhsaId);
Assert.Contains(ProvenanceFieldMasks.VersionRanges, issue.FieldMask);
}
private static Advisory CreateOsvAdvisory(string ghsaId, string? severity, bool includeRanges)
{
var timestamp = DateTimeOffset.UtcNow;
return new Advisory(
advisoryKey: $"osv-{ghsaId.ToLowerInvariant()}",
title: $"OSV {ghsaId}",
summary: null,
language: null,
published: timestamp,
modified: timestamp,
severity: severity,
exploitKnown: false,
aliases: new[] { ghsaId },
references: Array.Empty<AdvisoryReference>(),
affectedPackages: includeRanges ? new[] { CreatePackage(timestamp, includeRanges) } : Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[]
{
new AdvisoryProvenance("osv", "map", ghsaId, timestamp, new[] { ProvenanceFieldMasks.Advisory })
});
}
private static Advisory CreateGhsaAdvisory(string ghsaId, string? severity, bool includeRanges)
{
var timestamp = DateTimeOffset.UtcNow;
return new Advisory(
advisoryKey: ghsaId.ToLowerInvariant(),
title: $"GHSA {ghsaId}",
summary: null,
language: null,
published: timestamp,
modified: timestamp,
severity: severity,
exploitKnown: false,
aliases: new[] { ghsaId },
references: Array.Empty<AdvisoryReference>(),
affectedPackages: includeRanges ? new[] { CreatePackage(timestamp, includeRanges) } : Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[]
{
new AdvisoryProvenance("ghsa", "map", ghsaId, timestamp, new[] { ProvenanceFieldMasks.Advisory })
});
}
private static AffectedPackage CreatePackage(DateTimeOffset recordedAt, bool includeRanges)
{
var ranges = includeRanges
? new[]
{
new AffectedVersionRange(
rangeKind: "semver",
introducedVersion: "1.0.0",
fixedVersion: "1.2.0",
lastAffectedVersion: null,
rangeExpression: null,
provenance: new AdvisoryProvenance("mapper", "range", "package@1", recordedAt, new[] { ProvenanceFieldMasks.VersionRanges }),
primitives: null)
}
: Array.Empty<AffectedVersionRange>();
return new AffectedPackage(
type: "semver",
identifier: "pkg@1",
platform: null,
versionRanges: ranges,
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: new[] { new AdvisoryProvenance("mapper", "package", "pkg@1", recordedAt, new[] { ProvenanceFieldMasks.AffectedPackages }) });
}
}

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class ProvenanceDiagnosticsTests
{
[Fact]
public void RecordMissing_AddsExpectedTagsAndDeduplicates()
{
ResetState();
var measurements = new List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)>();
using var listener = CreateListener(measurements);
var baseline = DateTimeOffset.UtcNow;
ProvenanceDiagnostics.RecordMissing("source-A", "range:pkg", baseline, new[] { ProvenanceFieldMasks.VersionRanges });
ProvenanceDiagnostics.RecordMissing("source-A", "range:pkg", baseline.AddMinutes(5), new[] { ProvenanceFieldMasks.VersionRanges });
ProvenanceDiagnostics.RecordMissing("source-A", "reference:https://example", baseline.AddMinutes(10), new[] { ProvenanceFieldMasks.References });
listener.Dispose();
Assert.Equal(2, measurements.Count);
var first = measurements[0];
Assert.Equal(1, first.Value);
Assert.Equal("concelier.provenance.missing", first.Instrument);
Assert.Equal("source-A", first.Tags["source"]);
Assert.Equal("range:pkg", first.Tags["component"]);
Assert.Equal("range", first.Tags["category"]);
Assert.Equal("high", first.Tags["severity"]);
Assert.Equal(ProvenanceFieldMasks.VersionRanges, first.Tags["fieldMask"]);
var second = measurements[1];
Assert.Equal("concelier.provenance.missing", second.Instrument);
Assert.Equal("reference", second.Tags["category"]);
Assert.Equal("low", second.Tags["severity"]);
Assert.Equal(ProvenanceFieldMasks.References, second.Tags["fieldMask"]);
}
[Fact]
public void ReportResumeWindow_ClearsTrackedEntries_WhenWindowBackfills()
{
ResetState();
var timestamp = DateTimeOffset.UtcNow;
ProvenanceDiagnostics.RecordMissing("source-B", "package:lib", timestamp);
var (recorded, earliest, syncRoot) = GetInternalState();
lock (syncRoot)
{
Assert.True(earliest.ContainsKey("source-B"));
Assert.Contains(recorded, entry => entry.StartsWith("source-B|", StringComparison.OrdinalIgnoreCase));
}
ProvenanceDiagnostics.ReportResumeWindow("source-B", timestamp.AddMinutes(-5), NullLogger.Instance);
lock (syncRoot)
{
Assert.False(earliest.ContainsKey("source-B"));
Assert.DoesNotContain(recorded, entry => entry.StartsWith("source-B|", StringComparison.OrdinalIgnoreCase));
}
}
[Fact]
public void ReportResumeWindow_RetainsEntries_WhenWindowTooRecent()
{
ResetState();
var timestamp = DateTimeOffset.UtcNow;
ProvenanceDiagnostics.RecordMissing("source-C", "range:pkg", timestamp);
ProvenanceDiagnostics.ReportResumeWindow("source-C", timestamp.AddMinutes(1), NullLogger.Instance);
var (recorded, earliest, syncRoot) = GetInternalState();
lock (syncRoot)
{
Assert.True(earliest.ContainsKey("source-C"));
Assert.Contains(recorded, entry => entry.StartsWith("source-C|", StringComparison.OrdinalIgnoreCase));
}
}
[Fact]
public void RecordRangePrimitive_EmitsCoverageMetric()
{
var range = new AffectedVersionRange(
rangeKind: "evr",
introducedVersion: "1:1.1.1n-0+deb11u2",
fixedVersion: null,
lastAffectedVersion: null,
rangeExpression: null,
provenance: new AdvisoryProvenance("source-D", "range", "pkg", DateTimeOffset.UtcNow),
primitives: new RangePrimitives(
SemVer: null,
Nevra: null,
Evr: new EvrPrimitive(
new EvrComponent(1, "1.1.1n", "0+deb11u2"),
null,
null),
VendorExtensions: new Dictionary<string, string> { ["debian.release"] = "bullseye" }));
var measurements = new List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)>();
using var listener = CreateListener(measurements, "concelier.range.primitives");
ProvenanceDiagnostics.RecordRangePrimitive("source-D", range);
listener.Dispose();
var record = Assert.Single(measurements);
Assert.Equal("concelier.range.primitives", record.Instrument);
Assert.Equal(1, record.Value);
Assert.Equal("source-D", record.Tags["source"]);
Assert.Equal("evr", record.Tags["rangeKind"]);
Assert.Equal("evr", record.Tags["primitiveKinds"]);
Assert.Equal("true", record.Tags["hasVendorExtensions"]);
}
private static MeterListener CreateListener(
List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)> measurements,
params string[] instrumentNames)
{
var allowed = instrumentNames is { Length: > 0 } ? instrumentNames : new[] { "concelier.provenance.missing" };
var allowedSet = new HashSet<string>(allowed, StringComparer.OrdinalIgnoreCase);
var listener = new MeterListener
{
InstrumentPublished = (instrument, l) =>
{
if (instrument.Meter.Name == "StellaOps.Concelier.Models.Provenance" && allowedSet.Contains(instrument.Name))
{
l.EnableMeasurementEvents(instrument);
}
}
};
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var tag in tags)
{
dict[tag.Key] = tag.Value;
}
measurements.Add((instrument.Name, measurement, dict));
});
listener.Start();
return listener;
}
private static void ResetState()
{
var (_, _, syncRoot) = GetInternalState();
lock (syncRoot)
{
var (recorded, earliest, _) = GetInternalState();
recorded.Clear();
earliest.Clear();
}
}
private static (HashSet<string> Recorded, Dictionary<string, DateTimeOffset> Earliest, object SyncRoot) GetInternalState()
{
var type = typeof(ProvenanceDiagnostics);
var recordedField = type.GetField("RecordedComponents", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("RecordedComponents not found");
var earliestField = type.GetField("EarliestMissing", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("EarliestMissing not found");
var syncField = type.GetField("SyncRoot", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("SyncRoot not found");
var recorded = (HashSet<string>)recordedField.GetValue(null)!;
var earliest = (Dictionary<string, DateTimeOffset>)earliestField.GetValue(null)!;
var sync = syncField.GetValue(null)!;
return (recorded, earliest, sync);
}
}

View File

@@ -0,0 +1,41 @@
using System.Collections.Generic;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class RangePrimitivesTests
{
[Fact]
public void GetCoverageTag_ReturnsSpecificKinds()
{
var primitives = new RangePrimitives(
new SemVerPrimitive("1.0.0", true, "1.2.0", false, null, false, null),
new NevraPrimitive(null, null, null),
null,
null);
Assert.Equal("nevra+semver", primitives.GetCoverageTag());
}
[Fact]
public void GetCoverageTag_ReturnsVendorWhenOnlyExtensions()
{
var primitives = new RangePrimitives(
null,
null,
null,
new Dictionary<string, string> { ["vendor.status"] = "beta" });
Assert.True(primitives.HasVendorExtensions);
Assert.Equal("vendor", primitives.GetCoverageTag());
}
[Fact]
public void GetCoverageTag_ReturnsNoneWhenEmpty()
{
var primitives = new RangePrimitives(null, null, null, null);
Assert.False(primitives.HasVendorExtensions);
Assert.Equal("none", primitives.GetCoverageTag());
}
}

View File

@@ -0,0 +1,189 @@
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class SemVerPrimitiveTests
{
[Theory]
[InlineData("1.0.0", true, "2.0.0", false, null, false, null, null, SemVerPrimitiveStyles.Range)]
[InlineData("1.0.0", true, null, false, null, false, null, null, SemVerPrimitiveStyles.GreaterThanOrEqual)]
[InlineData("1.0.0", false, null, false, null, false, null, null, SemVerPrimitiveStyles.GreaterThan)]
[InlineData(null, true, "2.0.0", false, null, false, null, null, SemVerPrimitiveStyles.LessThan)]
[InlineData(null, true, "2.0.0", true, null, false, null, null, SemVerPrimitiveStyles.LessThanOrEqual)]
[InlineData(null, true, null, false, "2.0.0", true, null, null, SemVerPrimitiveStyles.LessThanOrEqual)]
[InlineData(null, true, null, false, "2.0.0", false, null, null, SemVerPrimitiveStyles.LessThan)]
[InlineData(null, true, null, false, null, false, null, "1.5.0", SemVerPrimitiveStyles.Exact)]
public void StyleReflectsSemantics(
string? introduced,
bool introducedInclusive,
string? fixedVersion,
bool fixedInclusive,
string? lastAffected,
bool lastAffectedInclusive,
string? constraintExpression,
string? exactValue,
string expectedStyle)
{
var primitive = new SemVerPrimitive(
introduced,
introducedInclusive,
fixedVersion,
fixedInclusive,
lastAffected,
lastAffectedInclusive,
constraintExpression,
exactValue);
Assert.Equal(expectedStyle, primitive.Style);
}
[Fact]
public void EqualityIncludesExactValue()
{
var baseline = new SemVerPrimitive(
Introduced: null,
IntroducedInclusive: true,
Fixed: null,
FixedInclusive: false,
LastAffected: null,
LastAffectedInclusive: false,
ConstraintExpression: null);
var variant = baseline with { ExactValue = "1.2.3" };
Assert.NotEqual(baseline, variant);
Assert.Equal(SemVerPrimitiveStyles.Exact, variant.Style);
Assert.Equal(SemVerPrimitiveStyles.Range, baseline.Style);
}
[Fact]
public void ToNormalizedVersionRule_MapsRangeBounds()
{
var primitive = new SemVerPrimitive(
Introduced: "1.0.0",
IntroducedInclusive: true,
Fixed: "2.0.0",
FixedInclusive: false,
LastAffected: null,
LastAffectedInclusive: false,
ConstraintExpression: ">=1.0.0 <2.0.0");
var rule = primitive.ToNormalizedVersionRule();
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionSchemes.SemVer, rule!.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.Range, rule.Type);
Assert.Equal("1.0.0", rule.Min);
Assert.True(rule.MinInclusive);
Assert.Equal("2.0.0", rule.Max);
Assert.False(rule.MaxInclusive);
Assert.Null(rule.Value);
Assert.Equal(">=1.0.0 <2.0.0", rule.Notes);
}
[Fact]
public void ToNormalizedVersionRule_ExactUsesExactValue()
{
var primitive = new SemVerPrimitive(
Introduced: null,
IntroducedInclusive: true,
Fixed: null,
FixedInclusive: false,
LastAffected: null,
LastAffectedInclusive: false,
ConstraintExpression: null,
ExactValue: "3.1.4");
var rule = primitive.ToNormalizedVersionRule("from-ghsa");
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionSchemes.SemVer, rule!.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.Exact, rule.Type);
Assert.Null(rule.Min);
Assert.Null(rule.Max);
Assert.Equal("3.1.4", rule.Value);
Assert.Equal("from-ghsa", rule.Notes);
}
[Fact]
public void ToNormalizedVersionRule_GreaterThanMapsMinimum()
{
var primitive = new SemVerPrimitive(
Introduced: "1.5.0",
IntroducedInclusive: false,
Fixed: null,
FixedInclusive: false,
LastAffected: null,
LastAffectedInclusive: false,
ConstraintExpression: null);
var rule = primitive.ToNormalizedVersionRule();
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionSchemes.SemVer, rule!.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.GreaterThan, rule.Type);
Assert.Equal("1.5.0", rule.Min);
Assert.False(rule.MinInclusive);
Assert.Null(rule.Max);
Assert.Null(rule.Value);
Assert.Null(rule.Notes);
}
[Fact]
public void ToNormalizedVersionRule_UsesConstraintExpressionAsFallbackNotes()
{
var primitive = new SemVerPrimitive(
Introduced: "1.4.0",
IntroducedInclusive: false,
Fixed: null,
FixedInclusive: false,
LastAffected: null,
LastAffectedInclusive: false,
ConstraintExpression: "> 1.4.0");
var rule = primitive.ToNormalizedVersionRule();
Assert.NotNull(rule);
Assert.Equal("> 1.4.0", rule!.Notes);
}
[Fact]
public void ToNormalizedVersionRule_ExactCarriesConstraintExpressionWhenNotesMissing()
{
var primitive = new SemVerPrimitive(
Introduced: null,
IntroducedInclusive: true,
Fixed: null,
FixedInclusive: false,
LastAffected: null,
LastAffectedInclusive: false,
ConstraintExpression: "= 3.2.1",
ExactValue: "3.2.1");
var rule = primitive.ToNormalizedVersionRule();
Assert.NotNull(rule);
Assert.Equal(NormalizedVersionRuleTypes.Exact, rule!.Type);
Assert.Equal("3.2.1", rule.Value);
Assert.Equal("= 3.2.1", rule.Notes);
}
[Fact]
public void ToNormalizedVersionRule_ExplicitNotesOverrideConstraintExpression()
{
var primitive = new SemVerPrimitive(
Introduced: "1.0.0",
IntroducedInclusive: true,
Fixed: "1.1.0",
FixedInclusive: false,
LastAffected: null,
LastAffectedInclusive: false,
ConstraintExpression: ">=1.0.0 <1.1.0");
var rule = primitive.ToNormalizedVersionRule("ghsa:range");
Assert.NotNull(rule);
Assert.Equal("ghsa:range", rule!.Notes);
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Models.Tests;
public sealed class SerializationDeterminismTests
{
private static readonly string[] Cultures =
{
"en-US",
"fr-FR",
"tr-TR",
"ja-JP",
"ar-SA"
};
[Fact]
public void CanonicalSerializer_ProducesStableJsonAcrossCultures()
{
var examples = CanonicalExampleFactory.GetExamples().ToArray();
var baseline = SerializeUnderCulture(CultureInfo.InvariantCulture, examples);
foreach (var cultureName in Cultures)
{
var culture = CultureInfo.GetCultureInfo(cultureName);
var serialized = SerializeUnderCulture(culture, examples);
Assert.Equal(baseline.Count, serialized.Count);
for (var i = 0; i < baseline.Count; i++)
{
Assert.Equal(baseline[i].Compact, serialized[i].Compact);
Assert.Equal(baseline[i].Indented, serialized[i].Indented);
}
}
}
private static List<(string Name, string Compact, string Indented)> SerializeUnderCulture(
CultureInfo culture,
IReadOnlyList<(string Name, Advisory Advisory)> examples)
{
var originalCulture = CultureInfo.CurrentCulture;
var originalUiCulture = CultureInfo.CurrentUICulture;
try
{
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
var results = new List<(string Name, string Compact, string Indented)>(examples.Count);
foreach (var (name, advisory) in examples)
{
var compact = CanonicalJsonSerializer.Serialize(advisory);
var indented = CanonicalJsonSerializer.SerializeIndented(advisory);
results.Add((name, compact, indented));
}
return results;
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUiCulture;
}
}
}

View File

@@ -0,0 +1,36 @@
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Models.Tests;
public sealed class SeverityNormalizationTests
{
[Theory]
[InlineData("CRITICAL", "critical")]
[InlineData("Important", "high")]
[InlineData("moderate", "medium")]
[InlineData("Minor", "low")]
[InlineData("Info", "informational")]
[InlineData("negligible", "none")]
[InlineData("unknown", "unknown")]
[InlineData("Sev Critical", "critical")]
[InlineData("HIGH vendor", "high")]
[InlineData("Informative", "informational")]
[InlineData("Not Applicable", "none")]
[InlineData("Undetermined", "unknown")]
[InlineData("Priority 0", "critical")]
[InlineData("Priority-2", "medium")]
[InlineData("N/A", "none")]
[InlineData("custom-level", "custom-level")]
public void Normalize_ReturnsExpectedCanonicalValue(string input, string expected)
{
var normalized = SeverityNormalization.Normalize(input);
Assert.Equal(expected, normalized);
}
[Fact]
public void Normalize_ReturnsNullWhenInputNullOrWhitespace()
{
Assert.Null(SeverityNormalization.Normalize(null));
Assert.Null(SeverityNormalization.Normalize(" "));
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(UpdateGoldens)' == 'true'">
<EnvironmentVariables Include="UPDATE_GOLDENS">
<Value>1</Value>
</EnvironmentVariables>
</ItemGroup>
</Project>