Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
/// <summary>
/// Represents an OCI artifact descriptor emitted by the BuildX generator.
/// </summary>
public sealed record DescriptorArtifact(
[property: JsonPropertyName("mediaType")] string MediaType,
[property: JsonPropertyName("digest")] string Digest,
[property: JsonPropertyName("size")] long Size,
[property: JsonPropertyName("annotations")] IReadOnlyDictionary<string, string> Annotations);

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
/// <summary>
/// Root payload describing BuildX generator output with provenance placeholders.
/// </summary>
public sealed record DescriptorDocument(
[property: JsonPropertyName("schema")] string Schema,
[property: JsonPropertyName("generatedAt")] DateTimeOffset GeneratedAt,
[property: JsonPropertyName("generator")] DescriptorGeneratorMetadata Generator,
[property: JsonPropertyName("subject")] DescriptorSubject Subject,
[property: JsonPropertyName("artifact")] DescriptorArtifact Artifact,
[property: JsonPropertyName("provenance")] DescriptorProvenance Provenance,
[property: JsonPropertyName("metadata")] IReadOnlyDictionary<string, string> Metadata);

View File

@@ -0,0 +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.");
}
var sbomDigest = await ComputeFileDigestAsync(sbomFile, cancellationToken).ConfigureAwait(false);
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);
return new DescriptorDocument(
Schema: Schema,
GeneratedAt: timeProvider.GetUtcNow(),
Generator: generatorMetadata,
Subject: subject,
Artifact: artifact,
Provenance: provenance,
Metadata: metadata);
}
private static string ComputeDeterministicNonce(DescriptorRequest request, FileInfo sbomFile, string sbomDigest)
{
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);
var payload = Encoding.UTF8.GetBytes(builder.ToString());
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(payload, hash);
Span<byte> nonceBytes = stackalloc byte[16];
hash[..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 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;
}
}

View File

@@ -0,0 +1,7 @@
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
public sealed record DescriptorGeneratorMetadata(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("version")] string Version);

View File

@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
/// <summary>
/// Provenance placeholders that the Attestor will fulfil post-build.
/// </summary>
public sealed record DescriptorProvenance(
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("expectedDsseSha256")] string ExpectedDsseSha256,
[property: JsonPropertyName("nonce")] string Nonce,
[property: JsonPropertyName("attestorUri")] string? AttestorUri,
[property: JsonPropertyName("predicateType")] string PredicateType);

View File

@@ -0,0 +1,45 @@
using System;
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
/// <summary>
/// Request for generating BuildX descriptor artifacts.
/// </summary>
public sealed record DescriptorRequest
{
public string ImageDigest { get; init; } = string.Empty;
public string SbomPath { get; init; } = string.Empty;
public string SbomMediaType { get; init; } = "application/vnd.cyclonedx+json";
public string SbomFormat { get; init; } = "cyclonedx-json";
public string SbomArtifactType { get; init; } = "application/vnd.stellaops.sbom.layer+json";
public string SbomKind { get; init; } = "inventory";
public string SubjectMediaType { get; init; } = "application/vnd.oci.image.manifest.v1+json";
public string GeneratorVersion { get; init; } = "0.0.0";
public string? GeneratorName { get; init; }
public string? LicenseId { get; init; }
public string? SbomName { get; init; }
public string? Repository { get; init; }
public string? BuildRef { get; init; }
public string? AttestorUri { get; init; }
public string PredicateType { get; init; } = "https://slsa.dev/provenance/v1";
public DescriptorRequest Validate()
{
if (string.IsNullOrWhiteSpace(ImageDigest))
{
throw new BuildxPluginException("Image digest is required.");
}
if (!ImageDigest.Contains(':', StringComparison.Ordinal))
{
throw new BuildxPluginException("Image digest must include the algorithm prefix, e.g. 'sha256:...'.");
}
if (string.IsNullOrWhiteSpace(SbomPath))
{
throw new BuildxPluginException("SBOM path is required.");
}
return this;
}
}

View File

@@ -0,0 +1,7 @@
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
public sealed record DescriptorSubject(
[property: JsonPropertyName("mediaType")] string MediaType,
[property: JsonPropertyName("digest")] string Digest);