using System; using System.Collections.Generic; using System.Linq; using StellaOps.Concelier.Models; using StellaOps.Concelier.Connector.Common; using StellaOps.Concelier.Connector.Common.Packages; using StellaOps.Concelier.Storage.Mongo.Documents; using StellaOps.Concelier.Storage.Mongo.Dtos; using StellaOps.Concelier.Storage.Mongo.PsirtFlags; namespace StellaOps.Concelier.Connector.Vndr.Apple.Internal; internal static class AppleMapper { public static (Advisory Advisory, PsirtFlagRecord? Flag) Map( AppleDetailDto dto, DocumentRecord document, DtoRecord dtoRecord) { ArgumentNullException.ThrowIfNull(dto); ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(dtoRecord); var recordedAt = dtoRecord.ValidatedAt.ToUniversalTime(); var fetchProvenance = new AdvisoryProvenance( VndrAppleConnectorPlugin.SourceName, "document", document.Uri, document.FetchedAt.ToUniversalTime()); var mapProvenance = new AdvisoryProvenance( VndrAppleConnectorPlugin.SourceName, "map", dto.AdvisoryId, recordedAt); var aliases = BuildAliases(dto); var references = BuildReferences(dto, recordedAt); var affected = BuildAffected(dto, recordedAt); var advisory = new Advisory( advisoryKey: dto.AdvisoryId, title: dto.Title, summary: dto.Summary, language: "en", published: dto.Published.ToUniversalTime(), modified: dto.Updated?.ToUniversalTime(), severity: null, exploitKnown: false, aliases: aliases, references: references, affectedPackages: affected, cvssMetrics: Array.Empty(), provenance: new[] { fetchProvenance, mapProvenance }); PsirtFlagRecord? flag = dto.RapidSecurityResponse ? new PsirtFlagRecord(dto.AdvisoryId, "Apple", VndrAppleConnectorPlugin.SourceName, dto.ArticleId, recordedAt) : null; return (advisory, flag); } private static IReadOnlyList BuildAliases(AppleDetailDto dto) { var set = new HashSet(StringComparer.OrdinalIgnoreCase) { dto.AdvisoryId, dto.ArticleId, }; foreach (var cve in dto.CveIds) { if (!string.IsNullOrWhiteSpace(cve)) { set.Add(cve.Trim()); } } var aliases = set.ToList(); aliases.Sort(StringComparer.OrdinalIgnoreCase); return aliases; } private static IReadOnlyList BuildReferences(AppleDetailDto dto, DateTimeOffset recordedAt) { if (dto.References.Count == 0) { return Array.Empty(); } var list = new List(dto.References.Count); foreach (var reference in dto.References) { if (string.IsNullOrWhiteSpace(reference.Url)) { continue; } try { var provenance = new AdvisoryProvenance( VndrAppleConnectorPlugin.SourceName, "reference", reference.Url, recordedAt); list.Add(new AdvisoryReference( url: reference.Url, kind: reference.Kind, sourceTag: null, summary: reference.Title, provenance: provenance)); } catch (ArgumentException) { // ignore invalid URLs } } if (list.Count == 0) { return Array.Empty(); } list.Sort(static (left, right) => StringComparer.OrdinalIgnoreCase.Compare(left.Url, right.Url)); return list; } private static IReadOnlyList BuildAffected(AppleDetailDto dto, DateTimeOffset recordedAt) { if (dto.Affected.Count == 0) { return Array.Empty(); } var packages = new List(dto.Affected.Count); foreach (var product in dto.Affected) { if (string.IsNullOrWhiteSpace(product.Name)) { continue; } var provenance = new[] { new AdvisoryProvenance( VndrAppleConnectorPlugin.SourceName, "affected", product.Name, recordedAt), }; var ranges = BuildRanges(product, recordedAt); var normalizedVersions = BuildNormalizedVersions(product, ranges); packages.Add(new AffectedPackage( type: AffectedPackageTypes.Vendor, identifier: product.Name, platform: product.Platform, versionRanges: ranges, statuses: Array.Empty(), provenance: provenance, normalizedVersions: normalizedVersions)); } return packages.Count == 0 ? Array.Empty() : packages; } private static IReadOnlyList BuildRanges(AppleAffectedProductDto product, DateTimeOffset recordedAt) { if (string.IsNullOrWhiteSpace(product.Version) && string.IsNullOrWhiteSpace(product.Build)) { return Array.Empty(); } var provenance = new AdvisoryProvenance( VndrAppleConnectorPlugin.SourceName, "range", product.Name, recordedAt); var extensions = new Dictionary(StringComparer.Ordinal); if (!string.IsNullOrWhiteSpace(product.Version)) { extensions["apple.version.raw"] = product.Version; } if (!string.IsNullOrWhiteSpace(product.Build)) { extensions["apple.build"] = product.Build; } if (!string.IsNullOrWhiteSpace(product.Platform)) { extensions["apple.platform"] = product.Platform; } var primitives = extensions.Count == 0 ? null : new RangePrimitives( SemVer: TryCreateSemVerPrimitive(product.Version), Nevra: null, Evr: null, VendorExtensions: extensions); var sanitizedVersion = PackageCoordinateHelper.TryParseSemVer(product.Version, out _, out var normalizedVersion) ? normalizedVersion : product.Version; return new[] { new AffectedVersionRange( rangeKind: "vendor", introducedVersion: null, fixedVersion: sanitizedVersion, lastAffectedVersion: null, rangeExpression: product.Version, provenance: provenance, primitives: primitives), }; } private static SemVerPrimitive? TryCreateSemVerPrimitive(string? version) { if (string.IsNullOrWhiteSpace(version)) { return null; } if (!PackageCoordinateHelper.TryParseSemVer(version, out _, out var normalized)) { return null; } // treat as fixed version, unknown introduced/last affected return new SemVerPrimitive( Introduced: null, IntroducedInclusive: true, Fixed: normalized, FixedInclusive: true, LastAffected: null, LastAffectedInclusive: true, ConstraintExpression: null); } private static IReadOnlyList BuildNormalizedVersions( AppleAffectedProductDto product, IReadOnlyList ranges) { if (ranges.Count == 0) { return Array.Empty(); } var segments = new List(); if (!string.IsNullOrWhiteSpace(product.Platform)) { segments.Add(product.Platform.Trim()); } if (!string.IsNullOrWhiteSpace(product.Name)) { segments.Add(product.Name.Trim()); } var note = segments.Count == 0 ? null : $"apple:{string.Join(':', segments)}"; var rules = new List(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() : rules.ToArray(); } }