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:
StellaOps Bot
2025-12-23 07:46:34 +02:00
parent e47627cfff
commit 7e384ab610
77 changed files with 153346 additions and 209 deletions

View File

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

View File

@@ -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; }
}