Rename Concelier Source modules to Connector

This commit is contained in:
2025-10-18 20:11:18 +03:00
parent 0137856fdb
commit 6524626230
789 changed files with 1489 additions and 1489 deletions

View File

@@ -0,0 +1,182 @@
{
"advisoryKey": "CVE-2025-4242",
"affectedPackages": [
{
"type": "cpe",
"identifier": "cpe:2.3:a:conflict:package:1.0:*:*:*:*:*:*:*",
"platform": null,
"versionRanges": [
{
"fixedVersion": "1.4",
"introducedVersion": "1.0",
"lastAffectedVersion": "1.0",
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": ">=1.0 <1.4 ==1.0",
"exactValue": "1.0.0",
"fixed": "1.4.0",
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": "1.0.0",
"lastAffectedInclusive": true,
"style": "exact"
},
"vendorExtensions": {
"versionStartIncluding": "1.0",
"versionEndExcluding": "1.4",
"version": "1.0"
}
},
"provenance": {
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cve/2.0?cveId=CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">=1.0 <1.4 ==1.0",
"rangeKind": "cpe"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "1.0.0",
"notes": "nvd:CVE-2025-4242"
}
],
"statuses": [],
"provenance": [
{
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cve/2.0?cveId=CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"CVE-2025-4242"
],
"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": "2025-03-04T02: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-269",
"name": null,
"uri": "https://cwe.mitre.org/data/definitions/269.html",
"provenance": [
{
"source": "nvd",
"kind": "weakness",
"value": "CWE-269",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"cwes[]"
]
}
]
}
],
"description": "NVD baseline summary for conflict-package allowing container escape.",
"exploitKnown": false,
"language": "en",
"modified": "2025-03-03T09:45:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/rest/json/cve/2.0?cveId=CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-03T10:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "nvd",
"kind": "mapping",
"value": "CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2025-03-01T10:15:00+00:00",
"references": [
{
"kind": "weakness",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/269.html",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "CWE-269",
"summary": null,
"url": "https://cwe.mitre.org/data/definitions/269.html"
},
{
"kind": "vendor advisory",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "NVD",
"summary": null,
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242"
}
],
"severity": "critical",
"summary": "NVD baseline summary for conflict-package allowing container escape.",
"title": "CVE-2025-4242"
}

View File

@@ -0,0 +1,108 @@
{
"advisoryKey": "GHSA-credit-parity",
"affectedPackages": [],
"aliases": [
"CVE-2025-5555",
"GHSA-credit-parity"
],
"credits": [
{
"displayName": "Bob Maintainer",
"role": "remediation_developer",
"contacts": [
"https://github.com/acme/bob-maintainer"
],
"provenance": {
"source": "ghsa",
"kind": "credit",
"value": "ghsa:bob-maintainer",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"credits[]"
]
}
},
{
"displayName": "Alice Researcher",
"role": "reporter",
"contacts": [
"mailto:alice.researcher@example.com"
],
"provenance": {
"source": "ghsa",
"kind": "credit",
"value": "ghsa:alice-researcher",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"credits[]"
]
}
}
],
"cvssMetrics": [],
"exploitKnown": false,
"language": "en",
"modified": "2025-10-10T12:00:00+00:00",
"provenance": [
{
"source": "ghsa",
"kind": "document",
"value": "security/advisories/GHSA-credit-parity",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "ghsa",
"kind": "mapping",
"value": "GHSA-credit-parity",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2025-10-09T18:30:00+00:00",
"references": [
{
"kind": "patch",
"provenance": {
"source": "ghsa",
"kind": "reference",
"value": "https://example.com/ghsa/patch",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": null,
"summary": null,
"url": "https://example.com/ghsa/patch"
},
{
"kind": "advisory",
"provenance": {
"source": "ghsa",
"kind": "reference",
"value": "https://github.com/advisories/GHSA-credit-parity",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": null,
"summary": null,
"url": "https://github.com/advisories/GHSA-credit-parity"
}
],
"severity": "medium",
"summary": "Credit parity regression fixture",
"title": "Credit parity regression fixture"
}

View File

@@ -0,0 +1,108 @@
{
"advisoryKey": "CVE-2025-5555",
"affectedPackages": [],
"aliases": [
"CVE-2025-5555",
"GHSA-credit-parity"
],
"credits": [
{
"displayName": "Bob Maintainer",
"role": "remediation_developer",
"contacts": [
"https://github.com/acme/bob-maintainer"
],
"provenance": {
"source": "nvd",
"kind": "credit",
"value": "nvd:bob-maintainer",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"credits[]"
]
}
},
{
"displayName": "Alice Researcher",
"role": "reporter",
"contacts": [
"mailto:alice.researcher@example.com"
],
"provenance": {
"source": "nvd",
"kind": "credit",
"value": "nvd:alice-researcher",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"credits[]"
]
}
}
],
"cvssMetrics": [],
"exploitKnown": false,
"language": "en",
"modified": "2025-10-10T12:00:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/vuln/detail/CVE-2025-5555",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "nvd",
"kind": "mapping",
"value": "CVE-2025-5555",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2025-10-09T18:30:00+00:00",
"references": [
{
"kind": "report",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://example.com/nvd/reference",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": null,
"summary": null,
"url": "https://example.com/nvd/reference"
},
{
"kind": "advisory",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://services.nvd.nist.gov/vuln/detail/CVE-2025-5555",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": null,
"summary": null,
"url": "https://services.nvd.nist.gov/vuln/detail/CVE-2025-5555"
}
],
"severity": "medium",
"summary": "Credit parity regression fixture",
"title": "Credit parity regression fixture"
}

View File

@@ -0,0 +1,108 @@
{
"advisoryKey": "GHSA-credit-parity",
"affectedPackages": [],
"aliases": [
"CVE-2025-5555",
"GHSA-credit-parity"
],
"credits": [
{
"displayName": "Bob Maintainer",
"role": "remediation_developer",
"contacts": [
"https://github.com/acme/bob-maintainer"
],
"provenance": {
"source": "osv",
"kind": "credit",
"value": "osv:bob-maintainer",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"credits[]"
]
}
},
{
"displayName": "Alice Researcher",
"role": "reporter",
"contacts": [
"mailto:alice.researcher@example.com"
],
"provenance": {
"source": "osv",
"kind": "credit",
"value": "osv:alice-researcher",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"credits[]"
]
}
}
],
"cvssMetrics": [],
"exploitKnown": false,
"language": "en",
"modified": "2025-10-10T12:00:00+00:00",
"provenance": [
{
"source": "osv",
"kind": "document",
"value": "https://osv.dev/vulnerability/GHSA-credit-parity",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "osv",
"kind": "mapping",
"value": "GHSA-credit-parity",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2025-10-09T18:30:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "osv",
"kind": "reference",
"value": "https://github.com/advisories/GHSA-credit-parity",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": null,
"summary": null,
"url": "https://github.com/advisories/GHSA-credit-parity"
},
{
"kind": "advisory",
"provenance": {
"source": "osv",
"kind": "reference",
"value": "https://osv.dev/vulnerability/GHSA-credit-parity",
"decisionReason": null,
"recordedAt": "2025-10-10T15:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": null,
"summary": null,
"url": "https://osv.dev/vulnerability/GHSA-credit-parity"
}
],
"severity": "medium",
"summary": "Credit parity regression fixture",
"title": "Credit parity regression fixture"
}

View File

@@ -0,0 +1,6 @@
{
"resultsPerPage": 1,
"startIndex": 0,
"totalResults": 1,
"vulnerabilities": "this-should-be-an-array"
}

View File

@@ -0,0 +1,69 @@
{
"resultsPerPage": 2,
"startIndex": 0,
"totalResults": 5,
"vulnerabilities": [
{
"cve": {
"id": "CVE-2024-1000",
"sourceIdentifier": "nvd@nist.gov",
"published": "2024-02-01T10:00:00Z",
"lastModified": "2024-02-02T10:00:00Z",
"descriptions": [
{ "lang": "en", "value": "Multipage vulnerability one." }
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"baseScore": 9.8,
"baseSeverity": "CRITICAL"
}
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_a:1.0:*:*:*:*:*:*:*" }
]
}
]
}
}
},
{
"cve": {
"id": "CVE-2024-1001",
"sourceIdentifier": "nvd@nist.gov",
"published": "2024-02-01T11:00:00Z",
"lastModified": "2024-02-02T11:00:00Z",
"descriptions": [
{ "lang": "en", "value": "Multipage vulnerability two." }
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:P/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:L",
"baseScore": 5.1,
"baseSeverity": "MEDIUM"
}
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_b:2.0:*:*:*:*:*:*:*" }
]
}
]
}
}
}
]
}

View File

@@ -0,0 +1,69 @@
{
"resultsPerPage": 2,
"startIndex": 2,
"totalResults": 5,
"vulnerabilities": [
{
"cve": {
"id": "CVE-2024-1002",
"sourceIdentifier": "nvd@nist.gov",
"published": "2024-02-01T12:00:00Z",
"lastModified": "2024-02-02T12:00:00Z",
"descriptions": [
{ "lang": "en", "value": "Multipage vulnerability three." }
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
"baseScore": 3.1,
"baseSeverity": "LOW"
}
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_c:3.0:*:*:*:*:*:*:*" }
]
}
]
}
}
},
{
"cve": {
"id": "CVE-2024-1003",
"sourceIdentifier": "nvd@nist.gov",
"published": "2024-02-01T13:00:00Z",
"lastModified": "2024-02-02T13:00:00Z",
"descriptions": [
{ "lang": "en", "value": "Multipage vulnerability four." }
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:M/I:L/A:L",
"baseScore": 7.4,
"baseSeverity": "HIGH"
}
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_d:4.0:*:*:*:*:*:*:*" }
]
}
]
}
}
}
]
}

View File

@@ -0,0 +1,38 @@
{
"resultsPerPage": 2,
"startIndex": 4,
"totalResults": 5,
"vulnerabilities": [
{
"cve": {
"id": "CVE-2024-1004",
"sourceIdentifier": "nvd@nist.gov",
"published": "2024-02-01T14:00:00Z",
"lastModified": "2024-02-02T14:00:00Z",
"descriptions": [
{ "lang": "en", "value": "Multipage vulnerability five." }
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L",
"baseScore": 7.9,
"baseSeverity": "HIGH"
}
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_e:5.0:*:*:*:*:*:*:*" }
]
}
]
}
}
}
]
}

View File

@@ -0,0 +1,101 @@
{
"resultsPerPage": 2000,
"startIndex": 0,
"totalResults": 2,
"vulnerabilities": [
{
"cve": {
"id": "CVE-2024-0001",
"sourceIdentifier": "nvd@nist.gov",
"published": "2024-01-01T10:00:00Z",
"lastModified": "2024-01-02T10:00:00Z",
"descriptions": [
{ "lang": "en", "value": "Example vulnerability one." }
],
"references": [
{
"url": "https://vendor.example.com/advisories/0001",
"source": "Vendor",
"tags": ["Vendor Advisory"]
}
],
"weaknesses": [
{
"description": [
{ "lang": "en", "value": "CWE-79" },
{ "lang": "en", "value": "Improper Neutralization of Input" }
]
}
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"baseScore": 9.8,
"baseSeverity": "CRITICAL"
}
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*" }
]
}
]
}
}
},
{
"cve": {
"id": "CVE-2024-0002",
"sourceIdentifier": "nvd@nist.gov",
"published": "2024-01-01T11:00:00Z",
"lastModified": "2024-01-02T11:00:00Z",
"descriptions": [
{ "lang": "fr", "value": "Description française" },
{ "lang": "en", "value": "Example vulnerability two." }
],
"references": [
{
"url": "https://cisa.example.gov/alerts/0002",
"source": "CISA",
"tags": ["US Government Resource"]
}
],
"weaknesses": [
{
"description": [
{ "lang": "en", "value": "CWE-89" },
{ "lang": "en", "value": "SQL Injection" }
]
}
],
"metrics": {
"cvssMetricV30": [
{
"cvssData": {
"vectorString": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"baseScore": 4.6,
"baseSeverity": "MEDIUM"
}
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*" },
{ "vulnerable": false, "criteria": "cpe:2.3:a:example:product_two:2.1:*:*:*:*:*:*:*" }
]
}
]
}
}
}
]
}

View File

@@ -0,0 +1,45 @@
{
"resultsPerPage": 2000,
"startIndex": 0,
"totalResults": 1,
"vulnerabilities": [
{
"cve": {
"id": "CVE-2024-0003",
"sourceIdentifier": "nvd@nist.gov",
"published": "2024-01-01T12:00:00Z",
"lastModified": "2024-01-02T12:00:00Z",
"descriptions": [
{ "lang": "en", "value": "Example vulnerability three." }
],
"references": [
{
"url": "https://example.org/patches/0003",
"source": "Vendor",
"tags": ["Patch"]
}
],
"metrics": {
"cvssMetricV2": [
{
"cvssData": {
"vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P",
"baseScore": 6.8,
"baseSeverity": "MEDIUM"
}
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_three:3.5:*:*:*:*:*:*:*" }
]
}
]
}
}
}
]
}

View File

@@ -0,0 +1,51 @@
{
"resultsPerPage": 2000,
"startIndex": 0,
"totalResults": 1,
"vulnerabilities": [
{
"cve": {
"id": "CVE-2024-0001",
"sourceIdentifier": "nvd@nist.gov",
"published": "2024-01-01T10:00:00Z",
"lastModified": "2024-01-03T12:00:00Z",
"descriptions": [
{ "lang": "en", "value": "Example vulnerability one updated." }
],
"references": [
{
"url": "https://vendor.example.com/advisories/0001",
"source": "Vendor",
"tags": ["Vendor Advisory"]
},
{
"url": "https://kb.example.com/articles/0001",
"source": "KnowledgeBase",
"tags": ["Third Party Advisory"]
}
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"baseScore": 8.8,
"baseSeverity": "HIGH"
}
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*" },
{ "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.1:*:*:*:*:*:*:*" }
]
}
]
}
}
}
]
}

View File

@@ -0,0 +1,103 @@
using System.Text.Json;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Nvd.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace StellaOps.Concelier.Connector.Nvd.Tests;
public sealed class NvdConflictFixtureTests
{
[Fact]
public void ConflictFixture_MatchesSnapshot()
{
const string payload = """
{
"vulnerabilities": [
{
"cve": {
"id": "CVE-2025-4242",
"published": "2025-03-01T10:15:00Z",
"lastModified": "2025-03-03T09:45:00Z",
"descriptions": [
{ "lang": "en", "value": "NVD baseline summary for conflict-package allowing container escape." }
],
"references": [
{
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242",
"source": "NVD",
"tags": ["Vendor Advisory"]
}
],
"weaknesses": [
{
"description": [
{ "lang": "en", "value": "CWE-269" }
]
}
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"baseScore": 9.8,
"baseSeverity": "CRITICAL"
},
"exploitabilityScore": 3.9,
"impactScore": 5.9
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{
"criteria": "cpe:2.3:a:conflict:package:1.0:*:*:*:*:*:*:*",
"vulnerable": true,
"versionStartIncluding": "1.0",
"versionEndExcluding": "1.4"
}
]
}
]
}
}
}
]
}
""";
using var document = JsonDocument.Parse(payload);
var sourceDocument = new DocumentRecord(
Id: Guid.Parse("1a6a0700-2dd0-4f69-bb37-64ca77e51c91"),
SourceName: NvdConnectorPlugin.SourceName,
Uri: "https://services.nvd.nist.gov/rest/json/cve/2.0?cveId=CVE-2025-4242",
FetchedAt: new DateTimeOffset(2025, 3, 3, 10, 0, 0, TimeSpan.Zero),
Sha256: "sha256-nvd-conflict-fixture",
Status: "completed",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: "\"etag-nvd-conflict\"",
LastModified: new DateTimeOffset(2025, 3, 3, 9, 45, 0, TimeSpan.Zero),
GridFsId: null);
var advisories = NvdMapper.Map(document, sourceDocument, new DateTimeOffset(2025, 3, 4, 2, 0, 0, TimeSpan.Zero));
var advisory = Assert.Single(advisories);
var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n").TrimEnd();
var expectedPath = Path.Combine(AppContext.BaseDirectory, "Nvd", "Fixtures", "conflict-nvd.canonical.json");
var expected = File.ReadAllText(expectedPath).Replace("\r\n", "\n").TrimEnd();
if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(AppContext.BaseDirectory, "Nvd", "Fixtures", "conflict-nvd.canonical.actual.json");
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
File.WriteAllText(actualPath, snapshot);
}
Assert.Equal(expected, snapshot);
}
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Bson;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Nvd;
using StellaOps.Concelier.Connector.Nvd.Configuration;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Testing;
using StellaOps.Concelier.Testing;
using System.Net;
namespace StellaOps.Concelier.Connector.Nvd.Tests;
[Collection("mongo-fixture")]
public sealed class NvdConnectorHarnessTests : IAsyncLifetime
{
private readonly ConnectorTestHarness _harness;
public NvdConnectorHarnessTests(MongoIntegrationFixture fixture)
{
_harness = new ConnectorTestHarness(fixture, new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero), NvdOptions.HttpClientName);
}
[Fact]
public async Task FetchAsync_MultiPagePersistsStartIndexMetadata()
{
await _harness.ResetAsync();
var options = new NvdOptions
{
BaseEndpoint = new Uri("https://nvd.example.test/api"),
WindowSize = TimeSpan.FromHours(1),
WindowOverlap = TimeSpan.FromMinutes(5),
InitialBackfill = TimeSpan.FromHours(2),
};
var timeProvider = _harness.TimeProvider;
var handler = _harness.Handler;
var windowStart = timeProvider.GetUtcNow() - options.InitialBackfill;
var windowEnd = windowStart + options.WindowSize;
var firstUri = BuildRequestUri(options, windowStart, windowEnd);
var secondUri = BuildRequestUri(options, windowStart, windowEnd, startIndex: 2);
var thirdUri = BuildRequestUri(options, windowStart, windowEnd, startIndex: 4);
handler.AddJsonResponse(firstUri, ReadFixture("nvd-multipage-1.json"));
handler.AddJsonResponse(secondUri, ReadFixture("nvd-multipage-2.json"));
handler.AddJsonResponse(thirdUri, ReadFixture("nvd-multipage-3.json"));
await _harness.EnsureServiceProviderAsync(services =>
{
services.AddNvdConnector(opts =>
{
opts.BaseEndpoint = options.BaseEndpoint;
opts.WindowSize = options.WindowSize;
opts.WindowOverlap = options.WindowOverlap;
opts.InitialBackfill = options.InitialBackfill;
});
});
var provider = _harness.ServiceProvider;
var connector = new NvdConnectorPlugin().Create(provider);
await connector.FetchAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var firstDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, firstUri.ToString(), CancellationToken.None);
Assert.NotNull(firstDocument);
Assert.Equal("0", firstDocument!.Metadata["startIndex"]);
var secondDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, secondUri.ToString(), CancellationToken.None);
Assert.NotNull(secondDocument);
Assert.Equal("2", secondDocument!.Metadata["startIndex"]);
var thirdDocument = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, thirdUri.ToString(), CancellationToken.None);
Assert.NotNull(thirdDocument);
Assert.Equal("4", thirdDocument!.Metadata["startIndex"]);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pending)
? pending.AsBsonArray
: new BsonArray();
Assert.Equal(3, pendingDocuments.Count);
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => _harness.ResetAsync();
private static Uri BuildRequestUri(NvdOptions options, DateTimeOffset start, DateTimeOffset end, int startIndex = 0)
{
var builder = new UriBuilder(options.BaseEndpoint);
var parameters = new Dictionary<string, string>
{
["lastModifiedStartDate"] = start.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
["lastModifiedEndDate"] = end.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
["resultsPerPage"] = "2000",
};
if (startIndex > 0)
{
parameters["startIndex"] = startIndex.ToString(CultureInfo.InvariantCulture);
}
builder.Query = string.Join("&", parameters.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
return builder.Uri;
}
private static string ReadFixture(string filename)
{
var baseDirectory = AppContext.BaseDirectory;
var primary = Path.Combine(baseDirectory, "Source", "Nvd", "Fixtures", filename);
if (File.Exists(primary))
{
return File.ReadAllText(primary);
}
var secondary = Path.Combine(baseDirectory, "Nvd", "Fixtures", filename);
if (File.Exists(secondary))
{
return File.ReadAllText(secondary);
}
throw new FileNotFoundException($"Fixture '{filename}' was not found in the test output directory.");
}
}

View File

@@ -0,0 +1,653 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Nvd;
using StellaOps.Concelier.Connector.Nvd.Configuration;
using StellaOps.Concelier.Connector.Nvd.Internal;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Storage.Mongo.ChangeHistory;
using StellaOps.Concelier.Testing;
namespace StellaOps.Concelier.Connector.Nvd.Tests;
[Collection("mongo-fixture")]
public sealed class NvdConnectorTests : IAsyncLifetime
{
private readonly MongoIntegrationFixture _fixture;
private FakeTimeProvider _timeProvider;
private readonly DateTimeOffset _initialNow;
private readonly CannedHttpMessageHandler _handler;
private ServiceProvider? _serviceProvider;
public NvdConnectorTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
_initialNow = new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero);
_timeProvider = new FakeTimeProvider(_initialNow);
_handler = new CannedHttpMessageHandler();
}
[Fact]
public async Task FetchParseMap_FlowProducesCanonicalAdvisories()
{
await ResetDatabaseAsync();
var options = new NvdOptions
{
BaseEndpoint = new Uri("https://nvd.example.test/api"),
WindowSize = TimeSpan.FromHours(1),
WindowOverlap = TimeSpan.FromMinutes(5),
InitialBackfill = TimeSpan.FromHours(2),
};
var window1Start = _timeProvider.GetUtcNow() - options.InitialBackfill;
var window1End = window1Start + options.WindowSize;
_handler.AddJsonResponse(BuildRequestUri(options, window1Start, window1End), ReadFixture("nvd-window-1.json"));
await EnsureServiceProviderAsync(options);
var provider = _serviceProvider!;
var connector = new NvdConnectorPlugin().Create(provider);
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0001");
Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0002");
var cve1 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0001");
var package1 = Assert.Single(cve1.AffectedPackages);
var range1 = Assert.Single(package1.VersionRanges);
Assert.Equal("cpe", range1.RangeKind);
Assert.Equal("1.0", range1.IntroducedVersion);
Assert.Null(range1.FixedVersion);
Assert.Equal("1.0", range1.LastAffectedVersion);
Assert.Equal("==1.0", range1.RangeExpression);
Assert.NotNull(range1.Primitives);
Assert.Equal("1.0", range1.Primitives!.VendorExtensions!["version"]);
Assert.Contains(cve1.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-79");
var cvss1 = Assert.Single(cve1.CvssMetrics);
Assert.Equal("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", cvss1.Provenance.Value);
var cve2 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0002");
var package2 = Assert.Single(cve2.AffectedPackages);
var range2 = Assert.Single(package2.VersionRanges);
Assert.Equal("cpe", range2.RangeKind);
Assert.Equal("2.0", range2.IntroducedVersion);
Assert.Null(range2.FixedVersion);
Assert.Equal("2.0", range2.LastAffectedVersion);
Assert.Equal("==2.0", range2.RangeExpression);
Assert.NotNull(range2.Primitives);
Assert.Equal("2.0", range2.Primitives!.VendorExtensions!["version"]);
Assert.Contains(cve2.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-89");
var cvss2 = Assert.Single(cve2.CvssMetrics);
Assert.Equal("CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L", cvss2.Provenance.Value);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursorDocument = state!.Cursor;
Assert.NotNull(cursorDocument);
var lastWindowEnd = cursorDocument.TryGetValue("windowEnd", out var endValue) ? ReadDateTime(endValue) : (DateTimeOffset?)null;
Assert.Equal(window1End.UtcDateTime, lastWindowEnd?.UtcDateTime);
_timeProvider.Advance(TimeSpan.FromHours(1));
var now = _timeProvider.GetUtcNow();
var startCandidate = (lastWindowEnd ?? window1End) - options.WindowOverlap;
var backfillLimit = now - options.InitialBackfill;
var window2Start = startCandidate < backfillLimit ? backfillLimit : startCandidate;
var window2End = window2Start + options.WindowSize;
if (window2End > now)
{
window2End = now;
}
_handler.AddJsonResponse(BuildRequestUri(options, window2Start, window2End), ReadFixture("nvd-window-2.json"));
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
Assert.Equal(3, advisories.Count);
Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0003");
var cve3 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0003");
var package3 = Assert.Single(cve3.AffectedPackages);
var range3 = Assert.Single(package3.VersionRanges);
Assert.Equal("3.5", range3.IntroducedVersion);
Assert.Equal("3.5", range3.LastAffectedVersion);
Assert.Equal("==3.5", range3.RangeExpression);
Assert.NotNull(range3.Primitives);
Assert.Equal("3.5", range3.Primitives!.VendorExtensions!["version"]);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var finalState = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(finalState);
var pendingDocuments = finalState!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
? pendingDocs.AsBsonArray
: new BsonArray();
Assert.Empty(pendingDocuments);
}
[Fact]
public async Task FetchAsync_MultiPageWindowFetchesAllPages()
{
await ResetDatabaseAsync();
var options = new NvdOptions
{
BaseEndpoint = new Uri("https://nvd.example.test/api"),
WindowSize = TimeSpan.FromHours(1),
WindowOverlap = TimeSpan.FromMinutes(5),
InitialBackfill = TimeSpan.FromHours(2),
};
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
var windowEnd = windowStart + options.WindowSize;
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd), ReadFixture("nvd-multipage-1.json"));
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 2), ReadFixture("nvd-multipage-2.json"));
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
await EnsureServiceProviderAsync(options);
var provider = _serviceProvider!;
var connector = new NvdConnectorPlugin().Create(provider);
await connector.FetchAsync(provider, CancellationToken.None);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pendingDocuments = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
? pendingDocs.AsBsonArray.Select(v => Guid.Parse(v.AsString)).ToArray()
: Array.Empty<Guid>();
Assert.Equal(3, pendingDocuments.Length);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
var advisoryKeys = advisories.Select(advisory => advisory.AdvisoryKey).OrderBy(k => k).ToArray();
Assert.Equal(new[] { "CVE-2024-1000", "CVE-2024-1001", "CVE-2024-1002", "CVE-2024-1003", "CVE-2024-1004" }, advisoryKeys);
}
[Fact]
public async Task Observability_RecordsCountersForSuccessfulFlow()
{
await ResetDatabaseAsync();
var options = new NvdOptions
{
BaseEndpoint = new Uri("https://nvd.example.test/api"),
WindowSize = TimeSpan.FromHours(1),
WindowOverlap = TimeSpan.FromMinutes(5),
InitialBackfill = TimeSpan.FromHours(2),
};
using var collector = new MetricCollector(NvdDiagnostics.MeterName);
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
var windowEnd = windowStart + options.WindowSize;
var handler = new CannedHttpMessageHandler();
handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd), ReadFixture("nvd-multipage-1.json"));
handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 2), ReadFixture("nvd-multipage-2.json"));
handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd, startIndex: 4), ReadFixture("nvd-multipage-3.json"));
await using var provider = await CreateServiceProviderAsync(options, handler);
var connector = new NvdConnectorPlugin().Create(provider);
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
Assert.Equal(3, collector.GetValue("nvd.fetch.attempts"));
Assert.Equal(3, collector.GetValue("nvd.fetch.documents"));
Assert.Equal(0, collector.GetValue("nvd.fetch.failures"));
Assert.Equal(0, collector.GetValue("nvd.fetch.unchanged"));
Assert.Equal(3, collector.GetValue("nvd.parse.success"));
Assert.Equal(0, collector.GetValue("nvd.parse.failures"));
Assert.Equal(0, collector.GetValue("nvd.parse.quarantine"));
Assert.Equal(5, collector.GetValue("nvd.map.success"));
}
[Fact]
public async Task ChangeHistory_RecordsDifferencesForModifiedCve()
{
await ResetDatabaseAsync();
var options = new NvdOptions
{
BaseEndpoint = new Uri("https://nvd.example.test/api"),
WindowSize = TimeSpan.FromHours(1),
WindowOverlap = TimeSpan.FromMinutes(5),
InitialBackfill = TimeSpan.FromHours(2),
};
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
var windowEnd = windowStart + options.WindowSize;
_handler.AddJsonResponse(BuildRequestUri(options, windowStart, windowEnd), ReadFixture("nvd-window-1.json"));
await EnsureServiceProviderAsync(options);
var provider = _serviceProvider!;
var connector = new NvdConnectorPlugin().Create(provider);
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var historyStore = provider.GetRequiredService<IChangeHistoryStore>();
var historyEntries = await historyStore.GetRecentAsync("nvd", "CVE-2024-0001", 5, CancellationToken.None);
Assert.Empty(historyEntries);
_timeProvider.Advance(TimeSpan.FromHours(2));
var now = _timeProvider.GetUtcNow();
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursorDocument = state!.Cursor;
var lastWindowEnd = cursorDocument.TryGetValue("windowEnd", out var endValue) ? ReadDateTime(endValue) : (DateTimeOffset?)null;
var startCandidate = (lastWindowEnd ?? windowEnd) - options.WindowOverlap;
var backfillLimit = now - options.InitialBackfill;
var window2Start = startCandidate < backfillLimit ? backfillLimit : startCandidate;
var window2End = window2Start + options.WindowSize;
if (window2End > now)
{
window2End = now;
}
_handler.AddJsonResponse(BuildRequestUri(options, window2Start, window2End), ReadFixture("nvd-window-update.json"));
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var updatedAdvisory = await advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
Assert.NotNull(updatedAdvisory);
Assert.Equal("high", updatedAdvisory!.Severity);
historyEntries = await historyStore.GetRecentAsync("nvd", "CVE-2024-0001", 5, CancellationToken.None);
Assert.NotEmpty(historyEntries);
var latest = historyEntries[0];
Assert.Equal("nvd", latest.SourceName);
Assert.Equal("CVE-2024-0001", latest.AdvisoryKey);
Assert.NotNull(latest.PreviousHash);
Assert.NotEqual(latest.PreviousHash, latest.CurrentHash);
Assert.Contains(latest.Changes, change => change.Field == "severity" && change.ChangeType == "Modified");
Assert.Contains(latest.Changes, change => change.Field == "references" && change.ChangeType == "Modified");
}
[Fact]
public async Task ParseAsync_InvalidSchema_QuarantinesDocumentAndEmitsMetric()
{
await ResetDatabaseAsync();
var options = new NvdOptions
{
BaseEndpoint = new Uri("https://nvd.example.test/api"),
WindowSize = TimeSpan.FromHours(1),
WindowOverlap = TimeSpan.FromMinutes(5),
InitialBackfill = TimeSpan.FromHours(2),
};
using var collector = new MetricCollector(NvdDiagnostics.MeterName);
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
var windowEnd = windowStart + options.WindowSize;
var requestUri = BuildRequestUri(options, windowStart, windowEnd);
_handler.AddJsonResponse(requestUri, ReadFixture("nvd-invalid-schema.json"));
await EnsureServiceProviderAsync(options);
var provider = _serviceProvider!;
var connector = new NvdConnectorPlugin().Create(provider);
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var document = await documentStore.FindBySourceAndUriAsync(NvdConnectorPlugin.SourceName, requestUri.ToString(), CancellationToken.None);
Assert.NotNull(document);
Assert.Equal(DocumentStatuses.Failed, document!.Status);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pendingDocs = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue) ? pendingDocsValue.AsBsonArray : new BsonArray();
Assert.Empty(pendingDocs);
var pendingMappings = state.Cursor.TryGetValue("pendingMappings", out var pendingMappingsValue) ? pendingMappingsValue.AsBsonArray : new BsonArray();
Assert.Empty(pendingMappings);
Assert.Equal(1, collector.GetValue("nvd.fetch.documents"));
Assert.Equal(0, collector.GetValue("nvd.parse.success"));
Assert.Equal(1, collector.GetValue("nvd.parse.quarantine"));
Assert.Equal(0, collector.GetValue("nvd.map.success"));
}
[Fact]
public async Task ResetDatabase_IsolatesRuns()
{
await ResetDatabaseAsync();
var options = new NvdOptions
{
BaseEndpoint = new Uri("https://nvd.example.test/api"),
WindowSize = TimeSpan.FromHours(1),
WindowOverlap = TimeSpan.FromMinutes(5),
InitialBackfill = TimeSpan.FromHours(2),
};
var start = _timeProvider.GetUtcNow() - options.InitialBackfill;
var end = start + options.WindowSize;
_handler.AddJsonResponse(BuildRequestUri(options, start, end), ReadFixture("nvd-window-1.json"));
await EnsureServiceProviderAsync(options);
var provider = _serviceProvider!;
var connector = new NvdConnectorPlugin().Create(provider);
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var firstRunKeys = (await advisoryStore.GetRecentAsync(10, CancellationToken.None))
.Select(advisory => advisory.AdvisoryKey)
.OrderBy(k => k)
.ToArray();
Assert.Equal(new[] { "CVE-2024-0001", "CVE-2024-0002" }, firstRunKeys);
await ResetDatabaseAsync();
options = new NvdOptions
{
BaseEndpoint = new Uri("https://nvd.example.test/api"),
WindowSize = TimeSpan.FromHours(1),
WindowOverlap = TimeSpan.FromMinutes(5),
InitialBackfill = TimeSpan.FromHours(2),
};
start = _timeProvider.GetUtcNow() - options.InitialBackfill;
end = start + options.WindowSize;
_handler.AddJsonResponse(BuildRequestUri(options, start, end), ReadFixture("nvd-window-2.json"));
await EnsureServiceProviderAsync(options);
provider = _serviceProvider!;
connector = new NvdConnectorPlugin().Create(provider);
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var secondRunKeys = (await advisoryStore.GetRecentAsync(10, CancellationToken.None))
.Select(advisory => advisory.AdvisoryKey)
.OrderBy(k => k)
.ToArray();
Assert.Equal(new[] { "CVE-2024-0003" }, secondRunKeys);
}
private async Task EnsureServiceProviderAsync(NvdOptions options)
{
if (_serviceProvider is not null)
{
return;
}
_serviceProvider = await CreateServiceProviderAsync(options, _handler);
}
[Fact]
public async Task Resume_CompletesPendingDocumentsAfterRestart()
{
await ResetDatabaseAsync();
var options = new NvdOptions
{
BaseEndpoint = new Uri("https://nvd.example.test/api"),
WindowSize = TimeSpan.FromHours(1),
WindowOverlap = TimeSpan.FromMinutes(5),
InitialBackfill = TimeSpan.FromHours(2),
};
var windowStart = _timeProvider.GetUtcNow() - options.InitialBackfill;
var windowEnd = windowStart + options.WindowSize;
var requestUri = BuildRequestUri(options, windowStart, windowEnd);
var fetchHandler = new CannedHttpMessageHandler();
fetchHandler.AddJsonResponse(requestUri, ReadFixture("nvd-window-1.json"));
Guid[] pendingDocumentIds;
await using (var fetchProvider = await CreateServiceProviderAsync(options, fetchHandler))
{
var connector = new NvdConnectorPlugin().Create(fetchProvider);
await connector.FetchAsync(fetchProvider, CancellationToken.None);
var stateRepository = fetchProvider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var pending = state!.Cursor.TryGetValue("pendingDocuments", out var value)
? value.AsBsonArray
: new BsonArray();
Assert.NotEmpty(pending);
pendingDocumentIds = pending.Select(v => Guid.Parse(v.AsString)).ToArray();
}
var resumeHandler = new CannedHttpMessageHandler();
await using (var resumeProvider = await CreateServiceProviderAsync(options, resumeHandler))
{
var resumeConnector = new NvdConnectorPlugin().Create(resumeProvider);
await resumeConnector.ParseAsync(resumeProvider, CancellationToken.None);
await resumeConnector.MapAsync(resumeProvider, CancellationToken.None);
var documentStore = resumeProvider.GetRequiredService<IDocumentStore>();
foreach (var documentId in pendingDocumentIds)
{
var document = await documentStore.FindAsync(documentId, CancellationToken.None);
Assert.NotNull(document);
Assert.Equal(DocumentStatuses.Mapped, document!.Status);
}
var advisoryStore = resumeProvider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
Assert.NotEmpty(advisories);
var stateRepository = resumeProvider.GetRequiredService<ISourceStateRepository>();
var finalState = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(finalState);
var cursor = finalState!.Cursor;
var finalPendingDocs = cursor.TryGetValue("pendingDocuments", out var pendingDocs) ? pendingDocs.AsBsonArray : new BsonArray();
Assert.Empty(finalPendingDocs);
var finalPendingMappings = cursor.TryGetValue("pendingMappings", out var pendingMappings) ? pendingMappings.AsBsonArray : new BsonArray();
Assert.Empty(finalPendingMappings);
}
}
private Task ResetDatabaseAsync()
{
return ResetDatabaseInternalAsync();
}
private async Task<ServiceProvider> CreateServiceProviderAsync(NvdOptions options, CannedHttpMessageHandler handler)
{
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
services.AddSingleton<TimeProvider>(_timeProvider);
services.AddSingleton(handler);
services.AddMongoStorage(storageOptions =>
{
storageOptions.ConnectionString = _fixture.Runner.ConnectionString;
storageOptions.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
storageOptions.CommandTimeout = TimeSpan.FromSeconds(5);
});
services.AddSourceCommon();
services.AddNvdConnector(configure: opts =>
{
opts.BaseEndpoint = options.BaseEndpoint;
opts.WindowSize = options.WindowSize;
opts.WindowOverlap = options.WindowOverlap;
opts.InitialBackfill = options.InitialBackfill;
});
services.Configure<HttpClientFactoryOptions>(NvdOptions.HttpClientName, builderOptions =>
{
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
{
builder.PrimaryHandler = handler;
});
});
var provider = services.BuildServiceProvider();
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
await bootstrapper.InitializeAsync(CancellationToken.None);
return provider;
}
private async Task ResetDatabaseInternalAsync()
{
if (_serviceProvider is not null)
{
if (_serviceProvider is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
_serviceProvider.Dispose();
}
_serviceProvider = null;
}
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
_handler.Clear();
_timeProvider = new FakeTimeProvider(_initialNow);
}
private sealed class MetricCollector : IDisposable
{
private readonly MeterListener _listener;
private readonly ConcurrentDictionary<string, long> _measurements = new(StringComparer.OrdinalIgnoreCase);
public MetricCollector(string meterName)
{
_listener = new MeterListener
{
InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name == meterName)
{
listener.EnableMeasurementEvents(instrument);
}
}
};
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
_measurements.AddOrUpdate(instrument.Name, measurement, (_, existing) => existing + measurement);
});
_listener.Start();
}
public long GetValue(string instrumentName)
=> _measurements.TryGetValue(instrumentName, out var value) ? value : 0;
public void Dispose()
{
_listener.Dispose();
}
}
private static Uri BuildRequestUri(NvdOptions options, DateTimeOffset start, DateTimeOffset end, int startIndex = 0)
{
var builder = new UriBuilder(options.BaseEndpoint);
var parameters = new Dictionary<string, string>
{
["lastModifiedStartDate"] = start.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
["lastModifiedEndDate"] = end.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"),
["resultsPerPage"] = "2000",
};
if (startIndex > 0)
{
parameters["startIndex"] = startIndex.ToString(CultureInfo.InvariantCulture);
}
builder.Query = string.Join("&", parameters.Select(static kvp => $"{System.Net.WebUtility.UrlEncode(kvp.Key)}={System.Net.WebUtility.UrlEncode(kvp.Value)}"));
return builder.Uri;
}
private static DateTimeOffset? ReadDateTime(BsonValue value)
{
return value.BsonType switch
{
BsonType.DateTime => DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc),
BsonType.String when DateTimeOffset.TryParse(value.AsString, out var parsed) => parsed.ToUniversalTime(),
_ => null,
};
}
private static string ReadFixture(string filename)
{
var baseDirectory = AppContext.BaseDirectory;
var primary = Path.Combine(baseDirectory, "Source", "Nvd", "Fixtures", filename);
if (File.Exists(primary))
{
return File.ReadAllText(primary);
}
var secondary = Path.Combine(baseDirectory, "Nvd", "Fixtures", filename);
if (File.Exists(secondary))
{
return File.ReadAllText(secondary);
}
throw new FileNotFoundException($"Fixture '{filename}' was not found in the test output directory.");
}
public Task InitializeAsync() => Task.CompletedTask;
public async Task DisposeAsync()
{
await ResetDatabaseInternalAsync();
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using StellaOps.Concelier.Core;
using StellaOps.Concelier.Exporter.Json;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Connector.Nvd.Tests.Nvd;
public sealed class NvdMergeExportParityTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
[Fact]
public async Task CanonicalMerge_PreservesCreditsAndReferences_ExporterMaintainsParity()
{
var ghsa = LoadFixture("credit-parity.ghsa.json");
var osv = LoadFixture("credit-parity.osv.json");
var nvd = LoadFixture("credit-parity.nvd.json");
var merger = new CanonicalMerger();
var result = merger.Merge("CVE-2025-5555", ghsa, nvd, osv);
var merged = result.Advisory;
Assert.NotNull(merged);
var creditKeys = merged!.Credits
.Select(static credit => $"{credit.Role}|{credit.DisplayName}|{string.Join("|", credit.Contacts.OrderBy(static c => c, StringComparer.Ordinal))}")
.ToHashSet(StringComparer.Ordinal);
Assert.Equal(2, creditKeys.Count);
Assert.Contains("reporter|Alice Researcher|mailto:alice.researcher@example.com", creditKeys);
Assert.Contains("remediation_developer|Bob Maintainer|https://github.com/acme/bob-maintainer", creditKeys);
var referenceUrls = merged.References.Select(static reference => reference.Url).ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.Equal(5, referenceUrls.Count);
Assert.Contains($"https://github.com/advisories/GHSA-credit-parity", referenceUrls);
Assert.Contains("https://example.com/ghsa/patch", referenceUrls);
Assert.Contains($"https://osv.dev/vulnerability/GHSA-credit-parity", referenceUrls);
Assert.Contains($"https://services.nvd.nist.gov/vuln/detail/CVE-2025-5555", referenceUrls);
Assert.Contains("https://example.com/nvd/reference", referenceUrls);
using var tempDirectory = new TempDirectory();
var options = new JsonExportOptions { OutputRoot = tempDirectory.Path };
var builder = new JsonExportSnapshotBuilder(options, new VulnListJsonExportPathResolver());
var exportResult = await builder.WriteAsync(new[] { merged }, new DateTimeOffset(2025, 10, 11, 0, 0, 0, TimeSpan.Zero));
Assert.Single(exportResult.Files);
var exportFile = exportResult.Files[0];
var exportPath = Path.Combine(exportResult.ExportDirectory, exportFile.RelativePath.Replace('/', Path.DirectorySeparatorChar));
Assert.True(File.Exists(exportPath));
var exported = JsonSerializer.Deserialize<Advisory>(await File.ReadAllTextAsync(exportPath), SerializerOptions);
Assert.NotNull(exported);
var exportedCredits = exported!.Credits
.Select(static credit => $"{credit.Role}|{credit.DisplayName}|{string.Join("|", credit.Contacts.OrderBy(static c => c, StringComparer.Ordinal))}")
.ToHashSet(StringComparer.Ordinal);
Assert.Equal(creditKeys, exportedCredits);
var exportedReferences = exported.References.Select(static reference => reference.Url).ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.Equal(referenceUrls, exportedReferences);
}
private static Advisory LoadFixture(string fileName)
{
var path = Path.Combine(AppContext.BaseDirectory, "Nvd", "Fixtures", fileName);
return JsonSerializer.Deserialize<Advisory>(File.ReadAllText(path), SerializerOptions)
?? throw new InvalidOperationException($"Failed to deserialize fixture '{fileName}'.");
}
private sealed class TempDirectory : IDisposable
{
public TempDirectory()
{
Path = Directory.CreateTempSubdirectory("nvd-merge-export").FullName;
}
public string Path { get; }
public void Dispose()
{
try
{
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
catch
{
// best effort cleanup
}
}
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Exporter.Json/StellaOps.Concelier.Exporter.Json.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Connector.Nvd/StellaOps.Concelier.Connector.Nvd.csproj" />
<ProjectReference Include="../StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Nvd/Fixtures/*.json" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>