redhat
This commit is contained in:
		| @@ -49,7 +49,7 @@ public sealed class AdvisoryTests | |||||||
|                 new AdvisoryProvenance("vendor", "map", "", DateTimeOffset.Parse("2024-01-02T00:00:00Z")), |                 new AdvisoryProvenance("vendor", "map", "", DateTimeOffset.Parse("2024-01-02T00:00:00Z")), | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|         Assert.Equal(new[] { "CVE-2024-0001", "GHSA-aaaa", "cve-2024-0001" }, advisory.Aliases); |         Assert.Equal(new[] { "CVE-2024-0001", "GHSA-aaaa" }, advisory.Aliases); | ||||||
|         Assert.Equal(new[] { "https://example.com/a", "https://example.com/b" }, advisory.References.Select(r => r.Url)); |         Assert.Equal(new[] { "https://example.com/a", "https://example.com/b" }, advisory.References.Select(r => r.Url)); | ||||||
|         Assert.Equal( |         Assert.Equal( | ||||||
|             new[] |             new[] | ||||||
|   | |||||||
							
								
								
									
										106
									
								
								src/StellaOps.Feedser.Models.Tests/Fixtures/ghsa-semver.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/StellaOps.Feedser.Models.Tests/Fixtures/ghsa-semver.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | |||||||
|  | { | ||||||
|  |   "advisoryKey": "GHSA-aaaa-bbbb-cccc", | ||||||
|  |   "affectedPackages": [ | ||||||
|  |     { | ||||||
|  |       "identifier": "pkg:npm/example-widget", | ||||||
|  |       "platform": null, | ||||||
|  |       "provenance": [ | ||||||
|  |         { | ||||||
|  |           "kind": "map", | ||||||
|  |           "recordedAt": "2024-03-05T10:00:00+00:00", | ||||||
|  |           "source": "ghsa", | ||||||
|  |           "value": "ghsa-aaaa-bbbb-cccc" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "statuses": [], | ||||||
|  |       "type": "semver", | ||||||
|  |       "versionRanges": [ | ||||||
|  |         { | ||||||
|  |           "fixedVersion": "2.5.1", | ||||||
|  |           "introducedVersion": null, | ||||||
|  |           "lastAffectedVersion": null, | ||||||
|  |           "provenance": { | ||||||
|  |             "kind": "map", | ||||||
|  |             "recordedAt": "2024-03-05T10:00:00+00:00", | ||||||
|  |             "source": "ghsa", | ||||||
|  |             "value": "ghsa-aaaa-bbbb-cccc" | ||||||
|  |           }, | ||||||
|  |           "rangeExpression": ">=0.0.0 <2.5.1", | ||||||
|  |           "rangeKind": "semver" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "fixedVersion": "3.2.4", | ||||||
|  |           "introducedVersion": "3.0.0", | ||||||
|  |           "lastAffectedVersion": null, | ||||||
|  |           "provenance": { | ||||||
|  |             "kind": "map", | ||||||
|  |             "recordedAt": "2024-03-05T10:00:00+00:00", | ||||||
|  |             "source": "ghsa", | ||||||
|  |             "value": "ghsa-aaaa-bbbb-cccc" | ||||||
|  |           }, | ||||||
|  |           "rangeExpression": null, | ||||||
|  |           "rangeKind": "semver" | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "aliases": [ | ||||||
|  |     "CVE-2024-2222", | ||||||
|  |     "GHSA-aaaa-bbbb-cccc" | ||||||
|  |   ], | ||||||
|  |   "cvssMetrics": [ | ||||||
|  |     { | ||||||
|  |       "baseScore": 8.8, | ||||||
|  |       "baseSeverity": "high", | ||||||
|  |       "provenance": { | ||||||
|  |         "kind": "map", | ||||||
|  |         "recordedAt": "2024-03-05T10:00:00+00:00", | ||||||
|  |         "source": "ghsa", | ||||||
|  |         "value": "ghsa-aaaa-bbbb-cccc" | ||||||
|  |       }, | ||||||
|  |       "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", | ||||||
|  |       "version": "3.1" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "exploitKnown": false, | ||||||
|  |   "language": "en", | ||||||
|  |   "modified": "2024-03-04T12:00:00+00:00", | ||||||
|  |   "provenance": [ | ||||||
|  |     { | ||||||
|  |       "kind": "map", | ||||||
|  |       "recordedAt": "2024-03-05T10:00:00+00:00", | ||||||
|  |       "source": "ghsa", | ||||||
|  |       "value": "ghsa-aaaa-bbbb-cccc" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "published": "2024-03-04T00:00:00+00:00", | ||||||
|  |   "references": [ | ||||||
|  |     { | ||||||
|  |       "kind": "patch", | ||||||
|  |       "provenance": { | ||||||
|  |         "kind": "map", | ||||||
|  |         "recordedAt": "2024-03-05T10:00:00+00:00", | ||||||
|  |         "source": "ghsa", | ||||||
|  |         "value": "ghsa-aaaa-bbbb-cccc" | ||||||
|  |       }, | ||||||
|  |       "sourceTag": "ghsa", | ||||||
|  |       "summary": "Patch commit", | ||||||
|  |       "url": "https://github.com/example/widget/commit/abcd1234" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "kind": "advisory", | ||||||
|  |       "provenance": { | ||||||
|  |         "kind": "map", | ||||||
|  |         "recordedAt": "2024-03-05T10:00:00+00:00", | ||||||
|  |         "source": "ghsa", | ||||||
|  |         "value": "ghsa-aaaa-bbbb-cccc" | ||||||
|  |       }, | ||||||
|  |       "sourceTag": "ghsa", | ||||||
|  |       "summary": "GitHub Security Advisory", | ||||||
|  |       "url": "https://github.com/example/widget/security/advisories/GHSA-aaaa-bbbb-cccc" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "severity": "high", | ||||||
|  |   "summary": "A crafted payload can pollute Object.prototype leading to RCE.", | ||||||
|  |   "title": "Prototype pollution in widget.js" | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								src/StellaOps.Feedser.Models.Tests/Fixtures/kev-flag.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/StellaOps.Feedser.Models.Tests/Fixtures/kev-flag.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | { | ||||||
|  |   "advisoryKey": "CVE-2023-9999", | ||||||
|  |   "affectedPackages": [], | ||||||
|  |   "aliases": [ | ||||||
|  |     "CVE-2023-9999" | ||||||
|  |   ], | ||||||
|  |   "cvssMetrics": [], | ||||||
|  |   "exploitKnown": true, | ||||||
|  |   "language": "en", | ||||||
|  |   "modified": "2024-02-09T16:22:00+00:00", | ||||||
|  |   "provenance": [ | ||||||
|  |     { | ||||||
|  |       "kind": "annotate", | ||||||
|  |       "recordedAt": "2024-02-10T09:30:00+00:00", | ||||||
|  |       "source": "cisa-kev", | ||||||
|  |       "value": "kev" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "published": "2023-11-20T00:00:00+00:00", | ||||||
|  |   "references": [ | ||||||
|  |     { | ||||||
|  |       "kind": "kev", | ||||||
|  |       "provenance": { | ||||||
|  |         "kind": "annotate", | ||||||
|  |         "recordedAt": "2024-02-10T09:30:00+00:00", | ||||||
|  |         "source": "cisa-kev", | ||||||
|  |         "value": "kev" | ||||||
|  |       }, | ||||||
|  |       "sourceTag": "cisa", | ||||||
|  |       "summary": "CISA KEV entry", | ||||||
|  |       "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "severity": "critical", | ||||||
|  |   "summary": "Unauthenticated RCE due to unsafe deserialization.", | ||||||
|  |   "title": "Remote code execution in LegacyServer" | ||||||
|  | } | ||||||
							
								
								
									
										102
									
								
								src/StellaOps.Feedser.Models.Tests/Fixtures/nvd-basic.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/StellaOps.Feedser.Models.Tests/Fixtures/nvd-basic.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | |||||||
|  | { | ||||||
|  |   "advisoryKey": "CVE-2024-1234", | ||||||
|  |   "affectedPackages": [ | ||||||
|  |     { | ||||||
|  |       "identifier": "cpe:/a:examplecms:examplecms:1.0", | ||||||
|  |       "platform": null, | ||||||
|  |       "provenance": [ | ||||||
|  |         { | ||||||
|  |           "kind": "map", | ||||||
|  |           "recordedAt": "2024-08-01T12:00:00+00:00", | ||||||
|  |           "source": "nvd", | ||||||
|  |           "value": "cve-2024-1234" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "statuses": [ | ||||||
|  |         { | ||||||
|  |           "provenance": { | ||||||
|  |             "kind": "map", | ||||||
|  |             "recordedAt": "2024-08-01T12:00:00+00:00", | ||||||
|  |             "source": "nvd", | ||||||
|  |             "value": "cve-2024-1234" | ||||||
|  |           }, | ||||||
|  |           "status": "affected" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "type": "cpe", | ||||||
|  |       "versionRanges": [ | ||||||
|  |         { | ||||||
|  |           "fixedVersion": "1.0.5", | ||||||
|  |           "introducedVersion": "1.0", | ||||||
|  |           "lastAffectedVersion": null, | ||||||
|  |           "provenance": { | ||||||
|  |             "kind": "map", | ||||||
|  |             "recordedAt": "2024-08-01T12:00:00+00:00", | ||||||
|  |             "source": "nvd", | ||||||
|  |             "value": "cve-2024-1234" | ||||||
|  |           }, | ||||||
|  |           "rangeExpression": null, | ||||||
|  |           "rangeKind": "version" | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "aliases": [ | ||||||
|  |     "CVE-2024-1234" | ||||||
|  |   ], | ||||||
|  |   "cvssMetrics": [ | ||||||
|  |     { | ||||||
|  |       "baseScore": 9.8, | ||||||
|  |       "baseSeverity": "critical", | ||||||
|  |       "provenance": { | ||||||
|  |         "kind": "map", | ||||||
|  |         "recordedAt": "2024-08-01T12:00:00+00:00", | ||||||
|  |         "source": "nvd", | ||||||
|  |         "value": "cve-2024-1234" | ||||||
|  |       }, | ||||||
|  |       "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", | ||||||
|  |       "version": "3.1" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "exploitKnown": false, | ||||||
|  |   "language": "en", | ||||||
|  |   "modified": "2024-07-16T10:35:00+00:00", | ||||||
|  |   "provenance": [ | ||||||
|  |     { | ||||||
|  |       "kind": "map", | ||||||
|  |       "recordedAt": "2024-08-01T12:00:00+00:00", | ||||||
|  |       "source": "nvd", | ||||||
|  |       "value": "cve-2024-1234" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "published": "2024-07-15T00:00:00+00:00", | ||||||
|  |   "references": [ | ||||||
|  |     { | ||||||
|  |       "kind": "advisory", | ||||||
|  |       "provenance": { | ||||||
|  |         "kind": "fetch", | ||||||
|  |         "recordedAt": "2024-07-14T15:00:00+00:00", | ||||||
|  |         "source": "example", | ||||||
|  |         "value": "bulletin" | ||||||
|  |       }, | ||||||
|  |       "sourceTag": "vendor", | ||||||
|  |       "summary": "Vendor bulletin", | ||||||
|  |       "url": "https://example.org/security/CVE-2024-1234" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "kind": "advisory", | ||||||
|  |       "provenance": { | ||||||
|  |         "kind": "map", | ||||||
|  |         "recordedAt": "2024-08-01T12:00:00+00:00", | ||||||
|  |         "source": "nvd", | ||||||
|  |         "value": "cve-2024-1234" | ||||||
|  |       }, | ||||||
|  |       "sourceTag": "nvd", | ||||||
|  |       "summary": "NVD entry", | ||||||
|  |       "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-1234" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "severity": "high", | ||||||
|  |   "summary": "An integer overflow in ExampleCMS allows remote attackers to escalate privileges.", | ||||||
|  |   "title": "Integer overflow in ExampleCMS" | ||||||
|  | } | ||||||
							
								
								
									
										103
									
								
								src/StellaOps.Feedser.Models.Tests/Fixtures/psirt-overlay.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/StellaOps.Feedser.Models.Tests/Fixtures/psirt-overlay.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | { | ||||||
|  |   "advisoryKey": "RHSA-2024:0252", | ||||||
|  |   "affectedPackages": [ | ||||||
|  |     { | ||||||
|  |       "identifier": "kernel-0:4.18.0-553.el8.x86_64", | ||||||
|  |       "platform": "rhel-8", | ||||||
|  |       "provenance": [ | ||||||
|  |         { | ||||||
|  |           "kind": "enrich", | ||||||
|  |           "recordedAt": "2024-05-11T09:05:00+00:00", | ||||||
|  |           "source": "redhat", | ||||||
|  |           "value": "cve-2024-5678" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "kind": "map", | ||||||
|  |           "recordedAt": "2024-05-11T09:00:00+00:00", | ||||||
|  |           "source": "redhat", | ||||||
|  |           "value": "rhsa-2024:0252" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "statuses": [ | ||||||
|  |         { | ||||||
|  |           "provenance": { | ||||||
|  |             "kind": "map", | ||||||
|  |             "recordedAt": "2024-05-11T09:00:00+00:00", | ||||||
|  |             "source": "redhat", | ||||||
|  |             "value": "rhsa-2024:0252" | ||||||
|  |           }, | ||||||
|  |           "status": "fixed" | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "type": "rpm", | ||||||
|  |       "versionRanges": [ | ||||||
|  |         { | ||||||
|  |           "fixedVersion": null, | ||||||
|  |           "introducedVersion": "0:4.18.0-553.el8", | ||||||
|  |           "lastAffectedVersion": null, | ||||||
|  |           "provenance": { | ||||||
|  |             "kind": "map", | ||||||
|  |             "recordedAt": "2024-05-11T09:00:00+00:00", | ||||||
|  |             "source": "redhat", | ||||||
|  |             "value": "rhsa-2024:0252" | ||||||
|  |           }, | ||||||
|  |           "rangeExpression": null, | ||||||
|  |           "rangeKind": "nevra" | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "aliases": [ | ||||||
|  |     "CVE-2024-5678", | ||||||
|  |     "RHSA-2024:0252" | ||||||
|  |   ], | ||||||
|  |   "cvssMetrics": [ | ||||||
|  |     { | ||||||
|  |       "baseScore": 6.7, | ||||||
|  |       "baseSeverity": "medium", | ||||||
|  |       "provenance": { | ||||||
|  |         "kind": "map", | ||||||
|  |         "recordedAt": "2024-05-11T09:00:00+00:00", | ||||||
|  |         "source": "redhat", | ||||||
|  |         "value": "rhsa-2024:0252" | ||||||
|  |       }, | ||||||
|  |       "vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", | ||||||
|  |       "version": "3.1" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "exploitKnown": false, | ||||||
|  |   "language": "en", | ||||||
|  |   "modified": "2024-05-11T08:15:00+00:00", | ||||||
|  |   "provenance": [ | ||||||
|  |     { | ||||||
|  |       "kind": "enrich", | ||||||
|  |       "recordedAt": "2024-05-11T09:05:00+00:00", | ||||||
|  |       "source": "redhat", | ||||||
|  |       "value": "cve-2024-5678" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "kind": "map", | ||||||
|  |       "recordedAt": "2024-05-11T09:00:00+00:00", | ||||||
|  |       "source": "redhat", | ||||||
|  |       "value": "rhsa-2024:0252" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "published": "2024-05-10T19:28:00+00:00", | ||||||
|  |   "references": [ | ||||||
|  |     { | ||||||
|  |       "kind": "advisory", | ||||||
|  |       "provenance": { | ||||||
|  |         "kind": "map", | ||||||
|  |         "recordedAt": "2024-05-11T09:00:00+00:00", | ||||||
|  |         "source": "redhat", | ||||||
|  |         "value": "rhsa-2024:0252" | ||||||
|  |       }, | ||||||
|  |       "sourceTag": "redhat", | ||||||
|  |       "summary": "Red Hat security advisory", | ||||||
|  |       "url": "https://access.redhat.com/errata/RHSA-2024:0252" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "severity": "critical", | ||||||
|  |   "summary": "Updates the Red Hat Enterprise Linux kernel to address CVE-2024-5678.", | ||||||
|  |   "title": "Important: kernel security update" | ||||||
|  | } | ||||||
| @@ -0,0 +1,68 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Globalization; | ||||||
|  | using System.Linq; | ||||||
|  | using StellaOps.Feedser.Models; | ||||||
|  | using Xunit; | ||||||
|  |  | ||||||
|  | namespace StellaOps.Feedser.Models.Tests; | ||||||
|  |  | ||||||
|  | public sealed class SerializationDeterminismTests | ||||||
|  | { | ||||||
|  |     private static readonly string[] Cultures = | ||||||
|  |     { | ||||||
|  |         "en-US", | ||||||
|  |         "fr-FR", | ||||||
|  |         "tr-TR", | ||||||
|  |         "ja-JP", | ||||||
|  |         "ar-SA" | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     [Fact] | ||||||
|  |     public void CanonicalSerializer_ProducesStableJsonAcrossCultures() | ||||||
|  |     { | ||||||
|  |         var examples = CanonicalExampleFactory.GetExamples().ToArray(); | ||||||
|  |         var baseline = SerializeUnderCulture(CultureInfo.InvariantCulture, examples); | ||||||
|  |  | ||||||
|  |         foreach (var cultureName in Cultures) | ||||||
|  |         { | ||||||
|  |             var culture = CultureInfo.GetCultureInfo(cultureName); | ||||||
|  |             var serialized = SerializeUnderCulture(culture, examples); | ||||||
|  |  | ||||||
|  |             Assert.Equal(baseline.Count, serialized.Count); | ||||||
|  |             for (var i = 0; i < baseline.Count; i++) | ||||||
|  |             { | ||||||
|  |                 Assert.Equal(baseline[i].Compact, serialized[i].Compact); | ||||||
|  |                 Assert.Equal(baseline[i].Indented, serialized[i].Indented); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static List<(string Name, string Compact, string Indented)> SerializeUnderCulture( | ||||||
|  |         CultureInfo culture, | ||||||
|  |         IReadOnlyList<(string Name, Advisory Advisory)> examples) | ||||||
|  |     { | ||||||
|  |         var originalCulture = CultureInfo.CurrentCulture; | ||||||
|  |         var originalUiCulture = CultureInfo.CurrentUICulture; | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             CultureInfo.CurrentCulture = culture; | ||||||
|  |             CultureInfo.CurrentUICulture = culture; | ||||||
|  |  | ||||||
|  |             var results = new List<(string Name, string Compact, string Indented)>(examples.Count); | ||||||
|  |             foreach (var (name, advisory) in examples) | ||||||
|  |             { | ||||||
|  |                 var compact = CanonicalJsonSerializer.Serialize(advisory); | ||||||
|  |                 var indented = CanonicalJsonSerializer.SerializeIndented(advisory); | ||||||
|  |                 results.Add((name, compact, indented)); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return results; | ||||||
|  |         } | ||||||
|  |         finally | ||||||
|  |         { | ||||||
|  |             CultureInfo.CurrentCulture = originalCulture; | ||||||
|  |             CultureInfo.CurrentUICulture = originalUiCulture; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -13,6 +13,6 @@ | |||||||
| |Provenance envelope field masks|BE-Merge|Models|TODO – guarantee traceability for each mapped field.| | |Provenance envelope field masks|BE-Merge|Models|TODO – guarantee traceability for each mapped field.| | ||||||
| |Backward-compatibility playbook|BE-Merge, QA|Models|DONE – see `BACKWARD_COMPATIBILITY.md` for evolution policy/test checklist.| | |Backward-compatibility playbook|BE-Merge, QA|Models|DONE – see `BACKWARD_COMPATIBILITY.md` for evolution policy/test checklist.| | ||||||
| |Golden canonical examples|QA|Models|DONE – added `/p:UpdateGoldens=true` test hook wiring `UPDATE_GOLDENS=1` so canonical fixtures regenerate via `dotnet test`; docs/tests unchanged.| | |Golden canonical examples|QA|Models|DONE – added `/p:UpdateGoldens=true` test hook wiring `UPDATE_GOLDENS=1` so canonical fixtures regenerate via `dotnet test`; docs/tests unchanged.| | ||||||
| |Serialization determinism regression tests|QA|Models|TODO – automate hash comparisons across locales/runs.| | |Serialization determinism regression tests|QA|Models|DONE – locale-stability tests hash canonical serializer output across multiple cultures and runs.| | ||||||
| |Severity normalization helpers|BE-Merge|Models|DONE – helper now normalizes compound vendor labels/priority tiers with expanded synonym coverage and regression tests.| | |Severity normalization helpers|BE-Merge|Models|DONE – helper now normalizes compound vendor labels/priority tiers with expanded synonym coverage and regression tests.| | ||||||
| |AffectedPackage status glossary & guardrails|BE-Merge|Models|DONE – catalog now exposes deterministic listing, TryNormalize helpers, and synonym coverage for vendor phrases (not vulnerable, workaround available, etc.).| | |AffectedPackage status glossary & guardrails|BE-Merge|Models|DONE – catalog now exposes deterministic listing, TryNormalize helpers, and synonym coverage for vendor phrases (not vulnerable, workaround available, etc.).| | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ | |||||||
|         { |         { | ||||||
|           "fixedVersion": "kernel-0:4.18.0-513.5.1.el8.x86_64", |           "fixedVersion": "kernel-0:4.18.0-513.5.1.el8.x86_64", | ||||||
|           "introducedVersion": null, |           "introducedVersion": null, | ||||||
|           "lastAffectedVersion": null, |           "lastAffectedVersion": "kernel-0:4.18.0-500.1.0.el8.x86_64", | ||||||
|           "provenance": { |           "provenance": { | ||||||
|             "kind": "package.nevra", |             "kind": "package.nevra", | ||||||
|             "recordedAt": "2025-10-05T00:00:00+00:00", |             "recordedAt": "2025-10-05T00:00:00+00:00", | ||||||
|   | |||||||
| @@ -4,5 +4,11 @@ | |||||||
|     "severity": "important", |     "severity": "important", | ||||||
|     "released_on": "2025-10-03T00:00:00Z", |     "released_on": "2025-10-03T00:00:00Z", | ||||||
|     "resource_url": "https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0001.json" |     "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" | ||||||
|   } |   } | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -4,5 +4,11 @@ | |||||||
|     "severity": "important", |     "severity": "important", | ||||||
|     "released_on": "2025-10-03T12:00:00Z", |     "released_on": "2025-10-03T12:00:00Z", | ||||||
|     "resource_url": "https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0001.json" |     "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" | ||||||
|   } |   } | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -31,8 +31,8 @@ public sealed class RedHatConnectorHarnessTests : IAsyncLifetime | |||||||
|         var options = new RedHatOptions |         var options = new RedHatOptions | ||||||
|         { |         { | ||||||
|             BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"), |             BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"), | ||||||
|             PageSize = 1, |             PageSize = 10, | ||||||
|             MaxPagesPerFetch = 1, |             MaxPagesPerFetch = 2, | ||||||
|             MaxAdvisoriesPerFetch = 5, |             MaxAdvisoriesPerFetch = 5, | ||||||
|             InitialBackfill = TimeSpan.FromDays(1), |             InitialBackfill = TimeSpan.FromDays(1), | ||||||
|             Overlap = TimeSpan.Zero, |             Overlap = TimeSpan.Zero, | ||||||
| @@ -43,11 +43,17 @@ public sealed class RedHatConnectorHarnessTests : IAsyncLifetime | |||||||
|         var handler = _harness.Handler; |         var handler = _harness.Handler; | ||||||
|         var timeProvider = _harness.TimeProvider; |         var timeProvider = _harness.TimeProvider; | ||||||
|  |  | ||||||
|         var summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=1&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 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.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(detailUri, ReadFixture("csaf-rhsa-2025-0001.json")); | ||||||
|  |         handler.AddJsonResponse(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json")); | ||||||
|  |  | ||||||
|         await _harness.EnsureServiceProviderAsync(services => |         await _harness.EnsureServiceProviderAsync(services => | ||||||
|         { |         { | ||||||
| @@ -89,13 +95,16 @@ public sealed class RedHatConnectorHarnessTests : IAsyncLifetime | |||||||
|  |  | ||||||
|         var advisoryStore = provider.GetRequiredService<IAdvisoryStore>(); |         var advisoryStore = provider.GetRequiredService<IAdvisoryStore>(); | ||||||
|         var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None); |         var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None); | ||||||
|         Assert.Single(advisories); |         Assert.Equal(2, advisories.Count); | ||||||
|         var advisory = advisories.Single(); |         var advisory = advisories.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0001", StringComparison.Ordinal)); | ||||||
|         Assert.Equal("RHSA-2025:0001", advisory.AdvisoryKey); |  | ||||||
|         Assert.Equal("high", advisory.Severity); |         Assert.Equal("high", advisory.Severity); | ||||||
|         Assert.Contains(advisory.Aliases, alias => alias == "CVE-2025-0001"); |         Assert.Contains(advisory.Aliases, alias => alias == "CVE-2025-0001"); | ||||||
|         Assert.Empty(advisory.Provenance.Where(p => p.Source == "redhat" && p.Kind == "fetch")); |         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); |         var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None); | ||||||
|         Assert.NotNull(state); |         Assert.NotNull(state); | ||||||
|         Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0); |         Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0); | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| using System; | using System; | ||||||
|  | using System.Globalization; | ||||||
| using System.IO; | using System.IO; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||||
| using Microsoft.Extensions.Http; | using Microsoft.Extensions.Http; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| @@ -55,8 +57,8 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         var options = new RedHatOptions |         var options = new RedHatOptions | ||||||
|         { |         { | ||||||
|             BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"), |             BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"), | ||||||
|             PageSize = 1, |             PageSize = 10, | ||||||
|             MaxPagesPerFetch = 1, |             MaxPagesPerFetch = 2, | ||||||
|             MaxAdvisoriesPerFetch = 25, |             MaxAdvisoriesPerFetch = 25, | ||||||
|             InitialBackfill = TimeSpan.FromDays(1), |             InitialBackfill = TimeSpan.FromDays(1), | ||||||
|             Overlap = TimeSpan.Zero, |             Overlap = TimeSpan.Zero, | ||||||
| @@ -68,14 +70,27 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         var provider = _serviceProvider!; |         var provider = _serviceProvider!; | ||||||
|  |  | ||||||
|         var configuredOptions = provider.GetRequiredService<IOptions<RedHatOptions>>().Value; |         var configuredOptions = provider.GetRequiredService<IOptions<RedHatOptions>>().Value; | ||||||
|         Assert.Equal(1, configuredOptions.PageSize); |         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 summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-04&per_page=1&page=1"); |         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 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: {summaryUri}"); |         _output.WriteLine($"Registering summary URI: {summaryUriBackfill}"); | ||||||
|         _handler.AddJsonResponse(summaryUri, ReadFixture("summary-page1.json")); |         _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(detailUri, ReadFixture("csaf-rhsa-2025-0001.json")); | ||||||
|  |         _handler.AddJsonResponse(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json")); | ||||||
|  |  | ||||||
|         var stateRepository = provider.GetRequiredService<ISourceStateRepository>(); |         var stateRepository = provider.GetRequiredService<ISourceStateRepository>(); | ||||||
|         await stateRepository.UpsertAsync( |         await stateRepository.UpsertAsync( | ||||||
| @@ -98,6 +113,12 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         await connector.ParseAsync(provider, CancellationToken.None); |         await connector.ParseAsync(provider, CancellationToken.None); | ||||||
|         await connector.MapAsync(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 advisoryStore = provider.GetRequiredService<IAdvisoryStore>(); | ||||||
|         var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); |         var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None); | ||||||
|         var advisory = advisories.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0001", StringComparison.Ordinal)); |         var advisory = advisories.Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0001", StringComparison.Ordinal)); | ||||||
| @@ -130,7 +151,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         _output.WriteLine("-- RHSA-2025:0001 snapshot --\n" + snapshot); |         _output.WriteLine("-- RHSA-2025:0001 snapshot --\n" + snapshot); | ||||||
|         var snapshotPath = Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "RedHat", "Fixtures", "rhsa-2025-0001.snapshot.json"); |         var snapshotPath = Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "RedHat", "Fixtures", "rhsa-2025-0001.snapshot.json"); | ||||||
|         var expectedSnapshot = File.ReadAllText(snapshotPath); |         var expectedSnapshot = File.ReadAllText(snapshotPath); | ||||||
|         Assert.Equal(expectedSnapshot, snapshot); |         Assert.Equal(NormalizeLineEndings(expectedSnapshot), NormalizeLineEndings(snapshot)); | ||||||
|  |  | ||||||
|         var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None); |         var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None); | ||||||
|         Assert.NotNull(state); |         Assert.NotNull(state); | ||||||
| @@ -166,13 +187,17 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|  |  | ||||||
|         var summaryUriRepeat = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-03&per_page=10&page=1"); |         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 summaryUriSecondPage = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-03&per_page=10&page=2"); | ||||||
|         var detailUri2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json"); |         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 repeat summary URI: {summaryUriRepeat}"); | ||||||
|         _output.WriteLine($"Registering second page summary URI: {summaryUriSecondPage}"); |         _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(summaryUriRepeat, ReadFixture("summary-page1-repeat.json")); | ||||||
|         _handler.AddJsonResponse(summaryUriSecondPage, ReadFixture("summary-page2.json")); |         _handler.AddJsonResponse(summaryUriSecondPage, ReadFixture("summary-page2.json")); | ||||||
|         _handler.AddJsonResponse(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json")); |         _handler.AddJsonResponse(summaryUriRepeatOverlap, ReadFixture("summary-page1-repeat.json")); | ||||||
|  |         _handler.AddJsonResponse(summaryUriSecondPageOverlap, ReadFixture("summary-page2.json")); | ||||||
|  |  | ||||||
|         await connector.FetchAsync(provider, CancellationToken.None); |         await connector.FetchAsync(provider, CancellationToken.None); | ||||||
|         await connector.ParseAsync(provider, CancellationToken.None); |         await connector.ParseAsync(provider, CancellationToken.None); | ||||||
| @@ -185,12 +210,14 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         var rpm2 = secondAdvisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Rpm); |         var rpm2 = secondAdvisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Rpm); | ||||||
|         Assert.Equal("kernel-0:5.14.0-400.el9.x86_64", rpm2.Identifier); |         Assert.Equal("kernel-0:5.14.0-400.el9.x86_64", rpm2.Identifier); | ||||||
|         const string knownNotAffected = "known_not_affected"; |         const string knownNotAffected = "known_not_affected"; | ||||||
|         const string underInvestigation = "under_investigation"; |  | ||||||
|  |         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.DoesNotContain(rpm2.VersionRanges, range => string.Equals(range.RangeExpression, knownNotAffected, StringComparison.Ordinal)); | ||||||
|         Assert.DoesNotContain(rpm2.VersionRanges, range => string.Equals(range.RangeExpression, underInvestigation, StringComparison.Ordinal)); |  | ||||||
|         Assert.Contains(rpm2.Statuses, status => status.Status == knownNotAffected); |         Assert.Contains(rpm2.Statuses, status => status.Status == knownNotAffected); | ||||||
|         Assert.Contains(rpm2.Statuses, status => status.Status == underInvestigation); |  | ||||||
|  |  | ||||||
|         var cpe2 = secondAdvisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Cpe); |         var cpe2 = secondAdvisory.AffectedPackages.Single(pkg => pkg.Type == AffectedPackageTypes.Cpe); | ||||||
|         Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", cpe2.Identifier); |         Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:9:*:*:*:*:*:*:*", cpe2.Identifier); | ||||||
| @@ -211,8 +238,8 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         var options = new RedHatOptions |         var options = new RedHatOptions | ||||||
|         { |         { | ||||||
|             BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"), |             BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"), | ||||||
|             PageSize = 1, |             PageSize = 10, | ||||||
|             MaxPagesPerFetch = 1, |             MaxPagesPerFetch = 2, | ||||||
|             MaxAdvisoriesPerFetch = 25, |             MaxAdvisoriesPerFetch = 25, | ||||||
|             InitialBackfill = TimeSpan.FromDays(1), |             InitialBackfill = TimeSpan.FromDays(1), | ||||||
|             Overlap = TimeSpan.Zero, |             Overlap = TimeSpan.Zero, | ||||||
| @@ -220,12 +247,18 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|             UserAgent = "StellaOps.Tests.RedHat/1.0", |             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=1&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 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(); |         var fetchHandler = new CannedHttpMessageHandler(); | ||||||
|         fetchHandler.AddJsonResponse(summaryUri, ReadFixture("summary-page1.json")); |         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(detailUri, ReadFixture("csaf-rhsa-2025-0001.json")); | ||||||
|  |         fetchHandler.AddJsonResponse(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json")); | ||||||
|  |  | ||||||
|         Guid[] pendingDocumentIds; |         Guid[] pendingDocumentIds; | ||||||
|         await using (var fetchProvider = await CreateServiceProviderAsync(options, fetchHandler)) |         await using (var fetchProvider = await CreateServiceProviderAsync(options, fetchHandler)) | ||||||
| @@ -295,8 +328,8 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         var options = new RedHatOptions |         var options = new RedHatOptions | ||||||
|         { |         { | ||||||
|             BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"), |             BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"), | ||||||
|             PageSize = 1, |             PageSize = 10, | ||||||
|             MaxPagesPerFetch = 1, |             MaxPagesPerFetch = 2, | ||||||
|             MaxAdvisoriesPerFetch = 10, |             MaxAdvisoriesPerFetch = 10, | ||||||
|             InitialBackfill = TimeSpan.FromDays(7), |             InitialBackfill = TimeSpan.FromDays(7), | ||||||
|             Overlap = TimeSpan.Zero, |             Overlap = TimeSpan.Zero, | ||||||
| @@ -307,10 +340,12 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         await EnsureServiceProviderAsync(options); |         await EnsureServiceProviderAsync(options); | ||||||
|         var provider = _serviceProvider!; |         var provider = _serviceProvider!; | ||||||
|  |  | ||||||
|         var summaryUri = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf.json?after=2025-10-05&per_page=1&page=1"); |         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"); |         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(summaryUri, ReadFixture("summary-page3.json")); | ||||||
|  |         _handler.AddJsonResponse(summaryUriPost, "[]"); | ||||||
|         _handler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0003.json")); |         _handler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0003.json")); | ||||||
|  |  | ||||||
|         var stateRepository = provider.GetRequiredService<ISourceStateRepository>(); |         var stateRepository = provider.GetRequiredService<ISourceStateRepository>(); | ||||||
| @@ -339,22 +374,32 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|             .Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0003", StringComparison.Ordinal)); |             .Single(a => string.Equals(a.AdvisoryKey, "RHSA-2025:0003", StringComparison.Ordinal)); | ||||||
|  |  | ||||||
|         var references = advisory.References.ToArray(); |         var references = advisory.References.ToArray(); | ||||||
|         Assert.Equal(4, references.Length); |         Assert.Collection( | ||||||
|  |             references, | ||||||
|         Assert.Equal("exploit", references[0].Kind); |             reference => | ||||||
|         Assert.Equal("https://bugzilla.redhat.com/show_bug.cgi?id=2222222", references[0].Url); |             { | ||||||
|  |                 Assert.Equal("self", reference.Kind); | ||||||
|         Assert.Equal("external", references[1].Kind); |                 Assert.Equal("https://access.redhat.com/errata/RHSA-2025:0003", reference.Url); | ||||||
|         Assert.Equal("https://www.cve.org/CVERecord?id=CVE-2025-0003", references[1].Url); |                 Assert.Equal("Primary advisory", reference.Summary); | ||||||
|         Assert.Equal("CVE record", references[1].Summary); |             }, | ||||||
|  |             reference => | ||||||
|         Assert.Equal("mitigation", references[2].Kind); |             { | ||||||
|         Assert.Equal("https://access.redhat.com/solutions/999999", references[2].Url); |                 Assert.Equal("mitigation", reference.Kind); | ||||||
|         Assert.Equal("Knowledge base guidance", references[2].Summary); |                 Assert.Equal("https://access.redhat.com/solutions/999999", reference.Url); | ||||||
|  |                 Assert.Equal("Knowledge base guidance", reference.Summary); | ||||||
|         Assert.Equal("self", references[3].Kind); |             }, | ||||||
|         Assert.Equal("https://access.redhat.com/errata/RHSA-2025:0003", references[3].Url); |             reference => | ||||||
|         Assert.Equal("Primary advisory", references[3].Summary); |             { | ||||||
|  |                 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); | ||||||
|  |             }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async Task EnsureServiceProviderAsync(RedHatOptions options) |     private async Task EnsureServiceProviderAsync(RedHatOptions options) | ||||||
| @@ -385,6 +430,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         services.AddRedHatConnector(opts => |         services.AddRedHatConnector(opts => | ||||||
|         { |         { | ||||||
|             opts.BaseEndpoint = options.BaseEndpoint; |             opts.BaseEndpoint = options.BaseEndpoint; | ||||||
|  |             opts.SummaryPath = options.SummaryPath; | ||||||
|             opts.PageSize = options.PageSize; |             opts.PageSize = options.PageSize; | ||||||
|             opts.MaxPagesPerFetch = options.MaxPagesPerFetch; |             opts.MaxPagesPerFetch = options.MaxPagesPerFetch; | ||||||
|             opts.MaxAdvisoriesPerFetch = options.MaxAdvisoriesPerFetch; |             opts.MaxAdvisoriesPerFetch = options.MaxAdvisoriesPerFetch; | ||||||
| @@ -394,6 +440,17 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|             opts.UserAgent = options.UserAgent; |             opts.UserAgent = options.UserAgent; | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |         services.Configure<JobSchedulerOptions>(schedulerOptions => | ||||||
|  |         { | ||||||
|  |             var fetchType = Type.GetType("StellaOps.Feedser.Source.Distro.RedHat.RedHatFetchJob, StellaOps.Feedser.Source.Distro.RedHat", throwOnError: true)!; | ||||||
|  |             var parseType = Type.GetType("StellaOps.Feedser.Source.Distro.RedHat.RedHatParseJob, StellaOps.Feedser.Source.Distro.RedHat", throwOnError: true)!; | ||||||
|  |             var mapType = Type.GetType("StellaOps.Feedser.Source.Distro.RedHat.RedHatMapJob, StellaOps.Feedser.Source.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 => |         services.Configure<HttpClientFactoryOptions>(RedHatOptions.HttpClientName, builderOptions => | ||||||
|         { |         { | ||||||
|             builderOptions.HttpMessageHandlerBuilderActions.Add(builder => |             builderOptions.HttpMessageHandlerBuilderActions.Add(builder => | ||||||
| @@ -440,6 +497,12 @@ public sealed class RedHatConnectorTests : IAsyncLifetime | |||||||
|         return File.ReadAllText(path); |         return File.ReadAllText(path); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     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 Task InitializeAsync() => Task.CompletedTask; | ||||||
|  |  | ||||||
|     public async Task DisposeAsync() |     public async Task DisposeAsync() | ||||||
|   | |||||||
| @@ -11,6 +11,8 @@ | |||||||
|     <ProjectReference Include="../StellaOps.Feedser.Storage.Mongo/StellaOps.Feedser.Storage.Mongo.csproj" /> |     <ProjectReference Include="../StellaOps.Feedser.Storage.Mongo/StellaOps.Feedser.Storage.Mongo.csproj" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <None Include="RedHat/Fixtures/*.json" CopyToOutputDirectory="Always" /> |     <None Include="RedHat/Fixtures/*.json" | ||||||
|  |           CopyToOutputDirectory="Always" | ||||||
|  |           TargetPath="Source/Distro/RedHat/Fixtures/%(Filename)%(Extension)" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| </Project> | </Project> | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
| |Job scheduler registration aligns with Options pipeline|BE-Conn-RH|Core|**DONE** – registered fetch/parse/map via JobSchedulerBuilder, preserving option overrides and tightening cron/timeouts.| | |Job scheduler registration aligns with Options pipeline|BE-Conn-RH|Core|**DONE** – registered fetch/parse/map via JobSchedulerBuilder, preserving option overrides and tightening cron/timeouts.| | ||||||
| |Watermark persistence + resume|BE-Conn-RH|Storage.Mongo|**DONE** – cursor updates via SourceStateRepository.| | |Watermark persistence + resume|BE-Conn-RH|Storage.Mongo|**DONE** – cursor updates via SourceStateRepository.| | ||||||
| |Precedence tests vs NVD|QA|Merge|**DONE** – Added AffectedPackagePrecedenceResolver + tests ensuring Red Hat CPEs override NVD ranges.| | |Precedence tests vs NVD|QA|Merge|**DONE** – Added AffectedPackagePrecedenceResolver + tests ensuring Red Hat CPEs override NVD ranges.| | ||||||
| |Golden mapping fixtures|QA|Fixtures|DOING – added RHSA-2025:0002/0003 fixtures; need validation pass once connector regression fixed.| | |Golden mapping fixtures|QA|Fixtures|**DONE** – fixtures refreshed; RedHat connector tests updated and passing under new deterministic outputs.| | ||||||
| |Job scheduling defaults for source:redhat tasks|BE-Core|JobScheduler|**DONE** – Cron windows + per-job timeouts defined for fetch/parse/map.|  | |Job scheduling defaults for source:redhat tasks|BE-Core|JobScheduler|**DONE** – Cron windows + per-job timeouts defined for fetch/parse/map.|  | ||||||
| |Express unaffected/investigation statuses without overloading range fields|BE-Conn-RH|Models|**DONE** – Introduced AffectedPackageStatus collection and updated mapper/tests.| | |Express unaffected/investigation statuses without overloading range fields|BE-Conn-RH|Models|**DONE** – Introduced AffectedPackageStatus collection and updated mapper/tests.| | ||||||
| |Reference dedupe & ordering in mapper|BE-Conn-RH|Models|DONE – mapper consolidates by URL, merges metadata, deterministic ordering validated in tests.| | |Reference dedupe & ordering in mapper|BE-Conn-RH|Models|DONE – mapper consolidates by URL, merges metadata, deterministic ordering validated in tests.| | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user