// ----------------------------------------------------------------------------- // 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; /// /// Command handlers for binary reachability CLI commands. /// internal static class BinaryCommandHandlers { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true }; /// /// Handle 'stella binary submit' command. /// public static async Task HandleSubmitAsync( IServiceProvider services, string? graphPath, string? binaryPath, bool analyze, bool sign, string? registry, bool verbose, CancellationToken cancellationToken) { var loggerFactory = services.GetRequiredService(); 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; } } /// /// Handle 'stella binary info' command. /// public static async Task HandleInfoAsync( IServiceProvider services, string hash, string format, bool verbose, CancellationToken cancellationToken) { var loggerFactory = services.GetRequiredService(); 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; } } /// /// Handle 'stella binary symbols' command. /// public static async Task 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(); 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; } } /// /// Handle 'stella binary verify' command. /// public static async Task HandleVerifyAsync( IServiceProvider services, string graphPath, string dssePath, string? publicKey, string? rekorUrl, bool verbose, CancellationToken cancellationToken) { var loggerFactory = services.GetRequiredService(); 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; } } /// /// Handle 'stella binary inspect' command (SCANINT-14). /// public static async Task HandleInspectAsync( IServiceProvider services, string filePath, string format, bool verbose, CancellationToken cancellationToken) { var loggerFactory = services.GetRequiredService(); 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; } } /// /// Handle 'stella binary lookup' command (SCANINT-15). /// public static async Task HandleLookupAsync( IServiceProvider services, string buildId, string? distro, string? release, string format, bool verbose, CancellationToken cancellationToken) { var loggerFactory = services.GetRequiredService(); 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; } } /// /// Handle 'stella binary fingerprint' command (SCANINT-16). /// public static async Task HandleFingerprintAsync( IServiceProvider services, string filePath, string algorithm, string? function, string format, bool verbose, CancellationToken cancellationToken) { var loggerFactory = services.GetRequiredService(); 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.System).GetUtcNow().ToString("O") }; 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; } } /// /// Handle 'stella binary callgraph' command (CALLGRAPH-01). /// Extracts call graph from native binary and computes deterministic SHA-256 digest. /// public static async Task HandleCallGraphAsync( IServiceProvider services, string filePath, string format, string? outputPath, string? emitSbomPath, string? scanId, bool verbose, CancellationToken cancellationToken) { var loggerFactory = services.GetRequiredService(); 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.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.System; var extractorLogger = loggerFactory.CreateLogger(); 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; } } /// /// Injects callgraph digest as a property into a CycloneDX SBOM. /// private static async Task 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; }