Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created CycloneDX and SPDX SBOM files for both reachable and unreachable images. - Added symbols.json detailing function entry and sink points in the WordPress code. - Included runtime traces for function calls in both reachable and unreachable scenarios. - Developed OpenVEX files indicating vulnerability status and justification for both cases. - Updated README for evaluator harness to guide integration with scanner output.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Cas;
|
||||
|
||||
@@ -11,23 +11,25 @@ namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Cas;
|
||||
/// </summary>
|
||||
public sealed class LocalCasClient
|
||||
{
|
||||
private readonly string rootDirectory;
|
||||
private readonly string algorithm;
|
||||
|
||||
public LocalCasClient(LocalCasOptions options)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
algorithm = options.Algorithm.ToLowerInvariant();
|
||||
private readonly string rootDirectory;
|
||||
private readonly string algorithm;
|
||||
private readonly ICryptoHash hash;
|
||||
|
||||
public LocalCasClient(LocalCasOptions options, ICryptoHash hash)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
this.hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
|
||||
algorithm = options.Algorithm.ToLowerInvariant();
|
||||
if (!string.Equals(algorithm, "sha256", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Only the sha256 algorithm is supported.", nameof(options));
|
||||
}
|
||||
|
||||
rootDirectory = Path.GetFullPath(options.RootDirectory);
|
||||
rootDirectory = Path.GetFullPath(options.RootDirectory);
|
||||
}
|
||||
|
||||
public Task<CasWriteResult> VerifyWriteAsync(CancellationToken cancellationToken)
|
||||
@@ -65,10 +67,8 @@ public sealed class LocalCasClient
|
||||
return Path.Combine(rootDirectory, algorithm, prefix, $"{suffix}.bin");
|
||||
}
|
||||
|
||||
private static string ComputeDigest(ReadOnlySpan<byte> content)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[32];
|
||||
SHA256.HashData(content, buffer);
|
||||
return Convert.ToHexString(buffer).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
private string ComputeDigest(ReadOnlySpan<byte> content)
|
||||
{
|
||||
return hash.ComputeHashHex(content, HashAlgorithms.Sha256);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
|
||||
@@ -16,13 +16,14 @@ public sealed class DescriptorGenerator
|
||||
{
|
||||
public const string Schema = "stellaops.buildx.descriptor.v1";
|
||||
|
||||
private readonly TimeProvider timeProvider;
|
||||
|
||||
public DescriptorGenerator(TimeProvider timeProvider)
|
||||
{
|
||||
timeProvider ??= TimeProvider.System;
|
||||
this.timeProvider = timeProvider;
|
||||
}
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ICryptoHash _hash;
|
||||
|
||||
public DescriptorGenerator(TimeProvider timeProvider, ICryptoHash hash)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
}
|
||||
|
||||
public async Task<DescriptorDocument> CreateAsync(DescriptorRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -78,7 +79,7 @@ public sealed class DescriptorGenerator
|
||||
|
||||
return new DescriptorDocument(
|
||||
Schema: Schema,
|
||||
GeneratedAt: timeProvider.GetUtcNow(),
|
||||
GeneratedAt: _timeProvider.GetUtcNow(),
|
||||
Generator: generatorMetadata,
|
||||
Subject: subject,
|
||||
Artifact: artifact,
|
||||
@@ -86,7 +87,7 @@ public sealed class DescriptorGenerator
|
||||
Metadata: metadata);
|
||||
}
|
||||
|
||||
private static string ComputeDeterministicNonce(DescriptorRequest request, FileInfo sbomFile, string sbomDigest)
|
||||
private string ComputeDeterministicNonce(DescriptorRequest request, FileInfo sbomFile, string sbomDigest)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("stellaops.buildx.nonce.v1");
|
||||
@@ -108,45 +109,33 @@ public sealed class DescriptorGenerator
|
||||
builder.AppendLine(request.PredicateType);
|
||||
|
||||
var payload = Encoding.UTF8.GetBytes(builder.ToString());
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(payload, hash);
|
||||
|
||||
var digest = _hash.ComputeHash(payload, HashAlgorithms.Sha256);
|
||||
Span<byte> nonceBytes = stackalloc byte[16];
|
||||
hash[..16].CopyTo(nonceBytes);
|
||||
digest.AsSpan(0, 16).CopyTo(nonceBytes);
|
||||
return Convert.ToHexString(nonceBytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static async Task<string> ComputeFileDigestAsync(FileInfo file, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = new FileStream(
|
||||
file.FullName,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read,
|
||||
bufferSize: 128 * 1024,
|
||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
|
||||
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
|
||||
var buffer = new byte[128 * 1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
|
||||
{
|
||||
hash.AppendData(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
var digest = hash.GetHashAndReset();
|
||||
return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static string ComputeExpectedDsseDigest(string imageDigest, string sbomDigest, string nonce)
|
||||
{
|
||||
var payload = $"{imageDigest}\n{sbomDigest}\n{nonce}";
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(payload);
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(bytes, hash);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
private async Task<string> ComputeFileDigestAsync(FileInfo file, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = new FileStream(
|
||||
file.FullName,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read,
|
||||
bufferSize: 128 * 1024,
|
||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
|
||||
var digest = await _hash.ComputeHashAsync(stream, HashAlgorithms.Sha256, cancellationToken).ConfigureAwait(false);
|
||||
return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private string ComputeExpectedDsseDigest(string imageDigest, string sbomDigest, string nonce)
|
||||
{
|
||||
var payload = $"{imageDigest}\n{sbomDigest}\n{nonce}";
|
||||
var bytes = Encoding.UTF8.GetBytes(payload);
|
||||
var digest = _hash.ComputeHash(bytes, HashAlgorithms.Sha256);
|
||||
return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> BuildArtifactAnnotations(DescriptorRequest request, string nonce, string expectedDsse)
|
||||
{
|
||||
|
||||
@@ -1,438 +1,440 @@
|
||||
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;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Surface;
|
||||
|
||||
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 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<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");
|
||||
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);
|
||||
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);
|
||||
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 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")
|
||||
?? casRoot;
|
||||
var bucket = GetOption(args, "--surface-bucket")
|
||||
?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_BUCKET")
|
||||
?? 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")
|
||||
?? "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 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;
|
||||
}
|
||||
}
|
||||
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.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;
|
||||
|
||||
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");
|
||||
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 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")
|
||||
?? casRoot;
|
||||
var bucket = GetOption(args, "--surface-bucket")
|
||||
?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_BUCKET")
|
||||
?? 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")
|
||||
?? "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 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>StellaOps.Scanner.Sbomer.BuildXPlugin</AssemblyName>
|
||||
<RootNamespace>StellaOps.Scanner.Sbomer.BuildXPlugin</RootNamespace>
|
||||
<Version>0.1.0-alpha</Version>
|
||||
<FileVersion>0.1.0.0</FileVersion>
|
||||
<AssemblyVersion>0.1.0.0</AssemblyVersion>
|
||||
<InformationalVersion>0.1.0-alpha</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>StellaOps.Scanner.Sbomer.BuildXPlugin</AssemblyName>
|
||||
<RootNamespace>StellaOps.Scanner.Sbomer.BuildXPlugin</RootNamespace>
|
||||
<Version>0.1.0-alpha</Version>
|
||||
<FileVersion>0.1.0.0</FileVersion>
|
||||
<AssemblyVersion>0.1.0.0</AssemblyVersion>
|
||||
<InformationalVersion>0.1.0-alpha</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="stellaops.sbom-indexer.manifest.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Surface;
|
||||
|
||||
@@ -67,11 +67,11 @@ internal static class SurfaceCasLayout
|
||||
return $"cas://{normalizedBucket}/{normalizedKey}";
|
||||
}
|
||||
|
||||
public static string ComputeDigest(ReadOnlySpan<byte> content)
|
||||
public static string ComputeDigest(ICryptoHash hash, ReadOnlySpan<byte> content, string algorithmId = HashAlgorithms.Sha256)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(content, hash);
|
||||
return $"{Sha256}:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
var hex = hash.ComputeHashHex(content, algorithmId);
|
||||
var prefix = algorithmId.Equals(HashAlgorithms.Sha256, StringComparison.OrdinalIgnoreCase) ? Sha256 : algorithmId.ToLowerInvariant();
|
||||
return prefix + ":" + hex;
|
||||
}
|
||||
|
||||
public static async Task<string> WriteBytesAsync(string rootDirectory, string objectKey, byte[] bytes, CancellationToken cancellationToken)
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Surface;
|
||||
@@ -19,10 +20,12 @@ internal sealed class SurfaceManifestWriter
|
||||
};
|
||||
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ICryptoHash _hash;
|
||||
|
||||
public SurfaceManifestWriter(TimeProvider timeProvider)
|
||||
public SurfaceManifestWriter(TimeProvider timeProvider, ICryptoHash hash)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
}
|
||||
|
||||
public async Task<SurfaceManifestWriteResult?> WriteAsync(SurfaceOptions options, CancellationToken cancellationToken)
|
||||
@@ -71,7 +74,7 @@ internal sealed class SurfaceManifestWriter
|
||||
View: null,
|
||||
CasKind: SurfaceCasKind.EntryTraceGraph,
|
||||
FilePath: EnsurePath(options.EntryTraceGraphPath!, "EntryTrace graph path is required."));
|
||||
artifacts.Add(await PersistArtifactAsync(descriptor, cacheRoot, bucket, rootPrefix, cancellationToken).ConfigureAwait(false));
|
||||
artifacts.Add(await PersistArtifactAsync(descriptor, cacheRoot, bucket, rootPrefix, _hash, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.EntryTraceNdjsonPath))
|
||||
@@ -83,7 +86,7 @@ internal sealed class SurfaceManifestWriter
|
||||
View: null,
|
||||
CasKind: SurfaceCasKind.EntryTraceNdjson,
|
||||
FilePath: EnsurePath(options.EntryTraceNdjsonPath!, "EntryTrace NDJSON path is required."));
|
||||
artifacts.Add(await PersistArtifactAsync(descriptor, cacheRoot, bucket, rootPrefix, cancellationToken).ConfigureAwait(false));
|
||||
artifacts.Add(await PersistArtifactAsync(descriptor, cacheRoot, bucket, rootPrefix, _hash, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.LayerFragmentsPath))
|
||||
@@ -95,7 +98,7 @@ internal sealed class SurfaceManifestWriter
|
||||
View: "inventory",
|
||||
CasKind: SurfaceCasKind.LayerFragments,
|
||||
FilePath: EnsurePath(options.LayerFragmentsPath!, "Layer fragments path is required."));
|
||||
artifacts.Add(await PersistArtifactAsync(descriptor, cacheRoot, bucket, rootPrefix, cancellationToken).ConfigureAwait(false));
|
||||
artifacts.Add(await PersistArtifactAsync(descriptor, cacheRoot, bucket, rootPrefix, _hash, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
if (artifacts.Count == 0)
|
||||
@@ -127,7 +130,7 @@ internal sealed class SurfaceManifestWriter
|
||||
};
|
||||
|
||||
var manifestBytes = JsonSerializer.SerializeToUtf8Bytes(manifestDocument, ManifestSerializerOptions);
|
||||
var manifestDigest = SurfaceCasLayout.ComputeDigest(manifestBytes);
|
||||
var manifestDigest = SurfaceCasLayout.ComputeDigest(_hash, manifestBytes);
|
||||
var manifestKey = SurfaceCasLayout.BuildObjectKey(rootPrefix, SurfaceCasKind.Manifest, manifestDigest);
|
||||
var manifestPath = await SurfaceCasLayout.WriteBytesAsync(cacheRoot, manifestKey, manifestBytes, cancellationToken).ConfigureAwait(false);
|
||||
var manifestUri = SurfaceCasLayout.BuildCasUri(bucket, manifestKey);
|
||||
@@ -157,6 +160,7 @@ internal sealed class SurfaceManifestWriter
|
||||
string cacheRoot,
|
||||
string bucket,
|
||||
string rootPrefix,
|
||||
ICryptoHash hash,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -167,7 +171,7 @@ internal sealed class SurfaceManifestWriter
|
||||
}
|
||||
|
||||
var content = await File.ReadAllBytesAsync(descriptor.FilePath, cancellationToken).ConfigureAwait(false);
|
||||
var digest = SurfaceCasLayout.ComputeDigest(content);
|
||||
var digest = SurfaceCasLayout.ComputeDigest(hash, content);
|
||||
var objectKey = SurfaceCasLayout.BuildObjectKey(rootPrefix, descriptor.CasKind, digest);
|
||||
var filePath = await SurfaceCasLayout.WriteBytesAsync(cacheRoot, objectKey, content, cancellationToken).ConfigureAwait(false);
|
||||
var uri = SurfaceCasLayout.BuildCasUri(bucket, objectKey);
|
||||
|
||||
Reference in New Issue
Block a user