Initial commit (history squashed)
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
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
This commit is contained in:
313
src/StellaOps.Feedser.Source.Distro.Suse/Internal/SuseMapper.cs
Normal file
313
src/StellaOps.Feedser.Source.Distro.Suse/Internal/SuseMapper.cs
Normal file
@@ -0,0 +1,313 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user