sprints completion. new product advisories prepared
This commit is contained in:
@@ -0,0 +1,672 @@
|
||||
// <copyright file="SignedSbomArchiveBuilderTests.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_016_SCANNER_signed_sbom_archive_spec (SBOM-SPEC-011)
|
||||
// </copyright>
|
||||
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using StellaOps.TestKit;
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="SignedSbomArchiveBuilder"/>.
|
||||
/// Sprint: SPRINT_20260112_016_SCANNER_signed_sbom_archive_spec (SBOM-SPEC-011)
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public sealed class SignedSbomArchiveBuilderTests : IDisposable
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTime = new(2026, 1, 16, 10, 30, 0, TimeSpan.Zero);
|
||||
private readonly SignedSbomArchiveBuilder _builder;
|
||||
private readonly List<Stream> _streamsToDispose = new();
|
||||
|
||||
public SignedSbomArchiveBuilderTests()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(FixedTime);
|
||||
_builder = new SignedSbomArchiveBuilder(timeProvider, NullLogger<SignedSbomArchiveBuilder>.Instance);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var stream in _streamsToDispose)
|
||||
{
|
||||
stream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#region Archive Structure Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithMinimalInput_CreatesValidArchive()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Size > 0);
|
||||
Assert.StartsWith("signed-sbom-", result.FileName);
|
||||
Assert.EndsWith(".tar.gz", result.FileName);
|
||||
Assert.Equal("application/gzip", result.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_IncludesMandatoryFiles()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert - Extract and verify file list
|
||||
var files = await ExtractTarGzFileListAsync(result.Stream);
|
||||
|
||||
Assert.Contains(files, f => f.EndsWith("manifest.json"));
|
||||
Assert.Contains(files, f => f.EndsWith("metadata.json"));
|
||||
Assert.Contains(files, f => f.EndsWith("sbom.spdx.json") || f.EndsWith("sbom.cdx.json"));
|
||||
Assert.Contains(files, f => f.EndsWith("sbom.dsse.json"));
|
||||
Assert.Contains(files, f => f.EndsWith("certs/signing-cert.pem"));
|
||||
Assert.Contains(files, f => f.EndsWith("VERIFY.md"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithSpdxFormat_UsesSpdxFileName()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with { SbomFormat = "spdx-2.3" };
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var files = await ExtractTarGzFileListAsync(result.Stream);
|
||||
Assert.Contains(files, f => f.EndsWith("sbom.spdx.json"));
|
||||
Assert.DoesNotContain(files, f => f.EndsWith("sbom.cdx.json"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithCycloneDxFormat_UsesCdxFileName()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with { SbomFormat = "cyclonedx-1.7" };
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var files = await ExtractTarGzFileListAsync(result.Stream);
|
||||
Assert.Contains(files, f => f.EndsWith("sbom.cdx.json"));
|
||||
Assert.DoesNotContain(files, f => f.EndsWith("sbom.spdx.json"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optional Content Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithSigningChain_IncludesChainFile()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with
|
||||
{
|
||||
SigningChainPem = "-----BEGIN CERTIFICATE-----\nCHAIN\n-----END CERTIFICATE-----"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var files = await ExtractTarGzFileListAsync(result.Stream);
|
||||
Assert.Contains(files, f => f.EndsWith("certs/signing-chain.pem"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithFulcioRoot_IncludesFulcioRootFile()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with
|
||||
{
|
||||
FulcioRootPem = "-----BEGIN CERTIFICATE-----\nFULCIO\n-----END CERTIFICATE-----"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var files = await ExtractTarGzFileListAsync(result.Stream);
|
||||
Assert.Contains(files, f => f.EndsWith("certs/fulcio-root.pem"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithRekorProof_IncludesRekorFiles()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with
|
||||
{
|
||||
IncludeRekorProof = true,
|
||||
RekorInclusionProofBytes = Encoding.UTF8.GetBytes("{\"proof\": \"test\"}"),
|
||||
RekorCheckpointBytes = Encoding.UTF8.GetBytes("checkpoint"),
|
||||
RekorPublicKeyPem = "-----BEGIN PUBLIC KEY-----\nREKOR\n-----END PUBLIC KEY-----",
|
||||
RekorLogIndex = 12345678
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var files = await ExtractTarGzFileListAsync(result.Stream);
|
||||
Assert.Contains(files, f => f.EndsWith("rekor-proof/inclusion-proof.json"));
|
||||
Assert.Contains(files, f => f.EndsWith("rekor-proof/checkpoint.sig"));
|
||||
Assert.Contains(files, f => f.EndsWith("rekor-proof/rekor-public.pem"));
|
||||
Assert.Equal(12345678, result.RekorLogIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithRekorProofDisabled_ExcludesRekorFiles()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with
|
||||
{
|
||||
IncludeRekorProof = false
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var files = await ExtractTarGzFileListAsync(result.Stream);
|
||||
Assert.DoesNotContain(files, f => f.Contains("rekor-proof/"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithSchemas_IncludesSchemasReadme()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with { IncludeSchemas = true };
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var files = await ExtractTarGzFileListAsync(result.Stream);
|
||||
Assert.Contains(files, f => f.EndsWith("schemas/README.md"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithoutSchemas_ExcludesSchemasDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with { IncludeSchemas = false };
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var files = await ExtractTarGzFileListAsync(result.Stream);
|
||||
Assert.DoesNotContain(files, f => f.Contains("schemas/"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Digest and Hash Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ComputesCorrectSbomDigest()
|
||||
{
|
||||
// Arrange
|
||||
var sbomContent = "{\"spdxVersion\": \"SPDX-2.3\"}";
|
||||
var sbomBytes = Encoding.UTF8.GetBytes(sbomContent);
|
||||
var expectedDigest = ComputeSha256Hex(sbomBytes);
|
||||
|
||||
var request = CreateMinimalRequest() with { SbomBytes = sbomBytes };
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedDigest, result.SbomDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ComputesNonEmptyArchiveDigest()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.ArchiveDigest);
|
||||
Assert.Equal(64, result.ArchiveDigest.Length); // SHA-256 hex string length
|
||||
Assert.Matches("^[a-f0-9]{64}$", result.ArchiveDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ComputesNonEmptyMerkleRoot()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.MerkleRoot);
|
||||
Assert.StartsWith("sha256:", result.MerkleRoot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SameInput_ProducesSameSbomDigest()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest();
|
||||
|
||||
// Act
|
||||
var result1 = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result1.Stream);
|
||||
|
||||
var result2 = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result2.Stream);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result1.SbomDigest, result2.SbomDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SameInput_ProducesSameMerkleRoot()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest();
|
||||
|
||||
// Act
|
||||
var result1 = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result1.Stream);
|
||||
|
||||
var result2 = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result2.Stream);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result1.MerkleRoot, result2.MerkleRoot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Metadata Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_MetadataContainsRequiredFields()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with
|
||||
{
|
||||
ImageRef = "ghcr.io/test/image:v1.0.0",
|
||||
ImageDigest = "sha256:abc123",
|
||||
SbomFormat = "spdx-2.3",
|
||||
ComponentCount = 10,
|
||||
PackageCount = 5,
|
||||
FileCount = 100,
|
||||
SignatureIssuer = "https://accounts.google.com",
|
||||
SignatureSubject = "test@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert - Extract and parse metadata.json
|
||||
var metadataJson = await ExtractFileContentAsync(result.Stream, "metadata.json");
|
||||
Assert.NotNull(metadataJson);
|
||||
|
||||
var metadata = JsonSerializer.Deserialize<JsonElement>(metadataJson);
|
||||
|
||||
Assert.Equal("1.0.0", metadata.GetProperty("schemaVersion").GetString());
|
||||
Assert.True(metadata.TryGetProperty("stellaOps", out _));
|
||||
Assert.True(metadata.TryGetProperty("generation", out _));
|
||||
Assert.True(metadata.TryGetProperty("input", out _));
|
||||
Assert.True(metadata.TryGetProperty("sbom", out _));
|
||||
Assert.True(metadata.TryGetProperty("signature", out _));
|
||||
|
||||
var input = metadata.GetProperty("input");
|
||||
Assert.Equal("ghcr.io/test/image:v1.0.0", input.GetProperty("imageRef").GetString());
|
||||
Assert.Equal("sha256:abc123", input.GetProperty("imageDigest").GetString());
|
||||
|
||||
var sbom = metadata.GetProperty("sbom");
|
||||
Assert.Equal("spdx-2.3", sbom.GetProperty("format").GetString());
|
||||
Assert.Equal(10, sbom.GetProperty("componentCount").GetInt32());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Manifest Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ManifestListsAllFiles()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with { IncludeSchemas = true };
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert - Extract and parse manifest.json
|
||||
var manifestJson = await ExtractFileContentAsync(result.Stream, "manifest.json");
|
||||
Assert.NotNull(manifestJson);
|
||||
|
||||
var manifest = JsonSerializer.Deserialize<JsonElement>(manifestJson);
|
||||
|
||||
Assert.Equal("1.0.0", manifest.GetProperty("schemaVersion").GetString());
|
||||
Assert.True(manifest.TryGetProperty("archiveId", out _));
|
||||
Assert.True(manifest.TryGetProperty("generatedAt", out _));
|
||||
Assert.True(manifest.TryGetProperty("files", out _));
|
||||
Assert.True(manifest.TryGetProperty("merkleRoot", out _));
|
||||
Assert.True(manifest.TryGetProperty("totalFiles", out _));
|
||||
Assert.True(manifest.TryGetProperty("totalSize", out _));
|
||||
|
||||
var files = manifest.GetProperty("files");
|
||||
Assert.True(files.GetArrayLength() > 0);
|
||||
|
||||
// Verify each file entry has required fields
|
||||
foreach (var file in files.EnumerateArray())
|
||||
{
|
||||
Assert.True(file.TryGetProperty("path", out _));
|
||||
Assert.True(file.TryGetProperty("sha256", out _));
|
||||
Assert.True(file.TryGetProperty("size", out _));
|
||||
Assert.True(file.TryGetProperty("mediaType", out _));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ManifestFileHashesAreValid()
|
||||
{
|
||||
// Arrange
|
||||
var sbomContent = "{\"test\": \"sbom\"}";
|
||||
var request = CreateMinimalRequest() with
|
||||
{
|
||||
SbomBytes = Encoding.UTF8.GetBytes(sbomContent),
|
||||
SbomFormat = "spdx-2.3"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var manifestJson = await ExtractFileContentAsync(result.Stream, "manifest.json");
|
||||
var manifest = JsonSerializer.Deserialize<JsonElement>(manifestJson);
|
||||
|
||||
var files = manifest.GetProperty("files");
|
||||
var sbomEntry = files.EnumerateArray()
|
||||
.FirstOrDefault(f => f.GetProperty("path").GetString()?.EndsWith("sbom.spdx.json") == true);
|
||||
|
||||
Assert.NotNull(sbomEntry.GetProperty("sha256").GetString());
|
||||
|
||||
// Verify SBOM hash matches expected
|
||||
var expectedHash = ComputeSha256Hex(Encoding.UTF8.GetBytes(sbomContent));
|
||||
Assert.Equal(expectedHash, sbomEntry.GetProperty("sha256").GetString());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region VERIFY.md Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_VerifyMdContainsVerificationInstructions()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with
|
||||
{
|
||||
SbomFormat = "spdx-2.3",
|
||||
RekorLogIndex = 12345678
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var verifyMd = await ExtractFileContentAsync(result.Stream, "VERIFY.md");
|
||||
Assert.NotNull(verifyMd);
|
||||
|
||||
Assert.Contains("# SBOM Archive Verification", verifyMd);
|
||||
Assert.Contains("Quick Verification", verifyMd);
|
||||
Assert.Contains("Signature Verification", verifyMd);
|
||||
Assert.Contains("cosign verify-blob", verifyMd);
|
||||
Assert.Contains("sbom.spdx.json", verifyMd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_VerifyMdIncludesRekorSectionWhenAvailable()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest() with
|
||||
{
|
||||
IncludeRekorProof = true,
|
||||
RekorLogIndex = 12345678
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var verifyMd = await ExtractFileContentAsync(result.Stream, "VERIFY.md");
|
||||
|
||||
Assert.Contains("Rekor Transparency Log", verifyMd);
|
||||
Assert.Contains("12345678", verifyMd);
|
||||
Assert.Contains("rekor-cli verify", verifyMd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_VerifyMdIncludesFileHashTable()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest();
|
||||
|
||||
// Act
|
||||
var result = await _builder.BuildAsync(request);
|
||||
_streamsToDispose.Add(result.Stream);
|
||||
|
||||
// Assert
|
||||
var verifyMd = await ExtractFileContentAsync(result.Stream, "VERIFY.md");
|
||||
|
||||
Assert.Contains("Archive Contents", verifyMd);
|
||||
Assert.Contains("| File | Size | SHA-256 |", verifyMd);
|
||||
Assert.Contains("Merkle Root", verifyMd);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_WithNullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => _builder.BuildAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_SupportsCancellation()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateMinimalRequest();
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||
() => _builder.BuildAsync(request, cts.Token));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Helpers
|
||||
|
||||
private static SignedSbomArchiveRequest CreateMinimalRequest()
|
||||
{
|
||||
var sbomBytes = Encoding.UTF8.GetBytes("{\"spdxVersion\": \"SPDX-2.3\", \"packages\": []}");
|
||||
var dsseBytes = Encoding.UTF8.GetBytes("""
|
||||
{
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"payload": "base64-encoded-payload",
|
||||
"signatures": [{"sig": "test-signature"}]
|
||||
}
|
||||
""");
|
||||
var certPem = """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBkTCB+wIJAKHBfFmJ/r7CMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnRl
|
||||
c3RjYTAeFw0yNjAxMTYwMDAwMDBaFw0yNzAxMTYwMDAwMDBaMBExDzANBgNVBAMM
|
||||
BnRlc3RjYTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC5Q2QRqzFVcFm5AwQKDQCu
|
||||
xK5nMPVPu9F4Nz7Q3z5F5w5F5w5F5w5F5w5F5w5F5w5F5w5F5w5F5w5F5w5F5w5F
|
||||
AgMBAAGjUDBOMB0GA1UdDgQWBBQExample0MB8GA1UdIwQYMBaAFExample0MAwGA
|
||||
1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQExample
|
||||
-----END CERTIFICATE-----
|
||||
""";
|
||||
|
||||
return new SignedSbomArchiveRequest
|
||||
{
|
||||
ScanId = ScanId.CreateNew(),
|
||||
SbomBytes = sbomBytes,
|
||||
SbomFormat = "spdx-2.3",
|
||||
DsseEnvelopeBytes = dsseBytes,
|
||||
SigningCertPem = certPem,
|
||||
ImageRef = "ghcr.io/test/image:latest",
|
||||
ImageDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
ComponentCount = 5,
|
||||
PackageCount = 3,
|
||||
FileCount = 20,
|
||||
IncludeRekorProof = false,
|
||||
IncludeSchemas = false
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<List<string>> ExtractTarGzFileListAsync(Stream stream)
|
||||
{
|
||||
var files = new List<string>();
|
||||
stream.Position = 0;
|
||||
|
||||
await using var gzipStream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true);
|
||||
using var memoryStream = new MemoryStream();
|
||||
await gzipStream.CopyToAsync(memoryStream);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
var buffer = new byte[512];
|
||||
|
||||
while (memoryStream.Position < memoryStream.Length - 1024)
|
||||
{
|
||||
var bytesRead = await memoryStream.ReadAsync(buffer.AsMemory(0, 512));
|
||||
if (bytesRead < 512) break;
|
||||
|
||||
// Check for end-of-archive marker (all zeros)
|
||||
if (buffer.All(b => b == 0)) break;
|
||||
|
||||
// Extract file name from header (first 100 bytes)
|
||||
var nameEnd = Array.IndexOf(buffer, (byte)0);
|
||||
if (nameEnd < 0) nameEnd = 100;
|
||||
var fileName = Encoding.ASCII.GetString(buffer, 0, Math.Min(nameEnd, 100)).TrimEnd('\0');
|
||||
|
||||
if (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
files.Add(fileName);
|
||||
}
|
||||
|
||||
// Get file size from header (bytes 124-135, octal)
|
||||
var sizeStr = Encoding.ASCII.GetString(buffer, 124, 11).Trim('\0', ' ');
|
||||
var fileSize = string.IsNullOrEmpty(sizeStr) ? 0 : Convert.ToInt64(sizeStr, 8);
|
||||
|
||||
// Skip file content (rounded up to 512-byte boundary)
|
||||
var paddedSize = ((fileSize + 511) / 512) * 512;
|
||||
memoryStream.Position += paddedSize;
|
||||
}
|
||||
|
||||
stream.Position = 0;
|
||||
return files;
|
||||
}
|
||||
|
||||
private static async Task<string?> ExtractFileContentAsync(Stream stream, string fileNamePattern)
|
||||
{
|
||||
stream.Position = 0;
|
||||
|
||||
await using var gzipStream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true);
|
||||
using var memoryStream = new MemoryStream();
|
||||
await gzipStream.CopyToAsync(memoryStream);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
var headerBuffer = new byte[512];
|
||||
|
||||
while (memoryStream.Position < memoryStream.Length - 1024)
|
||||
{
|
||||
var bytesRead = await memoryStream.ReadAsync(headerBuffer.AsMemory(0, 512));
|
||||
if (bytesRead < 512) break;
|
||||
|
||||
// Check for end-of-archive marker
|
||||
if (headerBuffer.All(b => b == 0)) break;
|
||||
|
||||
// Extract file name
|
||||
var nameEnd = Array.IndexOf(headerBuffer, (byte)0);
|
||||
if (nameEnd < 0) nameEnd = 100;
|
||||
var fileName = Encoding.ASCII.GetString(headerBuffer, 0, Math.Min(nameEnd, 100)).TrimEnd('\0');
|
||||
|
||||
// Get file size
|
||||
var sizeStr = Encoding.ASCII.GetString(headerBuffer, 124, 11).Trim('\0', ' ');
|
||||
var fileSize = string.IsNullOrEmpty(sizeStr) ? 0 : Convert.ToInt64(sizeStr, 8);
|
||||
|
||||
if (fileName.EndsWith(fileNamePattern))
|
||||
{
|
||||
var contentBuffer = new byte[fileSize];
|
||||
await memoryStream.ReadAsync(contentBuffer.AsMemory(0, (int)fileSize));
|
||||
stream.Position = 0;
|
||||
return Encoding.UTF8.GetString(contentBuffer);
|
||||
}
|
||||
|
||||
// Skip file content
|
||||
var paddedSize = ((fileSize + 511) / 512) * 512;
|
||||
memoryStream.Position += paddedSize - fileSize; // We haven't read content, so skip entire padded block
|
||||
memoryStream.Position += fileSize;
|
||||
}
|
||||
|
||||
stream.Position = 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ComputeSha256Hex(byte[] data)
|
||||
{
|
||||
var hash = SHA256.HashData(data);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user