Rename Concelier Source modules to Connector

This commit is contained in:
2025-10-18 20:11:18 +03:00
parent 0137856fdb
commit 6524626230
789 changed files with 1489 additions and 1489 deletions

View File

@@ -0,0 +1,64 @@
using System.Linq;
using MongoDB.Bson;
using StellaOps.Concelier.Connector.Common.Cursors;
namespace StellaOps.Concelier.Connector.Nvd.Internal;
internal sealed record NvdCursor(
TimeWindowCursorState Window,
IReadOnlyCollection<Guid> PendingDocuments,
IReadOnlyCollection<Guid> PendingMappings)
{
public static NvdCursor Empty { get; } = new(TimeWindowCursorState.Empty, Array.Empty<Guid>(), Array.Empty<Guid>());
public BsonDocument ToBsonDocument()
{
var document = new BsonDocument();
Window.WriteTo(document);
document["pendingDocuments"] = new BsonArray(PendingDocuments.Select(id => id.ToString()));
document["pendingMappings"] = new BsonArray(PendingMappings.Select(id => id.ToString()));
return document;
}
public static NvdCursor FromBsonDocument(BsonDocument? document)
{
if (document is null || document.ElementCount == 0)
{
return Empty;
}
var window = TimeWindowCursorState.FromBsonDocument(document);
var pendingDocuments = ReadGuidArray(document, "pendingDocuments");
var pendingMappings = ReadGuidArray(document, "pendingMappings");
return new NvdCursor(window, pendingDocuments, pendingMappings);
}
private static IReadOnlyCollection<Guid> ReadGuidArray(BsonDocument document, string field)
{
if (!document.TryGetValue(field, out var value) || value is not BsonArray array)
{
return Array.Empty<Guid>();
}
var results = new List<Guid>(array.Count);
foreach (var element in array)
{
if (Guid.TryParse(element.AsString, out var guid))
{
results.Add(guid);
}
}
return results;
}
public NvdCursor WithWindow(TimeWindow window)
=> this with { Window = Window.WithWindow(window) };
public NvdCursor WithPendingDocuments(IEnumerable<Guid> ids)
=> this with { PendingDocuments = ids?.Distinct().ToArray() ?? Array.Empty<Guid>() };
public NvdCursor WithPendingMappings(IEnumerable<Guid> ids)
=> this with { PendingMappings = ids?.Distinct().ToArray() ?? Array.Empty<Guid>() };
}

View File

@@ -0,0 +1,76 @@
using System.Diagnostics.Metrics;
namespace StellaOps.Concelier.Connector.Nvd.Internal;
public sealed class NvdDiagnostics : IDisposable
{
public const string MeterName = "StellaOps.Concelier.Connector.Nvd";
public const string MeterVersion = "1.0.0";
private readonly Meter _meter;
private readonly Counter<long> _fetchAttempts;
private readonly Counter<long> _fetchDocuments;
private readonly Counter<long> _fetchFailures;
private readonly Counter<long> _fetchUnchanged;
private readonly Counter<long> _parseSuccess;
private readonly Counter<long> _parseFailures;
private readonly Counter<long> _parseQuarantine;
private readonly Counter<long> _mapSuccess;
public NvdDiagnostics()
{
_meter = new Meter(MeterName, MeterVersion);
_fetchAttempts = _meter.CreateCounter<long>(
name: "nvd.fetch.attempts",
unit: "operations",
description: "Number of NVD fetch operations attempted, including paginated windows.");
_fetchDocuments = _meter.CreateCounter<long>(
name: "nvd.fetch.documents",
unit: "documents",
description: "Count of NVD documents fetched and persisted.");
_fetchFailures = _meter.CreateCounter<long>(
name: "nvd.fetch.failures",
unit: "operations",
description: "Count of NVD fetch attempts that resulted in an error or missing document.");
_fetchUnchanged = _meter.CreateCounter<long>(
name: "nvd.fetch.unchanged",
unit: "operations",
description: "Count of NVD fetch attempts returning 304 Not Modified.");
_parseSuccess = _meter.CreateCounter<long>(
name: "nvd.parse.success",
unit: "documents",
description: "Count of NVD documents successfully validated and converted into DTOs.");
_parseFailures = _meter.CreateCounter<long>(
name: "nvd.parse.failures",
unit: "documents",
description: "Count of NVD documents that failed parsing due to missing content or read errors.");
_parseQuarantine = _meter.CreateCounter<long>(
name: "nvd.parse.quarantine",
unit: "documents",
description: "Count of NVD documents quarantined due to schema validation failures.");
_mapSuccess = _meter.CreateCounter<long>(
name: "nvd.map.success",
unit: "advisories",
description: "Count of canonical advisories produced by NVD mapping.");
}
public void FetchAttempt() => _fetchAttempts.Add(1);
public void FetchDocument() => _fetchDocuments.Add(1);
public void FetchFailure() => _fetchFailures.Add(1);
public void FetchUnchanged() => _fetchUnchanged.Add(1);
public void ParseSuccess() => _parseSuccess.Add(1);
public void ParseFailure() => _parseFailures.Add(1);
public void ParseQuarantine() => _parseQuarantine.Add(1);
public void MapSuccess(long count = 1) => _mapSuccess.Add(count);
public Meter Meter => _meter;
public void Dispose() => _meter.Dispose();
}

View File

@@ -0,0 +1,774 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Text.Json;
using NuGet.Versioning;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Normalization.Identifiers;
using StellaOps.Concelier.Normalization.Cvss;
using StellaOps.Concelier.Normalization.Text;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace StellaOps.Concelier.Connector.Nvd.Internal;
internal static class NvdMapper
{
public static IReadOnlyList<Advisory> Map(JsonDocument document, DocumentRecord sourceDocument, DateTimeOffset recordedAt)
{
ArgumentNullException.ThrowIfNull(document);
ArgumentNullException.ThrowIfNull(sourceDocument);
if (!document.RootElement.TryGetProperty("vulnerabilities", out var vulnerabilities) || vulnerabilities.ValueKind != JsonValueKind.Array)
{
return Array.Empty<Advisory>();
}
var advisories = new List<Advisory>(vulnerabilities.GetArrayLength());
var index = 0;
foreach (var vulnerability in vulnerabilities.EnumerateArray())
{
if (!vulnerability.TryGetProperty("cve", out var cve) || cve.ValueKind != JsonValueKind.Object)
{
index++;
continue;
}
if (!cve.TryGetProperty("id", out var idElement) || idElement.ValueKind != JsonValueKind.String)
{
index++;
continue;
}
var cveId = idElement.GetString();
var advisoryKey = string.IsNullOrWhiteSpace(cveId)
? $"nvd:{sourceDocument.Id:N}:{index}"
: cveId;
var published = TryGetDateTime(cve, "published");
var modified = TryGetDateTime(cve, "lastModified");
var description = GetNormalizedDescription(cve);
var weaknessMetadata = GetWeaknessMetadata(cve);
var references = GetReferences(cve, sourceDocument, recordedAt, weaknessMetadata);
var affectedPackages = GetAffectedPackages(cve, cveId, sourceDocument, recordedAt);
var cvssMetrics = GetCvssMetrics(cve, sourceDocument, recordedAt, out var severity);
var weaknesses = BuildWeaknesses(weaknessMetadata, recordedAt);
var canonicalMetricId = cvssMetrics.Count > 0
? $"{cvssMetrics[0].Version}|{cvssMetrics[0].Vector}"
: null;
var provenance = new[]
{
new AdvisoryProvenance(
NvdConnectorPlugin.SourceName,
"document",
sourceDocument.Uri,
sourceDocument.FetchedAt,
new[] { ProvenanceFieldMasks.Advisory }),
new AdvisoryProvenance(
NvdConnectorPlugin.SourceName,
"mapping",
string.IsNullOrWhiteSpace(cveId) ? advisoryKey : cveId,
recordedAt,
new[] { ProvenanceFieldMasks.Advisory }),
};
var title = string.IsNullOrWhiteSpace(cveId) ? advisoryKey : cveId;
var aliasCandidates = new List<string>(capacity: 2);
if (!string.IsNullOrWhiteSpace(cveId))
{
aliasCandidates.Add(cveId);
}
aliasCandidates.Add(advisoryKey);
var advisory = new Advisory(
advisoryKey: advisoryKey,
title: title,
summary: string.IsNullOrEmpty(description.Text) ? null : description.Text,
language: description.Language,
published: published,
modified: modified,
severity: severity,
exploitKnown: false,
aliases: aliasCandidates,
references: references,
affectedPackages: affectedPackages,
cvssMetrics: cvssMetrics,
provenance: provenance,
description: string.IsNullOrEmpty(description.Text) ? null : description.Text,
cwes: weaknesses,
canonicalMetricId: canonicalMetricId);
advisories.Add(advisory);
index++;
}
return advisories;
}
private static NormalizedDescription GetNormalizedDescription(JsonElement cve)
{
var candidates = new List<LocalizedText>();
if (cve.TryGetProperty("descriptions", out var descriptions) && descriptions.ValueKind == JsonValueKind.Array)
{
foreach (var item in descriptions.EnumerateArray())
{
if (item.ValueKind != JsonValueKind.Object)
{
continue;
}
var text = item.TryGetProperty("value", out var valueElement) && valueElement.ValueKind == JsonValueKind.String
? valueElement.GetString()
: null;
var lang = item.TryGetProperty("lang", out var langElement) && langElement.ValueKind == JsonValueKind.String
? langElement.GetString()
: null;
if (!string.IsNullOrWhiteSpace(text))
{
candidates.Add(new LocalizedText(text, lang));
}
}
}
return DescriptionNormalizer.Normalize(candidates);
}
private static DateTimeOffset? TryGetDateTime(JsonElement element, string propertyName)
{
if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.String)
{
return null;
}
return DateTimeOffset.TryParse(property.GetString(), out var parsed) ? parsed : null;
}
private static IReadOnlyList<AdvisoryReference> GetReferences(
JsonElement cve,
DocumentRecord document,
DateTimeOffset recordedAt,
IReadOnlyList<WeaknessMetadata> weaknesses)
{
var references = new List<AdvisoryReference>();
if (!cve.TryGetProperty("references", out var referencesElement) || referencesElement.ValueKind != JsonValueKind.Array)
{
AppendWeaknessReferences(references, weaknesses, recordedAt);
return references;
}
foreach (var reference in referencesElement.EnumerateArray())
{
if (!reference.TryGetProperty("url", out var urlElement) || urlElement.ValueKind != JsonValueKind.String)
{
continue;
}
var url = urlElement.GetString();
if (string.IsNullOrWhiteSpace(url) || !Validation.LooksLikeHttpUrl(url))
{
continue;
}
var sourceTag = reference.TryGetProperty("source", out var sourceElement) ? sourceElement.GetString() : null;
string? kind = null;
if (reference.TryGetProperty("tags", out var tagsElement) && tagsElement.ValueKind == JsonValueKind.Array)
{
kind = tagsElement.EnumerateArray().Select(static t => t.GetString()).FirstOrDefault(static tag => !string.IsNullOrWhiteSpace(tag))?.ToLowerInvariant();
}
references.Add(new AdvisoryReference(
url: url,
kind: kind,
sourceTag: sourceTag,
summary: null,
provenance: new AdvisoryProvenance(
NvdConnectorPlugin.SourceName,
"reference",
url,
recordedAt,
new[] { ProvenanceFieldMasks.References })));
}
AppendWeaknessReferences(references, weaknesses, recordedAt);
return references;
}
private static IReadOnlyList<WeaknessMetadata> GetWeaknessMetadata(JsonElement cve)
{
if (!cve.TryGetProperty("weaknesses", out var weaknesses) || weaknesses.ValueKind != JsonValueKind.Array)
{
return Array.Empty<WeaknessMetadata>();
}
var list = new List<WeaknessMetadata>(weaknesses.GetArrayLength());
foreach (var weakness in weaknesses.EnumerateArray())
{
if (!weakness.TryGetProperty("description", out var descriptions) || descriptions.ValueKind != JsonValueKind.Array)
{
continue;
}
string? cweId = null;
string? name = null;
foreach (var description in descriptions.EnumerateArray())
{
if (description.ValueKind != JsonValueKind.Object)
{
continue;
}
if (!description.TryGetProperty("value", out var valueElement) || valueElement.ValueKind != JsonValueKind.String)
{
continue;
}
var value = valueElement.GetString();
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
var trimmed = value.Trim();
if (trimmed.StartsWith("CWE-", StringComparison.OrdinalIgnoreCase))
{
cweId ??= trimmed.ToUpperInvariant();
}
else
{
name ??= trimmed;
}
}
if (string.IsNullOrWhiteSpace(cweId))
{
continue;
}
list.Add(new WeaknessMetadata(cweId, name));
}
return list.Count == 0 ? Array.Empty<WeaknessMetadata>() : list;
}
private static IReadOnlyList<AdvisoryWeakness> BuildWeaknesses(IReadOnlyList<WeaknessMetadata> metadata, DateTimeOffset recordedAt)
{
if (metadata.Count == 0)
{
return Array.Empty<AdvisoryWeakness>();
}
var list = new List<AdvisoryWeakness>(metadata.Count);
foreach (var entry in metadata)
{
var provenance = new AdvisoryProvenance(
NvdConnectorPlugin.SourceName,
"weakness",
entry.CweId,
recordedAt,
new[] { ProvenanceFieldMasks.Weaknesses });
var provenanceArray = ImmutableArray.Create(provenance);
list.Add(new AdvisoryWeakness(
taxonomy: "cwe",
identifier: entry.CweId,
name: entry.Name,
uri: BuildCweUrl(entry.CweId),
provenance: provenanceArray));
}
return list;
}
private static void AppendWeaknessReferences(
List<AdvisoryReference> references,
IReadOnlyList<WeaknessMetadata> weaknesses,
DateTimeOffset recordedAt)
{
if (weaknesses.Count == 0)
{
return;
}
var existing = new HashSet<string>(references.Select(reference => reference.Url), StringComparer.OrdinalIgnoreCase);
foreach (var weakness in weaknesses)
{
var url = BuildCweUrl(weakness.CweId);
if (url is null || existing.Contains(url))
{
continue;
}
var provenance = new AdvisoryProvenance(
NvdConnectorPlugin.SourceName,
"reference",
url,
recordedAt,
new[] { ProvenanceFieldMasks.References });
references.Add(new AdvisoryReference(url, "weakness", weakness.CweId, weakness.Name, provenance));
existing.Add(url);
}
}
private static IReadOnlyList<AffectedPackage> GetAffectedPackages(JsonElement cve, string? cveId, DocumentRecord document, DateTimeOffset recordedAt)
{
var packages = new Dictionary<string, PackageAccumulator>(StringComparer.Ordinal);
if (!cve.TryGetProperty("configurations", out var configurations) || configurations.ValueKind != JsonValueKind.Object)
{
return Array.Empty<AffectedPackage>();
}
if (!configurations.TryGetProperty("nodes", out var nodes) || nodes.ValueKind != JsonValueKind.Array)
{
return Array.Empty<AffectedPackage>();
}
foreach (var node in nodes.EnumerateArray())
{
if (!node.TryGetProperty("cpeMatch", out var matches) || matches.ValueKind != JsonValueKind.Array)
{
continue;
}
foreach (var match in matches.EnumerateArray())
{
if (match.TryGetProperty("vulnerable", out var vulnerableElement) && vulnerableElement.ValueKind == JsonValueKind.False)
{
continue;
}
if (!match.TryGetProperty("criteria", out var criteriaElement) || criteriaElement.ValueKind != JsonValueKind.String)
{
continue;
}
var criteria = criteriaElement.GetString();
if (string.IsNullOrWhiteSpace(criteria))
{
continue;
}
var identifier = IdentifierNormalizer.TryNormalizeCpe(criteria, out var normalizedCpe) && !string.IsNullOrWhiteSpace(normalizedCpe)
? normalizedCpe
: criteria.Trim();
var provenance = new AdvisoryProvenance(
NvdConnectorPlugin.SourceName,
"cpe",
document.Uri,
recordedAt,
new[] { ProvenanceFieldMasks.AffectedPackages });
if (!packages.TryGetValue(identifier, out var accumulator))
{
accumulator = new PackageAccumulator();
packages[identifier] = accumulator;
}
var range = BuildVersionRange(match, criteria, provenance);
if (range is not null)
{
accumulator.Ranges.Add(range);
}
accumulator.Provenance.Add(provenance);
}
}
if (packages.Count == 0)
{
return Array.Empty<AffectedPackage>();
}
return packages
.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal)
.Select(kvp =>
{
var ranges = kvp.Value.Ranges.Count == 0
? Array.Empty<AffectedVersionRange>()
: kvp.Value.Ranges
.OrderBy(static range => range, AffectedVersionRangeComparer.Instance)
.ToArray();
var provenance = kvp.Value.Provenance
.OrderBy(static p => p.Source, StringComparer.Ordinal)
.ThenBy(static p => p.Kind, StringComparer.Ordinal)
.ThenBy(static p => p.Value, StringComparer.Ordinal)
.ThenBy(static p => p.RecordedAt.UtcDateTime)
.ToArray();
var normalizedNote = string.IsNullOrWhiteSpace(cveId)
? $"nvd:{document.Id:N}"
: $"nvd:{cveId}";
var normalizedVersions = new List<NormalizedVersionRule>(ranges.Length);
foreach (var range in ranges)
{
var rule = range.ToNormalizedVersionRule(normalizedNote);
if (rule is not null)
{
normalizedVersions.Add(rule);
}
}
return new AffectedPackage(
type: AffectedPackageTypes.Cpe,
identifier: kvp.Key,
platform: null,
versionRanges: ranges,
statuses: Array.Empty<AffectedPackageStatus>(),
provenance: provenance,
normalizedVersions: normalizedVersions.Count == 0
? Array.Empty<NormalizedVersionRule>()
: normalizedVersions.ToArray());
})
.ToArray();
}
private static IReadOnlyList<CvssMetric> GetCvssMetrics(JsonElement cve, DocumentRecord document, DateTimeOffset recordedAt, out string? severity)
{
severity = null;
if (!cve.TryGetProperty("metrics", out var metrics) || metrics.ValueKind != JsonValueKind.Object)
{
return Array.Empty<CvssMetric>();
}
var sources = new[] { "cvssMetricV31", "cvssMetricV30", "cvssMetricV2" };
foreach (var source in sources)
{
if (!metrics.TryGetProperty(source, out var array) || array.ValueKind != JsonValueKind.Array)
{
continue;
}
var list = new List<CvssMetric>();
foreach (var item in array.EnumerateArray())
{
if (!item.TryGetProperty("cvssData", out var data) || data.ValueKind != JsonValueKind.Object)
{
continue;
}
if (!data.TryGetProperty("vectorString", out var vectorElement) || vectorElement.ValueKind != JsonValueKind.String)
{
continue;
}
if (!data.TryGetProperty("baseScore", out var scoreElement) || scoreElement.ValueKind != JsonValueKind.Number)
{
continue;
}
if (!data.TryGetProperty("baseSeverity", out var severityElement) || severityElement.ValueKind != JsonValueKind.String)
{
continue;
}
var vector = vectorElement.GetString() ?? string.Empty;
var baseScore = scoreElement.GetDouble();
var baseSeverity = severityElement.GetString();
var versionToken = source switch
{
"cvssMetricV30" => "3.0",
"cvssMetricV31" => "3.1",
_ => "2.0",
};
if (!CvssMetricNormalizer.TryNormalize(versionToken, vector, baseScore, baseSeverity, out var normalized))
{
continue;
}
severity ??= normalized.BaseSeverity;
list.Add(normalized.ToModel(new AdvisoryProvenance(
NvdConnectorPlugin.SourceName,
"cvss",
normalized.Vector,
recordedAt,
new[] { ProvenanceFieldMasks.CvssMetrics })));
}
if (list.Count > 0)
{
return list;
}
}
return Array.Empty<CvssMetric>();
}
private static AffectedVersionRange? BuildVersionRange(JsonElement match, string criteria, AdvisoryProvenance provenance)
{
static string? ReadString(JsonElement parent, string property)
{
if (!parent.TryGetProperty(property, out var value) || value.ValueKind != JsonValueKind.String)
{
return null;
}
var text = value.GetString();
return string.IsNullOrWhiteSpace(text) ? null : text.Trim();
}
var version = ReadString(match, "version");
if (string.Equals(version, "*", StringComparison.Ordinal))
{
version = null;
}
version ??= TryExtractVersionFromCriteria(criteria);
var versionStartIncluding = ReadString(match, "versionStartIncluding");
var versionStartExcluding = ReadString(match, "versionStartExcluding");
var versionEndIncluding = ReadString(match, "versionEndIncluding");
var versionEndExcluding = ReadString(match, "versionEndExcluding");
var vendorExtensions = new Dictionary<string, string>(StringComparer.Ordinal);
if (versionStartIncluding is not null)
{
vendorExtensions["versionStartIncluding"] = versionStartIncluding;
}
if (versionStartExcluding is not null)
{
vendorExtensions["versionStartExcluding"] = versionStartExcluding;
}
if (versionEndIncluding is not null)
{
vendorExtensions["versionEndIncluding"] = versionEndIncluding;
}
if (versionEndExcluding is not null)
{
vendorExtensions["versionEndExcluding"] = versionEndExcluding;
}
if (version is not null)
{
vendorExtensions["version"] = version;
}
string? introduced = null;
string? fixedVersion = null;
string? lastAffected = null;
string? exactVersion = null;
var expressionParts = new List<string>();
var introducedInclusive = true;
var fixedInclusive = false;
var lastInclusive = true;
if (versionStartIncluding is not null)
{
introduced = versionStartIncluding;
introducedInclusive = true;
expressionParts.Add($">={versionStartIncluding}");
}
if (versionStartExcluding is not null)
{
if (introduced is null)
{
introduced = versionStartExcluding;
introducedInclusive = false;
}
expressionParts.Add($">{versionStartExcluding}");
}
if (versionEndExcluding is not null)
{
fixedVersion = versionEndExcluding;
fixedInclusive = false;
expressionParts.Add($"<{versionEndExcluding}");
}
if (versionEndIncluding is not null)
{
lastAffected = versionEndIncluding;
lastInclusive = true;
expressionParts.Add($"<={versionEndIncluding}");
}
if (version is not null)
{
introduced = version;
introducedInclusive = true;
lastAffected = version;
lastInclusive = true;
exactVersion = version;
expressionParts.Add($"=={version}");
}
if (introduced is null && fixedVersion is null && lastAffected is null && vendorExtensions.Count == 0)
{
return null;
}
var rangeExpression = expressionParts.Count > 0 ? string.Join(' ', expressionParts) : null;
IReadOnlyDictionary<string, string>? extensions = vendorExtensions.Count == 0 ? null : vendorExtensions;
SemVerPrimitive? semVerPrimitive = null;
if (TryBuildSemVerPrimitive(
introduced,
introducedInclusive,
fixedVersion,
fixedInclusive,
lastAffected,
lastInclusive,
exactVersion,
rangeExpression,
out var primitive))
{
semVerPrimitive = primitive;
}
var primitives = semVerPrimitive is null && extensions is null
? null
: new RangePrimitives(semVerPrimitive, null, null, extensions);
var provenanceValue = provenance.Value ?? criteria;
var rangeProvenance = new AdvisoryProvenance(
provenance.Source,
provenance.Kind,
provenanceValue,
provenance.RecordedAt,
new[] { ProvenanceFieldMasks.VersionRanges });
return new AffectedVersionRange(
rangeKind: "cpe",
introducedVersion: introduced,
fixedVersion: fixedVersion,
lastAffectedVersion: lastAffected,
rangeExpression: rangeExpression,
provenance: rangeProvenance,
primitives);
}
private static bool TryBuildSemVerPrimitive(
string? introduced,
bool introducedInclusive,
string? fixedVersion,
bool fixedInclusive,
string? lastAffected,
bool lastInclusive,
string? exactVersion,
string? constraintExpression,
out SemVerPrimitive? primitive)
{
primitive = null;
if (!TryNormalizeSemVer(introduced, out var normalizedIntroduced)
|| !TryNormalizeSemVer(fixedVersion, out var normalizedFixed)
|| !TryNormalizeSemVer(lastAffected, out var normalizedLast)
|| !TryNormalizeSemVer(exactVersion, out var normalizedExact))
{
return false;
}
if (normalizedIntroduced is null && normalizedFixed is null && normalizedLast is null && normalizedExact is null)
{
return false;
}
primitive = new SemVerPrimitive(
Introduced: normalizedIntroduced,
IntroducedInclusive: normalizedIntroduced is null ? true : introducedInclusive,
Fixed: normalizedFixed,
FixedInclusive: normalizedFixed is null ? false : fixedInclusive,
LastAffected: normalizedLast,
LastAffectedInclusive: normalizedLast is null ? false : lastInclusive,
ConstraintExpression: constraintExpression,
ExactValue: normalizedExact);
return true;
}
private static bool TryNormalizeSemVer(string? value, out string? normalized)
{
normalized = null;
if (string.IsNullOrWhiteSpace(value))
{
return true;
}
var trimmed = value.Trim();
if (trimmed.StartsWith("v", StringComparison.OrdinalIgnoreCase) && trimmed.Length > 1)
{
trimmed = trimmed[1..];
}
if (!NuGetVersion.TryParse(trimmed, out var parsed))
{
return false;
}
normalized = parsed.ToNormalizedString();
return true;
}
private static string? BuildCweUrl(string cweId)
{
var dashIndex = cweId.IndexOf('-');
if (dashIndex < 0 || dashIndex == cweId.Length - 1)
{
return null;
}
var digits = new StringBuilder();
for (var i = dashIndex + 1; i < cweId.Length; i++)
{
var ch = cweId[i];
if (char.IsDigit(ch))
{
digits.Append(ch);
}
}
return digits.Length == 0 ? null : $"https://cwe.mitre.org/data/definitions/{digits}.html";
}
private static string? TryExtractVersionFromCriteria(string criteria)
{
if (string.IsNullOrWhiteSpace(criteria))
{
return null;
}
var segments = criteria.Split(':');
if (segments.Length < 6)
{
return null;
}
var version = segments[5];
if (string.IsNullOrWhiteSpace(version))
{
return null;
}
if (string.Equals(version, "*", StringComparison.Ordinal) || string.Equals(version, "-", StringComparison.Ordinal))
{
return null;
}
return version;
}
private readonly record struct WeaknessMetadata(string CweId, string? Name);
private sealed class PackageAccumulator
{
public List<AffectedVersionRange> Ranges { get; } = new();
public List<AdvisoryProvenance> Provenance { get; } = new();
}
}

View File

@@ -0,0 +1,25 @@
using System.IO;
using System.Reflection;
using System.Threading;
using Json.Schema;
namespace StellaOps.Concelier.Connector.Nvd.Internal;
internal static class NvdSchemaProvider
{
private static readonly Lazy<JsonSchema> Cached = new(LoadSchema, LazyThreadSafetyMode.ExecutionAndPublication);
public static JsonSchema Schema => Cached.Value;
private static JsonSchema LoadSchema()
{
var assembly = typeof(NvdSchemaProvider).GetTypeInfo().Assembly;
const string resourceName = "StellaOps.Concelier.Connector.Nvd.Schemas.nvd-vulnerability.schema.json";
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Embedded schema '{resourceName}' not found.");
using var reader = new StreamReader(stream);
var schemaText = reader.ReadToEnd();
return JsonSchema.FromText(schemaText);
}
}