using System; using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json.Serialization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StellaOps.Cryptography; using StellaOps.Scanner.Sbomer.BuildXPlugin.Attestation; using StellaOps.Scanner.Sbomer.BuildXPlugin.Cas; using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor; using StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest; using StellaOps.Scanner.Sbomer.BuildXPlugin.Surface; using StellaOps.Scanner.Surface.Env; using StellaOps.Scanner.Surface.Secrets; namespace StellaOps.Scanner.Sbomer.BuildXPlugin; internal static class Program { private static readonly JsonSerializerOptions ManifestPrintOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private static readonly JsonSerializerOptions DescriptorJsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private static async Task Main(string[] args) { using var cancellation = new CancellationTokenSource(); Console.CancelKeyPress += (_, eventArgs) => { eventArgs.Cancel = true; cancellation.Cancel(); }; var command = args.Length > 0 ? args[0].ToLowerInvariant() : "handshake"; var commandArgs = args.Skip(1).ToArray(); try { return command switch { "handshake" => await RunHandshakeAsync(commandArgs, cancellation.Token).ConfigureAwait(false), "manifest" => await RunManifestAsync(commandArgs, cancellation.Token).ConfigureAwait(false), "descriptor" or "annotate" => await RunDescriptorAsync(commandArgs, cancellation.Token).ConfigureAwait(false), "version" => RunVersion(), "help" or "--help" or "-h" => PrintHelp(), _ => UnknownCommand(command) }; } catch (OperationCanceledException) { Console.Error.WriteLine("Operation cancelled."); return 130; } catch (BuildxPluginException ex) { Console.Error.WriteLine(ex.Message); return 2; } catch (Exception ex) { Console.Error.WriteLine($"Unhandled error: {ex}"); return 1; } } private static async Task RunHandshakeAsync(string[] args, CancellationToken cancellationToken) { var manifestDirectory = ResolveManifestDirectory(args); var loader = new BuildxPluginManifestLoader(manifestDirectory); var manifest = await loader.LoadDefaultAsync(cancellationToken).ConfigureAwait(false); var casRoot = ResolveCasRoot(args, manifest); var hash = CryptoHashFactory.CreateDefault(); var casClient = new LocalCasClient(new LocalCasOptions { RootDirectory = casRoot, Algorithm = "sha256" }, hash); var result = await casClient.VerifyWriteAsync(cancellationToken).ConfigureAwait(false); Console.WriteLine($"handshake ok: {manifest.Id}@{manifest.Version} → {result.Algorithm}:{result.Digest}"); Console.WriteLine(result.Path); return 0; } private static async Task RunManifestAsync(string[] args, CancellationToken cancellationToken) { var manifestDirectory = ResolveManifestDirectory(args); var loader = new BuildxPluginManifestLoader(manifestDirectory); var manifest = await loader.LoadDefaultAsync(cancellationToken).ConfigureAwait(false); var json = JsonSerializer.Serialize(manifest, ManifestPrintOptions); Console.WriteLine(json); return 0; } private static int RunVersion() { var assembly = Assembly.GetExecutingAssembly(); var version = assembly.GetCustomAttribute()?.InformationalVersion ?? assembly.GetName().Version?.ToString() ?? "unknown"; Console.WriteLine(version); return 0; } private static int PrintHelp() { Console.WriteLine("StellaOps BuildX SBOM generator"); Console.WriteLine("Usage:"); Console.WriteLine(" stellaops-buildx [handshake|manifest|descriptor|version]"); Console.WriteLine(); Console.WriteLine("Commands:"); Console.WriteLine(" handshake Probe the local CAS and ensure manifests are discoverable."); Console.WriteLine(" manifest Print the resolved manifest JSON."); Console.WriteLine(" descriptor Emit OCI descriptor + provenance placeholder for the provided SBOM."); Console.WriteLine(" version Print the plug-in version."); Console.WriteLine(); Console.WriteLine("Options:"); Console.WriteLine(" --manifest Override the manifest directory."); Console.WriteLine(" --cas Override the CAS root directory."); Console.WriteLine(" --image (descriptor) Image digest the SBOM belongs to."); Console.WriteLine(" --sbom (descriptor) Path to the SBOM file to describe."); Console.WriteLine(" --attestor (descriptor) Optional Attestor endpoint for provenance placeholders."); Console.WriteLine(" --attestor-token Bearer token for Attestor requests (or STELLAOPS_ATTESTOR_TOKEN)."); Console.WriteLine(" --attestor-insecure Skip TLS verification for Attestor requests (dev/test only)."); Console.WriteLine(" --surface-layer-fragments Persist layer fragments JSON into Surface.FS."); Console.WriteLine(" --surface-entrytrace-graph Persist EntryTrace graph JSON into Surface.FS."); Console.WriteLine(" --surface-entrytrace-ndjson Persist EntryTrace NDJSON into Surface.FS."); Console.WriteLine(" --surface-cache-root Override Surface cache root (defaults to CAS root)."); Console.WriteLine(" --surface-bucket Bucket name used in Surface CAS URIs (default scanner-artifacts)."); Console.WriteLine(" --surface-tenant Tenant identifier recorded in the Surface manifest."); return 0; } private static int UnknownCommand(string command) { Console.Error.WriteLine($"Unknown command '{command}'. Use 'help' for usage."); return 1; } private static string ResolveManifestDirectory(string[] args) { var explicitPath = GetOption(args, "--manifest") ?? Environment.GetEnvironmentVariable("STELLAOPS_BUILDX_MANIFEST_DIR"); if (!string.IsNullOrWhiteSpace(explicitPath)) { return Path.GetFullPath(explicitPath); } var defaultDirectory = Path.Combine(AppContext.BaseDirectory, "plugins", "scanner", "buildx"); if (Directory.Exists(defaultDirectory)) { return defaultDirectory; } return AppContext.BaseDirectory; } private static string ResolveCasRoot(string[] args, BuildxPluginManifest manifest) { var overrideValue = GetOption(args, "--cas") ?? Environment.GetEnvironmentVariable("STELLAOPS_SCANNER_CAS_ROOT"); if (!string.IsNullOrWhiteSpace(overrideValue)) { return Path.GetFullPath(overrideValue); } var manifestDefault = manifest.Cas.DefaultRoot; if (!string.IsNullOrWhiteSpace(manifestDefault)) { if (Path.IsPathRooted(manifestDefault)) { return Path.GetFullPath(manifestDefault); } var baseDirectory = manifest.SourceDirectory ?? AppContext.BaseDirectory; return Path.GetFullPath(Path.Combine(baseDirectory, manifestDefault)); } return Path.Combine(AppContext.BaseDirectory, "cas"); } private static async Task RunDescriptorAsync(string[] args, CancellationToken cancellationToken) { var manifestDirectory = ResolveManifestDirectory(args); var loader = new BuildxPluginManifestLoader(manifestDirectory); var manifest = await loader.LoadDefaultAsync(cancellationToken).ConfigureAwait(false); var casRoot = ResolveCasRoot(args, manifest); var imageDigest = RequireOption(args, "--image"); var sbomPath = RequireOption(args, "--sbom"); var sbomMediaType = GetOption(args, "--media-type") ?? "application/vnd.cyclonedx+json"; var sbomFormat = GetOption(args, "--sbom-format") ?? "cyclonedx-json"; var sbomKind = GetOption(args, "--sbom-kind") ?? "inventory"; var artifactType = GetOption(args, "--artifact-type") ?? "application/vnd.stellaops.sbom.layer+json"; var subjectMediaType = GetOption(args, "--subject-media-type") ?? "application/vnd.oci.image.manifest.v1+json"; var predicateType = GetOption(args, "--predicate-type") ?? "https://slsa.dev/provenance/v1"; var licenseId = GetOption(args, "--license-id") ?? Environment.GetEnvironmentVariable("STELLAOPS_LICENSE_ID"); var repository = GetOption(args, "--repository"); var buildRef = GetOption(args, "--build-ref"); var sbomName = GetOption(args, "--sbom-name") ?? Path.GetFileName(sbomPath); var attestorUriText = GetOption(args, "--attestor") ?? Environment.GetEnvironmentVariable("STELLAOPS_ATTESTOR_URL"); var attestorToken = GetOption(args, "--attestor-token") ?? Environment.GetEnvironmentVariable("STELLAOPS_ATTESTOR_TOKEN") ?? TryResolveAttestationToken(); // Fallback to Surface.Secrets var attestorInsecure = GetFlag(args, "--attestor-insecure") || string.Equals(Environment.GetEnvironmentVariable("STELLAOPS_ATTESTOR_INSECURE"), "true", StringComparison.OrdinalIgnoreCase); Uri? attestorUri = null; if (!string.IsNullOrWhiteSpace(attestorUriText)) { attestorUri = new Uri(attestorUriText, UriKind.Absolute); } var assembly = Assembly.GetExecutingAssembly(); var version = assembly.GetCustomAttribute()?.InformationalVersion ?? assembly.GetName().Version?.ToString() ?? "0.0.0"; var request = new DescriptorRequest { ImageDigest = imageDigest, SbomPath = sbomPath, SbomMediaType = sbomMediaType, SbomFormat = sbomFormat, SbomKind = sbomKind, SbomArtifactType = artifactType, SubjectMediaType = subjectMediaType, PredicateType = predicateType, GeneratorVersion = version, GeneratorName = assembly.GetName().Name, LicenseId = licenseId, SbomName = sbomName, Repository = repository, BuildRef = buildRef, AttestorUri = attestorUri?.ToString() }.Validate(); var generator = new DescriptorGenerator(TimeProvider.System, CryptoHashFactory.CreateDefault()); var document = await generator.CreateAsync(request, cancellationToken).ConfigureAwait(false); if (attestorUri is not null) { using var httpClient = CreateAttestorHttpClient(attestorUri, attestorToken, attestorInsecure); var attestorClient = new AttestorClient(httpClient); await attestorClient.SendPlaceholderAsync(attestorUri, document, cancellationToken).ConfigureAwait(false); } await TryPublishSurfaceArtifactsAsync(args, request, casRoot, version, cancellationToken).ConfigureAwait(false); var json = JsonSerializer.Serialize(document, DescriptorJsonOptions); Console.WriteLine(json); return 0; } private static async Task TryPublishSurfaceArtifactsAsync( string[] args, DescriptorRequest descriptorRequest, string casRoot, string generatorVersion, CancellationToken cancellationToken) { var surfaceOptions = ResolveSurfaceOptions(args, descriptorRequest, casRoot, generatorVersion); if (surfaceOptions is null || !surfaceOptions.HasArtifacts) { return; } var writer = new SurfaceManifestWriter(TimeProvider.System, CryptoHashFactory.CreateDefault()); var result = await writer.WriteAsync(surfaceOptions, cancellationToken).ConfigureAwait(false); if (result is null) { return; } Console.Error.WriteLine($"surface manifest stored: {result.ManifestUri} ({result.Document.Artifacts.Count} artefacts)"); } private static SurfaceOptions? ResolveSurfaceOptions( string[] args, DescriptorRequest descriptorRequest, string casRoot, string generatorVersion) { var surfaceEnv = TryResolveSurfaceEnvironment(); var layerFragmentsPath = GetOption(args, "--surface-layer-fragments") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_LAYER_FRAGMENTS"); var entryTraceGraphPath = GetOption(args, "--surface-entrytrace-graph") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_ENTRYTRACE_GRAPH"); var entryTraceNdjsonPath = GetOption(args, "--surface-entrytrace-ndjson") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_ENTRYTRACE_NDJSON"); if (string.IsNullOrWhiteSpace(layerFragmentsPath) && string.IsNullOrWhiteSpace(entryTraceGraphPath) && string.IsNullOrWhiteSpace(entryTraceNdjsonPath)) { return null; } var cacheRoot = GetOption(args, "--surface-cache-root") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_CACHE_ROOT") ?? surfaceEnv?.CacheRoot.FullName ?? casRoot; var bucket = GetOption(args, "--surface-bucket") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_BUCKET") ?? surfaceEnv?.SurfaceFsBucket ?? SurfaceCasLayout.DefaultBucket; var rootPrefix = GetOption(args, "--surface-root-prefix") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_ROOT_PREFIX") ?? SurfaceCasLayout.DefaultRootPrefix; var tenant = GetOption(args, "--surface-tenant") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_TENANT") ?? surfaceEnv?.Tenant ?? "default"; var component = GetOption(args, "--surface-component") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_COMPONENT") ?? "scanner.buildx"; var componentVersion = GetOption(args, "--surface-component-version") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_COMPONENT_VERSION") ?? generatorVersion; var workerInstance = GetOption(args, "--surface-worker-instance") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_WORKER_INSTANCE") ?? Environment.MachineName; var attemptValue = GetOption(args, "--surface-attempt") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_ATTEMPT"); var attempt = 1; if (!string.IsNullOrWhiteSpace(attemptValue) && int.TryParse(attemptValue, out var parsedAttempt) && parsedAttempt > 0) { attempt = parsedAttempt; } var scanId = GetOption(args, "--surface-scan-id") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_SCAN_ID") ?? descriptorRequest.SbomName ?? descriptorRequest.ImageDigest; var manifestOutput = GetOption(args, "--surface-manifest-output") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_MANIFEST_OUTPUT"); return new SurfaceOptions( CacheRoot: cacheRoot, CacheBucket: bucket, RootPrefix: rootPrefix, Tenant: tenant, Component: component, ComponentVersion: componentVersion, WorkerInstance: workerInstance, Attempt: attempt, ImageDigest: descriptorRequest.ImageDigest, ScanId: scanId, LayerFragmentsPath: layerFragmentsPath, EntryTraceGraphPath: entryTraceGraphPath, EntryTraceNdjsonPath: entryTraceNdjsonPath, ManifestOutputPath: manifestOutput); } private static SurfaceEnvironmentSettings? TryResolveSurfaceEnvironment() { try { var configuration = new ConfigurationBuilder() .AddEnvironmentVariables() .Build(); var services = new ServiceCollection(); services.AddSingleton(configuration); services.AddLogging(); services.AddSurfaceEnvironment(options => { options.ComponentName = "Scanner.BuildXPlugin"; options.AddPrefix("SCANNER"); options.AddPrefix("SURFACE"); options.RequireSurfaceEndpoint = false; }); using var provider = services.BuildServiceProvider(); var env = provider.GetService(); return env?.Settings; } catch { // Silent fallback to legacy options/env without breaking plugin execution. return null; } } private static string? TryResolveAttestationToken() { try { var configuration = new ConfigurationBuilder() .AddEnvironmentVariables() .Build(); var services = new ServiceCollection(); services.AddSingleton(configuration); services.AddLogging(); services.AddSurfaceEnvironment(options => { options.ComponentName = "Scanner.BuildXPlugin"; options.AddPrefix("SCANNER"); options.AddPrefix("SURFACE"); options.RequireSurfaceEndpoint = false; }); services.AddSurfaceSecrets(options => { options.ComponentName = "Scanner.BuildXPlugin"; options.EnableCaching = true; options.EnableAuditLogging = false; // No need for audit in CLI tool }); using var provider = services.BuildServiceProvider(); var secretProvider = provider.GetService(); var env = provider.GetService(); if (secretProvider is null || env is null) { return null; } var tenant = env.Settings.Secrets.Tenant; var request = new SurfaceSecretRequest( Tenant: tenant, Component: "Scanner.BuildXPlugin", SecretType: "attestation"); using var handle = secretProvider.GetAsync(request).AsTask().GetAwaiter().GetResult(); var secret = SurfaceSecretParser.ParseAttestationSecret(handle); // Return the API key or token for attestor authentication return secret.RekorApiToken; } catch { // Silent fallback - secrets not available via Surface.Secrets return null; } } private static CasAccessSecret? TryResolveCasCredentials() { try { var configuration = new ConfigurationBuilder() .AddEnvironmentVariables() .Build(); var services = new ServiceCollection(); services.AddSingleton(configuration); services.AddLogging(); services.AddSurfaceEnvironment(options => { options.ComponentName = "Scanner.BuildXPlugin"; options.AddPrefix("SCANNER"); options.AddPrefix("SURFACE"); options.RequireSurfaceEndpoint = false; }); services.AddSurfaceSecrets(options => { options.ComponentName = "Scanner.BuildXPlugin"; options.EnableCaching = true; options.EnableAuditLogging = false; // No need for audit in CLI tool }); using var provider = services.BuildServiceProvider(); var secretProvider = provider.GetService(); var env = provider.GetService(); if (secretProvider is null || env is null) { return null; } var tenant = env.Settings.Secrets.Tenant; var request = new SurfaceSecretRequest( Tenant: tenant, Component: "Scanner.BuildXPlugin", SecretType: "cas-access"); using var handle = secretProvider.GetAsync(request).AsTask().GetAwaiter().GetResult(); return SurfaceSecretParser.ParseCasAccessSecret(handle); } catch { // Silent fallback - CAS secrets not available via Surface.Secrets return null; } } private static string? GetOption(string[] args, string optionName) { for (var i = 0; i < args.Length; i++) { var argument = args[i]; if (string.Equals(argument, optionName, StringComparison.OrdinalIgnoreCase)) { if (i + 1 >= args.Length) { throw new BuildxPluginException($"Option '{optionName}' requires a value."); } return args[i + 1]; } if (argument.StartsWith(optionName + "=", StringComparison.OrdinalIgnoreCase)) { return argument[(optionName.Length + 1)..]; } } return null; } private static bool GetFlag(string[] args, string optionName) { foreach (var argument in args) { if (string.Equals(argument, optionName, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static string RequireOption(string[] args, string optionName) { var value = GetOption(args, optionName); if (string.IsNullOrWhiteSpace(value)) { throw new BuildxPluginException($"Option '{optionName}' is required."); } return value; } private static HttpClient CreateAttestorHttpClient(Uri attestorUri, string? bearerToken, bool insecure) { var handler = new HttpClientHandler { CheckCertificateRevocationList = true, }; if (insecure && string.Equals(attestorUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { #pragma warning disable S4830 // Explicitly gated by --attestor-insecure flag/env for dev/test usage. handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; #pragma warning restore S4830 } var client = new HttpClient(handler, disposeHandler: true) { Timeout = TimeSpan.FromSeconds(30) }; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if (!string.IsNullOrWhiteSpace(bearerToken)) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); } return client; } }