save progress
This commit is contained in:
@@ -5,10 +5,14 @@
|
||||
// Description: Command handlers for binary reachability operations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Spectre.Console;
|
||||
using StellaOps.Scanner.CallGraph;
|
||||
using StellaOps.Scanner.CallGraph.Binary;
|
||||
|
||||
namespace StellaOps.Cli.Commands.Binary;
|
||||
|
||||
@@ -632,6 +636,238 @@ internal static class BinaryCommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle 'stella binary callgraph' command (CALLGRAPH-01).
|
||||
/// Extracts call graph from native binary and computes deterministic SHA-256 digest.
|
||||
/// </summary>
|
||||
public static async Task<int> HandleCallGraphAsync(
|
||||
IServiceProvider services,
|
||||
string filePath,
|
||||
string format,
|
||||
string? outputPath,
|
||||
string? emitSbomPath,
|
||||
string? scanId,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("binary-callgraph");
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] File not found: {filePath}");
|
||||
return ExitCodes.FileNotFound;
|
||||
}
|
||||
|
||||
// Resolve scan ID (auto-generate if not provided)
|
||||
var effectiveScanId = scanId ?? $"cli-{Path.GetFileName(filePath)}-{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||
|
||||
CallGraphSnapshot snapshot = null!;
|
||||
|
||||
await AnsiConsole.Status()
|
||||
.StartAsync("Extracting binary call graph...", async ctx =>
|
||||
{
|
||||
ctx.Status("Loading binary...");
|
||||
|
||||
// Get the binary call graph extractor
|
||||
var timeProvider = services.GetService<TimeProvider>() ?? TimeProvider.System;
|
||||
var extractorLogger = loggerFactory.CreateLogger<BinaryCallGraphExtractor>();
|
||||
var extractor = new BinaryCallGraphExtractor(extractorLogger, timeProvider);
|
||||
|
||||
ctx.Status("Analyzing symbols and relocations...");
|
||||
|
||||
var request = new CallGraphExtractionRequest(
|
||||
ScanId: effectiveScanId,
|
||||
Language: "native",
|
||||
TargetPath: filePath);
|
||||
|
||||
snapshot = await extractor.ExtractAsync(request, cancellationToken);
|
||||
|
||||
ctx.Status("Computing digest...");
|
||||
});
|
||||
|
||||
// Format output based on requested format
|
||||
string output;
|
||||
switch (format)
|
||||
{
|
||||
case "digest":
|
||||
output = snapshot.GraphDigest;
|
||||
break;
|
||||
|
||||
case "json":
|
||||
output = JsonSerializer.Serialize(new
|
||||
{
|
||||
scanId = snapshot.ScanId,
|
||||
graphDigest = snapshot.GraphDigest,
|
||||
language = snapshot.Language,
|
||||
extractedAt = snapshot.ExtractedAt.ToString("O", CultureInfo.InvariantCulture),
|
||||
nodeCount = snapshot.Nodes.Length,
|
||||
edgeCount = snapshot.Edges.Length,
|
||||
entrypointCount = snapshot.EntrypointIds.Length,
|
||||
nodes = snapshot.Nodes,
|
||||
edges = snapshot.Edges,
|
||||
entrypointIds = snapshot.EntrypointIds
|
||||
}, JsonOptions);
|
||||
break;
|
||||
|
||||
case "summary":
|
||||
default:
|
||||
output = string.Join(Environment.NewLine,
|
||||
[
|
||||
$"GraphDigest: {snapshot.GraphDigest}",
|
||||
$"ScanId: {snapshot.ScanId}",
|
||||
$"Language: {snapshot.Language}",
|
||||
$"ExtractedAt: {snapshot.ExtractedAt:O}",
|
||||
$"Nodes: {snapshot.Nodes.Length}",
|
||||
$"Edges: {snapshot.Edges.Length}",
|
||||
$"Entrypoints: {snapshot.EntrypointIds.Length}"
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
// Write output
|
||||
if (!string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
await File.WriteAllTextAsync(outputPath, output, cancellationToken);
|
||||
AnsiConsole.MarkupLine($"[green]Output written to:[/] {outputPath}");
|
||||
}
|
||||
else if (format == "digest")
|
||||
{
|
||||
// For digest-only format, just output the digest
|
||||
Console.WriteLine(output);
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.WriteLine(output);
|
||||
}
|
||||
|
||||
// Inject into SBOM if requested
|
||||
if (!string.IsNullOrWhiteSpace(emitSbomPath))
|
||||
{
|
||||
var sbomResult = await InjectCallGraphDigestIntoSbomAsync(
|
||||
emitSbomPath,
|
||||
filePath,
|
||||
snapshot,
|
||||
cancellationToken);
|
||||
|
||||
if (sbomResult == 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[green]Callgraph digest injected into SBOM:[/] {emitSbomPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[yellow]Warning:[/] Failed to inject digest into SBOM");
|
||||
}
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Extracted call graph for {Path}: {Nodes} nodes, {Edges} edges, digest={Digest}",
|
||||
filePath,
|
||||
snapshot.Nodes.Length,
|
||||
snapshot.Edges.Length,
|
||||
snapshot.GraphDigest);
|
||||
}
|
||||
|
||||
return ExitCodes.Success;
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
|
||||
logger.LogError(ex, "Unsupported binary format for {Path}", filePath);
|
||||
return ExitCodes.InvalidArguments;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
|
||||
logger.LogError(ex, "Failed to extract call graph for {Path}", filePath);
|
||||
return ExitCodes.GeneralError;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects callgraph digest as a property into a CycloneDX SBOM.
|
||||
/// </summary>
|
||||
private static async Task<int> InjectCallGraphDigestIntoSbomAsync(
|
||||
string sbomPath,
|
||||
string binaryPath,
|
||||
CallGraphSnapshot snapshot,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(sbomPath))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sbomJson = await File.ReadAllTextAsync(sbomPath, cancellationToken);
|
||||
var doc = JsonNode.Parse(sbomJson) as JsonObject;
|
||||
|
||||
if (doc == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Ensure metadata.properties exists
|
||||
var metadata = doc["metadata"] as JsonObject;
|
||||
if (metadata == null)
|
||||
{
|
||||
metadata = new JsonObject();
|
||||
doc["metadata"] = metadata;
|
||||
}
|
||||
|
||||
var properties = metadata["properties"] as JsonArray;
|
||||
if (properties == null)
|
||||
{
|
||||
properties = new JsonArray();
|
||||
metadata["properties"] = properties;
|
||||
}
|
||||
|
||||
var binaryName = Path.GetFileName(binaryPath);
|
||||
|
||||
// Add callgraph properties using stellaops namespace
|
||||
properties.Add(new JsonObject
|
||||
{
|
||||
["name"] = $"stellaops:callgraph:digest:{binaryName}",
|
||||
["value"] = snapshot.GraphDigest
|
||||
});
|
||||
properties.Add(new JsonObject
|
||||
{
|
||||
["name"] = $"stellaops:callgraph:nodeCount:{binaryName}",
|
||||
["value"] = snapshot.Nodes.Length.ToString(CultureInfo.InvariantCulture)
|
||||
});
|
||||
properties.Add(new JsonObject
|
||||
{
|
||||
["name"] = $"stellaops:callgraph:edgeCount:{binaryName}",
|
||||
["value"] = snapshot.Edges.Length.ToString(CultureInfo.InvariantCulture)
|
||||
});
|
||||
properties.Add(new JsonObject
|
||||
{
|
||||
["name"] = $"stellaops:callgraph:entrypointCount:{binaryName}",
|
||||
["value"] = snapshot.EntrypointIds.Length.ToString(CultureInfo.InvariantCulture)
|
||||
});
|
||||
properties.Add(new JsonObject
|
||||
{
|
||||
["name"] = $"stellaops:callgraph:extractedAt:{binaryName}",
|
||||
["value"] = snapshot.ExtractedAt.ToString("O", CultureInfo.InvariantCulture)
|
||||
});
|
||||
|
||||
// Write updated SBOM
|
||||
var updatedJson = doc.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(sbomPath, updatedJson, cancellationToken);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static string DetectFormat(byte[] header)
|
||||
{
|
||||
// ELF magic: 0x7f 'E' 'L' 'F'
|
||||
|
||||
Reference in New Issue
Block a user