Rename Concelier Source modules to Connector
This commit is contained in:
		@@ -0,0 +1,342 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using StellaOps.Concelier.Models;
 | 
			
		||||
using StellaOps.Concelier.Normalization.Distro;
 | 
			
		||||
using StellaOps.Concelier.Storage.Mongo.Documents;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Concelier.Connector.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;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var normalizedVersions = BuildNormalizedVersions(package, ranges);
 | 
			
		||||
 | 
			
		||||
            packages.Add(new AffectedPackage(
 | 
			
		||||
                AffectedPackageTypes.Rpm,
 | 
			
		||||
                identifier: nevra!.ToCanonicalString(),
 | 
			
		||||
                platform: package.Platform,
 | 
			
		||||
                versionRanges: ranges,
 | 
			
		||||
                statuses: BuildStatuses(package, affectedProvenance),
 | 
			
		||||
                provenance: new[] { affectedProvenance },
 | 
			
		||||
                normalizedVersions: normalizedVersions));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
 | 
			
		||||
        SusePackageStateDto package,
 | 
			
		||||
        IReadOnlyList<AffectedVersionRange> ranges)
 | 
			
		||||
    {
 | 
			
		||||
        if (ranges.Count == 0)
 | 
			
		||||
        {
 | 
			
		||||
            return Array.Empty<NormalizedVersionRule>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var note = string.IsNullOrWhiteSpace(package.Platform)
 | 
			
		||||
            ? null
 | 
			
		||||
            : $"suse:{package.Platform.Trim()}";
 | 
			
		||||
 | 
			
		||||
        var rules = new List<NormalizedVersionRule>(ranges.Count);
 | 
			
		||||
        foreach (var range in ranges)
 | 
			
		||||
        {
 | 
			
		||||
            var rule = range.ToNormalizedVersionRule(note);
 | 
			
		||||
            if (rule is not null)
 | 
			
		||||
            {
 | 
			
		||||
                rules.Add(rule);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return rules.Count == 0 ? Array.Empty<NormalizedVersionRule>() : rules;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user