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);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Scanner.Storage;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Options;
|
||||
@@ -76,6 +77,11 @@ public sealed class ScannerWebServiceOptions
|
||||
/// </summary>
|
||||
public EventsOptions Events { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sovereign cryptography configuration for this host.
|
||||
/// </summary>
|
||||
public StellaOpsCryptoOptions Crypto { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Runtime ingestion configuration.
|
||||
/// </summary>
|
||||
|
||||
@@ -48,13 +48,15 @@ builder.Configuration.AddStellaOpsDefaults(options =>
|
||||
|
||||
var contentRoot = builder.Environment.ContentRootPath;
|
||||
|
||||
var bootstrapOptions = builder.Configuration.BindOptions<ScannerWebServiceOptions>(
|
||||
ScannerWebServiceOptions.SectionName,
|
||||
(opts, _) =>
|
||||
{
|
||||
ScannerWebServiceOptionsPostConfigure.Apply(opts, contentRoot);
|
||||
ScannerWebServiceOptionsValidator.Validate(opts);
|
||||
});
|
||||
var bootstrapOptions = builder.Configuration.BindOptions<ScannerWebServiceOptions>(
|
||||
ScannerWebServiceOptions.SectionName,
|
||||
(opts, _) =>
|
||||
{
|
||||
ScannerWebServiceOptionsPostConfigure.Apply(opts, contentRoot);
|
||||
ScannerWebServiceOptionsValidator.Validate(opts);
|
||||
});
|
||||
|
||||
builder.Services.AddStellaOpsCrypto(bootstrapOptions.Crypto);
|
||||
|
||||
builder.Services.AddOptions<ScannerWebServiceOptions>()
|
||||
.Bind(builder.Configuration.GetSection(ScannerWebServiceOptions.SectionName))
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
using StellaOps.Scanner.Storage.ObjectStore;
|
||||
@@ -36,6 +37,7 @@ internal sealed class SurfacePointerService : ISurfacePointerService
|
||||
private readonly ISurfaceEnvironment _surfaceEnvironment;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<SurfacePointerService> _logger;
|
||||
private readonly ICryptoHash _hash;
|
||||
|
||||
public SurfacePointerService(
|
||||
LinkRepository linkRepository,
|
||||
@@ -43,7 +45,8 @@ internal sealed class SurfacePointerService : ISurfacePointerService
|
||||
IOptionsMonitor<ScannerWebServiceOptions> optionsMonitor,
|
||||
ISurfaceEnvironment surfaceEnvironment,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<SurfacePointerService> logger)
|
||||
ILogger<SurfacePointerService> logger,
|
||||
ICryptoHash hash)
|
||||
{
|
||||
_linkRepository = linkRepository ?? throw new ArgumentNullException(nameof(linkRepository));
|
||||
_artifactRepository = artifactRepository ?? throw new ArgumentNullException(nameof(artifactRepository));
|
||||
@@ -51,6 +54,7 @@ internal sealed class SurfacePointerService : ISurfacePointerService
|
||||
_surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
}
|
||||
|
||||
public async Task<SurfacePointersDto?> TryBuildAsync(string imageDigest, CancellationToken cancellationToken)
|
||||
@@ -275,15 +279,9 @@ internal sealed class SurfacePointerService : ISurfacePointerService
|
||||
? string.Empty
|
||||
: value.Trim().TrimEnd('/');
|
||||
|
||||
private static string ComputeDigest(ReadOnlySpan<byte> payload)
|
||||
private string ComputeDigest(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
if (!SHA256.TryHashData(payload, hash, out _))
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
hash = sha.ComputeHash(payload.ToArray());
|
||||
}
|
||||
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
var hex = _hash.ComputeHashHex(payload, HashAlgorithms.Sha256);
|
||||
return $"sha256:{hex}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SCAN-REPLAY-186-001 | TODO | Scanner WebService Guild | REPLAY-CORE-185-001 | Implement scan `record` mode producing replay manifests/bundles, capture policy/feed/tool hashes, and update `docs/modules/scanner/architecture.md` referencing `docs/replay/DETERMINISTIC_REPLAY.md` Section 6. | API/worker integration tests cover record mode; docs merged; replay artifacts stored per spec. |
|
||||
| SCANNER-SURFACE-02 | DONE (2025-11-05) | Scanner WebService Guild | SURFACE-FS-02 | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata.<br>2025-11-05: Surface pointers projected through scan/report endpoints, orchestrator samples + DSSE fixtures refreshed with manifest block, readiness tests updated to use validator stub. | OpenAPI updated; clients regenerated; integration tests validate pointer presence and tenancy. |
|
||||
| SCANNER-CRYPTO-90-001 | TODO | Scanner WebService Guild, Security Guild | SEC-CRYPTO-90-003, SEC-CRYPTO-90-004 | Route hashing/signing flows (`ScanIdGenerator`, `ReportSigner`, Sbomer Buildx plugin) through `ICryptoProviderRegistry` so sovereign deployments can select `ru.cryptopro.csp` / `ru.pkcs11` providers. Reference `docs/security/crypto-routing-audit-2025-11-07.md`. | Config toggles verified for default + RU bundles; report/scan APIs emit signatures via registry-backed providers; regression tests updated. |
|
||||
| SCANNER-ENV-02 | TODO (2025-11-06) | Scanner WebService Guild, Ops Guild | SURFACE-ENV-02 | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration.<br>2025-11-02: Cache root resolution switched to helper; feature flag bindings updated; Helm/Compose updates pending review.<br>2025-11-05 14:55Z: Aligning readiness checks, docs, and Helm/Compose templates with Surface.Env outputs and planning test coverage for configuration fallbacks.<br>2025-11-06 17:05Z: Surface.Env documentation/README refreshed; warning catalogue captured for ops handoff.<br>2025-11-06 07:45Z: Helm values (dev/stage/prod/airgap/mirror) and Compose examples updated with `SCANNER_SURFACE_*` defaults plus rollout warning note in `deploy/README.md`.<br>2025-11-06 07:55Z: Paused; follow-up automation captured under `DEVOPS-OPENSSL-11-001/002` and pending Surface.Env readiness tests. | Service uses helper; env table documented; helm/compose templates updated. |
|
||||
> 2025-11-05 19:18Z: Added configurator to project wiring and unit test ensuring Surface.Env cache root is honoured.
|
||||
| SCANNER-SECRETS-02 | DONE (2025-11-06) | Scanner WebService Guild, Security Guild | SURFACE-SECRETS-02 | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens).<br>2025-11-02: Export/report flows now depend on Surface.Secrets stub; integration tests in progress.<br>2025-11-06: Restarting work to eliminate file-based secrets, plumb provider handles through report/export services, and extend failure/rotation tests.<br>2025-11-06 21:40Z: Added configurator + storage post-config to hydrate artifact/CAS credentials from `cas-access` secrets with unit coverage.<br>2025-11-06 23:58Z: Registry & attestation secrets now resolved via Surface.Secrets (options + tests updated); dotnet test suites executed with .NET 10 RC2 runtime where available. | Secrets fetched through shared provider; unit/integration tests cover rotation + failure cases. |
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Options;
|
||||
@@ -17,13 +18,15 @@ public sealed class ScannerWorkerOptions
|
||||
|
||||
public PollingOptions Polling { get; } = new();
|
||||
|
||||
public AuthorityOptions Authority { get; } = new();
|
||||
|
||||
public AuthorityOptions Authority { get; } = new();
|
||||
|
||||
public TelemetryOptions Telemetry { get; } = new();
|
||||
|
||||
public ShutdownOptions Shutdown { get; } = new();
|
||||
|
||||
public AnalyzerOptions Analyzers { get; } = new();
|
||||
|
||||
public StellaOpsCryptoOptions Crypto { get; } = new();
|
||||
|
||||
public sealed class QueueOptions
|
||||
{
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
using StellaOps.Scanner.Analyzers.OS;
|
||||
@@ -22,9 +21,9 @@ using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
using StellaOps.Scanner.Worker.Diagnostics;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing;
|
||||
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing;
|
||||
|
||||
internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
@@ -32,17 +31,19 @@ internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||
private readonly ILanguageAnalyzerPluginCatalog _languageCatalog;
|
||||
private readonly ScannerWorkerOptions _options;
|
||||
private readonly ILogger<CompositeScanAnalyzerDispatcher> _logger;
|
||||
private readonly ICryptoHash _hash;
|
||||
private readonly ScannerWorkerMetrics _metrics;
|
||||
private IReadOnlyList<string> _osPluginDirectories = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _languagePluginDirectories = Array.Empty<string>();
|
||||
|
||||
public CompositeScanAnalyzerDispatcher(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOSAnalyzerPluginCatalog osCatalog,
|
||||
private IReadOnlyList<string> _osPluginDirectories = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _languagePluginDirectories = Array.Empty<string>();
|
||||
|
||||
public CompositeScanAnalyzerDispatcher(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOSAnalyzerPluginCatalog osCatalog,
|
||||
ILanguageAnalyzerPluginCatalog languageCatalog,
|
||||
IOptions<ScannerWorkerOptions> options,
|
||||
ILogger<CompositeScanAnalyzerDispatcher> logger,
|
||||
ScannerWorkerMetrics metrics)
|
||||
ScannerWorkerMetrics metrics,
|
||||
ICryptoHash hash)
|
||||
{
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_osCatalog = osCatalog ?? throw new ArgumentNullException(nameof(osCatalog));
|
||||
@@ -50,97 +51,98 @@ internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
||||
|
||||
LoadPlugins();
|
||||
}
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var services = scope.ServiceProvider;
|
||||
|
||||
var osAnalyzers = _osCatalog.CreateAnalyzers(services);
|
||||
var languageAnalyzers = _languageCatalog.CreateAnalyzers(services);
|
||||
|
||||
if (osAnalyzers.Count == 0 && languageAnalyzers.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("No analyzer plug-ins available; skipping analyzer stage for job {JobId}.", context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var metadata = new Dictionary<string, string>(context.Lease.Metadata, StringComparer.Ordinal);
|
||||
var rootfsPath = ResolvePath(metadata, _options.Analyzers.RootFilesystemMetadataKey);
|
||||
var workspacePath = ResolvePath(metadata, _options.Analyzers.WorkspaceMetadataKey) ?? rootfsPath;
|
||||
|
||||
if (osAnalyzers.Count > 0)
|
||||
{
|
||||
await ExecuteOsAnalyzersAsync(context, osAnalyzers, services, rootfsPath, workspacePath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (languageAnalyzers.Count > 0)
|
||||
{
|
||||
await ExecuteLanguageAnalyzersAsync(context, languageAnalyzers, services, workspacePath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteOsAnalyzersAsync(
|
||||
ScanJobContext context,
|
||||
IReadOnlyList<IOSPackageAnalyzer> analyzers,
|
||||
IServiceProvider services,
|
||||
string? rootfsPath,
|
||||
string? workspacePath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (rootfsPath is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Metadata key '{MetadataKey}' missing for job {JobId}; unable to locate root filesystem. OS analyzers skipped.",
|
||||
_options.Analyzers.RootFilesystemMetadataKey,
|
||||
context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
var results = new List<OSPackageAnalyzerResult>(analyzers.Count);
|
||||
|
||||
foreach (var analyzer in analyzers)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var analyzerLogger = loggerFactory.CreateLogger(analyzer.GetType());
|
||||
var analyzerContext = new OSPackageAnalyzerContext(rootfsPath, workspacePath, context.TimeProvider, analyzerLogger, context.Lease.Metadata);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await analyzer.AnalyzeAsync(analyzerContext, cancellationToken).ConfigureAwait(false);
|
||||
results.Add(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Analyzer {AnalyzerId} failed for job {JobId}.", analyzer.AnalyzerId, context.JobId);
|
||||
}
|
||||
}
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dictionary = results.ToDictionary(result => result.AnalyzerId, StringComparer.OrdinalIgnoreCase);
|
||||
context.Analysis.Set(ScanAnalysisKeys.OsPackageAnalyzers, dictionary);
|
||||
|
||||
var fragments = OsComponentMapper.ToLayerFragments(results);
|
||||
if (!fragments.IsDefaultOrEmpty)
|
||||
{
|
||||
context.Analysis.AppendLayerFragments(fragments);
|
||||
context.Analysis.Set(ScanAnalysisKeys.OsComponentFragments, fragments);
|
||||
}
|
||||
}
|
||||
|
||||
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
|
||||
LoadPlugins();
|
||||
}
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var services = scope.ServiceProvider;
|
||||
|
||||
var osAnalyzers = _osCatalog.CreateAnalyzers(services);
|
||||
var languageAnalyzers = _languageCatalog.CreateAnalyzers(services);
|
||||
|
||||
if (osAnalyzers.Count == 0 && languageAnalyzers.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("No analyzer plug-ins available; skipping analyzer stage for job {JobId}.", context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var metadata = new Dictionary<string, string>(context.Lease.Metadata, StringComparer.Ordinal);
|
||||
var rootfsPath = ResolvePath(metadata, _options.Analyzers.RootFilesystemMetadataKey);
|
||||
var workspacePath = ResolvePath(metadata, _options.Analyzers.WorkspaceMetadataKey) ?? rootfsPath;
|
||||
|
||||
if (osAnalyzers.Count > 0)
|
||||
{
|
||||
await ExecuteOsAnalyzersAsync(context, osAnalyzers, services, rootfsPath, workspacePath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (languageAnalyzers.Count > 0)
|
||||
{
|
||||
await ExecuteLanguageAnalyzersAsync(context, languageAnalyzers, services, workspacePath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteOsAnalyzersAsync(
|
||||
ScanJobContext context,
|
||||
IReadOnlyList<IOSPackageAnalyzer> analyzers,
|
||||
IServiceProvider services,
|
||||
string? rootfsPath,
|
||||
string? workspacePath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (rootfsPath is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Metadata key '{MetadataKey}' missing for job {JobId}; unable to locate root filesystem. OS analyzers skipped.",
|
||||
_options.Analyzers.RootFilesystemMetadataKey,
|
||||
context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
var results = new List<OSPackageAnalyzerResult>(analyzers.Count);
|
||||
|
||||
foreach (var analyzer in analyzers)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var analyzerLogger = loggerFactory.CreateLogger(analyzer.GetType());
|
||||
var analyzerContext = new OSPackageAnalyzerContext(rootfsPath, workspacePath, context.TimeProvider, analyzerLogger, context.Lease.Metadata);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await analyzer.AnalyzeAsync(analyzerContext, cancellationToken).ConfigureAwait(false);
|
||||
results.Add(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Analyzer {AnalyzerId} failed for job {JobId}.", analyzer.AnalyzerId, context.JobId);
|
||||
}
|
||||
}
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dictionary = results.ToDictionary(result => result.AnalyzerId, StringComparer.OrdinalIgnoreCase);
|
||||
context.Analysis.Set(ScanAnalysisKeys.OsPackageAnalyzers, dictionary);
|
||||
|
||||
var fragments = OsComponentMapper.ToLayerFragments(results);
|
||||
if (!fragments.IsDefaultOrEmpty)
|
||||
{
|
||||
context.Analysis.AppendLayerFragments(fragments);
|
||||
context.Analysis.Set(ScanAnalysisKeys.OsComponentFragments, fragments);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteLanguageAnalyzersAsync(
|
||||
ScanJobContext context,
|
||||
IReadOnlyList<ILanguageAnalyzer> analyzers,
|
||||
@@ -189,7 +191,7 @@ internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||
context.JobId);
|
||||
|
||||
var fallbackBytes = Encoding.UTF8.GetBytes(workspacePath);
|
||||
workspaceFingerprint = Convert.ToHexString(SHA256.HashData(fallbackBytes)).ToLowerInvariant();
|
||||
workspaceFingerprint = _hash.ComputeHashHex(fallbackBytes, HashAlgorithms.Sha256);
|
||||
}
|
||||
|
||||
var cache = services.GetRequiredService<ISurfaceCache>();
|
||||
@@ -261,85 +263,85 @@ internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||
context.Analysis.Set(ScanAnalysisKeys.LanguageComponentFragments, immutableFragments);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPlugins()
|
||||
{
|
||||
_osPluginDirectories = NormalizeDirectories(_options.Analyzers.PluginDirectories, Path.Combine("plugins", "scanner", "analyzers", "os"));
|
||||
for (var i = 0; i < _osPluginDirectories.Count; i++)
|
||||
{
|
||||
var directory = _osPluginDirectories[i];
|
||||
var seal = i == _osPluginDirectories.Count - 1;
|
||||
|
||||
try
|
||||
{
|
||||
_osCatalog.LoadFromDirectory(directory, seal);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load OS analyzer plug-ins from {Directory}.", directory);
|
||||
}
|
||||
}
|
||||
|
||||
_languagePluginDirectories = NormalizeDirectories(_options.Analyzers.LanguagePluginDirectories, Path.Combine("plugins", "scanner", "analyzers", "lang"));
|
||||
for (var i = 0; i < _languagePluginDirectories.Count; i++)
|
||||
{
|
||||
var directory = _languagePluginDirectories[i];
|
||||
var seal = i == _languagePluginDirectories.Count - 1;
|
||||
|
||||
try
|
||||
{
|
||||
_languageCatalog.LoadFromDirectory(directory, seal);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load language analyzer plug-ins from {Directory}.", directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> NormalizeDirectories(IEnumerable<string> configured, string fallbackRelative)
|
||||
{
|
||||
var directories = new List<string>();
|
||||
foreach (var configuredPath in configured ?? Array.Empty<string>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuredPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = configuredPath;
|
||||
if (!Path.IsPathRooted(path))
|
||||
{
|
||||
path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, path));
|
||||
}
|
||||
|
||||
directories.Add(path);
|
||||
}
|
||||
|
||||
if (directories.Count == 0)
|
||||
{
|
||||
var fallback = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, fallbackRelative));
|
||||
directories.Add(fallback);
|
||||
}
|
||||
|
||||
return new ReadOnlyCollection<string>(directories);
|
||||
}
|
||||
|
||||
private static string? ResolvePath(IReadOnlyDictionary<string, string> metadata, string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!metadata.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
return Path.IsPathRooted(trimmed)
|
||||
? trimmed
|
||||
: Path.GetFullPath(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPlugins()
|
||||
{
|
||||
_osPluginDirectories = NormalizeDirectories(_options.Analyzers.PluginDirectories, Path.Combine("plugins", "scanner", "analyzers", "os"));
|
||||
for (var i = 0; i < _osPluginDirectories.Count; i++)
|
||||
{
|
||||
var directory = _osPluginDirectories[i];
|
||||
var seal = i == _osPluginDirectories.Count - 1;
|
||||
|
||||
try
|
||||
{
|
||||
_osCatalog.LoadFromDirectory(directory, seal);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load OS analyzer plug-ins from {Directory}.", directory);
|
||||
}
|
||||
}
|
||||
|
||||
_languagePluginDirectories = NormalizeDirectories(_options.Analyzers.LanguagePluginDirectories, Path.Combine("plugins", "scanner", "analyzers", "lang"));
|
||||
for (var i = 0; i < _languagePluginDirectories.Count; i++)
|
||||
{
|
||||
var directory = _languagePluginDirectories[i];
|
||||
var seal = i == _languagePluginDirectories.Count - 1;
|
||||
|
||||
try
|
||||
{
|
||||
_languageCatalog.LoadFromDirectory(directory, seal);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load language analyzer plug-ins from {Directory}.", directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> NormalizeDirectories(IEnumerable<string> configured, string fallbackRelative)
|
||||
{
|
||||
var directories = new List<string>();
|
||||
foreach (var configuredPath in configured ?? Array.Empty<string>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuredPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = configuredPath;
|
||||
if (!Path.IsPathRooted(path))
|
||||
{
|
||||
path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, path));
|
||||
}
|
||||
|
||||
directories.Add(path);
|
||||
}
|
||||
|
||||
if (directories.Count == 0)
|
||||
{
|
||||
var fallback = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, fallbackRelative));
|
||||
directories.Add(fallback);
|
||||
}
|
||||
|
||||
return new ReadOnlyCollection<string>(directories);
|
||||
}
|
||||
|
||||
private static string? ResolvePath(IReadOnlyDictionary<string, string> metadata, string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!metadata.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
return Path.IsPathRooted(trimmed)
|
||||
? trimmed
|
||||
: Path.GetFullPath(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -56,6 +55,7 @@ public sealed class EntryTraceExecutionService : IEntryTraceExecutionService
|
||||
private readonly ISurfaceCache _surfaceCache;
|
||||
private readonly ISurfaceSecretProvider _surfaceSecrets;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ICryptoHash _hash;
|
||||
|
||||
public EntryTraceExecutionService(
|
||||
IEntryTraceAnalyzer analyzer,
|
||||
@@ -69,7 +69,8 @@ public sealed class EntryTraceExecutionService : IEntryTraceExecutionService
|
||||
ISurfaceEnvironment surfaceEnvironment,
|
||||
ISurfaceCache surfaceCache,
|
||||
ISurfaceSecretProvider surfaceSecrets,
|
||||
IServiceProvider serviceProvider)
|
||||
IServiceProvider serviceProvider,
|
||||
ICryptoHash hash)
|
||||
{
|
||||
_analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
|
||||
_entryTraceOptions = (entryTraceOptions ?? throw new ArgumentNullException(nameof(entryTraceOptions))).Value ?? new EntryTraceAnalyzerOptions();
|
||||
@@ -83,6 +84,7 @@ public sealed class EntryTraceExecutionService : IEntryTraceExecutionService
|
||||
_surfaceCache = surfaceCache ?? throw new ArgumentNullException(nameof(surfaceCache));
|
||||
_surfaceSecrets = surfaceSecrets ?? throw new ArgumentNullException(nameof(surfaceSecrets));
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
}
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
@@ -376,7 +378,7 @@ public sealed class EntryTraceExecutionService : IEntryTraceExecutionService
|
||||
return true;
|
||||
}
|
||||
|
||||
private static SurfaceCacheKey CreateCacheKey(
|
||||
private SurfaceCacheKey CreateCacheKey(
|
||||
string imageDigest,
|
||||
EntryTraceImageContext context,
|
||||
string tenant,
|
||||
@@ -390,11 +392,11 @@ public sealed class EntryTraceExecutionService : IEntryTraceExecutionService
|
||||
builder.Append('|').Append(ComputeEnvironmentFingerprint(context.Context.Environment));
|
||||
builder.Append('|').Append(optionsFingerprint);
|
||||
|
||||
var hash = ComputeSha256(builder.ToString());
|
||||
return new SurfaceCacheKey(CacheNamespace, tenant, hash);
|
||||
var fingerprint = ComputeSha256(builder.ToString());
|
||||
return new SurfaceCacheKey(CacheNamespace, tenant, fingerprint);
|
||||
}
|
||||
|
||||
private static string ComputeOptionsFingerprint(EntryTraceAnalyzerOptions options)
|
||||
private string ComputeOptionsFingerprint(EntryTraceAnalyzerOptions options)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(options.MaxDepth);
|
||||
@@ -404,7 +406,7 @@ public sealed class EntryTraceExecutionService : IEntryTraceExecutionService
|
||||
return ComputeSha256(builder.ToString());
|
||||
}
|
||||
|
||||
private static string ComputeEntrypointSignature(EntrypointSpecification specification)
|
||||
private string ComputeEntrypointSignature(EntrypointSpecification specification)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendJoin(',', specification.Entrypoint);
|
||||
@@ -415,7 +417,7 @@ public sealed class EntryTraceExecutionService : IEntryTraceExecutionService
|
||||
return ComputeSha256(builder.ToString());
|
||||
}
|
||||
|
||||
private static string ComputeEnvironmentFingerprint(ImmutableDictionary<string, string> environment)
|
||||
private string ComputeEnvironmentFingerprint(ImmutableDictionary<string, string> environment)
|
||||
{
|
||||
if (environment.Count == 0)
|
||||
{
|
||||
@@ -431,12 +433,10 @@ public sealed class EntryTraceExecutionService : IEntryTraceExecutionService
|
||||
return ComputeSha256(builder.ToString());
|
||||
}
|
||||
|
||||
private static string ComputeSha256(string value)
|
||||
private string ComputeSha256(string value)
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
return _hash.ComputeHashHex(bytes, HashAlgorithms.Sha256);
|
||||
}
|
||||
|
||||
private static string? ResolvePath(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -15,6 +14,7 @@ using StellaOps.Scanner.Storage.ObjectStore;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.Storage.Services;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.Surface;
|
||||
|
||||
@@ -58,6 +58,7 @@ internal sealed class SurfaceManifestPublisher : ISurfaceManifestPublisher
|
||||
private readonly ISurfaceEnvironment _surfaceEnvironment;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<SurfaceManifestPublisher> _logger;
|
||||
private readonly ICryptoHash _hash;
|
||||
|
||||
public SurfaceManifestPublisher(
|
||||
IArtifactObjectStore objectStore,
|
||||
@@ -66,7 +67,8 @@ internal sealed class SurfaceManifestPublisher : ISurfaceManifestPublisher
|
||||
IOptions<ScannerStorageOptions> storageOptions,
|
||||
ISurfaceEnvironment surfaceEnvironment,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<SurfaceManifestPublisher> logger)
|
||||
ILogger<SurfaceManifestPublisher> logger,
|
||||
ICryptoHash hash)
|
||||
{
|
||||
_objectStore = objectStore ?? throw new ArgumentNullException(nameof(objectStore));
|
||||
_artifactRepository = artifactRepository ?? throw new ArgumentNullException(nameof(artifactRepository));
|
||||
@@ -75,6 +77,7 @@ internal sealed class SurfaceManifestPublisher : ISurfaceManifestPublisher
|
||||
_surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
}
|
||||
|
||||
public async Task<SurfaceManifestPublishResult> PublishAsync(SurfaceManifestRequest request, CancellationToken cancellationToken)
|
||||
@@ -245,14 +248,13 @@ internal sealed class SurfaceManifestPublisher : ISurfaceManifestPublisher
|
||||
_ => format.ToString().ToLowerInvariant()
|
||||
};
|
||||
|
||||
private static string ComputeDigest(ReadOnlySpan<byte> content)
|
||||
private string ComputeDigest(ReadOnlySpan<byte> content)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(content, hash);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
var hex = _hash.ComputeHashHex(content, HashAlgorithms.Sha256);
|
||||
return $"sha256:{hex}";
|
||||
}
|
||||
|
||||
private static string ComputeDigest(byte[] content)
|
||||
private string ComputeDigest(byte[] content)
|
||||
=> ComputeDigest(content.AsSpan());
|
||||
|
||||
private static string NormalizeDigest(string digest)
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -15,6 +14,7 @@ using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
using StellaOps.Scanner.Worker.Diagnostics;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.Surface;
|
||||
|
||||
@@ -37,6 +37,7 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
|
||||
private readonly ISurfaceEnvironment _surfaceEnvironment;
|
||||
private readonly ScannerWorkerMetrics _metrics;
|
||||
private readonly ILogger<SurfaceManifestStageExecutor> _logger;
|
||||
private readonly ICryptoHash _hash;
|
||||
private readonly string _componentVersion;
|
||||
|
||||
public SurfaceManifestStageExecutor(
|
||||
@@ -44,13 +45,15 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
|
||||
ISurfaceCache surfaceCache,
|
||||
ISurfaceEnvironment surfaceEnvironment,
|
||||
ScannerWorkerMetrics metrics,
|
||||
ILogger<SurfaceManifestStageExecutor> logger)
|
||||
ILogger<SurfaceManifestStageExecutor> logger,
|
||||
ICryptoHash hash)
|
||||
{
|
||||
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
|
||||
_surfaceCache = surfaceCache ?? throw new ArgumentNullException(nameof(surfaceCache));
|
||||
_surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment));
|
||||
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
_componentVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
|
||||
}
|
||||
|
||||
@@ -274,11 +277,10 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
|
||||
return digest.Trim();
|
||||
}
|
||||
|
||||
private static string ComputeDigest(ReadOnlySpan<byte> content)
|
||||
private string ComputeDigest(ReadOnlySpan<byte> content)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(content, hash);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
var hex = _hash.ComputeHashHex(content, HashAlgorithms.Sha256);
|
||||
return $"sha256:{hex}";
|
||||
}
|
||||
|
||||
private static readonly IFormatProvider CultureInfoInvariant = System.Globalization.CultureInfo.InvariantCulture;
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Scanner.Cache;
|
||||
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
@@ -71,6 +72,7 @@ builder.Services.AddSingleton<ScannerWorkerHostedService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ScannerWorkerHostedService>());
|
||||
|
||||
var workerOptions = builder.Configuration.GetSection(ScannerWorkerOptions.SectionName).Get<ScannerWorkerOptions>() ?? new ScannerWorkerOptions();
|
||||
builder.Services.AddStellaOpsCrypto(workerOptions.Crypto);
|
||||
|
||||
builder.Services.Configure<HostOptions>(options =>
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SCAN-REPLAY-186-002 | TODO | Scanner Worker Guild | REPLAY-CORE-185-001 | Enforce deterministic analyzer execution when consuming replay input bundles, emit layer Merkle metadata, and author `docs/modules/scanner/deterministic-execution.md` summarising invariants from `docs/replay/DETERMINISTIC_REPLAY.md` Section 4. | Replay mode analyzers pass determinism tests; new doc merged; integration fixtures updated. |
|
||||
| SCANNER-CRYPTO-90-001 | DONE (2025-11-08) | Scanner Worker Guild & Security Guild | SEC-CRYPTO-90-005 | Route remaining hashing and digest consumers (Surface pointers, manifest publishers, CAS helpers, Sbomer plugins) through ICryptoHash and the configured provider registry.<br>2025-11-08: Worker EntryTrace service, CAS helpers, and Sbomer plugin now depend on ICryptoHash; Local CAS + manifest writer persisted digests via providers; tests updated with CryptoHashFactory/TestCryptoHash helpers; runtime SHA256 calls removed. | No direct SHA256.Create() usage in worker runtime; constructors accept ICryptoHash; tests updated. |
|
||||
| SCANNER-SURFACE-01 | DONE (2025-11-06) | Scanner Worker Guild | SURFACE-FS-02 | Persist Surface.FS manifests after analyzer stages, including layer CAS metadata and EntryTrace fragments.<br>2025-11-02: Draft Surface.FS manifests emitted for sample scans; telemetry counters under review.<br>2025-11-06: Resuming with manifest writer abstraction, rotation metadata, and telemetry counters for Surface.FS persistence.<br>2025-11-06 21:05Z: Stage now persists manifest/payload caches, exports metrics to Prometheus/Grafana, and WebService pointer tests validate consumption. | Integration tests prove cache entries exist; telemetry counters exported. |
|
||||
> 2025-11-05 19:18Z: Bound root directory to resolved Surface.Env settings and added unit coverage around the configurator.
|
||||
> 2025-11-06 18:45Z: Resuming manifest persistence—planning publisher abstraction refactor, CAS storage wiring, and telemetry/test coverage.
|
||||
@@ -10,3 +11,4 @@
|
||||
> 2025-11-06 21:05Z: Completed Surface manifest cache + metrics work; tests/docs updated and task ready to close.
|
||||
| SCANNER-ENV-01 | TODO (2025-11-06) | Scanner Worker Guild | SURFACE-ENV-02 | Replace ad-hoc environment reads with `StellaOps.Scanner.Surface.Env` helpers for cache roots and CAS endpoints.<br>2025-11-02: Worker bootstrap now resolves cache roots via helper; warning path documented; smoke tests running.<br>2025-11-05 14:55Z: Extending helper usage into cache/secrets configuration, updating worker validator wiring, and drafting docs/tests for new Surface.Env outputs.<br>2025-11-06 17:05Z: README/design docs updated with warning catalogue; startup logging guidance captured for ops runbooks.<br>2025-11-06 07:45Z: Helm/Compose env profiles (dev/stage/prod/airgap/mirror) now seed `SCANNER_SURFACE_*` defaults to keep worker cache roots aligned with Surface.Env helpers.<br>2025-11-06 07:55Z: Paused; pending automation tracked via `DEVOPS-OPENSSL-11-001/002` and Surface.Env test fixtures. | Worker boots with helper; misconfiguration warnings documented; smoke tests updated. |
|
||||
| SCANNER-SECRETS-01 | DONE (2025-11-06) | Scanner Worker Guild, Security Guild | SURFACE-SECRETS-02 | Adopt `StellaOps.Scanner.Surface.Secrets` for registry/CAS credentials during scan execution.<br>2025-11-02: Surface.Secrets provider wired for CAS token retrieval; integration tests added.<br>2025-11-06: Replaced registry credential plumbing with shared provider, added registry secret stage + metrics, and installed .NET 10 RC2 to validate parser/stage suites via targeted `dotnet test`. | Secrets fetched via shared provider; legacy secret code removed; integration tests cover rotation. |
|
||||
| SCAN-REACH-201-002 | DOING (2025-11-08) | Scanner Worker Guild | SIGNALS-24-002 | Implement language-aware reachability lifters (JVM/WALA, .NET Roslyn+IL, Go SSA, Node/Deno TS AST, Rust MIR, Swift SIL, shell/binary analyzers) emitting canonical SymbolIDs, CAS-stored callgraphs, and `reachability:*` SBOM tags consumed by Signals + Policy. | Fixture library + unit tests per language; CAS manifests published; SBOM components carry reachability tags; docs updated. |
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability;
|
||||
|
||||
public sealed class ReachabilityGraphBuilder
|
||||
{
|
||||
private const string GraphSchemaVersion = "1.0";
|
||||
private readonly HashSet<string> nodes = new(StringComparer.Ordinal);
|
||||
private readonly HashSet<ReachabilityEdge> edges = new();
|
||||
|
||||
public ReachabilityGraphBuilder AddNode(string symbolId)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(symbolId))
|
||||
{
|
||||
nodes.Add(symbolId.Trim());
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReachabilityGraphBuilder AddEdge(string from, string to, string kind = "call")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to))
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
var edge = new ReachabilityEdge(from.Trim(), to.Trim(), string.IsNullOrWhiteSpace(kind) ? "call" : kind.Trim());
|
||||
edges.Add(edge);
|
||||
nodes.Add(edge.From);
|
||||
nodes.Add(edge.To);
|
||||
return this;
|
||||
}
|
||||
|
||||
public string BuildJson(bool indented = true)
|
||||
{
|
||||
var payload = new ReachabilityGraphPayload
|
||||
{
|
||||
SchemaVersion = GraphSchemaVersion,
|
||||
Nodes = nodes.Select(id => new ReachabilityNode(id)).ToList(),
|
||||
Edges = edges.Select(edge => new ReachabilityEdgePayload(edge.From, edge.To, edge.Kind)).ToList()
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = indented
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(payload, options);
|
||||
}
|
||||
|
||||
public static ReachabilityGraphBuilder FromFixture(string variantPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(variantPath);
|
||||
var builder = new ReachabilityGraphBuilder();
|
||||
|
||||
foreach (var fileName in new[] { "callgraph.static.json", "callgraph.framework.json" })
|
||||
{
|
||||
var path = Path.Combine(variantPath, fileName);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var stream = File.OpenRead(path);
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
var root = document.RootElement;
|
||||
|
||||
if (root.TryGetProperty("nodes", out var nodesElement) && nodesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var node in nodesElement.EnumerateArray())
|
||||
{
|
||||
var sid = node.TryGetProperty("sid", out var sidElement)
|
||||
? sidElement.GetString()
|
||||
: node.GetProperty("id").GetString();
|
||||
builder.AddNode(sid ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("edges", out var edgesElement) && edgesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var edge in edgesElement.EnumerateArray())
|
||||
{
|
||||
var from = edge.TryGetProperty("from", out var fromEl)
|
||||
? fromEl.GetString()
|
||||
: edge.GetProperty("source").GetString();
|
||||
var to = edge.TryGetProperty("to", out var toEl)
|
||||
? toEl.GetString()
|
||||
: edge.GetProperty("target").GetString();
|
||||
var kind = edge.TryGetProperty("kind", out var kindEl)
|
||||
? kindEl.GetString()
|
||||
: edge.TryGetProperty("type", out var typeEl)
|
||||
? typeEl.GetString()
|
||||
: "call";
|
||||
|
||||
builder.AddEdge(from ?? string.Empty, to ?? string.Empty, kind ?? "call");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private sealed record ReachabilityEdge(string From, string To, string Kind);
|
||||
|
||||
private sealed record ReachabilityNode(string Sid);
|
||||
|
||||
private sealed record ReachabilityEdgePayload(string From, string To, string Kind);
|
||||
|
||||
private sealed record ReachabilityGraphPayload
|
||||
{
|
||||
public string SchemaVersion { get; set; } = GraphSchemaVersion;
|
||||
public List<ReachabilityNode> Nodes { get; set; } = new();
|
||||
public List<ReachabilityEdgePayload> Edges { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StellaOps.Replay.Core;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability;
|
||||
|
||||
/// <summary>
|
||||
/// Helper that projects reachability artifacts into the replay manifest.
|
||||
/// </summary>
|
||||
public sealed class ReachabilityReplayWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Attaches reachability graphs and runtime traces to the supplied replay manifest.
|
||||
/// </summary>
|
||||
public void AttachEvidence(
|
||||
ReplayManifest manifest,
|
||||
IEnumerable<ReachabilityReplayGraph>? graphs,
|
||||
IEnumerable<ReachabilityReplayTrace>? traces)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
|
||||
WriteGraphs(manifest, graphs);
|
||||
WriteTraces(manifest, traces);
|
||||
}
|
||||
|
||||
private static void WriteGraphs(ReplayManifest manifest, IEnumerable<ReachabilityReplayGraph>? graphs)
|
||||
{
|
||||
if (graphs is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sanitized = graphs
|
||||
.Where(graph => graph is not null)
|
||||
.Select(graph => NormalizeGraph(graph!))
|
||||
.Where(graph => graph is not null)
|
||||
.Select(graph => graph!)
|
||||
.DistinctBy(graph => (graph.Kind, graph.CasUri, graph.Sha256, graph.Analyzer, graph.Version))
|
||||
.OrderBy(graph => graph.CasUri, StringComparer.Ordinal)
|
||||
.ThenBy(graph => graph.Kind, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
foreach (var graph in sanitized)
|
||||
{
|
||||
manifest.AddReachabilityGraph(new ReplayReachabilityGraphReference
|
||||
{
|
||||
Kind = graph.Kind,
|
||||
CasUri = graph.CasUri,
|
||||
Sha256 = graph.Sha256,
|
||||
Analyzer = graph.Analyzer,
|
||||
Version = graph.Version
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteTraces(ReplayManifest manifest, IEnumerable<ReachabilityReplayTrace>? traces)
|
||||
{
|
||||
if (traces is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalized = traces
|
||||
.Where(trace => trace is not null)
|
||||
.Select(trace => NormalizeTrace(trace!))
|
||||
.Where(trace => trace is not null)
|
||||
.Select(trace => trace!)
|
||||
.ToList();
|
||||
var collapsed = normalized
|
||||
.GroupBy(trace => (trace.Source, trace.CasUri, trace.Sha256))
|
||||
.Select(group => group.OrderBy(t => t.RecordedAt).First())
|
||||
.OrderBy(trace => trace.RecordedAt)
|
||||
.ThenBy(trace => trace.CasUri, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
foreach (var trace in collapsed)
|
||||
{
|
||||
manifest.AddReachabilityTrace(new ReplayReachabilityTraceReference
|
||||
{
|
||||
Source = trace.Source,
|
||||
CasUri = trace.CasUri,
|
||||
Sha256 = trace.Sha256,
|
||||
RecordedAt = trace.RecordedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static NormalizedGraph? NormalizeGraph(ReachabilityReplayGraph graph)
|
||||
{
|
||||
var casUri = Normalize(graph.CasUri);
|
||||
if (string.IsNullOrEmpty(casUri))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new NormalizedGraph(
|
||||
Kind: Normalize(graph.Kind) ?? "static",
|
||||
CasUri: casUri,
|
||||
Sha256: NormalizeHash(graph.Sha256),
|
||||
Analyzer: Normalize(graph.Analyzer) ?? string.Empty,
|
||||
Version: Normalize(graph.Version) ?? string.Empty);
|
||||
}
|
||||
|
||||
private static NormalizedTrace? NormalizeTrace(ReachabilityReplayTrace trace)
|
||||
{
|
||||
var casUri = Normalize(trace.CasUri);
|
||||
if (string.IsNullOrEmpty(casUri))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new NormalizedTrace(
|
||||
Source: Normalize(trace.Source) ?? string.Empty,
|
||||
CasUri: casUri,
|
||||
Sha256: NormalizeHash(trace.Sha256),
|
||||
RecordedAt: trace.RecordedAt.ToUniversalTime());
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
private static string NormalizeHash(string? hash)
|
||||
=> string.IsNullOrWhiteSpace(hash) ? string.Empty : hash.Trim().ToLowerInvariant();
|
||||
|
||||
private sealed record NormalizedGraph(
|
||||
string Kind,
|
||||
string CasUri,
|
||||
string Sha256,
|
||||
string Analyzer,
|
||||
string Version);
|
||||
|
||||
private sealed record NormalizedTrace(
|
||||
string Source,
|
||||
string CasUri,
|
||||
string Sha256,
|
||||
DateTimeOffset RecordedAt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a CAS-backed reachability graph emitted by Scanner.
|
||||
/// </summary>
|
||||
public sealed record ReachabilityReplayGraph(
|
||||
string? Kind,
|
||||
string? CasUri,
|
||||
string? Sha256,
|
||||
string? Analyzer,
|
||||
string? Version);
|
||||
|
||||
/// <summary>
|
||||
/// Describes a runtime trace artifact emitted by Scanner/Zastava.
|
||||
/// </summary>
|
||||
public sealed record ReachabilityReplayTrace(
|
||||
string? Source,
|
||||
string? CasUri,
|
||||
string? Sha256,
|
||||
DateTimeOffset RecordedAt);
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" Version="10.0.0-preview.7.25380.108" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -3,11 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.FS;
|
||||
|
||||
@@ -20,12 +20,14 @@ public sealed class FileSurfaceManifestStore :
|
||||
{
|
||||
private readonly ILogger<FileSurfaceManifestStore> _logger;
|
||||
private readonly SurfaceManifestPathBuilder _pathBuilder;
|
||||
private readonly ICryptoHash _hash;
|
||||
private readonly SemaphoreSlim _publishGate = new(1, 1);
|
||||
|
||||
public FileSurfaceManifestStore(
|
||||
IOptions<SurfaceCacheOptions> cacheOptions,
|
||||
IOptions<SurfaceManifestStoreOptions> storeOptions,
|
||||
ILogger<FileSurfaceManifestStore> logger)
|
||||
ILogger<FileSurfaceManifestStore> logger,
|
||||
ICryptoHash hash)
|
||||
{
|
||||
if (cacheOptions is null)
|
||||
{
|
||||
@@ -38,6 +40,7 @@ public sealed class FileSurfaceManifestStore :
|
||||
}
|
||||
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
|
||||
_pathBuilder = new SurfaceManifestPathBuilder(cacheOptions.Value, storeOptions.Value);
|
||||
}
|
||||
|
||||
@@ -183,11 +186,10 @@ public sealed class FileSurfaceManifestStore :
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeDigest(ReadOnlySpan<byte> bytes)
|
||||
private string ComputeDigest(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
var digest = _hash.ComputeHash(bytes, HashAlgorithms.Sha256);
|
||||
return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static SurfaceManifestArtifact NormalizeArtifact(SurfaceManifestArtifact artifact)
|
||||
|
||||
@@ -16,12 +16,15 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.7.25380.108" />
|
||||
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\..\\..\\__Libraries\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Cas;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Cas;
|
||||
|
||||
public sealed class LocalCasClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerifyWriteAsync_WritesProbeObject()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var client = new LocalCasClient(new LocalCasOptions
|
||||
{
|
||||
RootDirectory = temp.Path,
|
||||
Algorithm = "sha256"
|
||||
});
|
||||
|
||||
var result = await client.VerifyWriteAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal("sha256", result.Algorithm);
|
||||
Assert.True(File.Exists(result.Path));
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(result.Path);
|
||||
Assert.Equal("stellaops-buildx-probe"u8.ToArray(), bytes);
|
||||
|
||||
var expectedDigest = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
Assert.Equal(expectedDigest, result.Digest);
|
||||
}
|
||||
}
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Cas;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Cas;
|
||||
|
||||
public sealed class LocalCasClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerifyWriteAsync_WritesProbeObject()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var client = new LocalCasClient(new LocalCasOptions
|
||||
{
|
||||
RootDirectory = temp.Path,
|
||||
Algorithm = "sha256"
|
||||
}, CryptoHashFactory.CreateDefault());
|
||||
|
||||
var result = await client.VerifyWriteAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal("sha256", result.Algorithm);
|
||||
Assert.True(File.Exists(result.Path));
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(result.Path);
|
||||
Assert.Equal("stellaops-buildx-probe"u8.ToArray(), bytes);
|
||||
|
||||
var expectedDigest = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
Assert.Equal(expectedDigest, result.Digest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
@@ -36,15 +37,49 @@ public sealed class DescriptorCommandSurfaceTests
|
||||
var manifestOutputPath = Path.Combine(temp.Path, "out", "surface-manifest.json");
|
||||
|
||||
var repoRoot = TestPathHelper.FindRepositoryRoot();
|
||||
var manifestDirectory = Path.Combine(repoRoot, "src", "Scanner", "StellaOps.Scanner.Sbomer.BuildXPlugin");
|
||||
var pluginAssembly = typeof(BuildxPluginManifest).Assembly.Location;
|
||||
var normalizedRoot = repoRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
var rootFolder = Path.GetFileName(normalizedRoot);
|
||||
var actualRepoRoot = string.Equals(rootFolder, "src", StringComparison.OrdinalIgnoreCase)
|
||||
? Directory.GetParent(normalizedRoot)?.FullName ?? normalizedRoot
|
||||
: normalizedRoot;
|
||||
var sourceRoot = string.Equals(rootFolder, "src", StringComparison.OrdinalIgnoreCase)
|
||||
? normalizedRoot
|
||||
: Path.Combine(actualRepoRoot, "src");
|
||||
|
||||
var pluginProjectRoot = Path.Combine(sourceRoot, "Scanner", "StellaOps.Scanner.Sbomer.BuildXPlugin");
|
||||
|
||||
var manifestDirectoryCandidates = new[]
|
||||
{
|
||||
Path.Combine(actualRepoRoot, "plugins", "scanner", "buildx", "StellaOps.Scanner.Sbomer.BuildXPlugin"),
|
||||
pluginProjectRoot
|
||||
};
|
||||
|
||||
var manifestDirectory = manifestDirectoryCandidates.FirstOrDefault(Directory.Exists)
|
||||
?? throw new DirectoryNotFoundException(
|
||||
$"BuildX manifest directory not found under '{string.Join("', '", manifestDirectoryCandidates)}'.");
|
||||
var testsOutputDirectory = Path.GetDirectoryName(typeof(DescriptorCommandSurfaceTests).Assembly.Location)
|
||||
?? throw new InvalidOperationException("Unable to resolve test assembly directory.");
|
||||
var targetFramework = new DirectoryInfo(testsOutputDirectory).Name;
|
||||
var configuration = Directory.GetParent(testsOutputDirectory)?.Name ?? "Debug";
|
||||
|
||||
var pluginAssembly = Path.Combine(
|
||||
pluginProjectRoot,
|
||||
"bin",
|
||||
configuration,
|
||||
targetFramework,
|
||||
"StellaOps.Scanner.Sbomer.BuildXPlugin.dll");
|
||||
|
||||
if (!File.Exists(pluginAssembly))
|
||||
{
|
||||
throw new FileNotFoundException($"BuildX plug-in assembly not found at '{pluginAssembly}'.");
|
||||
}
|
||||
|
||||
var psi = new ProcessStartInfo("dotnet")
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = repoRoot
|
||||
WorkingDirectory = actualRepoRoot
|
||||
};
|
||||
|
||||
psi.ArgumentList.Add(pluginAssembly);
|
||||
@@ -82,7 +117,9 @@ public sealed class DescriptorCommandSurfaceTests
|
||||
var descriptor = JsonSerializer.Deserialize<DescriptorDocument>(stdout, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal("stellaops.buildx.descriptor.v1", descriptor!.Schema);
|
||||
Assert.Equal("sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c", descriptor.Artifact.Digest);
|
||||
var hash = CryptoHashFactory.CreateDefault();
|
||||
var expectedDigest = ComputeSha256Digest(hash, sbomPath);
|
||||
Assert.Equal(expectedDigest, descriptor.Artifact.Digest);
|
||||
|
||||
Assert.Contains("surface manifest stored", stderr, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(File.Exists(manifestOutputPath));
|
||||
@@ -109,14 +146,21 @@ public sealed class DescriptorCommandSurfaceTests
|
||||
throw new InvalidOperationException($"Unsupported CAS URI {casUri}.");
|
||||
}
|
||||
|
||||
var slashIndex = casUri.IndexOf(/, prefix.Length);
|
||||
var slashIndex = casUri.IndexOf('/', prefix.Length);
|
||||
if (slashIndex < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"CAS URI {casUri} does not contain a bucket path.");
|
||||
}
|
||||
|
||||
var relative = casUri[(slashIndex + 1)..];
|
||||
var localPath = Path.Combine(casRoot, relative.Replace(/, Path.DirectorySeparatorChar));
|
||||
var localPath = Path.Combine(casRoot, relative.Replace('/', Path.DirectorySeparatorChar));
|
||||
return localPath;
|
||||
}
|
||||
|
||||
private static string ComputeSha256Digest(ICryptoHash hash, string filePath)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(filePath);
|
||||
var digest = hash.ComputeHash(bytes, HashAlgorithms.Sha256);
|
||||
return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
@@ -23,7 +24,7 @@ public sealed class DescriptorGeneratorTests
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
var generator = CreateGenerator(fakeTime);
|
||||
|
||||
var request = new DescriptorRequest
|
||||
{
|
||||
@@ -72,7 +73,7 @@ public sealed class DescriptorGeneratorTests
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
var generator = CreateGenerator(fakeTime);
|
||||
|
||||
var request = new DescriptorRequest
|
||||
{
|
||||
@@ -109,7 +110,7 @@ public sealed class DescriptorGeneratorTests
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
var generator = CreateGenerator(fakeTime);
|
||||
|
||||
var baseline = new DescriptorRequest
|
||||
{
|
||||
@@ -133,7 +134,10 @@ public sealed class DescriptorGeneratorTests
|
||||
Assert.NotEqual(baselineDocument.Provenance.ExpectedDsseSha256, variantDocument.Provenance.ExpectedDsseSha256);
|
||||
}
|
||||
|
||||
private static string ComputeSha256File(string path)
|
||||
private static DescriptorGenerator CreateGenerator(TimeProvider timeProvider)
|
||||
=> new(timeProvider, CryptoHashFactory.CreateDefault());
|
||||
|
||||
private static string ComputeSha256File(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
var hash = SHA256.HashData(stream);
|
||||
|
||||
@@ -7,8 +7,9 @@ using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
@@ -48,7 +49,7 @@ public sealed class DescriptorGoldenTests
|
||||
}.Validate();
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
var generator = CreateGenerator(fakeTime);
|
||||
var document = await generator.CreateAsync(request, CancellationToken.None);
|
||||
var actualJson = JsonSerializer.Serialize(document, SerializerOptions);
|
||||
var normalizedJson = NormalizeDescriptorJson(actualJson, Path.GetFileName(sbomPath));
|
||||
@@ -129,4 +130,6 @@ public sealed class DescriptorGoldenTests
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static DescriptorGenerator CreateGenerator(TimeProvider timeProvider)
|
||||
=> new(timeProvider, CryptoHashFactory.CreateDefault());
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Surface;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
@@ -42,7 +43,7 @@ public sealed class SurfaceManifestWriterTests
|
||||
EntryTraceNdjsonPath: ndjsonPath,
|
||||
ManifestOutputPath: manifestOutputPath);
|
||||
|
||||
var writer = new SurfaceManifestWriter(TimeProvider.System);
|
||||
var writer = new SurfaceManifestWriter(TimeProvider.System, CryptoHashFactory.CreateDefault());
|
||||
var result = await writer.WriteAsync(options, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
@@ -88,7 +89,7 @@ public sealed class SurfaceManifestWriterTests
|
||||
EntryTraceNdjsonPath: null,
|
||||
ManifestOutputPath: null);
|
||||
|
||||
var writer = new SurfaceManifestWriter(TimeProvider.System);
|
||||
var writer = new SurfaceManifestWriter(TimeProvider.System, CryptoHashFactory.CreateDefault());
|
||||
var result = await writer.WriteAsync(options, CancellationToken.None);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Scanner.Worker.Diagnostics;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using Xunit;
|
||||
using StellaOps.Scanner.Worker.Tests.TestInfrastructure;
|
||||
using Xunit;
|
||||
using WorkerOptions = StellaOps.Scanner.Worker.Options.ScannerWorkerOptions;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests;
|
||||
@@ -108,7 +109,8 @@ public sealed class CompositeScanAnalyzerDispatcherTests
|
||||
languageCatalog,
|
||||
options,
|
||||
loggerFactory.CreateLogger<CompositeScanAnalyzerDispatcher>(),
|
||||
metrics);
|
||||
metrics,
|
||||
new TestCryptoHash());
|
||||
|
||||
var lease = new TestJobLease(metadata);
|
||||
var context = new ScanJobContext(lease, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None);
|
||||
|
||||
@@ -19,6 +19,7 @@ using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using StellaOps.Scanner.Worker.Tests.TestInfrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests;
|
||||
@@ -166,7 +167,8 @@ public sealed class EntryTraceExecutionServiceTests : IDisposable
|
||||
ISurfaceCache? surfaceCache = null,
|
||||
ISurfaceValidatorRunner? surfaceValidator = null,
|
||||
ISurfaceSecretProvider? surfaceSecrets = null,
|
||||
ISurfaceEnvironment? surfaceEnvironment = null)
|
||||
ISurfaceEnvironment? surfaceEnvironment = null,
|
||||
ICryptoHash? hash = null)
|
||||
{
|
||||
var workerOptions = new ScannerWorkerOptions();
|
||||
var entryTraceOptions = new EntryTraceAnalyzerOptions();
|
||||
@@ -176,6 +178,7 @@ public sealed class EntryTraceExecutionServiceTests : IDisposable
|
||||
surfaceCache ??= new InMemorySurfaceCache();
|
||||
surfaceValidator ??= new NoopSurfaceValidatorRunner();
|
||||
surfaceSecrets ??= new StubSurfaceSecretProvider();
|
||||
hash ??= new TestCryptoHash();
|
||||
var serviceProvider = new ServiceCollection()
|
||||
.AddSingleton<ISurfaceEnvironment>(surfaceEnvironment)
|
||||
.BuildServiceProvider();
|
||||
@@ -192,7 +195,8 @@ public sealed class EntryTraceExecutionServiceTests : IDisposable
|
||||
surfaceEnvironment,
|
||||
surfaceCache,
|
||||
surfaceSecrets,
|
||||
serviceProvider);
|
||||
serviceProvider,
|
||||
hash);
|
||||
}
|
||||
|
||||
private static ScanJobContext CreateContext(IReadOnlyDictionary<string, string> metadata)
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.EntryTrace;
|
||||
@@ -18,6 +17,7 @@ using StellaOps.Scanner.Worker.Processing;
|
||||
using StellaOps.Scanner.Worker.Processing.Surface;
|
||||
using StellaOps.Scanner.Worker.Tests.TestInfrastructure;
|
||||
using Xunit;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests;
|
||||
|
||||
@@ -34,12 +34,14 @@ public sealed class SurfaceManifestStageExecutorTests
|
||||
using var listener = new WorkerMeterListener();
|
||||
listener.Start();
|
||||
|
||||
var hash = new DefaultCryptoHash();
|
||||
var executor = new SurfaceManifestStageExecutor(
|
||||
publisher,
|
||||
cache,
|
||||
environment,
|
||||
metrics,
|
||||
NullLogger<SurfaceManifestStageExecutor>.Instance);
|
||||
NullLogger<SurfaceManifestStageExecutor>.Instance,
|
||||
hash);
|
||||
|
||||
var context = CreateContext();
|
||||
|
||||
@@ -68,12 +70,14 @@ public sealed class SurfaceManifestStageExecutorTests
|
||||
using var listener = new WorkerMeterListener();
|
||||
listener.Start();
|
||||
|
||||
var hash = new DefaultCryptoHash();
|
||||
var executor = new SurfaceManifestStageExecutor(
|
||||
publisher,
|
||||
cache,
|
||||
environment,
|
||||
metrics,
|
||||
NullLogger<SurfaceManifestStageExecutor>.Instance);
|
||||
NullLogger<SurfaceManifestStageExecutor>.Instance,
|
||||
hash);
|
||||
|
||||
var context = CreateContext();
|
||||
PopulateAnalysis(context);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests.TestInfrastructure;
|
||||
|
||||
internal sealed class TestCryptoHash : ICryptoHash
|
||||
{
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
{
|
||||
using var algorithm = CreateAlgorithm(algorithmId);
|
||||
return algorithm.ComputeHash(data.ToArray());
|
||||
}
|
||||
|
||||
public string ComputeHashHex(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> Convert.ToHexString(ComputeHash(data, algorithmId)).ToLowerInvariant();
|
||||
|
||||
public string ComputeHashBase64(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> Convert.ToBase64String(ComputeHash(data, algorithmId));
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var algorithm = CreateAlgorithm(algorithmId);
|
||||
await using var buffer = new MemoryStream();
|
||||
await stream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
return algorithm.ComputeHash(buffer.ToArray());
|
||||
}
|
||||
|
||||
public async ValueTask<string> ComputeHashHexAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = await ComputeHashAsync(stream, algorithmId, cancellationToken).ConfigureAwait(false);
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static HashAlgorithm CreateAlgorithm(string? algorithmId)
|
||||
{
|
||||
return algorithmId?.ToUpperInvariant() switch
|
||||
{
|
||||
null or "" or HashAlgorithms.Sha256 => SHA256.Create(),
|
||||
HashAlgorithms.Sha512 => SHA512.Create(),
|
||||
_ => throw new NotSupportedException($"Test crypto hash does not support algorithm {algorithmId}.")
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user