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 StellaOps.Scanner.Sbomer.BuildXPlugin.Attestation; using StellaOps.Scanner.Sbomer.BuildXPlugin.Cas; using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor; using StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest; 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 casClient = new LocalCasClient(new LocalCasOptions { RootDirectory = casRoot, Algorithm = "sha256" }); 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)."); 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 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"); 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); 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); } var json = JsonSerializer.Serialize(document, DescriptorJsonOptions); Console.WriteLine(json); return 0; } 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; } }