// // Copyright (c) StellaOps. Licensed under the BUSL-1.1. // using System.Collections.Immutable; using System.Text; namespace StellaOps.Scanner.Emit.Pedigree; /// /// Builds patch info entries from Feedser hunk signatures. /// Sprint: SPRINT_20260107_005_002 Task PD-007 /// public sealed class PatchInfoBuilder { private readonly List _patches = new(); /// /// Adds a backport patch. /// /// URL to the patch file. /// Patch diff content. /// CVE IDs resolved by this patch. /// Source of the patch (e.g., "debian-security"). /// This builder for fluent chaining. public PatchInfoBuilder AddBackport( string? diffUrl = null, string? diffText = null, IEnumerable? resolvesCves = null, string? source = null) { return AddPatch(PatchType.Backport, diffUrl, diffText, resolvesCves, source: source); } /// /// Adds a cherry-pick patch (from upstream). /// /// URL to the patch file. /// Patch diff content. /// CVE IDs resolved by this patch. /// Source of the patch. /// This builder for fluent chaining. public PatchInfoBuilder AddCherryPick( string? diffUrl = null, string? diffText = null, IEnumerable? resolvesCves = null, string? source = null) { return AddPatch(PatchType.CherryPick, diffUrl, diffText, resolvesCves, source: source); } /// /// Adds an unofficial patch (vendor/custom). /// /// URL to the patch file. /// Patch diff content. /// CVE IDs resolved by this patch. /// Source of the patch. /// This builder for fluent chaining. public PatchInfoBuilder AddUnofficialPatch( string? diffUrl = null, string? diffText = null, IEnumerable? resolvesCves = null, string? source = null) { return AddPatch(PatchType.Unofficial, diffUrl, diffText, resolvesCves, source: source); } /// /// Adds a patch with full configuration. /// /// Patch type. /// URL to the patch file. /// Patch diff content. /// CVE IDs resolved by this patch. /// Functions affected by this patch. /// Source of the patch. /// This builder for fluent chaining. public PatchInfoBuilder AddPatch( PatchType type, string? diffUrl = null, string? diffText = null, IEnumerable? resolvesCves = null, IEnumerable? affectedFunctions = null, string? source = null) { var resolutions = resolvesCves? .Where(cve => !string.IsNullOrEmpty(cve)) .Select(cve => new PatchResolution { Id = cve.ToUpperInvariant(), SourceName = DetermineSourceName(cve) }) .ToImmutableArray() ?? ImmutableArray.Empty; _patches.Add(new PatchInfo { Type = type, DiffUrl = diffUrl, DiffText = NormalizeDiffText(diffText), Resolves = resolutions, AffectedFunctions = affectedFunctions?.ToImmutableArray() ?? ImmutableArray.Empty, Source = source }); return this; } /// /// Adds a patch from Feedser patch origin. /// /// Origin type from Feedser (upstream, distro, vendor). /// URL to the patch. /// Diff content. /// CVEs resolved. /// Affected function names. /// Patch source identifier. /// This builder for fluent chaining. public PatchInfoBuilder AddFromFeedserOrigin( string feedserOrigin, string? diffUrl = null, string? diffText = null, IEnumerable? resolvesCves = null, IEnumerable? affectedFunctions = null, string? source = null) { var type = MapFeedserOriginToType(feedserOrigin); return AddPatch(type, diffUrl, diffText, resolvesCves, affectedFunctions, source); } /// /// Adds a patch with resolution references including source URLs. /// /// Patch type. /// Full resolution references. /// URL to the patch. /// Diff content. /// Patch source. /// This builder for fluent chaining. public PatchInfoBuilder AddPatchWithResolutions( PatchType type, IEnumerable resolutions, string? diffUrl = null, string? diffText = null, string? source = null) { _patches.Add(new PatchInfo { Type = type, DiffUrl = diffUrl, DiffText = NormalizeDiffText(diffText), Resolves = resolutions.ToImmutableArray(), Source = source }); return this; } /// /// Builds the immutable array of patches. /// /// Immutable array of patch info. public ImmutableArray Build() { return _patches .OrderBy(p => p.Type) .ThenBy(p => p.Source, StringComparer.OrdinalIgnoreCase) .ThenBy(p => p.DiffUrl, StringComparer.Ordinal) .ToImmutableArray(); } /// /// Clears the builder for reuse. /// /// This builder for fluent chaining. public PatchInfoBuilder Clear() { _patches.Clear(); return this; } private static PatchType MapFeedserOriginToType(string origin) => origin.ToLowerInvariant() switch { "upstream" => PatchType.CherryPick, "distro" => PatchType.Backport, "vendor" => PatchType.Unofficial, "backport" => PatchType.Backport, "cherrypick" or "cherry-pick" => PatchType.CherryPick, _ => PatchType.Unofficial }; private static string DetermineSourceName(string cveId) { var upper = cveId.ToUpperInvariant(); if (upper.StartsWith("CVE-", StringComparison.Ordinal)) { return "NVD"; } if (upper.StartsWith("GHSA-", StringComparison.Ordinal)) { return "GitHub"; } if (upper.StartsWith("OSV-", StringComparison.Ordinal)) { return "OSV"; } return "Unknown"; } private static string? NormalizeDiffText(string? diffText, int maxLength = 50000) { if (string.IsNullOrEmpty(diffText)) { return null; } // Normalize line endings var normalized = diffText .Replace("\r\n", "\n", StringComparison.Ordinal) .Replace("\r", "\n", StringComparison.Ordinal); // Truncate if too long if (normalized.Length <= maxLength) { return normalized; } // Find a good truncation point (end of a hunk) var truncateAt = normalized.LastIndexOf("\n@@", maxLength - 100, StringComparison.Ordinal); if (truncateAt < maxLength / 2) { truncateAt = maxLength - 50; } var sb = new StringBuilder(truncateAt + 100); sb.Append(normalized.AsSpan(0, truncateAt)); sb.AppendLine(); sb.AppendLine("... (truncated)"); return sb.ToString(); } }