more audit work
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
// <copyright file="PatchInfoBuilder.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user