Some checks failed
		
		
	
	Build Test Deploy / build-test (push) Has been cancelled
				
			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
				
			Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
			
				
	
	
		
			448 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			448 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System.Collections.Generic;
 | |
| using System.Collections.Immutable;
 | |
| using System.Linq;
 | |
| using System.Text;
 | |
| using StellaOps.Feedser.Models;
 | |
| using StellaOps.Feedser.Normalization.Cvss;
 | |
| using StellaOps.Feedser.Normalization.SemVer;
 | |
| using StellaOps.Feedser.Storage.Mongo.Documents;
 | |
| 
 | |
| namespace StellaOps.Feedser.Source.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);
 | |
|     }
 | |
| }
 |