Files
git.stella-ops.org/src/StellaOps.Concelier.Connector.Vndr.Apple/Internal/AppleMapper.cs

282 lines
8.7 KiB
C#

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<CvssMetric>(),
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<string> BuildAliases(AppleDetailDto dto)
{
var set = new HashSet<string>(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<AdvisoryReference> BuildReferences(AppleDetailDto dto, DateTimeOffset recordedAt)
{
if (dto.References.Count == 0)
{
return Array.Empty<AdvisoryReference>();
}
var list = new List<AdvisoryReference>(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<AdvisoryReference>();
}
list.Sort(static (left, right) => StringComparer.OrdinalIgnoreCase.Compare(left.Url, right.Url));
return list;
}
private static IReadOnlyList<AffectedPackage> BuildAffected(AppleDetailDto dto, DateTimeOffset recordedAt)
{
if (dto.Affected.Count == 0)
{
return Array.Empty<AffectedPackage>();
}
var packages = new List<AffectedPackage>(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<AffectedPackageStatus>(),
provenance: provenance,
normalizedVersions: normalizedVersions));
}
return packages.Count == 0 ? Array.Empty<AffectedPackage>() : packages;
}
private static IReadOnlyList<AffectedVersionRange> BuildRanges(AppleAffectedProductDto product, DateTimeOffset recordedAt)
{
if (string.IsNullOrWhiteSpace(product.Version) && string.IsNullOrWhiteSpace(product.Build))
{
return Array.Empty<AffectedVersionRange>();
}
var provenance = new AdvisoryProvenance(
VndrAppleConnectorPlugin.SourceName,
"range",
product.Name,
recordedAt);
var extensions = new Dictionary<string, string>(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<NormalizedVersionRule> BuildNormalizedVersions(
AppleAffectedProductDto product,
IReadOnlyList<AffectedVersionRange> ranges)
{
if (ranges.Count == 0)
{
return Array.Empty<NormalizedVersionRule>();
}
var segments = new List<string>();
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<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.ToArray();
}
}