Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"document": {
|
||||
"aggregate_severity": {
|
||||
"text": "Important"
|
||||
},
|
||||
"lang": "en",
|
||||
"notes": [
|
||||
{
|
||||
"category": "summary",
|
||||
"text": "An update fixes a critical kernel issue."
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"category": "self",
|
||||
"summary": "RHSA advisory",
|
||||
"url": "https://access.redhat.com/errata/RHSA-2025:0001"
|
||||
}
|
||||
],
|
||||
"title": "Red Hat Security Advisory: Example kernel update",
|
||||
"tracking": {
|
||||
"id": "RHSA-2025:0001",
|
||||
"initial_release_date": "2025-10-02T00:00:00+00:00",
|
||||
"current_release_date": "2025-10-03T00:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"product_tree": {
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_family",
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_name",
|
||||
"product": {
|
||||
"name": "Red Hat Enterprise Linux 8",
|
||||
"product_id": "8Base-RHEL-8",
|
||||
"product_identification_helper": {
|
||||
"cpe": "cpe:/o:redhat:enterprise_linux:8"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "product_release",
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_version",
|
||||
"product": {
|
||||
"name": "kernel-0:4.18.0-513.5.1.el8.x86_64",
|
||||
"product_id": "kernel-0:4.18.0-513.5.1.el8.x86_64",
|
||||
"product_identification_helper": {
|
||||
"purl": "pkg:rpm/redhat/kernel@4.18.0-513.5.1.el8?arch=x86_64"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": "CVE-2025-0001",
|
||||
"references": [
|
||||
{
|
||||
"category": "external",
|
||||
"summary": "CVE record",
|
||||
"url": "https://www.cve.org/CVERecord?id=CVE-2025-0001"
|
||||
}
|
||||
],
|
||||
"scores": [
|
||||
{
|
||||
"cvss_v3": {
|
||||
"baseScore": 9.8,
|
||||
"baseSeverity": "CRITICAL",
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"version": "3.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"product_status": {
|
||||
"fixed": [
|
||||
"8Base-RHEL-8:kernel-0:4.18.0-513.5.1.el8.x86_64"
|
||||
],
|
||||
"first_fixed": [
|
||||
"8Base-RHEL-8:kernel-0:4.18.0-513.5.1.el8.x86_64"
|
||||
],
|
||||
"known_affected": [
|
||||
"8Base-RHEL-8",
|
||||
"8Base-RHEL-8:kernel-0:4.18.0-500.1.0.el8.x86_64"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"document": {
|
||||
"aggregate_severity": {
|
||||
"text": "Moderate"
|
||||
},
|
||||
"lang": "en",
|
||||
"notes": [
|
||||
{
|
||||
"category": "summary",
|
||||
"text": "Second advisory covering unaffected packages."
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"category": "self",
|
||||
"summary": "RHSA advisory",
|
||||
"url": "https://access.redhat.com/errata/RHSA-2025:0002"
|
||||
}
|
||||
],
|
||||
"title": "Red Hat Security Advisory: Follow-up kernel status",
|
||||
"tracking": {
|
||||
"id": "RHSA-2025:0002",
|
||||
"initial_release_date": "2025-10-05T12:00:00+00:00",
|
||||
"current_release_date": "2025-10-05T12:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"product_tree": {
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_family",
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_name",
|
||||
"product": {
|
||||
"name": "Red Hat Enterprise Linux 9",
|
||||
"product_id": "9Base-RHEL-9",
|
||||
"product_identification_helper": {
|
||||
"cpe": "cpe:/o:redhat:enterprise_linux:9"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "product_release",
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_version",
|
||||
"product": {
|
||||
"name": "kernel-0:5.14.0-400.el9.x86_64",
|
||||
"product_id": "kernel-0:5.14.0-400.el9.x86_64",
|
||||
"product_identification_helper": {
|
||||
"purl": "pkg:rpm/redhat/kernel@5.14.0-400.el9?arch=x86_64"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": "CVE-2025-0002",
|
||||
"references": [
|
||||
{
|
||||
"category": "external",
|
||||
"summary": "CVE record",
|
||||
"url": "https://www.cve.org/CVERecord?id=CVE-2025-0002"
|
||||
}
|
||||
],
|
||||
"product_status": {
|
||||
"known_not_affected": [
|
||||
"9Base-RHEL-9",
|
||||
"9Base-RHEL-9:kernel-0:5.14.0-400.el9.x86_64"
|
||||
],
|
||||
"under_investigation": [
|
||||
"9Base-RHEL-9:kernel-0:5.14.0-401.el9.x86_64"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"document": {
|
||||
"aggregate_severity": {
|
||||
"text": "Important"
|
||||
},
|
||||
"lang": "en",
|
||||
"notes": [
|
||||
{
|
||||
"category": "summary",
|
||||
"text": "Advisory with mixed reference sources to verify dedupe ordering."
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"category": "self",
|
||||
"summary": "Primary advisory",
|
||||
"url": "https://access.redhat.com/errata/RHSA-2025:0003"
|
||||
},
|
||||
{
|
||||
"category": "self",
|
||||
"summary": "",
|
||||
"url": "https://access.redhat.com/errata/RHSA-2025:0003"
|
||||
},
|
||||
{
|
||||
"category": "mitigation",
|
||||
"summary": "Knowledge base guidance",
|
||||
"url": "https://access.redhat.com/solutions/999999"
|
||||
}
|
||||
],
|
||||
"title": "Red Hat Security Advisory: Reference dedupe validation",
|
||||
"tracking": {
|
||||
"id": "RHSA-2025:0003",
|
||||
"initial_release_date": "2025-10-06T09:00:00+00:00",
|
||||
"current_release_date": "2025-10-06T09:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"product_tree": {
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_family",
|
||||
"branches": [
|
||||
{
|
||||
"category": "product_name",
|
||||
"product": {
|
||||
"name": "Red Hat Enterprise Linux 9",
|
||||
"product_id": "9Base-RHEL-9",
|
||||
"product_identification_helper": {
|
||||
"cpe": "cpe:/o:redhat:enterprise_linux:9"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": "CVE-2025-0003",
|
||||
"references": [
|
||||
{
|
||||
"category": "external",
|
||||
"summary": "CVE record",
|
||||
"url": "https://www.cve.org/CVERecord?id=CVE-2025-0003"
|
||||
},
|
||||
{
|
||||
"category": "external",
|
||||
"summary": "",
|
||||
"url": "https://www.cve.org/CVERecord?id=CVE-2025-0003"
|
||||
},
|
||||
{
|
||||
"category": "exploit",
|
||||
"summary": "Exploit tracking",
|
||||
"url": "https://bugzilla.redhat.com/show_bug.cgi?id=2222222"
|
||||
}
|
||||
],
|
||||
"scores": [
|
||||
{
|
||||
"cvss_v3": {
|
||||
"baseScore": 7.5,
|
||||
"baseSeverity": "HIGH",
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
|
||||
"version": "3.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"product_status": {
|
||||
"known_affected": [
|
||||
"9Base-RHEL-9"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"advisoryKey": "RHSA-2025:0001",
|
||||
"affectedPackages": [
|
||||
{
|
||||
"type": "cpe",
|
||||
"identifier": "cpe:2.3:o:redhat:enterprise_linux:8:*:*:*:*:*:*:*",
|
||||
"platform": "Red Hat Enterprise Linux 8",
|
||||
"versionRanges": [],
|
||||
"normalizedVersions": [],
|
||||
"statuses": [
|
||||
{
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "oval",
|
||||
"value": "8Base-RHEL-8",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T00:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"status": "known_affected"
|
||||
}
|
||||
],
|
||||
"provenance": [
|
||||
{
|
||||
"source": "redhat",
|
||||
"kind": "oval",
|
||||
"value": "8Base-RHEL-8",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T00:00:00+00:00",
|
||||
"fieldMask": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rpm",
|
||||
"identifier": "kernel-0:4.18.0-513.5.1.el8.x86_64",
|
||||
"platform": "Red Hat Enterprise Linux 8",
|
||||
"versionRanges": [
|
||||
{
|
||||
"fixedVersion": "kernel-0:4.18.0-513.5.1.el8.x86_64",
|
||||
"introducedVersion": null,
|
||||
"lastAffectedVersion": "kernel-0:4.18.0-500.1.0.el8.x86_64",
|
||||
"primitives": {
|
||||
"evr": null,
|
||||
"hasVendorExtensions": false,
|
||||
"nevra": {
|
||||
"fixed": {
|
||||
"architecture": "x86_64",
|
||||
"epoch": 0,
|
||||
"name": "kernel",
|
||||
"release": "513.5.1.el8",
|
||||
"version": "4.18.0"
|
||||
},
|
||||
"introduced": null,
|
||||
"lastAffected": {
|
||||
"architecture": "x86_64",
|
||||
"epoch": 0,
|
||||
"name": "kernel",
|
||||
"release": "500.1.0.el8",
|
||||
"version": "4.18.0"
|
||||
}
|
||||
},
|
||||
"semVer": null,
|
||||
"vendorExtensions": null
|
||||
},
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "package.nevra",
|
||||
"value": "kernel-0:4.18.0-513.5.1.el8.x86_64",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T00:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"rangeExpression": null,
|
||||
"rangeKind": "nevra"
|
||||
}
|
||||
],
|
||||
"normalizedVersions": [],
|
||||
"statuses": [],
|
||||
"provenance": [
|
||||
{
|
||||
"source": "redhat",
|
||||
"kind": "package.nevra",
|
||||
"value": "kernel-0:4.18.0-513.5.1.el8.x86_64",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T00:00:00+00:00",
|
||||
"fieldMask": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"CVE-2025-0001",
|
||||
"RHSA-2025:0001"
|
||||
],
|
||||
"canonicalMetricId": null,
|
||||
"credits": [],
|
||||
"cvssMetrics": [
|
||||
{
|
||||
"baseScore": 9.8,
|
||||
"baseSeverity": "critical",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "cvss",
|
||||
"value": "CVE-2025-0001",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T00:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"version": "3.1"
|
||||
}
|
||||
],
|
||||
"cwes": [],
|
||||
"description": null,
|
||||
"exploitKnown": false,
|
||||
"language": "en",
|
||||
"modified": "2025-10-03T00:00:00+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"source": "redhat",
|
||||
"kind": "advisory",
|
||||
"value": "RHSA-2025:0001",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T00:00:00+00:00",
|
||||
"fieldMask": []
|
||||
}
|
||||
],
|
||||
"published": "2025-10-02T00:00:00+00:00",
|
||||
"references": [
|
||||
{
|
||||
"kind": "self",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "reference",
|
||||
"value": "https://access.redhat.com/errata/RHSA-2025:0001",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T00:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"sourceTag": null,
|
||||
"summary": "RHSA advisory",
|
||||
"url": "https://access.redhat.com/errata/RHSA-2025:0001"
|
||||
},
|
||||
{
|
||||
"kind": "external",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "reference",
|
||||
"value": "https://www.cve.org/CVERecord?id=CVE-2025-0001",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T00:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"sourceTag": null,
|
||||
"summary": "CVE record",
|
||||
"url": "https://www.cve.org/CVERecord?id=CVE-2025-0001"
|
||||
}
|
||||
],
|
||||
"severity": "high",
|
||||
"summary": "An update fixes a critical kernel issue.",
|
||||
"title": "Red Hat Security Advisory: Example kernel update"
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"advisoryKey": "RHSA-2025:0002",
|
||||
"affectedPackages": [
|
||||
{
|
||||
"type": "cpe",
|
||||
"identifier": "cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
|
||||
"platform": "Red Hat Enterprise Linux 9",
|
||||
"versionRanges": [],
|
||||
"normalizedVersions": [],
|
||||
"statuses": [
|
||||
{
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "oval",
|
||||
"value": "9Base-RHEL-9",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T12:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"status": "known_not_affected"
|
||||
},
|
||||
{
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "oval",
|
||||
"value": "9Base-RHEL-9",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T12:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"status": "under_investigation"
|
||||
}
|
||||
],
|
||||
"provenance": [
|
||||
{
|
||||
"source": "redhat",
|
||||
"kind": "oval",
|
||||
"value": "9Base-RHEL-9",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T12:00:00+00:00",
|
||||
"fieldMask": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rpm",
|
||||
"identifier": "kernel-0:5.14.0-400.el9.x86_64",
|
||||
"platform": "Red Hat Enterprise Linux 9",
|
||||
"versionRanges": [],
|
||||
"normalizedVersions": [],
|
||||
"statuses": [
|
||||
{
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "package.nevra",
|
||||
"value": "kernel-0:5.14.0-400.el9.x86_64",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T12:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"status": "known_not_affected"
|
||||
}
|
||||
],
|
||||
"provenance": [
|
||||
{
|
||||
"source": "redhat",
|
||||
"kind": "package.nevra",
|
||||
"value": "kernel-0:5.14.0-400.el9.x86_64",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T12:00:00+00:00",
|
||||
"fieldMask": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"CVE-2025-0002",
|
||||
"RHSA-2025:0002"
|
||||
],
|
||||
"canonicalMetricId": null,
|
||||
"credits": [],
|
||||
"cvssMetrics": [],
|
||||
"cwes": [],
|
||||
"description": null,
|
||||
"exploitKnown": false,
|
||||
"language": "en",
|
||||
"modified": "2025-10-05T12:00:00+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"source": "redhat",
|
||||
"kind": "advisory",
|
||||
"value": "RHSA-2025:0002",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T12:00:00+00:00",
|
||||
"fieldMask": []
|
||||
}
|
||||
],
|
||||
"published": "2025-10-05T12:00:00+00:00",
|
||||
"references": [
|
||||
{
|
||||
"kind": "self",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "reference",
|
||||
"value": "https://access.redhat.com/errata/RHSA-2025:0002",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T12:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"sourceTag": null,
|
||||
"summary": "RHSA advisory",
|
||||
"url": "https://access.redhat.com/errata/RHSA-2025:0002"
|
||||
},
|
||||
{
|
||||
"kind": "external",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "reference",
|
||||
"value": "https://www.cve.org/CVERecord?id=CVE-2025-0002",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-05T12:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"sourceTag": null,
|
||||
"summary": "CVE record",
|
||||
"url": "https://www.cve.org/CVERecord?id=CVE-2025-0002"
|
||||
}
|
||||
],
|
||||
"severity": "medium",
|
||||
"summary": "Second advisory covering unaffected packages.",
|
||||
"title": "Red Hat Security Advisory: Follow-up kernel status"
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"advisoryKey": "RHSA-2025:0003",
|
||||
"affectedPackages": [
|
||||
{
|
||||
"type": "cpe",
|
||||
"identifier": "cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*",
|
||||
"platform": "Red Hat Enterprise Linux 9",
|
||||
"versionRanges": [],
|
||||
"normalizedVersions": [],
|
||||
"statuses": [
|
||||
{
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "oval",
|
||||
"value": "9Base-RHEL-9",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-06T09:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"status": "known_affected"
|
||||
}
|
||||
],
|
||||
"provenance": [
|
||||
{
|
||||
"source": "redhat",
|
||||
"kind": "oval",
|
||||
"value": "9Base-RHEL-9",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-06T09:00:00+00:00",
|
||||
"fieldMask": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
"CVE-2025-0003",
|
||||
"RHSA-2025:0003"
|
||||
],
|
||||
"canonicalMetricId": null,
|
||||
"credits": [],
|
||||
"cvssMetrics": [
|
||||
{
|
||||
"baseScore": 7.5,
|
||||
"baseSeverity": "high",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "cvss",
|
||||
"value": "CVE-2025-0003",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-06T09:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
|
||||
"version": "3.1"
|
||||
}
|
||||
],
|
||||
"cwes": [],
|
||||
"description": null,
|
||||
"exploitKnown": false,
|
||||
"language": "en",
|
||||
"modified": "2025-10-06T09:00:00+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"source": "redhat",
|
||||
"kind": "advisory",
|
||||
"value": "RHSA-2025:0003",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-06T09:00:00+00:00",
|
||||
"fieldMask": []
|
||||
}
|
||||
],
|
||||
"published": "2025-10-06T09:00:00+00:00",
|
||||
"references": [
|
||||
{
|
||||
"kind": "self",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "reference",
|
||||
"value": "https://access.redhat.com/errata/RHSA-2025:0003",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-06T09:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"sourceTag": null,
|
||||
"summary": "Primary advisory",
|
||||
"url": "https://access.redhat.com/errata/RHSA-2025:0003"
|
||||
},
|
||||
{
|
||||
"kind": "mitigation",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "reference",
|
||||
"value": "https://access.redhat.com/solutions/999999",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-06T09:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"sourceTag": null,
|
||||
"summary": "Knowledge base guidance",
|
||||
"url": "https://access.redhat.com/solutions/999999"
|
||||
},
|
||||
{
|
||||
"kind": "exploit",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "reference",
|
||||
"value": "https://bugzilla.redhat.com/show_bug.cgi?id=2222222",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-06T09:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"sourceTag": null,
|
||||
"summary": "Exploit tracking",
|
||||
"url": "https://bugzilla.redhat.com/show_bug.cgi?id=2222222"
|
||||
},
|
||||
{
|
||||
"kind": "external",
|
||||
"provenance": {
|
||||
"source": "redhat",
|
||||
"kind": "reference",
|
||||
"value": "https://www.cve.org/CVERecord?id=CVE-2025-0003",
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-06T09:00:00+00:00",
|
||||
"fieldMask": []
|
||||
},
|
||||
"sourceTag": null,
|
||||
"summary": "CVE record",
|
||||
"url": "https://www.cve.org/CVERecord?id=CVE-2025-0003"
|
||||
}
|
||||
],
|
||||
"severity": "high",
|
||||
"summary": "Advisory with mixed reference sources to verify dedupe ordering.",
|
||||
"title": "Red Hat Security Advisory: Reference dedupe validation"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"RHSA": "RHSA-2025:0001",
|
||||
"severity": "important",
|
||||
"released_on": "2025-10-03T00:00:00Z",
|
||||
"resource_url": "https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0001.json"
|
||||
},
|
||||
{
|
||||
"RHSA": "RHSA-2025:0002",
|
||||
"severity": "moderate",
|
||||
"released_on": "2025-10-05T12:00:00Z",
|
||||
"resource_url": "https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"RHSA": "RHSA-2025:0001",
|
||||
"severity": "important",
|
||||
"released_on": "2025-10-03T12:00:00Z",
|
||||
"resource_url": "https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0001.json"
|
||||
},
|
||||
{
|
||||
"RHSA": "RHSA-2025:0002",
|
||||
"severity": "moderate",
|
||||
"released_on": "2025-10-05T12:00:00Z",
|
||||
"resource_url": "https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"RHSA": "RHSA-2025:0002",
|
||||
"severity": "moderate",
|
||||
"released_on": "2025-10-05T12:00:00Z",
|
||||
"resource_url": "https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"RHSA": "RHSA-2025:0003",
|
||||
"severity": "important",
|
||||
"released_on": "2025-10-06T09:00:00Z",
|
||||
"resource_url": "https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0003.json"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.RedHat;
|
||||
using StellaOps.Concelier.Connector.Distro.RedHat.Configuration;
|
||||
using StellaOps.Concelier.Storage.Mongo;
|
||||
using StellaOps.Concelier.Storage.Mongo.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using StellaOps.Concelier.Testing;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.RedHat.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class RedHatConnectorHarnessTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ConnectorTestHarness _harness;
|
||||
|
||||
public RedHatConnectorHarnessTests(MongoIntegrationFixture fixture)
|
||||
{
|
||||
_harness = new ConnectorTestHarness(fixture, new DateTimeOffset(2025, 10, 5, 0, 0, 0, TimeSpan.Zero), RedHatOptions.HttpClientName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_WithHarness_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await _harness.ResetAsync();
|
||||
|
||||
var options = new RedHatOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
|
||||
PageSize = 10,
|
||||
MaxPagesPerFetch = 2,
|
||||
MaxAdvisoriesPerFetch = 5,
|
||||
InitialBackfill = TimeSpan.FromDays(1),
|
||||
Overlap = TimeSpan.Zero,
|
||||
FetchTimeout = TimeSpan.FromSeconds(30),
|
||||
UserAgent = "StellaOps.Tests.RedHatHarness/1.0",
|
||||
};
|
||||
|
||||
var handler = _harness.Handler;
|
||||
var timeProvider = _harness.TimeProvider;
|
||||
|
||||
var summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=10&page=1");
|
||||
var summaryUriPost = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=1");
|
||||
var summaryUriPostPage2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=2");
|
||||
var detailUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0001.json");
|
||||
var detailUri2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json");
|
||||
|
||||
handler.AddJsonResponse(summaryUri, ReadFixture("summary-page1-repeat.json"));
|
||||
handler.AddJsonResponse(summaryUriPost, "[]");
|
||||
handler.AddJsonResponse(summaryUriPostPage2, "[]");
|
||||
handler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0001.json"));
|
||||
handler.AddJsonResponse(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json"));
|
||||
|
||||
await _harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddRedHatConnector(opts =>
|
||||
{
|
||||
opts.BaseEndpoint = options.BaseEndpoint;
|
||||
opts.PageSize = options.PageSize;
|
||||
opts.MaxPagesPerFetch = options.MaxPagesPerFetch;
|
||||
opts.MaxAdvisoriesPerFetch = options.MaxAdvisoriesPerFetch;
|
||||
opts.InitialBackfill = options.InitialBackfill;
|
||||
opts.Overlap = options.Overlap;
|
||||
opts.FetchTimeout = options.FetchTimeout;
|
||||
opts.UserAgent = options.UserAgent;
|
||||
});
|
||||
});
|
||||
|
||||
var provider = _harness.ServiceProvider;
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
await stateRepository.UpsertAsync(
|
||||
new SourceStateRecord(
|
||||
RedHatConnectorPlugin.SourceName,
|
||||
Enabled: true,
|
||||
Paused: false,
|
||||
Cursor: new BsonDocument(),
|
||||
LastSuccess: null,
|
||||
LastFailure: null,
|
||||
FailCount: 0,
|
||||
BackoffUntil: null,
|
||||
UpdatedAt: timeProvider.GetUtcNow(),
|
||||
LastFailureReason: null),
|
||||
CancellationToken.None);
|
||||
|
||||
var connector = new RedHatConnectorPlugin().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(5, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
var advisory = advisories.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0001", StringComparison.Ordinal));
|
||||
Assert.Equal("high", advisory.Severity);
|
||||
Assert.Contains(advisory.Aliases, alias => alias == "CVE-2025-0001");
|
||||
Assert.Empty(advisory.Provenance.Where(p => p.Source == "redhat" && p.Kind == "fetch"));
|
||||
|
||||
var secondAdvisory = advisories.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0002", StringComparison.Ordinal));
|
||||
Assert.Equal("medium", secondAdvisory.Severity, ignoreCase: true);
|
||||
Assert.Contains(secondAdvisory.Aliases, alias => alias == "CVE-2025-0002");
|
||||
|
||||
var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings) && pendingMappings.AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => _harness.ResetAsync();
|
||||
|
||||
private static string ReadFixture(string filename)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "RedHat", "Fixtures", filename);
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Text.Json;
|
||||
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.Connector.Common;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Distro.RedHat;
|
||||
using StellaOps.Concelier.Connector.Distro.RedHat.Configuration;
|
||||
using StellaOps.Concelier.Connector.Distro.RedHat.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
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.Testing;
|
||||
using StellaOps.Plugin;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.RedHat.Tests;
|
||||
|
||||
[Collection("mongo-fixture")]
|
||||
public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MongoIntegrationFixture _fixture;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly DateTimeOffset _initialNow;
|
||||
private readonly CannedHttpMessageHandler _handler;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private ServiceProvider? _serviceProvider;
|
||||
private const bool ForceUpdateGoldens = false;
|
||||
|
||||
public RedHatConnectorTests(MongoIntegrationFixture fixture, ITestOutputHelper output)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_initialNow = new DateTimeOffset(2025, 10, 5, 0, 0, 0, TimeSpan.Zero);
|
||||
_timeProvider = new FakeTimeProvider(_initialNow);
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new RedHatOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
|
||||
PageSize = 10,
|
||||
MaxPagesPerFetch = 2,
|
||||
MaxAdvisoriesPerFetch = 25,
|
||||
InitialBackfill = TimeSpan.FromDays(1),
|
||||
Overlap = TimeSpan.Zero,
|
||||
FetchTimeout = TimeSpan.FromSeconds(30),
|
||||
UserAgent = "StellaOps.Tests.RedHat/1.0",
|
||||
};
|
||||
|
||||
await EnsureServiceProviderAsync(options);
|
||||
var provider = _serviceProvider!;
|
||||
|
||||
var configuredOptions = provider.GetRequiredService<IOptions<RedHatOptions>>().Value;
|
||||
Assert.Equal(10, configuredOptions.PageSize);
|
||||
Assert.Equal(TimeSpan.FromDays(1), configuredOptions.InitialBackfill);
|
||||
Assert.Equal(TimeSpan.Zero, configuredOptions.Overlap);
|
||||
_output.WriteLine($"InitialBackfill configured: {configuredOptions.InitialBackfill}");
|
||||
_output.WriteLine($"TimeProvider now: {_timeProvider.GetUtcNow():O}");
|
||||
|
||||
var summaryUriBackfill = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-03&per_page=10&page=1");
|
||||
var summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=10&page=1");
|
||||
var summaryUriPost = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=1");
|
||||
var summaryUriPostPage2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=2");
|
||||
var detailUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0001.json");
|
||||
var detailUri2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json");
|
||||
|
||||
_output.WriteLine($"Registering summary URI: {summaryUriBackfill}");
|
||||
_output.WriteLine($"Registering summary URI (overlap): {summaryUri}");
|
||||
_handler.AddJsonResponse(summaryUriBackfill, ReadFixture("summary-page1.json"));
|
||||
_handler.AddJsonResponse(summaryUri, ReadFixture("summary-page1-repeat.json"));
|
||||
_handler.AddJsonResponse(summaryUriPost, "[]");
|
||||
_handler.AddJsonResponse(summaryUriPostPage2, "[]");
|
||||
_handler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0001.json"));
|
||||
_handler.AddJsonResponse(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json"));
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
await stateRepository.UpsertAsync(
|
||||
new SourceStateRecord(
|
||||
RedHatConnectorPlugin.SourceName,
|
||||
Enabled: true,
|
||||
Paused: false,
|
||||
Cursor: new BsonDocument(),
|
||||
LastSuccess: null,
|
||||
LastFailure: null,
|
||||
FailCount: 0,
|
||||
BackoffUntil: null,
|
||||
UpdatedAt: _timeProvider.GetUtcNow(),
|
||||
LastFailureReason: null),
|
||||
CancellationToken.None);
|
||||
|
||||
var connector = new RedHatConnectorPlugin().Create(provider);
|
||||
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
await connector.MapAsync(provider, CancellationToken.None);
|
||||
|
||||
|
||||
foreach (var request in _handler.Requests)
|
||||
{
|
||||
_output.WriteLine($"Captured request: {request.Uri}");
|
||||
}
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
var advisory = advisories.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0001", StringComparison.Ordinal));
|
||||
Assert.Equal("red hat security advisory: example kernel update", advisory.Title.ToLowerInvariant());
|
||||
Assert.Contains("RHSA-2025:0001", advisory.Aliases);
|
||||
Assert.Contains("CVE-2025-0001", advisory.Aliases);
|
||||
Assert.Equal("high", advisory.Severity);
|
||||
Assert.Equal("en", advisory.Language);
|
||||
|
||||
var rpmPackage = advisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Rpm);
|
||||
_output.WriteLine($"RPM statuses count: {rpmPackage.Statuses.Length}");
|
||||
_output.WriteLine($"RPM ranges count: {rpmPackage.VersionRanges.Length}");
|
||||
foreach (var range in rpmPackage.VersionRanges)
|
||||
{
|
||||
_output.WriteLine($"Range fixed={range.FixedVersion}, last={range.LastAffectedVersion}, expr={range.RangeExpression}");
|
||||
}
|
||||
Assert.Equal("kernel-0:4.18.0-513.5.1.el8.x86_64", rpmPackage.Identifier);
|
||||
var fixedRange = Assert.Single(
|
||||
rpmPackage.VersionRanges,
|
||||
range => string.Equals(range.FixedVersion, "kernel-0:4.18.0-513.5.1.el8.x86_64", StringComparison.Ordinal));
|
||||
Assert.Equal("kernel-0:4.18.0-500.1.0.el8.x86_64", fixedRange.LastAffectedVersion);
|
||||
var nevraPrimitive = fixedRange.Primitives?.Nevra;
|
||||
Assert.NotNull(nevraPrimitive);
|
||||
Assert.Null(nevraPrimitive!.Introduced);
|
||||
Assert.Equal("kernel", nevraPrimitive.Fixed?.Name);
|
||||
|
||||
var cpePackage = advisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Cpe);
|
||||
Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:8:*:*:*:*:*:*:*", cpePackage.Identifier);
|
||||
|
||||
Assert.Contains(advisory.References, reference => reference.Url == "https://access.redhat.com/errata/RHSA-2025:0001");
|
||||
Assert.Contains(advisory.References, reference => reference.Url == "https://www.cve.org/CVERecord?id=CVE-2025-0001");
|
||||
|
||||
var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n");
|
||||
_output.WriteLine("-- RHSA-2025:0001 snapshot --\n" + snapshot);
|
||||
var snapshotPath = ProjectFixturePath("rhsa-2025-0001.snapshot.json");
|
||||
if (ShouldUpdateGoldens())
|
||||
{
|
||||
File.WriteAllText(snapshotPath, snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
var expectedSnapshot = File.ReadAllText(snapshotPath);
|
||||
Assert.Equal(NormalizeLineEndings(expectedSnapshot), NormalizeLineEndings(snapshot));
|
||||
|
||||
var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs2) && pendingDocs2.AsBsonArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings2) && pendingMappings2.AsBsonArray.Count == 0);
|
||||
|
||||
const string fetchKind = "source:redhat:fetch";
|
||||
const string parseKind = "source:redhat:parse";
|
||||
const string mapKind = "source:redhat:map";
|
||||
|
||||
var schedulerOptions = provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<JobSchedulerOptions>>().Value;
|
||||
Assert.True(schedulerOptions.Definitions.TryGetValue(fetchKind, out var fetchDefinition));
|
||||
Assert.True(schedulerOptions.Definitions.TryGetValue(parseKind, out var parseDefinition));
|
||||
Assert.True(schedulerOptions.Definitions.TryGetValue(mapKind, out var mapDefinition));
|
||||
|
||||
Assert.Equal("RedHatFetchJob", fetchDefinition.JobType.Name);
|
||||
Assert.Equal(TimeSpan.FromMinutes(12), fetchDefinition.Timeout);
|
||||
Assert.Equal(TimeSpan.FromMinutes(6), fetchDefinition.LeaseDuration);
|
||||
Assert.Equal("0,15,30,45 * * * *", fetchDefinition.CronExpression);
|
||||
Assert.True(fetchDefinition.Enabled);
|
||||
|
||||
Assert.Equal("RedHatParseJob", parseDefinition.JobType.Name);
|
||||
Assert.Equal(TimeSpan.FromMinutes(15), parseDefinition.Timeout);
|
||||
Assert.Equal(TimeSpan.FromMinutes(6), parseDefinition.LeaseDuration);
|
||||
Assert.Equal("5,20,35,50 * * * *", parseDefinition.CronExpression);
|
||||
Assert.True(parseDefinition.Enabled);
|
||||
|
||||
Assert.Equal("RedHatMapJob", mapDefinition.JobType.Name);
|
||||
Assert.Equal(TimeSpan.FromMinutes(20), mapDefinition.Timeout);
|
||||
Assert.Equal(TimeSpan.FromMinutes(6), mapDefinition.LeaseDuration);
|
||||
Assert.Equal("10,25,40,55 * * * *", mapDefinition.CronExpression);
|
||||
Assert.True(mapDefinition.Enabled);
|
||||
|
||||
var summaryUriRepeat = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-03&per_page=10&page=1");
|
||||
var summaryUriSecondPage = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-03&per_page=10&page=2");
|
||||
var summaryUriRepeatOverlap = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=10&page=1");
|
||||
var summaryUriSecondPageOverlap = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=10&page=2");
|
||||
|
||||
_output.WriteLine($"Registering repeat summary URI: {summaryUriRepeat}");
|
||||
_output.WriteLine($"Registering second page summary URI: {summaryUriSecondPage}");
|
||||
_output.WriteLine($"Registering overlap repeat summary URI: {summaryUriRepeatOverlap}");
|
||||
_output.WriteLine($"Registering overlap second page summary URI: {summaryUriSecondPageOverlap}");
|
||||
_handler.AddJsonResponse(summaryUriRepeat, ReadFixture("summary-page1-repeat.json"));
|
||||
_handler.AddJsonResponse(summaryUriSecondPage, ReadFixture("summary-page2.json"));
|
||||
_handler.AddJsonResponse(summaryUriRepeatOverlap, ReadFixture("summary-page1-repeat.json"));
|
||||
_handler.AddJsonResponse(summaryUriSecondPageOverlap, ReadFixture("summary-page2.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(2, advisories.Count);
|
||||
|
||||
var secondAdvisory = advisories.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0002", StringComparison.Ordinal));
|
||||
var rpm2 = secondAdvisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Rpm);
|
||||
Assert.Equal("kernel-0:5.14.0-400.el9.x86_64", rpm2.Identifier);
|
||||
const string knownNotAffected = "known_not_affected";
|
||||
|
||||
foreach (var status in rpm2.Statuses)
|
||||
{
|
||||
_output.WriteLine($"RPM2 status: {status.Status}");
|
||||
}
|
||||
|
||||
Assert.DoesNotContain(rpm2.VersionRanges, range => string.Equals(range.RangeExpression, knownNotAffected, StringComparison.Ordinal));
|
||||
Assert.Contains(rpm2.Statuses, status => status.Status == knownNotAffected);
|
||||
|
||||
var cpe2 = secondAdvisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Cpe);
|
||||
Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", cpe2.Identifier);
|
||||
Assert.Empty(cpe2.VersionRanges);
|
||||
Assert.Contains(cpe2.Statuses, status => status.Status == knownNotAffected);
|
||||
|
||||
state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs3) && pendingDocs3.AsBsonArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings3) && pendingMappings3.AsBsonArray.Count == 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GoldenFixturesMatchSnapshots()
|
||||
{
|
||||
var fixtures = new[]
|
||||
{
|
||||
new GoldenFixtureCase(
|
||||
AdvisoryId: "RHSA-2025:0001",
|
||||
InputFile: "csaf-rhsa-2025-0001.json",
|
||||
SnapshotFile: "rhsa-2025-0001.snapshot.json",
|
||||
ValidatedAt: DateTimeOffset.Parse("2025-10-05T00:00:00Z")),
|
||||
new GoldenFixtureCase(
|
||||
AdvisoryId: "RHSA-2025:0002",
|
||||
InputFile: "csaf-rhsa-2025-0002.json",
|
||||
SnapshotFile: "rhsa-2025-0002.snapshot.json",
|
||||
ValidatedAt: DateTimeOffset.Parse("2025-10-05T12:00:00Z")),
|
||||
new GoldenFixtureCase(
|
||||
AdvisoryId: "RHSA-2025:0003",
|
||||
InputFile: "csaf-rhsa-2025-0003.json",
|
||||
SnapshotFile: "rhsa-2025-0003.snapshot.json",
|
||||
ValidatedAt: DateTimeOffset.Parse("2025-10-06T09:00:00Z")),
|
||||
};
|
||||
|
||||
var updateGoldens = ShouldUpdateGoldens();
|
||||
|
||||
foreach (var fixture in fixtures)
|
||||
{
|
||||
var snapshot = MapFixtureToSnapshot(fixture);
|
||||
var snapshotPath = ProjectFixturePath(fixture.SnapshotFile);
|
||||
|
||||
if (updateGoldens)
|
||||
{
|
||||
File.WriteAllText(snapshotPath, snapshot);
|
||||
continue;
|
||||
}
|
||||
|
||||
var expected = File.ReadAllText(snapshotPath).Replace("\r\n", "\n");
|
||||
Assert.Equal(expected, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resume_CompletesPendingDocumentsAfterRestart()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new RedHatOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
|
||||
PageSize = 10,
|
||||
MaxPagesPerFetch = 2,
|
||||
MaxAdvisoriesPerFetch = 25,
|
||||
InitialBackfill = TimeSpan.FromDays(1),
|
||||
Overlap = TimeSpan.Zero,
|
||||
FetchTimeout = TimeSpan.FromSeconds(30),
|
||||
UserAgent = "StellaOps.Tests.RedHat/1.0",
|
||||
};
|
||||
|
||||
var summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=10&page=1");
|
||||
var summaryUriPost = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=1");
|
||||
var summaryUriPostPage2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=2");
|
||||
var detailUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0001.json");
|
||||
var detailUri2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json");
|
||||
|
||||
var fetchHandler = new CannedHttpMessageHandler();
|
||||
fetchHandler.AddJsonResponse(summaryUri, ReadFixture("summary-page1-repeat.json"));
|
||||
fetchHandler.AddJsonResponse(summaryUriPost, "[]");
|
||||
fetchHandler.AddJsonResponse(summaryUriPostPage2, "[]");
|
||||
fetchHandler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0001.json"));
|
||||
fetchHandler.AddJsonResponse(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json"));
|
||||
|
||||
Guid[] pendingDocumentIds;
|
||||
await using (var fetchProvider = await CreateServiceProviderAsync(options, fetchHandler))
|
||||
{
|
||||
var stateRepository = fetchProvider.GetRequiredService<ISourceStateRepository>();
|
||||
await stateRepository.UpsertAsync(
|
||||
new SourceStateRecord(
|
||||
RedHatConnectorPlugin.SourceName,
|
||||
Enabled: true,
|
||||
Paused: false,
|
||||
Cursor: new BsonDocument(),
|
||||
LastSuccess: null,
|
||||
LastFailure: null,
|
||||
FailCount: 0,
|
||||
BackoffUntil: null,
|
||||
UpdatedAt: _timeProvider.GetUtcNow(),
|
||||
LastFailureReason: null),
|
||||
CancellationToken.None);
|
||||
|
||||
var connector = new RedHatConnectorPlugin().Create(fetchProvider);
|
||||
await connector.FetchAsync(fetchProvider, CancellationToken.None);
|
||||
|
||||
var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
var pendingDocs = state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocsValue)
|
||||
? pendingDocsValue.AsBsonArray
|
||||
: new BsonArray();
|
||||
Assert.NotEmpty(pendingDocs);
|
||||
pendingDocumentIds = pendingDocs.Select(value => Guid.Parse(value.AsString)).ToArray();
|
||||
}
|
||||
|
||||
var resumeHandler = new CannedHttpMessageHandler();
|
||||
await using (var resumeProvider = await CreateServiceProviderAsync(options, resumeHandler))
|
||||
{
|
||||
var resumeConnector = new RedHatConnectorPlugin().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(RedHatConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(finalState);
|
||||
var finalPendingDocs = finalState!.Cursor.TryGetValue("pendingDocuments", out var docsValue) ? docsValue.AsBsonArray : new BsonArray();
|
||||
Assert.Empty(finalPendingDocs);
|
||||
var finalPendingMappings = finalState.Cursor.TryGetValue("pendingMappings", out var mappingsValue) ? mappingsValue.AsBsonArray : new BsonArray();
|
||||
Assert.Empty(finalPendingMappings);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAsync_DeduplicatesReferencesAndOrdersDeterministically()
|
||||
{
|
||||
await ResetDatabaseAsync();
|
||||
|
||||
var options = new RedHatOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
|
||||
PageSize = 10,
|
||||
MaxPagesPerFetch = 2,
|
||||
MaxAdvisoriesPerFetch = 10,
|
||||
InitialBackfill = TimeSpan.FromDays(7),
|
||||
Overlap = TimeSpan.Zero,
|
||||
FetchTimeout = TimeSpan.FromSeconds(30),
|
||||
UserAgent = "StellaOps.Tests.RedHat/1.0",
|
||||
};
|
||||
|
||||
await EnsureServiceProviderAsync(options);
|
||||
var provider = _serviceProvider!;
|
||||
|
||||
var summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-09-28&per_page=10&page=1");
|
||||
var summaryUriPost = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=10&page=1");
|
||||
var detailUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0003.json");
|
||||
|
||||
_handler.AddJsonResponse(summaryUri, ReadFixture("summary-page3.json"));
|
||||
_handler.AddJsonResponse(summaryUriPost, "[]");
|
||||
_handler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0003.json"));
|
||||
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
await stateRepository.UpsertAsync(
|
||||
new SourceStateRecord(
|
||||
RedHatConnectorPlugin.SourceName,
|
||||
Enabled: true,
|
||||
Paused: false,
|
||||
Cursor: new BsonDocument(),
|
||||
LastSuccess: null,
|
||||
LastFailure: null,
|
||||
FailCount: 0,
|
||||
BackoffUntil: null,
|
||||
UpdatedAt: _timeProvider.GetUtcNow(),
|
||||
LastFailureReason: null),
|
||||
CancellationToken.None);
|
||||
|
||||
var connector = new RedHatConnectorPlugin().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 advisory = (await advisoryStore.GetRecentAsync(10, CancellationToken.None))
|
||||
.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0003", StringComparison.Ordinal));
|
||||
|
||||
var references = advisory.References.ToArray();
|
||||
Assert.Collection(
|
||||
references,
|
||||
reference =>
|
||||
{
|
||||
Assert.Equal("self", reference.Kind);
|
||||
Assert.Equal("https://access.redhat.com/errata/RHSA-2025:0003", reference.Url);
|
||||
Assert.Equal("Primary advisory", reference.Summary);
|
||||
},
|
||||
reference =>
|
||||
{
|
||||
Assert.Equal("mitigation", reference.Kind);
|
||||
Assert.Equal("https://access.redhat.com/solutions/999999", reference.Url);
|
||||
Assert.Equal("Knowledge base guidance", reference.Summary);
|
||||
},
|
||||
reference =>
|
||||
{
|
||||
Assert.Equal("exploit", reference.Kind);
|
||||
Assert.Equal("https://bugzilla.redhat.com/show_bug.cgi?id=2222222", reference.Url);
|
||||
Assert.Equal("Exploit tracking", reference.Summary);
|
||||
},
|
||||
reference =>
|
||||
{
|
||||
Assert.Equal("external", reference.Kind);
|
||||
Assert.Equal("https://www.cve.org/CVERecord?id=CVE-2025-0003", reference.Url);
|
||||
Assert.Equal("CVE record", reference.Summary);
|
||||
});
|
||||
Assert.Equal(4, references.Length);
|
||||
|
||||
Assert.Equal("self", references[0].Kind);
|
||||
Assert.Equal("https://access.redhat.com/errata/RHSA-2025:0003", references[0].Url);
|
||||
Assert.Equal("Primary advisory", references[0].Summary);
|
||||
|
||||
Assert.Equal("mitigation", references[1].Kind);
|
||||
Assert.Equal("https://access.redhat.com/solutions/999999", references[1].Url);
|
||||
Assert.Equal("Knowledge base guidance", references[1].Summary);
|
||||
|
||||
Assert.Equal("exploit", references[2].Kind);
|
||||
Assert.Equal("https://bugzilla.redhat.com/show_bug.cgi?id=2222222", references[2].Url);
|
||||
|
||||
Assert.Equal("external", references[3].Kind);
|
||||
Assert.Equal("https://www.cve.org/CVERecord?id=CVE-2025-0003", references[3].Url);
|
||||
Assert.Equal("CVE record", references[3].Summary);
|
||||
}
|
||||
|
||||
private static string MapFixtureToSnapshot(GoldenFixtureCase fixture)
|
||||
{
|
||||
var jsonPath = ProjectFixturePath(fixture.InputFile);
|
||||
var json = File.ReadAllText(jsonPath);
|
||||
|
||||
using var jsonDocument = JsonDocument.Parse(json);
|
||||
var bson = BsonDocument.Parse(json);
|
||||
|
||||
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["advisoryId"] = fixture.AdvisoryId,
|
||||
};
|
||||
|
||||
var document = new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
RedHatConnectorPlugin.SourceName,
|
||||
$"https://access.redhat.com/hydra/rest/securitydata/csaf/{fixture.AdvisoryId}.json",
|
||||
fixture.ValidatedAt,
|
||||
new string('0', 64),
|
||||
DocumentStatuses.Mapped,
|
||||
"application/json",
|
||||
Headers: null,
|
||||
Metadata: metadata,
|
||||
Etag: null,
|
||||
LastModified: fixture.ValidatedAt,
|
||||
GridFsId: null);
|
||||
|
||||
var dto = new DtoRecord(Guid.NewGuid(), document.Id, RedHatConnectorPlugin.SourceName, "redhat.csaf.v2", bson, fixture.ValidatedAt);
|
||||
|
||||
var advisory = RedHatMapper.Map(RedHatConnectorPlugin.SourceName, dto, document, jsonDocument);
|
||||
Assert.NotNull(advisory);
|
||||
|
||||
return SnapshotSerializer.ToSnapshot(advisory!).Replace("\r\n", "\n");
|
||||
}
|
||||
|
||||
private static bool ShouldUpdateGoldens()
|
||||
=> ForceUpdateGoldens
|
||||
|| IsTruthy(Environment.GetEnvironmentVariable("UPDATE_GOLDENS"))
|
||||
|| IsTruthy(Environment.GetEnvironmentVariable("DOTNET_TEST_UPDATE_GOLDENS"));
|
||||
|
||||
private static bool IsTruthy(string? value)
|
||||
=> !string.IsNullOrWhiteSpace(value)
|
||||
&& (string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private sealed record GoldenFixtureCase(string AdvisoryId, string InputFile, string SnapshotFile, DateTimeOffset ValidatedAt);
|
||||
|
||||
private static string ProjectFixturePath(string filename)
|
||||
=> Path.Combine(GetProjectRoot(), "RedHat", "Fixtures", filename);
|
||||
|
||||
private static string GetProjectRoot()
|
||||
=> Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
|
||||
|
||||
private async Task EnsureServiceProviderAsync(RedHatOptions options)
|
||||
{
|
||||
if (_serviceProvider is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_serviceProvider = await CreateServiceProviderAsync(options, _handler);
|
||||
}
|
||||
|
||||
private async Task<ServiceProvider> CreateServiceProviderAsync(RedHatOptions 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.AddRedHatConnector(opts =>
|
||||
{
|
||||
opts.BaseEndpoint = options.BaseEndpoint;
|
||||
opts.SummaryPath = options.SummaryPath;
|
||||
opts.PageSize = options.PageSize;
|
||||
opts.MaxPagesPerFetch = options.MaxPagesPerFetch;
|
||||
opts.MaxAdvisoriesPerFetch = options.MaxAdvisoriesPerFetch;
|
||||
opts.InitialBackfill = options.InitialBackfill;
|
||||
opts.Overlap = options.Overlap;
|
||||
opts.FetchTimeout = options.FetchTimeout;
|
||||
opts.UserAgent = options.UserAgent;
|
||||
});
|
||||
|
||||
services.Configure<JobSchedulerOptions>(schedulerOptions =>
|
||||
{
|
||||
var fetchType = Type.GetType("StellaOps.Concelier.Connector.Distro.RedHat.RedHatFetchJob, StellaOps.Concelier.Connector.Distro.RedHat", throwOnError: true)!;
|
||||
var parseType = Type.GetType("StellaOps.Concelier.Connector.Distro.RedHat.RedHatParseJob, StellaOps.Concelier.Connector.Distro.RedHat", throwOnError: true)!;
|
||||
var mapType = Type.GetType("StellaOps.Concelier.Connector.Distro.RedHat.RedHatMapJob, StellaOps.Concelier.Connector.Distro.RedHat", throwOnError: true)!;
|
||||
|
||||
schedulerOptions.Definitions["source:redhat:fetch"] = new JobDefinition("source:redhat:fetch", fetchType, TimeSpan.FromMinutes(12), TimeSpan.FromMinutes(6), "0,15,30,45 * * * *", true);
|
||||
schedulerOptions.Definitions["source:redhat:parse"] = new JobDefinition("source:redhat:parse", parseType, TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(6), "5,20,35,50 * * * *", true);
|
||||
schedulerOptions.Definitions["source:redhat:map"] = new JobDefinition("source:redhat:map", mapType, TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(6), "10,25,40,55 * * * *", true);
|
||||
});
|
||||
|
||||
services.Configure<HttpClientFactoryOptions>(RedHatOptions.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 Task ResetDatabaseAsync()
|
||||
{
|
||||
return ResetDatabaseInternalAsync();
|
||||
}
|
||||
|
||||
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.SetUtcNow(_initialNow);
|
||||
}
|
||||
|
||||
private static string ReadFixture(string name)
|
||||
=> File.ReadAllText(ResolveFixturePath(name));
|
||||
|
||||
private static string ResolveFixturePath(string filename)
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "RedHat", "Fixtures", filename),
|
||||
Path.Combine(AppContext.BaseDirectory, "RedHat", "Fixtures", filename),
|
||||
};
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"Fixture '{filename}' not found in output directory.", filename);
|
||||
}
|
||||
|
||||
private static string NormalizeLineEndings(string value)
|
||||
{
|
||||
var normalized = value.Replace("\r\n", "\n").Replace('\r', '\n');
|
||||
return normalized.TrimEnd('\n');
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await ResetDatabaseInternalAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Distro.RedHat/StellaOps.Concelier.Connector.Distro.RedHat.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="RedHat/Fixtures/*.json" CopyToOutputDirectory="Always" TargetPath="Source/Distro/RedHat/Fixtures/%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user