up
This commit is contained in:
		| @@ -44,6 +44,18 @@ public static class SemVerPrimitiveExtensions | ||||
|                 notes: resolvedNotes); | ||||
|         } | ||||
|  | ||||
|         if (!string.IsNullOrEmpty(introduced) && string.IsNullOrEmpty(fixedVersion) && !string.IsNullOrEmpty(lastAffected)) | ||||
|         { | ||||
|             return new NormalizedVersionRule( | ||||
|                 scheme, | ||||
|                 NormalizedVersionRuleTypes.Range, | ||||
|                 min: introduced, | ||||
|                 minInclusive: primitive.IntroducedInclusive, | ||||
|                 max: lastAffected, | ||||
|                 maxInclusive: primitive.LastAffectedInclusive, | ||||
|                 notes: resolvedNotes); | ||||
|         } | ||||
|  | ||||
|         if (!string.IsNullOrEmpty(introduced) && string.IsNullOrEmpty(fixedVersion) && string.IsNullOrEmpty(lastAffected)) | ||||
|         { | ||||
|             var type = primitive.IntroducedInclusive ? NormalizedVersionRuleTypes.GreaterThanOrEqual : NormalizedVersionRuleTypes.GreaterThan; | ||||
|   | ||||
| @@ -110,4 +110,52 @@ public sealed class SemVerRangeRuleBuilderTests | ||||
|         Assert.Equal("2.5.1-alpha.1+build.7", normalized.Value); | ||||
|         Assert.Equal(Note, normalized.Notes); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Build_ParsesComparatorWithoutCommaSeparators() | ||||
|     { | ||||
|         var results = SemVerRangeRuleBuilder.Build(">=1.0.0 <1.2.0", null, Note); | ||||
|         var result = Assert.Single(results); | ||||
|  | ||||
|         var primitive = result.Primitive; | ||||
|         Assert.Equal("1.0.0", primitive.Introduced); | ||||
|         Assert.True(primitive.IntroducedInclusive); | ||||
|         Assert.Equal("1.2.0", primitive.Fixed); | ||||
|         Assert.False(primitive.FixedInclusive); | ||||
|         Assert.Equal(">= 1.0.0, < 1.2.0", primitive.ConstraintExpression); | ||||
|  | ||||
|         var normalized = result.NormalizedRule; | ||||
|         Assert.Equal(NormalizedVersionRuleTypes.Range, normalized.Type); | ||||
|         Assert.Equal("1.0.0", normalized.Min); | ||||
|         Assert.True(normalized.MinInclusive); | ||||
|         Assert.Equal("1.2.0", normalized.Max); | ||||
|         Assert.False(normalized.MaxInclusive); | ||||
|         Assert.Equal(Note, normalized.Notes); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void Build_HandlesMultipleSegmentsSeparatedByOr() | ||||
|     { | ||||
|         var results = SemVerRangeRuleBuilder.Build(">=1.0.0 <1.2.0 || >=2.0.0 <2.2.0", null, Note); | ||||
|         Assert.Equal(2, results.Count); | ||||
|  | ||||
|         var first = results[0]; | ||||
|         Assert.Equal("1.0.0", first.Primitive.Introduced); | ||||
|         Assert.Equal("1.2.0", first.Primitive.Fixed); | ||||
|         Assert.Equal(NormalizedVersionRuleTypes.Range, first.NormalizedRule.Type); | ||||
|         Assert.Equal("1.0.0", first.NormalizedRule.Min); | ||||
|         Assert.Equal("1.2.0", first.NormalizedRule.Max); | ||||
|  | ||||
|         var second = results[1]; | ||||
|         Assert.Equal("2.0.0", second.Primitive.Introduced); | ||||
|         Assert.Equal("2.2.0", second.Primitive.Fixed); | ||||
|         Assert.Equal(NormalizedVersionRuleTypes.Range, second.NormalizedRule.Type); | ||||
|         Assert.Equal("2.0.0", second.NormalizedRule.Min); | ||||
|         Assert.Equal("2.2.0", second.NormalizedRule.Max); | ||||
|  | ||||
|         foreach (var result in results) | ||||
|         { | ||||
|             Assert.Equal(Note, result.NormalizedRule.Notes); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| @@ -15,6 +16,7 @@ namespace StellaOps.Feedser.Normalization.SemVer; | ||||
| public static class SemVerRangeRuleBuilder | ||||
| { | ||||
|     private static readonly Regex ComparatorRegex = new(@"^(?<op>>=|<=|>|<|==|=)\s*(?<value>.+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant); | ||||
|     private static readonly Regex ComparatorTokenRegex = new(@"(?<token>(?<op>>=|<=|>|<|==|=)\s*(?<value>[^,\s\|]+))", RegexOptions.Compiled | RegexOptions.CultureInvariant); | ||||
|     private static readonly Regex HyphenRegex = new(@"^\s*(?<start>.+?)\s+-\s+(?<end>.+)\s*$", RegexOptions.Compiled | RegexOptions.CultureInvariant); | ||||
|     private static readonly char[] SegmentTrimCharacters = { '(', ')', '[', ']', '{', '}', ';' }; | ||||
|     private static readonly char[] FragmentSplitCharacters = { ',', ' ' }; | ||||
| @@ -303,21 +305,29 @@ public static class SemVerRangeRuleBuilder | ||||
|  | ||||
|         foreach (var fragment in fragments) | ||||
|         { | ||||
|             if (TryParseComparatorFragment(fragment, constraintTokens, ref introduced, ref introducedInclusive, ref hasIntroduced, ref fixedVersion, ref fixedInclusive, ref hasFixed, ref lastAffected, ref lastInclusive, ref hasLast, ref exactValue)) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var parts = fragment.Split(FragmentSplitCharacters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); | ||||
|             var handled = false; | ||||
|             foreach (var part in parts) | ||||
|  | ||||
|             foreach (Match match in ComparatorTokenRegex.Matches(fragment)) | ||||
|             { | ||||
|                 if (TryParseComparatorFragment(part, constraintTokens, ref introduced, ref introducedInclusive, ref hasIntroduced, ref fixedVersion, ref fixedInclusive, ref hasFixed, ref lastAffected, ref lastInclusive, ref hasLast, ref exactValue)) | ||||
|                 if (match.Groups["token"].Success | ||||
|                     && TryParseComparatorFragment(match.Groups["token"].Value, constraintTokens, ref introduced, ref introducedInclusive, ref hasIntroduced, ref fixedVersion, ref fixedInclusive, ref hasFixed, ref lastAffected, ref lastInclusive, ref hasLast, ref exactValue)) | ||||
|                 { | ||||
|                     handled = true; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (!handled) | ||||
|             { | ||||
|                 var parts = fragment.Split(FragmentSplitCharacters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); | ||||
|                 foreach (var part in parts) | ||||
|                 { | ||||
|                     if (TryParseComparatorFragment(part, constraintTokens, ref introduced, ref introducedInclusive, ref hasIntroduced, ref fixedVersion, ref fixedInclusive, ref hasFixed, ref lastAffected, ref lastInclusive, ref hasLast, ref exactValue)) | ||||
|                     { | ||||
|                         handled = true; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (handled) | ||||
|             { | ||||
|                 continue; | ||||
| @@ -339,7 +349,8 @@ public static class SemVerRangeRuleBuilder | ||||
|                 FixedInclusive: true, | ||||
|                 LastAffected: null, | ||||
|                 LastAffectedInclusive: false, | ||||
|                 ConstraintExpression: constraintTokens.Count > 0 ? string.Join(", ", constraintTokens) : expression.Trim()); | ||||
|                 ConstraintExpression: constraintTokens.Count > 0 ? string.Join(", ", constraintTokens) : expression.Trim(), | ||||
|                 ExactValue: exactValue); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
| @@ -426,32 +437,32 @@ public static class SemVerRangeRuleBuilder | ||||
|         switch (op) | ||||
|         { | ||||
|             case ">=": | ||||
|                 introduced = value; | ||||
|                 introduced = value!; | ||||
|                 introducedInclusive = true; | ||||
|                 hasIntroduced = true; | ||||
|                 constraintTokens.Add($">= {value}"); | ||||
|                 break; | ||||
|             case ">": | ||||
|                 introduced = value; | ||||
|                 introduced = value!; | ||||
|                 introducedInclusive = false; | ||||
|                 hasIntroduced = true; | ||||
|                 constraintTokens.Add($"> {value}"); | ||||
|                 break; | ||||
|             case "<=": | ||||
|                 lastAffected = value; | ||||
|                 lastAffected = value!; | ||||
|                 lastInclusive = true; | ||||
|                 hasLast = true; | ||||
|                 constraintTokens.Add($"<= {value}"); | ||||
|                 break; | ||||
|             case "<": | ||||
|                 fixedVersion = value; | ||||
|                 fixedVersion = value!; | ||||
|                 fixedInclusive = false; | ||||
|                 hasFixed = true; | ||||
|                 constraintTokens.Add($"< {value}"); | ||||
|                 break; | ||||
|             case "=": | ||||
|             case "==": | ||||
|                 exactValue = value; | ||||
|                 exactValue = value!; | ||||
|                 constraintTokens.Add($"= {value}"); | ||||
|                 break; | ||||
|         } | ||||
| @@ -459,7 +470,7 @@ public static class SemVerRangeRuleBuilder | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     private static bool TryNormalizeVersion(string? value, out string normalized) | ||||
|     private static bool TryNormalizeVersion(string? value, [NotNullWhen(true)] out string normalized) | ||||
|     { | ||||
|         normalized = string.Empty; | ||||
|         if (string.IsNullOrWhiteSpace(value)) | ||||
| @@ -492,22 +503,23 @@ public static class SemVerRangeRuleBuilder | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     private static bool TryParseSemanticVersion(string value, out SemanticVersion version, out string normalized) | ||||
|     private static bool TryParseSemanticVersion(string value, [NotNullWhen(true)] out SemanticVersion version, out string normalized) | ||||
|     { | ||||
|         version = null!; | ||||
|         normalized = string.Empty; | ||||
|  | ||||
|         var candidate = RemoveLeadingV(value); | ||||
|         if (!SemanticVersion.TryParse(candidate, out version)) | ||||
|         if (!SemanticVersion.TryParse(candidate, out var parsed)) | ||||
|         { | ||||
|             candidate = ExpandSemanticVersion(candidate); | ||||
|             if (!SemanticVersion.TryParse(candidate, out version)) | ||||
|             if (!SemanticVersion.TryParse(candidate, out parsed)) | ||||
|             { | ||||
|                 version = null!; | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         normalized = FormatVersion(version); | ||||
|         version = parsed!; | ||||
|         normalized = FormatVersion(parsed); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -4,5 +4,6 @@ | ||||
| |Canonical NEVRA/EVR parsing helpers|BE-Norm (Distro WG)|Models|DONE – `Normalization.Distro` exposes parsers + canonical formatters consumed by Merge comparers/tests.| | ||||
| |PURL/CPE identifier normalization|BE-Norm (OSS WG)|Models|DONE – canonical PURL/CPE helpers feed connectors and exporter tooling.| | ||||
| |CPE normalization escape handling|BE-Norm (OSS WG)|Normalization identifiers|DONE – percent-decoding, edition sub-field expansion, and deterministic escaping landed in `Cpe23` with new tests covering boundary cases.| | ||||
| |CVSS metric normalization & severity bands|BE-Norm (Risk WG)|Models|DONE – `CvssMetricNormalizer` unifies vectors, recomputes scores/severities, and is wired through NVD/RedHat/JVN mappers with unit coverage.| | ||||
| |Description and locale normalization pipeline|BE-Norm (I18N)|Source connectors|DONE – `DescriptionNormalizer` strips markup, collapses whitespace, and provides locale fallback used by core mappers.| | ||||
| |CVSS metric normalization & severity bands|BE-Norm (Risk WG)|Models|DONE – `CvssMetricNormalizer` unifies vectors, recomputes scores/severities, and is wired through NVD/RedHat/JVN mappers with unit coverage.| | ||||
| |Description and locale normalization pipeline|BE-Norm (I18N)|Source connectors|DONE – `DescriptionNormalizer` strips markup, collapses whitespace, and provides locale fallback used by core mappers.| | ||||
| |SemVer normalized rule emitter (FEEDNORM-NORM-02-001)|BE-Norm (SemVer WG)|Models, `FASTER_MODELING_AND_NORMALIZATION.md`|**DONE (2025-10-12)** – `SemVerRangeRuleBuilder` now parses comparator chains without comma delimiters, supports multi-segment `||` ranges, pushes exact-value metadata, and new tests document the contract for connector teams.| | ||||
|   | ||||
| @@ -37,6 +37,8 @@ public sealed class GhsaConnector : IFeedConnector | ||||
|     private readonly GhsaDiagnostics _diagnostics; | ||||
|     private readonly TimeProvider _timeProvider; | ||||
|     private readonly ILogger<GhsaConnector> _logger; | ||||
|     private readonly object _rateLimitWarningLock = new(); | ||||
|     private readonly Dictionary<(string Phase, string Resource), bool> _rateLimitWarnings = new(); | ||||
|  | ||||
|     public GhsaConnector( | ||||
|         SourceFetchService fetchService, | ||||
| @@ -412,6 +414,62 @@ public sealed class GhsaConnector : IFeedConnector | ||||
|         await _stateRepository.UpdateCursorAsync(SourceName, cursor.ToBsonDocument(), _timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false); | ||||
|     } | ||||
|  | ||||
|     private bool ShouldLogRateLimitWarning(in GhsaRateLimitSnapshot snapshot, out bool recovered) | ||||
|     { | ||||
|         recovered = false; | ||||
|  | ||||
|         if (!snapshot.Remaining.HasValue) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         var key = (snapshot.Phase, snapshot.Resource ?? "global"); | ||||
|         var warn = snapshot.Remaining.Value <= _options.RateLimitWarningThreshold; | ||||
|  | ||||
|         lock (_rateLimitWarningLock) | ||||
|         { | ||||
|             var previouslyWarned = _rateLimitWarnings.TryGetValue(key, out var flagged) && flagged; | ||||
|  | ||||
|             if (warn) | ||||
|             { | ||||
|                 if (previouslyWarned) | ||||
|                 { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 _rateLimitWarnings[key] = true; | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             if (previouslyWarned) | ||||
|             { | ||||
|                 _rateLimitWarnings.Remove(key); | ||||
|                 recovered = true; | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static double? CalculateHeadroomPercentage(in GhsaRateLimitSnapshot snapshot) | ||||
|     { | ||||
|         if (!snapshot.Limit.HasValue || !snapshot.Remaining.HasValue) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         var limit = snapshot.Limit.Value; | ||||
|         if (limit <= 0) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return (double)snapshot.Remaining.Value / limit * 100d; | ||||
|     } | ||||
|  | ||||
|     private static string FormatHeadroom(double? headroomPct) | ||||
|         => headroomPct.HasValue ? $" (headroom {headroomPct.Value:F1}%)" : string.Empty; | ||||
|  | ||||
|     private async Task<bool> ApplyRateLimitAsync(IReadOnlyDictionary<string, string>? headers, string phase, CancellationToken cancellationToken) | ||||
|     { | ||||
|         var snapshot = GhsaRateLimitParser.TryParse(headers, _timeProvider.GetUtcNow(), phase); | ||||
| @@ -422,19 +480,31 @@ public sealed class GhsaConnector : IFeedConnector | ||||
|  | ||||
|         _diagnostics.RecordRateLimit(snapshot.Value); | ||||
|  | ||||
|         if (snapshot.Value.Remaining.HasValue && snapshot.Value.Remaining.Value <= _options.RateLimitWarningThreshold) | ||||
|         var headroomPct = CalculateHeadroomPercentage(snapshot.Value); | ||||
|         if (ShouldLogRateLimitWarning(snapshot.Value, out var recovered)) | ||||
|         { | ||||
|             var resetMessage = snapshot.Value.ResetAfter.HasValue | ||||
|                 ? $" (resets in {snapshot.Value.ResetAfter.Value:c})" | ||||
|                 : snapshot.Value.ResetAt.HasValue ? $" (resets at {snapshot.Value.ResetAt.Value:O})" : string.Empty; | ||||
|  | ||||
|             _logger.LogWarning( | ||||
|                 "GHSA rate limit warning: remaining {Remaining} of {Limit} for {Phase} {Resource}{ResetMessage}", | ||||
|                 "GHSA rate limit warning: remaining {Remaining} of {Limit} for {Phase} {Resource}{ResetMessage}{Headroom}", | ||||
|                 snapshot.Value.Remaining, | ||||
|                 snapshot.Value.Limit, | ||||
|                 phase, | ||||
|                 snapshot.Value.Resource ?? "global", | ||||
|                 resetMessage); | ||||
|                 resetMessage, | ||||
|                 FormatHeadroom(headroomPct)); | ||||
|         } | ||||
|         else if (recovered) | ||||
|         { | ||||
|             _logger.LogInformation( | ||||
|                 "GHSA rate limit recovered for {Phase} {Resource}: remaining {Remaining} of {Limit}{Headroom}", | ||||
|                 phase, | ||||
|                 snapshot.Value.Resource ?? "global", | ||||
|                 snapshot.Value.Remaining, | ||||
|                 snapshot.Value.Limit, | ||||
|                 FormatHeadroom(headroomPct)); | ||||
|         } | ||||
|  | ||||
|         if (snapshot.Value.Remaining.HasValue && snapshot.Value.Remaining.Value <= 0) | ||||
| @@ -445,10 +515,11 @@ public sealed class GhsaConnector : IFeedConnector | ||||
|             if (delay > TimeSpan.Zero) | ||||
|             { | ||||
|                 _logger.LogWarning( | ||||
|                     "GHSA rate limit exhausted for {Phase} {Resource}; delaying {Delay}", | ||||
|                     "GHSA rate limit exhausted for {Phase} {Resource}; delaying {Delay}{Headroom}", | ||||
|                     phase, | ||||
|                     snapshot.Value.Resource ?? "global", | ||||
|                     delay); | ||||
|                     delay, | ||||
|                     FormatHeadroom(headroomPct)); | ||||
|                 await Task.Delay(delay, cancellationToken).ConfigureAwait(false); | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.Metrics; | ||||
|  | ||||
| namespace StellaOps.Feedser.Source.Ghsa.Internal; | ||||
| @@ -19,9 +20,12 @@ public sealed class GhsaDiagnostics : IDisposable | ||||
|     private readonly Histogram<long> _rateLimitRemaining; | ||||
|     private readonly Histogram<long> _rateLimitLimit; | ||||
|     private readonly Histogram<double> _rateLimitResetSeconds; | ||||
|     private readonly Histogram<double> _rateLimitHeadroomPct; | ||||
|     private readonly ObservableGauge<double> _rateLimitHeadroomGauge; | ||||
|     private readonly Counter<long> _rateLimitExhausted; | ||||
|     private readonly object _rateLimitLock = new(); | ||||
|     private GhsaRateLimitSnapshot? _lastRateLimitSnapshot; | ||||
|     private readonly Dictionary<(string Phase, string? Resource), GhsaRateLimitSnapshot> _rateLimitSnapshots = new(); | ||||
|  | ||||
|     public GhsaDiagnostics() | ||||
|     { | ||||
| @@ -37,6 +41,8 @@ public sealed class GhsaDiagnostics : IDisposable | ||||
|         _rateLimitRemaining = _meter.CreateHistogram<long>("ghsa.ratelimit.remaining", unit: "requests"); | ||||
|         _rateLimitLimit = _meter.CreateHistogram<long>("ghsa.ratelimit.limit", unit: "requests"); | ||||
|         _rateLimitResetSeconds = _meter.CreateHistogram<double>("ghsa.ratelimit.reset_seconds", unit: "s"); | ||||
|         _rateLimitHeadroomPct = _meter.CreateHistogram<double>("ghsa.ratelimit.headroom_pct", unit: "percent"); | ||||
|         _rateLimitHeadroomGauge = _meter.CreateObservableGauge("ghsa.ratelimit.headroom_pct_current", ObserveHeadroom, unit: "percent"); | ||||
|         _rateLimitExhausted = _meter.CreateCounter<long>("ghsa.ratelimit.exhausted", unit: "events"); | ||||
|     } | ||||
|  | ||||
| @@ -79,9 +85,15 @@ public sealed class GhsaDiagnostics : IDisposable | ||||
|             _rateLimitResetSeconds.Record(snapshot.ResetAfter.Value.TotalSeconds, tags); | ||||
|         } | ||||
|  | ||||
|         if (TryCalculateHeadroom(snapshot, out var headroom)) | ||||
|         { | ||||
|             _rateLimitHeadroomPct.Record(headroom, tags); | ||||
|         } | ||||
|  | ||||
|         lock (_rateLimitLock) | ||||
|         { | ||||
|             _lastRateLimitSnapshot = snapshot; | ||||
|             _rateLimitSnapshots[(snapshot.Phase, snapshot.Resource)] = snapshot; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -96,5 +108,48 @@ public sealed class GhsaDiagnostics : IDisposable | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void Dispose() => _meter.Dispose(); | ||||
|     private IEnumerable<Measurement<double>> ObserveHeadroom() | ||||
|     { | ||||
|         lock (_rateLimitLock) | ||||
|         { | ||||
|             if (_rateLimitSnapshots.Count == 0) | ||||
|             { | ||||
|                 yield break; | ||||
|             } | ||||
|  | ||||
|             foreach (var snapshot in _rateLimitSnapshots.Values) | ||||
|             { | ||||
|                 if (TryCalculateHeadroom(snapshot, out var headroom)) | ||||
|                 { | ||||
|                     yield return new Measurement<double>( | ||||
|                         headroom, | ||||
|                         new KeyValuePair<string, object?>("phase", snapshot.Phase), | ||||
|                         new KeyValuePair<string, object?>("resource", snapshot.Resource ?? "unknown")); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static bool TryCalculateHeadroom(in GhsaRateLimitSnapshot snapshot, out double headroomPct) | ||||
|     { | ||||
|         headroomPct = 0; | ||||
|         if (!snapshot.Limit.HasValue || !snapshot.Remaining.HasValue) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         var limit = snapshot.Limit.Value; | ||||
|         if (limit <= 0) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         headroomPct = (double)snapshot.Remaining.Value / limit * 100d; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public void Dispose() | ||||
|     { | ||||
|         _meter.Dispose(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,5 +11,7 @@ | ||||
| |Production credential & scheduler rollout|Ops, BE-Conn-GHSA|Docs, WebService|**DONE (2025-10-12)** – Scheduler defaults registered via `JobSchedulerBuilder`, credential provisioning documented (Compose/Helm samples), and staged backfill guidance captured in `docs/ops/feedser-ghsa-operations.md`.| | ||||
| |FEEDCONN-GHSA-04-002 Conflict regression fixtures|BE-Conn-GHSA, QA|Merge `FEEDMERGE-ENGINE-04-001`|**DONE (2025-10-12)** – Added `conflict-ghsa.canonical.json` + `GhsaConflictFixtureTests`; SemVer ranges and credits align with merge precedence triple and shareable with QA. Validation: `dotnet test src/StellaOps.Feedser.Source.Ghsa.Tests/StellaOps.Feedser.Source.Ghsa.Tests.csproj --filter GhsaConflictFixtureTests`.| | ||||
| |FEEDCONN-GHSA-02-004 GHSA credits & ecosystem severity mapping|BE-Conn-GHSA|Models `FEEDMODELS-SCHEMA-01-002`|**DONE (2025-10-11)** – Mapper emits advisory credits with provenance masks, fixtures assert role/contact ordering, and severity normalization remains unchanged.| | ||||
| |FEEDCONN-GHSA-02-007 Credit parity regression fixtures|BE-Conn-GHSA, QA|Source.Nvd, Source.Osv|**DONE (2025-10-12)** – Credit parity fixtures recorded, regression tests cover GHSA/OSV/NVD alignment, and regeneration workflow documented in `docs/dev/fixtures.md`.| | ||||
| |FEEDCONN-GHSA-02-007 Credit parity regression fixtures|BE-Conn-GHSA, QA|Source.Nvd, Source.Osv|**DONE (2025-10-12)** – Parity fixtures regenerated via `tools/FixtureUpdater`, normalized SemVer notes verified against GHSA/NVD/OSV snapshots, and the fixtures guide now documents the headroom checks.| | ||||
| |FEEDCONN-GHSA-02-001 Normalized versions rollout|BE-Conn-GHSA|Models `FEEDMODELS-SCHEMA-01-003`, Normalization playbook|**DONE (2025-10-11)** – GHSA mapper now emits SemVer primitives + normalized ranges, fixtures refreshed, connector tests passing; report logged via FEEDMERGE-COORD-02-900.| | ||||
| |FEEDCONN-GHSA-02-005 Quota monitoring hardening|BE-Conn-GHSA, Observability|Source.Common metrics|**DONE (2025-10-12)** – Diagnostics expose headroom histograms/gauges, warning logs dedupe below the configured threshold, and the ops runbook gained alerting and mitigation guidance.| | ||||
| |FEEDCONN-GHSA-02-006 Scheduler rollout integration|BE-Conn-GHSA, Ops|Job scheduler|**DONE (2025-10-12)** – Dependency routine tests assert cron/timeouts, and the runbook highlights cron overrides plus backoff toggles for staged rollouts.| | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "resultsPerPage": 1, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 1, | ||||
|   "vulnerabilities": "this-should-be-an-array" | ||||
| } | ||||
| { | ||||
|   "resultsPerPage": 1, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 1, | ||||
|   "vulnerabilities": "this-should-be-an-array" | ||||
| } | ||||
|   | ||||
| @@ -1,69 +1,69 @@ | ||||
| { | ||||
|   "resultsPerPage": 2, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 5, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1000", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T10:00:00Z", | ||||
|         "lastModified": "2024-02-02T10:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability one." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", | ||||
|                 "baseScore": 9.8, | ||||
|                 "baseSeverity": "CRITICAL" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_a:1.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1001", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T11:00:00Z", | ||||
|         "lastModified": "2024-02-02T11:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability two." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:P/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:L", | ||||
|                 "baseScore": 5.1, | ||||
|                 "baseSeverity": "MEDIUM" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_b:2.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| { | ||||
|   "resultsPerPage": 2, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 5, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1000", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T10:00:00Z", | ||||
|         "lastModified": "2024-02-02T10:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability one." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", | ||||
|                 "baseScore": 9.8, | ||||
|                 "baseSeverity": "CRITICAL" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_a:1.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1001", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T11:00:00Z", | ||||
|         "lastModified": "2024-02-02T11:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability two." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:P/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:L", | ||||
|                 "baseScore": 5.1, | ||||
|                 "baseSeverity": "MEDIUM" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_b:2.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -1,69 +1,69 @@ | ||||
| { | ||||
|   "resultsPerPage": 2, | ||||
|   "startIndex": 2, | ||||
|   "totalResults": 5, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1002", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T12:00:00Z", | ||||
|         "lastModified": "2024-02-02T12:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability three." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", | ||||
|                 "baseScore": 3.1, | ||||
|                 "baseSeverity": "LOW" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_c:3.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1003", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T13:00:00Z", | ||||
|         "lastModified": "2024-02-02T13:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability four." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:M/I:L/A:L", | ||||
|                 "baseScore": 7.4, | ||||
|                 "baseSeverity": "HIGH" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_d:4.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| { | ||||
|   "resultsPerPage": 2, | ||||
|   "startIndex": 2, | ||||
|   "totalResults": 5, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1002", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T12:00:00Z", | ||||
|         "lastModified": "2024-02-02T12:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability three." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", | ||||
|                 "baseScore": 3.1, | ||||
|                 "baseSeverity": "LOW" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_c:3.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1003", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T13:00:00Z", | ||||
|         "lastModified": "2024-02-02T13:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability four." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:M/I:L/A:L", | ||||
|                 "baseScore": 7.4, | ||||
|                 "baseSeverity": "HIGH" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_d:4.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -1,38 +1,38 @@ | ||||
| { | ||||
|   "resultsPerPage": 2, | ||||
|   "startIndex": 4, | ||||
|   "totalResults": 5, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1004", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T14:00:00Z", | ||||
|         "lastModified": "2024-02-02T14:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability five." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L", | ||||
|                 "baseScore": 7.9, | ||||
|                 "baseSeverity": "HIGH" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_e:5.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| { | ||||
|   "resultsPerPage": 2, | ||||
|   "startIndex": 4, | ||||
|   "totalResults": 5, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-1004", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-02-01T14:00:00Z", | ||||
|         "lastModified": "2024-02-02T14:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Multipage vulnerability five." } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:L/I:H/A:L", | ||||
|                 "baseScore": 7.9, | ||||
|                 "baseSeverity": "HIGH" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_e:5.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -1,23 +1,23 @@ | ||||
| { | ||||
|   "resultsPerPage": 2000, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 2, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-0001", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-01-01T10:00:00Z", | ||||
|         "lastModified": "2024-01-02T10:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Example vulnerability one." } | ||||
|         ], | ||||
|         "references": [ | ||||
|           { | ||||
|             "url": "https://vendor.example.com/advisories/0001", | ||||
|             "source": "Vendor", | ||||
|             "tags": ["Vendor Advisory"] | ||||
|           } | ||||
| { | ||||
|   "resultsPerPage": 2000, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 2, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-0001", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-01-01T10:00:00Z", | ||||
|         "lastModified": "2024-01-02T10:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Example vulnerability one." } | ||||
|         ], | ||||
|         "references": [ | ||||
|           { | ||||
|             "url": "https://vendor.example.com/advisories/0001", | ||||
|             "source": "Vendor", | ||||
|             "tags": ["Vendor Advisory"] | ||||
|           } | ||||
|         ], | ||||
|         "weaknesses": [ | ||||
|           { | ||||
| @@ -28,43 +28,43 @@ | ||||
|           } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", | ||||
|                 "baseScore": 9.8, | ||||
|                 "baseSeverity": "CRITICAL" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-0002", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-01-01T11:00:00Z", | ||||
|         "lastModified": "2024-01-02T11:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "fr", "value": "Description française" }, | ||||
|           { "lang": "en", "value": "Example vulnerability two." } | ||||
|         ], | ||||
|         "references": [ | ||||
|           { | ||||
|             "url": "https://cisa.example.gov/alerts/0002", | ||||
|             "source": "CISA", | ||||
|             "tags": ["US Government Resource"] | ||||
|           } | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", | ||||
|                 "baseScore": 9.8, | ||||
|                 "baseSeverity": "CRITICAL" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-0002", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-01-01T11:00:00Z", | ||||
|         "lastModified": "2024-01-02T11:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "fr", "value": "Description française" }, | ||||
|           { "lang": "en", "value": "Example vulnerability two." } | ||||
|         ], | ||||
|         "references": [ | ||||
|           { | ||||
|             "url": "https://cisa.example.gov/alerts/0002", | ||||
|             "source": "CISA", | ||||
|             "tags": ["US Government Resource"] | ||||
|           } | ||||
|         ], | ||||
|         "weaknesses": [ | ||||
|           { | ||||
| @@ -75,27 +75,27 @@ | ||||
|           } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV30": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L", | ||||
|                 "baseScore": 4.6, | ||||
|                 "baseSeverity": "MEDIUM" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*" }, | ||||
|                 { "vulnerable": false, "criteria": "cpe:2.3:a:example:product_two:2.1:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|           "cvssMetricV30": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L", | ||||
|                 "baseScore": 4.6, | ||||
|                 "baseSeverity": "MEDIUM" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*" }, | ||||
|                 { "vulnerable": false, "criteria": "cpe:2.3:a:example:product_two:2.1:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -1,45 +1,45 @@ | ||||
| { | ||||
|   "resultsPerPage": 2000, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 1, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-0003", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-01-01T12:00:00Z", | ||||
|         "lastModified": "2024-01-02T12:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Example vulnerability three." } | ||||
|         ], | ||||
|         "references": [ | ||||
|           { | ||||
|             "url": "https://example.org/patches/0003", | ||||
|             "source": "Vendor", | ||||
|             "tags": ["Patch"] | ||||
|           } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV2": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P", | ||||
|                 "baseScore": 6.8, | ||||
|                 "baseSeverity": "MEDIUM" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_three:3.5:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| { | ||||
|   "resultsPerPage": 2000, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 1, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-0003", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-01-01T12:00:00Z", | ||||
|         "lastModified": "2024-01-02T12:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Example vulnerability three." } | ||||
|         ], | ||||
|         "references": [ | ||||
|           { | ||||
|             "url": "https://example.org/patches/0003", | ||||
|             "source": "Vendor", | ||||
|             "tags": ["Patch"] | ||||
|           } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV2": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P", | ||||
|                 "baseScore": 6.8, | ||||
|                 "baseSeverity": "MEDIUM" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_three:3.5:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -1,51 +1,51 @@ | ||||
| { | ||||
|   "resultsPerPage": 2000, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 1, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-0001", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-01-01T10:00:00Z", | ||||
|         "lastModified": "2024-01-03T12:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Example vulnerability one updated." } | ||||
|         ], | ||||
|         "references": [ | ||||
|           { | ||||
|             "url": "https://vendor.example.com/advisories/0001", | ||||
|             "source": "Vendor", | ||||
|             "tags": ["Vendor Advisory"] | ||||
|           }, | ||||
|           { | ||||
|             "url": "https://kb.example.com/articles/0001", | ||||
|             "source": "KnowledgeBase", | ||||
|             "tags": ["Third Party Advisory"] | ||||
|           } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", | ||||
|                 "baseScore": 8.8, | ||||
|                 "baseSeverity": "HIGH" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*" }, | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.1:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| { | ||||
|   "resultsPerPage": 2000, | ||||
|   "startIndex": 0, | ||||
|   "totalResults": 1, | ||||
|   "vulnerabilities": [ | ||||
|     { | ||||
|       "cve": { | ||||
|         "id": "CVE-2024-0001", | ||||
|         "sourceIdentifier": "nvd@nist.gov", | ||||
|         "published": "2024-01-01T10:00:00Z", | ||||
|         "lastModified": "2024-01-03T12:00:00Z", | ||||
|         "descriptions": [ | ||||
|           { "lang": "en", "value": "Example vulnerability one updated." } | ||||
|         ], | ||||
|         "references": [ | ||||
|           { | ||||
|             "url": "https://vendor.example.com/advisories/0001", | ||||
|             "source": "Vendor", | ||||
|             "tags": ["Vendor Advisory"] | ||||
|           }, | ||||
|           { | ||||
|             "url": "https://kb.example.com/articles/0001", | ||||
|             "source": "KnowledgeBase", | ||||
|             "tags": ["Third Party Advisory"] | ||||
|           } | ||||
|         ], | ||||
|         "metrics": { | ||||
|           "cvssMetricV31": [ | ||||
|             { | ||||
|               "cvssData": { | ||||
|                 "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", | ||||
|                 "baseScore": 8.8, | ||||
|                 "baseSeverity": "HIGH" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         "configurations": { | ||||
|           "nodes": [ | ||||
|             { | ||||
|               "cpeMatch": [ | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*" }, | ||||
|                 { "vulnerable": true, "criteria": "cpe:2.3:a:example:product_one:1.1:*:*:*:*:*:*:*" } | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -33,7 +33,7 @@ | ||||
|               "kind": "range", | ||||
|               "value": "pkg:golang/github.com/opencontainers/image-spec", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-10-12T12:01:51.8509671+00:00", | ||||
|               "recordedAt": "2025-10-12T19:48:04.2184266+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].versionranges[]" | ||||
|               ] | ||||
| @@ -61,7 +61,7 @@ | ||||
|             "kind": "affected", | ||||
|             "value": "pkg:golang/github.com/opencontainers/image-spec", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-10-12T12:01:51.8509671+00:00", | ||||
|             "recordedAt": "2025-10-12T19:48:04.2184266+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
| @@ -83,7 +83,7 @@ | ||||
|           "kind": "cvss", | ||||
|           "value": "CVSS_V3", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8509671+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2184266+00:00", | ||||
|           "fieldMask": [] | ||||
|         }, | ||||
|         "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:N/I:L/A:N", | ||||
| @@ -99,7 +99,7 @@ | ||||
|         "kind": "document", | ||||
|         "value": "https://osv.dev/vulnerability/GHSA-77vh-xpmg-72qh", | ||||
|         "decisionReason": null, | ||||
|         "recordedAt": "2021-11-18T16:02:41+00:00", | ||||
|         "recordedAt": "2025-10-12T19:48:04.2181708+00:00", | ||||
|         "fieldMask": [ | ||||
|           "advisory" | ||||
|         ] | ||||
| @@ -109,7 +109,7 @@ | ||||
|         "kind": "mapping", | ||||
|         "value": "GHSA-77vh-xpmg-72qh", | ||||
|         "decisionReason": null, | ||||
|         "recordedAt": "2025-10-12T12:01:51.8509671+00:00", | ||||
|         "recordedAt": "2025-10-12T19:48:04.2184266+00:00", | ||||
|         "fieldMask": [ | ||||
|           "advisory" | ||||
|         ] | ||||
| @@ -124,7 +124,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8509671+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2184266+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -140,7 +140,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/opencontainers/image-spec", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8509671+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2184266+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -156,7 +156,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/opencontainers/image-spec/commit/693428a734f5bab1a84bd2f990d92ef1111cd60c", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8509671+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2184266+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -172,7 +172,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/opencontainers/image-spec/releases/tag/v1.0.2", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8509671+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2184266+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -188,7 +188,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/opencontainers/image-spec/security/advisories/GHSA-77vh-xpmg-72qh", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8509671+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2184266+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -236,7 +236,7 @@ | ||||
|               "kind": "range", | ||||
|               "value": "pkg:maven/org.apache.logging.log4j/log4j-core", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|               "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].versionranges[]" | ||||
|               ] | ||||
| @@ -264,7 +264,7 @@ | ||||
|             "kind": "affected", | ||||
|             "value": "pkg:maven/org.apache.logging.log4j/log4j-core", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|             "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
| @@ -302,7 +302,7 @@ | ||||
|               "kind": "range", | ||||
|               "value": "pkg:maven/org.apache.logging.log4j/log4j-core", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|               "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].versionranges[]" | ||||
|               ] | ||||
| @@ -330,7 +330,7 @@ | ||||
|             "kind": "affected", | ||||
|             "value": "pkg:maven/org.apache.logging.log4j/log4j-core", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|             "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
| @@ -368,7 +368,7 @@ | ||||
|               "kind": "range", | ||||
|               "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|               "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].versionranges[]" | ||||
|               ] | ||||
| @@ -396,7 +396,7 @@ | ||||
|             "kind": "affected", | ||||
|             "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|             "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
| @@ -434,7 +434,7 @@ | ||||
|               "kind": "range", | ||||
|               "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|               "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].versionranges[]" | ||||
|               ] | ||||
| @@ -462,7 +462,7 @@ | ||||
|             "kind": "affected", | ||||
|             "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|             "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
| @@ -500,7 +500,7 @@ | ||||
|               "kind": "range", | ||||
|               "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|               "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].versionranges[]" | ||||
|               ] | ||||
| @@ -528,7 +528,7 @@ | ||||
|             "kind": "affected", | ||||
|             "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|             "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
| @@ -566,7 +566,7 @@ | ||||
|               "kind": "range", | ||||
|               "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|               "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].versionranges[]" | ||||
|               ] | ||||
| @@ -594,7 +594,7 @@ | ||||
|             "kind": "affected", | ||||
|             "value": "pkg:maven/org.ops4j.pax.logging/pax-logging-log4j2", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|             "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
| @@ -616,7 +616,7 @@ | ||||
|           "kind": "cvss", | ||||
|           "value": "CVSS_V3", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [] | ||||
|         }, | ||||
|         "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H", | ||||
| @@ -632,7 +632,7 @@ | ||||
|         "kind": "document", | ||||
|         "value": "https://osv.dev/vulnerability/GHSA-7rjr-3q55-vv33", | ||||
|         "decisionReason": null, | ||||
|         "recordedAt": "2021-12-14T18:01:28+00:00", | ||||
|         "recordedAt": "2025-10-12T19:48:04.2355464+00:00", | ||||
|         "fieldMask": [ | ||||
|           "advisory" | ||||
|         ] | ||||
| @@ -642,7 +642,7 @@ | ||||
|         "kind": "mapping", | ||||
|         "value": "GHSA-7rjr-3q55-vv33", | ||||
|         "decisionReason": null, | ||||
|         "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|         "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|         "fieldMask": [ | ||||
|           "advisory" | ||||
|         ] | ||||
| @@ -657,7 +657,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "http://www.openwall.com/lists/oss-security/2021/12/14/4", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -673,7 +673,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "http://www.openwall.com/lists/oss-security/2021/12/15/3", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -689,7 +689,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "http://www.openwall.com/lists/oss-security/2021/12/18/1", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -705,7 +705,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-397453.pdf", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -721,7 +721,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-479842.pdf", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -737,7 +737,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-661247.pdf", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -753,7 +753,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://cert-portal.siemens.com/productcert/pdf/ssa-714170.pdf", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -769,7 +769,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/advisories/GHSA-jfh8-c2jp-5v3q", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -785,7 +785,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EOKPQGV24RRBBI4TBZUDQMM4MEH7MXCY", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -801,7 +801,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/SIG7FZULMNK2XF6FZRU4VWYDQXNMUGAJ", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -817,7 +817,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://logging.apache.org/log4j/2.x/security.html", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -833,7 +833,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://nvd.nist.gov/vuln/detail/CVE-2021-45046", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -849,7 +849,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0032", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -865,7 +865,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-apache-log4j-qRuKNEbd", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -881,7 +881,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://security.gentoo.org/glsa/202310-16", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -897,7 +897,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://www.cve.org/CVERecord?id=CVE-2021-44228", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -913,7 +913,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://www.debian.org/security/2021/dsa-5022", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -929,7 +929,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00646.html", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -945,7 +945,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://www.kb.cert.org/vuls/id/930724", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -961,7 +961,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://www.openwall.com/lists/oss-security/2021/12/14/4", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -977,7 +977,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://www.oracle.com/security-alerts/alert-cve-2021-44228.html", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -993,7 +993,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://www.oracle.com/security-alerts/cpuapr2022.html", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1009,7 +1009,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://www.oracle.com/security-alerts/cpujan2022.html", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1025,7 +1025,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://www.oracle.com/security-alerts/cpujul2022.html", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.862103+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2365076+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1073,7 +1073,7 @@ | ||||
|               "kind": "range", | ||||
|               "value": "pkg:pypi/pyload-ng", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-10-12T12:01:51.8437105+00:00", | ||||
|               "recordedAt": "2025-10-12T19:48:04.2065811+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].versionranges[]" | ||||
|               ] | ||||
| @@ -1101,7 +1101,7 @@ | ||||
|             "kind": "affected", | ||||
|             "value": "pkg:pypi/pyload-ng", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-10-12T12:01:51.8437105+00:00", | ||||
|             "recordedAt": "2025-10-12T19:48:04.2065811+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
| @@ -1123,7 +1123,7 @@ | ||||
|           "kind": "cvss", | ||||
|           "value": "CVSS_V3", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8437105+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2065811+00:00", | ||||
|           "fieldMask": [] | ||||
|         }, | ||||
|         "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N", | ||||
| @@ -1139,7 +1139,7 @@ | ||||
|         "kind": "document", | ||||
|         "value": "https://osv.dev/vulnerability/GHSA-cjjf-27cc-pvmv", | ||||
|         "decisionReason": null, | ||||
|         "recordedAt": "2025-10-09T15:19:48+00:00", | ||||
|         "recordedAt": "2025-10-12T19:48:04.2061911+00:00", | ||||
|         "fieldMask": [ | ||||
|           "advisory" | ||||
|         ] | ||||
| @@ -1149,7 +1149,7 @@ | ||||
|         "kind": "mapping", | ||||
|         "value": "GHSA-cjjf-27cc-pvmv", | ||||
|         "decisionReason": null, | ||||
|         "recordedAt": "2025-10-12T12:01:51.8437105+00:00", | ||||
|         "recordedAt": "2025-10-12T19:48:04.2065811+00:00", | ||||
|         "fieldMask": [ | ||||
|           "advisory" | ||||
|         ] | ||||
| @@ -1164,7 +1164,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/pyload/pyload", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8437105+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2065811+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1180,7 +1180,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/pyload/pyload/commit/5823327d0b797161c7195a1f660266d30a69f0ca", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8437105+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2065811+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1196,7 +1196,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/pyload/pyload/pull/4624", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8437105+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2065811+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1212,7 +1212,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/pyload/pyload/security/advisories/GHSA-cjjf-27cc-pvmv", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8437105+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.2065811+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1260,7 +1260,7 @@ | ||||
|               "kind": "range", | ||||
|               "value": "pkg:pypi/social-auth-app-django", | ||||
|               "decisionReason": null, | ||||
|               "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|               "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|               "fieldMask": [ | ||||
|                 "affectedpackages[].versionranges[]" | ||||
|               ] | ||||
| @@ -1288,7 +1288,7 @@ | ||||
|             "kind": "affected", | ||||
|             "value": "pkg:pypi/social-auth-app-django", | ||||
|             "decisionReason": null, | ||||
|             "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|             "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|             "fieldMask": [ | ||||
|               "affectedpackages[]" | ||||
|             ] | ||||
| @@ -1311,7 +1311,7 @@ | ||||
|         "kind": "document", | ||||
|         "value": "https://osv.dev/vulnerability/GHSA-wv4w-6qv2-qqfg", | ||||
|         "decisionReason": null, | ||||
|         "recordedAt": "2025-10-09T17:08:05+00:00", | ||||
|         "recordedAt": "2025-10-12T19:48:04.0743113+00:00", | ||||
|         "fieldMask": [ | ||||
|           "advisory" | ||||
|         ] | ||||
| @@ -1321,7 +1321,7 @@ | ||||
|         "kind": "mapping", | ||||
|         "value": "GHSA-wv4w-6qv2-qqfg", | ||||
|         "decisionReason": null, | ||||
|         "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|         "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|         "fieldMask": [ | ||||
|           "advisory" | ||||
|         ] | ||||
| @@ -1336,7 +1336,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/python-social-auth/social-app-django", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1352,7 +1352,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/python-social-auth/social-app-django/commit/10c80e2ebabeccd4e9c84ad0e16e1db74148ed4c", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1368,7 +1368,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/python-social-auth/social-app-django/issues/220", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1384,7 +1384,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/python-social-auth/social-app-django/issues/231", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1400,7 +1400,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/python-social-auth/social-app-django/issues/634", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1416,7 +1416,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/python-social-auth/social-app-django/pull/803", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
| @@ -1432,7 +1432,7 @@ | ||||
|           "kind": "reference", | ||||
|           "value": "https://github.com/python-social-auth/social-app-django/security/advisories/GHSA-wv4w-6qv2-qqfg", | ||||
|           "decisionReason": null, | ||||
|           "recordedAt": "2025-10-12T12:01:51.8047195+00:00", | ||||
|           "recordedAt": "2025-10-12T19:48:04.1231115+00:00", | ||||
|           "fieldMask": [ | ||||
|             "references[]" | ||||
|           ] | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,25 +1,28 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Text.Json; | ||||
| using MongoDB.Bson; | ||||
| using StellaOps.Feedser.Models; | ||||
| using StellaOps.Feedser.Source.Common; | ||||
| using StellaOps.Feedser.Source.Osv; | ||||
| using StellaOps.Feedser.Source.Osv.Internal; | ||||
| using StellaOps.Feedser.Storage.Mongo.Documents; | ||||
| using StellaOps.Feedser.Storage.Mongo.Dtos; | ||||
| using Xunit; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using System.Reflection; | ||||
| using MongoDB.Bson; | ||||
| using StellaOps.Feedser.Models; | ||||
| using StellaOps.Feedser.Source.Common; | ||||
| using StellaOps.Feedser.Source.Osv; | ||||
| using StellaOps.Feedser.Source.Osv.Internal; | ||||
| using StellaOps.Feedser.Normalization.Identifiers; | ||||
| using StellaOps.Feedser.Storage.Mongo.Documents; | ||||
| using StellaOps.Feedser.Storage.Mongo.Dtos; | ||||
| using Xunit; | ||||
|  | ||||
| namespace StellaOps.Feedser.Source.Osv.Tests; | ||||
|  | ||||
| public sealed class OsvMapperTests | ||||
| { | ||||
|     [Fact] | ||||
|     public void Map_NormalizesAliasesReferencesAndRanges() | ||||
|     { | ||||
|         var published = DateTimeOffset.UtcNow.AddDays(-2); | ||||
|         var modified = DateTimeOffset.UtcNow.AddDays(-1); | ||||
|  | ||||
|     public void Map_NormalizesAliasesReferencesAndRanges() | ||||
|     { | ||||
|         var published = DateTimeOffset.UtcNow.AddDays(-2); | ||||
|         var modified = DateTimeOffset.UtcNow.AddDays(-1); | ||||
|  | ||||
|         using var databaseSpecificJson = JsonDocument.Parse("{}"); | ||||
|         using var ecosystemSpecificJson = JsonDocument.Parse("{}"); | ||||
|  | ||||
| @@ -117,7 +120,81 @@ public sealed class OsvMapperTests | ||||
|         Assert.Equal("1.0.1", semver.Fixed); | ||||
|         Assert.False(semver.FixedInclusive); | ||||
|  | ||||
|         Assert.Single(advisory.CvssMetrics); | ||||
|         Assert.Equal("3.1", advisory.CvssMetrics[0].Version); | ||||
|     } | ||||
| } | ||||
|         Assert.Single(advisory.CvssMetrics); | ||||
|         Assert.Equal("3.1", advisory.CvssMetrics[0].Version); | ||||
|     } | ||||
|  | ||||
|     [Theory] | ||||
|     [InlineData("Go", "github.com/example/project", "pkg:golang/github.com/example/project")] | ||||
|     [InlineData("PyPI", "social_auth_app_django", "pkg:pypi/social-auth-app-django")] | ||||
|     [InlineData("npm", "@Scope/Package", "pkg:npm/%40scope/package")] | ||||
|     [InlineData("Maven", "org.example:library", "pkg:maven/org.example/library")] | ||||
|     [InlineData("crates", "serde", "pkg:cargo/serde")] | ||||
|     public void Map_InfersCanonicalPackageUrlWhenPurlMissing(string ecosystem, string packageName, string expectedIdentifier) | ||||
|     { | ||||
|         var dto = new OsvVulnerabilityDto | ||||
|         { | ||||
|             Id = $"OSV-{ecosystem}-PURL", | ||||
|             Summary = "Test advisory", | ||||
|             Details = "Details", | ||||
|             Published = DateTimeOffset.UtcNow.AddDays(-1), | ||||
|             Modified = DateTimeOffset.UtcNow, | ||||
|             Affected = new[] | ||||
|             { | ||||
|                 new OsvAffectedPackageDto | ||||
|                 { | ||||
|                     Package = new OsvPackageDto | ||||
|                     { | ||||
|                         Ecosystem = ecosystem, | ||||
|                         Name = packageName, | ||||
|                         Purl = null, | ||||
|                     }, | ||||
|                     Ranges = null, | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         if (string.Equals(ecosystem, "npm", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             Assert.True(IdentifierNormalizer.TryNormalizePackageUrl("pkg:npm/%40scope/package", out var canonical)); | ||||
|             Assert.Equal(expectedIdentifier, canonical); | ||||
|         } | ||||
|  | ||||
|         var method = typeof(OsvMapper).GetMethod("DetermineIdentifier", BindingFlags.NonPublic | BindingFlags.Static); | ||||
|         Assert.NotNull(method); | ||||
|         var directIdentifier = method!.Invoke(null, new object?[] { dto.Affected![0].Package!, ecosystem }) as string; | ||||
|         Assert.Equal(expectedIdentifier, directIdentifier); | ||||
|  | ||||
|         var (document, dtoRecord) = CreateDocumentAndDtoRecord(dto, ecosystem); | ||||
|         var advisory = OsvMapper.Map(dto, document, dtoRecord, ecosystem); | ||||
|  | ||||
|         var affected = Assert.Single(advisory.AffectedPackages); | ||||
|         Assert.Equal(expectedIdentifier, affected.Identifier); | ||||
|     } | ||||
|  | ||||
|     private static (DocumentRecord Document, DtoRecord DtoRecord) CreateDocumentAndDtoRecord(OsvVulnerabilityDto dto, string ecosystem) | ||||
|     { | ||||
|         var recordedAt = DateTimeOffset.UtcNow; | ||||
|         var document = new DocumentRecord( | ||||
|             Guid.NewGuid(), | ||||
|             OsvConnectorPlugin.SourceName, | ||||
|             $"https://osv.dev/vulnerability/{dto.Id}", | ||||
|             recordedAt, | ||||
|             "sha256", | ||||
|             DocumentStatuses.PendingParse, | ||||
|             "application/json", | ||||
|             null, | ||||
|             new Dictionary<string, string>(StringComparer.Ordinal) | ||||
|             { | ||||
|                 ["osv.ecosystem"] = ecosystem, | ||||
|             }, | ||||
|             null, | ||||
|             dto.Modified, | ||||
|             null, | ||||
|             null); | ||||
|  | ||||
|         var payload = new BsonDocument("id", dto.Id); | ||||
|         var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, recordedAt); | ||||
|         return (document, dtoRecord); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,9 +12,29 @@ using StellaOps.Feedser.Storage.Mongo.Dtos; | ||||
|  | ||||
| namespace StellaOps.Feedser.Source.Osv.Internal; | ||||
|  | ||||
| internal static class OsvMapper | ||||
| { | ||||
|     private static readonly string[] SeverityOrder = { "none", "low", "medium", "high", "critical" }; | ||||
| internal static class OsvMapper | ||||
| { | ||||
|     private static readonly string[] SeverityOrder = { "none", "low", "medium", "high", "critical" }; | ||||
|  | ||||
|     private static readonly IReadOnlyDictionary<string, Func<string, string>> PackageUrlBuilders = | ||||
|         new Dictionary<string, Func<string, string>>(StringComparer.OrdinalIgnoreCase) | ||||
|         { | ||||
|             ["pypi"] = static name => $"pkg:pypi/{NormalizePyPiName(name)}", | ||||
|             ["python"] = static name => $"pkg:pypi/{NormalizePyPiName(name)}", | ||||
|             ["maven"] = static name => $"pkg:maven/{NormalizeMavenName(name)}", | ||||
|             ["go"] = static name => $"pkg:golang/{NormalizeGoName(name)}", | ||||
|             ["golang"] = static name => $"pkg:golang/{NormalizeGoName(name)}", | ||||
|             ["crates"] = static name => $"pkg:cargo/{NormalizeCratesName(name)}", | ||||
|             ["crates.io"] = static name => $"pkg:cargo/{NormalizeCratesName(name)}", | ||||
|             ["cargo"] = static name => $"pkg:cargo/{NormalizeCratesName(name)}", | ||||
|             ["nuget"] = static name => $"pkg:nuget/{NormalizeNugetName(name)}", | ||||
|             ["rubygems"] = static name => $"pkg:gem/{NormalizeRubyName(name)}", | ||||
|             ["gem"] = static name => $"pkg:gem/{NormalizeRubyName(name)}", | ||||
|             ["packagist"] = static name => $"pkg:composer/{NormalizeComposerName(name)}", | ||||
|             ["composer"] = static name => $"pkg:composer/{NormalizeComposerName(name)}", | ||||
|             ["hex"] = static name => $"pkg:hex/{NormalizeHexName(name)}", | ||||
|             ["hex.pm"] = static name => $"pkg:hex/{NormalizeHexName(name)}", | ||||
|         }; | ||||
|  | ||||
|     public static Advisory Map( | ||||
|         OsvVulnerabilityDto dto, | ||||
| @@ -416,24 +436,35 @@ internal static class OsvMapper | ||||
|         return new RangePrimitives(semver, null, null, null); | ||||
|     } | ||||
|  | ||||
|     private static string? DetermineIdentifier(OsvPackageDto package, string ecosystem) | ||||
|     { | ||||
|         if (!string.IsNullOrWhiteSpace(package.Purl) | ||||
|             && IdentifierNormalizer.TryNormalizePackageUrl(package.Purl, out var normalized)) | ||||
|         { | ||||
|             return normalized; | ||||
|         } | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(package.Name)) | ||||
|         { | ||||
|             var name = package.Name.Trim(); | ||||
|             return string.IsNullOrWhiteSpace(package.Ecosystem) | ||||
|                 ? $"{ecosystem}:{name}" | ||||
|                 : $"{package.Ecosystem.Trim()}:{name}"; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|     private static string? DetermineIdentifier(OsvPackageDto package, string ecosystem) | ||||
|     { | ||||
|         if (IdentifierNormalizer.TryNormalizePackageUrl(package.Purl, out var normalized)) | ||||
|         { | ||||
|             return normalized; | ||||
|         } | ||||
|  | ||||
|         var name = Validation.TrimToNull(package.Name); | ||||
|         if (name is null) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         var ecosystemHint = Validation.TrimToNull(package.Ecosystem) ?? Validation.TrimToNull(ecosystem); | ||||
|         if (ecosystemHint is not null | ||||
|             && string.Equals(ecosystemHint, "npm", StringComparison.OrdinalIgnoreCase) | ||||
|             && TryBuildNpmPackageUrl(name, out normalized)) | ||||
|         { | ||||
|             return normalized; | ||||
|         } | ||||
|  | ||||
|         if (TryBuildCanonicalPackageUrl(ecosystemHint, name, out normalized)) | ||||
|         { | ||||
|             return normalized; | ||||
|         } | ||||
|  | ||||
|         var fallbackEcosystem = ecosystemHint ?? Validation.TrimToNull(ecosystem) ?? "osv"; | ||||
|         return $"{fallbackEcosystem}:{name}"; | ||||
|     } | ||||
|  | ||||
|     private static IReadOnlyList<CvssMetric> BuildCvssMetrics(OsvVulnerabilityDto dto, DateTimeOffset recordedAt, out string? severity) | ||||
|     { | ||||
| @@ -520,11 +551,11 @@ internal static class OsvMapper | ||||
|         return left.Provenance.RecordedAt.CompareTo(right.Provenance.RecordedAt); | ||||
|     } | ||||
|  | ||||
|     private static int CompareNullable(string? left, string? right) | ||||
|     { | ||||
|         if (left is null && right is null) | ||||
|         { | ||||
|             return 0; | ||||
|     private static int CompareNullable(string? left, string? right) | ||||
|     { | ||||
|         if (left is null && right is null) | ||||
|         { | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         if (left is null) | ||||
| @@ -536,7 +567,80 @@ internal static class OsvMapper | ||||
|         { | ||||
|             return -1; | ||||
|         } | ||||
|  | ||||
|         return StringComparer.Ordinal.Compare(left, right); | ||||
|     } | ||||
| } | ||||
|  | ||||
|         return StringComparer.Ordinal.Compare(left, right); | ||||
|     } | ||||
|  | ||||
|     private static bool TryBuildCanonicalPackageUrl(string? ecosystem, string name, out string? canonical) | ||||
|     { | ||||
|         canonical = null; | ||||
|         var trimmedEcosystem = Validation.TrimToNull(ecosystem); | ||||
|         if (trimmedEcosystem is null) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (!PackageUrlBuilders.TryGetValue(trimmedEcosystem, out var factory)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         var candidate = factory(name.Trim()); | ||||
|         return IdentifierNormalizer.TryNormalizePackageUrl(candidate, out canonical); | ||||
|     } | ||||
|  | ||||
|     private static string NormalizePyPiName(string name) => name.Trim().Replace('_', '-'); | ||||
|  | ||||
|     private static bool TryBuildNpmPackageUrl(string name, out string? canonical) | ||||
|     { | ||||
|         canonical = null; | ||||
|         var trimmed = name.Trim(); | ||||
|         if (trimmed.Length == 0) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (trimmed[0] == '@') | ||||
|         { | ||||
|             var slashIndex = trimmed.IndexOf('/', 1); | ||||
|             if (slashIndex <= 1 || slashIndex >= trimmed.Length - 1) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             var scope = trimmed[..slashIndex].ToLowerInvariant(); | ||||
|             var package = trimmed[(slashIndex + 1)..].ToLowerInvariant(); | ||||
|             var candidate = $"pkg:npm/{Uri.EscapeDataString(scope)}/{Uri.EscapeDataString(package)}"; | ||||
|             return IdentifierNormalizer.TryNormalizePackageUrl(candidate, out canonical); | ||||
|         } | ||||
|  | ||||
|         if (trimmed.Contains('/', StringComparison.Ordinal)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         var normalized = trimmed.ToLowerInvariant(); | ||||
|         var simpleCandidate = $"pkg:npm/{Uri.EscapeDataString(normalized)}"; | ||||
|         return IdentifierNormalizer.TryNormalizePackageUrl(simpleCandidate, out canonical); | ||||
|     } | ||||
|  | ||||
|     private static string NormalizeMavenName(string name) | ||||
|     { | ||||
|         var trimmed = name.Trim(); | ||||
|         return trimmed.Contains(':', StringComparison.Ordinal) | ||||
|             ? trimmed.Replace(':', '/') | ||||
|             : trimmed.Replace('\\', '/'); | ||||
|     } | ||||
|  | ||||
|     private static string NormalizeGoName(string name) => name.Trim(); | ||||
|  | ||||
|     private static string NormalizeCratesName(string name) => name.Trim(); | ||||
|  | ||||
|     private static string NormalizeNugetName(string name) => name.Trim(); | ||||
|  | ||||
|     private static string NormalizeRubyName(string name) => name.Trim(); | ||||
|  | ||||
|     private static string NormalizeComposerName(string name) => name.Trim(); | ||||
|  | ||||
|     private static string NormalizeHexName(string name) => name.Trim(); | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
| |Connector DI routine & job registration|BE-Conn-OSV|Core|**DONE** – DI routine registers fetch/parse/map jobs with scheduler.| | ||||
| |Implement OSV fetch/parse/map skeleton|BE-Conn-OSV|Source.Common|**DONE** – connector now persists documents, DTOs, and canonical advisories.| | ||||
| |FEEDCONN-OSV-02-004 OSV references & credits alignment|BE-Conn-OSV|Models `FEEDMODELS-SCHEMA-01-002`|**DONE (2025-10-11)** – Mapper normalizes references with provenance masks, emits advisory credits, and regression fixtures/assertions cover the new fields.| | ||||
| |FEEDCONN-OSV-02-005 Fixture updater workflow|BE-Conn-OSV, QA|Docs|TODO – Document `tools/FixtureUpdater`, add parity regression steps, and ensure future refreshes capture credit metadata consistently.| | ||||
| |FEEDCONN-OSV-02-005 Fixture updater workflow|BE-Conn-OSV, QA|Docs|**DONE (2025-10-12)** – Canonical PURL derivation now covers Go + scoped npm advisories without upstream `purl`; legacy invalid npm names still fall back to `ecosystem:name`. OSV/GHSA/NVD suites and normalization/storage tests rerun clean.| | ||||
| |FEEDCONN-OSV-02-003 Normalized versions rollout|BE-Conn-OSV|Models `FEEDMODELS-SCHEMA-01-003`, Normalization playbook|**DONE (2025-10-11)** – `OsvMapper` now emits SemVer primitives + normalized rules with `osv:{ecosystem}:{advisoryId}:{identifier}` notes; npm/PyPI/Parity fixtures refreshed; merge coordination pinged (OSV handoff).| | ||||
| |FEEDCONN-OSV-04-003 Parity fixture refresh|QA, BE-Conn-OSV|Normalized versions rollout, GHSA parity tests|**DONE (2025-10-12)** – Parity fixtures include normalizedVersions notes (`osv:<ecosystem>:<id>:<purl>`); regression math rerun via `dotnet test src/StellaOps.Feedser.Source.Osv.Tests` and docs flagged for workflow sync.| | ||||
| |FEEDCONN-OSV-04-002 Conflict regression fixtures|BE-Conn-OSV, QA|Merge `FEEDMERGE-ENGINE-04-001`|**DONE (2025-10-12)** – Added `conflict-osv.canonical.json` + regression asserting SemVer range + CVSS medium severity; dataset matches GHSA/NVD fixtures for merge tests. Validation: `dotnet test src/StellaOps.Feedser.Source.Osv.Tests/StellaOps.Feedser.Source.Osv.Tests.csproj --filter OsvConflictFixtureTests`.| | ||||
|   | ||||
| @@ -62,7 +62,12 @@ public sealed class AdvisoryStorePerformanceTests : IClassFixture<MongoIntegrati | ||||
|             await bootstrapper.InitializeAsync(CancellationToken.None); | ||||
|  | ||||
|             var aliasStore = new AliasStore(database, NullLogger<AliasStore>.Instance); | ||||
|             var store = new AdvisoryStore(database, aliasStore, NullLogger<AdvisoryStore>.Instance, TimeProvider.System); | ||||
|             var store = new AdvisoryStore( | ||||
|                 database, | ||||
|                 aliasStore, | ||||
|                 NullLogger<AdvisoryStore>.Instance, | ||||
|                 Options.Create(new MongoStorageOptions()), | ||||
|                 TimeProvider.System); | ||||
|             using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45)); | ||||
|  | ||||
|             // Warm up collections (indexes, serialization caches) so perf timings exclude one-time setup work. | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Microsoft.Extensions.Logging.Abstractions; | ||||
| using MongoDB.Driver; | ||||
| using StellaOps.Feedser.Models; | ||||
| using Microsoft.Extensions.Logging.Abstractions; | ||||
| using Microsoft.Extensions.Options; | ||||
| using MongoDB.Driver; | ||||
| using StellaOps.Feedser.Models; | ||||
| using StellaOps.Feedser.Storage.Mongo.Advisories; | ||||
| using StellaOps.Feedser.Storage.Mongo.Aliases; | ||||
|  | ||||
| @@ -25,8 +26,13 @@ public sealed class AdvisoryStoreTests : IClassFixture<MongoIntegrationFixture> | ||||
|         await DropCollectionAsync(MongoStorageDefaults.Collections.Advisory); | ||||
|         await DropCollectionAsync(MongoStorageDefaults.Collections.Alias); | ||||
|  | ||||
|         var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance); | ||||
|         var store = new AdvisoryStore(_fixture.Database, aliasStore, NullLogger<AdvisoryStore>.Instance, TimeProvider.System); | ||||
|         var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance); | ||||
|         var store = new AdvisoryStore( | ||||
|             _fixture.Database, | ||||
|             aliasStore, | ||||
|             NullLogger<AdvisoryStore>.Instance, | ||||
|             Options.Create(new MongoStorageOptions()), | ||||
|             TimeProvider.System); | ||||
|         var advisory = new Advisory( | ||||
|             advisoryKey: "ADV-1", | ||||
|             title: "Sample Advisory", | ||||
| @@ -62,8 +68,13 @@ public sealed class AdvisoryStoreTests : IClassFixture<MongoIntegrationFixture> | ||||
|         await DropCollectionAsync(MongoStorageDefaults.Collections.Advisory); | ||||
|         await DropCollectionAsync(MongoStorageDefaults.Collections.Alias); | ||||
|  | ||||
|         var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance); | ||||
|         var store = new AdvisoryStore(_fixture.Database, aliasStore, NullLogger<AdvisoryStore>.Instance, TimeProvider.System); | ||||
|         var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance); | ||||
|         var store = new AdvisoryStore( | ||||
|             _fixture.Database, | ||||
|             aliasStore, | ||||
|             NullLogger<AdvisoryStore>.Instance, | ||||
|             Options.Create(new MongoStorageOptions()), | ||||
|             TimeProvider.System); | ||||
|  | ||||
|         var recordedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); | ||||
|         var provenance = new AdvisoryProvenance("source-x", "mapper", "payload-123", recordedAt); | ||||
| @@ -144,15 +155,147 @@ public sealed class AdvisoryStoreTests : IClassFixture<MongoIntegrationFixture> | ||||
|         Assert.NotNull(fetchedRange.Primitives); | ||||
|         Assert.Equal(rangePrimitives.SemVer, fetchedRange.Primitives!.SemVer); | ||||
|         Assert.Equal(rangePrimitives.Nevra, fetchedRange.Primitives.Nevra); | ||||
|         Assert.Equal(rangePrimitives.Evr, fetchedRange.Primitives.Evr); | ||||
|         Assert.Equal(rangePrimitives.VendorExtensions, fetchedRange.Primitives.VendorExtensions); | ||||
|     } | ||||
|  | ||||
|     private async Task DropCollectionAsync(string collectionName) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             await _fixture.Database.DropCollectionAsync(collectionName); | ||||
|         Assert.Equal(rangePrimitives.Evr, fetchedRange.Primitives.Evr); | ||||
|         Assert.Equal(rangePrimitives.VendorExtensions, fetchedRange.Primitives.VendorExtensions); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task UpsertAsync_SkipsNormalizedVersionsWhenFeatureDisabled() | ||||
|     { | ||||
|         await DropCollectionAsync(MongoStorageDefaults.Collections.Advisory); | ||||
|         await DropCollectionAsync(MongoStorageDefaults.Collections.Alias); | ||||
|  | ||||
|         var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance); | ||||
|         var store = new AdvisoryStore( | ||||
|             _fixture.Database, | ||||
|             aliasStore, | ||||
|             NullLogger<AdvisoryStore>.Instance, | ||||
|             Options.Create(new MongoStorageOptions { EnableSemVerStyle = false }), | ||||
|             TimeProvider.System); | ||||
|  | ||||
|         var advisory = CreateNormalizedAdvisory("ADV-NORM-DISABLED"); | ||||
|         await store.UpsertAsync(advisory, CancellationToken.None); | ||||
|  | ||||
|         var document = await _fixture.Database | ||||
|             .GetCollection<AdvisoryDocument>(MongoStorageDefaults.Collections.Advisory) | ||||
|             .Find(x => x.AdvisoryKey == advisory.AdvisoryKey) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         Assert.NotNull(document); | ||||
|         Assert.True(document!.NormalizedVersions is null || document.NormalizedVersions.Count == 0); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task UpsertAsync_PopulatesNormalizedVersionsWhenFeatureEnabled() | ||||
|     { | ||||
|         await DropCollectionAsync(MongoStorageDefaults.Collections.Advisory); | ||||
|         await DropCollectionAsync(MongoStorageDefaults.Collections.Alias); | ||||
|  | ||||
|         var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance); | ||||
|         var store = new AdvisoryStore( | ||||
|             _fixture.Database, | ||||
|             aliasStore, | ||||
|             NullLogger<AdvisoryStore>.Instance, | ||||
|             Options.Create(new MongoStorageOptions { EnableSemVerStyle = true }), | ||||
|             TimeProvider.System); | ||||
|  | ||||
|         var advisory = CreateNormalizedAdvisory("ADV-NORM-ENABLED"); | ||||
|         await store.UpsertAsync(advisory, CancellationToken.None); | ||||
|  | ||||
|         var document = await _fixture.Database | ||||
|             .GetCollection<AdvisoryDocument>(MongoStorageDefaults.Collections.Advisory) | ||||
|             .Find(x => x.AdvisoryKey == advisory.AdvisoryKey) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         Assert.NotNull(document); | ||||
|         var normalizedCollection = document!.NormalizedVersions; | ||||
|         Assert.NotNull(normalizedCollection); | ||||
|         var normalized = Assert.Single(normalizedCollection!); | ||||
|         Assert.Equal("pkg:npm/example", normalized.PackageId); | ||||
|         Assert.Equal(AffectedPackageTypes.SemVer, normalized.PackageType); | ||||
|         Assert.Equal(NormalizedVersionSchemes.SemVer, normalized.Scheme); | ||||
|         Assert.Equal(NormalizedVersionRuleTypes.Range, normalized.Type); | ||||
|         Assert.Equal("range", normalized.Style); | ||||
|         Assert.Equal("1.0.0", normalized.Min); | ||||
|         Assert.True(normalized.MinInclusive); | ||||
|         Assert.Equal("2.0.0", normalized.Max); | ||||
|         Assert.False(normalized.MaxInclusive); | ||||
|         Assert.Null(normalized.Value); | ||||
|         Assert.Equal("ghsa:pkg:npm/example", normalized.Notes); | ||||
|         Assert.Equal("range-decision", normalized.DecisionReason); | ||||
|         Assert.Equal(">= 1.0.0 < 2.0.0", normalized.Constraint); | ||||
|         Assert.Equal("ghsa", normalized.Source); | ||||
|         Assert.Equal(new DateTime(2025, 10, 9, 0, 0, 0, DateTimeKind.Utc), normalized.RecordedAtUtc); | ||||
|     } | ||||
|  | ||||
|     private static Advisory CreateNormalizedAdvisory(string advisoryKey) | ||||
|     { | ||||
|         var recordedAt = new DateTimeOffset(2025, 10, 9, 0, 0, 0, TimeSpan.Zero); | ||||
|         var rangeProvenance = new AdvisoryProvenance( | ||||
|             source: "ghsa", | ||||
|             kind: "affected-range", | ||||
|             value: "pkg:npm/example", | ||||
|             recordedAt: recordedAt, | ||||
|             fieldMask: new[] { "affectedpackages[].versionranges[]" }, | ||||
|             decisionReason: "range-decision"); | ||||
|  | ||||
|         var semverPrimitive = new SemVerPrimitive( | ||||
|             Introduced: "1.0.0", | ||||
|             IntroducedInclusive: true, | ||||
|             Fixed: "2.0.0", | ||||
|             FixedInclusive: false, | ||||
|             LastAffected: null, | ||||
|             LastAffectedInclusive: false, | ||||
|             ConstraintExpression: ">= 1.0.0 < 2.0.0"); | ||||
|  | ||||
|         var normalizedRule = semverPrimitive.ToNormalizedVersionRule("ghsa:pkg:npm/example")!; | ||||
|         var versionRange = new AffectedVersionRange( | ||||
|             rangeKind: "semver", | ||||
|             introducedVersion: "1.0.0", | ||||
|             fixedVersion: "2.0.0", | ||||
|             lastAffectedVersion: null, | ||||
|             rangeExpression: ">= 1.0.0 < 2.0.0", | ||||
|             provenance: rangeProvenance, | ||||
|             primitives: new RangePrimitives(semverPrimitive, null, null, null)); | ||||
|  | ||||
|         var package = new AffectedPackage( | ||||
|             type: AffectedPackageTypes.SemVer, | ||||
|             identifier: "pkg:npm/example", | ||||
|             platform: "npm", | ||||
|             versionRanges: new[] { versionRange }, | ||||
|             statuses: Array.Empty<AffectedPackageStatus>(), | ||||
|             provenance: new[] { rangeProvenance }, | ||||
|             normalizedVersions: new[] { normalizedRule }); | ||||
|  | ||||
|         var advisoryProvenance = new AdvisoryProvenance( | ||||
|             source: "ghsa", | ||||
|             kind: "document", | ||||
|             value: advisoryKey, | ||||
|             recordedAt: recordedAt, | ||||
|             fieldMask: new[] { "advisory" }, | ||||
|             decisionReason: "document-decision"); | ||||
|  | ||||
|         return new Advisory( | ||||
|             advisoryKey: advisoryKey, | ||||
|             title: "Normalized advisory", | ||||
|             summary: "Contains normalized versions for storage testing.", | ||||
|             language: "en", | ||||
|             published: recordedAt, | ||||
|             modified: recordedAt, | ||||
|             severity: "medium", | ||||
|             exploitKnown: false, | ||||
|             aliases: new[] { $"{advisoryKey}-ALIAS" }, | ||||
|             references: Array.Empty<AdvisoryReference>(), | ||||
|             affectedPackages: new[] { package }, | ||||
|             cvssMetrics: Array.Empty<CvssMetric>(), | ||||
|             provenance: new[] { advisoryProvenance }); | ||||
|     } | ||||
|  | ||||
|     private async Task DropCollectionAsync(string collectionName) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             await _fixture.Database.DropCollectionAsync(collectionName); | ||||
|         } | ||||
|         catch (MongoDB.Driver.MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|   | ||||
| @@ -0,0 +1,97 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using System.Threading; | ||||
| using Microsoft.Extensions.Logging.Abstractions; | ||||
| using Microsoft.Extensions.Options; | ||||
| using MongoDB.Bson; | ||||
| using MongoDB.Driver; | ||||
| using StellaOps.Feedser.Storage.Mongo; | ||||
| using StellaOps.Feedser.Storage.Mongo.Migrations; | ||||
| using Xunit; | ||||
|  | ||||
| namespace StellaOps.Feedser.Storage.Mongo.Tests; | ||||
|  | ||||
| [Collection("mongo-fixture")] | ||||
| public sealed class MongoBootstrapperTests : IClassFixture<MongoIntegrationFixture> | ||||
| { | ||||
|     private readonly MongoIntegrationFixture _fixture; | ||||
|  | ||||
|     public MongoBootstrapperTests(MongoIntegrationFixture fixture) | ||||
|     { | ||||
|         _fixture = fixture; | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task InitializeAsync_CreatesNormalizedIndexesWhenSemVerStyleEnabled() | ||||
|     { | ||||
|         var databaseName = $"feedser-bootstrap-semver-{Guid.NewGuid():N}"; | ||||
|         var database = _fixture.Client.GetDatabase(databaseName); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var runner = new MongoMigrationRunner( | ||||
|                 database, | ||||
|                 Array.Empty<IMongoMigration>(), | ||||
|                 NullLogger<MongoMigrationRunner>.Instance, | ||||
|                 TimeProvider.System); | ||||
|  | ||||
|             var bootstrapper = new MongoBootstrapper( | ||||
|                 database, | ||||
|                 Options.Create(new MongoStorageOptions { EnableSemVerStyle = true }), | ||||
|                 NullLogger<MongoBootstrapper>.Instance, | ||||
|                 runner); | ||||
|  | ||||
|             await bootstrapper.InitializeAsync(CancellationToken.None); | ||||
|  | ||||
|             var indexCursor = await database | ||||
|                 .GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Advisory) | ||||
|                 .Indexes | ||||
|                 .ListAsync(); | ||||
|             var indexNames = (await indexCursor.ToListAsync()).Select(x => x["name"].AsString).ToArray(); | ||||
|  | ||||
|             Assert.Contains("advisory_normalizedVersions_pkg_scheme_type", indexNames); | ||||
|             Assert.Contains("advisory_normalizedVersions_value", indexNames); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             await _fixture.Client.DropDatabaseAsync(databaseName); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task InitializeAsync_DoesNotCreateNormalizedIndexesWhenFeatureDisabled() | ||||
|     { | ||||
|         var databaseName = $"feedser-bootstrap-no-semver-{Guid.NewGuid():N}"; | ||||
|         var database = _fixture.Client.GetDatabase(databaseName); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var runner = new MongoMigrationRunner( | ||||
|                 database, | ||||
|                 Array.Empty<IMongoMigration>(), | ||||
|                 NullLogger<MongoMigrationRunner>.Instance, | ||||
|                 TimeProvider.System); | ||||
|  | ||||
|             var bootstrapper = new MongoBootstrapper( | ||||
|                 database, | ||||
|                 Options.Create(new MongoStorageOptions { EnableSemVerStyle = false }), | ||||
|                 NullLogger<MongoBootstrapper>.Instance, | ||||
|                 runner); | ||||
|  | ||||
|             await bootstrapper.InitializeAsync(CancellationToken.None); | ||||
|  | ||||
|             var indexCursor = await database | ||||
|                 .GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Advisory) | ||||
|                 .Indexes | ||||
|                 .ListAsync(); | ||||
|             var indexNames = (await indexCursor.ToListAsync()).Select(x => x["name"].AsString).ToArray(); | ||||
|  | ||||
|             Assert.DoesNotContain("advisory_normalizedVersions_pkg_scheme_type", indexNames); | ||||
|             Assert.DoesNotContain("advisory_normalizedVersions_value", indexNames); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             await _fixture.Client.DropDatabaseAsync(databaseName); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -4,33 +4,37 @@ using System.Linq; | ||||
| using System.Runtime.CompilerServices; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using MongoDB.Bson; | ||||
| using MongoDB.Driver; | ||||
| using StellaOps.Feedser.Models; | ||||
| using StellaOps.Feedser.Storage.Mongo.Aliases; | ||||
|  | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.Extensions.Options; | ||||
| using MongoDB.Bson; | ||||
| using MongoDB.Driver; | ||||
| using StellaOps.Feedser.Models; | ||||
| using StellaOps.Feedser.Storage.Mongo.Aliases; | ||||
|  | ||||
| namespace StellaOps.Feedser.Storage.Mongo.Advisories; | ||||
|  | ||||
| public sealed class AdvisoryStore : IAdvisoryStore | ||||
| { | ||||
|     private readonly IMongoCollection<AdvisoryDocument> _collection; | ||||
|     private readonly ILogger<AdvisoryStore> _logger; | ||||
|     private readonly IAliasStore _aliasStore; | ||||
|     private readonly TimeProvider _timeProvider; | ||||
|  | ||||
|     public AdvisoryStore( | ||||
|         IMongoDatabase database, | ||||
|         IAliasStore aliasStore, | ||||
|         ILogger<AdvisoryStore> logger, | ||||
|         TimeProvider? timeProvider = null) | ||||
|     { | ||||
|         _collection = (database ?? throw new ArgumentNullException(nameof(database))) | ||||
|             .GetCollection<AdvisoryDocument>(MongoStorageDefaults.Collections.Advisory); | ||||
|         _aliasStore = aliasStore ?? throw new ArgumentNullException(nameof(aliasStore)); | ||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|         _timeProvider = timeProvider ?? TimeProvider.System; | ||||
|     } | ||||
|     private readonly IMongoCollection<AdvisoryDocument> _collection; | ||||
|     private readonly ILogger<AdvisoryStore> _logger; | ||||
|     private readonly IAliasStore _aliasStore; | ||||
|     private readonly TimeProvider _timeProvider; | ||||
|     private readonly MongoStorageOptions _options; | ||||
|  | ||||
|     public AdvisoryStore( | ||||
|         IMongoDatabase database, | ||||
|         IAliasStore aliasStore, | ||||
|         ILogger<AdvisoryStore> logger, | ||||
|         IOptions<MongoStorageOptions> options, | ||||
|         TimeProvider? timeProvider = null) | ||||
|     { | ||||
|         _collection = (database ?? throw new ArgumentNullException(nameof(database))) | ||||
|             .GetCollection<AdvisoryDocument>(MongoStorageDefaults.Collections.Advisory); | ||||
|         _aliasStore = aliasStore ?? throw new ArgumentNullException(nameof(aliasStore)); | ||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|         _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); | ||||
|         _timeProvider = timeProvider ?? TimeProvider.System; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public async Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken) | ||||
| @@ -51,7 +55,9 @@ public sealed class AdvisoryStore : IAdvisoryStore | ||||
|         } | ||||
|  | ||||
|         var payload = CanonicalJsonSerializer.Serialize(advisory); | ||||
|         var normalizedVersions = NormalizedVersionDocumentFactory.Create(advisory); | ||||
|         var normalizedVersions = _options.EnableSemVerStyle | ||||
|             ? NormalizedVersionDocumentFactory.Create(advisory) | ||||
|             : null; | ||||
|         var document = new AdvisoryDocument | ||||
|         { | ||||
|             AdvisoryKey = advisory.AdvisoryKey, | ||||
|   | ||||
| @@ -41,10 +41,10 @@ internal static class NormalizedVersionDocumentFactory | ||||
|                     ?? package.Provenance.FirstOrDefault()?.RecordedAt | ||||
|                     ?? advisoryFallbackRecordedAt; | ||||
|  | ||||
|                 var constraint = matchingRange?.RangeExpression | ||||
|                     ?? matchingRange?.Primitives?.SemVer?.ConstraintExpression; | ||||
|                 var constraint = matchingRange?.Primitives?.SemVer?.ConstraintExpression | ||||
|                     ?? matchingRange?.RangeExpression; | ||||
|  | ||||
|                 var style = rule.Type; | ||||
|                 var style = matchingRange?.Primitives?.SemVer?.Style ?? rule.Type; | ||||
|  | ||||
|                 documents.Add(new NormalizedVersionDocument | ||||
|                 { | ||||
|   | ||||
| @@ -125,21 +125,35 @@ public sealed class MongoBootstrapper | ||||
|     private Task EnsureAdvisoryIndexesAsync(CancellationToken cancellationToken) | ||||
|     { | ||||
|         var collection = _database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Advisory); | ||||
|         var indexes = new List<CreateIndexModel<BsonDocument>> | ||||
|         { | ||||
|             new( | ||||
|                 Builders<BsonDocument>.IndexKeys.Ascending("advisoryKey"), | ||||
|                 new CreateIndexOptions { Name = "advisory_key_unique", Unique = true }), | ||||
|             new( | ||||
|                 Builders<BsonDocument>.IndexKeys.Descending("modified"), | ||||
|                 new CreateIndexOptions { Name = "advisory_modified_desc" }), | ||||
|             new( | ||||
|                 Builders<BsonDocument>.IndexKeys.Descending("published"), | ||||
|                 new CreateIndexOptions { Name = "advisory_published_desc" }), | ||||
|         }; | ||||
|  | ||||
|         return collection.Indexes.CreateManyAsync(indexes, cancellationToken); | ||||
|     } | ||||
|         var indexes = new List<CreateIndexModel<BsonDocument>> | ||||
|         { | ||||
|             new( | ||||
|                 Builders<BsonDocument>.IndexKeys.Ascending("advisoryKey"), | ||||
|                 new CreateIndexOptions { Name = "advisory_key_unique", Unique = true }), | ||||
|             new( | ||||
|                 Builders<BsonDocument>.IndexKeys.Descending("modified"), | ||||
|                 new CreateIndexOptions { Name = "advisory_modified_desc" }), | ||||
|             new( | ||||
|                 Builders<BsonDocument>.IndexKeys.Descending("published"), | ||||
|                 new CreateIndexOptions { Name = "advisory_published_desc" }), | ||||
|         }; | ||||
|  | ||||
|         if (_options.EnableSemVerStyle) | ||||
|         { | ||||
|             indexes.Add(new CreateIndexModel<BsonDocument>( | ||||
|                 Builders<BsonDocument>.IndexKeys | ||||
|                     .Ascending("normalizedVersions.packageId") | ||||
|                     .Ascending("normalizedVersions.scheme") | ||||
|                     .Ascending("normalizedVersions.type"), | ||||
|                 new CreateIndexOptions { Name = "advisory_normalizedVersions_pkg_scheme_type" })); | ||||
|  | ||||
|             indexes.Add(new CreateIndexModel<BsonDocument>( | ||||
|                 Builders<BsonDocument>.IndexKeys.Ascending("normalizedVersions.value"), | ||||
|                 new CreateIndexOptions { Name = "advisory_normalizedVersions_value", Sparse = true })); | ||||
|         } | ||||
|  | ||||
|         return collection.Indexes.CreateManyAsync(indexes, cancellationToken); | ||||
|     } | ||||
|  | ||||
|     private Task EnsureDocumentsIndexesAsync(CancellationToken cancellationToken) | ||||
|     { | ||||
|   | ||||
| @@ -81,10 +81,11 @@ public static class ServiceCollectionExtensions | ||||
|  | ||||
|         services.AddHostedService<RawDocumentRetentionService>(); | ||||
|  | ||||
|         services.AddSingleton<MongoMigrationRunner>(); | ||||
|         services.AddSingleton<IMongoMigration, EnsureDocumentExpiryIndexesMigration>(); | ||||
|         services.AddSingleton<IMongoMigration, EnsureGridFsExpiryIndexesMigration>(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
|         services.AddSingleton<MongoMigrationRunner>(); | ||||
|         services.AddSingleton<IMongoMigration, EnsureDocumentExpiryIndexesMigration>(); | ||||
|         services.AddSingleton<IMongoMigration, EnsureGridFsExpiryIndexesMigration>(); | ||||
|         services.AddSingleton<IMongoMigration, SemVerStyleBackfillMigration>(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,9 +11,11 @@ | ||||
| |ExportState store|BE-Export|Exporters|DONE – ExportStateStore upserts and retrieves exporter metadata.| | ||||
| |Performance tests for large advisories|QA|Storage.Mongo|DONE – `AdvisoryStorePerformanceTests` exercises large payload upsert/find throughput budgets.| | ||||
| |Migration playbook for schema/index changes|BE-Storage|Storage.Mongo|DONE – `MongoMigrationRunner` executes `IMongoMigration` steps recorded in `schema_migrations`; see `MIGRATIONS.md`.| | ||||
| |Raw document retention/TTL strategy|BE-Storage|Storage.Mongo|DONE – retention options flow into `RawDocumentRetentionService` and TTL migrations for `document`/GridFS indexes.| | ||||
| |Persist last failure reason in SourceState|BE-Storage|Storage.Mongo|DONE – `MongoSourceStateRepository.MarkFailureAsync` stores `lastFailureReason` with length guard + reset on success.| | ||||
| |Raw document retention/TTL strategy|BE-Storage|Storage.Mongo|DONE – retention options flow into `RawDocumentRetentionService` and TTL migrations for `document`/GridFS indexes.| | ||||
| |Persist last failure reason in SourceState|BE-Storage|Storage.Mongo|DONE – `MongoSourceStateRepository.MarkFailureAsync` stores `lastFailureReason` with length guard + reset on success.| | ||||
| |AdvisoryStore range primitives deserialization|BE-Storage|Models|DONE – BSON helpers handle `RangePrimitives`; regression test covers SemVer/NEVRA/EVR envelopes persisted through Mongo.| | ||||
| |FEEDSTORAGE-DATA-03-001 Merge event provenance audit prep|BE-Storage|Merge|DONE – merge events now persist field-level decision reasons via `MergeFieldDecision` documents for analytics. **Coordination:** log any new precedence signals to storage@ so indexes/serializers stay aligned.| | ||||
| |FEEDSTORAGE-DATA-02-001 Normalized range dual-write + backfill|BE-Storage|Core|DONE – advisory documents store `normalizedVersions`, migration respects `EnableSemVerStyle`, and decision reasons flow into normalized write path. **Action:** connector owners confirm `EnableSemVerStyle=true` readiness before 2025-10-18 rollout.| | ||||
| |FEEDSTORAGE-DATA-02-001 Normalized range dual-write + backfill|BE-Storage|Core|**DONE (2025-10-12)** – `AdvisoryStore` honors `EnableSemVerStyle`, dual-writes normalized docs, and SemVer backfill migration registered for staged rollout.| | ||||
| |FEEDSTORAGE-TESTS-02-004 Restore AdvisoryStore build after normalized versions refactor|QA|Storage.Mongo|DONE – storage tests updated to cover normalized version payloads and new provenance fields. **Heads-up:** QA to watch for fixture bumps touching normalized rule arrays when connectors roll out support.| | ||||
| |FEEDSTORAGE-DATA-02-002 Provenance decision persistence|BE-Storage|Models `FEEDMODELS-SCHEMA-01-002`|**DONE (2025-10-12)** – Normalized documents carry decision reasons/source/timestamps with regression coverage verifying SemVer notes + provenance fallbacks.| | ||||
| |FEEDSTORAGE-DATA-02-003 Normalized versions index creation|BE-Storage|Normalization, Mongo bootstrapper|**DONE (2025-10-12)** – Bootstrapper seeds `normalizedVersions.*` indexes when SemVer style is enabled; docs/tests confirm index presence.| | ||||
|   | ||||
		Reference in New Issue
	
	Block a user