Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Emit/Pedigree/PatchInfoBuilder.cs

245 lines
8.3 KiB
C#

// <copyright file="PatchInfoBuilder.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
using System.Text;
namespace StellaOps.Scanner.Emit.Pedigree;
/// <summary>
/// Builds patch info entries from Feedser hunk signatures.
/// Sprint: SPRINT_20260107_005_002 Task PD-007
/// </summary>
public sealed class PatchInfoBuilder
{
private readonly List<PatchInfo> _patches = new();
/// <summary>
/// Adds a backport patch.
/// </summary>
/// <param name="diffUrl">URL to the patch file.</param>
/// <param name="diffText">Patch diff content.</param>
/// <param name="resolvesCves">CVE IDs resolved by this patch.</param>
/// <param name="source">Source of the patch (e.g., "debian-security").</param>
/// <returns>This builder for fluent chaining.</returns>
public PatchInfoBuilder AddBackport(
string? diffUrl = null,
string? diffText = null,
IEnumerable<string>? resolvesCves = null,
string? source = null)
{
return AddPatch(PatchType.Backport, diffUrl, diffText, resolvesCves, source: source);
}
/// <summary>
/// Adds a cherry-pick patch (from upstream).
/// </summary>
/// <param name="diffUrl">URL to the patch file.</param>
/// <param name="diffText">Patch diff content.</param>
/// <param name="resolvesCves">CVE IDs resolved by this patch.</param>
/// <param name="source">Source of the patch.</param>
/// <returns>This builder for fluent chaining.</returns>
public PatchInfoBuilder AddCherryPick(
string? diffUrl = null,
string? diffText = null,
IEnumerable<string>? resolvesCves = null,
string? source = null)
{
return AddPatch(PatchType.CherryPick, diffUrl, diffText, resolvesCves, source: source);
}
/// <summary>
/// Adds an unofficial patch (vendor/custom).
/// </summary>
/// <param name="diffUrl">URL to the patch file.</param>
/// <param name="diffText">Patch diff content.</param>
/// <param name="resolvesCves">CVE IDs resolved by this patch.</param>
/// <param name="source">Source of the patch.</param>
/// <returns>This builder for fluent chaining.</returns>
public PatchInfoBuilder AddUnofficialPatch(
string? diffUrl = null,
string? diffText = null,
IEnumerable<string>? resolvesCves = null,
string? source = null)
{
return AddPatch(PatchType.Unofficial, diffUrl, diffText, resolvesCves, source: source);
}
/// <summary>
/// Adds a patch with full configuration.
/// </summary>
/// <param name="type">Patch type.</param>
/// <param name="diffUrl">URL to the patch file.</param>
/// <param name="diffText">Patch diff content.</param>
/// <param name="resolvesCves">CVE IDs resolved by this patch.</param>
/// <param name="affectedFunctions">Functions affected by this patch.</param>
/// <param name="source">Source of the patch.</param>
/// <returns>This builder for fluent chaining.</returns>
public PatchInfoBuilder AddPatch(
PatchType type,
string? diffUrl = null,
string? diffText = null,
IEnumerable<string>? resolvesCves = null,
IEnumerable<string>? 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<PatchResolution>.Empty;
_patches.Add(new PatchInfo
{
Type = type,
DiffUrl = diffUrl,
DiffText = NormalizeDiffText(diffText),
Resolves = resolutions,
AffectedFunctions = affectedFunctions?.ToImmutableArray() ?? ImmutableArray<string>.Empty,
Source = source
});
return this;
}
/// <summary>
/// Adds a patch from Feedser patch origin.
/// </summary>
/// <param name="feedserOrigin">Origin type from Feedser (upstream, distro, vendor).</param>
/// <param name="diffUrl">URL to the patch.</param>
/// <param name="diffText">Diff content.</param>
/// <param name="resolvesCves">CVEs resolved.</param>
/// <param name="affectedFunctions">Affected function names.</param>
/// <param name="source">Patch source identifier.</param>
/// <returns>This builder for fluent chaining.</returns>
public PatchInfoBuilder AddFromFeedserOrigin(
string feedserOrigin,
string? diffUrl = null,
string? diffText = null,
IEnumerable<string>? resolvesCves = null,
IEnumerable<string>? affectedFunctions = null,
string? source = null)
{
var type = MapFeedserOriginToType(feedserOrigin);
return AddPatch(type, diffUrl, diffText, resolvesCves, affectedFunctions, source);
}
/// <summary>
/// Adds a patch with resolution references including source URLs.
/// </summary>
/// <param name="type">Patch type.</param>
/// <param name="resolutions">Full resolution references.</param>
/// <param name="diffUrl">URL to the patch.</param>
/// <param name="diffText">Diff content.</param>
/// <param name="source">Patch source.</param>
/// <returns>This builder for fluent chaining.</returns>
public PatchInfoBuilder AddPatchWithResolutions(
PatchType type,
IEnumerable<PatchResolution> 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;
}
/// <summary>
/// Builds the immutable array of patches.
/// </summary>
/// <returns>Immutable array of patch info.</returns>
public ImmutableArray<PatchInfo> Build()
{
return _patches
.OrderBy(p => p.Type)
.ThenBy(p => p.Source, StringComparer.OrdinalIgnoreCase)
.ThenBy(p => p.DiffUrl, StringComparer.Ordinal)
.ToImmutableArray();
}
/// <summary>
/// Clears the builder for reuse.
/// </summary>
/// <returns>This builder for fluent chaining.</returns>
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();
}
}