audit, advisories and doctors/setup work
This commit is contained in:
317
src/Tools/GoldenPairs/GoldenPairsApp.cs
Normal file
317
src/Tools/GoldenPairs/GoldenPairsApp.cs
Normal file
@@ -0,0 +1,317 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Tools.GoldenPairs.Models;
|
||||
using StellaOps.Tools.GoldenPairs.Serialization;
|
||||
using StellaOps.Tools.GoldenPairs.Services;
|
||||
|
||||
namespace StellaOps.Tools.GoldenPairs;
|
||||
|
||||
public static class GoldenPairsApp
|
||||
{
|
||||
public static Task<int> RunAsync(string[] args)
|
||||
{
|
||||
var repoRootOption = new Option<DirectoryInfo?>("--repo-root")
|
||||
{
|
||||
Description = "Repository root (defaults to nearest folder containing src/StellaOps.sln)."
|
||||
};
|
||||
|
||||
var datasetRootOption = new Option<DirectoryInfo?>("--dataset-root")
|
||||
{
|
||||
Description = "Dataset root (defaults to datasets/golden-pairs under repo root)."
|
||||
};
|
||||
|
||||
var root = new RootCommand("Golden pairs corpus tooling.");
|
||||
root.AddGlobalOption(repoRootOption);
|
||||
root.AddGlobalOption(datasetRootOption);
|
||||
|
||||
root.AddCommand(BuildMirrorCommand(repoRootOption, datasetRootOption));
|
||||
root.AddCommand(BuildDiffCommand(repoRootOption, datasetRootOption));
|
||||
root.AddCommand(BuildValidateCommand(repoRootOption, datasetRootOption));
|
||||
|
||||
return root.InvokeAsync(args);
|
||||
}
|
||||
|
||||
private static Command BuildMirrorCommand(Option<DirectoryInfo?> repoRootOption, Option<DirectoryInfo?> datasetRootOption)
|
||||
{
|
||||
var cveArgument = new Argument<string>("cve", "CVE identifier to mirror.");
|
||||
var command = new Command("mirror", "Fetch artifacts for a golden pair.");
|
||||
command.AddArgument(cveArgument);
|
||||
|
||||
command.SetAction(async (parseResult, cancellationToken) =>
|
||||
{
|
||||
var context = ResolveContext(parseResult, repoRootOption, datasetRootOption);
|
||||
if (context is null)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
using var provider = context.Provider;
|
||||
var loader = provider.GetRequiredService<GoldenPairLoader>();
|
||||
var mirror = provider.GetRequiredService<IPackageMirrorService>();
|
||||
var layout = provider.GetRequiredService<GoldenPairLayout>();
|
||||
|
||||
var cve = parseResult.GetValue(cveArgument) ?? string.Empty;
|
||||
var loadResult = await loader.LoadAsync(cve, cancellationToken).ConfigureAwait(false);
|
||||
if (!loadResult.IsValid || loadResult.Metadata is null)
|
||||
{
|
||||
return ReportLoadErrors(loadResult.Errors);
|
||||
}
|
||||
|
||||
var metadata = loadResult.Metadata;
|
||||
Directory.CreateDirectory(layout.GetOriginalDirectory(metadata.Cve));
|
||||
Directory.CreateDirectory(layout.GetPatchedDirectory(metadata.Cve));
|
||||
|
||||
var originalResult = await mirror.FetchAsync(metadata.Original, layout.GetOriginalDirectory(metadata.Cve), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (!originalResult.Success)
|
||||
{
|
||||
Console.Error.WriteLine($"[FAIL] Original mirror failed: {originalResult.ErrorMessage}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var originalPath = EnsureArtifactNamed(metadata, layout.GetOriginalDirectory(metadata.Cve), originalResult.LocalPath);
|
||||
WriteShaFile(originalPath, originalResult.ActualSha256);
|
||||
|
||||
var patchedResult = await mirror.FetchAsync(metadata.Patched, layout.GetPatchedDirectory(metadata.Cve), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (!patchedResult.Success)
|
||||
{
|
||||
Console.Error.WriteLine($"[FAIL] Patched mirror failed: {patchedResult.ErrorMessage}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var patchedPath = EnsureArtifactNamed(metadata, layout.GetPatchedDirectory(metadata.Cve), patchedResult.LocalPath);
|
||||
WriteShaFile(patchedPath, patchedResult.ActualSha256);
|
||||
|
||||
Console.WriteLine($"[OK] Mirrored {metadata.Cve} into {layout.GetPairDirectory(metadata.Cve)}");
|
||||
return 0;
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildDiffCommand(Option<DirectoryInfo?> repoRootOption, Option<DirectoryInfo?> datasetRootOption)
|
||||
{
|
||||
var cveArgument = new Argument<string>("cve", "CVE identifier to diff.");
|
||||
var command = new Command("diff", "Run diff analysis on a golden pair.");
|
||||
command.AddArgument(cveArgument);
|
||||
|
||||
var outputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output format: json or table.",
|
||||
DefaultValueFactory = _ => "json"
|
||||
};
|
||||
command.AddOption(outputOption);
|
||||
|
||||
command.SetAction(async (parseResult, cancellationToken) =>
|
||||
{
|
||||
var context = ResolveContext(parseResult, repoRootOption, datasetRootOption);
|
||||
if (context is null)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
using var provider = context.Provider;
|
||||
var loader = provider.GetRequiredService<GoldenPairLoader>();
|
||||
var diff = provider.GetRequiredService<IDiffPipelineService>();
|
||||
var layout = provider.GetRequiredService<GoldenPairLayout>();
|
||||
|
||||
var cve = parseResult.GetValue(cveArgument) ?? string.Empty;
|
||||
var loadResult = await loader.LoadAsync(cve, cancellationToken).ConfigureAwait(false);
|
||||
if (!loadResult.IsValid || loadResult.Metadata is null)
|
||||
{
|
||||
return ReportLoadErrors(loadResult.Errors);
|
||||
}
|
||||
|
||||
GoldenDiffReport report;
|
||||
try
|
||||
{
|
||||
report = await diff.DiffAsync(loadResult.Metadata, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[FAIL] Diff failed: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var reportJson = GoldenPairsJsonSerializer.SerializeIndented(report);
|
||||
var reportPath = layout.GetDiffReportPath(loadResult.Metadata.Cve);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(reportPath)!);
|
||||
await File.WriteAllTextAsync(reportPath, reportJson, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var output = parseResult.GetValue(outputOption) ?? "json";
|
||||
if (string.Equals(output, "table", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
WriteTableReport(report);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(reportJson);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[OK] Diff report written to {reportPath}");
|
||||
return report.MatchesExpected ? 0 : 1;
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildValidateCommand(Option<DirectoryInfo?> repoRootOption, Option<DirectoryInfo?> datasetRootOption)
|
||||
{
|
||||
var command = new Command("validate", "Validate all golden pairs in the corpus.");
|
||||
var failFastOption = new Option<bool>("--fail-fast")
|
||||
{
|
||||
Description = "Stop at first failure."
|
||||
};
|
||||
command.AddOption(failFastOption);
|
||||
|
||||
command.SetAction(async (parseResult, cancellationToken) =>
|
||||
{
|
||||
var context = ResolveContext(parseResult, repoRootOption, datasetRootOption);
|
||||
if (context is null)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
using var provider = context.Provider;
|
||||
var loader = provider.GetRequiredService<GoldenPairLoader>();
|
||||
var diff = provider.GetRequiredService<IDiffPipelineService>();
|
||||
var layout = provider.GetRequiredService<GoldenPairLayout>();
|
||||
|
||||
var failFast = parseResult.GetValue(failFastOption);
|
||||
var pairDirectories = Directory.EnumerateDirectories(layout.DatasetRoot, "CVE-*", SearchOption.TopDirectoryOnly)
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
var failures = 0;
|
||||
foreach (var pairDir in pairDirectories)
|
||||
{
|
||||
var metadataPath = Path.Combine(pairDir, GoldenPairLayout.DefaultMetadataFileName);
|
||||
var loadResult = await loader.LoadFromPathAsync(metadataPath, cancellationToken).ConfigureAwait(false);
|
||||
if (!loadResult.IsValid || loadResult.Metadata is null)
|
||||
{
|
||||
failures++;
|
||||
Console.Error.WriteLine($"[FAIL] {Path.GetFileName(pairDir)}: metadata invalid.");
|
||||
if (failFast)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var report = await diff.DiffAsync(loadResult.Metadata, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var reportJson = GoldenPairsJsonSerializer.SerializeIndented(report);
|
||||
await File.WriteAllTextAsync(layout.GetDiffReportPath(loadResult.Metadata.Cve), reportJson, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (report.MatchesExpected)
|
||||
{
|
||||
Console.WriteLine($"[OK] {loadResult.Metadata.Cve} validated.");
|
||||
}
|
||||
else
|
||||
{
|
||||
failures++;
|
||||
Console.Error.WriteLine($"[FAIL] {loadResult.Metadata.Cve} mismatch.");
|
||||
if (failFast)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failures++;
|
||||
Console.Error.WriteLine($"[FAIL] {loadResult.Metadata.Cve}: {ex.Message}");
|
||||
if (failFast)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"Summary: {pairDirectories.Length - failures}/{pairDirectories.Length} passed.");
|
||||
return failures == 0 ? 0 : 1;
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static GoldenPairsContext? ResolveContext(
|
||||
ParseResult parseResult,
|
||||
Option<DirectoryInfo?> repoRootOption,
|
||||
Option<DirectoryInfo?> datasetRootOption)
|
||||
{
|
||||
var repoRoot = parseResult.GetValue(repoRootOption)?.FullName;
|
||||
var datasetRoot = parseResult.GetValue(datasetRootOption)?.FullName;
|
||||
|
||||
var resolvedRepoRoot = GoldenPairsPaths.TryResolveRepoRoot(repoRoot);
|
||||
if (resolvedRepoRoot is null)
|
||||
{
|
||||
Console.Error.WriteLine("[FAIL] Unable to resolve repo root. Provide --repo-root explicitly.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var resolvedDatasetRoot = GoldenPairsPaths.ResolveDatasetRoot(resolvedRepoRoot, datasetRoot);
|
||||
var metadataSchemaPath = GoldenPairsPaths.ResolveMetadataSchemaPath(resolvedRepoRoot);
|
||||
var indexSchemaPath = GoldenPairsPaths.ResolveIndexSchemaPath(resolvedRepoRoot);
|
||||
|
||||
var provider = GoldenPairsServiceFactory.Build(resolvedDatasetRoot, metadataSchemaPath, indexSchemaPath);
|
||||
return new GoldenPairsContext(resolvedRepoRoot, resolvedDatasetRoot, provider);
|
||||
}
|
||||
|
||||
private static int ReportLoadErrors(ImmutableArray<GoldenPairLoadError> errors)
|
||||
{
|
||||
foreach (var error in errors)
|
||||
{
|
||||
var location = string.IsNullOrWhiteSpace(error.InstanceLocation) ? string.Empty : $" ({error.InstanceLocation})";
|
||||
Console.Error.WriteLine($"[FAIL] {error.Message}{location}");
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static string EnsureArtifactNamed(GoldenPairMetadata metadata, string destinationDirectory, string currentPath)
|
||||
{
|
||||
var expectedPath = Path.Combine(destinationDirectory, metadata.Artifact.Name);
|
||||
if (string.Equals(currentPath, expectedPath, StringComparison.Ordinal))
|
||||
{
|
||||
return currentPath;
|
||||
}
|
||||
|
||||
if (File.Exists(expectedPath))
|
||||
{
|
||||
File.Delete(expectedPath);
|
||||
}
|
||||
|
||||
File.Move(currentPath, expectedPath);
|
||||
return expectedPath;
|
||||
}
|
||||
|
||||
private static void WriteShaFile(string filePath, string sha256)
|
||||
{
|
||||
var shaPath = $"{filePath}.sha256";
|
||||
File.WriteAllText(shaPath, $"{sha256}\n", Encoding.ASCII);
|
||||
}
|
||||
|
||||
private static void WriteTableReport(GoldenDiffReport report)
|
||||
{
|
||||
Console.WriteLine($"CVE: {report.Cve}");
|
||||
Console.WriteLine($"Verdict: {report.Verdict.ToString().ToLowerInvariant()} ({report.Confidence:F2})");
|
||||
Console.WriteLine($"Matches expected: {report.MatchesExpected}");
|
||||
Console.WriteLine("Sections:");
|
||||
foreach (var section in report.Sections)
|
||||
{
|
||||
Console.WriteLine($" {section.Name} - {section.Status.ToString().ToLowerInvariant()}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record GoldenPairsContext(
|
||||
string RepoRoot,
|
||||
string DatasetRoot,
|
||||
ServiceProvider Provider);
|
||||
}
|
||||
Reference in New Issue
Block a user