This commit is contained in:
StellaOps Bot
2025-12-07 22:49:53 +02:00
parent 11597679ed
commit 7c24ed96ee
204 changed files with 23313 additions and 1430 deletions

View File

@@ -0,0 +1,316 @@
using System.Formats.Tar;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Cli.Services;
using Xunit;
namespace StellaOps.Cli.Tests.Services;
public sealed class DevPortalBundleVerifierTests : IDisposable
{
private readonly string _tempDir;
private readonly DevPortalBundleVerifier _verifier;
public DevPortalBundleVerifierTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"devportal-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_verifier = new DevPortalBundleVerifier(NullLogger<DevPortalBundleVerifier>.Instance);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
[Fact]
public async Task VerifyBundleAsync_ReturnsSuccess_ForValidBundle()
{
var bundlePath = CreateValidBundle();
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
Assert.Equal("verified", result.Status);
Assert.Equal(DevPortalVerifyExitCode.Success, result.ExitCode);
Assert.Equal("a1b2c3d4-e5f6-7890-abcd-ef1234567890", result.BundleId);
Assert.NotNull(result.RootHash);
Assert.True(result.RootHash!.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase));
Assert.Equal(1, result.Entries);
}
[Fact]
public async Task VerifyBundleAsync_ReturnsUnexpected_WhenBundleNotFound()
{
var nonExistentPath = Path.Combine(_tempDir, "nonexistent.tgz");
var result = await _verifier.VerifyBundleAsync(nonExistentPath, offline: true, CancellationToken.None);
Assert.Equal("failed", result.Status);
Assert.Equal(DevPortalVerifyExitCode.Unexpected, result.ExitCode);
Assert.Contains("not found", result.ErrorMessage, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task VerifyBundleAsync_ReturnsChecksumMismatch_WhenSha256DoesNotMatch()
{
var bundlePath = CreateValidBundle();
var sha256Path = bundlePath + ".sha256";
// Write incorrect hash
await File.WriteAllTextAsync(sha256Path, "0000000000000000000000000000000000000000000000000000000000000000 bundle.tgz");
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
Assert.Equal("failed", result.Status);
Assert.Equal(DevPortalVerifyExitCode.ChecksumMismatch, result.ExitCode);
}
[Fact]
public async Task VerifyBundleAsync_SucceedsWithoutSha256File()
{
var bundlePath = CreateValidBundle();
// Remove .sha256 file if exists
var sha256Path = bundlePath + ".sha256";
if (File.Exists(sha256Path))
{
File.Delete(sha256Path);
}
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
Assert.Equal("verified", result.Status);
Assert.Equal(DevPortalVerifyExitCode.Success, result.ExitCode);
}
[Fact]
public async Task VerifyBundleAsync_ReturnsTsaMissing_WhenOnlineAndNoTimestamp()
{
var bundlePath = CreateBundleWithoutTimestamp();
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: false, CancellationToken.None);
Assert.Equal("failed", result.Status);
Assert.Equal(DevPortalVerifyExitCode.TsaMissing, result.ExitCode);
}
[Fact]
public async Task VerifyBundleAsync_DetectsPortableBundle()
{
var bundlePath = CreatePortableBundle();
var result = await _verifier.VerifyBundleAsync(bundlePath, offline: true, CancellationToken.None);
Assert.Equal("verified", result.Status);
Assert.True(result.Portable);
}
[Fact]
public void ToJson_OutputsKeysSortedAlphabetically()
{
var result = new DevPortalBundleVerificationResult
{
Status = "verified",
BundleId = "test-id",
RootHash = "sha256:abc123",
Entries = 3,
CreatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
Portable = false,
ExitCode = DevPortalVerifyExitCode.Success
};
var json = result.ToJson();
// Keys should be in alphabetical order
var keys = JsonDocument.Parse(json).RootElement.EnumerateObject()
.Select(p => p.Name)
.ToList();
var sortedKeys = keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
Assert.Equal(sortedKeys, keys);
}
private string CreateValidBundle()
{
var bundlePath = Path.Combine(_tempDir, $"bundle-{Guid.NewGuid():N}.tgz");
var manifest = new
{
bundleId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
tenantId = "00000000-0000-0000-0000-000000000001",
kind = 2,
createdAt = "2025-12-07T10:30:00Z",
metadata = new Dictionary<string, string> { ["source"] = "test" },
entries = new[]
{
new
{
section = "sbom",
canonicalPath = "sbom/cyclonedx.json",
sha256 = new string('a', 64),
sizeBytes = 1024,
mediaType = "application/vnd.cyclonedx+json"
}
}
};
var manifestJson = JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = false });
var manifestPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(manifestJson));
var signature = new
{
payloadType = "application/vnd.stella.evidence.manifest+json",
payload = manifestPayload,
signature = Convert.ToBase64String(Encoding.UTF8.GetBytes("test-signature")),
keyId = "key-1",
algorithm = "ES256",
provider = "StellaOps",
signedAt = "2025-12-07T10:30:05Z",
timestampedAt = "2025-12-07T10:30:06Z",
timestampAuthority = "https://freetsa.org/tsr",
timestampToken = Convert.ToBase64String(Encoding.UTF8.GetBytes("tsa-token"))
};
var bundleMetadata = new
{
bundleId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
tenantId = "00000000-0000-0000-0000-000000000001",
kind = 2,
status = 3,
rootHash = new string('f', 64),
storageKey = "evidence/bundle.tgz",
createdAt = "2025-12-07T10:30:00Z",
sealedAt = "2025-12-07T10:30:05Z"
};
CreateTgzBundle(bundlePath, manifestJson, signature, bundleMetadata);
return bundlePath;
}
private string CreateBundleWithoutTimestamp()
{
var bundlePath = Path.Combine(_tempDir, $"bundle-no-tsa-{Guid.NewGuid():N}.tgz");
var manifest = new
{
bundleId = "b2c3d4e5-f6a7-8901-bcde-f23456789012",
tenantId = "00000000-0000-0000-0000-000000000001",
kind = 2,
createdAt = "2025-12-07T10:30:00Z",
entries = Array.Empty<object>()
};
var manifestJson = JsonSerializer.Serialize(manifest);
var manifestPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(manifestJson));
var signature = new
{
payloadType = "application/vnd.stella.evidence.manifest+json",
payload = manifestPayload,
signature = Convert.ToBase64String(Encoding.UTF8.GetBytes("test-signature")),
keyId = "key-1",
algorithm = "ES256",
provider = "StellaOps",
signedAt = "2025-12-07T10:30:05Z"
// No timestampedAt, timestampAuthority, timestampToken
};
var bundleMetadata = new
{
bundleId = "b2c3d4e5-f6a7-8901-bcde-f23456789012",
tenantId = "00000000-0000-0000-0000-000000000001",
kind = 2,
status = 3,
rootHash = new string('e', 64),
storageKey = "evidence/bundle.tgz",
createdAt = "2025-12-07T10:30:00Z",
sealedAt = "2025-12-07T10:30:05Z"
};
CreateTgzBundle(bundlePath, manifestJson, signature, bundleMetadata);
return bundlePath;
}
private string CreatePortableBundle()
{
var bundlePath = Path.Combine(_tempDir, $"portable-{Guid.NewGuid():N}.tgz");
var manifest = new
{
bundleId = "c3d4e5f6-a7b8-9012-cdef-345678901234",
kind = 1,
createdAt = "2025-12-07T10:30:00Z",
entries = Array.Empty<object>()
};
var manifestJson = JsonSerializer.Serialize(manifest);
var manifestPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(manifestJson));
var signature = new
{
payloadType = "application/vnd.stella.evidence.manifest+json",
payload = manifestPayload,
signature = Convert.ToBase64String(Encoding.UTF8.GetBytes("test-signature")),
keyId = "key-1",
algorithm = "ES256",
provider = "StellaOps",
signedAt = "2025-12-07T10:30:05Z",
timestampedAt = "2025-12-07T10:30:06Z",
timestampAuthority = "tsa.default",
timestampToken = Convert.ToBase64String(Encoding.UTF8.GetBytes("tsa-token"))
};
var bundleMetadata = new
{
bundleId = "c3d4e5f6-a7b8-9012-cdef-345678901234",
kind = 1,
status = 3,
rootHash = new string('d', 64),
createdAt = "2025-12-07T10:30:00Z",
sealedAt = "2025-12-07T10:30:05Z",
portableGeneratedAt = "2025-12-07T10:35:00Z" // Indicates portable bundle
};
CreateTgzBundle(bundlePath, manifestJson, signature, bundleMetadata);
return bundlePath;
}
private static void CreateTgzBundle(string bundlePath, string manifestJson, object signature, object bundleMetadata)
{
using var memoryStream = new MemoryStream();
using (var gzipStream = new GZipStream(memoryStream, CompressionLevel.Optimal, leaveOpen: true))
using (var tarWriter = new TarWriter(gzipStream))
{
AddTarEntry(tarWriter, "manifest.json", manifestJson);
AddTarEntry(tarWriter, "signature.json", JsonSerializer.Serialize(signature));
AddTarEntry(tarWriter, "bundle.json", JsonSerializer.Serialize(bundleMetadata));
AddTarEntry(tarWriter, "checksums.txt", $"# checksums\n{new string('f', 64)} sbom/cyclonedx.json\n");
}
memoryStream.Position = 0;
using var fileStream = File.Create(bundlePath);
memoryStream.CopyTo(fileStream);
}
private static void AddTarEntry(TarWriter writer, string name, string content)
{
var entry = new PaxTarEntry(TarEntryType.RegularFile, name)
{
Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead,
ModificationTime = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
};
var bytes = Encoding.UTF8.GetBytes(content);
entry.DataStream = new MemoryStream(bytes);
writer.WriteEntry(entry);
}
}