consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,24 @@
# Excititor CycloneDX Formats Tests Agent Charter
## Mission
Validate CycloneDX normalization, component reconciliation, and export output with deterministic fixtures.
## Responsibilities
- Cover analysis state/justification mapping and metadata capture.
- Cover component reconciliation for purl/cpe diagnostics.
- Cover exporter output structure and severity mapping.
## Required Reading
- docs/modules/excititor/architecture.md
- docs/modules/excititor/operations/graph-linkouts-implementation.md
## Definition of Done
- Tests cover success and failure paths for CycloneDX normalizer and exporter.
- Fixtures avoid nondeterministic inputs (time, random).
## Working Agreement
- 1. Update task status to DOING/DONE in the sprint file and local TASKS.md.
- 2. Review this charter and required docs before coding.
- 3. Keep outputs deterministic (ordering, timestamps, hashes) and offline-friendly.
- 4. Add tests for negative/error paths.
- 5. Revert to TODO if paused; capture context in PR notes.

View File

@@ -0,0 +1,39 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Formats.CycloneDX;
using StellaOps.TestKit;
namespace StellaOps.Excititor.Formats.CycloneDX.Tests;
public sealed class CycloneDxComponentReconcilerTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Reconcile_AssignsBomRefsAndDiagnostics()
{
var claims = ImmutableArray.Create(
new VexClaim(
"CVE-2025-7000",
"vendor:one",
new VexProduct("pkg:demo/component@1.0.0", "Demo Component", "1.0.0", "pkg:demo/component@1.0.0"),
VexClaimStatus.Affected,
new VexClaimDocument(VexDocumentFormat.CycloneDx, "sha256:doc1", new Uri("https://example.com/vex/1")),
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow),
new VexClaim(
"CVE-2025-7000",
"vendor:two",
new VexProduct("component-key", "Component Key"),
VexClaimStatus.NotAffected,
new VexClaimDocument(VexDocumentFormat.CycloneDx, "sha256:doc2", new Uri("https://example.com/vex/2")),
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow));
var result = CycloneDxComponentReconciler.Reconcile(claims);
result.Components.Should().HaveCount(2);
result.ComponentRefs.Should().ContainKey(("CVE-2025-7000", "component-key"));
result.Diagnostics.Keys.Should().Contain("missing_purl");
}
}

View File

@@ -0,0 +1,138 @@
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Evidence;
using StellaOps.Excititor.Formats.CycloneDX;
using StellaOps.TestKit;
namespace StellaOps.Excititor.Formats.CycloneDX.Tests;
public sealed class CycloneDxExporterTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SerializeAsync_WritesCycloneDxVexDocument()
{
var claims = ImmutableArray.Create(
new VexClaim(
"CVE-2025-6000",
"vendor:demo",
new VexProduct("pkg:demo/component@1.2.3", "Demo Component", "1.2.3", "pkg:demo/component@1.2.3"),
VexClaimStatus.Fixed,
new VexClaimDocument(VexDocumentFormat.CycloneDx, "sha256:doc1", new Uri("https://example.com/cyclonedx/1")),
new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2025, 10, 11, 0, 0, 0, TimeSpan.Zero),
detail: "Issue resolved in 1.2.3",
signals: new VexSignalSnapshot(
new VexSeveritySignal(
scheme: "cvss-4.0",
score: 9.3,
label: "critical",
vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H"))));
var request = new VexExportRequest(
VexQuery.Empty,
ImmutableArray<VexConsensus>.Empty,
claims,
new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero));
var exporter = new CycloneDxExporter();
await using var stream = new MemoryStream();
var result = await exporter.SerializeAsync(request, stream, CancellationToken.None);
stream.Position = 0;
using var document = JsonDocument.Parse(stream);
var root = document.RootElement;
root.GetProperty("bomFormat").GetString().Should().Be("CycloneDX");
root.GetProperty("specVersion").GetString().Should().Be("1.7");
root.GetProperty("components").EnumerateArray().Should().HaveCount(1);
root.GetProperty("vulnerabilities").EnumerateArray().Should().HaveCount(1);
var vulnerability = root.GetProperty("vulnerabilities").EnumerateArray().Single();
var rating = vulnerability.GetProperty("ratings").EnumerateArray().Single();
rating.GetProperty("method").GetString().Should().Be("CVSSv4");
rating.GetProperty("score").GetDouble().Should().Be(9.3);
rating.GetProperty("severity").GetString().Should().Be("critical");
rating.GetProperty("vector").GetString().Should().Be("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H");
var affect = vulnerability.GetProperty("affects").EnumerateArray().Single();
var affectedVersion = affect.GetProperty("versions").EnumerateArray().Single();
affectedVersion.GetProperty("version").GetString().Should().Be("1.2.3");
var source = vulnerability.GetProperty("source");
source.GetProperty("name").GetString().Should().Be("vendor:demo");
source.GetProperty("url").GetString().Should().Be("pkg:demo/component@1.2.3");
result.Metadata.Should().ContainKey("cyclonedx.vulnerabilityCount");
result.Metadata["cyclonedx.componentCount"].Should().Be("1");
result.Digest.Algorithm.Should().Be("sha256");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SerializeAsync_IncludesEvidenceMetadata()
{
var claim = new VexClaim(
"CVE-2025-7001",
"vendor:evidence",
new VexProduct("pkg:demo/agent@2.0.0", "Demo Agent", "2.0.0", "pkg:demo/agent@2.0.0"),
VexClaimStatus.NotAffected,
new VexClaimDocument(VexDocumentFormat.CycloneDx, "sha256:doc2", new Uri("https://example.com/cyclonedx/2")),
new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2025, 10, 11, 0, 0, 0, TimeSpan.Zero),
justification: VexJustification.CodeNotReachable);
var entryId = VexEvidenceLinkIds.BuildVexEntryId(claim.VulnerabilityId, claim.Product.Key);
var evidence = new VexEvidenceLink
{
LinkId = "vexlink:test",
VexEntryId = entryId,
EvidenceType = EvidenceType.BinaryDiff,
EvidenceUri = "oci://registry/evidence@sha256:feed",
EnvelopeDigest = "sha256:feed",
PredicateType = "stellaops.binarydiff.v1",
Confidence = 0.95,
Justification = VexJustification.CodeNotPresent,
EvidenceCreatedAt = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero),
LinkedAt = new DateTimeOffset(2025, 10, 12, 0, 0, 1, TimeSpan.Zero),
SignatureValidated = true
};
var evidenceLinks = ImmutableDictionary<string, VexEvidenceLinkSet>.Empty.Add(
entryId,
new VexEvidenceLinkSet
{
VexEntryId = entryId,
Links = ImmutableArray.Create(evidence)
});
var request = new VexExportRequest(
VexQuery.Empty,
ImmutableArray<VexConsensus>.Empty,
ImmutableArray.Create(claim),
new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero),
evidenceLinks);
var exporter = new CycloneDxExporter();
await using var stream = new MemoryStream();
await exporter.SerializeAsync(request, stream, CancellationToken.None);
stream.Position = 0;
using var document = JsonDocument.Parse(stream);
var vulnerability = document.RootElement.GetProperty("vulnerabilities").EnumerateArray().Single();
vulnerability.GetProperty("analysis").GetProperty("detail").GetString()
.Should().Be("Evidence: oci://registry/evidence@sha256:feed");
var properties = vulnerability.GetProperty("properties").EnumerateArray().ToArray();
properties.Should().Contain(p => p.GetProperty("name").GetString() == "stellaops:evidence:type");
properties.Should().Contain(p => p.GetProperty("name").GetString() == "stellaops:evidence:uri");
properties.Should().Contain(p => p.GetProperty("name").GetString() == "stellaops:evidence:confidence");
properties.Should().Contain(p => p.GetProperty("name").GetString() == "stellaops:evidence:predicate-type");
properties.Should().Contain(p => p.GetProperty("name").GetString() == "stellaops:evidence:envelope-digest");
properties.Should().Contain(p => p.GetProperty("name").GetString() == "stellaops:evidence:signature-validated");
}
}

View File

@@ -0,0 +1,144 @@
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Formats.CycloneDX;
using StellaOps.TestKit;
namespace StellaOps.Excititor.Formats.CycloneDX.Tests;
public sealed class CycloneDxNormalizerTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task NormalizeAsync_MapsAnalysisStateAndJustification()
{
var json = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:1234",
"version": "7",
"metadata": {
"timestamp": "2025-10-15T12:00:00Z"
},
"components": [
{
"bom-ref": "pkg:npm/acme/lib@1.0.0",
"name": "acme-lib",
"version": "1.0.0",
"purl": "pkg:npm/acme/lib@1.0.0"
}
],
"vulnerabilities": [
{
"id": "CVE-2025-1000",
"detail": "Library issue",
"analysis": {
"state": "not_affected",
"justification": "code_not_present",
"response": [ "can_not_fix", "will_not_fix" ]
},
"affects": [
{ "ref": "pkg:npm/acme/lib@1.0.0" }
]
},
{
"id": "CVE-2025-1001",
"description": "Investigating impact",
"analysis": {
"state": "in_triage"
},
"affects": [
{ "ref": "pkg:npm/missing/component@2.0.0" }
]
}
]
}
""";
var rawDocument = new VexRawDocument(
"excititor:cyclonedx",
VexDocumentFormat.CycloneDx,
new Uri("https://example.org/vex.json"),
new DateTimeOffset(2025, 10, 16, 0, 0, 0, TimeSpan.Zero),
"sha256:dummydigest",
Encoding.UTF8.GetBytes(json),
ImmutableDictionary<string, string>.Empty);
var provider = new VexProvider("excititor:cyclonedx", "CycloneDX Provider", VexProviderKind.Vendor);
var normalizer = new CycloneDxNormalizer(NullLogger<CycloneDxNormalizer>.Instance);
var batch = await normalizer.NormalizeAsync(rawDocument, provider, CancellationToken.None);
batch.Claims.Should().HaveCount(2);
var notAffected = batch.Claims.Single(c => c.VulnerabilityId == "CVE-2025-1000");
notAffected.Status.Should().Be(VexClaimStatus.NotAffected);
notAffected.Justification.Should().Be(VexJustification.CodeNotPresent);
notAffected.Product.Key.Should().Be("pkg:npm/acme/lib@1.0.0");
notAffected.Product.Purl.Should().Be("pkg:npm/acme/lib@1.0.0");
notAffected.Document.Revision.Should().Be("7");
notAffected.AdditionalMetadata["cyclonedx.specVersion"].Should().Be("1.4");
notAffected.AdditionalMetadata["cyclonedx.analysis.state"].Should().Be("not_affected");
notAffected.AdditionalMetadata["cyclonedx.analysis.response"].Should().Be("can_not_fix,will_not_fix");
var investigating = batch.Claims.Single(c => c.VulnerabilityId == "CVE-2025-1001");
investigating.Status.Should().Be(VexClaimStatus.UnderInvestigation);
investigating.Justification.Should().BeNull();
investigating.Product.Key.Should().Be("pkg:npm/missing/component@2.0.0");
investigating.Product.Name.Should().Be("pkg:npm/missing/component@2.0.0");
investigating.AdditionalMetadata.Should().ContainKey("cyclonedx.specVersion");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task NormalizeAsync_NormalizesSpecVersion()
{
var json = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.7.0",
"metadata": {
"timestamp": "2025-10-15T12:00:00Z"
},
"components": [
{
"bom-ref": "pkg:npm/acme/lib@2.1.0",
"name": "acme-lib",
"version": "2.1.0",
"purl": "pkg:npm/acme/lib@2.1.0"
}
],
"vulnerabilities": [
{
"id": "CVE-2025-2000",
"analysis": { "state": "affected" },
"affects": [
{ "ref": "pkg:npm/acme/lib@2.1.0" }
]
}
]
}
""";
var rawDocument = new VexRawDocument(
"excititor:cyclonedx",
VexDocumentFormat.CycloneDx,
new Uri("https://example.org/vex-17.json"),
new DateTimeOffset(2025, 10, 16, 0, 0, 0, TimeSpan.Zero),
"sha256:dummydigest",
Encoding.UTF8.GetBytes(json),
ImmutableDictionary<string, string>.Empty);
var provider = new VexProvider("excititor:cyclonedx", "CycloneDX Provider", VexProviderKind.Vendor);
var normalizer = new CycloneDxNormalizer(NullLogger<CycloneDxNormalizer>.Instance);
var batch = await normalizer.NormalizeAsync(rawDocument, provider, CancellationToken.None);
batch.Claims.Should().HaveCount(1);
batch.Claims[0].AdditionalMetadata["cyclonedx.specVersion"].Should().Be("1.7");
}
}

View File

@@ -0,0 +1,357 @@
// -----------------------------------------------------------------------------
// CycloneDxExportSnapshotTests.cs
// Sprint: SPRINT_5100_0009_0003 - Excititor Module Test Implementation
// Task: EXCITITOR-5100-008 - Add snapshot tests for CycloneDX VEX export — canonical JSON
// Description: Snapshot tests verifying canonical CycloneDX output for VEX export
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Formats.CycloneDX;
using Xunit;
namespace StellaOps.Excititor.Formats.CycloneDX.Tests.Snapshots;
/// <summary>
/// Snapshot tests for CycloneDX format export.
/// Verifies canonical, deterministic JSON output per Model L0 (Core/Formats) requirements.
///
/// Snapshot regeneration: Set UPDATE_CYCLONEDX_SNAPSHOTS=1 environment variable.
/// </summary>
[Trait("Category", "Unit")]
[Trait("Category", "Snapshot")]
[Trait("Category", "L0")]
public sealed class CycloneDxExportSnapshotTests
{
private readonly ITestOutputHelper _output;
private readonly CycloneDxExporter _exporter;
private readonly string _snapshotsDir;
private readonly bool _updateSnapshots;
private static readonly JsonSerializerOptions CanonicalOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public CycloneDxExportSnapshotTests(ITestOutputHelper output)
{
_output = output;
_exporter = new CycloneDxExporter();
_snapshotsDir = Path.Combine(AppContext.BaseDirectory, "Snapshots", "Fixtures");
_updateSnapshots = Environment.GetEnvironmentVariable("UPDATE_CYCLONEDX_SNAPSHOTS") == "1";
if (!Directory.Exists(_snapshotsDir))
{
Directory.CreateDirectory(_snapshotsDir);
}
}
[Fact]
public async Task Export_MinimalClaim_MatchesSnapshot()
{
// Arrange
var claims = ImmutableArray.Create(CreateMinimalClaim());
var request = CreateExportRequest(claims);
// Act
var json = await ExportToJsonAsync(request);
// Assert
await AssertOrUpdateSnapshotAsync("cyclonedx-minimal.snapshot.json", json);
}
[Fact]
public async Task Export_WithCvssRating_MatchesSnapshot()
{
// Arrange
var claims = ImmutableArray.Create(CreateClaimWithCvss());
var request = CreateExportRequest(claims);
// Act
var json = await ExportToJsonAsync(request);
// Assert
await AssertOrUpdateSnapshotAsync("cyclonedx-cvss.snapshot.json", json);
}
[Fact]
public async Task Export_MultipleClaims_MatchesSnapshot()
{
// Arrange
var claims = ImmutableArray.Create(
CreateClaimWithStatus("CVE-2024-5001", VexClaimStatus.Affected),
CreateClaimWithStatus("CVE-2024-5002", VexClaimStatus.NotAffected),
CreateClaimWithStatus("CVE-2024-5003", VexClaimStatus.UnderInvestigation),
CreateClaimWithStatus("CVE-2024-5004", VexClaimStatus.Fixed)
);
var request = CreateExportRequest(claims);
// Act
var json = await ExportToJsonAsync(request);
// Assert
await AssertOrUpdateSnapshotAsync("cyclonedx-multiple.snapshot.json", json);
}
[Fact]
public async Task Export_MultipleComponents_MatchesSnapshot()
{
// Arrange
var claims = ImmutableArray.Create(
CreateClaimForComponent("pkg:npm/lodash@4.17.21", "lodash", "4.17.21", "CVE-2024-6001"),
CreateClaimForComponent("pkg:npm/express@4.18.2", "express", "4.18.2", "CVE-2024-6002"),
CreateClaimForComponent("pkg:pypi/django@4.2.0", "django", "4.2.0", "CVE-2024-6003"),
CreateClaimForComponent("pkg:maven/org.apache.commons/commons-text@1.10.0", "commons-text", "1.10.0", "CVE-2024-6004")
);
var request = CreateExportRequest(claims);
// Act
var json = await ExportToJsonAsync(request);
// Assert
await AssertOrUpdateSnapshotAsync("cyclonedx-multicomponent.snapshot.json", json);
}
[Fact]
public async Task Export_IsDeterministic_HashStable()
{
// Arrange
var claims = ImmutableArray.Create(CreateClaimWithCvss());
var request = CreateExportRequest(claims);
// Act - export multiple times
var hashes = new HashSet<string>();
for (int i = 0; i < 10; i++)
{
var json = await ExportToJsonAsync(request);
var hash = ComputeHash(json);
hashes.Add(hash);
}
// Assert
hashes.Should().HaveCount(1, "Multiple exports should produce identical JSON");
_output.WriteLine($"Stable hash: {hashes.First()}");
}
[Fact]
public async Task Export_DigestMatchesContent()
{
// Arrange
var claims = ImmutableArray.Create(CreateClaimWithCvss());
var request = CreateExportRequest(claims);
// Act
var digest1 = _exporter.Digest(request);
await using var stream = new MemoryStream();
var result = await _exporter.SerializeAsync(request, stream, CancellationToken.None);
// Assert
digest1.Should().NotBeNull();
digest1.Should().Be(result.Digest, "Pre-computed digest should match serialization result");
// Verify digest is actually based on content
stream.Position = 0;
var content = await new StreamReader(stream).ReadToEndAsync();
_output.WriteLine($"Content length: {content.Length}");
_output.WriteLine($"Export digest: {result.Digest}");
}
[Fact]
public async Task Export_EmptyClaims_MatchesSnapshot()
{
// Arrange
var request = CreateExportRequest(ImmutableArray<VexClaim>.Empty);
// Act
var json = await ExportToJsonAsync(request);
// Assert
await AssertOrUpdateSnapshotAsync("cyclonedx-empty.snapshot.json", json);
}
[Fact]
public async Task Export_ParallelExports_AreDeterministic()
{
// Arrange
var claims = ImmutableArray.Create(CreateClaimWithCvss());
var request = CreateExportRequest(claims);
// Act - parallel exports
var tasks = Enumerable.Range(0, 10)
.Select(_ => Task.Run(async () =>
{
var json = await ExportToJsonAsync(request);
return ComputeHash(json);
}));
var hashes = await Task.WhenAll(tasks);
// Assert
hashes.Distinct().Should().HaveCount(1, "Parallel exports must produce identical output");
}
[Fact]
public async Task Export_CycloneDxStructure_ContainsRequiredFields()
{
// Arrange
var claims = ImmutableArray.Create(CreateClaimWithCvss());
var request = CreateExportRequest(claims);
// Act
var json = await ExportToJsonAsync(request);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Assert - CycloneDX 1.7 required fields for VEX
root.TryGetProperty("bomFormat", out var bomFormat).Should().BeTrue();
bomFormat.GetString().Should().Be("CycloneDX");
root.TryGetProperty("specVersion", out var specVersion).Should().BeTrue();
specVersion.GetString().Should().Be("1.7");
root.TryGetProperty("vulnerabilities", out _).Should().BeTrue("VEX BOM should have vulnerabilities array");
}
[Fact]
public async Task Export_ResultContainsMetadata()
{
// Arrange
var claims = ImmutableArray.Create(
CreateMinimalClaim(),
CreateClaimWithCvss()
);
var request = CreateExportRequest(claims);
// Act
await using var stream = new MemoryStream();
var result = await _exporter.SerializeAsync(request, stream, CancellationToken.None);
// Assert
_exporter.Format.Should().Be(VexExportFormat.CycloneDx);
result.Metadata.Should().ContainKey("cyclonedx.vulnerabilityCount");
result.Metadata.Should().ContainKey("cyclonedx.componentCount");
result.Digest.Algorithm.Should().Be("sha256");
}
#region Helper Methods
private async Task<string> ExportToJsonAsync(VexExportRequest request)
{
await using var stream = new MemoryStream();
await _exporter.SerializeAsync(request, stream, CancellationToken.None);
stream.Position = 0;
using var reader = new StreamReader(stream, Encoding.UTF8);
return await reader.ReadToEndAsync();
}
private async Task AssertOrUpdateSnapshotAsync(string snapshotName, string actual)
{
var snapshotPath = Path.Combine(_snapshotsDir, snapshotName);
if (_updateSnapshots)
{
await File.WriteAllTextAsync(snapshotPath, actual, Encoding.UTF8);
_output.WriteLine($"Updated snapshot: {snapshotName}");
return;
}
if (!File.Exists(snapshotPath))
{
await File.WriteAllTextAsync(snapshotPath, actual, Encoding.UTF8);
_output.WriteLine($"Created new snapshot: {snapshotName}");
return;
}
var expected = await File.ReadAllTextAsync(snapshotPath, Encoding.UTF8);
// Parse and re-serialize for comparison (handles formatting differences)
var expectedDoc = JsonDocument.Parse(expected);
var actualDoc = JsonDocument.Parse(actual);
var expectedNormalized = JsonSerializer.Serialize(expectedDoc.RootElement, CanonicalOptions);
var actualNormalized = JsonSerializer.Serialize(actualDoc.RootElement, CanonicalOptions);
actualNormalized.Should().Be(expectedNormalized,
$"CycloneDX export should match snapshot {snapshotName}. Set UPDATE_CYCLONEDX_SNAPSHOTS=1 to update.");
}
private static string ComputeHash(string json)
{
var bytes = Encoding.UTF8.GetBytes(json);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static VexExportRequest CreateExportRequest(ImmutableArray<VexClaim> claims)
{
return new VexExportRequest(
VexQuery.Empty,
ImmutableArray<VexConsensus>.Empty,
claims,
new DateTimeOffset(2025, 1, 15, 12, 0, 0, TimeSpan.Zero));
}
private static VexClaim CreateMinimalClaim()
{
return new VexClaim(
"CVE-2024-44444",
"cyclonedx-minimal-source",
new VexProduct("pkg:npm/minimal@1.0.0", "Minimal Package", "1.0.0", "pkg:npm/minimal@1.0.0"),
VexClaimStatus.NotAffected,
new VexClaimDocument(VexDocumentFormat.CycloneDx, "sha256:cdx-minimal", new Uri("https://example.com/cdx/minimal")),
new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
}
private static VexClaim CreateClaimWithCvss()
{
return new VexClaim(
"CVE-2024-55555",
"cyclonedx-cvss-source",
new VexProduct("pkg:npm/vulnerable@2.0.0", "Vulnerable Component", "2.0.0", "pkg:npm/vulnerable@2.0.0"),
VexClaimStatus.Affected,
new VexClaimDocument(VexDocumentFormat.CycloneDx, "sha256:cdx-cvss", new Uri("https://example.com/cdx/cvss")),
new DateTimeOffset(2025, 1, 10, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2025, 1, 15, 0, 0, 0, TimeSpan.Zero),
detail: "Critical vulnerability with high CVSS score",
signals: new VexSignalSnapshot(
new VexSeveritySignal(
scheme: "cvss-4.0",
score: 9.3,
label: "critical",
vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H")));
}
private static VexClaim CreateClaimWithStatus(string cveId, VexClaimStatus status)
{
return new VexClaim(
cveId,
$"cyclonedx-source-{cveId}",
new VexProduct($"pkg:npm/pkg-{cveId}@1.0.0", $"Package {cveId}", "1.0.0", $"pkg:npm/pkg-{cveId}@1.0.0"),
status,
new VexClaimDocument(VexDocumentFormat.CycloneDx, $"sha256:{cveId}", new Uri($"https://example.com/cdx/{cveId}")),
new DateTimeOffset(2025, 1, 10, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2025, 1, 15, 0, 0, 0, TimeSpan.Zero));
}
private static VexClaim CreateClaimForComponent(string purl, string name, string version, string cveId)
{
return new VexClaim(
cveId,
$"cyclonedx-source-{name}",
new VexProduct(purl, name, version, purl),
VexClaimStatus.Fixed,
new VexClaimDocument(VexDocumentFormat.CycloneDx, $"sha256:{name}", new Uri($"https://example.com/cdx/{name}")),
new DateTimeOffset(2025, 1, 10, 0, 0, 0, TimeSpan.Zero),
new DateTimeOffset(2025, 1, 15, 0, 0, 0, TimeSpan.Zero),
detail: $"Vulnerability in {name} fixed in version {version}");
}
#endregion
}

View File

@@ -0,0 +1,16 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Excititor.Formats.CycloneDX/StellaOps.Excititor.Formats.CycloneDX.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
# Excititor CycloneDX Formats Tests Task Board
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`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-0320-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0320-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0320-A | DONE | Waived (test project; revalidated 2026-01-07). |
| VEX-LINK-CYCLONEDX-TESTS-0001 | DONE | SPRINT_20260113_003_001 - Evidence properties tests. |