Files
git.stella-ops.org/src/StellaOps.Feedser.Source.Distro.Suse/Internal/SuseMapper.cs
master b97fc7685a
Some checks failed
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Initial commit (history squashed)
2025-10-11 23:28:35 +03:00

314 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Distro;
using StellaOps.Feedser.Storage.Mongo.Documents;
namespace StellaOps.Feedser.Source.Distro.Suse.Internal;
internal static class SuseMapper
{
public static Advisory Map(SuseAdvisoryDto dto, DocumentRecord document, DateTimeOffset recordedAt)
{
ArgumentNullException.ThrowIfNull(dto);
ArgumentNullException.ThrowIfNull(document);
var aliases = BuildAliases(dto);
var references = BuildReferences(dto, recordedAt);
var packages = BuildPackages(dto, recordedAt);
var fetchProvenance = new AdvisoryProvenance(
SuseConnectorPlugin.SourceName,
"document",
document.Uri,
document.FetchedAt.ToUniversalTime());
var mapProvenance = new AdvisoryProvenance(
SuseConnectorPlugin.SourceName,
"mapping",
dto.AdvisoryId,
recordedAt);
var published = dto.Published;
var modified = DateTimeOffset.Compare(recordedAt, dto.Published) >= 0 ? recordedAt : dto.Published;
return new Advisory(
advisoryKey: dto.AdvisoryId,
title: dto.Title ?? dto.AdvisoryId,
summary: dto.Summary,
language: "en",
published: published,
modified: modified,
severity: null,
exploitKnown: false,
aliases: aliases,
references: references,
affectedPackages: packages,
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { fetchProvenance, mapProvenance });
}
private static string[] BuildAliases(SuseAdvisoryDto dto)
{
var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
dto.AdvisoryId
};
foreach (var cve in dto.CveIds ?? Array.Empty<string>())
{
if (!string.IsNullOrWhiteSpace(cve))
{
aliases.Add(cve.Trim());
}
}
return aliases.OrderBy(static alias => alias, StringComparer.OrdinalIgnoreCase).ToArray();
}
private static AdvisoryReference[] BuildReferences(SuseAdvisoryDto dto, DateTimeOffset recordedAt)
{
if (dto.References is null || dto.References.Count == 0)
{
return Array.Empty<AdvisoryReference>();
}
var references = new List<AdvisoryReference>(dto.References.Count);
foreach (var reference in dto.References)
{
if (string.IsNullOrWhiteSpace(reference.Url))
{
continue;
}
try
{
var provenance = new AdvisoryProvenance(
SuseConnectorPlugin.SourceName,
"reference",
reference.Url,
recordedAt);
references.Add(new AdvisoryReference(
reference.Url.Trim(),
NormalizeReferenceKind(reference.Kind),
reference.Kind,
reference.Title,
provenance));
}
catch (ArgumentException)
{
// Ignore malformed URLs to keep advisory mapping resilient.
}
}
return references.Count == 0
? Array.Empty<AdvisoryReference>()
: references
.OrderBy(static reference => reference.Url, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static string? NormalizeReferenceKind(string? kind)
{
if (string.IsNullOrWhiteSpace(kind))
{
return null;
}
return kind.Trim().ToLowerInvariant() switch
{
"cve" => "cve",
"self" => "advisory",
"external" => "external",
_ => null,
};
}
private static IReadOnlyList<AffectedPackage> BuildPackages(SuseAdvisoryDto dto, DateTimeOffset recordedAt)
{
if (dto.Packages is null || dto.Packages.Count == 0)
{
return Array.Empty<AffectedPackage>();
}
var packages = new List<AffectedPackage>(dto.Packages.Count);
foreach (var package in dto.Packages)
{
if (string.IsNullOrWhiteSpace(package.CanonicalNevra))
{
continue;
}
Nevra? nevra;
if (!Nevra.TryParse(package.CanonicalNevra, out nevra))
{
continue;
}
var affectedProvenance = new AdvisoryProvenance(
SuseConnectorPlugin.SourceName,
"affected",
$"{package.Platform}:{package.CanonicalNevra}",
recordedAt);
var ranges = BuildVersionRanges(package, nevra!, recordedAt);
if (ranges.Count == 0 && string.Equals(package.Status, "not_affected", StringComparison.OrdinalIgnoreCase))
{
continue;
}
packages.Add(new AffectedPackage(
AffectedPackageTypes.Rpm,
identifier: nevra!.ToCanonicalString(),
platform: package.Platform,
versionRanges: ranges,
statuses: BuildStatuses(package, affectedProvenance),
provenance: new[] { affectedProvenance }));
}
return packages.Count == 0
? Array.Empty<AffectedPackage>()
: packages
.OrderBy(static pkg => pkg.Platform, StringComparer.OrdinalIgnoreCase)
.ThenBy(static pkg => pkg.Identifier, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static IReadOnlyList<AffectedPackageStatus> BuildStatuses(SusePackageStateDto package, AdvisoryProvenance provenance)
{
if (string.IsNullOrWhiteSpace(package.Status))
{
return Array.Empty<AffectedPackageStatus>();
}
return new[]
{
new AffectedPackageStatus(package.Status, provenance)
};
}
private static IReadOnlyList<AffectedVersionRange> BuildVersionRanges(SusePackageStateDto package, Nevra nevra, DateTimeOffset recordedAt)
{
var introducedComponent = ParseNevraComponent(package.IntroducedVersion, nevra);
var fixedComponent = ParseNevraComponent(package.FixedVersion, nevra);
var lastAffectedComponent = ParseNevraComponent(package.LastAffectedVersion, nevra);
if (introducedComponent is null && fixedComponent is null && lastAffectedComponent is null)
{
return Array.Empty<AffectedVersionRange>();
}
var rangeProvenance = new AdvisoryProvenance(
SuseConnectorPlugin.SourceName,
"range",
$"{package.Platform}:{nevra.ToCanonicalString()}",
recordedAt);
var extensions = new Dictionary<string, string>(StringComparer.Ordinal)
{
["suse.status"] = package.Status
};
var rangeExpression = BuildRangeExpression(package.IntroducedVersion, package.FixedVersion, package.LastAffectedVersion);
var range = new AffectedVersionRange(
rangeKind: "nevra",
introducedVersion: package.IntroducedVersion,
fixedVersion: package.FixedVersion,
lastAffectedVersion: package.LastAffectedVersion,
rangeExpression: rangeExpression,
provenance: rangeProvenance,
primitives: new RangePrimitives(
SemVer: null,
Nevra: new NevraPrimitive(introducedComponent, fixedComponent, lastAffectedComponent),
Evr: null,
VendorExtensions: extensions));
return new[] { range };
}
private static NevraComponent? ParseNevraComponent(string? version, Nevra nevra)
{
if (string.IsNullOrWhiteSpace(version))
{
return null;
}
if (!TrySplitNevraVersion(version.Trim(), out var epoch, out var ver, out var rel))
{
return null;
}
return new NevraComponent(
nevra.Name,
epoch,
ver,
rel,
string.IsNullOrWhiteSpace(nevra.Architecture) ? null : nevra.Architecture);
}
private static bool TrySplitNevraVersion(string value, out int epoch, out string version, out string release)
{
epoch = 0;
version = string.Empty;
release = string.Empty;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
var trimmed = value.Trim();
var dashIndex = trimmed.LastIndexOf('-');
if (dashIndex <= 0 || dashIndex >= trimmed.Length - 1)
{
return false;
}
release = trimmed[(dashIndex + 1)..];
var versionSegment = trimmed[..dashIndex];
var epochIndex = versionSegment.IndexOf(':');
if (epochIndex >= 0)
{
var epochPart = versionSegment[..epochIndex];
version = epochIndex < versionSegment.Length - 1 ? versionSegment[(epochIndex + 1)..] : string.Empty;
if (epochPart.Length > 0 && !int.TryParse(epochPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out epoch))
{
epoch = 0;
return false;
}
}
else
{
version = versionSegment;
}
return !string.IsNullOrWhiteSpace(version) && !string.IsNullOrWhiteSpace(release);
}
private static string? BuildRangeExpression(string? introduced, string? fixedVersion, string? lastAffected)
{
var parts = new List<string>(3);
if (!string.IsNullOrWhiteSpace(introduced))
{
parts.Add($"introduced:{introduced}");
}
if (!string.IsNullOrWhiteSpace(fixedVersion))
{
parts.Add($"fixed:{fixedVersion}");
}
if (!string.IsNullOrWhiteSpace(lastAffected))
{
parts.Add($"last:{lastAffected}");
}
return parts.Count == 0 ? null : string.Join(" ", parts);
}
}