Rename Concelier Source modules to Connector
This commit is contained in:
		| @@ -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" | ||||
| } | ||||
| @@ -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" | ||||
| } | ||||
| @@ -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" | ||||
| } | ||||
| @@ -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" | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "resultsPerPage": 1, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 1, | ||||
|   "vulnerabilities": "this-should-be-an-array" | ||||
| } | ||||
| @@ -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:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -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:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -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:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -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:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -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:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -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:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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."); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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> | ||||
		Reference in New Issue
	
	Block a user