Add determinism tests for verdict artifact generation and update SHA256 sums script
- Implemented comprehensive tests for verdict artifact generation to ensure deterministic outputs across various scenarios, including identical inputs, parallel execution, and change ordering. - Created helper methods for generating sample verdict inputs and computing canonical hashes. - Added tests to validate the stability of canonical hashes, proof spine ordering, and summary statistics. - Introduced a new PowerShell script to update SHA256 sums for files, ensuring accurate hash generation and file integrity checks.
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CveParserSnapshotTests.cs
|
||||
// Sprint: SPRINT_5100_0007_0005
|
||||
// Task: CONN-FIX-005
|
||||
// Description: CVE parser snapshot tests for fixture validation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Concelier.Connector.Cve.Internal;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Cve.Tests.Cve;
|
||||
|
||||
/// <summary>
|
||||
/// Parser snapshot tests for the CVE connector.
|
||||
/// Verifies that raw CVE JSON fixtures parse to expected canonical Advisory output.
|
||||
/// </summary>
|
||||
public sealed class CveParserSnapshotTests
|
||||
{
|
||||
private static readonly string BaseDirectory = AppContext.BaseDirectory;
|
||||
private static readonly string FixturesDirectory = Path.Combine(BaseDirectory, "Fixtures");
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Determinism")]
|
||||
public void ParseCveRecord_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture("cve-CVE-2024-0001.json");
|
||||
|
||||
// Act
|
||||
var results = new List<string>();
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var advisory = ParseToAdvisory(rawJson);
|
||||
results.Add(CanonJson.Serialize(advisory));
|
||||
}
|
||||
|
||||
// Assert
|
||||
results.Distinct().Should().HaveCount(1,
|
||||
"parsing CVE record multiple times should produce identical output");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Parser")]
|
||||
public void CveRecordParser_ExtractsCveId()
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture("cve-CVE-2024-0001.json");
|
||||
var content = Encoding.UTF8.GetBytes(rawJson);
|
||||
|
||||
// Act
|
||||
var dto = CveRecordParser.Parse(content);
|
||||
|
||||
// Assert
|
||||
dto.CveId.Should().Be("CVE-2024-0001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Parser")]
|
||||
public void CveRecordParser_ExtractsTitle()
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture("cve-CVE-2024-0001.json");
|
||||
var content = Encoding.UTF8.GetBytes(rawJson);
|
||||
|
||||
// Act
|
||||
var dto = CveRecordParser.Parse(content);
|
||||
|
||||
// Assert
|
||||
dto.Title.Should().Be("Example Product Remote Code Execution");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Parser")]
|
||||
public void CveRecordParser_ExtractsAliases()
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture("cve-CVE-2024-0001.json");
|
||||
var content = Encoding.UTF8.GetBytes(rawJson);
|
||||
|
||||
// Act
|
||||
var dto = CveRecordParser.Parse(content);
|
||||
|
||||
// Assert
|
||||
dto.Aliases.Should().Contain("CVE-2024-0001", "CVE ID should be in aliases");
|
||||
dto.Aliases.Should().Contain("GHSA-xxxx-yyyy-zzzz", "GHSA alias should be in aliases");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Parser")]
|
||||
public void CveRecordParser_ExtractsReferences()
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture("cve-CVE-2024-0001.json");
|
||||
var content = Encoding.UTF8.GetBytes(rawJson);
|
||||
|
||||
// Act
|
||||
var dto = CveRecordParser.Parse(content);
|
||||
|
||||
// Assert
|
||||
dto.References.Should().HaveCount(2);
|
||||
dto.References.Should().Contain(r => r.Url == "https://example.com/security/advisory");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Parser")]
|
||||
public void CveRecordParser_ExtractsAffected()
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture("cve-CVE-2024-0001.json");
|
||||
var content = Encoding.UTF8.GetBytes(rawJson);
|
||||
|
||||
// Act
|
||||
var dto = CveRecordParser.Parse(content);
|
||||
|
||||
// Assert
|
||||
dto.Affected.Should().HaveCount(1);
|
||||
dto.Affected[0].Vendor.Should().Be("ExampleVendor");
|
||||
dto.Affected[0].Product.Should().Be("ExampleProduct");
|
||||
dto.Affected[0].Versions.Should().HaveCountGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Parser")]
|
||||
public void CveRecordParser_ExtractsMetrics()
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture("cve-CVE-2024-0001.json");
|
||||
var content = Encoding.UTF8.GetBytes(rawJson);
|
||||
|
||||
// Act
|
||||
var dto = CveRecordParser.Parse(content);
|
||||
|
||||
// Assert
|
||||
dto.Metrics.Should().HaveCount(1);
|
||||
dto.Metrics[0].CvssV31.Should().NotBeNull();
|
||||
dto.Metrics[0].CvssV31!.BaseScore.Should().Be(9.8);
|
||||
dto.Metrics[0].CvssV31.BaseSeverity.Should().Be("CRITICAL");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Parser")]
|
||||
public void CveMapper_SetsSeverityFromCvss()
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture("cve-CVE-2024-0001.json");
|
||||
|
||||
// Act
|
||||
var advisory = ParseToAdvisory(rawJson);
|
||||
|
||||
// Assert
|
||||
advisory.Severity.Should().Be("critical");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Parser")]
|
||||
public void CveMapper_CreatesCvssMetrics()
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture("cve-CVE-2024-0001.json");
|
||||
|
||||
// Act
|
||||
var advisory = ParseToAdvisory(rawJson);
|
||||
|
||||
// Assert
|
||||
advisory.CvssMetrics.Should().HaveCount(1);
|
||||
advisory.CvssMetrics[0].BaseScore.Should().Be(9.8);
|
||||
advisory.CvssMetrics[0].Version.Should().Be("3.1");
|
||||
advisory.CvssMetrics[0].Vector.Should().Contain("CVSS:3.1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void CveRecordParser_MissingMetadata_ThrowsJsonException()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = """{"dataType": "CVE_RECORD"}""";
|
||||
var content = Encoding.UTF8.GetBytes(invalidJson);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => CveRecordParser.Parse(content);
|
||||
act.Should().Throw<JsonException>().WithMessage("*cveMetadata*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void CveRecordParser_MissingCveId_ThrowsJsonException()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = """{"cveMetadata": {"state": "PUBLISHED"}}""";
|
||||
var content = Encoding.UTF8.GetBytes(invalidJson);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => CveRecordParser.Parse(content);
|
||||
act.Should().Throw<JsonException>().WithMessage("*cveId*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void CveRecordParser_MinimalRecord_ParsesSuccessfully()
|
||||
{
|
||||
// Arrange - minimal CVE record with only required fields
|
||||
var minimalJson = """
|
||||
{
|
||||
"cveMetadata": {
|
||||
"cveId": "CVE-2024-9999",
|
||||
"state": "PUBLISHED"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var content = Encoding.UTF8.GetBytes(minimalJson);
|
||||
|
||||
// Act
|
||||
var dto = CveRecordParser.Parse(content);
|
||||
|
||||
// Assert
|
||||
dto.CveId.Should().Be("CVE-2024-9999");
|
||||
dto.Aliases.Should().Contain("CVE-2024-9999");
|
||||
dto.References.Should().BeEmpty();
|
||||
dto.Affected.Should().BeEmpty();
|
||||
dto.Metrics.Should().BeEmpty();
|
||||
}
|
||||
|
||||
private static Models.Advisory ParseToAdvisory(string rawJson)
|
||||
{
|
||||
var content = Encoding.UTF8.GetBytes(rawJson);
|
||||
var dto = CveRecordParser.Parse(content);
|
||||
|
||||
// Use fixed recordedAt for deterministic output
|
||||
var recordedAt = new DateTimeOffset(2024, 10, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var document = CreateTestDocumentRecord(dto.CveId, recordedAt);
|
||||
return CveMapper.Map(dto, document, recordedAt);
|
||||
}
|
||||
|
||||
private static DocumentRecord CreateTestDocumentRecord(string cveId, DateTimeOffset recordedAt) =>
|
||||
new(
|
||||
Id: Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"),
|
||||
SourceName: CveConnectorPlugin.SourceName,
|
||||
Uri: $"https://cveawg.mitre.org/api/cve/{cveId}",
|
||||
FetchedAt: recordedAt,
|
||||
Sha256: "sha256-test",
|
||||
Status: "completed",
|
||||
ContentType: "application/json",
|
||||
Headers: null,
|
||||
Metadata: null,
|
||||
Etag: null,
|
||||
LastModified: recordedAt,
|
||||
PayloadId: null);
|
||||
|
||||
private static string ReadFixture(string fileName)
|
||||
{
|
||||
var path = Path.Combine(FixturesDirectory, fileName);
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Cve/StellaOps.Concelier.Connector.Cve.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Testing/StellaOps.Concelier.Testing.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Canonical.Json/StellaOps.Canonical.Json.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures/*.json" CopyToOutputDirectory="Always" />
|
||||
|
||||
Reference in New Issue
Block a user