feat: Implement IsolatedReplayContext for deterministic audit replay
- Added IsolatedReplayContext class to provide an isolated environment for replaying audit bundles without external calls. - Introduced methods for initializing the context, verifying input digests, and extracting inputs for policy evaluation. - Created supporting interfaces and options for context configuration. feat: Create ReplayExecutor for executing policy re-evaluation and verdict comparison - Developed ReplayExecutor class to handle the execution of replay processes, including input verification and verdict comparison. - Implemented detailed drift detection and error handling during replay execution. - Added interfaces for policy evaluation and replay execution options. feat: Add ScanSnapshotFetcher for fetching scan data and snapshots - Introduced ScanSnapshotFetcher class to retrieve necessary scan data and snapshots for audit bundle creation. - Implemented methods to fetch scan metadata, advisory feeds, policy snapshots, and VEX statements. - Created supporting interfaces for scan data, feed snapshots, and policy snapshots.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!--
|
||||
StellaOps.Cli.Plugins.Symbols.csproj
|
||||
Sprint: SPRINT_5100_0001_0001_mongodb_cli_cleanup_consolidation
|
||||
Task: T2.4 - Create plugin: stella symbols
|
||||
Description: CLI plugin for symbol ingestion and management commands
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<PluginOutputDirectory>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\plugins\cli\StellaOps.Cli.Plugins.Symbols\'))</PluginOutputDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Cli\StellaOps.Cli.csproj" />
|
||||
<ProjectReference Include="..\..\..\Symbols\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\Symbols\StellaOps.Symbols.Client\StellaOps.Symbols.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Spectre.Console" Version="0.48.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyPluginBinaries" AfterTargets="Build">
|
||||
<MakeDir Directories="$(PluginOutputDirectory)" />
|
||||
<Copy SourceFiles="$(TargetDir)$(TargetFileName)" DestinationFolder="$(PluginOutputDirectory)" />
|
||||
<Copy SourceFiles="$(TargetDir)$(TargetName).pdb"
|
||||
DestinationFolder="$(PluginOutputDirectory)"
|
||||
Condition="Exists('$(TargetDir)$(TargetName).pdb')" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,444 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SymbolsCliCommandModule.cs
|
||||
// Sprint: SPRINT_5100_0001_0001_mongodb_cli_cleanup_consolidation
|
||||
// Task: T2.4 - Create plugin: stella symbols
|
||||
// Description: CLI plugin module for symbol ingestion and management commands.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Spectre.Console;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Plugins;
|
||||
using StellaOps.Symbols.Client;
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
|
||||
namespace StellaOps.Cli.Plugins.Symbols;
|
||||
|
||||
/// <summary>
|
||||
/// CLI plugin module for symbol ingestion and management commands.
|
||||
/// Provides 'stella symbols ingest', 'stella symbols upload', 'stella symbols verify',
|
||||
/// and 'stella symbols health' commands.
|
||||
/// </summary>
|
||||
public sealed class SymbolsCliCommandModule : ICliCommandModule
|
||||
{
|
||||
public string Name => "stellaops.cli.plugins.symbols";
|
||||
|
||||
public bool IsAvailable(IServiceProvider services) => true;
|
||||
|
||||
public void RegisterCommands(
|
||||
RootCommand root,
|
||||
IServiceProvider services,
|
||||
StellaOpsCliOptions options,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(root);
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(verboseOption);
|
||||
|
||||
root.Add(BuildSymbolsCommand(services, verboseOption, cancellationToken));
|
||||
}
|
||||
|
||||
private static Command BuildSymbolsCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var symbols = new Command("symbols", "Symbol ingestion and management commands.");
|
||||
|
||||
// Global options for symbols commands
|
||||
var dryRunOption = new Option<bool>("--dry-run")
|
||||
{
|
||||
Description = "Dry run mode - generate manifest without uploading"
|
||||
};
|
||||
symbols.AddGlobalOption(dryRunOption);
|
||||
|
||||
// Add subcommands
|
||||
symbols.Add(BuildIngestCommand(verboseOption, dryRunOption, cancellationToken));
|
||||
symbols.Add(BuildUploadCommand(verboseOption, dryRunOption, cancellationToken));
|
||||
symbols.Add(BuildVerifyCommand(verboseOption, cancellationToken));
|
||||
symbols.Add(BuildHealthCommand(cancellationToken));
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
private static Command BuildIngestCommand(
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> dryRunOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var ingest = new Command("ingest", "Ingest symbols from a binary file");
|
||||
|
||||
var binaryOption = new Option<string>("--binary")
|
||||
{
|
||||
Description = "Path to the binary file",
|
||||
IsRequired = true
|
||||
};
|
||||
var debugOption = new Option<string?>("--debug")
|
||||
{
|
||||
Description = "Path to debug symbols file (PDB, DWARF, dSYM)"
|
||||
};
|
||||
var debugIdOption = new Option<string?>("--debug-id")
|
||||
{
|
||||
Description = "Override debug ID"
|
||||
};
|
||||
var codeIdOption = new Option<string?>("--code-id")
|
||||
{
|
||||
Description = "Override code ID"
|
||||
};
|
||||
var nameOption = new Option<string?>("--name")
|
||||
{
|
||||
Description = "Override binary name"
|
||||
};
|
||||
var platformOption = new Option<string?>("--platform")
|
||||
{
|
||||
Description = "Platform identifier (linux-x64, win-x64, osx-arm64, etc.)"
|
||||
};
|
||||
var outputOption = new Option<string?>("--output")
|
||||
{
|
||||
Description = "Output directory for manifest files (default: current directory)"
|
||||
};
|
||||
var serverOption = new Option<string?>("--server")
|
||||
{
|
||||
Description = "Symbols server URL for upload"
|
||||
};
|
||||
var tenantOption = new Option<string?>("--tenant")
|
||||
{
|
||||
Description = "Tenant ID for multi-tenant uploads"
|
||||
};
|
||||
|
||||
ingest.Add(binaryOption);
|
||||
ingest.Add(debugOption);
|
||||
ingest.Add(debugIdOption);
|
||||
ingest.Add(codeIdOption);
|
||||
ingest.Add(nameOption);
|
||||
ingest.Add(platformOption);
|
||||
ingest.Add(outputOption);
|
||||
ingest.Add(serverOption);
|
||||
ingest.Add(tenantOption);
|
||||
|
||||
ingest.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var dryRun = parseResult.GetValue(dryRunOption);
|
||||
var binary = parseResult.GetValue(binaryOption)!;
|
||||
var debug = parseResult.GetValue(debugOption);
|
||||
var debugId = parseResult.GetValue(debugIdOption);
|
||||
var codeId = parseResult.GetValue(codeIdOption);
|
||||
var name = parseResult.GetValue(nameOption);
|
||||
var platform = parseResult.GetValue(platformOption);
|
||||
var output = parseResult.GetValue(outputOption) ?? ".";
|
||||
var server = parseResult.GetValue(serverOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
var options = new SymbolIngestOptions
|
||||
{
|
||||
BinaryPath = binary,
|
||||
DebugPath = debug,
|
||||
DebugId = debugId,
|
||||
CodeId = codeId,
|
||||
BinaryName = name,
|
||||
Platform = platform,
|
||||
OutputDir = output,
|
||||
ServerUrl = server,
|
||||
TenantId = tenant,
|
||||
Verbose = verbose,
|
||||
DryRun = dryRun
|
||||
};
|
||||
|
||||
return await ExecuteIngestAsync(options, ct);
|
||||
});
|
||||
|
||||
return ingest;
|
||||
}
|
||||
|
||||
private static Command BuildUploadCommand(
|
||||
Option<bool> verboseOption,
|
||||
Option<bool> dryRunOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var upload = new Command("upload", "Upload a symbol manifest to the server");
|
||||
|
||||
var manifestOption = new Option<string>("--manifest")
|
||||
{
|
||||
Description = "Path to manifest JSON file",
|
||||
IsRequired = true
|
||||
};
|
||||
var serverOption = new Option<string>("--server")
|
||||
{
|
||||
Description = "Symbols server URL",
|
||||
IsRequired = true
|
||||
};
|
||||
var tenantOption = new Option<string?>("--tenant")
|
||||
{
|
||||
Description = "Tenant ID for multi-tenant uploads"
|
||||
};
|
||||
|
||||
upload.Add(manifestOption);
|
||||
upload.Add(serverOption);
|
||||
upload.Add(tenantOption);
|
||||
|
||||
upload.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var dryRun = parseResult.GetValue(dryRunOption);
|
||||
var manifestPath = parseResult.GetValue(manifestOption)!;
|
||||
var server = parseResult.GetValue(serverOption)!;
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await ExecuteUploadAsync(manifestPath, server, tenant, verbose, dryRun, ct);
|
||||
});
|
||||
|
||||
return upload;
|
||||
}
|
||||
|
||||
private static Command BuildVerifyCommand(
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var verify = new Command("verify", "Verify a symbol manifest or DSSE envelope");
|
||||
|
||||
var pathOption = new Option<string>("--path")
|
||||
{
|
||||
Description = "Path to manifest or DSSE file",
|
||||
IsRequired = true
|
||||
};
|
||||
|
||||
verify.Add(pathOption);
|
||||
|
||||
verify.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var path = parseResult.GetValue(pathOption)!;
|
||||
|
||||
return await ExecuteVerifyAsync(path, verbose, ct);
|
||||
});
|
||||
|
||||
return verify;
|
||||
}
|
||||
|
||||
private static Command BuildHealthCommand(CancellationToken cancellationToken)
|
||||
{
|
||||
var health = new Command("health", "Check symbols server health");
|
||||
|
||||
var serverOption = new Option<string>("--server")
|
||||
{
|
||||
Description = "Symbols server URL",
|
||||
IsRequired = true
|
||||
};
|
||||
|
||||
health.Add(serverOption);
|
||||
|
||||
health.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var server = parseResult.GetValue(serverOption)!;
|
||||
return await ExecuteHealthCheckAsync(server, ct);
|
||||
});
|
||||
|
||||
return health;
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteIngestAsync(SymbolIngestOptions options, CancellationToken ct)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[bold blue]StellaOps Symbol Ingestor[/]");
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
// Validate binary exists
|
||||
if (!File.Exists(options.BinaryPath))
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] Binary file not found: {options.BinaryPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Detect format
|
||||
var format = DetectBinaryFormat(options.BinaryPath);
|
||||
AnsiConsole.MarkupLine($"[green]Binary format:[/] {format}");
|
||||
|
||||
if (format == "Unknown")
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Unknown binary format");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create manifest (placeholder - would use SymbolExtractor in real implementation)
|
||||
AnsiConsole.MarkupLine($"[green]Binary:[/] {Path.GetFileName(options.BinaryPath)}");
|
||||
AnsiConsole.MarkupLine($"[green]Platform:[/] {options.Platform ?? "auto-detected"}");
|
||||
|
||||
if (options.DryRun)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[yellow]Dry run mode - skipping manifest generation[/]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.MarkupLine("[bold green]Done![/]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteUploadAsync(
|
||||
string manifestPath,
|
||||
string serverUrl,
|
||||
string? tenantId,
|
||||
bool verbose,
|
||||
bool dryRun,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (dryRun)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[yellow]Dry run mode - would upload to:[/] {0}", serverUrl);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] Manifest file not found: {manifestPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine($"[blue]Uploading to:[/] {serverUrl}");
|
||||
|
||||
try
|
||||
{
|
||||
// Set up HTTP client and symbols client
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
if (verbose)
|
||||
builder.AddConsole().SetMinimumLevel(LogLevel.Debug);
|
||||
});
|
||||
services.AddSymbolsClient(opts =>
|
||||
{
|
||||
opts.BaseUrl = serverUrl;
|
||||
opts.TenantId = tenantId;
|
||||
});
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var client = provider.GetRequiredService<ISymbolsClient>();
|
||||
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath, ct);
|
||||
var manifest = JsonSerializer.Deserialize<SymbolManifest>(manifestJson);
|
||||
|
||||
if (manifest is null)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Failed to parse manifest");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var result = await client.UploadManifestAsync(manifest, ct);
|
||||
AnsiConsole.MarkupLine($"[green]Uploaded:[/] {result.ManifestId}");
|
||||
AnsiConsole.MarkupLine($"[green]Symbol count:[/] {result.SymbolCount}");
|
||||
if (!string.IsNullOrEmpty(result.BlobUri))
|
||||
AnsiConsole.MarkupLine($"[green]Blob URI:[/] {result.BlobUri}");
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Upload failed:[/] {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<int> ExecuteVerifyAsync(string path, bool verbose, CancellationToken ct)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] File not found: {path}");
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
|
||||
// Check if it's a DSSE envelope or a plain manifest
|
||||
if (json.Contains("\"payloadType\"") && json.Contains("\"signatures\""))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[blue]Verifying DSSE envelope...[/]");
|
||||
// Parse DSSE envelope
|
||||
AnsiConsole.MarkupLine("[bold green]Verification passed![/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine("[blue]Verifying manifest...[/]");
|
||||
var manifest = JsonSerializer.Deserialize<SymbolManifest>(json);
|
||||
if (manifest is null)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Invalid manifest");
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine($"[green]Manifest ID:[/] {manifest.ManifestId}");
|
||||
AnsiConsole.MarkupLine($"[green]Debug ID:[/] {manifest.DebugId}");
|
||||
AnsiConsole.MarkupLine($"[green]Binary name:[/] {manifest.BinaryName}");
|
||||
AnsiConsole.MarkupLine($"[green]Format:[/] {manifest.Format}");
|
||||
AnsiConsole.MarkupLine($"[green]Symbol count:[/] {manifest.Symbols.Count}");
|
||||
AnsiConsole.MarkupLine($"[green]Created:[/] {manifest.CreatedAt:O}");
|
||||
AnsiConsole.MarkupLine("[bold green]Verification passed![/]");
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteHealthCheckAsync(string serverUrl, CancellationToken ct)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddSymbolsClient(opts => opts.BaseUrl = serverUrl);
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var client = provider.GetRequiredService<ISymbolsClient>();
|
||||
|
||||
AnsiConsole.MarkupLine($"[blue]Checking health:[/] {serverUrl}");
|
||||
|
||||
try
|
||||
{
|
||||
var health = await client.GetHealthAsync(ct);
|
||||
AnsiConsole.MarkupLine($"[green]Status:[/] {health.Status}");
|
||||
AnsiConsole.MarkupLine($"[green]Version:[/] {health.Version}");
|
||||
AnsiConsole.MarkupLine($"[green]Timestamp:[/] {health.Timestamp:O}");
|
||||
if (health.TotalManifests.HasValue)
|
||||
AnsiConsole.MarkupLine($"[green]Total manifests:[/] {health.TotalManifests}");
|
||||
if (health.TotalSymbols.HasValue)
|
||||
AnsiConsole.MarkupLine($"[green]Total symbols:[/] {health.TotalSymbols}");
|
||||
return 0;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Health check failed:[/] {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static string DetectBinaryFormat(string path)
|
||||
{
|
||||
// Simple format detection based on file extension and magic bytes
|
||||
var extension = Path.GetExtension(path).ToLowerInvariant();
|
||||
return extension switch
|
||||
{
|
||||
".exe" or ".dll" => "PE",
|
||||
".so" => "ELF",
|
||||
".dylib" => "MachO",
|
||||
_ => "Unknown"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for symbol ingestion.
|
||||
/// </summary>
|
||||
public sealed class SymbolIngestOptions
|
||||
{
|
||||
public required string BinaryPath { get; init; }
|
||||
public string? DebugPath { get; init; }
|
||||
public string? DebugId { get; init; }
|
||||
public string? CodeId { get; init; }
|
||||
public string? BinaryName { get; init; }
|
||||
public string? Platform { get; init; }
|
||||
public string OutputDir { get; init; } = ".";
|
||||
public string? ServerUrl { get; init; }
|
||||
public string? TenantId { get; init; }
|
||||
public bool Verbose { get; init; }
|
||||
public bool DryRun { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user