up
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
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
This commit is contained in:
@@ -1,92 +1,92 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Merge.Identity;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AdvisoryIdentityResolverTests
|
||||
{
|
||||
private readonly AdvisoryIdentityResolver _resolver = new();
|
||||
|
||||
[Fact]
|
||||
public void Resolve_GroupsBySharedCveAlias()
|
||||
{
|
||||
var nvd = CreateAdvisory("CVE-2025-1234", aliases: new[] { "CVE-2025-1234" }, source: "nvd");
|
||||
var vendor = CreateAdvisory("VSA-2025-01", aliases: new[] { "CVE-2025-1234", "VSA-2025-01" }, source: "vendor");
|
||||
|
||||
var clusters = _resolver.Resolve(new[] { nvd, vendor });
|
||||
|
||||
var cluster = Assert.Single(clusters);
|
||||
Assert.Equal("CVE-2025-1234", cluster.AdvisoryKey);
|
||||
Assert.Equal(2, cluster.Advisories.Length);
|
||||
Assert.True(cluster.Aliases.Any(alias => alias.Value == "CVE-2025-1234"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_PrefersPsirtAliasWhenNoCve()
|
||||
{
|
||||
var vendor = CreateAdvisory("VMSA-2025-0001", aliases: new[] { "VMSA-2025-0001" }, source: "vmware");
|
||||
var osv = CreateAdvisory("OSV-2025-1", aliases: new[] { "OSV-2025-1", "GHSA-xxxx-yyyy-zzzz", "VMSA-2025-0001" }, source: "osv");
|
||||
|
||||
var clusters = _resolver.Resolve(new[] { vendor, osv });
|
||||
|
||||
var cluster = Assert.Single(clusters);
|
||||
Assert.Equal("VMSA-2025-0001", cluster.AdvisoryKey);
|
||||
Assert.Equal(2, cluster.Advisories.Length);
|
||||
Assert.True(cluster.Aliases.Any(alias => alias.Value == "VMSA-2025-0001"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_FallsBackToGhsaWhenOnlyGhsaPresent()
|
||||
{
|
||||
var ghsa = CreateAdvisory("GHSA-aaaa-bbbb-cccc", aliases: new[] { "GHSA-aaaa-bbbb-cccc" }, source: "ghsa");
|
||||
var osv = CreateAdvisory("OSV-2025-99", aliases: new[] { "OSV-2025-99", "GHSA-aaaa-bbbb-cccc" }, source: "osv");
|
||||
|
||||
var clusters = _resolver.Resolve(new[] { ghsa, osv });
|
||||
|
||||
var cluster = Assert.Single(clusters);
|
||||
Assert.Equal("GHSA-aaaa-bbbb-cccc", cluster.AdvisoryKey);
|
||||
Assert.Equal(2, cluster.Advisories.Length);
|
||||
Assert.True(cluster.Aliases.Any(alias => alias.Value == "GHSA-aaaa-bbbb-cccc"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_GroupsByKeyWhenNoAliases()
|
||||
{
|
||||
var first = CreateAdvisory("custom-1", aliases: Array.Empty<string>(), source: "source-a");
|
||||
var second = CreateAdvisory("custom-1", aliases: Array.Empty<string>(), source: "source-b");
|
||||
|
||||
var clusters = _resolver.Resolve(new[] { first, second });
|
||||
|
||||
var cluster = Assert.Single(clusters);
|
||||
Assert.Equal("custom-1", cluster.AdvisoryKey);
|
||||
Assert.Equal(2, cluster.Advisories.Length);
|
||||
Assert.Contains(cluster.Aliases, alias => alias.Value == "custom-1");
|
||||
}
|
||||
|
||||
private static Advisory CreateAdvisory(string key, string[] aliases, string source)
|
||||
{
|
||||
var provenance = new[]
|
||||
{
|
||||
new AdvisoryProvenance(source, "mapping", key, DateTimeOffset.UtcNow),
|
||||
};
|
||||
|
||||
return new Advisory(
|
||||
key,
|
||||
$"{key} title",
|
||||
$"{key} summary",
|
||||
"en",
|
||||
DateTimeOffset.UtcNow,
|
||||
DateTimeOffset.UtcNow,
|
||||
null,
|
||||
exploitKnown: false,
|
||||
aliases,
|
||||
Array.Empty<AdvisoryCredit>(),
|
||||
Array.Empty<AdvisoryReference>(),
|
||||
Array.Empty<AffectedPackage>(),
|
||||
Array.Empty<CvssMetric>(),
|
||||
provenance);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Merge.Identity;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AdvisoryIdentityResolverTests
|
||||
{
|
||||
private readonly AdvisoryIdentityResolver _resolver = new();
|
||||
|
||||
[Fact]
|
||||
public void Resolve_GroupsBySharedCveAlias()
|
||||
{
|
||||
var nvd = CreateAdvisory("CVE-2025-1234", aliases: new[] { "CVE-2025-1234" }, source: "nvd");
|
||||
var vendor = CreateAdvisory("VSA-2025-01", aliases: new[] { "CVE-2025-1234", "VSA-2025-01" }, source: "vendor");
|
||||
|
||||
var clusters = _resolver.Resolve(new[] { nvd, vendor });
|
||||
|
||||
var cluster = Assert.Single(clusters);
|
||||
Assert.Equal("CVE-2025-1234", cluster.AdvisoryKey);
|
||||
Assert.Equal(2, cluster.Advisories.Length);
|
||||
Assert.True(cluster.Aliases.Any(alias => alias.Value == "CVE-2025-1234"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_PrefersPsirtAliasWhenNoCve()
|
||||
{
|
||||
var vendor = CreateAdvisory("VMSA-2025-0001", aliases: new[] { "VMSA-2025-0001" }, source: "vmware");
|
||||
var osv = CreateAdvisory("OSV-2025-1", aliases: new[] { "OSV-2025-1", "GHSA-xxxx-yyyy-zzzz", "VMSA-2025-0001" }, source: "osv");
|
||||
|
||||
var clusters = _resolver.Resolve(new[] { vendor, osv });
|
||||
|
||||
var cluster = Assert.Single(clusters);
|
||||
Assert.Equal("VMSA-2025-0001", cluster.AdvisoryKey);
|
||||
Assert.Equal(2, cluster.Advisories.Length);
|
||||
Assert.True(cluster.Aliases.Any(alias => alias.Value == "VMSA-2025-0001"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_FallsBackToGhsaWhenOnlyGhsaPresent()
|
||||
{
|
||||
var ghsa = CreateAdvisory("GHSA-aaaa-bbbb-cccc", aliases: new[] { "GHSA-aaaa-bbbb-cccc" }, source: "ghsa");
|
||||
var osv = CreateAdvisory("OSV-2025-99", aliases: new[] { "OSV-2025-99", "GHSA-aaaa-bbbb-cccc" }, source: "osv");
|
||||
|
||||
var clusters = _resolver.Resolve(new[] { ghsa, osv });
|
||||
|
||||
var cluster = Assert.Single(clusters);
|
||||
Assert.Equal("GHSA-aaaa-bbbb-cccc", cluster.AdvisoryKey);
|
||||
Assert.Equal(2, cluster.Advisories.Length);
|
||||
Assert.True(cluster.Aliases.Any(alias => alias.Value == "GHSA-aaaa-bbbb-cccc"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_GroupsByKeyWhenNoAliases()
|
||||
{
|
||||
var first = CreateAdvisory("custom-1", aliases: Array.Empty<string>(), source: "source-a");
|
||||
var second = CreateAdvisory("custom-1", aliases: Array.Empty<string>(), source: "source-b");
|
||||
|
||||
var clusters = _resolver.Resolve(new[] { first, second });
|
||||
|
||||
var cluster = Assert.Single(clusters);
|
||||
Assert.Equal("custom-1", cluster.AdvisoryKey);
|
||||
Assert.Equal(2, cluster.Advisories.Length);
|
||||
Assert.Contains(cluster.Aliases, alias => alias.Value == "custom-1");
|
||||
}
|
||||
|
||||
private static Advisory CreateAdvisory(string key, string[] aliases, string source)
|
||||
{
|
||||
var provenance = new[]
|
||||
{
|
||||
new AdvisoryProvenance(source, "mapping", key, DateTimeOffset.UtcNow),
|
||||
};
|
||||
|
||||
return new Advisory(
|
||||
key,
|
||||
$"{key} title",
|
||||
$"{key} summary",
|
||||
"en",
|
||||
DateTimeOffset.UtcNow,
|
||||
DateTimeOffset.UtcNow,
|
||||
null,
|
||||
exploitKnown: false,
|
||||
aliases,
|
||||
Array.Empty<AdvisoryCredit>(),
|
||||
Array.Empty<AdvisoryReference>(),
|
||||
Array.Empty<AffectedPackage>(),
|
||||
Array.Empty<CvssMetric>(),
|
||||
provenance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Storage.Aliases;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
using StellaOps.Provenance.Mongo;
|
||||
using StellaOps.Provenance;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,96 +1,96 @@
|
||||
using System;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AffectedPackagePrecedenceResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public void Merge_PrefersRedHatOverNvdForSameCpe()
|
||||
{
|
||||
var redHat = new AffectedPackage(
|
||||
type: AffectedPackageTypes.Cpe,
|
||||
identifier: "cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
|
||||
platform: "RHEL 9",
|
||||
versionRanges: Array.Empty<AffectedVersionRange>(),
|
||||
statuses: new[]
|
||||
{
|
||||
new AffectedPackageStatus(
|
||||
status: "known_affected",
|
||||
provenance: new AdvisoryProvenance("redhat", "oval", "RHEL-9", DateTimeOffset.Parse("2025-10-01T00:00:00Z")))
|
||||
},
|
||||
provenance: new[]
|
||||
{
|
||||
new AdvisoryProvenance("redhat", "oval", "RHEL-9", DateTimeOffset.Parse("2025-10-01T00:00:00Z"))
|
||||
});
|
||||
|
||||
var nvd = new AffectedPackage(
|
||||
type: AffectedPackageTypes.Cpe,
|
||||
identifier: "cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
|
||||
platform: "RHEL 9",
|
||||
versionRanges: new[]
|
||||
{
|
||||
new AffectedVersionRange(
|
||||
rangeKind: "cpe",
|
||||
introducedVersion: null,
|
||||
fixedVersion: null,
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: "<=9.0",
|
||||
provenance: new AdvisoryProvenance("nvd", "cpe_match", "RHEL-9", DateTimeOffset.Parse("2025-09-30T00:00:00Z")))
|
||||
},
|
||||
provenance: new[]
|
||||
{
|
||||
new AdvisoryProvenance("nvd", "cpe_match", "RHEL-9", DateTimeOffset.Parse("2025-09-30T00:00:00Z"))
|
||||
});
|
||||
|
||||
var resolver = new AffectedPackagePrecedenceResolver();
|
||||
var result = resolver.Merge(new[] { nvd, redHat });
|
||||
|
||||
var package = Assert.Single(result.Packages);
|
||||
Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", package.Identifier);
|
||||
Assert.Empty(package.VersionRanges); // NVD range overridden
|
||||
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");
|
||||
|
||||
var rangeOverride = Assert.Single(result.Overrides);
|
||||
Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", rangeOverride.Identifier);
|
||||
Assert.Equal(0, rangeOverride.PrimaryRank);
|
||||
Assert.True(rangeOverride.SuppressedRank >= rangeOverride.PrimaryRank);
|
||||
Assert.Equal(0, rangeOverride.PrimaryRangeCount);
|
||||
Assert.Equal(1, rangeOverride.SuppressedRangeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_KeepsNvdWhenNoHigherPrecedence()
|
||||
{
|
||||
var nvd = new AffectedPackage(
|
||||
type: AffectedPackageTypes.Cpe,
|
||||
identifier: "cpe:2.3:a:example:product:1.0:*:*:*:*:*:*:*",
|
||||
platform: null,
|
||||
versionRanges: new[]
|
||||
{
|
||||
new AffectedVersionRange(
|
||||
rangeKind: "semver",
|
||||
introducedVersion: null,
|
||||
fixedVersion: "1.0.1",
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: "<1.0.1",
|
||||
provenance: new AdvisoryProvenance("nvd", "cpe_match", "product", DateTimeOffset.Parse("2025-09-01T00:00:00Z")))
|
||||
},
|
||||
provenance: new[]
|
||||
{
|
||||
new AdvisoryProvenance("nvd", "cpe_match", "product", DateTimeOffset.Parse("2025-09-01T00:00:00Z"))
|
||||
});
|
||||
|
||||
var resolver = new AffectedPackagePrecedenceResolver();
|
||||
var result = resolver.Merge(new[] { nvd });
|
||||
|
||||
var package = Assert.Single(result.Packages);
|
||||
Assert.Equal(nvd.Identifier, package.Identifier);
|
||||
Assert.Equal(nvd.VersionRanges.Single().RangeExpression, package.VersionRanges.Single().RangeExpression);
|
||||
Assert.Equal("nvd", package.Provenance.Single().Source);
|
||||
Assert.Empty(result.Overrides);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AffectedPackagePrecedenceResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public void Merge_PrefersRedHatOverNvdForSameCpe()
|
||||
{
|
||||
var redHat = new AffectedPackage(
|
||||
type: AffectedPackageTypes.Cpe,
|
||||
identifier: "cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
|
||||
platform: "RHEL 9",
|
||||
versionRanges: Array.Empty<AffectedVersionRange>(),
|
||||
statuses: new[]
|
||||
{
|
||||
new AffectedPackageStatus(
|
||||
status: "known_affected",
|
||||
provenance: new AdvisoryProvenance("redhat", "oval", "RHEL-9", DateTimeOffset.Parse("2025-10-01T00:00:00Z")))
|
||||
},
|
||||
provenance: new[]
|
||||
{
|
||||
new AdvisoryProvenance("redhat", "oval", "RHEL-9", DateTimeOffset.Parse("2025-10-01T00:00:00Z"))
|
||||
});
|
||||
|
||||
var nvd = new AffectedPackage(
|
||||
type: AffectedPackageTypes.Cpe,
|
||||
identifier: "cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
|
||||
platform: "RHEL 9",
|
||||
versionRanges: new[]
|
||||
{
|
||||
new AffectedVersionRange(
|
||||
rangeKind: "cpe",
|
||||
introducedVersion: null,
|
||||
fixedVersion: null,
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: "<=9.0",
|
||||
provenance: new AdvisoryProvenance("nvd", "cpe_match", "RHEL-9", DateTimeOffset.Parse("2025-09-30T00:00:00Z")))
|
||||
},
|
||||
provenance: new[]
|
||||
{
|
||||
new AdvisoryProvenance("nvd", "cpe_match", "RHEL-9", DateTimeOffset.Parse("2025-09-30T00:00:00Z"))
|
||||
});
|
||||
|
||||
var resolver = new AffectedPackagePrecedenceResolver();
|
||||
var result = resolver.Merge(new[] { nvd, redHat });
|
||||
|
||||
var package = Assert.Single(result.Packages);
|
||||
Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", package.Identifier);
|
||||
Assert.Empty(package.VersionRanges); // NVD range overridden
|
||||
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");
|
||||
|
||||
var rangeOverride = Assert.Single(result.Overrides);
|
||||
Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", rangeOverride.Identifier);
|
||||
Assert.Equal(0, rangeOverride.PrimaryRank);
|
||||
Assert.True(rangeOverride.SuppressedRank >= rangeOverride.PrimaryRank);
|
||||
Assert.Equal(0, rangeOverride.PrimaryRangeCount);
|
||||
Assert.Equal(1, rangeOverride.SuppressedRangeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_KeepsNvdWhenNoHigherPrecedence()
|
||||
{
|
||||
var nvd = new AffectedPackage(
|
||||
type: AffectedPackageTypes.Cpe,
|
||||
identifier: "cpe:2.3:a:example:product:1.0:*:*:*:*:*:*:*",
|
||||
platform: null,
|
||||
versionRanges: new[]
|
||||
{
|
||||
new AffectedVersionRange(
|
||||
rangeKind: "semver",
|
||||
introducedVersion: null,
|
||||
fixedVersion: "1.0.1",
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: "<1.0.1",
|
||||
provenance: new AdvisoryProvenance("nvd", "cpe_match", "product", DateTimeOffset.Parse("2025-09-01T00:00:00Z")))
|
||||
},
|
||||
provenance: new[]
|
||||
{
|
||||
new AdvisoryProvenance("nvd", "cpe_match", "product", DateTimeOffset.Parse("2025-09-01T00:00:00Z"))
|
||||
});
|
||||
|
||||
var resolver = new AffectedPackagePrecedenceResolver();
|
||||
var result = resolver.Merge(new[] { nvd });
|
||||
|
||||
var package = Assert.Single(result.Packages);
|
||||
Assert.Equal(nvd.Identifier, package.Identifier);
|
||||
Assert.Equal(nvd.VersionRanges.Single().RangeExpression, package.VersionRanges.Single().RangeExpression);
|
||||
Assert.Equal("nvd", package.Provenance.Single().Source);
|
||||
Assert.Empty(result.Overrides);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,59 +15,59 @@ public sealed class AliasGraphResolverTests
|
||||
var resolver = new AliasGraphResolver(aliasStore);
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-1",
|
||||
new[] { new AliasEntry("CVE", "CVE-2025-2000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-1") },
|
||||
timestamp,
|
||||
CancellationToken.None);
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-2",
|
||||
new[] { new AliasEntry("CVE", "CVE-2025-2000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-2") },
|
||||
timestamp.AddMinutes(1),
|
||||
CancellationToken.None);
|
||||
|
||||
var result = await resolver.ResolveAsync("ADV-1", CancellationToken.None);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("ADV-1", result.AdvisoryKey);
|
||||
Assert.NotEmpty(result.Collisions);
|
||||
var collision = Assert.Single(result.Collisions);
|
||||
Assert.Equal("CVE", collision.Scheme);
|
||||
Assert.Contains("ADV-1", collision.AdvisoryKeys);
|
||||
Assert.Contains("ADV-2", collision.AdvisoryKeys);
|
||||
}
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-1",
|
||||
new[] { new AliasEntry("CVE", "CVE-2025-2000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-1") },
|
||||
timestamp,
|
||||
CancellationToken.None);
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-2",
|
||||
new[] { new AliasEntry("CVE", "CVE-2025-2000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-2") },
|
||||
timestamp.AddMinutes(1),
|
||||
CancellationToken.None);
|
||||
|
||||
var result = await resolver.ResolveAsync("ADV-1", CancellationToken.None);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("ADV-1", result.AdvisoryKey);
|
||||
Assert.NotEmpty(result.Collisions);
|
||||
var collision = Assert.Single(result.Collisions);
|
||||
Assert.Equal("CVE", collision.Scheme);
|
||||
Assert.Contains("ADV-1", collision.AdvisoryKeys);
|
||||
Assert.Contains("ADV-2", collision.AdvisoryKeys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildComponentAsync_TracesConnectedAdvisories()
|
||||
{
|
||||
var aliasStore = new AliasStore();
|
||||
var resolver = new AliasGraphResolver(aliasStore);
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-A",
|
||||
new[] { new AliasEntry("CVE", "CVE-2025-4000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-A") },
|
||||
timestamp,
|
||||
CancellationToken.None);
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-B",
|
||||
new[] { new AliasEntry("CVE", "CVE-2025-4000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-B"), new AliasEntry("OSV", "OSV-2025-1") },
|
||||
timestamp.AddMinutes(1),
|
||||
CancellationToken.None);
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-C",
|
||||
new[] { new AliasEntry("OSV", "OSV-2025-1"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-C") },
|
||||
timestamp.AddMinutes(2),
|
||||
CancellationToken.None);
|
||||
|
||||
var component = await resolver.BuildComponentAsync("ADV-A", CancellationToken.None);
|
||||
Assert.Contains("ADV-A", component.AdvisoryKeys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains("ADV-B", component.AdvisoryKeys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains("ADV-C", component.AdvisoryKeys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.NotEmpty(component.Collisions);
|
||||
Assert.True(component.AliasMap.ContainsKey("ADV-A"));
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-A",
|
||||
new[] { new AliasEntry("CVE", "CVE-2025-4000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-A") },
|
||||
timestamp,
|
||||
CancellationToken.None);
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-B",
|
||||
new[] { new AliasEntry("CVE", "CVE-2025-4000"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-B"), new AliasEntry("OSV", "OSV-2025-1") },
|
||||
timestamp.AddMinutes(1),
|
||||
CancellationToken.None);
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-C",
|
||||
new[] { new AliasEntry("OSV", "OSV-2025-1"), new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-C") },
|
||||
timestamp.AddMinutes(2),
|
||||
CancellationToken.None);
|
||||
|
||||
var component = await resolver.BuildComponentAsync("ADV-A", CancellationToken.None);
|
||||
Assert.Contains("ADV-A", component.AdvisoryKeys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains("ADV-B", component.AdvisoryKeys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains("ADV-C", component.AdvisoryKeys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.NotEmpty(component.Collisions);
|
||||
Assert.True(component.AliasMap.ContainsKey("ADV-A"));
|
||||
Assert.Contains(component.AliasMap["ADV-B"], record => record.Scheme == "OSV" && record.Value == "OSV-2025-1");
|
||||
}
|
||||
|
||||
@@ -77,31 +77,31 @@ public sealed class AliasGraphResolverTests
|
||||
var aliasStore = new AliasStore();
|
||||
var resolver = new AliasGraphResolver(aliasStore);
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-OSV",
|
||||
new[]
|
||||
{
|
||||
new AliasEntry("OSV", "OSV-2025-2001"),
|
||||
new AliasEntry("GHSA", "GHSA-zzzz-zzzz-zzzz"),
|
||||
new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-OSV"),
|
||||
},
|
||||
timestamp,
|
||||
CancellationToken.None);
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-GHSA",
|
||||
new[]
|
||||
{
|
||||
new AliasEntry("GHSA", "GHSA-zzzz-zzzz-zzzz"),
|
||||
new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-GHSA"),
|
||||
},
|
||||
timestamp.AddMinutes(1),
|
||||
CancellationToken.None);
|
||||
|
||||
var component = await resolver.BuildComponentAsync("ADV-OSV", CancellationToken.None);
|
||||
|
||||
Assert.Contains("ADV-GHSA", component.AdvisoryKeys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains(component.Collisions, collision => collision.Scheme == "GHSA" && collision.Value == "GHSA-zzzz-zzzz-zzzz");
|
||||
}
|
||||
}
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-OSV",
|
||||
new[]
|
||||
{
|
||||
new AliasEntry("OSV", "OSV-2025-2001"),
|
||||
new AliasEntry("GHSA", "GHSA-zzzz-zzzz-zzzz"),
|
||||
new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-OSV"),
|
||||
},
|
||||
timestamp,
|
||||
CancellationToken.None);
|
||||
|
||||
await aliasStore.ReplaceAsync(
|
||||
"ADV-GHSA",
|
||||
new[]
|
||||
{
|
||||
new AliasEntry("GHSA", "GHSA-zzzz-zzzz-zzzz"),
|
||||
new AliasEntry(AliasStoreConstants.PrimaryScheme, "ADV-GHSA"),
|
||||
},
|
||||
timestamp.AddMinutes(1),
|
||||
CancellationToken.None);
|
||||
|
||||
var component = await resolver.BuildComponentAsync("ADV-OSV", CancellationToken.None);
|
||||
|
||||
Assert.Contains("ADV-GHSA", component.AdvisoryKeys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains(component.Collisions, collision => collision.Scheme == "GHSA" && collision.Value == "GHSA-zzzz-zzzz-zzzz");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class CanonicalHashCalculatorTests
|
||||
{
|
||||
private static readonly Advisory SampleAdvisory = new(
|
||||
advisoryKey: "CVE-2024-0001",
|
||||
title: "Sample advisory",
|
||||
summary: "A sample summary",
|
||||
language: "EN",
|
||||
published: DateTimeOffset.Parse("2024-01-01T00:00:00Z"),
|
||||
modified: DateTimeOffset.Parse("2024-01-02T00:00:00Z"),
|
||||
severity: "high",
|
||||
exploitKnown: true,
|
||||
aliases: new[] { "GHSA-xyz", "CVE-2024-0001" },
|
||||
references: new[]
|
||||
{
|
||||
new AdvisoryReference("https://example.com/advisory", "external", "vendor", summary: null, provenance: AdvisoryProvenance.Empty),
|
||||
new AdvisoryReference("https://example.com/blog", "article", "blog", summary: null, provenance: AdvisoryProvenance.Empty),
|
||||
},
|
||||
affectedPackages: new[]
|
||||
{
|
||||
new AffectedPackage(
|
||||
type: AffectedPackageTypes.SemVer,
|
||||
identifier: "pkg:npm/sample@1.0.0",
|
||||
platform: null,
|
||||
versionRanges: new[]
|
||||
{
|
||||
new AffectedVersionRange("semver", "1.0.0", "1.2.0", null, null, AdvisoryProvenance.Empty),
|
||||
new AffectedVersionRange("semver", "1.2.0", null, null, null, AdvisoryProvenance.Empty),
|
||||
},
|
||||
statuses: Array.Empty<AffectedPackageStatus>(),
|
||||
provenance: new[] { AdvisoryProvenance.Empty })
|
||||
},
|
||||
cvssMetrics: new[]
|
||||
{
|
||||
new CvssMetric("3.1", "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 9.8, "critical", AdvisoryProvenance.Empty)
|
||||
},
|
||||
provenance: new[] { AdvisoryProvenance.Empty });
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_ReturnsDeterministicValue()
|
||||
{
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
var first = calculator.ComputeHash(SampleAdvisory);
|
||||
var second = calculator.ComputeHash(SampleAdvisory);
|
||||
|
||||
Assert.Equal(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_IgnoresOrderingDifferences()
|
||||
{
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
|
||||
var reordered = new Advisory(
|
||||
SampleAdvisory.AdvisoryKey,
|
||||
SampleAdvisory.Title,
|
||||
SampleAdvisory.Summary,
|
||||
SampleAdvisory.Language,
|
||||
SampleAdvisory.Published,
|
||||
SampleAdvisory.Modified,
|
||||
SampleAdvisory.Severity,
|
||||
SampleAdvisory.ExploitKnown,
|
||||
aliases: SampleAdvisory.Aliases.Reverse().ToArray(),
|
||||
references: SampleAdvisory.References.Reverse().ToArray(),
|
||||
affectedPackages: SampleAdvisory.AffectedPackages.Reverse().ToArray(),
|
||||
cvssMetrics: SampleAdvisory.CvssMetrics.Reverse().ToArray(),
|
||||
provenance: SampleAdvisory.Provenance.Reverse().ToArray());
|
||||
|
||||
var originalHash = calculator.ComputeHash(SampleAdvisory);
|
||||
var reorderedHash = calculator.ComputeHash(reordered);
|
||||
|
||||
Assert.Equal(originalHash, reorderedHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_NullReturnsEmpty()
|
||||
{
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
Assert.Empty(calculator.ComputeHash(null));
|
||||
}
|
||||
}
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class CanonicalHashCalculatorTests
|
||||
{
|
||||
private static readonly Advisory SampleAdvisory = new(
|
||||
advisoryKey: "CVE-2024-0001",
|
||||
title: "Sample advisory",
|
||||
summary: "A sample summary",
|
||||
language: "EN",
|
||||
published: DateTimeOffset.Parse("2024-01-01T00:00:00Z"),
|
||||
modified: DateTimeOffset.Parse("2024-01-02T00:00:00Z"),
|
||||
severity: "high",
|
||||
exploitKnown: true,
|
||||
aliases: new[] { "GHSA-xyz", "CVE-2024-0001" },
|
||||
references: new[]
|
||||
{
|
||||
new AdvisoryReference("https://example.com/advisory", "external", "vendor", summary: null, provenance: AdvisoryProvenance.Empty),
|
||||
new AdvisoryReference("https://example.com/blog", "article", "blog", summary: null, provenance: AdvisoryProvenance.Empty),
|
||||
},
|
||||
affectedPackages: new[]
|
||||
{
|
||||
new AffectedPackage(
|
||||
type: AffectedPackageTypes.SemVer,
|
||||
identifier: "pkg:npm/sample@1.0.0",
|
||||
platform: null,
|
||||
versionRanges: new[]
|
||||
{
|
||||
new AffectedVersionRange("semver", "1.0.0", "1.2.0", null, null, AdvisoryProvenance.Empty),
|
||||
new AffectedVersionRange("semver", "1.2.0", null, null, null, AdvisoryProvenance.Empty),
|
||||
},
|
||||
statuses: Array.Empty<AffectedPackageStatus>(),
|
||||
provenance: new[] { AdvisoryProvenance.Empty })
|
||||
},
|
||||
cvssMetrics: new[]
|
||||
{
|
||||
new CvssMetric("3.1", "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 9.8, "critical", AdvisoryProvenance.Empty)
|
||||
},
|
||||
provenance: new[] { AdvisoryProvenance.Empty });
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_ReturnsDeterministicValue()
|
||||
{
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
var first = calculator.ComputeHash(SampleAdvisory);
|
||||
var second = calculator.ComputeHash(SampleAdvisory);
|
||||
|
||||
Assert.Equal(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_IgnoresOrderingDifferences()
|
||||
{
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
|
||||
var reordered = new Advisory(
|
||||
SampleAdvisory.AdvisoryKey,
|
||||
SampleAdvisory.Title,
|
||||
SampleAdvisory.Summary,
|
||||
SampleAdvisory.Language,
|
||||
SampleAdvisory.Published,
|
||||
SampleAdvisory.Modified,
|
||||
SampleAdvisory.Severity,
|
||||
SampleAdvisory.ExploitKnown,
|
||||
aliases: SampleAdvisory.Aliases.Reverse().ToArray(),
|
||||
references: SampleAdvisory.References.Reverse().ToArray(),
|
||||
affectedPackages: SampleAdvisory.AffectedPackages.Reverse().ToArray(),
|
||||
cvssMetrics: SampleAdvisory.CvssMetrics.Reverse().ToArray(),
|
||||
provenance: SampleAdvisory.Provenance.Reverse().ToArray());
|
||||
|
||||
var originalHash = calculator.ComputeHash(SampleAdvisory);
|
||||
var reorderedHash = calculator.ComputeHash(reordered);
|
||||
|
||||
Assert.Equal(originalHash, reorderedHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_NullReturnsEmpty()
|
||||
{
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
Assert.Empty(calculator.ComputeHash(null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class DebianEvrComparerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("1:1.2.3-1", 1, "1.2.3", "1")]
|
||||
[InlineData("1.2.3-1", 0, "1.2.3", "1")]
|
||||
[InlineData("2:4.5", 2, "4.5", "")]
|
||||
[InlineData("abc", 0, "abc", "")]
|
||||
public void TryParse_ReturnsComponents(string input, int expectedEpoch, string expectedVersion, string expectedRevision)
|
||||
{
|
||||
var success = DebianEvr.TryParse(input, out var evr);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.NotNull(evr);
|
||||
Assert.Equal(expectedEpoch, evr!.Epoch);
|
||||
Assert.Equal(expectedVersion, evr.Version);
|
||||
Assert.Equal(expectedRevision, evr.Revision);
|
||||
Assert.Equal(input, evr.Original);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(":1.0-1")]
|
||||
[InlineData("1:")]
|
||||
public void TryParse_InvalidInputs_ReturnFalse(string input)
|
||||
{
|
||||
var success = DebianEvr.TryParse(input, out var evr);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Null(evr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_PrefersHigherEpoch()
|
||||
{
|
||||
var lower = "0:2.0-1";
|
||||
var higher = "1:1.0-1";
|
||||
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(higher, lower) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UsesVersionOrdering()
|
||||
{
|
||||
var lower = "0:1.2.3-1";
|
||||
var higher = "0:1.10.0-1";
|
||||
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(higher, lower) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_TildeRanksEarlier()
|
||||
{
|
||||
var prerelease = "0:1.0~beta1-1";
|
||||
var stable = "0:1.0-1";
|
||||
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(prerelease, stable) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_RevisionBreaksTies()
|
||||
{
|
||||
var first = "0:1.0-1";
|
||||
var second = "0:1.0-2";
|
||||
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(second, first) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_FallsBackToOrdinalForInvalid()
|
||||
{
|
||||
var left = "not-an-evr";
|
||||
var right = "also-not";
|
||||
|
||||
var expected = Math.Sign(string.CompareOrdinal(left, right));
|
||||
var actual = Math.Sign(DebianEvrComparer.Instance.Compare(left, right));
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class DebianEvrComparerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("1:1.2.3-1", 1, "1.2.3", "1")]
|
||||
[InlineData("1.2.3-1", 0, "1.2.3", "1")]
|
||||
[InlineData("2:4.5", 2, "4.5", "")]
|
||||
[InlineData("abc", 0, "abc", "")]
|
||||
public void TryParse_ReturnsComponents(string input, int expectedEpoch, string expectedVersion, string expectedRevision)
|
||||
{
|
||||
var success = DebianEvr.TryParse(input, out var evr);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.NotNull(evr);
|
||||
Assert.Equal(expectedEpoch, evr!.Epoch);
|
||||
Assert.Equal(expectedVersion, evr.Version);
|
||||
Assert.Equal(expectedRevision, evr.Revision);
|
||||
Assert.Equal(input, evr.Original);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(":1.0-1")]
|
||||
[InlineData("1:")]
|
||||
public void TryParse_InvalidInputs_ReturnFalse(string input)
|
||||
{
|
||||
var success = DebianEvr.TryParse(input, out var evr);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Null(evr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_PrefersHigherEpoch()
|
||||
{
|
||||
var lower = "0:2.0-1";
|
||||
var higher = "1:1.0-1";
|
||||
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(higher, lower) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UsesVersionOrdering()
|
||||
{
|
||||
var lower = "0:1.2.3-1";
|
||||
var higher = "0:1.10.0-1";
|
||||
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(higher, lower) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_TildeRanksEarlier()
|
||||
{
|
||||
var prerelease = "0:1.0~beta1-1";
|
||||
var stable = "0:1.0-1";
|
||||
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(prerelease, stable) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_RevisionBreaksTies()
|
||||
{
|
||||
var first = "0:1.0-1";
|
||||
var second = "0:1.0-2";
|
||||
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(second, first) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_FallsBackToOrdinalForInvalid()
|
||||
{
|
||||
var left = "not-an-evr";
|
||||
var right = "also-not";
|
||||
|
||||
var expected = Math.Sign(string.CompareOrdinal(left, right));
|
||||
var actual = Math.Sign(DebianEvrComparer.Instance.Compare(left, right));
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class MergeEventWriterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AppendAsync_WritesRecordWithComputedHashes()
|
||||
{
|
||||
var store = new InMemoryMergeEventStore();
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2024-05-01T00:00:00Z"));
|
||||
var writer = new MergeEventWriter(store, calculator, timeProvider, NullLogger<MergeEventWriter>.Instance);
|
||||
|
||||
var before = CreateAdvisory("CVE-2024-0001", "Initial");
|
||||
var after = CreateAdvisory("CVE-2024-0001", "Sample", summary: "Updated");
|
||||
|
||||
var documentIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class MergeEventWriterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AppendAsync_WritesRecordWithComputedHashes()
|
||||
{
|
||||
var store = new InMemoryMergeEventStore();
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2024-05-01T00:00:00Z"));
|
||||
var writer = new MergeEventWriter(store, calculator, timeProvider, NullLogger<MergeEventWriter>.Instance);
|
||||
|
||||
var before = CreateAdvisory("CVE-2024-0001", "Initial");
|
||||
var after = CreateAdvisory("CVE-2024-0001", "Sample", summary: "Updated");
|
||||
|
||||
var documentIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
|
||||
var record = await writer.AppendAsync("CVE-2024-0001", before, after, documentIds, Array.Empty<MergeFieldDecision>(), CancellationToken.None);
|
||||
|
||||
Assert.NotEqual(Guid.Empty, record.Id);
|
||||
Assert.Equal("CVE-2024-0001", record.AdvisoryKey);
|
||||
Assert.True(record.AfterHash.Length > 0);
|
||||
Assert.Equal(timeProvider.GetUtcNow(), record.MergedAt);
|
||||
Assert.Equal(documentIds, record.InputDocumentIds);
|
||||
Assert.NotNull(store.LastRecord);
|
||||
Assert.Same(store.LastRecord, record);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppendAsync_NullBeforeUsesEmptyHash()
|
||||
{
|
||||
var store = new InMemoryMergeEventStore();
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2024-05-01T00:00:00Z"));
|
||||
var writer = new MergeEventWriter(store, calculator, timeProvider, NullLogger<MergeEventWriter>.Instance);
|
||||
|
||||
var after = CreateAdvisory("CVE-2024-0002", "Changed");
|
||||
|
||||
|
||||
Assert.NotEqual(Guid.Empty, record.Id);
|
||||
Assert.Equal("CVE-2024-0001", record.AdvisoryKey);
|
||||
Assert.True(record.AfterHash.Length > 0);
|
||||
Assert.Equal(timeProvider.GetUtcNow(), record.MergedAt);
|
||||
Assert.Equal(documentIds, record.InputDocumentIds);
|
||||
Assert.NotNull(store.LastRecord);
|
||||
Assert.Same(store.LastRecord, record);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppendAsync_NullBeforeUsesEmptyHash()
|
||||
{
|
||||
var store = new InMemoryMergeEventStore();
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2024-05-01T00:00:00Z"));
|
||||
var writer = new MergeEventWriter(store, calculator, timeProvider, NullLogger<MergeEventWriter>.Instance);
|
||||
|
||||
var after = CreateAdvisory("CVE-2024-0002", "Changed");
|
||||
|
||||
var record = await writer.AppendAsync("CVE-2024-0002", null, after, Array.Empty<Guid>(), Array.Empty<MergeFieldDecision>(), CancellationToken.None);
|
||||
|
||||
Assert.Empty(record.BeforeHash);
|
||||
Assert.True(record.AfterHash.Length > 0);
|
||||
}
|
||||
|
||||
|
||||
private static Advisory CreateAdvisory(string advisoryKey, string title, string? summary = null)
|
||||
{
|
||||
return new Advisory(
|
||||
advisoryKey,
|
||||
title,
|
||||
summary,
|
||||
language: "en",
|
||||
published: DateTimeOffset.Parse("2024-01-01T00:00:00Z"),
|
||||
modified: DateTimeOffset.Parse("2024-01-02T00:00:00Z"),
|
||||
severity: "medium",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { advisoryKey },
|
||||
references: new[]
|
||||
{
|
||||
new AdvisoryReference("https://example.com/" + advisoryKey.ToLowerInvariant(), "external", "vendor", summary: null, provenance: AdvisoryProvenance.Empty)
|
||||
},
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: Array.Empty<AdvisoryProvenance>());
|
||||
}
|
||||
|
||||
private sealed class InMemoryMergeEventStore : IMergeEventStore
|
||||
{
|
||||
public MergeEventRecord? LastRecord { get; private set; }
|
||||
|
||||
public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRecord = record;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<MergeEventRecord>>(Array.Empty<MergeEventRecord>());
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Empty(record.BeforeHash);
|
||||
Assert.True(record.AfterHash.Length > 0);
|
||||
}
|
||||
|
||||
|
||||
private static Advisory CreateAdvisory(string advisoryKey, string title, string? summary = null)
|
||||
{
|
||||
return new Advisory(
|
||||
advisoryKey,
|
||||
title,
|
||||
summary,
|
||||
language: "en",
|
||||
published: DateTimeOffset.Parse("2024-01-01T00:00:00Z"),
|
||||
modified: DateTimeOffset.Parse("2024-01-02T00:00:00Z"),
|
||||
severity: "medium",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { advisoryKey },
|
||||
references: new[]
|
||||
{
|
||||
new AdvisoryReference("https://example.com/" + advisoryKey.ToLowerInvariant(), "external", "vendor", summary: null, provenance: AdvisoryProvenance.Empty)
|
||||
},
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: Array.Empty<AdvisoryProvenance>());
|
||||
}
|
||||
|
||||
private sealed class InMemoryMergeEventStore : IMergeEventStore
|
||||
{
|
||||
public MergeEventRecord? LastRecord { get; private set; }
|
||||
|
||||
public Task AppendAsync(MergeEventRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRecord = record;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<MergeEventRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<IReadOnlyList<MergeEventRecord>>(Array.Empty<MergeEventRecord>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
@@ -15,69 +15,69 @@ public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime
|
||||
private MergeEventWriter? _mergeEventWriter;
|
||||
private AdvisoryPrecedenceMerger? _merger;
|
||||
private FakeTimeProvider? _timeProvider;
|
||||
|
||||
[Fact]
|
||||
public async Task MergePipeline_PsirtOverridesNvd_AndKevOnlyTogglesExploitKnown()
|
||||
{
|
||||
await EnsureInitializedAsync();
|
||||
|
||||
var merger = _merger!;
|
||||
var writer = _mergeEventWriter!;
|
||||
var store = _mergeEventStore!;
|
||||
var timeProvider = _timeProvider!;
|
||||
|
||||
var expectedTimestamp = timeProvider.GetUtcNow();
|
||||
|
||||
var nvd = CreateNvdBaseline();
|
||||
var vendor = CreateVendorOverride();
|
||||
var kev = CreateKevSignal();
|
||||
|
||||
var merged = merger.Merge(new[] { nvd, vendor, kev }).Advisory;
|
||||
|
||||
Assert.Equal("CVE-2025-1000", merged.AdvisoryKey);
|
||||
Assert.Equal("Vendor Security Advisory", merged.Title);
|
||||
Assert.Equal("Critical impact on supported platforms.", merged.Summary);
|
||||
Assert.Equal("critical", merged.Severity);
|
||||
Assert.True(merged.ExploitKnown);
|
||||
|
||||
var affected = Assert.Single(merged.AffectedPackages);
|
||||
Assert.Empty(affected.VersionRanges);
|
||||
Assert.Contains(affected.Statuses, status => status.Status == "known_affected" && status.Provenance.Source == "vendor");
|
||||
|
||||
var mergeProvenance = Assert.Single(merged.Provenance, p => p.Source == "merge");
|
||||
Assert.Equal("precedence", mergeProvenance.Kind);
|
||||
Assert.Equal(expectedTimestamp, mergeProvenance.RecordedAt);
|
||||
Assert.Contains("vendor", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("kev", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var inputDocumentIds = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
||||
var record = await writer.AppendAsync(merged.AdvisoryKey, nvd, merged, inputDocumentIds, Array.Empty<MergeFieldDecision>(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(expectedTimestamp, record.MergedAt);
|
||||
Assert.Equal(inputDocumentIds, record.InputDocumentIds);
|
||||
Assert.NotEqual(record.BeforeHash, record.AfterHash);
|
||||
|
||||
var records = await store.GetRecentAsync(merged.AdvisoryKey, 5, CancellationToken.None);
|
||||
var persisted = Assert.Single(records);
|
||||
Assert.Equal(record.Id, persisted.Id);
|
||||
Assert.Equal(merged.AdvisoryKey, persisted.AdvisoryKey);
|
||||
Assert.True(persisted.AfterHash.Length > 0);
|
||||
Assert.True(persisted.BeforeHash.Length > 0);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero))
|
||||
{
|
||||
AutoAdvanceAmount = TimeSpan.Zero,
|
||||
};
|
||||
_merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), _timeProvider);
|
||||
|
||||
[Fact]
|
||||
public async Task MergePipeline_PsirtOverridesNvd_AndKevOnlyTogglesExploitKnown()
|
||||
{
|
||||
await EnsureInitializedAsync();
|
||||
|
||||
var merger = _merger!;
|
||||
var writer = _mergeEventWriter!;
|
||||
var store = _mergeEventStore!;
|
||||
var timeProvider = _timeProvider!;
|
||||
|
||||
var expectedTimestamp = timeProvider.GetUtcNow();
|
||||
|
||||
var nvd = CreateNvdBaseline();
|
||||
var vendor = CreateVendorOverride();
|
||||
var kev = CreateKevSignal();
|
||||
|
||||
var merged = merger.Merge(new[] { nvd, vendor, kev }).Advisory;
|
||||
|
||||
Assert.Equal("CVE-2025-1000", merged.AdvisoryKey);
|
||||
Assert.Equal("Vendor Security Advisory", merged.Title);
|
||||
Assert.Equal("Critical impact on supported platforms.", merged.Summary);
|
||||
Assert.Equal("critical", merged.Severity);
|
||||
Assert.True(merged.ExploitKnown);
|
||||
|
||||
var affected = Assert.Single(merged.AffectedPackages);
|
||||
Assert.Empty(affected.VersionRanges);
|
||||
Assert.Contains(affected.Statuses, status => status.Status == "known_affected" && status.Provenance.Source == "vendor");
|
||||
|
||||
var mergeProvenance = Assert.Single(merged.Provenance, p => p.Source == "merge");
|
||||
Assert.Equal("precedence", mergeProvenance.Kind);
|
||||
Assert.Equal(expectedTimestamp, mergeProvenance.RecordedAt);
|
||||
Assert.Contains("vendor", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("kev", mergeProvenance.Value, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var inputDocumentIds = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
|
||||
var record = await writer.AppendAsync(merged.AdvisoryKey, nvd, merged, inputDocumentIds, Array.Empty<MergeFieldDecision>(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(expectedTimestamp, record.MergedAt);
|
||||
Assert.Equal(inputDocumentIds, record.InputDocumentIds);
|
||||
Assert.NotEqual(record.BeforeHash, record.AfterHash);
|
||||
|
||||
var records = await store.GetRecentAsync(merged.AdvisoryKey, 5, CancellationToken.None);
|
||||
var persisted = Assert.Single(records);
|
||||
Assert.Equal(record.Id, persisted.Id);
|
||||
Assert.Equal(merged.AdvisoryKey, persisted.AdvisoryKey);
|
||||
Assert.True(persisted.AfterHash.Length > 0);
|
||||
Assert.True(persisted.BeforeHash.Length > 0);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero))
|
||||
{
|
||||
AutoAdvanceAmount = TimeSpan.Zero,
|
||||
};
|
||||
_merger = new AdvisoryPrecedenceMerger(new AffectedPackagePrecedenceResolver(), _timeProvider);
|
||||
_mergeEventStore = new MergeEventStore();
|
||||
_mergeEventWriter = new MergeEventWriter(_mergeEventStore, new CanonicalHashCalculator(), _timeProvider, NullLogger<MergeEventWriter>.Instance);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
private async Task EnsureInitializedAsync()
|
||||
{
|
||||
if (_mergeEventWriter is null)
|
||||
@@ -85,115 +85,115 @@ public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime
|
||||
await InitializeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Task DropMergeCollectionAsync()
|
||||
{
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
// {
|
||||
// await _fixture.Database.DropCollectionAsync(MongoStorageDefaults.Collections.MergeEvent);
|
||||
// }
|
||||
// catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase))
|
||||
// catch (StorageCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// Collection has not been created yet – safe to ignore.
|
||||
}
|
||||
}
|
||||
|
||||
private static Advisory CreateNvdBaseline()
|
||||
{
|
||||
var provenance = new AdvisoryProvenance("nvd", "document", "https://nvd.nist.gov/vuln/detail/CVE-2025-1000", DateTimeOffset.Parse("2025-02-10T00:00:00Z"));
|
||||
return new Advisory(
|
||||
"CVE-2025-1000",
|
||||
"CVE-2025-1000",
|
||||
"Baseline description from NVD.",
|
||||
"en",
|
||||
DateTimeOffset.Parse("2025-02-05T00:00:00Z"),
|
||||
DateTimeOffset.Parse("2025-02-10T12:00:00Z"),
|
||||
"medium",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { "CVE-2025-1000" },
|
||||
references: new[]
|
||||
{
|
||||
new AdvisoryReference("https://nvd.nist.gov/vuln/detail/CVE-2025-1000", "advisory", "nvd", "NVD reference", provenance),
|
||||
},
|
||||
affectedPackages: new[]
|
||||
{
|
||||
new AffectedPackage(
|
||||
AffectedPackageTypes.Cpe,
|
||||
"cpe:2.3:o:vendor:product:1.0:*:*:*:*:*:*:*",
|
||||
"vendor-os",
|
||||
new[]
|
||||
{
|
||||
new AffectedVersionRange(
|
||||
rangeKind: "cpe",
|
||||
introducedVersion: null,
|
||||
fixedVersion: null,
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: "<=1.0",
|
||||
provenance: provenance)
|
||||
},
|
||||
Array.Empty<AffectedPackageStatus>(),
|
||||
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 CreateVendorOverride()
|
||||
{
|
||||
var provenance = new AdvisoryProvenance("vendor", "psirt", "VSA-2025-1000", DateTimeOffset.Parse("2025-02-11T00:00:00Z"));
|
||||
return new Advisory(
|
||||
"CVE-2025-1000",
|
||||
"Vendor Security Advisory",
|
||||
"Critical impact on supported platforms.",
|
||||
"en",
|
||||
DateTimeOffset.Parse("2025-02-06T00:00:00Z"),
|
||||
DateTimeOffset.Parse("2025-02-11T06:00:00Z"),
|
||||
"critical",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { "CVE-2025-1000", "VSA-2025-1000" },
|
||||
references: new[]
|
||||
{
|
||||
new AdvisoryReference("https://vendor.example/advisories/VSA-2025-1000", "advisory", "vendor", "Vendor advisory", provenance),
|
||||
},
|
||||
affectedPackages: new[]
|
||||
{
|
||||
new AffectedPackage(
|
||||
AffectedPackageTypes.Cpe,
|
||||
"cpe:2.3:o:vendor:product:1.0:*:*:*:*:*:*:*",
|
||||
"vendor-os",
|
||||
Array.Empty<AffectedVersionRange>(),
|
||||
new[]
|
||||
{
|
||||
new AffectedPackageStatus("known_affected", provenance)
|
||||
},
|
||||
new[] { provenance })
|
||||
},
|
||||
cvssMetrics: new[]
|
||||
{
|
||||
new CvssMetric("3.1", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", 10.0, "critical", provenance)
|
||||
},
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
|
||||
private static Advisory CreateKevSignal()
|
||||
{
|
||||
var provenance = new AdvisoryProvenance("kev", "catalog", "CVE-2025-1000", DateTimeOffset.Parse("2025-02-12T00:00:00Z"));
|
||||
return new Advisory(
|
||||
"CVE-2025-1000",
|
||||
"Known Exploited Vulnerability",
|
||||
null,
|
||||
null,
|
||||
published: null,
|
||||
modified: null,
|
||||
severity: null,
|
||||
exploitKnown: true,
|
||||
aliases: new[] { "KEV-CVE-2025-1000" },
|
||||
references: Array.Empty<AdvisoryReference>(),
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
}
|
||||
// Collection has not been created yet – safe to ignore.
|
||||
}
|
||||
}
|
||||
|
||||
private static Advisory CreateNvdBaseline()
|
||||
{
|
||||
var provenance = new AdvisoryProvenance("nvd", "document", "https://nvd.nist.gov/vuln/detail/CVE-2025-1000", DateTimeOffset.Parse("2025-02-10T00:00:00Z"));
|
||||
return new Advisory(
|
||||
"CVE-2025-1000",
|
||||
"CVE-2025-1000",
|
||||
"Baseline description from NVD.",
|
||||
"en",
|
||||
DateTimeOffset.Parse("2025-02-05T00:00:00Z"),
|
||||
DateTimeOffset.Parse("2025-02-10T12:00:00Z"),
|
||||
"medium",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { "CVE-2025-1000" },
|
||||
references: new[]
|
||||
{
|
||||
new AdvisoryReference("https://nvd.nist.gov/vuln/detail/CVE-2025-1000", "advisory", "nvd", "NVD reference", provenance),
|
||||
},
|
||||
affectedPackages: new[]
|
||||
{
|
||||
new AffectedPackage(
|
||||
AffectedPackageTypes.Cpe,
|
||||
"cpe:2.3:o:vendor:product:1.0:*:*:*:*:*:*:*",
|
||||
"vendor-os",
|
||||
new[]
|
||||
{
|
||||
new AffectedVersionRange(
|
||||
rangeKind: "cpe",
|
||||
introducedVersion: null,
|
||||
fixedVersion: null,
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: "<=1.0",
|
||||
provenance: provenance)
|
||||
},
|
||||
Array.Empty<AffectedPackageStatus>(),
|
||||
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 CreateVendorOverride()
|
||||
{
|
||||
var provenance = new AdvisoryProvenance("vendor", "psirt", "VSA-2025-1000", DateTimeOffset.Parse("2025-02-11T00:00:00Z"));
|
||||
return new Advisory(
|
||||
"CVE-2025-1000",
|
||||
"Vendor Security Advisory",
|
||||
"Critical impact on supported platforms.",
|
||||
"en",
|
||||
DateTimeOffset.Parse("2025-02-06T00:00:00Z"),
|
||||
DateTimeOffset.Parse("2025-02-11T06:00:00Z"),
|
||||
"critical",
|
||||
exploitKnown: false,
|
||||
aliases: new[] { "CVE-2025-1000", "VSA-2025-1000" },
|
||||
references: new[]
|
||||
{
|
||||
new AdvisoryReference("https://vendor.example/advisories/VSA-2025-1000", "advisory", "vendor", "Vendor advisory", provenance),
|
||||
},
|
||||
affectedPackages: new[]
|
||||
{
|
||||
new AffectedPackage(
|
||||
AffectedPackageTypes.Cpe,
|
||||
"cpe:2.3:o:vendor:product:1.0:*:*:*:*:*:*:*",
|
||||
"vendor-os",
|
||||
Array.Empty<AffectedVersionRange>(),
|
||||
new[]
|
||||
{
|
||||
new AffectedPackageStatus("known_affected", provenance)
|
||||
},
|
||||
new[] { provenance })
|
||||
},
|
||||
cvssMetrics: new[]
|
||||
{
|
||||
new CvssMetric("3.1", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", 10.0, "critical", provenance)
|
||||
},
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
|
||||
private static Advisory CreateKevSignal()
|
||||
{
|
||||
var provenance = new AdvisoryProvenance("kev", "catalog", "CVE-2025-1000", DateTimeOffset.Parse("2025-02-12T00:00:00Z"));
|
||||
return new Advisory(
|
||||
"CVE-2025-1000",
|
||||
"Known Exploited Vulnerability",
|
||||
null,
|
||||
null,
|
||||
published: null,
|
||||
modified: null,
|
||||
severity: null,
|
||||
exploitKnown: true,
|
||||
aliases: new[] { "KEV-CVE-2025-1000" },
|
||||
references: Array.Empty<AdvisoryReference>(),
|
||||
affectedPackages: Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: Array.Empty<CvssMetric>(),
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
internal sealed class MetricCollector : IDisposable
|
||||
{
|
||||
private readonly MeterListener _listener;
|
||||
private readonly List<MetricMeasurement> _measurements = new();
|
||||
|
||||
public MetricCollector(string meterName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(meterName))
|
||||
{
|
||||
throw new ArgumentException("Meter name is required", nameof(meterName));
|
||||
}
|
||||
|
||||
_listener = new MeterListener
|
||||
{
|
||||
InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == meterName)
|
||||
{
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
var tagArray = new KeyValuePair<string, object?>[tags.Length];
|
||||
for (var i = 0; i < tags.Length; i++)
|
||||
{
|
||||
tagArray[i] = tags[i];
|
||||
}
|
||||
|
||||
_measurements.Add(new MetricMeasurement(instrument.Name, measurement, tagArray));
|
||||
});
|
||||
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
public IReadOnlyList<MetricMeasurement> Measurements => _measurements;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_listener.Dispose();
|
||||
}
|
||||
|
||||
internal sealed record MetricMeasurement(
|
||||
string Name,
|
||||
long Value,
|
||||
IReadOnlyList<KeyValuePair<string, object?>> Tags);
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
internal sealed class MetricCollector : IDisposable
|
||||
{
|
||||
private readonly MeterListener _listener;
|
||||
private readonly List<MetricMeasurement> _measurements = new();
|
||||
|
||||
public MetricCollector(string meterName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(meterName))
|
||||
{
|
||||
throw new ArgumentException("Meter name is required", nameof(meterName));
|
||||
}
|
||||
|
||||
_listener = new MeterListener
|
||||
{
|
||||
InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == meterName)
|
||||
{
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
var tagArray = new KeyValuePair<string, object?>[tags.Length];
|
||||
for (var i = 0; i < tags.Length; i++)
|
||||
{
|
||||
tagArray[i] = tags[i];
|
||||
}
|
||||
|
||||
_measurements.Add(new MetricMeasurement(instrument.Name, measurement, tagArray));
|
||||
});
|
||||
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
public IReadOnlyList<MetricMeasurement> Measurements => _measurements;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_listener.Dispose();
|
||||
}
|
||||
|
||||
internal sealed record MetricMeasurement(
|
||||
string Name,
|
||||
long Value,
|
||||
IReadOnlyList<KeyValuePair<string, object?>> Tags);
|
||||
}
|
||||
|
||||
@@ -1,108 +1,108 @@
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class NevraComparerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("kernel-1:4.18.0-348.7.1.el8_5.x86_64", "kernel", 1, "4.18.0", "348.7.1.el8_5", "x86_64")]
|
||||
[InlineData("bash-5.1.8-2.fc35.x86_64", "bash", 0, "5.1.8", "2.fc35", "x86_64")]
|
||||
[InlineData("openssl-libs-1:1.1.1k-7.el8", "openssl-libs", 1, "1.1.1k", "7.el8", null)]
|
||||
[InlineData("java-11-openjdk-1:11.0.23.0.9-2.el9_4.ppc64le", "java-11-openjdk", 1, "11.0.23.0.9", "2.el9_4", "ppc64le")]
|
||||
[InlineData("bash-0:5.2.15-3.el9_4.arm64", "bash", 0, "5.2.15", "3.el9_4", "arm64")]
|
||||
[InlineData("podman-3:4.9.3-1.el9.x86_64", "podman", 3, "4.9.3", "1.el9", "x86_64")]
|
||||
public void TryParse_ReturnsExpectedComponents(string input, string expectedName, int expectedEpoch, string expectedVersion, string expectedRelease, string? expectedArch)
|
||||
{
|
||||
var success = Nevra.TryParse(input, out var nevra);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.NotNull(nevra);
|
||||
Assert.Equal(expectedName, nevra!.Name);
|
||||
Assert.Equal(expectedEpoch, nevra.Epoch);
|
||||
Assert.Equal(expectedVersion, nevra.Version);
|
||||
Assert.Equal(expectedRelease, nevra.Release);
|
||||
Assert.Equal(expectedArch, nevra.Architecture);
|
||||
Assert.Equal(input, nevra.Original);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("kernel4.18.0-80.el8")]
|
||||
[InlineData("kernel-4.18.0")]
|
||||
public void TryParse_InvalidInputs_ReturnFalse(string input)
|
||||
{
|
||||
var success = Nevra.TryParse(input, out var nevra);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Null(nevra);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_TrimsWhitespace()
|
||||
{
|
||||
var success = Nevra.TryParse(" kernel-0:4.18.0-80.el8.x86_64 ", out var nevra);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.NotNull(nevra);
|
||||
Assert.Equal("kernel", nevra!.Name);
|
||||
Assert.Equal("4.18.0", nevra.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_PrefersHigherEpoch()
|
||||
{
|
||||
var older = "kernel-0:4.18.0-348.7.1.el8_5.x86_64";
|
||||
var newer = "kernel-1:4.18.0-348.7.1.el8_5.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(newer, older) > 0);
|
||||
Assert.True(NevraComparer.Instance.Compare(older, newer) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UsesRpmVersionOrdering()
|
||||
{
|
||||
var lower = "kernel-0:4.18.0-80.el8.x86_64";
|
||||
var higher = "kernel-0:4.18.11-80.el8.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(higher, lower) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UsesReleaseOrdering()
|
||||
{
|
||||
var el8 = "bash-0:5.1.0-1.el8.x86_64";
|
||||
var el9 = "bash-0:5.1.0-1.el9.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(el9, el8) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_TildeRanksEarlier()
|
||||
{
|
||||
var prerelease = "bash-0:5.1.0~beta-1.fc34.x86_64";
|
||||
var stable = "bash-0:5.1.0-1.fc34.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(prerelease, stable) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_ConsidersArchitecture()
|
||||
{
|
||||
var noarch = "pkg-0:1.0-1.noarch";
|
||||
var arch = "pkg-0:1.0-1.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(noarch, arch) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_FallsBackToOrdinalForInvalid()
|
||||
{
|
||||
var left = "not-a-nevra";
|
||||
var right = "also-not";
|
||||
|
||||
var expected = Math.Sign(string.CompareOrdinal(left, right));
|
||||
var actual = Math.Sign(NevraComparer.Instance.Compare(left, right));
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class NevraComparerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("kernel-1:4.18.0-348.7.1.el8_5.x86_64", "kernel", 1, "4.18.0", "348.7.1.el8_5", "x86_64")]
|
||||
[InlineData("bash-5.1.8-2.fc35.x86_64", "bash", 0, "5.1.8", "2.fc35", "x86_64")]
|
||||
[InlineData("openssl-libs-1:1.1.1k-7.el8", "openssl-libs", 1, "1.1.1k", "7.el8", null)]
|
||||
[InlineData("java-11-openjdk-1:11.0.23.0.9-2.el9_4.ppc64le", "java-11-openjdk", 1, "11.0.23.0.9", "2.el9_4", "ppc64le")]
|
||||
[InlineData("bash-0:5.2.15-3.el9_4.arm64", "bash", 0, "5.2.15", "3.el9_4", "arm64")]
|
||||
[InlineData("podman-3:4.9.3-1.el9.x86_64", "podman", 3, "4.9.3", "1.el9", "x86_64")]
|
||||
public void TryParse_ReturnsExpectedComponents(string input, string expectedName, int expectedEpoch, string expectedVersion, string expectedRelease, string? expectedArch)
|
||||
{
|
||||
var success = Nevra.TryParse(input, out var nevra);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.NotNull(nevra);
|
||||
Assert.Equal(expectedName, nevra!.Name);
|
||||
Assert.Equal(expectedEpoch, nevra.Epoch);
|
||||
Assert.Equal(expectedVersion, nevra.Version);
|
||||
Assert.Equal(expectedRelease, nevra.Release);
|
||||
Assert.Equal(expectedArch, nevra.Architecture);
|
||||
Assert.Equal(input, nevra.Original);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("kernel4.18.0-80.el8")]
|
||||
[InlineData("kernel-4.18.0")]
|
||||
public void TryParse_InvalidInputs_ReturnFalse(string input)
|
||||
{
|
||||
var success = Nevra.TryParse(input, out var nevra);
|
||||
|
||||
Assert.False(success);
|
||||
Assert.Null(nevra);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_TrimsWhitespace()
|
||||
{
|
||||
var success = Nevra.TryParse(" kernel-0:4.18.0-80.el8.x86_64 ", out var nevra);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.NotNull(nevra);
|
||||
Assert.Equal("kernel", nevra!.Name);
|
||||
Assert.Equal("4.18.0", nevra.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_PrefersHigherEpoch()
|
||||
{
|
||||
var older = "kernel-0:4.18.0-348.7.1.el8_5.x86_64";
|
||||
var newer = "kernel-1:4.18.0-348.7.1.el8_5.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(newer, older) > 0);
|
||||
Assert.True(NevraComparer.Instance.Compare(older, newer) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UsesRpmVersionOrdering()
|
||||
{
|
||||
var lower = "kernel-0:4.18.0-80.el8.x86_64";
|
||||
var higher = "kernel-0:4.18.11-80.el8.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(higher, lower) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UsesReleaseOrdering()
|
||||
{
|
||||
var el8 = "bash-0:5.1.0-1.el8.x86_64";
|
||||
var el9 = "bash-0:5.1.0-1.el9.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(el9, el8) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_TildeRanksEarlier()
|
||||
{
|
||||
var prerelease = "bash-0:5.1.0~beta-1.fc34.x86_64";
|
||||
var stable = "bash-0:5.1.0-1.fc34.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(prerelease, stable) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_ConsidersArchitecture()
|
||||
{
|
||||
var noarch = "pkg-0:1.0-1.noarch";
|
||||
var arch = "pkg-0:1.0-1.x86_64";
|
||||
|
||||
Assert.True(NevraComparer.Instance.Compare(noarch, arch) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_FallsBackToOrdinalForInvalid()
|
||||
{
|
||||
var left = "not-a-nevra";
|
||||
var right = "also-not";
|
||||
|
||||
var expected = Math.Sign(string.CompareOrdinal(left, right));
|
||||
var actual = Math.Sign(NevraComparer.Instance.Compare(left, right));
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class SemanticVersionRangeResolverTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("1.2.3", true)]
|
||||
[InlineData("1.2.3-beta.1", true)]
|
||||
[InlineData("invalid", false)]
|
||||
[InlineData(null, false)]
|
||||
public void TryParse_ReturnsExpected(string? input, bool expected)
|
||||
{
|
||||
var success = SemanticVersionRangeResolver.TryParse(input, out var version);
|
||||
|
||||
Assert.Equal(expected, success);
|
||||
Assert.Equal(expected, version is not null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_ParsesSemanticVersions()
|
||||
{
|
||||
Assert.True(SemanticVersionRangeResolver.Compare("1.2.3", "1.2.2") > 0);
|
||||
Assert.True(SemanticVersionRangeResolver.Compare("1.2.3-beta", "1.2.3") < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UsesOrdinalFallbackForInvalid()
|
||||
{
|
||||
var left = "zzz";
|
||||
var right = "aaa";
|
||||
var expected = Math.Sign(string.CompareOrdinal(left, right));
|
||||
var actual = Math.Sign(SemanticVersionRangeResolver.Compare(left, right));
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveWindows_WithFixedVersion_ComputesExclusiveUpper()
|
||||
{
|
||||
var (introduced, exclusive, inclusive) = SemanticVersionRangeResolver.ResolveWindows("1.0.0", "1.2.0", null);
|
||||
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.0.0"), introduced);
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.2.0"), exclusive);
|
||||
Assert.Null(inclusive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveWindows_WithLastAffectedOnly_ComputesInclusiveAndExclusive()
|
||||
{
|
||||
var (introduced, exclusive, inclusive) = SemanticVersionRangeResolver.ResolveWindows("1.0.0", null, "1.1.5");
|
||||
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.0.0"), introduced);
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.1.6"), exclusive);
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.1.5"), inclusive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveWindows_WithNeither_ReturnsNullBounds()
|
||||
{
|
||||
var (introduced, exclusive, inclusive) = SemanticVersionRangeResolver.ResolveWindows(null, null, null);
|
||||
|
||||
Assert.Null(introduced);
|
||||
Assert.Null(exclusive);
|
||||
Assert.Null(inclusive);
|
||||
}
|
||||
}
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class SemanticVersionRangeResolverTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("1.2.3", true)]
|
||||
[InlineData("1.2.3-beta.1", true)]
|
||||
[InlineData("invalid", false)]
|
||||
[InlineData(null, false)]
|
||||
public void TryParse_ReturnsExpected(string? input, bool expected)
|
||||
{
|
||||
var success = SemanticVersionRangeResolver.TryParse(input, out var version);
|
||||
|
||||
Assert.Equal(expected, success);
|
||||
Assert.Equal(expected, version is not null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_ParsesSemanticVersions()
|
||||
{
|
||||
Assert.True(SemanticVersionRangeResolver.Compare("1.2.3", "1.2.2") > 0);
|
||||
Assert.True(SemanticVersionRangeResolver.Compare("1.2.3-beta", "1.2.3") < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_UsesOrdinalFallbackForInvalid()
|
||||
{
|
||||
var left = "zzz";
|
||||
var right = "aaa";
|
||||
var expected = Math.Sign(string.CompareOrdinal(left, right));
|
||||
var actual = Math.Sign(SemanticVersionRangeResolver.Compare(left, right));
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveWindows_WithFixedVersion_ComputesExclusiveUpper()
|
||||
{
|
||||
var (introduced, exclusive, inclusive) = SemanticVersionRangeResolver.ResolveWindows("1.0.0", "1.2.0", null);
|
||||
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.0.0"), introduced);
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.2.0"), exclusive);
|
||||
Assert.Null(inclusive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveWindows_WithLastAffectedOnly_ComputesInclusiveAndExclusive()
|
||||
{
|
||||
var (introduced, exclusive, inclusive) = SemanticVersionRangeResolver.ResolveWindows("1.0.0", null, "1.1.5");
|
||||
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.0.0"), introduced);
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.1.6"), exclusive);
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.1.5"), inclusive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveWindows_WithNeither_ReturnsNullBounds()
|
||||
{
|
||||
var (introduced, exclusive, inclusive) = SemanticVersionRangeResolver.ResolveWindows(null, null, null);
|
||||
|
||||
Assert.Null(introduced);
|
||||
Assert.Null(exclusive);
|
||||
Assert.Null(inclusive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
internal sealed class TestLogger<T> : ILogger<T>
|
||||
{
|
||||
private static readonly IDisposable NoopScope = new DisposableScope();
|
||||
|
||||
public List<LogEntry> Entries { get; } = new();
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
where TState : notnull
|
||||
=> NoopScope;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (formatter is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatter));
|
||||
}
|
||||
|
||||
IReadOnlyList<KeyValuePair<string, object?>>? structuredState = null;
|
||||
if (state is IReadOnlyList<KeyValuePair<string, object?>> list)
|
||||
{
|
||||
structuredState = list.ToArray();
|
||||
}
|
||||
else if (state is IEnumerable<KeyValuePair<string, object?>> enumerable)
|
||||
{
|
||||
structuredState = enumerable.ToArray();
|
||||
}
|
||||
|
||||
Entries.Add(new LogEntry(logLevel, eventId, formatter(state, exception), structuredState));
|
||||
}
|
||||
|
||||
internal sealed record LogEntry(
|
||||
LogLevel Level,
|
||||
EventId EventId,
|
||||
string Message,
|
||||
IReadOnlyList<KeyValuePair<string, object?>>? StructuredState);
|
||||
|
||||
private sealed class DisposableScope : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
internal sealed class TestLogger<T> : ILogger<T>
|
||||
{
|
||||
private static readonly IDisposable NoopScope = new DisposableScope();
|
||||
|
||||
public List<LogEntry> Entries { get; } = new();
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
where TState : notnull
|
||||
=> NoopScope;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (formatter is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatter));
|
||||
}
|
||||
|
||||
IReadOnlyList<KeyValuePair<string, object?>>? structuredState = null;
|
||||
if (state is IReadOnlyList<KeyValuePair<string, object?>> list)
|
||||
{
|
||||
structuredState = list.ToArray();
|
||||
}
|
||||
else if (state is IEnumerable<KeyValuePair<string, object?>> enumerable)
|
||||
{
|
||||
structuredState = enumerable.ToArray();
|
||||
}
|
||||
|
||||
Entries.Add(new LogEntry(logLevel, eventId, formatter(state, exception), structuredState));
|
||||
}
|
||||
|
||||
internal sealed record LogEntry(
|
||||
LogLevel Level,
|
||||
EventId EventId,
|
||||
string Message,
|
||||
IReadOnlyList<KeyValuePair<string, object?>>? StructuredState);
|
||||
|
||||
private sealed class DisposableScope : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user