Files
git.stella-ops.org/src/Cli/StellaOps.Cli/Commands/Binary/BinaryCommandHandlers.cs

938 lines
33 KiB
C#

// -----------------------------------------------------------------------------
// BinaryCommandHandlers.cs
// Sprint: SPRINT_3850_0001_0001_oci_storage_cli
// Tasks: T3, T4, T5, T6
// 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;
/// <summary>
/// Command handlers for binary reachability CLI commands.
/// </summary>
internal static class BinaryCommandHandlers
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
/// <summary>
/// Handle 'stella binary submit' command.
/// </summary>
public static async Task<int> HandleSubmitAsync(
IServiceProvider services,
string? graphPath,
string? binaryPath,
bool analyze,
bool sign,
string? registry,
bool verbose,
CancellationToken cancellationToken)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("binary-submit");
if (string.IsNullOrWhiteSpace(graphPath) && string.IsNullOrWhiteSpace(binaryPath))
{
AnsiConsole.MarkupLine("[red]Error:[/] Either --graph or --binary must be specified.");
return ExitCodes.InvalidArguments;
}
if (analyze && string.IsNullOrWhiteSpace(binaryPath))
{
AnsiConsole.MarkupLine("[red]Error:[/] --analyze requires --binary.");
return ExitCodes.InvalidArguments;
}
try
{
await AnsiConsole.Status()
.StartAsync("Submitting binary graph...", async ctx =>
{
if (analyze)
{
ctx.Status("Analyzing binary...");
AnsiConsole.MarkupLine($"[yellow]Analyzing binary:[/] {binaryPath}");
// TODO: Invoke binary analysis service
await Task.Delay(100, cancellationToken);
}
if (!string.IsNullOrWhiteSpace(graphPath))
{
ctx.Status($"Reading graph from {graphPath}...");
if (!File.Exists(graphPath))
{
throw new FileNotFoundException($"Graph file not found: {graphPath}");
}
var graphJson = await File.ReadAllTextAsync(graphPath, cancellationToken);
AnsiConsole.MarkupLine($"[green]✓[/] Graph loaded: {graphJson.Length} bytes");
}
if (sign)
{
ctx.Status("Signing graph with DSSE...");
AnsiConsole.MarkupLine("[yellow]Signing:[/] Generating DSSE attestation");
// TODO: Invoke signing service
await Task.Delay(100, cancellationToken);
}
if (!string.IsNullOrWhiteSpace(registry))
{
ctx.Status($"Pushing to {registry}...");
AnsiConsole.MarkupLine($"[yellow]Pushing:[/] {registry}");
// TODO: Invoke OCI push service
await Task.Delay(100, cancellationToken);
}
ctx.Status("Submitting to Scanner API...");
// TODO: Invoke Scanner API
await Task.Delay(100, cancellationToken);
});
var mockDigest = "blake3:abc123def456789...";
AnsiConsole.MarkupLine($"[green]✓ Graph submitted successfully[/]");
AnsiConsole.MarkupLine($" Digest: [cyan]{mockDigest}[/]");
if (verbose)
{
logger.LogInformation(
"Binary graph submitted: graph={GraphPath}, binary={BinaryPath}, sign={Sign}",
graphPath,
binaryPath,
sign);
}
return ExitCodes.Success;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
logger.LogError(ex, "Failed to submit binary graph");
return ExitCodes.GeneralError;
}
}
/// <summary>
/// Handle 'stella binary info' command.
/// </summary>
public static async Task<int> HandleInfoAsync(
IServiceProvider services,
string hash,
string format,
bool verbose,
CancellationToken cancellationToken)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("binary-info");
try
{
// TODO: Query Scanner API for graph info
await Task.Delay(50, cancellationToken);
var mockInfo = new
{
Digest = hash,
Format = "ELF x86_64",
BuildId = "gnu-build-id:5f0c7c3c...",
Nodes = 1247,
Edges = 3891,
Entrypoints = 5,
Attestation = "Signed (Rekor #12345678)"
};
if (format == "json")
{
var json = JsonSerializer.Serialize(mockInfo, JsonOptions);
AnsiConsole.WriteLine(json);
}
else
{
AnsiConsole.MarkupLine($"[bold]Binary Graph:[/] {mockInfo.Digest}");
AnsiConsole.MarkupLine($"Format: {mockInfo.Format}");
AnsiConsole.MarkupLine($"Build-ID: {mockInfo.BuildId}");
AnsiConsole.MarkupLine($"Nodes: [cyan]{mockInfo.Nodes}[/]");
AnsiConsole.MarkupLine($"Edges: [cyan]{mockInfo.Edges}[/]");
AnsiConsole.MarkupLine($"Entrypoints: [cyan]{mockInfo.Entrypoints}[/]");
AnsiConsole.MarkupLine($"Attestation: [green]{mockInfo.Attestation}[/]");
}
if (verbose)
{
logger.LogInformation("Retrieved graph info for {Hash}", hash);
}
return ExitCodes.Success;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
logger.LogError(ex, "Failed to retrieve graph info for {Hash}", hash);
return ExitCodes.GeneralError;
}
}
/// <summary>
/// Handle 'stella binary symbols' command.
/// </summary>
public static async Task<int> HandleSymbolsAsync(
IServiceProvider services,
string hash,
bool strippedOnly,
bool exportedOnly,
bool entrypointsOnly,
string? search,
string format,
int limit,
bool verbose,
CancellationToken cancellationToken)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("binary-symbols");
try
{
// TODO: Query Scanner API for symbols
await Task.Delay(50, cancellationToken);
var mockSymbols = new[]
{
new { Symbol = "main", Type = "entrypoint", Exported = true, Stripped = false },
new { Symbol = "ssl_connect", Type = "function", Exported = true, Stripped = false },
new { Symbol = "verify_cert", Type = "function", Exported = false, Stripped = false },
new { Symbol = "sub_401234", Type = "function", Exported = false, Stripped = true }
};
var filtered = mockSymbols.AsEnumerable();
if (strippedOnly)
filtered = filtered.Where(s => s.Stripped);
if (exportedOnly)
filtered = filtered.Where(s => s.Exported);
if (entrypointsOnly)
filtered = filtered.Where(s => s.Type == "entrypoint");
if (!string.IsNullOrWhiteSpace(search))
{
var pattern = search.Replace("*", ".*");
filtered = filtered.Where(s => System.Text.RegularExpressions.Regex.IsMatch(s.Symbol, pattern));
}
var results = filtered.Take(limit).ToArray();
if (format == "json")
{
var json = JsonSerializer.Serialize(results, JsonOptions);
AnsiConsole.WriteLine(json);
}
else
{
var table = new Table();
table.AddColumn("Symbol");
table.AddColumn("Type");
table.AddColumn("Exported");
table.AddColumn("Stripped");
foreach (var sym in results)
{
table.AddRow(
sym.Symbol,
sym.Type,
sym.Exported ? "[green]yes[/]" : "no",
sym.Stripped ? "[yellow]yes[/]" : "no");
}
AnsiConsole.Write(table);
AnsiConsole.MarkupLine($"\n[dim]Showing {results.Length} symbols (limit: {limit})[/]");
}
if (verbose)
{
logger.LogInformation(
"Retrieved {Count} symbols for {Hash}",
results.Length,
hash);
}
return ExitCodes.Success;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
logger.LogError(ex, "Failed to retrieve symbols for {Hash}", hash);
return ExitCodes.GeneralError;
}
}
/// <summary>
/// Handle 'stella binary verify' command.
/// </summary>
public static async Task<int> HandleVerifyAsync(
IServiceProvider services,
string graphPath,
string dssePath,
string? publicKey,
string? rekorUrl,
bool verbose,
CancellationToken cancellationToken)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("binary-verify");
try
{
if (!File.Exists(graphPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Graph file not found: {graphPath}");
return ExitCodes.FileNotFound;
}
if (!File.Exists(dssePath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] DSSE envelope not found: {dssePath}");
return ExitCodes.FileNotFound;
}
await AnsiConsole.Status()
.StartAsync("Verifying attestation...", async ctx =>
{
ctx.Status("Parsing DSSE envelope...");
await Task.Delay(50, cancellationToken);
ctx.Status("Verifying signature...");
// TODO: Invoke signature verification
await Task.Delay(100, cancellationToken);
ctx.Status("Verifying graph digest...");
// TODO: Verify graph hash matches predicate
await Task.Delay(50, cancellationToken);
if (!string.IsNullOrWhiteSpace(rekorUrl))
{
ctx.Status("Verifying Rekor inclusion...");
// TODO: Verify Rekor transparency log
await Task.Delay(100, cancellationToken);
}
});
AnsiConsole.MarkupLine("[green]✓ Verification successful[/]");
AnsiConsole.MarkupLine(" Signature: [green]Valid[/]");
AnsiConsole.MarkupLine(" Graph digest: [green]Matches[/]");
if (!string.IsNullOrWhiteSpace(rekorUrl))
{
AnsiConsole.MarkupLine($" Rekor: [green]Verified (entry #12345678)[/]");
}
if (verbose)
{
logger.LogInformation(
"Verified graph attestation: graph={GraphPath}, dsse={DssePath}",
graphPath,
dssePath);
}
return ExitCodes.Success;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]✗ Verification failed:[/] {ex.Message}");
logger.LogError(ex, "Failed to verify attestation");
return ExitCodes.VerificationFailed;
}
}
/// <summary>
/// Handle 'stella binary inspect' command (SCANINT-14).
/// </summary>
public static async Task<int> HandleInspectAsync(
IServiceProvider services,
string filePath,
string format,
bool verbose,
CancellationToken cancellationToken)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("binary-inspect");
try
{
if (!File.Exists(filePath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] File not found: {filePath}");
return ExitCodes.FileNotFound;
}
await AnsiConsole.Status()
.StartAsync("Analyzing binary...", async ctx =>
{
await Task.Delay(100, cancellationToken);
});
// Compute file hashes and extract identity
using var stream = File.OpenRead(filePath);
var sha256 = System.Security.Cryptography.SHA256.HashData(stream);
stream.Position = 0;
// Read ELF/PE/Mach-O header to determine format and architecture
var header = new byte[64];
await stream.ReadExactlyAsync(header, cancellationToken);
var binaryFormat = DetectFormat(header);
var architecture = DetectArchitecture(header, binaryFormat);
var buildId = ExtractBuildId(filePath); // Placeholder
var fileInfo = new FileInfo(filePath);
var result = new
{
Path = filePath,
Size = fileInfo.Length,
Format = binaryFormat,
Architecture = architecture,
BuildId = buildId ?? "(not found)",
Sha256 = Convert.ToHexStringLower(sha256),
BinaryKey = buildId ?? Convert.ToHexStringLower(sha256[..16])
};
if (format == "json")
{
var json = JsonSerializer.Serialize(result, JsonOptions);
AnsiConsole.WriteLine(json);
}
else
{
AnsiConsole.MarkupLine($"[bold]Binary:[/] {result.Path}");
AnsiConsole.MarkupLine($"Size: {result.Size:N0} bytes");
AnsiConsole.MarkupLine($"Format: [cyan]{result.Format}[/]");
AnsiConsole.MarkupLine($"Architecture: [cyan]{result.Architecture}[/]");
AnsiConsole.MarkupLine($"Build-ID: [cyan]{result.BuildId}[/]");
AnsiConsole.MarkupLine($"SHA256: [dim]{result.Sha256}[/]");
AnsiConsole.MarkupLine($"Binary Key: [green]{result.BinaryKey}[/]");
}
if (verbose)
{
logger.LogInformation("Inspected binary: {Path}", filePath);
}
return ExitCodes.Success;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
logger.LogError(ex, "Failed to inspect binary {Path}", filePath);
return ExitCodes.GeneralError;
}
}
/// <summary>
/// Handle 'stella binary lookup' command (SCANINT-15).
/// </summary>
public static async Task<int> HandleLookupAsync(
IServiceProvider services,
string buildId,
string? distro,
string? release,
string format,
bool verbose,
CancellationToken cancellationToken)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("binary-lookup");
try
{
await AnsiConsole.Status()
.StartAsync("Looking up vulnerabilities...", async ctx =>
{
// TODO: Call BinaryIndex API
await Task.Delay(100, cancellationToken);
});
// Mock results for now - in production, call IBinaryVulnerabilityService
var mockResults = new[]
{
new
{
CveId = "CVE-2024-1234",
Purl = "pkg:deb/debian/openssl@1.1.1n-0+deb11u3",
Method = "buildid_catalog",
Confidence = 0.95,
FixStatus = distro != null ? "fixed" : "unknown",
FixedVersion = distro != null ? "1.1.1n-0+deb11u4" : null
}
};
if (format == "json")
{
var json = JsonSerializer.Serialize(new
{
BuildId = buildId,
Distro = distro,
Release = release,
Matches = mockResults
}, JsonOptions);
AnsiConsole.WriteLine(json);
}
else
{
AnsiConsole.MarkupLine($"[bold]Build-ID:[/] {buildId}");
if (distro != null)
{
AnsiConsole.MarkupLine($"Distro: {distro}/{release ?? "any"}");
}
AnsiConsole.MarkupLine("");
if (mockResults.Length == 0)
{
AnsiConsole.MarkupLine("[green]No vulnerabilities found[/]");
}
else
{
var table = new Table();
table.AddColumn("CVE");
table.AddColumn("Package");
table.AddColumn("Method");
table.AddColumn("Confidence");
table.AddColumn("Fix Status");
foreach (var match in mockResults)
{
var statusMarkup = match.FixStatus switch
{
"fixed" => $"[green]Fixed ({match.FixedVersion})[/]",
"vulnerable" => "[red]Vulnerable[/]",
_ => "[yellow]Unknown[/]"
};
table.AddRow(
match.CveId,
match.Purl,
match.Method,
$"{match.Confidence:P0}",
statusMarkup);
}
AnsiConsole.Write(table);
}
}
if (verbose)
{
logger.LogInformation("Looked up Build-ID: {BuildId}", buildId);
}
return ExitCodes.Success;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
logger.LogError(ex, "Failed to lookup Build-ID {BuildId}", buildId);
return ExitCodes.GeneralError;
}
}
/// <summary>
/// Handle 'stella binary fingerprint' command (SCANINT-16).
/// </summary>
public static async Task<int> HandleFingerprintAsync(
IServiceProvider services,
string filePath,
string algorithm,
string? function,
string format,
bool verbose,
CancellationToken cancellationToken)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("binary-fingerprint");
try
{
if (!File.Exists(filePath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] File not found: {filePath}");
return ExitCodes.FileNotFound;
}
await AnsiConsole.Status()
.StartAsync($"Generating {algorithm} fingerprint...", async ctx =>
{
// TODO: Call actual fingerprinting service
await Task.Delay(200, cancellationToken);
});
// Mock fingerprint generation
using var stream = File.OpenRead(filePath);
var fileHash = System.Security.Cryptography.SHA256.HashData(stream);
// Simulate fingerprint based on algorithm
var fingerprintId = algorithm switch
{
"basic-block" => $"bb:{Convert.ToHexStringLower(fileHash[..16])}",
"cfg" => $"cfg:{Convert.ToHexStringLower(fileHash[..16])}",
"string-refs" => $"str:{Convert.ToHexStringLower(fileHash[..16])}",
_ => $"comb:{Convert.ToHexStringLower(fileHash[..16])}"
};
var result = new
{
File = filePath,
Algorithm = algorithm,
Function = function,
FingerprintId = fingerprintId,
FingerprintHash = Convert.ToHexStringLower(fileHash),
GeneratedAt = (services.GetService<TimeProvider>() ?? TimeProvider.System).GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
};
if (format == "json")
{
var json = JsonSerializer.Serialize(result, JsonOptions);
AnsiConsole.WriteLine(json);
}
else if (format == "hex")
{
AnsiConsole.WriteLine(result.FingerprintHash);
}
else
{
AnsiConsole.MarkupLine($"[bold]Fingerprint:[/] {result.FingerprintId}");
AnsiConsole.MarkupLine($"Algorithm: [cyan]{result.Algorithm}[/]");
if (function != null)
{
AnsiConsole.MarkupLine($"Function: [cyan]{function}[/]");
}
AnsiConsole.MarkupLine($"Hash: [dim]{result.FingerprintHash}[/]");
AnsiConsole.MarkupLine($"Generated: {result.GeneratedAt}");
}
if (verbose)
{
logger.LogInformation(
"Generated fingerprint for {Path} using {Algorithm}",
filePath,
algorithm);
}
return ExitCodes.Success;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
logger.LogError(ex, "Failed to fingerprint {Path}", filePath);
return ExitCodes.GeneralError;
}
}
/// <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 timeProvider = services.GetService<TimeProvider>() ?? TimeProvider.System;
var effectiveScanId = scanId ?? $"cli-{Path.GetFileName(filePath)}-{timeProvider.GetUtcNow():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'
if (header[0] == 0x7f && header[1] == 'E' && header[2] == 'L' && header[3] == 'F')
return "ELF";
// PE magic: 'M' 'Z'
if (header[0] == 'M' && header[1] == 'Z')
return "PE";
// Mach-O magic
if ((header[0] == 0xfe && header[1] == 0xed && header[2] == 0xfa && header[3] == 0xce) ||
(header[0] == 0xfe && header[1] == 0xed && header[2] == 0xfa && header[3] == 0xcf) ||
(header[0] == 0xcf && header[1] == 0xfa && header[2] == 0xed && header[3] == 0xfe) ||
(header[0] == 0xce && header[1] == 0xfa && header[2] == 0xed && header[3] == 0xfe))
return "Mach-O";
return "Unknown";
}
private static string DetectArchitecture(byte[] header, string format)
{
if (format == "ELF" && header.Length >= 19)
{
return header[18] switch
{
0x03 => "x86",
0x3e => "x86_64",
0xb7 => "aarch64",
0x28 => "arm",
_ => "unknown"
};
}
if (format == "PE")
{
return "x86/x86_64"; // Would need to parse PE header properly
}
if (format == "Mach-O")
{
// Check for 64-bit magic
if (header[3] == 0xcf || header[0] == 0xcf)
return "x86_64/aarch64";
return "x86/arm";
}
return "unknown";
}
private static string? ExtractBuildId(string filePath)
{
// In production, this would parse ELF .note.gnu.build-id section
// For now, return null
return null;
}
}
internal static class ExitCodes
{
public const int Success = 0;
public const int GeneralError = 1;
public const int InvalidArguments = 2;
public const int FileNotFound = 3;
public const int VerificationFailed = 4;
}