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,119 @@
|
||||
{
|
||||
"advisoryKey": "CVE-2024-0001",
|
||||
"affectedPackages": [
|
||||
{
|
||||
"type": "cpe",
|
||||
"identifier": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*",
|
||||
"platform": null,
|
||||
"versionRanges": [
|
||||
{
|
||||
"fixedVersion": null,
|
||||
"introducedVersion": null,
|
||||
"lastAffectedVersion": null,
|
||||
"primitives": {
|
||||
"evr": null,
|
||||
"hasVendorExtensions": true,
|
||||
"nevra": null,
|
||||
"semVer": null,
|
||||
"vendorExtensions": {
|
||||
"cpe": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*"
|
||||
}
|
||||
},
|
||||
"provenance": {
|
||||
"source": "nvd",
|
||||
"kind": "cpe",
|
||||
"value": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T10:00:00+00:00",
|
||||
"fieldMask": ["affectedpackages[].versionranges[]"]
|
||||
},
|
||||
"rangeExpression": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*",
|
||||
"rangeKind": "cpe"
|
||||
}
|
||||
],
|
||||
"normalizedVersions": [],
|
||||
"statuses": [],
|
||||
"provenance": [
|
||||
{
|
||||
"source": "nvd",
|
||||
"kind": "cpe",
|
||||
"value": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T10:00:00+00:00",
|
||||
"fieldMask": ["affectedpackages[]"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"aliases": ["CVE-2024-0001"],
|
||||
"canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"credits": [],
|
||||
"cvssMetrics": [
|
||||
{
|
||||
"baseScore": 9.8,
|
||||
"baseSeverity": "critical",
|
||||
"provenance": {
|
||||
"source": "nvd",
|
||||
"kind": "cvss",
|
||||
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T10:00:00+00:00",
|
||||
"fieldMask": ["cvssmetrics[]"]
|
||||
},
|
||||
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"version": "3.1"
|
||||
}
|
||||
],
|
||||
"cwes": [
|
||||
{
|
||||
"taxonomy": "cwe",
|
||||
"identifier": "CWE-79",
|
||||
"name": "Improper Neutralization of Input",
|
||||
"uri": "https://cwe.mitre.org/data/definitions/79.html",
|
||||
"provenance": [
|
||||
{
|
||||
"source": "nvd",
|
||||
"kind": "weakness",
|
||||
"value": "CWE-79",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T10:00:00+00:00",
|
||||
"fieldMask": ["cwes[]"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": "Example vulnerability one.",
|
||||
"exploitKnown": false,
|
||||
"language": "en",
|
||||
"modified": "2024-01-02T10:00:00+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"source": "nvd",
|
||||
"kind": "document",
|
||||
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T10:00:00+00:00",
|
||||
"fieldMask": ["advisory"]
|
||||
}
|
||||
],
|
||||
"published": "2024-01-01T10:00:00+00:00",
|
||||
"references": [
|
||||
{
|
||||
"kind": "vendor advisory",
|
||||
"provenance": {
|
||||
"source": "nvd",
|
||||
"kind": "reference",
|
||||
"value": "https://vendor.example.com/advisories/0001",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T10:00:00+00:00",
|
||||
"fieldMask": ["references[]"]
|
||||
},
|
||||
"sourceTag": "Vendor",
|
||||
"summary": null,
|
||||
"url": "https://vendor.example.com/advisories/0001"
|
||||
}
|
||||
],
|
||||
"severity": "critical",
|
||||
"summary": "Example vulnerability one.",
|
||||
"title": "CVE-2024-0001"
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"advisoryKey": "CVE-2024-0002",
|
||||
"affectedPackages": [
|
||||
{
|
||||
"type": "cpe",
|
||||
"identifier": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*",
|
||||
"platform": null,
|
||||
"versionRanges": [
|
||||
{
|
||||
"fixedVersion": null,
|
||||
"introducedVersion": null,
|
||||
"lastAffectedVersion": null,
|
||||
"primitives": {
|
||||
"evr": null,
|
||||
"hasVendorExtensions": true,
|
||||
"nevra": null,
|
||||
"semVer": null,
|
||||
"vendorExtensions": {
|
||||
"cpe": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*"
|
||||
}
|
||||
},
|
||||
"provenance": {
|
||||
"source": "nvd",
|
||||
"kind": "cpe",
|
||||
"value": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T11:00:00+00:00",
|
||||
"fieldMask": ["affectedpackages[].versionranges[]"]
|
||||
},
|
||||
"rangeExpression": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*",
|
||||
"rangeKind": "cpe"
|
||||
}
|
||||
],
|
||||
"normalizedVersions": [],
|
||||
"statuses": [],
|
||||
"provenance": [
|
||||
{
|
||||
"source": "nvd",
|
||||
"kind": "cpe",
|
||||
"value": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T11:00:00+00:00",
|
||||
"fieldMask": ["affectedpackages[]"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"aliases": ["CVE-2024-0002"],
|
||||
"canonicalMetricId": "3.0|CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
|
||||
"credits": [],
|
||||
"cvssMetrics": [
|
||||
{
|
||||
"baseScore": 4.6,
|
||||
"baseSeverity": "medium",
|
||||
"provenance": {
|
||||
"source": "nvd",
|
||||
"kind": "cvss",
|
||||
"value": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T11:00:00+00:00",
|
||||
"fieldMask": ["cvssmetrics[]"]
|
||||
},
|
||||
"vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
|
||||
"version": "3.0"
|
||||
}
|
||||
],
|
||||
"cwes": [
|
||||
{
|
||||
"taxonomy": "cwe",
|
||||
"identifier": "CWE-89",
|
||||
"name": "SQL Injection",
|
||||
"uri": "https://cwe.mitre.org/data/definitions/89.html",
|
||||
"provenance": [
|
||||
{
|
||||
"source": "nvd",
|
||||
"kind": "weakness",
|
||||
"value": "CWE-89",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T11:00:00+00:00",
|
||||
"fieldMask": ["cwes[]"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": "Example vulnerability two.",
|
||||
"exploitKnown": false,
|
||||
"language": "en",
|
||||
"modified": "2024-01-02T11:00:00+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"source": "nvd",
|
||||
"kind": "document",
|
||||
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T11:00:00+00:00",
|
||||
"fieldMask": ["advisory"]
|
||||
}
|
||||
],
|
||||
"published": "2024-01-01T11:00:00+00:00",
|
||||
"references": [
|
||||
{
|
||||
"kind": "us government resource",
|
||||
"provenance": {
|
||||
"source": "nvd",
|
||||
"kind": "reference",
|
||||
"value": "https://cisa.example.gov/alerts/0002",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2024-01-02T11:00:00+00:00",
|
||||
"fieldMask": ["references[]"]
|
||||
},
|
||||
"sourceTag": "CISA",
|
||||
"summary": null,
|
||||
"url": "https://cisa.example.gov/alerts/0002"
|
||||
}
|
||||
],
|
||||
"severity": "medium",
|
||||
"summary": "Example vulnerability two.",
|
||||
"title": "CVE-2024-0002"
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// NvdParserSnapshotTests.cs
|
||||
// Sprint: SPRINT_5100_0007_0005
|
||||
// Task: CONN-FIX-005
|
||||
// Description: NVD parser snapshot tests using TestKit ConnectorParserTestBase
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Concelier.Connector.Nvd.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.TestKit.Connectors;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Nvd.Tests.Nvd;
|
||||
|
||||
/// <summary>
|
||||
/// Parser snapshot tests for the NVD connector.
|
||||
/// Verifies that raw NVD JSON fixtures parse to expected canonical output.
|
||||
/// </summary>
|
||||
public sealed class NvdParserSnapshotTests : ConnectorParserTestBase<JsonDocument, IReadOnlyList<Advisory>>
|
||||
{
|
||||
private static readonly string BaseDirectory = AppContext.BaseDirectory;
|
||||
|
||||
protected override string FixturesDirectory =>
|
||||
Path.Combine(BaseDirectory, "Nvd", "Fixtures");
|
||||
|
||||
protected override string ExpectedDirectory =>
|
||||
Path.Combine(BaseDirectory, "Expected");
|
||||
|
||||
protected override JsonDocument DeserializeRaw(string json) =>
|
||||
JsonDocument.Parse(json);
|
||||
|
||||
protected override IReadOnlyList<Advisory> Parse(JsonDocument raw)
|
||||
{
|
||||
var sourceDocument = CreateTestDocumentRecord();
|
||||
var recordedAt = new DateTimeOffset(2024, 1, 2, 10, 0, 0, TimeSpan.Zero);
|
||||
return NvdMapper.Map(raw, sourceDocument, recordedAt);
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<Advisory> DeserializeNormalized(string json) =>
|
||||
CanonJson.Deserialize<List<Advisory>>(json) ?? new List<Advisory>();
|
||||
|
||||
protected override string SerializeToCanonical(IReadOnlyList<Advisory> model)
|
||||
{
|
||||
// For single advisory tests, serialize just the first advisory
|
||||
if (model.Count == 1)
|
||||
{
|
||||
return CanonJson.Serialize(model[0]);
|
||||
}
|
||||
return CanonJson.Serialize(model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Snapshot")]
|
||||
public void ParseNvdWindow1_CVE20240001_ProducesExpectedOutput()
|
||||
{
|
||||
VerifyParseSnapshotSingle("nvd-window-1.json", "nvd-window-1-CVE-2024-0001.canonical.json", "CVE-2024-0001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Snapshot")]
|
||||
public void ParseNvdWindow1_CVE20240002_ProducesExpectedOutput()
|
||||
{
|
||||
VerifyParseSnapshotSingle("nvd-window-1.json", "nvd-window-1-CVE-2024-0002.canonical.json", "CVE-2024-0002");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Determinism")]
|
||||
public void ParseNvdWindow1_IsDeterministic()
|
||||
{
|
||||
VerifyDeterministicParse("nvd-window-1.json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Determinism")]
|
||||
public void ParseNvdMultipage_IsDeterministic()
|
||||
{
|
||||
VerifyDeterministicParse("nvd-multipage-1.json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Snapshot")]
|
||||
public void ParseConflictNvd_ProducesExpectedOutput()
|
||||
{
|
||||
// The conflict fixture is inline in NvdConflictFixtureTests
|
||||
// This test verifies the canonical output matches
|
||||
VerifyParseSnapshotSingle("conflict-nvd.canonical.json", "conflict-nvd.canonical.json", "CVE-2025-4242");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a fixture parses to the expected canonical output for a single advisory.
|
||||
/// </summary>
|
||||
private void VerifyParseSnapshotSingle(string fixtureFile, string expectedFile, string advisoryKey)
|
||||
{
|
||||
// Arrange
|
||||
var rawJson = ReadFixture(fixtureFile);
|
||||
var expectedJson = ReadExpected(expectedFile).Replace("\r\n", "\n").TrimEnd();
|
||||
using var raw = DeserializeRaw(rawJson);
|
||||
|
||||
// Act
|
||||
var advisories = Parse(raw);
|
||||
var advisory = advisories.FirstOrDefault(a => a.AdvisoryKey == advisoryKey);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(advisory);
|
||||
var actualJson = CanonJson.Serialize(advisory).Replace("\r\n", "\n").TrimEnd();
|
||||
|
||||
if (actualJson != expectedJson)
|
||||
{
|
||||
// Write actual output for debugging
|
||||
var actualPath = Path.Combine(ExpectedDirectory, expectedFile.Replace(".json", ".actual.json"));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
File.WriteAllText(actualPath, actualJson);
|
||||
}
|
||||
|
||||
Assert.Equal(expectedJson, actualJson);
|
||||
}
|
||||
|
||||
private static DocumentRecord CreateTestDocumentRecord() =>
|
||||
new(
|
||||
Id: Guid.NewGuid(),
|
||||
SourceName: NvdConnectorPlugin.SourceName,
|
||||
Uri: "https://services.nvd.nist.gov/rest/json/cves/2.0",
|
||||
FetchedAt: new DateTimeOffset(2024, 1, 2, 10, 0, 0, TimeSpan.Zero),
|
||||
Sha256: "sha256-test",
|
||||
Status: "completed",
|
||||
ContentType: "application/json",
|
||||
Headers: null,
|
||||
Metadata: null,
|
||||
Etag: null,
|
||||
LastModified: null,
|
||||
PayloadId: null);
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// NvdResilienceTests.cs
|
||||
// Sprint: SPRINT_5100_0007_0005
|
||||
// Task: CONN-FIX-011
|
||||
// Description: Resilience tests for NVD connector - missing fields, invalid data
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Concelier.Connector.Nvd.Internal;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Nvd.Tests.Nvd;
|
||||
|
||||
/// <summary>
|
||||
/// Resilience tests for the NVD connector.
|
||||
/// Verifies graceful handling of partial, malformed, and edge-case inputs.
|
||||
/// </summary>
|
||||
public sealed class NvdResilienceTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedRecordedAt = new(2024, 10, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
#region Missing Fields Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_MissingVulnerabilitiesArray_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var json = """{"format": "NVD_CVE", "version": "2.0"}""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().BeEmpty("missing vulnerabilities array should return empty list");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_EmptyVulnerabilitiesArray_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var json = """{"vulnerabilities": []}""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().BeEmpty("empty vulnerabilities array should return empty list");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_VulnerabilityMissingCveObject_SkipsEntry()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{"notCve": {}},
|
||||
{"cve": {"id": "CVE-2024-0001"}}
|
||||
]
|
||||
}
|
||||
""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1, "should skip entry without cve object");
|
||||
advisories[0].AdvisoryKey.Should().Be("CVE-2024-0001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_VulnerabilityMissingId_SkipsEntry()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{"cve": {"descriptions": []}},
|
||||
{"cve": {"id": "CVE-2024-0002"}}
|
||||
]
|
||||
}
|
||||
""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1, "should skip entry without id");
|
||||
advisories[0].AdvisoryKey.Should().Be("CVE-2024-0002");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_VulnerabilityWithNullId_SkipsEntry()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{"cve": {"id": null}},
|
||||
{"cve": {"id": "CVE-2024-0003"}}
|
||||
]
|
||||
}
|
||||
""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1, "should skip entry with null id");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_VulnerabilityWithEmptyId_GeneratesSyntheticKey()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{"cve": {"id": ""}}
|
||||
]
|
||||
}
|
||||
""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1);
|
||||
advisories[0].AdvisoryKey.Should().StartWith("nvd:", "should generate synthetic key for empty id");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Invalid Date Format Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_InvalidPublishedDate_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0001",
|
||||
"published": "not-a-date"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1, "should still parse advisory with invalid date");
|
||||
advisories[0].Published.Should().BeNull("invalid date should result in null");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_MissingPublishedDate_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0001"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1);
|
||||
advisories[0].Published.Should().BeNull("missing date should result in null");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unknown Enum Value Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_UnknownCvssSeverity_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0001",
|
||||
"metrics": {
|
||||
"cvssMetricV31": [
|
||||
{
|
||||
"cvssData": {
|
||||
"version": "3.1",
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"baseScore": 9.8,
|
||||
"baseSeverity": "UNKNOWN_SEVERITY"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1, "should still parse advisory with unknown severity");
|
||||
// Unknown severity might be preserved or mapped to a default
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Determinism")]
|
||||
public void Map_SameInput_ProducesDeterministicOutput()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0001",
|
||||
"descriptions": [{"lang": "en", "value": "Test vulnerability"}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var results = new List<string>();
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
results.Add(CanonJson.Serialize(advisories));
|
||||
}
|
||||
|
||||
// Assert
|
||||
results.Distinct().Should().HaveCount(1,
|
||||
"same input should produce identical output on multiple runs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Determinism")]
|
||||
public void Map_ErrorHandling_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{"cve": {}},
|
||||
{"cve": {"id": "CVE-2024-0001"}},
|
||||
{"notCve": {}},
|
||||
{"cve": {"id": "CVE-2024-0002"}}
|
||||
]
|
||||
}
|
||||
""";
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var results = new List<int>();
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
results.Add(advisories.Count);
|
||||
}
|
||||
|
||||
// Assert
|
||||
results.Distinct().Should().HaveCount(1,
|
||||
"error handling should be deterministic");
|
||||
results[0].Should().Be(2, "should consistently skip invalid entries");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Null/Empty Input Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_NullDocument_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => NvdMapper.Map(null!, sourceDoc, FixedRecordedAt);
|
||||
act.Should().Throw<ArgumentNullException>()
|
||||
.WithParameterName("document");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_NullSourceDocument_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var json = """{"vulnerabilities": []}""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => NvdMapper.Map(document, null!, FixedRecordedAt);
|
||||
act.Should().Throw<ArgumentNullException>()
|
||||
.WithParameterName("sourceDocument");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Malformed JSON Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Parse_MalformedJson_ThrowsJsonException()
|
||||
{
|
||||
// Arrange
|
||||
var malformedJson = "{ invalid json }";
|
||||
|
||||
// Act & Assert
|
||||
var act = () => JsonDocument.Parse(malformedJson);
|
||||
act.Should().Throw<JsonException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Parse_TruncatedJson_ThrowsJsonException()
|
||||
{
|
||||
// Arrange
|
||||
var truncatedJson = """{"vulnerabilities": [{"cve": {"id": "CVE-2024""";
|
||||
|
||||
// Act & Assert
|
||||
var act = () => JsonDocument.Parse(truncatedJson);
|
||||
act.Should().Throw<JsonException>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Case Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_VeryLargeVulnerabilitiesArray_HandlesGracefully()
|
||||
{
|
||||
// Arrange - Create array with 1000 minimal vulnerabilities
|
||||
var vulnerabilities = string.Join(",",
|
||||
Enumerable.Range(1, 1000).Select(i => $"{{\"cve\": {{\"id\": \"CVE-2024-{i:D4}\"}}}}"));
|
||||
var json = $"{{\"vulnerabilities\": [{vulnerabilities}]}}";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1000, "should handle large arrays");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_DescriptionWithSpecialCharacters_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0001",
|
||||
"descriptions": [
|
||||
{
|
||||
"lang": "en",
|
||||
"value": "Test <script>alert('xss')</script> & \"quotes\" \n\t special chars 日本語"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1);
|
||||
advisories[0].Summary.Should().Contain("<script>", "special characters should be preserved");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Lane", "Unit")]
|
||||
[Trait("Category", "Resilience")]
|
||||
public void Map_VeryLongDescription_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var longDescription = new string('x', 100_000); // 100KB description
|
||||
var json = $$"""
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": {
|
||||
"id": "CVE-2024-0001",
|
||||
"descriptions": [{"lang": "en", "value": "{{longDescription}}"}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var sourceDoc = CreateTestDocumentRecord();
|
||||
|
||||
// Act
|
||||
var advisories = NvdMapper.Map(document, sourceDoc, FixedRecordedAt);
|
||||
|
||||
// Assert
|
||||
advisories.Should().HaveCount(1, "should handle very long descriptions");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static DocumentRecord CreateTestDocumentRecord() =>
|
||||
new(
|
||||
Id: Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"),
|
||||
SourceName: NvdConnectorPlugin.SourceName,
|
||||
Uri: "https://services.nvd.nist.gov/rest/json/cves/2.0",
|
||||
FetchedAt: FixedRecordedAt,
|
||||
Sha256: "sha256-test",
|
||||
Status: "completed",
|
||||
ContentType: "application/json",
|
||||
Headers: null,
|
||||
Metadata: null,
|
||||
Etag: null,
|
||||
LastModified: FixedRecordedAt,
|
||||
PayloadId: null);
|
||||
}
|
||||
@@ -11,8 +11,16 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Nvd/StellaOps.Concelier.Connector.Nvd.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Canonical.Json/StellaOps.Canonical.Json.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Nvd/Fixtures/*.json" CopyToOutputDirectory="Always" />
|
||||
<None Include="Expected/*.json" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user