Files
git.stella-ops.org/src/Cli/__Libraries/StellaOps.Cli.Plugins.Vex/VexCliOutput.cs
StellaOps Bot 83c37243e0 save progress
2026-01-03 11:02:24 +02:00

257 lines
9.2 KiB
C#

using System.Globalization;
using System.Text;
using System.Text.Json;
namespace StellaOps.Cli.Plugins.Vex;
internal static class VexCliOutput
{
private static readonly JsonSerializerOptions OutputJsonOptions = new()
{
WriteIndented = true
};
public static IReadOnlyList<AutoDowngradeCandidate> OrderCandidates(
IReadOnlyList<AutoDowngradeCandidate>? candidates)
{
if (candidates is null || candidates.Count == 0)
{
return Array.Empty<AutoDowngradeCandidate>();
}
return candidates
.OrderBy(candidate => candidate.CveId, StringComparer.Ordinal)
.ThenBy(candidate => candidate.Symbol, StringComparer.Ordinal)
.ThenBy(candidate => candidate.ComponentPath, StringComparer.Ordinal)
.ThenBy(candidate => candidate.ObservationCount)
.ThenBy(candidate => candidate.CpuPercentage)
.ThenBy(candidate => candidate.Confidence)
.ThenBy(candidate => candidate.ProductId, StringComparer.Ordinal)
.ThenBy(candidate => candidate.BuildId, StringComparer.Ordinal)
.ToList();
}
public static IReadOnlyList<NotReachableAnalysisEntry> OrderAnalyses(
IReadOnlyList<NotReachableAnalysisEntry>? analyses)
{
if (analyses is null || analyses.Count == 0)
{
return Array.Empty<NotReachableAnalysisEntry>();
}
return analyses
.OrderBy(entry => entry.CveId, StringComparer.Ordinal)
.ThenBy(entry => entry.Symbol, StringComparer.Ordinal)
.ThenBy(entry => entry.ComponentPath, StringComparer.Ordinal)
.ThenBy(entry => entry.Confidence)
.ThenBy(entry => entry.ProductId, StringComparer.Ordinal)
.ToList();
}
public static async Task<int> WriteAutoDowngradeResultsAsync(
AutoDowngradeCheckResult result,
bool dryRun,
OutputFormat format,
string? outputPath,
CancellationToken cancellationToken)
{
var candidates = result.Candidates ?? Array.Empty<AutoDowngradeCandidate>();
if (candidates.Count == 0 && format == OutputFormat.Table)
{
await Console.Out.WriteLineAsync("No hot vulnerable symbols detected.")
.ConfigureAwait(false);
return 0;
}
var content = format switch
{
OutputFormat.Json => JsonSerializer.Serialize(result, OutputJsonOptions),
OutputFormat.Csv => BuildAutoDowngradeCsv(candidates, dryRun),
_ => BuildAutoDowngradeTable(candidates, dryRun, result.ImageDigest)
};
if (!string.IsNullOrWhiteSpace(outputPath))
{
EnsureOutputDirectory(outputPath);
await File.WriteAllTextAsync(outputPath, content, cancellationToken).ConfigureAwait(false);
await Console.Out.WriteLineAsync($"Results written to: {outputPath}").ConfigureAwait(false);
}
else
{
await Console.Out.WriteLineAsync(content).ConfigureAwait(false);
}
return 0;
}
public static async Task WriteNotReachableResultsAsync(
NotReachableAnalysisResult result,
bool dryRun,
CancellationToken cancellationToken)
{
_ = cancellationToken;
var analyses = result.Analyses ?? Array.Empty<NotReachableAnalysisEntry>();
if (analyses.Count == 0)
{
await Console.Out.WriteLineAsync("No unreached vulnerable symbols found requiring VEX.")
.ConfigureAwait(false);
return;
}
var content = BuildNotReachableTable(analyses, dryRun);
await Console.Out.WriteLineAsync(content).ConfigureAwait(false);
}
public static async Task WriteStatementsAsync(
IReadOnlyList<object>? statements,
string outputPath,
CancellationToken cancellationToken)
{
EnsureOutputDirectory(outputPath);
var content = JsonSerializer.Serialize(statements ?? Array.Empty<object>(), OutputJsonOptions);
await File.WriteAllTextAsync(outputPath, content, cancellationToken).ConfigureAwait(false);
}
public static async Task<int> WriteErrorAsync(string message)
{
await Console.Error.WriteLineAsync(message).ConfigureAwait(false);
return 1;
}
public static async Task<int> WriteNotImplementedAsync(string commandName)
{
await Console.Error.WriteLineAsync($"{commandName} is not implemented.").ConfigureAwait(false);
return 2;
}
private static string BuildAutoDowngradeCsv(IReadOnlyList<AutoDowngradeCandidate> candidates, bool dryRun)
{
var builder = new StringBuilder();
builder.AppendLine("cve_id,symbol,component_path,cpu_percentage,observations,confidence,status");
foreach (var candidate in candidates)
{
var status = dryRun ? "pending" : "downgrade";
builder
.Append(EscapeCsv(candidate.CveId)).Append(',')
.Append(EscapeCsv(candidate.Symbol)).Append(',')
.Append(EscapeCsv(candidate.ComponentPath)).Append(',')
.Append(candidate.CpuPercentage.ToString("F2", CultureInfo.InvariantCulture)).Append(',')
.Append(candidate.ObservationCount.ToString(CultureInfo.InvariantCulture)).Append(',')
.Append(candidate.Confidence.ToString("F2", CultureInfo.InvariantCulture)).Append(',')
.Append(EscapeCsv(status))
.AppendLine();
}
return builder.ToString().TrimEnd();
}
private static string BuildAutoDowngradeTable(
IReadOnlyList<AutoDowngradeCandidate> candidates,
bool dryRun,
string? imageDigest)
{
var builder = new StringBuilder();
builder.AppendLine(dryRun ? "Auto-downgrade candidates (dry run)" : "Hot vulnerable symbols");
builder.AppendLine("CVE | Symbol | CPU% | Observations | Confidence | Status");
foreach (var candidate in candidates)
{
var status = dryRun ? "pending" : "downgrade";
builder
.Append(candidate.CveId).Append(" | ")
.Append(Truncate(candidate.Symbol, 40)).Append(" | ")
.Append(candidate.CpuPercentage.ToString("F1", CultureInfo.InvariantCulture)).Append(" | ")
.Append(candidate.ObservationCount.ToString(CultureInfo.InvariantCulture)).Append(" | ")
.Append(candidate.Confidence.ToString("F2", CultureInfo.InvariantCulture)).Append(" | ")
.Append(status)
.AppendLine();
}
builder.AppendLine();
builder.AppendLine($"Total candidates: {candidates.Count.ToString(CultureInfo.InvariantCulture)}");
if (candidates.Count > 0)
{
var maxCpu = candidates.Max(candidate => candidate.CpuPercentage);
builder.AppendLine($"Highest CPU: {maxCpu.ToString("F1", CultureInfo.InvariantCulture)}%");
}
if (!string.IsNullOrWhiteSpace(imageDigest))
{
builder.AppendLine($"Image: {imageDigest}");
}
return builder.ToString().TrimEnd();
}
private static string BuildNotReachableTable(IReadOnlyList<NotReachableAnalysisEntry> analyses, bool dryRun)
{
var builder = new StringBuilder();
builder.AppendLine("Symbols not reachable at runtime");
builder.AppendLine("CVE | Symbol | Component | Confidence | Reason");
foreach (var analysis in analyses)
{
var reason = string.IsNullOrWhiteSpace(analysis.PrimaryReason) ? "Unknown" : analysis.PrimaryReason;
builder
.Append(analysis.CveId).Append(" | ")
.Append(Truncate(analysis.Symbol, 30)).Append(" | ")
.Append(TruncatePath(analysis.ComponentPath, 30)).Append(" | ")
.Append(analysis.Confidence.ToString("F2", CultureInfo.InvariantCulture)).Append(" | ")
.Append(reason)
.AppendLine();
}
builder.AppendLine();
builder.AppendLine($"Total analyses: {analyses.Count.ToString(CultureInfo.InvariantCulture)}");
builder.AppendLine(dryRun ? "Mode: dry run" : "Mode: generate VEX");
return builder.ToString().TrimEnd();
}
private static string Truncate(string value, int maxLength)
{
if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
{
return value;
}
return value[..(maxLength - 3)] + "...";
}
private static string TruncatePath(string value, int maxLength)
{
if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
{
return value;
}
return "..." + value[^Math.Min(maxLength - 3, value.Length)..];
}
private static string EscapeCsv(string value)
{
if (value.IndexOfAny([',', '"', '\n', '\r']) < 0)
{
return value;
}
var escaped = value.Replace("\"", "\"\"");
return $"\"{escaped}\"";
}
private static void EnsureOutputDirectory(string outputPath)
{
var directory = Path.GetDirectoryName(outputPath);
if (string.IsNullOrWhiteSpace(directory))
{
return;
}
Directory.CreateDirectory(directory);
}
}