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")),
|
||||
});
|
||||
|
||||
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[]
|
||||
|
||||
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.|
|
||||
|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.|
|
||||
|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.|
|
||||
|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",
|
||||
"introducedVersion": null,
|
||||
"lastAffectedVersion": null,
|
||||
"lastAffectedVersion": "kernel-0:4.18.0-500.1.0.el8.x86_64",
|
||||
"provenance": {
|
||||
"kind": "package.nevra",
|
||||
"recordedAt": "2025-10-05T00:00:00+00:00",
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -31,8 +31,8 @@ public sealed class RedHatConnectorHarnessTests : IAsyncLifetime
|
||||
var options = new RedHatOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
|
||||
PageSize = 1,
|
||||
MaxPagesPerFetch = 1,
|
||||
PageSize = 10,
|
||||
MaxPagesPerFetch = 2,
|
||||
MaxAdvisoriesPerFetch = 5,
|
||||
InitialBackfill = TimeSpan.FromDays(1),
|
||||
Overlap = TimeSpan.Zero,
|
||||
@@ -43,11 +43,17 @@ public sealed class RedHatConnectorHarnessTests : IAsyncLifetime
|
||||
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=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 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(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json"));
|
||||
|
||||
await _harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
@@ -89,13 +95,16 @@ public sealed class RedHatConnectorHarnessTests : IAsyncLifetime
|
||||
|
||||
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None);
|
||||
Assert.Single(advisories);
|
||||
var advisory = advisories.Single();
|
||||
Assert.Equal("RHSA-2025:0001", advisory.AdvisoryKey);
|
||||
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);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -55,8 +57,8 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
var options = new RedHatOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
|
||||
PageSize = 1,
|
||||
MaxPagesPerFetch = 1,
|
||||
PageSize = 10,
|
||||
MaxPagesPerFetch = 2,
|
||||
MaxAdvisoriesPerFetch = 25,
|
||||
InitialBackfill = TimeSpan.FromDays(1),
|
||||
Overlap = TimeSpan.Zero,
|
||||
@@ -68,14 +70,27 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
var provider = _serviceProvider!;
|
||||
|
||||
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 detailUri2 = new Uri("https://access.redhat.com/hydra/rest/securitydata/csaf/RHSA-2025:0002.json");
|
||||
|
||||
_output.WriteLine($"Registering summary URI: {summaryUri}");
|
||||
_handler.AddJsonResponse(summaryUri, ReadFixture("summary-page1.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(
|
||||
@@ -98,6 +113,12 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
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));
|
||||
@@ -130,7 +151,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
_output.WriteLine("-- RHSA-2025:0001 snapshot --\n" + snapshot);
|
||||
var snapshotPath = Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "RedHat", "Fixtures", "rhsa-2025-0001.snapshot.json");
|
||||
var expectedSnapshot = File.ReadAllText(snapshotPath);
|
||||
Assert.Equal(expectedSnapshot, snapshot);
|
||||
Assert.Equal(NormalizeLineEndings(expectedSnapshot), NormalizeLineEndings(snapshot));
|
||||
|
||||
var state = await stateRepository.TryGetAsync(RedHatConnectorPlugin.SourceName, CancellationToken.None);
|
||||
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 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 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(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.ParseAsync(provider, CancellationToken.None);
|
||||
@@ -185,12 +210,14 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
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";
|
||||
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, underInvestigation, StringComparison.Ordinal));
|
||||
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);
|
||||
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
|
||||
{
|
||||
BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
|
||||
PageSize = 1,
|
||||
MaxPagesPerFetch = 1,
|
||||
PageSize = 10,
|
||||
MaxPagesPerFetch = 2,
|
||||
MaxAdvisoriesPerFetch = 25,
|
||||
InitialBackfill = TimeSpan.FromDays(1),
|
||||
Overlap = TimeSpan.Zero,
|
||||
@@ -220,12 +247,18 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
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 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.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(detailUri2, ReadFixture("csaf-rhsa-2025-0002.json"));
|
||||
|
||||
Guid[] pendingDocumentIds;
|
||||
await using (var fetchProvider = await CreateServiceProviderAsync(options, fetchHandler))
|
||||
@@ -295,8 +328,8 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
var options = new RedHatOptions
|
||||
{
|
||||
BaseEndpoint = new Uri("https://access.redhat.com/hydra/rest/securitydata"),
|
||||
PageSize = 1,
|
||||
MaxPagesPerFetch = 1,
|
||||
PageSize = 10,
|
||||
MaxPagesPerFetch = 2,
|
||||
MaxAdvisoriesPerFetch = 10,
|
||||
InitialBackfill = TimeSpan.FromDays(7),
|
||||
Overlap = TimeSpan.Zero,
|
||||
@@ -307,10 +340,12 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
await EnsureServiceProviderAsync(options);
|
||||
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");
|
||||
|
||||
_handler.AddJsonResponse(summaryUri, ReadFixture("summary-page3.json"));
|
||||
_handler.AddJsonResponse(summaryUriPost, "[]");
|
||||
_handler.AddJsonResponse(detailUri, ReadFixture("csaf-rhsa-2025-0003.json"));
|
||||
|
||||
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));
|
||||
|
||||
var references = advisory.References.ToArray();
|
||||
Assert.Equal(4, references.Length);
|
||||
|
||||
Assert.Equal("exploit", references[0].Kind);
|
||||
Assert.Equal("https://bugzilla.redhat.com/show_bug.cgi?id=2222222", references[0].Url);
|
||||
|
||||
Assert.Equal("external", references[1].Kind);
|
||||
Assert.Equal("https://www.cve.org/CVERecord?id=CVE-2025-0003", references[1].Url);
|
||||
Assert.Equal("CVE record", references[1].Summary);
|
||||
|
||||
Assert.Equal("mitigation", references[2].Kind);
|
||||
Assert.Equal("https://access.redhat.com/solutions/999999", references[2].Url);
|
||||
Assert.Equal("Knowledge base guidance", references[2].Summary);
|
||||
|
||||
Assert.Equal("self", references[3].Kind);
|
||||
Assert.Equal("https://access.redhat.com/errata/RHSA-2025:0003", references[3].Url);
|
||||
Assert.Equal("Primary advisory", references[3].Summary);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task EnsureServiceProviderAsync(RedHatOptions options)
|
||||
@@ -385,6 +430,7 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
services.AddRedHatConnector(opts =>
|
||||
{
|
||||
opts.BaseEndpoint = options.BaseEndpoint;
|
||||
opts.SummaryPath = options.SummaryPath;
|
||||
opts.PageSize = options.PageSize;
|
||||
opts.MaxPagesPerFetch = options.MaxPagesPerFetch;
|
||||
opts.MaxAdvisoriesPerFetch = options.MaxAdvisoriesPerFetch;
|
||||
@@ -394,6 +440,17 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
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 =>
|
||||
{
|
||||
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
|
||||
@@ -440,6 +497,12 @@ public sealed class RedHatConnectorTests : IAsyncLifetime
|
||||
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 async Task DisposeAsync()
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
<ProjectReference Include="../StellaOps.Feedser.Storage.Mongo/StellaOps.Feedser.Storage.Mongo.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="RedHat/Fixtures/*.json" CopyToOutputDirectory="Always" />
|
||||
<None Include="RedHat/Fixtures/*.json"
|
||||
CopyToOutputDirectory="Always"
|
||||
TargetPath="Source/Distro/RedHat/Fixtures/%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
</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.|
|
||||
|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.|
|
||||
|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.|
|
||||
|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 in New Issue
Block a user