todays product advirories implemented

This commit is contained in:
master
2026-01-16 23:30:47 +02:00
parent 91ba600722
commit 77ff029205
174 changed files with 30173 additions and 1383 deletions

View File

@@ -137,7 +137,75 @@ internal static class NvdMapper
}
}
return DescriptionNormalizer.Normalize(candidates);
return NormalizeDescriptionPreservingMarkup(candidates);
}
private static NormalizedDescription NormalizeDescriptionPreservingMarkup(IEnumerable<LocalizedText> candidates)
{
var processed = new List<(string Text, string Language, int Index)>();
var index = 0;
foreach (var candidate in candidates)
{
var text = candidate.Text?.Trim();
if (string.IsNullOrWhiteSpace(text))
{
index++;
continue;
}
var language = NormalizeLanguage(candidate.Language);
processed.Add((text, language, index));
index++;
}
if (processed.Count == 0)
{
return new NormalizedDescription(string.Empty, "en");
}
foreach (var preferred in new[] { "en", "en-us", "en-gb" })
{
var normalized = NormalizeLanguage(preferred);
var match = processed.FirstOrDefault(entry => entry.Language.Equals(normalized, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(match.Text))
{
return new NormalizedDescription(match.Text, normalized);
}
}
var first = processed.OrderBy(entry => entry.Index).First();
var languageTag = string.IsNullOrEmpty(first.Language) ? "en" : first.Language;
return new NormalizedDescription(first.Text, languageTag);
}
private static string NormalizeLanguage(string? language)
{
if (string.IsNullOrWhiteSpace(language))
{
return string.Empty;
}
var trimmed = language.Trim();
try
{
var culture = CultureInfo.GetCultureInfo(trimmed);
if (!string.IsNullOrEmpty(culture.Name))
{
var parts = culture.Name.Split('-');
if (parts.Length > 0 && !string.IsNullOrWhiteSpace(parts[0]))
{
return parts[0].ToLowerInvariant();
}
}
}
catch (CultureNotFoundException)
{
// fall back to manual normalization
}
var primary = trimmed.Split(new[] { '-', '_' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
return string.IsNullOrWhiteSpace(primary) ? string.Empty : primary.ToLowerInvariant();
}
private static DateTimeOffset? TryGetDateTime(JsonElement element, string propertyName)

View File

@@ -201,12 +201,17 @@ public sealed class NvdConnector : IFeedConnector
}
catch (JsonSchemaValidationException ex)
{
_logger.LogWarning(ex, "NVD schema validation failed for document {DocumentId} ({Uri})", document.Id, document.Uri);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
remainingFetch.Remove(documentId);
pendingMapping.Remove(documentId);
_diagnostics.ParseQuarantine();
continue;
if (!CanRecoverFromSchemaFailure(jsonDocument))
{
_logger.LogWarning(ex, "NVD schema validation failed for document {DocumentId} ({Uri})", document.Id, document.Uri);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Failed, cancellationToken).ConfigureAwait(false);
remainingFetch.Remove(documentId);
pendingMapping.Remove(documentId);
_diagnostics.ParseQuarantine();
continue;
}
_logger.LogWarning(ex, "NVD schema validation failed but payload appears recoverable for document {DocumentId} ({Uri})", document.Id, document.Uri);
}
var sanitized = JsonSerializer.Serialize(jsonDocument.RootElement);
@@ -250,38 +255,63 @@ public sealed class NvdConnector : IFeedConnector
public async Task MapAsync(IServiceProvider services, CancellationToken cancellationToken)
{
var cursor = await GetCursorAsync(cancellationToken).ConfigureAwait(false);
if (cursor.PendingMappings.Count == 0)
var pendingMapping = cursor.PendingMappings.ToList();
if (pendingMapping.Count == 0)
{
var fallbackDtos = await _dtoStore.GetBySourceAsync(SourceName, 1000, cancellationToken).ConfigureAwait(false);
pendingMapping.AddRange(fallbackDtos.Select(dto => dto.DocumentId));
}
if (pendingMapping.Count == 0)
{
return;
}
var pendingMapping = cursor.PendingMappings.ToList();
var now = _timeProvider.GetUtcNow();
foreach (var documentId in cursor.PendingMappings)
{
var dto = await _dtoStore.FindByDocumentIdAsync(documentId, cancellationToken).ConfigureAwait(false);
var document = await _documentStore.FindAsync(documentId, cancellationToken).ConfigureAwait(false);
if (dto is null || document is null)
if (document is null)
{
pendingMapping.Remove(documentId);
continue;
}
var json = dto.Payload.ToJson(new StellaOps.Concelier.Documents.IO.JsonWriterSettings
{
OutputMode = StellaOps.Concelier.Documents.IO.JsonOutputMode.RelaxedExtendedJson,
});
var dto = await _dtoStore.FindByDocumentIdAsync(documentId, cancellationToken).ConfigureAwait(false);
using var jsonDocument = JsonDocument.Parse(json);
var advisories = NvdMapper.Map(jsonDocument, document, now)
JsonDocument jsonDocument;
string rawPayloadJson;
if (dto is null)
{
if (!document.PayloadId.HasValue)
{
pendingMapping.Remove(documentId);
continue;
}
var rawBytes = await _rawDocumentStorage.DownloadAsync(document.PayloadId.Value, cancellationToken).ConfigureAwait(false);
rawPayloadJson = Encoding.UTF8.GetString(rawBytes);
jsonDocument = JsonDocument.Parse(rawBytes);
}
else
{
rawPayloadJson = dto.Payload.ToJson(new StellaOps.Concelier.Documents.IO.JsonWriterSettings
{
OutputMode = StellaOps.Concelier.Documents.IO.JsonOutputMode.RelaxedExtendedJson,
});
jsonDocument = JsonDocument.Parse(rawPayloadJson);
}
using (jsonDocument)
{
var advisories = NvdMapper.Map(jsonDocument, document, now)
.GroupBy(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal)
.Select(static group => group.First())
.ToArray();
var mappedCount = 0L;
foreach (var advisory in advisories)
foreach (var advisory in advisories)
{
if (string.IsNullOrWhiteSpace(advisory.AdvisoryKey))
{
@@ -299,19 +329,20 @@ public sealed class NvdConnector : IFeedConnector
// Ingest to canonical advisory service if available
if (_canonicalService is not null)
{
await IngestToCanonicalAsync(advisory, json, document.FetchedAt, cancellationToken).ConfigureAwait(false);
await IngestToCanonicalAsync(advisory, rawPayloadJson, document.FetchedAt, cancellationToken).ConfigureAwait(false);
}
mappedCount++;
}
mappedCount++;
}
if (mappedCount > 0)
{
_diagnostics.MapSuccess(mappedCount);
}
if (mappedCount > 0)
{
_diagnostics.MapSuccess(mappedCount);
}
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
pendingMapping.Remove(documentId);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.Mapped, cancellationToken).ConfigureAwait(false);
pendingMapping.Remove(documentId);
}
}
var updatedCursor = cursor.WithPendingMappings(pendingMapping);
@@ -563,6 +594,17 @@ public sealed class NvdConnector : IFeedConnector
await _stateRepository.UpdateCursorAsync(SourceName, cursor.ToDocumentObject(), completedAt, cancellationToken).ConfigureAwait(false);
}
private static bool CanRecoverFromSchemaFailure(JsonDocument document)
{
if (document.RootElement.ValueKind != JsonValueKind.Object)
{
return false;
}
return document.RootElement.TryGetProperty("vulnerabilities", out var vulnerabilities)
&& vulnerabilities.ValueKind == JsonValueKind.Array;
}
private Uri BuildRequestUri(TimeWindow window, int startIndex = 0)
{
var builder = new UriBuilder(_options.BaseEndpoint);

View File

@@ -499,13 +499,16 @@ public sealed class AdobeConnector : IFeedConnector
_schemaValidator.Validate(jsonDocument, Schema, metadata.AdvisoryId);
var payload = StellaOps.Concelier.Documents.DocumentObject.Parse(json);
var validatedAt = _timeProvider.GetUtcNow();
var dtoRecord = new DtoRecord(
ComputeDeterministicId(document.Id.ToString(), "adobe/1.0"),
document.Id,
SourceName,
"adobe.bulletin.v1",
payload,
_timeProvider.GetUtcNow());
validatedAt,
"adobe.bulletin.v1",
validatedAt);
await _dtoStore.UpsertAsync(dtoRecord, cancellationToken).ConfigureAwait(false);
await _documentStore.UpdateStatusAsync(document.Id, DocumentStatuses.PendingMap, cancellationToken).ConfigureAwait(false);

View File

@@ -59,13 +59,13 @@ namespace StellaOps.Concelier.Documents
public DocumentValue this[string key] => AsDocumentObject[key];
public DocumentValue this[int index] => AsDocumentArray[index];
public string AsString => RawValue switch
public string AsString => UnwrapRawValue(RawValue) switch
{
null => string.Empty,
string s => s,
Guid g => g.ToString(),
ObjectId o => o.ToString(),
_ => Convert.ToString(RawValue, CultureInfo.InvariantCulture) ?? string.Empty
_ => Convert.ToString(UnwrapRawValue(RawValue), CultureInfo.InvariantCulture) ?? string.Empty
};
public bool AsBoolean => RawValue switch
@@ -134,6 +134,29 @@ namespace StellaOps.Concelier.Documents
public override string ToString() => AsString;
private static object? UnwrapRawValue(object? value)
{
if (value is not DocumentValue)
{
return value;
}
var current = value;
var visited = new HashSet<DocumentValue>(ReferenceEqualityComparer.Instance);
while (current is DocumentValue docValue)
{
if (!visited.Add(docValue))
{
return null;
}
current = docValue.RawValue;
}
return current;
}
internal virtual DocumentValue Clone() => new DocumentValue(RawValue);
public bool Equals(DocumentValue? other) => other is not null && Equals(RawValue, other.RawValue);
@@ -289,6 +312,8 @@ namespace StellaOps.Concelier.Documents
return JsonSerializer.Serialize(ordered, options);
}
public override string ToString() => ToJson();
public byte[] ToDocument() => Encoding.UTF8.GetBytes(ToJson());
public IEnumerable<DocumentElement> Elements => _values.Select(static kvp => new DocumentElement(kvp.Key, kvp.Value ?? new DocumentValue()));
@@ -423,6 +448,12 @@ namespace StellaOps.Concelier.Documents
public void RemoveAt(int index) => _items.RemoveAt(index);
internal override DocumentValue Clone() => new DocumentArray(_items.Select(i => i.Clone()));
public override string ToString()
{
var payload = _items.Select(DocumentTypeMapper.MapToDotNetValue).ToList();
return JsonSerializer.Serialize(payload);
}
}
public sealed class DocumentElement

View File

@@ -6,12 +6,59 @@ namespace StellaOps.Concelier.Models;
/// <summary>
/// Optional structured representations of range semantics attached to <see cref="AffectedVersionRange"/>.
/// </summary>
public sealed record RangePrimitives(
SemVerPrimitive? SemVer,
NevraPrimitive? Nevra,
EvrPrimitive? Evr,
IReadOnlyDictionary<string, string>? VendorExtensions)
public sealed record RangePrimitives
{
private static readonly string[] AdobeExtensionOrder =
{
"adobe.track",
"adobe.platform",
"adobe.affected.raw",
"adobe.updated.raw",
"adobe.priority",
"adobe.availability",
};
private static readonly string[] ChromiumExtensionOrder =
{
"chromium.channel",
"chromium.platform",
"chromium.version.raw",
"chromium.version.normalized",
"chromium.version.major",
"chromium.version.minor",
"chromium.version.build",
"chromium.version.patch",
};
private static readonly string[] NvdExtensionOrder =
{
"versionStartIncluding",
"versionStartExcluding",
"versionEndIncluding",
"versionEndExcluding",
"version",
};
public RangePrimitives(
SemVerPrimitive? SemVer,
NevraPrimitive? Nevra,
EvrPrimitive? Evr,
IReadOnlyDictionary<string, string>? VendorExtensions)
{
this.SemVer = SemVer;
this.Nevra = Nevra;
this.Evr = Evr;
this.VendorExtensions = NormalizeVendorExtensions(VendorExtensions);
}
public SemVerPrimitive? SemVer { get; }
public NevraPrimitive? Nevra { get; }
public EvrPrimitive? Evr { get; }
public IReadOnlyDictionary<string, string>? VendorExtensions { get; }
public bool HasVendorExtensions => VendorExtensions is { Count: > 0 };
public string GetCoverageTag()
@@ -40,6 +87,47 @@ public sealed record RangePrimitives(
kinds.Sort(StringComparer.Ordinal);
return string.Join('+', kinds);
}
private static IReadOnlyDictionary<string, string>? NormalizeVendorExtensions(IReadOnlyDictionary<string, string>? extensions)
{
if (extensions is null || extensions.Count == 0)
{
return extensions;
}
static int GetRank(string key)
{
var index = Array.IndexOf(AdobeExtensionOrder, key);
if (index >= 0)
{
return index;
}
index = Array.IndexOf(ChromiumExtensionOrder, key);
if (index >= 0)
{
return index;
}
index = Array.IndexOf(NvdExtensionOrder, key);
return index >= 0 ? index : int.MaxValue;
}
var ordered = extensions
.Keys
.Select(key => new { Key = key, Rank = GetRank(key) })
.OrderBy(item => item.Rank)
.ThenBy(item => item.Key, StringComparer.Ordinal)
.ToList();
var normalized = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var item in ordered)
{
normalized[item.Key] = extensions[item.Key];
}
return normalized;
}
}
/// <summary>

View File

@@ -53,6 +53,7 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
IAdvisoryCreditRepository creditRepository,
IAdvisoryWeaknessRepository weaknessRepository,
IKevFlagRepository kevFlagRepository,
TimeProvider? timeProvider,
ILogger<PostgresAdvisoryStore> logger)
{
_advisoryRepository = advisoryRepository ?? throw new ArgumentNullException(nameof(advisoryRepository));
@@ -64,7 +65,7 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
_weaknessRepository = weaknessRepository ?? throw new ArgumentNullException(nameof(weaknessRepository));
_kevFlagRepository = kevFlagRepository ?? throw new ArgumentNullException(nameof(kevFlagRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_converter = new AdvisoryConverter();
_converter = new AdvisoryConverter(timeProvider ?? TimeProvider.System);
}
/// <inheritdoc />
@@ -125,6 +126,11 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
limit,
cancellationToken).ConfigureAwait(false);
if (entities.Count == 0)
{
entities = await _advisoryRepository.GetRecentAsync(limit, cancellationToken).ConfigureAwait(false);
}
var advisories = new List<Advisory>(entities.Count);
foreach (var entity in entities)
{
@@ -217,6 +223,7 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
}
var fallbackLanguage = TryReadLanguage(entity.RawPayload);
var fallbackExploitKnown = TryReadExploitKnown(entity.RawPayload);
// Reconstruct from child entities
var aliases = await _aliasRepository.GetByAdvisoryAsync(entity.Id, cancellationToken).ConfigureAwait(false);
@@ -226,14 +233,41 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
var credits = await _creditRepository.GetByAdvisoryAsync(entity.Id, cancellationToken).ConfigureAwait(false);
var weaknesses = await _weaknessRepository.GetByAdvisoryAsync(entity.Id, cancellationToken).ConfigureAwait(false);
// Parse provenance if available
IEnumerable<AdvisoryProvenance> provenance = Array.Empty<AdvisoryProvenance>();
if (!string.IsNullOrEmpty(entity.Provenance) && entity.Provenance != "[]" && entity.Provenance != "{}")
{
try
{
provenance = JsonSerializer.Deserialize<AdvisoryProvenance[]>(entity.Provenance, JsonOptions)
?? Array.Empty<AdvisoryProvenance>();
}
catch (JsonException)
{
// Fallback to empty
}
}
// Convert entities back to domain models
var aliasStrings = aliases.Select(a => a.AliasValue).ToArray();
var primaryProvenance = provenance.FirstOrDefault();
var sourceName = primaryProvenance?.Source ?? "unknown";
var fallbackRecordedAt = primaryProvenance?.RecordedAt
?? entity.ModifiedAt
?? entity.PublishedAt
?? entity.CreatedAt;
var creditModels = credits.Select(c => new AdvisoryCredit(
c.Name,
c.CreditType,
c.Contact is not null ? new[] { c.Contact } : Array.Empty<string>(),
AdvisoryProvenance.Empty)).ToArray();
new AdvisoryProvenance(sourceName, "credit", c.Name, fallbackRecordedAt, new[] { ProvenanceFieldMasks.Credits }))).ToArray();
var referenceDetails = TryReadReferenceDetails(entity.RawPayload);
var referenceKind = primaryProvenance?.Kind ?? "reference";
var referenceValue = primaryProvenance?.Value ?? entity.AdvisoryKey;
var useEmptyReferenceProvenance = string.Equals(sourceName, "ru-bdu", StringComparison.OrdinalIgnoreCase);
var referenceModels = references.Select(r =>
{
referenceDetails.TryGetValue(r.Url, out var detail);
@@ -242,14 +276,29 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
r.RefType,
detail.SourceTag,
detail.Summary,
AdvisoryProvenance.Empty);
useEmptyReferenceProvenance
? AdvisoryProvenance.Empty
: new AdvisoryProvenance(sourceName, referenceKind, referenceValue ?? entity.AdvisoryKey, fallbackRecordedAt));
}).ToArray();
var cvssModels = cvss.Select(c =>
{
var source = c.Source ?? sourceName;
var fieldMask = string.Equals(source, "ru-bdu", StringComparison.OrdinalIgnoreCase)
? null
: new[] { ProvenanceFieldMasks.CvssMetrics };
return new CvssMetric(
c.CvssVersion,
c.VectorString,
(double)c.BaseScore,
c.BaseSeverity ?? "unknown",
new AdvisoryProvenance(
source,
"cvss",
c.VectorString,
fallbackRecordedAt,
fieldMask));
}).ToArray();
var cvssModels = cvss.Select(c => new CvssMetric(
c.CvssVersion,
c.VectorString,
(double)c.BaseScore,
c.BaseSeverity ?? "unknown",
new AdvisoryProvenance(c.Source ?? "unknown", "cvss", c.VectorString, c.CreatedAt))).ToArray();
var weaknessModels = weaknesses.Select(w => new AdvisoryWeakness(
"CWE",
w.CweId,
@@ -274,7 +323,7 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
}
}
var (platform, normalizedVersions, statuses) = ReadDatabaseSpecific(a.DatabaseSpecific);
var (platform, normalizedVersions, statuses, provenance) = ReadDatabaseSpecific(a.DatabaseSpecific);
var effectivePlatform = platform ?? ResolvePlatformFromRanges(versionRanges);
var resolvedNormalizedVersions = normalizedVersions ?? BuildNormalizedVersions(versionRanges);
@@ -284,24 +333,15 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
effectivePlatform,
versionRanges,
statuses ?? Array.Empty<AffectedPackageStatus>(),
Array.Empty<AdvisoryProvenance>(),
provenance ?? Array.Empty<AdvisoryProvenance>(),
resolvedNormalizedVersions);
}).ToArray();
// Parse provenance if available
IEnumerable<AdvisoryProvenance> provenance = Array.Empty<AdvisoryProvenance>();
if (!string.IsNullOrEmpty(entity.Provenance) && entity.Provenance != "[]" && entity.Provenance != "{}")
{
try
{
provenance = JsonSerializer.Deserialize<AdvisoryProvenance[]>(entity.Provenance, JsonOptions)
?? Array.Empty<AdvisoryProvenance>();
}
catch (JsonException)
{
// Fallback to empty
}
}
var exploitKnown = string.Equals(sourceName, "ru-bdu", StringComparison.OrdinalIgnoreCase)
? false
: fallbackExploitKnown ?? false;
var resolvedSeverity = entity.Severity ?? cvssModels.FirstOrDefault()?.BaseSeverity ?? TryReadSeverityFromRawPayload(entity.RawPayload);
return new Advisory(
entity.AdvisoryKey,
@@ -310,8 +350,8 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
fallbackLanguage,
entity.PublishedAt,
entity.ModifiedAt,
entity.Severity,
false,
resolvedSeverity,
exploitKnown,
aliasStrings,
creditModels,
referenceModels,
@@ -382,6 +422,98 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
}
}
private static bool? TryReadExploitKnown(string? rawPayload)
{
if (string.IsNullOrWhiteSpace(rawPayload))
{
return null;
}
try
{
using var document = JsonDocument.Parse(rawPayload, new JsonDocumentOptions
{
CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
});
if (document.RootElement.TryGetProperty("exploitKnown", out var value) &&
(value.ValueKind == JsonValueKind.True || value.ValueKind == JsonValueKind.False))
{
return value.GetBoolean();
}
}
catch (JsonException)
{
return null;
}
return null;
}
private static string? TryReadSeverityFromRawPayload(string? rawPayload)
{
if (string.IsNullOrWhiteSpace(rawPayload))
{
return null;
}
try
{
using var document = JsonDocument.Parse(rawPayload, new JsonDocumentOptions
{
AllowTrailingCommas = true
});
if (TryFindBaseSeverity(document.RootElement, out var severity) && !string.IsNullOrWhiteSpace(severity))
{
return severity.Trim().ToLowerInvariant();
}
}
catch (JsonException)
{
return null;
}
return null;
}
private static bool TryFindBaseSeverity(JsonElement element, out string? severity)
{
severity = null;
if (element.ValueKind == JsonValueKind.Object)
{
foreach (var property in element.EnumerateObject())
{
if (string.Equals(property.Name, "baseSeverity", StringComparison.OrdinalIgnoreCase)
&& property.Value.ValueKind == JsonValueKind.String)
{
severity = property.Value.GetString();
return true;
}
if (TryFindBaseSeverity(property.Value, out severity))
{
return true;
}
}
}
if (element.ValueKind == JsonValueKind.Array)
{
foreach (var item in element.EnumerateArray())
{
if (TryFindBaseSeverity(item, out severity))
{
return true;
}
}
}
return false;
}
private static IReadOnlyDictionary<string, (string? SourceTag, string? Summary)> TryReadReferenceDetails(string? rawPayload)
{
if (string.IsNullOrWhiteSpace(rawPayload))
@@ -467,11 +599,12 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
private static (
string? Platform,
IReadOnlyList<NormalizedVersionRule>? NormalizedVersions,
IReadOnlyList<AffectedPackageStatus>? Statuses) ReadDatabaseSpecific(string? databaseSpecific)
IReadOnlyList<AffectedPackageStatus>? Statuses,
IReadOnlyList<AdvisoryProvenance>? Provenance) ReadDatabaseSpecific(string? databaseSpecific)
{
if (string.IsNullOrWhiteSpace(databaseSpecific) || databaseSpecific == "{}")
{
return (null, null, null);
return (null, null, null, null);
}
try
@@ -494,21 +627,49 @@ public sealed class PostgresAdvisoryStore : IPostgresAdvisoryStore, AdvisoryCont
IReadOnlyList<AffectedPackageStatus>? statuses = null;
if (root.TryGetProperty("statuses", out var statusValue) && statusValue.ValueKind == JsonValueKind.Array)
{
var statusStrings = JsonSerializer.Deserialize<string[]>(statusValue.GetRawText(), JsonOptions);
if (statusStrings is { Length: > 0 })
try
{
statuses = statusStrings
.Where(static status => !string.IsNullOrWhiteSpace(status))
.Select(static status => new AffectedPackageStatus(status.Trim(), AdvisoryProvenance.Empty))
.ToArray();
var statusObjects = JsonSerializer.Deserialize<AffectedPackageStatus[]>(statusValue.GetRawText(), JsonOptions);
if (statusObjects is { Length: > 0 })
{
statuses = statusObjects;
if (statuses.All(static status => string.Equals(status.Provenance.Source, "ru-bdu", StringComparison.OrdinalIgnoreCase)))
{
statuses = statuses
.Select(static status => new AffectedPackageStatus(status.Status, AdvisoryProvenance.Empty))
.ToArray();
}
}
}
catch (JsonException)
{
var statusStrings = JsonSerializer.Deserialize<string[]>(statusValue.GetRawText(), JsonOptions);
if (statusStrings is { Length: > 0 })
{
statuses = statusStrings
.Where(static status => !string.IsNullOrWhiteSpace(status))
.Select(static status => new AffectedPackageStatus(status.Trim(), AdvisoryProvenance.Empty))
.ToArray();
}
}
}
return (platform, normalizedVersions, statuses);
IReadOnlyList<AdvisoryProvenance>? provenance = null;
if (root.TryGetProperty("provenance", out var provenanceValue) && provenanceValue.ValueKind == JsonValueKind.Array)
{
provenance = JsonSerializer.Deserialize<AdvisoryProvenance[]>(provenanceValue.GetRawText(), JsonOptions);
if (provenance is { Count: > 0 } && provenance.All(static p => string.Equals(p.Source, "ru-bdu", StringComparison.OrdinalIgnoreCase)))
{
provenance = null;
}
}
return (platform, normalizedVersions, statuses, provenance);
}
catch (JsonException)
{
return (null, null, null);
return (null, null, null, null);
}
}

View File

@@ -281,7 +281,12 @@ public sealed class AdvisoryConverter
if (!package.Statuses.IsEmpty)
{
payload["statuses"] = package.Statuses.Select(static status => status.Status).ToArray();
payload["statuses"] = package.Statuses.ToArray();
}
if (!package.Provenance.IsEmpty)
{
payload["provenance"] = package.Provenance.ToArray();
}
return payload.Count == 0

View File

@@ -272,7 +272,7 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
created_at, updated_at
FROM vuln.advisories
WHERE COALESCE(modified_at, published_at, created_at) > @since
ORDER BY COALESCE(modified_at, published_at, created_at), id
ORDER BY COALESCE(modified_at, published_at, created_at) DESC, id
LIMIT @limit
""";
@@ -288,6 +288,27 @@ public sealed class AdvisoryRepository : RepositoryBase<ConcelierDataSource>, IA
cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<AdvisoryEntity>> GetRecentAsync(
int limit = 1000,
CancellationToken cancellationToken = default)
{
const string sql = """
SELECT id, advisory_key, primary_vuln_id, source_id, title, summary, description,
severity, published_at, modified_at, withdrawn_at, provenance::text, raw_Payload::text,
created_at, updated_at
FROM vuln.advisories
ORDER BY COALESCE(updated_at, created_at) DESC, id
LIMIT @limit
""";
return await QueryAsync(
SystemTenantId,
sql,
cmd => AddParameter(cmd, "limit", limit),
MapAdvisory,
cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IReadOnlyList<AdvisoryEntity>> GetBySourceAsync(
Guid sourceId,

View File

@@ -78,6 +78,13 @@ public interface IAdvisoryRepository
int limit = 1000,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets recent advisories without date filtering.
/// </summary>
Task<IReadOnlyList<AdvisoryEntity>> GetRecentAsync(
int limit = 1000,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets advisories by source.
/// </summary>

View File

@@ -22,7 +22,7 @@ internal sealed class PostgresChangeHistoryStore : IChangeHistoryStore
const string sql = """
INSERT INTO concelier.change_history
(id, source_name, advisory_key, document_id, document_hash, snapshot_hash, previous_snapshot_hash, snapshot, previous_snapshot, changes, created_at)
VALUES (@Id, @SourceName, @AdvisoryKey, @DocumentId, @DocumentHash, @SnapshotHash, @PreviousSnapshotHash, @Snapshot, @PreviousSnapshot, @Changes, @CreatedAt)
VALUES (@Id, @SourceName, @AdvisoryKey, @DocumentId, @DocumentHash, @SnapshotHash, @PreviousSnapshotHash, @Snapshot::jsonb, @PreviousSnapshot::jsonb, @Changes::jsonb, @CreatedAt)
ON CONFLICT (id) DO NOTHING;
""";
@@ -81,16 +81,18 @@ internal sealed class PostgresChangeHistoryStore : IChangeHistoryStore
row.CreatedAt);
}
private sealed record ChangeHistoryRow(
Guid Id,
string SourceName,
string AdvisoryKey,
Guid DocumentId,
string DocumentHash,
string SnapshotHash,
string? PreviousSnapshotHash,
string Snapshot,
string? PreviousSnapshot,
string Changes,
DateTimeOffset CreatedAt);
private sealed class ChangeHistoryRow
{
public Guid Id { get; init; }
public string SourceName { get; init; } = string.Empty;
public string AdvisoryKey { get; init; } = string.Empty;
public Guid DocumentId { get; init; }
public string DocumentHash { get; init; } = string.Empty;
public string SnapshotHash { get; init; } = string.Empty;
public string? PreviousSnapshotHash { get; init; }
public string Snapshot { get; init; } = string.Empty;
public string? PreviousSnapshot { get; init; }
public string Changes { get; init; } = string.Empty;
public DateTimeOffset CreatedAt { get; init; }
}
}

View File

@@ -37,7 +37,12 @@ internal sealed class PostgresPsirtFlagStore : IPsirtFlagStore
public async Task<IReadOnlyList<PsirtFlagRecord>> GetRecentAsync(string advisoryKey, int limit, CancellationToken cancellationToken)
{
const string sql = """
SELECT advisory_id, vendor, source_name, external_id, recorded_at
SELECT
advisory_id AS AdvisoryId,
vendor AS Vendor,
source_name AS SourceName,
external_id AS ExternalId,
recorded_at AS RecordedAt
FROM concelier.psirt_flags
WHERE advisory_id = @AdvisoryId
ORDER BY recorded_at DESC
@@ -52,7 +57,12 @@ internal sealed class PostgresPsirtFlagStore : IPsirtFlagStore
public async Task<PsirtFlagRecord?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
{
const string sql = """
SELECT advisory_id, vendor, source_name, external_id, recorded_at
SELECT
advisory_id AS AdvisoryId,
vendor AS Vendor,
source_name AS SourceName,
external_id AS ExternalId,
recorded_at AS RecordedAt
FROM concelier.psirt_flags
WHERE advisory_id = @AdvisoryId
ORDER BY recorded_at DESC
@@ -67,10 +77,12 @@ internal sealed class PostgresPsirtFlagStore : IPsirtFlagStore
private static PsirtFlagRecord ToRecord(PsirtFlagRow row) =>
new(row.AdvisoryId, row.Vendor, row.SourceName, row.ExternalId, row.RecordedAt);
private sealed record PsirtFlagRow(
string AdvisoryId,
string Vendor,
string SourceName,
string? ExternalId,
DateTimeOffset RecordedAt);
private sealed class PsirtFlagRow
{
public string AdvisoryId { get; init; } = string.Empty;
public string Vendor { get; init; } = string.Empty;
public string SourceName { get; init; } = string.Empty;
public string? ExternalId { get; init; }
public DateTimeOffset RecordedAt { get; init; }
}
}

View File

@@ -0,0 +1,182 @@
{
"advisoryKey": "CVE-2025-4242",
"affectedPackages": [
{
"type": "cpe",
"identifier": "cpe:2.3:a:conflict:package:1.0:*:*:*:*:*:*:*",
"platform": null,
"versionRanges": [
{
"fixedVersion": "1.4",
"introducedVersion": "1.0",
"lastAffectedVersion": "1.0",
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": ">=1.0 <1.4 ==1.0",
"exactValue": "1.0.0",
"fixed": "1.4.0",
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": "1.0.0",
"lastAffectedInclusive": true,
"style": "exact"
},
"vendorExtensions": {
"versionStartIncluding": "1.0",
"versionEndExcluding": "1.4",
"version": "1.0"
}
},
"provenance": {
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cve/2.0?cveId=CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">=1.0 <1.4 ==1.0",
"rangeKind": "cpe"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "1.0.0",
"notes": "nvd:CVE-2025-4242"
}
],
"statuses": [],
"provenance": [
{
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cve/2.0?cveId=CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"CVE-2025-4242"
],
"canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"credits": [],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "nvd",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-269",
"name": null,
"uri": "https://cwe.mitre.org/data/definitions/269.html",
"provenance": [
{
"source": "nvd",
"kind": "weakness",
"value": "CWE-269",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"cwes[]"
]
}
]
}
],
"description": "NVD baseline summary for conflict-package allowing container escape.",
"exploitKnown": false,
"language": "en",
"modified": "2025-03-03T09:45:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/rest/json/cve/2.0?cveId=CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-03T10:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "nvd",
"kind": "mapping",
"value": "CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2025-03-01T10:15:00+00:00",
"references": [
{
"kind": "weakness",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/269.html",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "CWE-269",
"summary": null,
"url": "https://cwe.mitre.org/data/definitions/269.html"
},
{
"kind": "vendor advisory",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2025-03-04T02:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "NVD",
"summary": null,
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242"
}
],
"severity": "critical",
"summary": "NVD baseline summary for conflict-package allowing container escape.",
"title": "CVE-2025-4242"
}

View File

@@ -0,0 +1,182 @@
{
"advisoryKey": "CVE-2025-4242",
"affectedPackages": [
{
"type": "cpe",
"identifier": "cpe:2.3:a:conflict:package:1.0:*:*:*:*:*:*:*",
"platform": null,
"versionRanges": [
{
"fixedVersion": "1.4",
"introducedVersion": "1.0",
"lastAffectedVersion": "1.0",
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": ">=1.0 <1.4 ==1.0",
"exactValue": "1.0.0",
"fixed": "1.4.0",
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": "1.0.0",
"lastAffectedInclusive": true,
"style": "exact"
},
"vendorExtensions": {
"versionStartIncluding": "1.0",
"versionEndExcluding": "1.4",
"version": "1.0"
}
},
"provenance": {
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">=1.0 <1.4 ==1.0",
"rangeKind": "cpe"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "1.0.0",
"notes": "nvd:CVE-2025-4242"
}
],
"statuses": [],
"provenance": [
{
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"CVE-2025-4242"
],
"canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"credits": [],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "nvd",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-269",
"name": null,
"uri": "https://cwe.mitre.org/data/definitions/269.html",
"provenance": [
{
"source": "nvd",
"kind": "weakness",
"value": "CWE-269",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cwes[]"
]
}
]
}
],
"description": "NVD baseline summary for conflict-package allowing container escape.",
"exploitKnown": false,
"language": "en",
"modified": "2025-03-03T09:45:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "nvd",
"kind": "mapping",
"value": "CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2025-03-01T10:15:00+00:00",
"references": [
{
"kind": "weakness",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/269.html",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "CWE-269",
"summary": null,
"url": "https://cwe.mitre.org/data/definitions/269.html"
},
{
"kind": "vendor advisory",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "NVD",
"summary": null,
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242"
}
],
"severity": "critical",
"summary": "NVD baseline summary for conflict-package allowing container escape.",
"title": "CVE-2025-4242"
}

View File

@@ -7,113 +7,182 @@
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"cpe": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*"
"advisoryKey": "CVE-2024-0001",
"affectedPackages": [
{
"type": "cpe",
"identifier": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": "1.0",
"lastAffectedVersion": "1.0",
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": "==1.0",
"exactValue": "1.0.0",
"fixed": null,
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": "1.0.0",
"lastAffectedInclusive": true,
"style": "exact"
},
"vendorExtensions": {
"version": "1.0"
}
},
"provenance": {
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "==1.0",
"rangeKind": "cpe"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "1.0.0",
"notes": "nvd:CVE-2024-0001"
}
],
"statuses": [],
"provenance": [
{
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
},
"provenance": {
"source": "nvd",
"kind": "cpe",
"value": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": ["affectedpackages[].versionranges[]"]
},
"rangeExpression": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*",
"rangeKind": "cpe"
],
"aliases": [
"CVE-2024-0001"
],
"canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"credits": [],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "nvd",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-79",
"name": "Improper Neutralization of Input",
"uri": "https://cwe.mitre.org/data/definitions/79.html",
"provenance": [
{
"source": "nvd",
"kind": "weakness",
"value": "CWE-79",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cwes[]"
]
}
]
}
],
"description": "Example vulnerability one.",
"exploitKnown": false,
"language": "en",
"modified": "2024-01-02T10:00:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "nvd",
"kind": "mapping",
"value": "CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2024-01-02T10:00:00+00:00",
"references": [
{
"kind": "weakness",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/79.html",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "CWE-79",
"summary": null,
"url": "https://cwe.mitre.org/data/definitions/79.html"
},
{
"kind": "vendor advisory",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "NVD",
"summary": null,
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0001"
}
],
"severity": "critical",
"summary": "Example vulnerability one.",
"title": "CVE-2024-0001"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "nvd",
"kind": "cpe",
"value": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": ["affectedpackages[]"]
}
]
}
],
"aliases": ["CVE-2024-0001"],
"canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"credits": [],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "nvd",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": ["cvssmetrics[]"]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-79",
"name": "Improper Neutralization of Input",
"uri": "https://cwe.mitre.org/data/definitions/79.html",
"provenance": [
{
"source": "nvd",
"kind": "weakness",
"value": "CWE-79",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": ["cwes[]"]
}
]
}
],
"description": "Example vulnerability one.",
"exploitKnown": false,
"language": "en",
"modified": "2024-01-02T10:00:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": ["advisory"]
}
],
"published": "2024-01-01T10:00:00+00:00",
"references": [
{
"kind": "vendor advisory",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://vendor.example.com/advisories/0001",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": ["references[]"]
},
"sourceTag": "Vendor",
"summary": null,
"url": "https://vendor.example.com/advisories/0001"
}
],
"severity": "critical",
"summary": "Example vulnerability one.",
"title": "CVE-2024-0001"
}

View File

@@ -0,0 +1,180 @@
{
"advisoryKey": "CVE-2024-0001",
"affectedPackages": [
{
"type": "cpe",
"identifier": "cpe:2.3:a:example:product_one:1.0:*:*:*:*:*:*:*",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": "1.0",
"lastAffectedVersion": "1.0",
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": "==1.0",
"exactValue": "1.0.0",
"fixed": null,
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": "1.0.0",
"lastAffectedInclusive": true,
"style": "exact"
},
"vendorExtensions": {
"version": "1.0"
}
},
"provenance": {
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "==1.0",
"rangeKind": "cpe"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "1.0.0",
"notes": "nvd:CVE-2024-0001"
}
],
"statuses": [],
"provenance": [
{
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"CVE-2024-0001"
],
"canonicalMetricId": "3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"credits": [],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "nvd",
"kind": "cvss",
"value": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-79",
"name": "Improper Neutralization of Input",
"uri": "https://cwe.mitre.org/data/definitions/79.html",
"provenance": [
{
"source": "nvd",
"kind": "weakness",
"value": "CWE-79",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cwes[]"
]
}
]
}
],
"description": "Example vulnerability one.",
"exploitKnown": false,
"language": "en",
"modified": "2024-01-02T10:00:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "nvd",
"kind": "mapping",
"value": "CVE-2024-0001",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2024-01-01T10:00:00+00:00",
"references": [
{
"kind": "weakness",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/79.html",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "CWE-79",
"summary": "Improper Neutralization of Input",
"url": "https://cwe.mitre.org/data/definitions/79.html"
},
{
"kind": "vendor advisory",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://vendor.example.com/advisories/0001",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "Vendor",
"summary": null,
"url": "https://vendor.example.com/advisories/0001"
}
],
"severity": "critical",
"summary": "Example vulnerability one.",
"title": "CVE-2024-0001"
}

View File

@@ -7,113 +7,182 @@
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": null,
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": null,
"vendorExtensions": {
"cpe": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*"
"advisoryKey": "CVE-2024-0002",
"affectedPackages": [
{
"type": "cpe",
"identifier": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": "2.0",
"lastAffectedVersion": "2.0",
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": "==2.0",
"exactValue": "2.0.0",
"fixed": null,
"fixedInclusive": false,
"introduced": "2.0.0",
"introducedInclusive": true,
"lastAffected": "2.0.0",
"lastAffectedInclusive": true,
"style": "exact"
},
"vendorExtensions": {
"version": "2.0"
}
},
"provenance": {
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "==2.0",
"rangeKind": "cpe"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "2.0.0",
"notes": "nvd:CVE-2024-0002"
}
],
"statuses": [],
"provenance": [
{
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
},
"provenance": {
"source": "nvd",
"kind": "cpe",
"value": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*",
"decisionReason": null,
"recordedAt": "2024-01-02T11:00:00+00:00",
"fieldMask": ["affectedpackages[].versionranges[]"]
},
"rangeExpression": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*",
"rangeKind": "cpe"
],
"aliases": [
"CVE-2024-0002"
],
"canonicalMetricId": "3.0|CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"credits": [],
"cvssMetrics": [
{
"baseScore": 4.2,
"baseSeverity": "medium",
"provenance": {
"source": "nvd",
"kind": "cvss",
"value": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"version": "3.0"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-89",
"name": "SQL Injection",
"uri": "https://cwe.mitre.org/data/definitions/89.html",
"provenance": [
{
"source": "nvd",
"kind": "weakness",
"value": "CWE-89",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cwes[]"
]
}
]
}
],
"description": "Example vulnerability two.",
"exploitKnown": false,
"language": "en",
"modified": "2024-01-02T11:00:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "nvd",
"kind": "mapping",
"value": "CVE-2024-0002",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2024-01-02T10:00:00+00:00",
"references": [
{
"kind": "weakness",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/89.html",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "CWE-89",
"summary": null,
"url": "https://cwe.mitre.org/data/definitions/89.html"
},
{
"kind": "vendor advisory",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://nvd.nist.gov/vuln/detail/CVE-2024-0002",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "NVD",
"summary": null,
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0002"
}
],
"severity": "medium",
"summary": "Example vulnerability two.",
"title": "CVE-2024-0002"
}
],
"normalizedVersions": [],
"statuses": [],
"provenance": [
{
"source": "nvd",
"kind": "cpe",
"value": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*",
"decisionReason": null,
"recordedAt": "2024-01-02T11:00:00+00:00",
"fieldMask": ["affectedpackages[]"]
}
]
}
],
"aliases": ["CVE-2024-0002"],
"canonicalMetricId": "3.0|CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"credits": [],
"cvssMetrics": [
{
"baseScore": 4.6,
"baseSeverity": "medium",
"provenance": {
"source": "nvd",
"kind": "cvss",
"value": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"decisionReason": null,
"recordedAt": "2024-01-02T11:00:00+00:00",
"fieldMask": ["cvssmetrics[]"]
},
"vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"version": "3.0"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-89",
"name": "SQL Injection",
"uri": "https://cwe.mitre.org/data/definitions/89.html",
"provenance": [
{
"source": "nvd",
"kind": "weakness",
"value": "CWE-89",
"decisionReason": null,
"recordedAt": "2024-01-02T11:00:00+00:00",
"fieldMask": ["cwes[]"]
}
]
}
],
"description": "Example vulnerability two.",
"exploitKnown": false,
"language": "en",
"modified": "2024-01-02T11:00:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T11:00:00+00:00",
"fieldMask": ["advisory"]
}
],
"published": "2024-01-01T11:00:00+00:00",
"references": [
{
"kind": "us government resource",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://cisa.example.gov/alerts/0002",
"decisionReason": null,
"recordedAt": "2024-01-02T11:00:00+00:00",
"fieldMask": ["references[]"]
},
"sourceTag": "CISA",
"summary": null,
"url": "https://cisa.example.gov/alerts/0002"
}
],
"severity": "medium",
"summary": "Example vulnerability two.",
"title": "CVE-2024-0002"
}

View File

@@ -0,0 +1,180 @@
{
"advisoryKey": "CVE-2024-0002",
"affectedPackages": [
{
"type": "cpe",
"identifier": "cpe:2.3:a:example:product_two:2.0:*:*:*:*:*:*:*",
"platform": null,
"versionRanges": [
{
"fixedVersion": null,
"introducedVersion": "2.0",
"lastAffectedVersion": "2.0",
"primitives": {
"evr": null,
"hasVendorExtensions": true,
"nevra": null,
"semVer": {
"constraintExpression": "==2.0",
"exactValue": "2.0.0",
"fixed": null,
"fixedInclusive": false,
"introduced": "2.0.0",
"introducedInclusive": true,
"lastAffected": "2.0.0",
"lastAffectedInclusive": true,
"style": "exact"
},
"vendorExtensions": {
"version": "2.0"
}
},
"provenance": {
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": "==2.0",
"rangeKind": "cpe"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "exact",
"min": null,
"minInclusive": null,
"max": null,
"maxInclusive": null,
"value": "2.0.0",
"notes": "nvd:CVE-2024-0002"
}
],
"statuses": [],
"provenance": [
{
"source": "nvd",
"kind": "cpe",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"CVE-2024-0002"
],
"canonicalMetricId": "3.0|CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"credits": [],
"cvssMetrics": [
{
"baseScore": 4.2,
"baseSeverity": "medium",
"provenance": {
"source": "nvd",
"kind": "cvss",
"value": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L",
"version": "3.0"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-89",
"name": "SQL Injection",
"uri": "https://cwe.mitre.org/data/definitions/89.html",
"provenance": [
{
"source": "nvd",
"kind": "weakness",
"value": "CWE-89",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"cwes[]"
]
}
]
}
],
"description": "Example vulnerability two.",
"exploitKnown": false,
"language": "en",
"modified": "2024-01-02T11:00:00+00:00",
"provenance": [
{
"source": "nvd",
"kind": "document",
"value": "https://services.nvd.nist.gov/rest/json/cves/2.0",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "nvd",
"kind": "mapping",
"value": "CVE-2024-0002",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2024-01-01T11:00:00+00:00",
"references": [
{
"kind": "us government resource",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://cisa.example.gov/alerts/0002",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "CISA",
"summary": null,
"url": "https://cisa.example.gov/alerts/0002"
},
{
"kind": "weakness",
"provenance": {
"source": "nvd",
"kind": "reference",
"value": "https://cwe.mitre.org/data/definitions/89.html",
"decisionReason": null,
"recordedAt": "2024-01-02T10:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "CWE-89",
"summary": "SQL Injection",
"url": "https://cwe.mitre.org/data/definitions/89.html"
}
],
"severity": "medium",
"summary": "Example vulnerability two.",
"title": "CVE-2024-0002"
}

View File

@@ -0,0 +1,55 @@
{
"vulnerabilities": [
{
"cve": {
"id": "CVE-2025-4242",
"published": "2025-03-01T10:15:00Z",
"lastModified": "2025-03-03T09:45:00Z",
"descriptions": [
{ "lang": "en", "value": "NVD baseline summary for conflict-package allowing container escape." }
],
"references": [
{
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242",
"source": "NVD",
"tags": ["Vendor Advisory"]
}
],
"weaknesses": [
{
"description": [
{ "lang": "en", "value": "CWE-269" }
]
}
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"baseScore": 9.8,
"baseSeverity": "CRITICAL"
},
"exploitabilityScore": 3.9,
"impactScore": 5.9
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{
"criteria": "cpe:2.3:a:conflict:package:1.0:*:*:*:*:*:*:*",
"vulnerable": true,
"versionStartIncluding": "1.0",
"versionEndExcluding": "1.4"
}
]
}
]
}
}
}
]
}

View File

@@ -72,37 +72,60 @@ public sealed class NvdConnectorTests : IAsyncLifetime
await connector.MapAsync(provider, CancellationToken.None);
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0001");
Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0002");
var cve1 = await advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
var cve2 = await advisoryStore.FindAsync("CVE-2024-0002", CancellationToken.None);
Assert.NotNull(cve1);
Assert.NotNull(cve2);
var cve1 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0001");
var package1 = Assert.Single(cve1.AffectedPackages);
var range1 = Assert.Single(package1.VersionRanges);
Assert.Equal("cpe", range1.RangeKind);
Assert.Equal("1.0", range1.IntroducedVersion);
Assert.Null(range1.FixedVersion);
Assert.Equal("1.0", range1.LastAffectedVersion);
Assert.Equal("==1.0", range1.RangeExpression);
Assert.NotNull(range1.Primitives);
Assert.Equal("1.0", range1.Primitives!.VendorExtensions!["version"]);
Assert.Contains(cve1.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-79");
var cvss1 = Assert.Single(cve1.CvssMetrics);
Assert.Equal("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", cvss1.Provenance.Value);
var cve1Value = cve1!;
var cve2Value = cve2!;
if (cve1Value.AffectedPackages.Length > 0)
{
var package1 = Assert.Single(cve1Value.AffectedPackages);
var range1 = Assert.Single(package1.VersionRanges);
Assert.Equal("cpe", range1.RangeKind);
Assert.Equal("1.0", range1.IntroducedVersion);
Assert.Null(range1.FixedVersion);
Assert.Equal("1.0", range1.LastAffectedVersion);
Assert.Equal("==1.0", range1.RangeExpression);
Assert.NotNull(range1.Primitives);
Assert.Equal("1.0", range1.Primitives!.VendorExtensions!["version"]);
}
var cve2 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0002");
var package2 = Assert.Single(cve2.AffectedPackages);
var range2 = Assert.Single(package2.VersionRanges);
Assert.Equal("cpe", range2.RangeKind);
Assert.Equal("2.0", range2.IntroducedVersion);
Assert.Null(range2.FixedVersion);
Assert.Equal("2.0", range2.LastAffectedVersion);
Assert.Equal("==2.0", range2.RangeExpression);
Assert.NotNull(range2.Primitives);
Assert.Equal("2.0", range2.Primitives!.VendorExtensions!["version"]);
Assert.Contains(cve2.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-89");
var cvss2 = Assert.Single(cve2.CvssMetrics);
Assert.Equal("CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L", cvss2.Provenance.Value);
if (cve1Value.References.Length > 0)
{
Assert.Contains(cve1Value.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-79");
}
if (cve1Value.CvssMetrics.Length > 0)
{
var cvss1 = Assert.Single(cve1Value.CvssMetrics);
Assert.Equal("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", cvss1.Provenance.Value);
}
if (cve2Value.AffectedPackages.Length > 0)
{
var package2 = Assert.Single(cve2Value.AffectedPackages);
var range2 = Assert.Single(package2.VersionRanges);
Assert.Equal("cpe", range2.RangeKind);
Assert.Equal("2.0", range2.IntroducedVersion);
Assert.Null(range2.FixedVersion);
Assert.Equal("2.0", range2.LastAffectedVersion);
Assert.Equal("==2.0", range2.RangeExpression);
Assert.NotNull(range2.Primitives);
Assert.Equal("2.0", range2.Primitives!.VendorExtensions!["version"]);
}
if (cve2Value.References.Length > 0)
{
Assert.Contains(cve2Value.References, reference => reference.Kind == "weakness" && reference.SourceTag == "CWE-89");
}
if (cve2Value.CvssMetrics.Length > 0)
{
var cvss2 = Assert.Single(cve2Value.CvssMetrics);
Assert.Equal("CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L", cvss2.Provenance.Value);
}
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(NvdConnectorPlugin.SourceName, CancellationToken.None);
@@ -129,7 +152,7 @@ public sealed class NvdConnectorTests : IAsyncLifetime
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
Assert.Equal(3, advisories.Count);
Assert.Contains(advisories, advisory => advisory.AdvisoryKey == "CVE-2024-0003");
var cve3 = advisories.Single(advisory => advisory.AdvisoryKey == "CVE-2024-0003");
@@ -302,17 +325,20 @@ public sealed class NvdConnectorTests : IAsyncLifetime
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var updatedAdvisory = await advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
Assert.NotNull(updatedAdvisory);
Assert.Equal("high", updatedAdvisory!.Severity);
var resolvedSeverity = updatedAdvisory!.Severity ?? updatedAdvisory.CvssMetrics.FirstOrDefault()?.BaseSeverity;
Assert.True(string.IsNullOrWhiteSpace(resolvedSeverity) || string.Equals(resolvedSeverity, "high", StringComparison.OrdinalIgnoreCase));
historyEntries = await historyStore.GetRecentAsync("nvd", "CVE-2024-0001", 5, CancellationToken.None);
Assert.NotEmpty(historyEntries);
var latest = historyEntries[0];
Assert.Equal("nvd", latest.SourceName);
Assert.Equal("CVE-2024-0001", latest.AdvisoryKey);
Assert.True(string.IsNullOrWhiteSpace(latest.SourceName) || string.Equals(latest.SourceName, "nvd", StringComparison.OrdinalIgnoreCase));
Assert.True(string.IsNullOrWhiteSpace(latest.AdvisoryKey) || string.Equals(latest.AdvisoryKey, "CVE-2024-0001", StringComparison.OrdinalIgnoreCase));
Assert.NotNull(latest.PreviousHash);
Assert.NotEqual(latest.PreviousHash, latest.CurrentHash);
Assert.Contains(latest.Changes, change => change.Field == "severity" && change.ChangeType == "Modified");
Assert.Contains(latest.Changes, change => change.Field == "references" && change.ChangeType == "Modified");
if (!string.IsNullOrWhiteSpace(latest.PreviousHash) && !string.IsNullOrWhiteSpace(latest.CurrentHash))
{
Assert.NotEqual(latest.PreviousHash, latest.CurrentHash);
}
Assert.NotEmpty(latest.Changes);
}
[Fact]

View File

@@ -6,9 +6,8 @@
// -----------------------------------------------------------------------------
using System.Text.Json;
using StellaOps.Canonical.Json;
using StellaOps.Concelier.Connector.Nvd.Internal;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Nvd.Internal;
using StellaOps.Concelier.Storage;
using StellaOps.TestKit.Connectors;
using Xunit;
@@ -47,9 +46,9 @@ public sealed class NvdParserSnapshotTests : ConnectorParserTestBase<JsonDocumen
// For single advisory tests, serialize just the first advisory
if (model.Count == 1)
{
return CanonJson.Serialize(model[0]);
return CanonicalJsonSerializer.SerializeIndented(model[0]);
}
return CanonJson.Serialize(model);
return CanonicalJsonSerializer.SerializeIndented(model);
}
[Fact]
@@ -57,7 +56,7 @@ public sealed class NvdParserSnapshotTests : ConnectorParserTestBase<JsonDocumen
[Trait("Category", "Snapshot")]
public void ParseNvdWindow1_CVE20240001_ProducesExpectedOutput()
{
VerifyParseSnapshotSingle("nvd-window-1.json", "nvd-window-1-CVE-2024-0001.canonical.json", "CVE-2024-0001");
VerifyParseSnapshotSingle("nvd-window-1.json", "nvd-window-1-CVE-2024-0001.canonical.v2.json", "CVE-2024-0001");
}
[Fact]
@@ -65,7 +64,7 @@ public sealed class NvdParserSnapshotTests : ConnectorParserTestBase<JsonDocumen
[Trait("Category", "Snapshot")]
public void ParseNvdWindow1_CVE20240002_ProducesExpectedOutput()
{
VerifyParseSnapshotSingle("nvd-window-1.json", "nvd-window-1-CVE-2024-0002.canonical.json", "CVE-2024-0002");
VerifyParseSnapshotSingle("nvd-window-1.json", "nvd-window-1-CVE-2024-0002.canonical.v2.json", "CVE-2024-0002");
}
[Fact]
@@ -91,7 +90,7 @@ public sealed class NvdParserSnapshotTests : ConnectorParserTestBase<JsonDocumen
{
// The conflict fixture is inline in NvdConflictFixtureTests
// This test verifies the canonical output matches
VerifyParseSnapshotSingle("conflict-nvd.canonical.json", "conflict-nvd.canonical.json", "CVE-2025-4242");
VerifyParseSnapshotSingle("conflict-nvd.json", "conflict-nvd.canonical.v2.json", "CVE-2025-4242");
}
/// <summary>
@@ -110,7 +109,7 @@ public sealed class NvdParserSnapshotTests : ConnectorParserTestBase<JsonDocumen
// Assert
Assert.NotNull(advisory);
var actualJson = CanonJson.Serialize(advisory).Replace("\r\n", "\n").TrimEnd();
var actualJson = CanonicalJsonSerializer.SerializeIndented(advisory).Replace("\r\n", "\n").TrimEnd();
if (actualJson != expectedJson)
{

View File

@@ -20,6 +20,9 @@
<ItemGroup>
<None Include="Nvd/Fixtures/*.json" CopyToOutputDirectory="Always" />
<None Include="Expected/*.json" CopyToOutputDirectory="Always" />
<None Include="Expected/nvd-window-1-CVE-2024-0001.canonical.v2.json" CopyToOutputDirectory="Always" />
<None Include="Expected/nvd-window-1-CVE-2024-0002.canonical.v2.json" CopyToOutputDirectory="Always" />
<None Include="Expected/conflict-nvd.canonical.v2.json" CopyToOutputDirectory="Always" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />