Rename Concelier Source modules to Connector
This commit is contained in:
447
src/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaMapper.cs
Normal file
447
src/StellaOps.Concelier.Connector.Ghsa/Internal/GhsaMapper.cs
Normal file
@@ -0,0 +1,447 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Normalization.Cvss;
|
||||
using StellaOps.Concelier.Normalization.SemVer;
|
||||
using StellaOps.Concelier.Storage.Mongo.Documents;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Ghsa.Internal;
|
||||
|
||||
internal static class GhsaMapper
|
||||
{
|
||||
private static readonly HashSet<string> SemVerEcosystems = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"npm",
|
||||
"maven",
|
||||
"pip",
|
||||
"rubygems",
|
||||
"composer",
|
||||
"nuget",
|
||||
"go",
|
||||
"cargo",
|
||||
};
|
||||
|
||||
public static Advisory Map(GhsaRecordDto dto, DocumentRecord document, DateTimeOffset recordedAt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dto);
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
|
||||
var fetchProvenance = new AdvisoryProvenance(
|
||||
GhsaConnectorPlugin.SourceName,
|
||||
"document",
|
||||
document.Uri,
|
||||
document.FetchedAt,
|
||||
new[] { ProvenanceFieldMasks.Advisory });
|
||||
var mapProvenance = new AdvisoryProvenance(
|
||||
GhsaConnectorPlugin.SourceName,
|
||||
"mapping",
|
||||
dto.GhsaId,
|
||||
recordedAt,
|
||||
new[] { ProvenanceFieldMasks.Advisory });
|
||||
|
||||
var aliases = dto.Aliases
|
||||
.Where(static alias => !string.IsNullOrWhiteSpace(alias))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var references = dto.References
|
||||
.Select(reference => CreateReference(reference, recordedAt))
|
||||
.Where(static reference => reference is not null)
|
||||
.Cast<AdvisoryReference>()
|
||||
.ToList();
|
||||
|
||||
var affected = CreateAffectedPackages(dto, recordedAt);
|
||||
var credits = CreateCredits(dto.Credits, recordedAt);
|
||||
var weaknesses = CreateWeaknesses(dto.Cwes, recordedAt);
|
||||
var cvssMetrics = CreateCvssMetrics(dto.Cvss, recordedAt, out var cvssSeverity, out var canonicalMetricId);
|
||||
|
||||
var severityHint = SeverityNormalization.Normalize(dto.Severity);
|
||||
var cvssSeverityHint = SeverityNormalization.Normalize(dto.Cvss?.Severity);
|
||||
var severity = severityHint ?? cvssSeverity ?? cvssSeverityHint;
|
||||
|
||||
if (canonicalMetricId is null)
|
||||
{
|
||||
var fallbackSeverity = severityHint ?? cvssSeverityHint ?? cvssSeverity;
|
||||
if (!string.IsNullOrWhiteSpace(fallbackSeverity))
|
||||
{
|
||||
canonicalMetricId = BuildSeverityCanonicalMetricId(fallbackSeverity);
|
||||
}
|
||||
}
|
||||
|
||||
var summary = dto.Summary ?? dto.Description;
|
||||
var description = Validation.TrimToNull(dto.Description);
|
||||
|
||||
return new Advisory(
|
||||
advisoryKey: dto.GhsaId,
|
||||
title: dto.Summary ?? dto.GhsaId,
|
||||
summary: summary,
|
||||
language: "en",
|
||||
published: dto.PublishedAt,
|
||||
modified: dto.UpdatedAt ?? dto.PublishedAt,
|
||||
severity: severity,
|
||||
exploitKnown: false,
|
||||
aliases: aliases,
|
||||
credits: credits,
|
||||
references: references,
|
||||
affectedPackages: affected,
|
||||
cvssMetrics: cvssMetrics,
|
||||
provenance: new[] { fetchProvenance, mapProvenance },
|
||||
description: description,
|
||||
cwes: weaknesses,
|
||||
canonicalMetricId: canonicalMetricId);
|
||||
}
|
||||
|
||||
private static string BuildSeverityCanonicalMetricId(string severity)
|
||||
=> $"{GhsaConnectorPlugin.SourceName}:severity/{severity}";
|
||||
|
||||
private static AdvisoryReference? CreateReference(GhsaReferenceDto reference, DateTimeOffset recordedAt)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reference.Url) || !Validation.LooksLikeHttpUrl(reference.Url))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var kind = reference.Type?.ToLowerInvariant();
|
||||
|
||||
return new AdvisoryReference(
|
||||
reference.Url,
|
||||
kind,
|
||||
reference.Name,
|
||||
summary: null,
|
||||
provenance: new AdvisoryProvenance(
|
||||
GhsaConnectorPlugin.SourceName,
|
||||
"reference",
|
||||
reference.Url,
|
||||
recordedAt,
|
||||
new[] { ProvenanceFieldMasks.References }));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AffectedPackage> CreateAffectedPackages(GhsaRecordDto dto, DateTimeOffset recordedAt)
|
||||
{
|
||||
if (dto.Affected.Count == 0)
|
||||
{
|
||||
return Array.Empty<AffectedPackage>();
|
||||
}
|
||||
|
||||
var packages = new List<AffectedPackage>(dto.Affected.Count);
|
||||
foreach (var affected in dto.Affected)
|
||||
{
|
||||
var ecosystem = string.IsNullOrWhiteSpace(affected.Ecosystem) ? "unknown" : affected.Ecosystem.Trim();
|
||||
var packageName = string.IsNullOrWhiteSpace(affected.PackageName) ? "unknown-package" : affected.PackageName.Trim();
|
||||
var identifier = $"{ecosystem.ToLowerInvariant()}:{packageName}";
|
||||
|
||||
var provenance = new[]
|
||||
{
|
||||
new AdvisoryProvenance(
|
||||
GhsaConnectorPlugin.SourceName,
|
||||
"affected",
|
||||
identifier,
|
||||
recordedAt,
|
||||
new[] { ProvenanceFieldMasks.AffectedPackages }),
|
||||
};
|
||||
|
||||
var rangeKind = SemVerEcosystems.Contains(ecosystem) ? "semver" : "vendor";
|
||||
var packageType = SemVerEcosystems.Contains(ecosystem) ? AffectedPackageTypes.SemVer : AffectedPackageTypes.Vendor;
|
||||
|
||||
var (ranges, normalizedVersions) = SemVerEcosystems.Contains(ecosystem)
|
||||
? CreateSemVerVersionArtifacts(affected, identifier, ecosystem, packageName, recordedAt)
|
||||
: CreateVendorVersionArtifacts(affected, rangeKind, identifier, ecosystem, packageName, recordedAt);
|
||||
|
||||
var statuses = new[]
|
||||
{
|
||||
new AffectedPackageStatus(
|
||||
"affected",
|
||||
new AdvisoryProvenance(
|
||||
GhsaConnectorPlugin.SourceName,
|
||||
"affected-status",
|
||||
identifier,
|
||||
recordedAt,
|
||||
new[] { ProvenanceFieldMasks.PackageStatuses })),
|
||||
};
|
||||
|
||||
packages.Add(new AffectedPackage(
|
||||
packageType,
|
||||
identifier,
|
||||
platform: null,
|
||||
versionRanges: ranges,
|
||||
statuses: statuses,
|
||||
provenance: provenance,
|
||||
normalizedVersions: normalizedVersions));
|
||||
}
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AdvisoryCredit> CreateCredits(IReadOnlyList<GhsaCreditDto> credits, DateTimeOffset recordedAt)
|
||||
{
|
||||
if (credits.Count == 0)
|
||||
{
|
||||
return Array.Empty<AdvisoryCredit>();
|
||||
}
|
||||
|
||||
var results = new List<AdvisoryCredit>(credits.Count);
|
||||
foreach (var credit in credits)
|
||||
{
|
||||
var displayName = Validation.TrimToNull(credit.Name) ?? Validation.TrimToNull(credit.Login);
|
||||
if (displayName is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var contacts = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(credit.ProfileUrl) && Validation.LooksLikeHttpUrl(credit.ProfileUrl))
|
||||
{
|
||||
contacts.Add(credit.ProfileUrl.Trim());
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(credit.Login))
|
||||
{
|
||||
contacts.Add($"https://github.com/{credit.Login.Trim()}");
|
||||
}
|
||||
|
||||
var provenance = new AdvisoryProvenance(
|
||||
GhsaConnectorPlugin.SourceName,
|
||||
"credit",
|
||||
displayName,
|
||||
recordedAt,
|
||||
new[] { ProvenanceFieldMasks.Credits });
|
||||
|
||||
results.Add(new AdvisoryCredit(displayName, credit.Type, contacts, provenance));
|
||||
}
|
||||
|
||||
return results.Count == 0 ? Array.Empty<AdvisoryCredit>() : results;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AdvisoryWeakness> CreateWeaknesses(IReadOnlyList<GhsaWeaknessDto> cwes, DateTimeOffset recordedAt)
|
||||
{
|
||||
if (cwes.Count == 0)
|
||||
{
|
||||
return Array.Empty<AdvisoryWeakness>();
|
||||
}
|
||||
|
||||
var list = new List<AdvisoryWeakness>(cwes.Count);
|
||||
foreach (var cwe in cwes)
|
||||
{
|
||||
if (cwe is null || string.IsNullOrWhiteSpace(cwe.CweId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var identifier = cwe.CweId.Trim();
|
||||
var provenance = new AdvisoryProvenance(
|
||||
GhsaConnectorPlugin.SourceName,
|
||||
"weakness",
|
||||
identifier,
|
||||
recordedAt,
|
||||
new[] { ProvenanceFieldMasks.Weaknesses });
|
||||
|
||||
var provenanceArray = ImmutableArray.Create(provenance);
|
||||
list.Add(new AdvisoryWeakness(
|
||||
taxonomy: "cwe",
|
||||
identifier: identifier,
|
||||
name: Validation.TrimToNull(cwe.Name),
|
||||
uri: BuildCweUrl(identifier),
|
||||
provenance: provenanceArray));
|
||||
}
|
||||
|
||||
return list.Count == 0 ? Array.Empty<AdvisoryWeakness>() : list;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CvssMetric> CreateCvssMetrics(GhsaCvssDto? cvss, DateTimeOffset recordedAt, out string? severity, out string? canonicalMetricId)
|
||||
{
|
||||
severity = null;
|
||||
canonicalMetricId = null;
|
||||
|
||||
if (cvss is null)
|
||||
{
|
||||
return Array.Empty<CvssMetric>();
|
||||
}
|
||||
|
||||
var vector = Validation.TrimToNull(cvss.VectorString);
|
||||
if (!CvssMetricNormalizer.TryNormalize(null, vector, cvss.Score, cvss.Severity, out var normalized))
|
||||
{
|
||||
return Array.Empty<CvssMetric>();
|
||||
}
|
||||
|
||||
severity = normalized.BaseSeverity;
|
||||
canonicalMetricId = $"{normalized.Version}|{normalized.Vector}";
|
||||
|
||||
var provenance = new AdvisoryProvenance(
|
||||
GhsaConnectorPlugin.SourceName,
|
||||
"cvss",
|
||||
normalized.Vector,
|
||||
recordedAt,
|
||||
new[] { ProvenanceFieldMasks.CvssMetrics });
|
||||
|
||||
return new[]
|
||||
{
|
||||
normalized.ToModel(provenance),
|
||||
};
|
||||
}
|
||||
|
||||
private static string? BuildCweUrl(string? cweId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cweId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmed = cweId.Trim();
|
||||
var dashIndex = trimmed.IndexOf('-');
|
||||
if (dashIndex < 0 || dashIndex == trimmed.Length - 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var digits = new StringBuilder();
|
||||
for (var i = dashIndex + 1; i < trimmed.Length; i++)
|
||||
{
|
||||
var ch = trimmed[i];
|
||||
if (char.IsDigit(ch))
|
||||
{
|
||||
digits.Append(ch);
|
||||
}
|
||||
}
|
||||
|
||||
return digits.Length == 0 ? null : $"https://cwe.mitre.org/data/definitions/{digits}.html";
|
||||
}
|
||||
|
||||
private static (IReadOnlyList<AffectedVersionRange> Ranges, IReadOnlyList<NormalizedVersionRule> Normalized) CreateSemVerVersionArtifacts(
|
||||
GhsaAffectedDto affected,
|
||||
string identifier,
|
||||
string ecosystem,
|
||||
string packageName,
|
||||
DateTimeOffset recordedAt)
|
||||
{
|
||||
var note = BuildNormalizedNote(identifier);
|
||||
var results = SemVerRangeRuleBuilder.Build(affected.VulnerableRange, affected.PatchedVersion, note);
|
||||
|
||||
if (results.Count > 0)
|
||||
{
|
||||
var ranges = new List<AffectedVersionRange>(results.Count);
|
||||
var normalized = new List<NormalizedVersionRule>(results.Count);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
var primitive = result.Primitive;
|
||||
var rangeExpression = ResolveRangeExpression(result.Expression, primitive.ConstraintExpression, affected.VulnerableRange);
|
||||
|
||||
ranges.Add(new AffectedVersionRange(
|
||||
rangeKind: "semver",
|
||||
introducedVersion: Validation.TrimToNull(primitive.Introduced),
|
||||
fixedVersion: Validation.TrimToNull(primitive.Fixed),
|
||||
lastAffectedVersion: Validation.TrimToNull(primitive.LastAffected),
|
||||
rangeExpression: rangeExpression,
|
||||
provenance: CreateRangeProvenance(identifier, recordedAt),
|
||||
primitives: new RangePrimitives(
|
||||
SemVer: primitive,
|
||||
Nevra: null,
|
||||
Evr: null,
|
||||
VendorExtensions: CreateVendorExtensions(ecosystem, packageName))));
|
||||
|
||||
normalized.Add(result.NormalizedRule);
|
||||
}
|
||||
|
||||
return (ranges.ToArray(), normalized.ToArray());
|
||||
}
|
||||
|
||||
var fallbackRange = CreateFallbackRange("semver", affected, identifier, ecosystem, packageName, recordedAt);
|
||||
if (fallbackRange is null)
|
||||
{
|
||||
return (Array.Empty<AffectedVersionRange>(), Array.Empty<NormalizedVersionRule>());
|
||||
}
|
||||
|
||||
var fallbackRule = fallbackRange.ToNormalizedVersionRule(note);
|
||||
var normalizedFallback = fallbackRule is null
|
||||
? Array.Empty<NormalizedVersionRule>()
|
||||
: new[] { fallbackRule };
|
||||
|
||||
return (new[] { fallbackRange }, normalizedFallback);
|
||||
}
|
||||
|
||||
private static (IReadOnlyList<AffectedVersionRange> Ranges, IReadOnlyList<NormalizedVersionRule> Normalized) CreateVendorVersionArtifacts(
|
||||
GhsaAffectedDto affected,
|
||||
string rangeKind,
|
||||
string identifier,
|
||||
string ecosystem,
|
||||
string packageName,
|
||||
DateTimeOffset recordedAt)
|
||||
{
|
||||
var range = CreateFallbackRange(rangeKind, affected, identifier, ecosystem, packageName, recordedAt);
|
||||
if (range is null)
|
||||
{
|
||||
return (Array.Empty<AffectedVersionRange>(), Array.Empty<NormalizedVersionRule>());
|
||||
}
|
||||
|
||||
return (new[] { range }, Array.Empty<NormalizedVersionRule>());
|
||||
}
|
||||
|
||||
private static AffectedVersionRange? CreateFallbackRange(
|
||||
string rangeKind,
|
||||
GhsaAffectedDto affected,
|
||||
string identifier,
|
||||
string ecosystem,
|
||||
string packageName,
|
||||
DateTimeOffset recordedAt)
|
||||
{
|
||||
var fixedVersion = Validation.TrimToNull(affected.PatchedVersion);
|
||||
var rangeExpression = Validation.TrimToNull(affected.VulnerableRange);
|
||||
|
||||
if (fixedVersion is null && rangeExpression is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AffectedVersionRange(
|
||||
rangeKind,
|
||||
introducedVersion: null,
|
||||
fixedVersion: fixedVersion,
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: rangeExpression,
|
||||
provenance: CreateRangeProvenance(identifier, recordedAt),
|
||||
primitives: new RangePrimitives(
|
||||
SemVer: null,
|
||||
Nevra: null,
|
||||
Evr: null,
|
||||
VendorExtensions: CreateVendorExtensions(ecosystem, packageName)));
|
||||
}
|
||||
|
||||
private static AdvisoryProvenance CreateRangeProvenance(string identifier, DateTimeOffset recordedAt)
|
||||
=> new(
|
||||
GhsaConnectorPlugin.SourceName,
|
||||
"affected-range",
|
||||
identifier,
|
||||
recordedAt,
|
||||
new[] { ProvenanceFieldMasks.VersionRanges });
|
||||
|
||||
private static IReadOnlyDictionary<string, string> CreateVendorExtensions(string ecosystem, string packageName)
|
||||
=> new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["ecosystem"] = ecosystem,
|
||||
["package"] = packageName,
|
||||
};
|
||||
|
||||
private static string? BuildNormalizedNote(string identifier)
|
||||
{
|
||||
var trimmed = Validation.TrimToNull(identifier);
|
||||
return trimmed is null ? null : $"ghsa:{trimmed}";
|
||||
}
|
||||
|
||||
private static string? ResolveRangeExpression(string? parsedExpression, string? constraintExpression, string? fallbackExpression)
|
||||
{
|
||||
var parsed = Validation.TrimToNull(parsedExpression);
|
||||
if (parsed is not null)
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
var constraint = Validation.TrimToNull(constraintExpression);
|
||||
if (constraint is not null)
|
||||
{
|
||||
return constraint;
|
||||
}
|
||||
|
||||
return Validation.TrimToNull(fallbackExpression);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user