tests fixes and sprints work

This commit is contained in:
master
2026-01-22 19:08:46 +02:00
parent c32fff8f86
commit 726d70dc7f
881 changed files with 134434 additions and 6228 deletions

View File

@@ -84,7 +84,18 @@
"versionRanges": [],
"normalizedVersions": [],
"statuses": [],
"provenance": []
"provenance": [
{
"source": "acsc",
"kind": "affected",
"value": "ExampleCo Router X",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages"
]
}
]
},
{
"type": "vendor",
@@ -93,7 +104,18 @@
"versionRanges": [],
"normalizedVersions": [],
"statuses": [],
"provenance": []
"provenance": [
{
"source": "acsc",
"kind": "affected",
"value": "ExampleCo Router Y",
"decisionReason": null,
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": [
"affectedpackages"
]
}
]
}
],
"aliases": [
@@ -152,11 +174,11 @@
{
"kind": "advisory",
"provenance": {
"source": "unknown",
"kind": "unspecified",
"value": null,
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/multi/rss",
"decisionReason": null,
"recordedAt": "1970-01-01T00:00:00+00:00",
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": "multi",
@@ -166,11 +188,11 @@
{
"kind": "reference",
"provenance": {
"source": "unknown",
"kind": "unspecified",
"value": null,
"source": "acsc",
"kind": "document",
"value": "https://origin.example/feeds/multi/rss",
"decisionReason": null,
"recordedAt": "1970-01-01T00:00:00+00:00",
"recordedAt": "2025-10-12T00:00:00+00:00",
"fieldMask": []
},
"sourceTag": null,

View File

@@ -0,0 +1,326 @@
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.Persistence.Postgres.Repositories;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Determinism;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.Persistence.Tests;
[Collection(ConcelierPostgresCollection.Name)]
public sealed class SbomRepositoryTests : IAsyncLifetime
{
private static readonly DateTimeOffset FixedNow =
new(2026, 1, 20, 12, 0, 0, TimeSpan.Zero);
private readonly ConcelierPostgresFixture _fixture;
private readonly ConcelierDataSource _dataSource;
private readonly SbomRepository _repository;
public SbomRepositoryTests(ConcelierPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
_dataSource = new ConcelierDataSource(
Options.Create(options),
NullLogger<ConcelierDataSource>.Instance);
_repository = new SbomRepository(
_dataSource,
NullLogger<SbomRepository>.Instance,
new FixedTimeProvider(FixedNow),
new SequentialGuidProvider());
}
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task StoreAsync_RoundTripsBySerialAndDigest()
{
var sbom = CreateSbom(serialNumber: "urn:sha256:deadbeef");
await _repository.StoreAsync(sbom);
var bySerial = await _repository.GetBySerialNumberAsync(sbom.SerialNumber);
var byDigest = await _repository.GetByArtifactDigestAsync("sha256:deadbeef");
bySerial.Should().NotBeNull();
bySerial!.SpecVersion.Should().Be("1.7");
bySerial.Metadata.RootComponentRef.Should().Be("root");
byDigest.Should().NotBeNull();
byDigest!.SerialNumber.Should().Be(sbom.SerialNumber);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Queries_ReturnServicesCryptoAndVulnerabilities()
{
var sbom = CreateSbom(serialNumber: "urn:sha256:feedface");
await _repository.StoreAsync(sbom);
var services = await _repository.GetServicesForArtifactAsync(sbom.SerialNumber);
var cryptoComponents = await _repository.GetComponentsWithCryptoAsync("sha256:feedface");
var vulnerabilities = await _repository.GetEmbeddedVulnerabilitiesAsync("sha256:feedface");
services.Should().ContainSingle(service => service.Name == "api-gateway");
var cryptoComponent = cryptoComponents.Should()
.ContainSingle(component => component.BomRef == "root")
.Which;
cryptoComponent.ModelCard.Should().NotBeNull();
cryptoComponent.ModelCard!.ModelParameters!.Task.Should().Be("classification");
vulnerabilities.Should().ContainSingle(vuln => vuln.Id == "CVE-2026-0001");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetByArtifactDigestAsync_ReturnsCompositionsAndDeclarations()
{
var sbom = CreateSbom(serialNumber: "urn:sha256:cccccccc");
await _repository.StoreAsync(sbom);
var byDigest = await _repository.GetByArtifactDigestAsync("sha256:cccccccc");
byDigest.Should().NotBeNull();
byDigest!.Compositions.Should().ContainSingle(composition =>
composition.Aggregate == CompositionAggregate.Complete);
byDigest.Declarations.Should().NotBeNull();
byDigest.Declarations!.Attestations.Should().ContainSingle(attestation =>
attestation.Predicate == "build");
byDigest.Declarations!.Affirmations.Should().ContainSingle(affirmation =>
affirmation.Statement == "SBOM verified");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LicenseQueries_ReturnExpectedResults()
{
var sbomAlpha = CreateLicensedSbom(
"urn:sha256:aaaaaaaa",
"root-alpha",
new ParsedComponent
{
BomRef = "root-alpha",
Name = "alpha-app",
Licenses =
[
new ParsedLicense
{
SpdxId = "MIT"
}
]
},
new ParsedComponent
{
BomRef = "lib-gpl",
Name = "lib-gpl",
Licenses =
[
new ParsedLicense
{
Expression = new WithException(
new SimpleLicense("GPL-2.0-only"),
"Classpath-exception-2.0")
}
]
},
new ParsedComponent
{
BomRef = "lib-empty",
Name = "lib-empty"
});
var sbomBeta = CreateLicensedSbom(
"urn:sha256:bbbbbbbb",
"root-beta",
new ParsedComponent
{
BomRef = "root-beta",
Name = "beta-app",
Licenses =
[
new ParsedLicense
{
SpdxId = "Apache-2.0"
}
]
},
new ParsedComponent
{
BomRef = "lib-prop",
Name = "lib-prop",
Licenses =
[
new ParsedLicense
{
SpdxId = "LicenseRef-Proprietary"
}
]
});
await _repository.StoreAsync(sbomAlpha);
await _repository.StoreAsync(sbomBeta);
var licenses = await _repository.GetLicensesForArtifactAsync("sha256:aaaaaaaa");
licenses.Should().Contain(license => license.SpdxId == "MIT");
licenses.Should().Contain(license => license.Expression is WithException);
var mitComponents = await _repository.GetComponentsByLicenseAsync("MIT");
mitComponents.Should().ContainSingle(component => component.BomRef == "root-alpha");
var gplComponents = await _repository.GetComponentsByLicenseAsync("GPL-2.0-only");
gplComponents.Should().ContainSingle(component => component.BomRef == "lib-gpl");
var noLicense = await _repository.GetComponentsWithoutLicenseAsync("sha256:aaaaaaaa");
noLicense.Should().ContainSingle(component => component.BomRef == "lib-empty");
var permissive = await _repository.GetComponentsByLicenseCategoryAsync(
"sha256:aaaaaaaa",
LicenseCategory.Permissive);
permissive.Should().ContainSingle(component => component.BomRef == "root-alpha");
var weakCopyleft = await _repository.GetComponentsByLicenseCategoryAsync(
"sha256:aaaaaaaa",
LicenseCategory.WeakCopyleft);
weakCopyleft.Should().ContainSingle(component => component.BomRef == "lib-gpl");
var proprietary = await _repository.GetComponentsByLicenseCategoryAsync(
"sha256:bbbbbbbb",
LicenseCategory.Proprietary);
proprietary.Should().ContainSingle(component => component.BomRef == "lib-prop");
var summary = await _repository.GetLicenseInventoryAsync("sha256:aaaaaaaa");
summary.TotalComponents.Should().Be(3);
summary.ComponentsWithLicense.Should().Be(2);
summary.ComponentsWithoutLicense.Should().Be(1);
summary.LicenseDistribution.Should().ContainKey("MIT");
summary.LicenseDistribution.Should().ContainKey("GPL-2.0-only");
summary.Expressions.Should().Contain("GPL-2.0-only WITH Classpath-exception-2.0");
}
private static ParsedSbom CreateSbom(string serialNumber)
{
return new ParsedSbom
{
Format = "cyclonedx",
SpecVersion = "1.7",
SerialNumber = serialNumber,
Components =
[
new ParsedComponent
{
BomRef = "root",
Name = "acme-app",
CryptoProperties = new ParsedCryptoProperties
{
AssetType = CryptoAssetType.Algorithm
},
ModelCard = new ParsedModelCard
{
ModelParameters = new ParsedModelParameters
{
Task = "classification"
}
}
},
new ParsedComponent
{
BomRef = "lib-1",
Name = "lib-one"
}
],
Services =
[
new ParsedService
{
BomRef = "svc-api",
Name = "api-gateway",
Endpoints = ["https://api.example.test"]
}
],
Vulnerabilities =
[
new ParsedVulnerability
{
Id = "CVE-2026-0001"
}
],
Compositions =
[
new ParsedComposition
{
Aggregate = CompositionAggregate.Complete,
Assemblies = ["root"],
Dependencies = ["lib-1"],
Vulnerabilities = ["CVE-2026-0001"]
}
],
Declarations = new ParsedDeclarations
{
Attestations =
[
new ParsedAttestation
{
Subjects = ["root"],
Predicate = "build",
Evidence = "evidence-ref"
}
],
Affirmations =
[
new ParsedAffirmation
{
Statement = "SBOM verified",
Signatories = ["acme"]
}
]
},
Metadata = new ParsedSbomMetadata
{
Name = "acme-app",
RootComponentRef = "root"
}
};
}
private static ParsedSbom CreateLicensedSbom(
string serialNumber,
string rootComponentRef,
params ParsedComponent[] components)
{
return new ParsedSbom
{
Format = "cyclonedx",
SpecVersion = "1.7",
SerialNumber = serialNumber,
Components = components.ToImmutableArray(),
Metadata = new ParsedSbomMetadata
{
Name = "licensed-app",
RootComponentRef = rootComponentRef
}
};
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _now;
public FixedTimeProvider(DateTimeOffset now)
{
_now = now;
}
public override DateTimeOffset GetUtcNow() => _now;
}
}

View File

@@ -8,3 +8,6 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0231-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0231-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0231-A | DONE | Waived (test project; revalidated 2026-01-07). |
| TASK-015-011 | DONE | Added SbomRepository integration coverage. |
| TASK-015-007d | DONE | Added license query coverage for SbomRepository. |
| TASK-015-013 | DONE | Added SbomRepository integration coverage for model cards and policy fields. |

View File

@@ -0,0 +1,117 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.Concelier.Core.Canonical;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Concelier.SbomIntegration.Vex;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.SbomIntegration.Tests;
public sealed class SbomAdvisoryMatcherVexTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task MatchAsync_FiltersNotAffectedVexStatements()
{
var sbomId = Guid.NewGuid();
var canonicalId = Guid.NewGuid();
var purl = "pkg:npm/example@1.0.0";
var advisory = CreateCanonicalAdvisory(canonicalId, "CVE-2026-5000", purl);
var canonicalService = new Mock<ICanonicalAdvisoryService>();
canonicalService
.Setup(service => service.GetByArtifactAsync(purl, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<CanonicalAdvisory> { advisory });
var sbomRepository = new Mock<ISbomRepository>();
var parsedSbom = CreateSbom();
sbomRepository
.Setup(repo => repo.GetByArtifactDigestAsync("sha256:abc", It.IsAny<CancellationToken>()))
.ReturnsAsync(parsedSbom);
var vexConsumer = new Mock<IVexConsumer>();
var vexResult = new VexConsumptionResult
{
Statements =
[
new ConsumedVexStatement
{
VulnerabilityId = "CVE-2026-5000",
Status = VexStatus.NotAffected,
Source = VexSource.SbomEmbedded,
TrustLevel = VexTrustLevel.Trusted
}
]
};
vexConsumer
.Setup(consumer => consumer.ConsumeAsync(parsedSbom.Vulnerabilities, It.IsAny<VexConsumptionPolicy>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(vexResult);
var policyLoader = new Mock<IVexConsumptionPolicyLoader>();
policyLoader
.Setup(loader => loader.LoadAsync(It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(VexConsumptionPolicyDefaults.Default);
var options = Options.Create(new VexConsumptionOptions { Enabled = true });
var logger = new Mock<ILogger<SbomAdvisoryMatcher>>();
var matcher = new SbomAdvisoryMatcher(
canonicalService.Object,
logger.Object,
timeProvider: null,
vexConsumer.Object,
sbomRepository.Object,
policyLoader.Object,
options);
var matches = await matcher.MatchAsync(sbomId, "sha256:abc", new[] { purl }, null, null);
Assert.Empty(matches);
}
private static ParsedSbom CreateSbom()
{
return new ParsedSbom
{
Format = "CycloneDX",
SpecVersion = "1.7",
SerialNumber = "urn:uuid:sbom",
Vulnerabilities =
[
new ParsedVulnerability
{
Id = "CVE-2026-5000",
Analysis = new ParsedVulnAnalysis
{
State = VexState.NotAffected,
Justification = VexJustification.ComponentNotPresent,
FirstIssued = DateTimeOffset.Parse("2026-01-20T00:00:00Z"),
LastUpdated = DateTimeOffset.Parse("2026-01-20T00:00:00Z")
}
}
],
Components = [],
Dependencies = [],
Services = [],
Compositions = [],
Annotations = [],
Metadata = new ParsedSbomMetadata()
};
}
private static CanonicalAdvisory CreateCanonicalAdvisory(Guid id, string cve, string affectsKey)
{
return new CanonicalAdvisory
{
Id = id,
Cve = cve,
AffectsKey = affectsKey,
MergeHash = "hash",
CreatedAt = DateTimeOffset.Parse("2026-01-20T00:00:00Z"),
UpdatedAt = DateTimeOffset.Parse("2026-01-20T00:00:00Z")
};
}
}

View File

@@ -223,6 +223,51 @@ public class SbomParserTests
result.Purls.Should().Contain("pkg:npm/axios@1.6.0");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ParseAsync_CycloneDX_ExtractsExternalReferenceCpesAndUnresolved()
{
var content = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"metadata": {
"component": {
"name": "root",
"version": "1.0.0",
"purl": "pkg:npm/root@1.0.0"
}
},
"components": [
{
"name": "with-cpe",
"version": "1.2.3",
"purl": "pkg:npm/with-cpe@1.2.3",
"externalReferences": [
{
"type": "cpe",
"url": "cpe:2.3:a:vendor:product:1.2.3:*:*:*:*:*:*:*"
}
]
},
{
"name": "no-purl",
"version": "2.0.0"
}
],
"dependencies": [],
"compositions": []
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
result.Cpes.Should().Contain("cpe:2.3:a:vendor:product:1.2.3:*:*:*:*:*:*:*");
result.UnresolvedComponents.Should().ContainSingle(c => c.Name == "no-purl");
}
#endregion
#region SPDX Tests
@@ -318,6 +363,102 @@ public class SbomParserTests
result.Cpes.Should().Contain("cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ParseAsync_SPDX_TracksUnresolvedPackages()
{
var spdxContent = """
{
"spdxVersion": "SPDX-2.3",
"packages": [
{
"SPDXID": "SPDXRef-Package",
"name": "nopurl",
"versionInfo": "1.0.0",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl"
}
]
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(spdxContent));
var result = await _parser.ParseAsync(stream, SbomFormat.SPDX);
result.Purls.Should().BeEmpty();
result.UnresolvedComponents.Should().ContainSingle(c => c.Name == "nopurl");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ParseAsync_SPDX3_ExtractsPurlsAndCpes()
{
var content = """
{
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
"@graph": [
{
"@type": "SpdxDocument",
"spdxId": "urn:doc"
},
{
"@type": "software_Package",
"spdxId": "spdx:pkg:1",
"name": "pkg1",
"packageUrl": "pkg:npm/pkg1@1.0.0",
"packageVersion": "1.0.0",
"externalIdentifier": [
{
"externalIdentifierType": "cpe23Type",
"identifier": "cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*"
}
]
},
{
"@type": "software_Package",
"spdxId": "spdx:pkg:2",
"name": "pkg2",
"softwareVersion": "2.0.0",
"externalIdentifier": [
{
"externalIdentifierType": "purl",
"identifier": "pkg:maven/org.example/app@2.0.0"
}
]
},
{
"@type": "software_Package",
"spdxId": "spdx:pkg:3",
"name": "pkg3"
},
{
"@type": "Profile",
"spdxId": "spdx:profile:1"
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
var result = await _parser.ParseAsync(stream, SbomFormat.SPDX);
result.PrimaryName.Should().Be("pkg1");
result.PrimaryVersion.Should().Be("1.0.0");
result.Purls.Should().Contain(new[]
{
"pkg:npm/pkg1@1.0.0",
"pkg:maven/org.example/app@2.0.0"
});
result.Cpes.Should().Contain("cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*");
result.UnresolvedComponents.Should().ContainSingle(c => c.Name == "pkg3");
}
#endregion
#region Format Detection Tests
@@ -375,7 +516,27 @@ public class SbomParserTests
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DetectFormatAsync_UnknownFormat_ReturnsNotDetected()
public async Task DetectFormatAsync_SPDX3_DetectsFormat()
{
var content = """
{
"@context": ["https://spdx.org/rdf/3.0.1/spdx-context.jsonld"],
"@graph": []
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
var result = await _parser.DetectFormatAsync(stream);
result.IsDetected.Should().BeTrue();
result.Format.Should().Be(SbomFormat.SPDX);
result.SpecVersion.Should().Be("3.0");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DetectFormatAsync_UnknownFormat_ReturnsNotDetected()
{
// Arrange
var content = """

View File

@@ -0,0 +1,81 @@
// -----------------------------------------------------------------------------
// SpdxLicenseExpressionValidatorTests.cs
// Sprint: SPRINT_20260119_015_Concelier_sbom_full_extraction
// Task: TASK-015-007c - SPDX license expression validation tests
// -----------------------------------------------------------------------------
using FluentAssertions;
using StellaOps.Concelier.SbomIntegration.Licensing;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.SbomIntegration.Tests;
public sealed class SpdxLicenseExpressionValidatorTests
{
private readonly SpdxLicenseExpressionValidator _validator = new();
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ValidateString_KnownExpression_IsValid()
{
var result = _validator.ValidateString("MIT AND Apache-2.0");
result.IsValid.Should().BeTrue();
result.Errors.Should().BeEmpty();
result.ReferencedLicenses.Should().Contain(new[] { "MIT", "Apache-2.0" });
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ValidateString_UnknownLicense_IsInvalid()
{
var result = _validator.ValidateString("NoSuchLicense");
result.IsValid.Should().BeFalse();
result.UnknownLicenses.Should().ContainSingle("NoSuchLicense");
result.Errors.Should().Contain(error => error.Contains("Unknown SPDX license identifier"));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ValidateString_LicenseRef_IsWarningOnly()
{
var result = _validator.ValidateString("LicenseRef-Internal");
result.IsValid.Should().BeTrue();
result.UnknownLicenses.Should().ContainSingle("LicenseRef-Internal");
result.Warnings.Should().Contain(warning => warning.Contains("LicenseRef identifier"));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ValidateString_DeprecatedLicense_IsWarningOnly()
{
var result = _validator.ValidateString("AGPL-1.0");
result.IsValid.Should().BeTrue();
result.DeprecatedLicenses.Should().ContainSingle("AGPL-1.0");
result.Warnings.Should().Contain(warning => warning.Contains("Deprecated SPDX license identifier"));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ValidateString_KnownException_IsValid()
{
var result = _validator.ValidateString("MIT WITH Classpath-exception-2.0");
result.IsValid.Should().BeTrue();
result.Errors.Should().BeEmpty();
result.ReferencedExceptions.Should().ContainSingle("Classpath-exception-2.0");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ValidateString_UnknownException_IsInvalid()
{
var result = _validator.ValidateString("MIT WITH Missing-exception");
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(error => error.Contains("Unknown SPDX license exception"));
}
}

View File

@@ -14,6 +14,10 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -23,4 +27,4 @@
<ProjectReference Include="..\..\..\Router/__Libraries/StellaOps.Messaging\StellaOps.Messaging.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -2,9 +2,13 @@
This board mirrors active sprint tasks for this module.
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
Additional source of truth: `docs/implplan/SPRINT_20260119_015_Concelier_sbom_full_extraction.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0238-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0238-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0238-A | DONE | Waived (test project; revalidated 2026-01-07). |
| TASK-015-012 | DONE | Added ParsedSbomParser branch coverage across CycloneDX/SPDX helpers (services, dataflows, licenses, crypto, VEX, AI profiles, references) plus round-trip JSON equivalence checks; line-rate 0.9586 and tests pass. |
| TASK-020-011 | DONE | Added unit tests for VEX consumption, merge, and reporter behaviors. |
| TASK-020-012 | DONE | Added integration test for CycloneDX embedded VEX parsing. |

View File

@@ -0,0 +1,42 @@
using StellaOps.Concelier.SbomIntegration.Vex;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.SbomIntegration.Tests;
public sealed class VexConflictResolverTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Resolve_UsesHighestTrust()
{
var resolver = new VexConflictResolver();
var statements = new[]
{
new VexStatement
{
VulnerabilityId = "CVE-2026-2000",
Status = VexStatus.Affected,
Source = VexSource.External,
TrustLevel = VexTrustLevel.Unverified,
Timestamp = DateTimeOffset.Parse("2026-01-20T00:00:00Z")
},
new VexStatement
{
VulnerabilityId = "CVE-2026-2000",
Status = VexStatus.Fixed,
Source = VexSource.External,
TrustLevel = VexTrustLevel.Verified,
Timestamp = DateTimeOffset.Parse("2026-01-19T00:00:00Z")
}
};
var resolution = resolver.Resolve(
"CVE-2026-2000",
statements,
VexConflictResolutionStrategy.HighestTrust);
Assert.NotNull(resolution.Selected);
Assert.Equal(VexTrustLevel.Verified, resolution.Selected!.TrustLevel);
}
}

View File

@@ -0,0 +1,91 @@
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Concelier.SbomIntegration.Vex;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.SbomIntegration.Tests;
public sealed class VexConsumerTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ConsumeAsync_ReturnsNotAffectedStatement()
{
var vulnerability = CreateVulnerability(
VexState.NotAffected,
VexJustification.ComponentNotPresent,
DateTimeOffset.Parse("2026-01-20T00:00:00Z"));
var consumer = CreateConsumer();
var result = await consumer.ConsumeAsync(
new[] { vulnerability },
VexConsumptionPolicyDefaults.Default,
CancellationToken.None);
Assert.Single(result.Statements);
Assert.Equal(VexStatus.NotAffected, result.Statements[0].Status);
Assert.Equal(VexTrustLevel.Trusted, result.Statements[0].TrustLevel);
Assert.Empty(result.Warnings);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ConsumeAsync_MissingJustification_FiltersStatement()
{
var vulnerability = CreateVulnerability(
VexState.NotAffected,
null,
DateTimeOffset.Parse("2026-01-20T00:00:00Z"));
var consumer = CreateConsumer();
var result = await consumer.ConsumeAsync(
new[] { vulnerability },
VexConsumptionPolicyDefaults.Default,
CancellationToken.None);
Assert.Empty(result.Statements);
Assert.Contains(result.Warnings, warning => warning.Code == "vex.justification.missing");
}
private static ParsedVulnerability CreateVulnerability(
VexState state,
VexJustification? justification,
DateTimeOffset timestamp)
{
return new ParsedVulnerability
{
Id = "CVE-2026-0001",
Analysis = new ParsedVulnAnalysis
{
State = state,
Justification = justification,
Response = [],
Detail = "reviewed",
FirstIssued = timestamp,
LastUpdated = timestamp
},
Affects = [],
Ratings = []
};
}
private static VexConsumer CreateConsumer()
{
var evaluator = new VexTrustEvaluator(new StubTimeProvider());
var resolver = new VexConflictResolver();
var merger = new VexMerger(resolver);
var extractors = new IVexStatementExtractor[]
{
new CycloneDxVexExtractor(),
new SpdxVexExtractor()
};
return new VexConsumer(evaluator, merger, extractors);
}
private sealed class StubTimeProvider : TimeProvider
{
public override DateTimeOffset GetUtcNow()
=> DateTimeOffset.Parse("2026-01-20T01:00:00Z");
}
}

View File

@@ -0,0 +1,57 @@
using StellaOps.Concelier.SbomIntegration.Vex;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.SbomIntegration.Tests;
public sealed class VexConsumptionReporterTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToJson_IncludesStatements()
{
var reporter = new VexConsumptionReporter();
var report = new VexConsumptionReport
{
OverallTrustLevel = VexTrustLevel.Trusted,
Statements =
[
new ConsumedVexStatement
{
VulnerabilityId = "CVE-2026-4000",
Status = VexStatus.Affected,
Source = VexSource.SbomEmbedded,
TrustLevel = VexTrustLevel.Trusted
}
]
};
var json = reporter.ToJson(report);
Assert.Contains("CVE-2026-4000", json);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToSarif_EmitsResults()
{
var reporter = new VexConsumptionReporter();
var report = new VexConsumptionReport
{
Statements =
[
new ConsumedVexStatement
{
VulnerabilityId = "CVE-2026-4001",
Status = VexStatus.Affected,
Source = VexSource.External,
TrustLevel = VexTrustLevel.Unverified
}
]
};
var sarif = reporter.ToSarif(report);
Assert.Contains("vex-affected", sarif);
}
}

View File

@@ -0,0 +1,73 @@
using System.Collections.Immutable;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Concelier.SbomIntegration.Vex;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.SbomIntegration.Tests;
public sealed class VexExtractorTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CycloneDxExtractor_MapsBomRefToPurl()
{
var sbom = CreateSbom("CycloneDX");
var extractor = new CycloneDxVexExtractor();
var statements = extractor.Extract(sbom);
Assert.Single(statements);
Assert.Contains("pkg:npm/example@1.0.0", statements[0].AffectedComponents);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SpdxExtractor_HandlesSpdxFormat()
{
var sbom = CreateSbom("SPDX");
var extractor = new SpdxVexExtractor();
Assert.True(extractor.CanHandle(sbom));
var statements = extractor.Extract(sbom);
Assert.Single(statements);
}
private static ParsedSbom CreateSbom(string format)
{
var component = new ParsedComponent
{
BomRef = "comp-1",
Name = "example",
Purl = "pkg:npm/example@1.0.0"
};
var vulnerability = new ParsedVulnerability
{
Id = "CVE-2026-1000",
Analysis = new ParsedVulnAnalysis
{
State = VexState.NotAffected,
Justification = VexJustification.ComponentNotPresent,
Response = ImmutableArray<string>.Empty,
Detail = "not present",
FirstIssued = DateTimeOffset.Parse("2026-01-20T00:00:00Z"),
LastUpdated = DateTimeOffset.Parse("2026-01-20T00:00:00Z")
},
Affects =
[
new ParsedVulnAffects { Ref = "comp-1" }
]
};
return new ParsedSbom
{
Format = format,
SpecVersion = "1.7",
SerialNumber = "urn:uuid:example",
Components = [component],
Vulnerabilities = [vulnerability],
Metadata = new ParsedSbomMetadata()
};
}
}

View File

@@ -0,0 +1,106 @@
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Concelier.SbomIntegration.Parsing;
using StellaOps.Concelier.SbomIntegration.Vex;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.SbomIntegration.Tests;
public sealed class VexIntegrationTests
{
[Trait("Category", TestCategories.Integration)]
[Fact]
public async Task ConsumeFromSbomAsync_ParsesEmbeddedCycloneDxVex()
{
var json = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"serialNumber": "urn:uuid:00000000-0000-0000-0000-000000000001",
"version": 1,
"metadata": {
"timestamp": "2026-01-20T00:00:00Z",
"component": {
"bom-ref": "comp-1",
"type": "library",
"name": "example",
"version": "1.0.0",
"purl": "pkg:npm/example@1.0.0"
},
"tools": [
{
"vendor": "test",
"name": "test",
"version": "1.0"
}
]
},
"components": [
{
"bom-ref": "comp-1",
"type": "library",
"name": "example",
"version": "1.0.0",
"purl": "pkg:npm/example@1.0.0"
}
],
"vulnerabilities": [
{
"id": "CVE-2026-9999",
"analysis": {
"state": "not_affected",
"justification": "component_not_present",
"response": ["will_not_fix"],
"detail": "component absent",
"lastUpdated": "2026-01-20T00:00:00Z"
},
"affects": [
{ "ref": "comp-1" }
],
"ratings": [
{
"method": "CVSSv3",
"score": 7.5,
"severity": "high"
}
]
}
]
}
""";
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
var parser = new ParsedSbomParser(NullLogger<ParsedSbomParser>.Instance);
var sbom = await parser.ParseAsync(stream, SbomFormat.CycloneDX, CancellationToken.None);
var consumer = CreateConsumer();
var result = await consumer.ConsumeFromSbomAsync(sbom, VexConsumptionPolicyDefaults.Default, CancellationToken.None);
Assert.Single(result.Statements);
Assert.Equal("CVE-2026-9999", result.Statements[0].VulnerabilityId);
Assert.Equal(VexStatus.NotAffected, result.Statements[0].Status);
Assert.Contains("pkg:npm/example@1.0.0", result.Statements[0].AffectedComponents);
}
private static VexConsumer CreateConsumer()
{
var evaluator = new VexTrustEvaluator(new StubTimeProvider());
var resolver = new VexConflictResolver();
var merger = new VexMerger(resolver);
var extractors = new IVexStatementExtractor[]
{
new CycloneDxVexExtractor(),
new SpdxVexExtractor()
};
return new VexConsumer(evaluator, merger, extractors);
}
private sealed class StubTimeProvider : TimeProvider
{
public override DateTimeOffset GetUtcNow()
=> DateTimeOffset.Parse("2026-01-20T01:00:00Z");
}
}

View File

@@ -0,0 +1,54 @@
using StellaOps.Concelier.SbomIntegration.Vex;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.SbomIntegration.Tests;
public sealed class VexMergerTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_ExternalPriorityPrefersExternalStatements()
{
var resolver = new VexConflictResolver();
var merger = new VexMerger(resolver);
var embedded = new[]
{
new VexStatement
{
VulnerabilityId = "CVE-2026-3000",
Status = VexStatus.Affected,
Source = VexSource.SbomEmbedded,
TrustLevel = VexTrustLevel.Trusted,
Timestamp = DateTimeOffset.Parse("2026-01-20T00:00:00Z")
}
};
var external = new[]
{
new VexStatement
{
VulnerabilityId = "CVE-2026-3000",
Status = VexStatus.Fixed,
Source = VexSource.External,
TrustLevel = VexTrustLevel.Unverified,
Timestamp = DateTimeOffset.Parse("2026-01-21T00:00:00Z")
}
};
var mergePolicy = new VexMergePolicy
{
Mode = VexMergeMode.ExternalPriority
};
var merged = merger.Merge(
embedded,
external,
mergePolicy,
VexConflictResolutionStrategy.MostRecent);
Assert.Single(merged.Statements);
Assert.Equal(VexSource.External, merged.Statements[0].Source);
}
}

View File

@@ -291,7 +291,7 @@ public sealed class FederationEndpointTests
};
services.AddSingleton(options);
services.AddSingleton<IOptions<ConcelierOptions>>(Options.Create(options));
services.AddSingleton<IOptions<ConcelierOptions>>(Microsoft.Extensions.Options.Options.Create(options));
services.AddSingleton<TimeProvider>(new FixedTimeProvider(_fixedNow));
services.AddSingleton<IBundleExportService>(new FakeBundleExportService());
services.AddSingleton<IBundleImportService>(new FakeBundleImportService(_fixedNow));
@@ -309,8 +309,6 @@ public sealed class FederationEndpointTests
public override DateTimeOffset GetUtcNow() => _now;
public override long GetTimestamp() => 0;
public override TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => TimeSpan.Zero;
}
private sealed class FakeBundleExportService : IBundleExportService