Files
git.stella-ops.org/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Program.cs
StellaOps Bot 1c6730a1d2
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
up
2025-11-28 00:45:16 +02:00

587 lines
23 KiB
C#

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<int> 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<int> 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<int> 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<AssemblyInformationalVersionAttribute>()?.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 <path> Override the manifest directory.");
Console.WriteLine(" --cas <path> Override the CAS root directory.");
Console.WriteLine(" --image <digest> (descriptor) Image digest the SBOM belongs to.");
Console.WriteLine(" --sbom <path> (descriptor) Path to the SBOM file to describe.");
Console.WriteLine(" --attestor <url> (descriptor) Optional Attestor endpoint for provenance placeholders.");
Console.WriteLine(" --attestor-token <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 <path> Persist layer fragments JSON into Surface.FS.");
Console.WriteLine(" --surface-entrytrace-graph <path> Persist EntryTrace graph JSON into Surface.FS.");
Console.WriteLine(" --surface-entrytrace-ndjson <path> Persist EntryTrace NDJSON into Surface.FS.");
Console.WriteLine(" --surface-cache-root <path> Override Surface cache root (defaults to CAS root).");
Console.WriteLine(" --surface-bucket <name> Bucket name used in Surface CAS URIs (default scanner-artifacts).");
Console.WriteLine(" --surface-tenant <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<int> 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<AssemblyInformationalVersionAttribute>()?.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<IConfiguration>(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<ISurfaceEnvironment>();
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<IConfiguration>(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<ISurfaceSecretProvider>();
var env = provider.GetService<ISurfaceEnvironment>();
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<IConfiguration>(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<ISurfaceSecretProvider>();
var env = provider.GetService<ISurfaceEnvironment>();
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;
}
}