245 lines
8.3 KiB
C#
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();
|
|
}
|
|
}
|