Refactor and enhance scanner worker functionality
- Cleaned up code formatting and organization across multiple files for improved readability. - Introduced `OsScanAnalyzerDispatcher` to handle OS analyzer execution and plugin loading. - Updated `ScanJobContext` to include an `Analysis` property for storing scan results. - Enhanced `ScanJobProcessor` to utilize the new `OsScanAnalyzerDispatcher`. - Improved logging and error handling in `ScanProgressReporter` for better traceability. - Updated project dependencies and added references to new analyzer plugins. - Revised task documentation to reflect current status and dependencies.
This commit is contained in:
@@ -1,180 +1,209 @@
|
||||
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;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
|
||||
/// <summary>
|
||||
/// Builds immutable OCI descriptors enriched with provenance placeholders.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<DescriptorDocument> CreateAsync(DescriptorRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ImageDigest))
|
||||
{
|
||||
throw new BuildxPluginException("Image digest must be provided.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.SbomPath))
|
||||
{
|
||||
throw new BuildxPluginException("SBOM path must be provided.");
|
||||
}
|
||||
|
||||
var sbomFile = new FileInfo(request.SbomPath);
|
||||
if (!sbomFile.Exists)
|
||||
{
|
||||
throw new BuildxPluginException($"SBOM file '{request.SbomPath}' was not found.");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
|
||||
/// <summary>
|
||||
/// Builds immutable OCI descriptors enriched with provenance placeholders.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<DescriptorDocument> CreateAsync(DescriptorRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ImageDigest))
|
||||
{
|
||||
throw new BuildxPluginException("Image digest must be provided.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.SbomPath))
|
||||
{
|
||||
throw new BuildxPluginException("SBOM path must be provided.");
|
||||
}
|
||||
|
||||
var sbomFile = new FileInfo(request.SbomPath);
|
||||
if (!sbomFile.Exists)
|
||||
{
|
||||
throw new BuildxPluginException($"SBOM file '{request.SbomPath}' was not found.");
|
||||
}
|
||||
|
||||
var sbomDigest = await ComputeFileDigestAsync(sbomFile, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var nonce = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
var nonce = ComputeDeterministicNonce(request, sbomFile, sbomDigest);
|
||||
var expectedDsseSha = ComputeExpectedDsseDigest(request.ImageDigest, sbomDigest, nonce);
|
||||
|
||||
var artifactAnnotations = BuildArtifactAnnotations(request, nonce, expectedDsseSha);
|
||||
|
||||
var subject = new DescriptorSubject(
|
||||
MediaType: request.SubjectMediaType,
|
||||
Digest: request.ImageDigest);
|
||||
|
||||
var artifact = new DescriptorArtifact(
|
||||
MediaType: request.SbomMediaType,
|
||||
Digest: sbomDigest,
|
||||
Size: sbomFile.Length,
|
||||
Annotations: artifactAnnotations);
|
||||
|
||||
var provenance = new DescriptorProvenance(
|
||||
Status: "pending",
|
||||
ExpectedDsseSha256: expectedDsseSha,
|
||||
Nonce: nonce,
|
||||
AttestorUri: request.AttestorUri,
|
||||
PredicateType: request.PredicateType);
|
||||
|
||||
var generatorMetadata = new DescriptorGeneratorMetadata(
|
||||
Name: request.GeneratorName ?? "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
Version: request.GeneratorVersion);
|
||||
|
||||
var metadata = BuildDocumentMetadata(request, sbomFile, sbomDigest);
|
||||
|
||||
|
||||
var subject = new DescriptorSubject(
|
||||
MediaType: request.SubjectMediaType,
|
||||
Digest: request.ImageDigest);
|
||||
|
||||
var artifact = new DescriptorArtifact(
|
||||
MediaType: request.SbomMediaType,
|
||||
Digest: sbomDigest,
|
||||
Size: sbomFile.Length,
|
||||
Annotations: artifactAnnotations);
|
||||
|
||||
var provenance = new DescriptorProvenance(
|
||||
Status: "pending",
|
||||
ExpectedDsseSha256: expectedDsseSha,
|
||||
Nonce: nonce,
|
||||
AttestorUri: request.AttestorUri,
|
||||
PredicateType: request.PredicateType);
|
||||
|
||||
var generatorMetadata = new DescriptorGeneratorMetadata(
|
||||
Name: request.GeneratorName ?? "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
Version: request.GeneratorVersion);
|
||||
|
||||
var metadata = BuildDocumentMetadata(request, sbomFile, sbomDigest);
|
||||
|
||||
return new DescriptorDocument(
|
||||
Schema: Schema,
|
||||
GeneratedAt: timeProvider.GetUtcNow(),
|
||||
Generator: generatorMetadata,
|
||||
Subject: subject,
|
||||
Artifact: artifact,
|
||||
Provenance: provenance,
|
||||
Generator: generatorMetadata,
|
||||
Subject: subject,
|
||||
Artifact: artifact,
|
||||
Provenance: provenance,
|
||||
Metadata: metadata);
|
||||
}
|
||||
|
||||
private static async Task<string> ComputeFileDigestAsync(FileInfo file, CancellationToken cancellationToken)
|
||||
private static string ComputeDeterministicNonce(DescriptorRequest request, FileInfo sbomFile, string sbomDigest)
|
||||
{
|
||||
await using var stream = new FileStream(
|
||||
file.FullName,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read,
|
||||
bufferSize: 128 * 1024,
|
||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("stellaops.buildx.nonce.v1");
|
||||
builder.AppendLine(request.ImageDigest);
|
||||
builder.AppendLine(sbomDigest);
|
||||
builder.AppendLine(sbomFile.Length.ToString(CultureInfo.InvariantCulture));
|
||||
builder.AppendLine(request.SbomMediaType);
|
||||
builder.AppendLine(request.SbomFormat);
|
||||
builder.AppendLine(request.SbomKind);
|
||||
builder.AppendLine(request.SbomArtifactType);
|
||||
builder.AppendLine(request.SubjectMediaType);
|
||||
builder.AppendLine(request.GeneratorVersion);
|
||||
builder.AppendLine(request.GeneratorName ?? string.Empty);
|
||||
builder.AppendLine(request.LicenseId ?? string.Empty);
|
||||
builder.AppendLine(request.SbomName ?? string.Empty);
|
||||
builder.AppendLine(request.Repository ?? string.Empty);
|
||||
builder.AppendLine(request.BuildRef ?? string.Empty);
|
||||
builder.AppendLine(request.AttestorUri ?? string.Empty);
|
||||
builder.AppendLine(request.PredicateType);
|
||||
|
||||
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);
|
||||
var payload = Encoding.UTF8.GetBytes(builder.ToString());
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(bytes, hash);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
SHA256.HashData(payload, hash);
|
||||
|
||||
Span<byte> nonceBytes = stackalloc byte[16];
|
||||
hash[..16].CopyTo(nonceBytes);
|
||||
return Convert.ToHexString(nonceBytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> BuildArtifactAnnotations(DescriptorRequest request, string nonce, string expectedDsse)
|
||||
{
|
||||
var annotations = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["org.opencontainers.artifact.type"] = request.SbomArtifactType,
|
||||
["org.stellaops.scanner.version"] = request.GeneratorVersion,
|
||||
["org.stellaops.sbom.kind"] = request.SbomKind,
|
||||
["org.stellaops.sbom.format"] = request.SbomFormat,
|
||||
["org.stellaops.provenance.status"] = "pending",
|
||||
["org.stellaops.provenance.dsse.sha256"] = expectedDsse,
|
||||
["org.stellaops.provenance.nonce"] = nonce
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.LicenseId))
|
||||
{
|
||||
annotations["org.stellaops.license.id"] = request.LicenseId!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.SbomName))
|
||||
{
|
||||
annotations["org.opencontainers.image.title"] = request.SbomName!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Repository))
|
||||
{
|
||||
annotations["org.stellaops.repository"] = request.Repository!;
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> BuildDocumentMetadata(DescriptorRequest request, FileInfo fileInfo, string sbomDigest)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["sbomDigest"] = sbomDigest,
|
||||
["sbomPath"] = fileInfo.FullName,
|
||||
["sbomMediaType"] = request.SbomMediaType,
|
||||
["subjectMediaType"] = request.SubjectMediaType
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Repository))
|
||||
{
|
||||
metadata["repository"] = request.Repository!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.BuildRef))
|
||||
{
|
||||
metadata["buildRef"] = request.BuildRef!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.AttestorUri))
|
||||
{
|
||||
metadata["attestorUri"] = request.AttestorUri!;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
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 static IReadOnlyDictionary<string, string> BuildArtifactAnnotations(DescriptorRequest request, string nonce, string expectedDsse)
|
||||
{
|
||||
var annotations = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["org.opencontainers.artifact.type"] = request.SbomArtifactType,
|
||||
["org.stellaops.scanner.version"] = request.GeneratorVersion,
|
||||
["org.stellaops.sbom.kind"] = request.SbomKind,
|
||||
["org.stellaops.sbom.format"] = request.SbomFormat,
|
||||
["org.stellaops.provenance.status"] = "pending",
|
||||
["org.stellaops.provenance.dsse.sha256"] = expectedDsse,
|
||||
["org.stellaops.provenance.nonce"] = nonce
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.LicenseId))
|
||||
{
|
||||
annotations["org.stellaops.license.id"] = request.LicenseId!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.SbomName))
|
||||
{
|
||||
annotations["org.opencontainers.image.title"] = request.SbomName!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Repository))
|
||||
{
|
||||
annotations["org.stellaops.repository"] = request.Repository!;
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> BuildDocumentMetadata(DescriptorRequest request, FileInfo fileInfo, string sbomDigest)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["sbomDigest"] = sbomDigest,
|
||||
["sbomPath"] = fileInfo.FullName,
|
||||
["sbomMediaType"] = request.SbomMediaType,
|
||||
["subjectMediaType"] = request.SubjectMediaType
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Repository))
|
||||
{
|
||||
metadata["repository"] = request.Repository!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.BuildRef))
|
||||
{
|
||||
metadata["buildRef"] = request.BuildRef!;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.AttestorUri))
|
||||
{
|
||||
metadata["attestorUri"] = request.AttestorUri!;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,5 @@
|
||||
| SP9-BLDX-09-001 | DONE | BuildX Guild | SCANNER-EMIT-10-601 (awareness) | Scaffold buildx driver, manifest, local CAS handshake; ensure plugin loads from `plugins/scanner/buildx/`. | Plugin manifest + loader tests; local CAS writes succeed; restart required to activate. |
|
||||
| SP9-BLDX-09-002 | DONE | BuildX Guild | SP9-BLDX-09-001 | Emit OCI annotations + provenance metadata for Attestor handoff (image + SBOM). | OCI descriptors include DSSE/provenance placeholders; Attestor mock accepts payload. |
|
||||
| SP9-BLDX-09-003 | DONE | BuildX Guild | SP9-BLDX-09-002 | CI demo pipeline: build sample image, produce SBOM, verify backend report wiring. | GitHub/CI job runs sample build within 5 s overhead; artifacts saved; documentation updated. |
|
||||
| SP9-BLDX-09-004 | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-002 | Stabilize descriptor nonce derivation so repeated builds emit deterministic placeholders. | Repeated descriptor runs with fixed inputs yield identical JSON; regression tests cover nonce determinism. |
|
||||
| SP9-BLDX-09-005 | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-004 | Integrate determinism check in GitHub/Gitea workflows and capture sample artifacts. | Determinism step runs in `.gitea/workflows/build-test-deploy.yml` and `samples/ci/buildx-demo`, producing matching descriptors + archived artifacts. |
|
||||
|
||||
Reference in New Issue
Block a user