save progress

This commit is contained in:
StellaOps Bot
2026-01-04 14:54:52 +02:00
parent c49b03a254
commit 3098e84de4
132 changed files with 19783 additions and 31 deletions

View File

@@ -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'