This commit is contained in:
Vladimir Moushkov
2025-10-15 10:03:56 +03:00
parent ea8226120c
commit ea1106ce7c
276 changed files with 21674 additions and 934 deletions

View File

@@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Cvss;
using StellaOps.Feedser.Normalization.SemVer;
using StellaOps.Feedser.Storage.Mongo.Documents;
namespace StellaOps.Feedser.Source.Ru.Nkcki.Internal;
@@ -80,56 +81,56 @@ internal static class RuNkckiMapper
private static IReadOnlyList<AdvisoryReference> BuildReferences(RuNkckiVulnerabilityDto dto, DocumentRecord document, DateTimeOffset recordedAt)
{
var references = new List<AdvisoryReference>
var references = new List<AdvisoryReference>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void AddReference(string? url, string kind, string? sourceTag, string? summary)
{
new(document.Uri, "details", "ru-nkcki", summary: null, new AdvisoryProvenance(
if (string.IsNullOrWhiteSpace(url))
{
return;
}
var key = $"{kind}|{url}";
if (!seen.Add(key))
{
return;
}
var provenance = new AdvisoryProvenance(
RuNkckiConnectorPlugin.SourceName,
"reference",
document.Uri,
url,
recordedAt,
new[] { ProvenanceFieldMasks.References }))
};
new[] { ProvenanceFieldMasks.References });
references.Add(new AdvisoryReference(url, kind, sourceTag, summary, provenance));
}
AddReference(document.Uri, "details", "ru-nkcki", null);
if (!string.IsNullOrWhiteSpace(dto.FstecId))
{
var slug = dto.FstecId!.Contains(':', StringComparison.Ordinal)
? dto.FstecId[(dto.FstecId.IndexOf(':') + 1)..]
: dto.FstecId;
var bduUrl = $"https://bdu.fstec.ru/vul/{slug}";
references.Add(new AdvisoryReference(bduUrl, "details", "bdu", summary: null, new AdvisoryProvenance(
RuNkckiConnectorPlugin.SourceName,
"reference",
bduUrl,
recordedAt,
new[] { ProvenanceFieldMasks.References })));
AddReference($"https://bdu.fstec.ru/vul/{slug}", "details", "bdu", null);
}
foreach (var url in dto.Urls)
{
if (string.IsNullOrWhiteSpace(url))
{
continue;
}
var kind = url.Contains("cert.gov.ru", StringComparison.OrdinalIgnoreCase) ? "details" : "external";
var sourceTag = url.Contains("siemens", StringComparison.OrdinalIgnoreCase) ? "vendor" : null;
references.Add(new AdvisoryReference(url, kind, sourceTag, summary: null, new AdvisoryProvenance(
RuNkckiConnectorPlugin.SourceName,
"reference",
url,
recordedAt,
new[] { ProvenanceFieldMasks.References })));
AddReference(url, kind, sourceTag, null);
}
if (dto.Cwe?.Number is int number)
{
var url = $"https://cwe.mitre.org/data/definitions/{number}.html";
references.Add(new AdvisoryReference(url, "cwe", "cwe", dto.Cwe.Description, new AdvisoryProvenance(
RuNkckiConnectorPlugin.SourceName,
"reference",
url,
recordedAt,
new[] { ProvenanceFieldMasks.References })));
AddReference(
$"https://cwe.mitre.org/data/definitions/{number}.html",
"cwe",
"cwe",
dto.Cwe.Description);
}
return references;
@@ -137,43 +138,68 @@ internal static class RuNkckiMapper
private static IReadOnlyList<AffectedPackage> BuildPackages(RuNkckiVulnerabilityDto dto, DateTimeOffset recordedAt)
{
if (string.IsNullOrWhiteSpace(dto.VulnerableSoftwareText))
if (!dto.VulnerableSoftwareEntries.IsDefaultOrEmpty && dto.VulnerableSoftwareEntries.Length > 0)
{
return Array.Empty<AffectedPackage>();
return CreatePackages(dto.VulnerableSoftwareEntries, dto, recordedAt);
}
var identifier = dto.VulnerableSoftwareText!.Replace('\n', ' ').Replace('\r', ' ').Trim();
if (identifier.Length == 0)
if (!string.IsNullOrWhiteSpace(dto.VulnerableSoftwareText))
{
return Array.Empty<AffectedPackage>();
var fallbackEntry = new RuNkckiSoftwareEntry(
dto.VulnerableSoftwareText!,
dto.VulnerableSoftwareText!,
ImmutableArray<string>.Empty);
return CreatePackages(new[] { fallbackEntry }, dto, recordedAt);
}
var packageProvenance = new AdvisoryProvenance(
RuNkckiConnectorPlugin.SourceName,
"package",
identifier,
recordedAt,
new[] { ProvenanceFieldMasks.AffectedPackages });
return Array.Empty<AffectedPackage>();
}
var status = new AffectedPackageStatus(
dto.PatchAvailable == true ? AffectedPackageStatusCatalog.Fixed : AffectedPackageStatusCatalog.Affected,
new AdvisoryProvenance(
private static IReadOnlyList<AffectedPackage> CreatePackages(IEnumerable<RuNkckiSoftwareEntry> entries, RuNkckiVulnerabilityDto dto, DateTimeOffset recordedAt)
{
var type = DeterminePackageType(dto);
var platform = dto.ProductCategories.IsDefaultOrEmpty || dto.ProductCategories.Length == 0
? null
: string.Join(", ", dto.ProductCategories);
var packages = new List<AffectedPackage>();
foreach (var entry in entries)
{
if (string.IsNullOrWhiteSpace(entry.Identifier))
{
continue;
}
var packageProvenance = new AdvisoryProvenance(
RuNkckiConnectorPlugin.SourceName,
"package-status",
dto.PatchAvailable == true ? "patch_available" : "affected",
"package",
entry.Evidence,
recordedAt,
new[] { ProvenanceFieldMasks.PackageStatuses }));
new[] { ProvenanceFieldMasks.AffectedPackages });
return new[]
{
new AffectedPackage(
dto.VulnerableSoftwareHasCpe == true ? AffectedPackageTypes.Cpe : AffectedPackageTypes.Vendor,
identifier,
platform: null,
versionRanges: null,
statuses: new[] { status },
provenance: new[] { packageProvenance })
};
var status = new AffectedPackageStatus(
dto.PatchAvailable == true ? AffectedPackageStatusCatalog.Fixed : AffectedPackageStatusCatalog.Affected,
new AdvisoryProvenance(
RuNkckiConnectorPlugin.SourceName,
"package-status",
dto.PatchAvailable == true ? "patch_available" : "affected",
recordedAt,
new[] { ProvenanceFieldMasks.PackageStatuses }));
var rangeMetadata = BuildRangeMetadata(entry, recordedAt);
packages.Add(new AffectedPackage(
type,
entry.Identifier,
platform,
rangeMetadata.Ranges,
new[] { status },
new[] { packageProvenance },
rangeMetadata.Normalized));
}
return packages;
}
private static IReadOnlyList<CvssMetric> BuildCvssMetrics(RuNkckiVulnerabilityDto dto, DateTimeOffset recordedAt, out string? severity)
@@ -194,6 +220,27 @@ internal static class RuNkckiMapper
severity ??= metric.BaseSeverity;
}
if (!string.IsNullOrWhiteSpace(dto.CvssVectorV4))
{
var vector = dto.CvssVectorV4.StartsWith("CVSS:", StringComparison.OrdinalIgnoreCase)
? dto.CvssVectorV4
: $"CVSS:4.0/{dto.CvssVectorV4}";
var score = dto.CvssScoreV4.HasValue
? Math.Round(dto.CvssScoreV4.Value, 1, MidpointRounding.AwayFromZero)
: 0.0;
var severityV4 = DetermineCvss4Severity(score);
var provenance = new AdvisoryProvenance(
RuNkckiConnectorPlugin.SourceName,
"cvss",
vector,
recordedAt,
new[] { ProvenanceFieldMasks.CvssMetrics });
metrics.Add(new CvssMetric("4.0", vector, score, severityV4, provenance));
severity ??= severityV4;
}
return metrics;
}
@@ -295,4 +342,104 @@ internal static class RuNkckiMapper
return false;
}
private static string DeterminePackageType(RuNkckiVulnerabilityDto dto)
{
if (dto.VulnerableSoftwareHasCpe == true)
{
return AffectedPackageTypes.Cpe;
}
if (!dto.ProductCategories.IsDefault && dto.ProductCategories.Any(static category =>
category.Contains("ics", StringComparison.OrdinalIgnoreCase)
|| category.Contains("scada", StringComparison.OrdinalIgnoreCase)))
{
return AffectedPackageTypes.IcsVendor;
}
return AffectedPackageTypes.Vendor;
}
private static (IReadOnlyList<AffectedVersionRange> Ranges, IReadOnlyList<NormalizedVersionRule> Normalized) BuildRangeMetadata(
RuNkckiSoftwareEntry entry,
DateTimeOffset recordedAt)
{
if (entry.RangeExpressions.IsDefaultOrEmpty || entry.RangeExpressions.Length == 0)
{
return (Array.Empty<AffectedVersionRange>(), Array.Empty<NormalizedVersionRule>());
}
var ranges = new List<AffectedVersionRange>();
var normalized = new List<NormalizedVersionRule>();
var dedupe = new HashSet<string>(StringComparer.Ordinal);
foreach (var expression in entry.RangeExpressions)
{
if (string.IsNullOrWhiteSpace(expression))
{
continue;
}
var results = SemVerRangeRuleBuilder.Build(expression, provenanceNote: entry.Evidence);
if (results.Count == 0)
{
continue;
}
foreach (var result in results)
{
var key = $"{result.Primitive.Introduced}|{result.Primitive.Fixed}|{result.Primitive.LastAffected}|{result.Expression}";
if (!dedupe.Add(key))
{
continue;
}
var provenance = new AdvisoryProvenance(
RuNkckiConnectorPlugin.SourceName,
"package-range",
entry.Evidence,
recordedAt,
new[] { ProvenanceFieldMasks.VersionRanges });
ranges.Add(new AffectedVersionRange(
NormalizedVersionSchemes.SemVer,
result.Primitive.Introduced,
result.Primitive.Fixed,
result.Primitive.LastAffected,
result.Expression,
provenance,
new RangePrimitives(result.Primitive, null, null, null)));
normalized.Add(result.NormalizedRule);
}
}
return (
ranges.Count == 0 ? Array.Empty<AffectedVersionRange>() : ranges,
normalized.Count == 0 ? Array.Empty<NormalizedVersionRule>() : normalized);
}
private static string DetermineCvss4Severity(double score)
{
if (score <= 0.0)
{
return "none";
}
if (score < 4.0)
{
return "low";
}
if (score < 7.0)
{
return "medium";
}
if (score < 9.0)
{
return "high";
}
return "critical";
}
}