tests fixes and sprints work
This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BundleVerifierTests.cs
|
||||
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
|
||||
// Task: GCB-003 - Implement standalone offline verifier
|
||||
// Description: Unit tests for BundleVerifier standalone verification logic
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Verifier.Tests;
|
||||
|
||||
public sealed class BundleVerifierTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
private readonly BundleVerifier _sut;
|
||||
|
||||
public BundleVerifierTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"verifier-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
_sut = new BundleVerifier();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
#region Verify Tests
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_NonexistentBundle_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = Path.Combine(_tempDir, "nonexistent.tar.gz"),
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(2); // Error
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidBundle_ReturnsPassed()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = false,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = true,
|
||||
VerifyPairs = true,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0); // Passed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_BundleWithBadDigest_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundleWithBadDigest();
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = false,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = true,
|
||||
VerifyPairs = false,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(1); // Failed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_UnsignedBundle_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = true,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = false,
|
||||
VerifyPairs = false,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(1); // Warning treated as failure
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithOutputReport_WritesReport()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var reportPath = Path.Combine(_tempDir, "report.md");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = false,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = true,
|
||||
VerifyPairs = true,
|
||||
OutputPath = reportPath,
|
||||
OutputFormat = ReportFormat.Markdown,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
File.Exists(reportPath).Should().BeTrue();
|
||||
var content = await File.ReadAllTextAsync(reportPath);
|
||||
content.Should().Contain("Bundle Verification Report");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithJsonReport_WritesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var reportPath = Path.Combine(_tempDir, "report.json");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
VerifySignatures = false,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = true,
|
||||
VerifyPairs = true,
|
||||
OutputPath = reportPath,
|
||||
OutputFormat = ReportFormat.Json,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
File.Exists(reportPath).Should().BeTrue();
|
||||
var content = await File.ReadAllTextAsync(reportPath);
|
||||
var json = JsonDocument.Parse(content);
|
||||
json.RootElement.GetProperty("overallStatus").GetString().Should().Be("Passed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithTrustedKeys_ValidatesSignerKey()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundleWithSignature("trusted-key-id");
|
||||
var trustedKeysPath = CreateTrustedKeysFile(["trusted-key-id"]);
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
TrustedKeysPath = trustedKeysPath,
|
||||
VerifySignatures = true,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = false,
|
||||
VerifyPairs = false,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0); // Key is trusted
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithUntrustedKey_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundleWithSignature("untrusted-key-id");
|
||||
var trustedKeysPath = CreateTrustedKeysFile(["trusted-key-id"]);
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
TrustedKeysPath = trustedKeysPath,
|
||||
VerifySignatures = true,
|
||||
VerifyTimestamps = false,
|
||||
VerifyDigests = false,
|
||||
VerifyPairs = false,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(1); // Key not trusted
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WithCancellation_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
var options = new VerifierOptions
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
Quiet = true
|
||||
};
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
await cts.CancelAsync();
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.VerifyAsync(options, cts.Token);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(2); // Error (cancelled)
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ShowInfo Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ShowInfoAsync_ValidBundle_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.ShowInfoAsync(bundlePath, ReportFormat.Text, quiet: true);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowInfoAsync_NonexistentBundle_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = Path.Combine(_tempDir, "nonexistent.tar.gz");
|
||||
|
||||
// Act
|
||||
var exitCode = await _sut.ShowInfoAsync(bundlePath, ReportFormat.Text, quiet: true);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private string CreateTestBundle(string package, string advisoryId, string distribution)
|
||||
{
|
||||
var stagingDir = Path.Combine(_tempDir, $"staging-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(stagingDir);
|
||||
|
||||
var pairId = $"{package}-{advisoryId}-{distribution}";
|
||||
var pairDir = Path.Combine(stagingDir, "pairs", pairId);
|
||||
Directory.CreateDirectory(pairDir);
|
||||
|
||||
// Create binaries
|
||||
File.WriteAllBytes(Path.Combine(pairDir, "pre.bin"), [1, 2, 3, 4]);
|
||||
File.WriteAllBytes(Path.Combine(pairDir, "post.bin"), [5, 6, 7, 8]);
|
||||
|
||||
// Create SBOM
|
||||
var sbom = new { spdxVersion = "SPDX-3.0.1", name = $"{package}-sbom" };
|
||||
var sbomContent = JsonSerializer.SerializeToUtf8Bytes(sbom);
|
||||
File.WriteAllBytes(Path.Combine(pairDir, "sbom.spdx.json"), sbomContent);
|
||||
var sbomDigest = ComputeHash(sbomContent);
|
||||
|
||||
// Create delta-sig
|
||||
var predicate = new { payloadType = "application/vnd.stella-ops.delta-sig+json", payload = "test" };
|
||||
var predicateContent = JsonSerializer.SerializeToUtf8Bytes(predicate);
|
||||
File.WriteAllBytes(Path.Combine(pairDir, "delta-sig.dsse.json"), predicateContent);
|
||||
var predicateDigest = ComputeHash(predicateContent);
|
||||
|
||||
// Create manifest
|
||||
var manifest = new
|
||||
{
|
||||
bundleId = $"test-bundle-{Guid.NewGuid():N}",
|
||||
schemaVersion = "1.0.0",
|
||||
createdAt = DateTimeOffset.UtcNow,
|
||||
generator = "BundleVerifierTests",
|
||||
pairs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
pairId,
|
||||
package,
|
||||
advisoryId,
|
||||
distribution,
|
||||
sbomDigest,
|
||||
deltaSigDigest = predicateDigest
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(stagingDir, "manifest.json"),
|
||||
JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
return CreateTarball(stagingDir);
|
||||
}
|
||||
|
||||
private string CreateTestBundleWithBadDigest()
|
||||
{
|
||||
var stagingDir = Path.Combine(_tempDir, $"staging-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(stagingDir);
|
||||
|
||||
var pairId = "openssl-CVE-2024-1234-debian";
|
||||
var pairDir = Path.Combine(stagingDir, "pairs", pairId);
|
||||
Directory.CreateDirectory(pairDir);
|
||||
|
||||
// Create SBOM with wrong digest in manifest
|
||||
var sbom = new { spdxVersion = "SPDX-3.0.1", name = "openssl-sbom" };
|
||||
File.WriteAllText(
|
||||
Path.Combine(pairDir, "sbom.spdx.json"),
|
||||
JsonSerializer.Serialize(sbom));
|
||||
|
||||
var manifest = new
|
||||
{
|
||||
bundleId = $"test-bundle-{Guid.NewGuid():N}",
|
||||
schemaVersion = "1.0.0",
|
||||
createdAt = DateTimeOffset.UtcNow,
|
||||
generator = "Test",
|
||||
pairs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
pairId,
|
||||
package = "openssl",
|
||||
advisoryId = "CVE-2024-1234",
|
||||
distribution = "debian",
|
||||
sbomDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000", // Wrong
|
||||
deltaSigDigest = (string?)null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(stagingDir, "manifest.json"),
|
||||
JsonSerializer.Serialize(manifest));
|
||||
|
||||
return CreateTarball(stagingDir);
|
||||
}
|
||||
|
||||
private string CreateTestBundleWithSignature(string keyId)
|
||||
{
|
||||
var stagingDir = Path.Combine(_tempDir, $"staging-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(stagingDir);
|
||||
|
||||
var manifest = new
|
||||
{
|
||||
bundleId = $"test-bundle-{Guid.NewGuid():N}",
|
||||
schemaVersion = "1.0.0",
|
||||
createdAt = DateTimeOffset.UtcNow,
|
||||
generator = "Test",
|
||||
pairs = Array.Empty<object>()
|
||||
};
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(stagingDir, "manifest.json"),
|
||||
JsonSerializer.Serialize(manifest));
|
||||
|
||||
var signature = new
|
||||
{
|
||||
signatureType = "cosign",
|
||||
keyId,
|
||||
placeholder = false
|
||||
};
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(stagingDir, "manifest.json.sig"),
|
||||
JsonSerializer.Serialize(signature));
|
||||
|
||||
return CreateTarball(stagingDir);
|
||||
}
|
||||
|
||||
private string CreateTrustedKeysFile(string[] keyIds)
|
||||
{
|
||||
var path = Path.Combine(_tempDir, $"trusted-keys-{Guid.NewGuid():N}.json");
|
||||
var keys = new { keyIds };
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(keys));
|
||||
return path;
|
||||
}
|
||||
|
||||
private string CreateTarball(string sourceDir)
|
||||
{
|
||||
var tarPath = Path.Combine(_tempDir, $"{Guid.NewGuid():N}.tar.gz");
|
||||
|
||||
var tempTar = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
using (var tarStream = File.Create(tempTar))
|
||||
{
|
||||
System.Formats.Tar.TarFile.CreateFromDirectory(
|
||||
sourceDir,
|
||||
tarStream,
|
||||
includeBaseDirectory: false);
|
||||
}
|
||||
|
||||
using var inputStream = File.OpenRead(tempTar);
|
||||
using var outputStream = File.Create(tarPath);
|
||||
using var gzipStream = new GZipStream(outputStream, CompressionLevel.Optimal);
|
||||
inputStream.CopyTo(gzipStream);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempTar))
|
||||
{
|
||||
File.Delete(tempTar);
|
||||
}
|
||||
|
||||
Directory.Delete(sourceDir, recursive: true);
|
||||
}
|
||||
|
||||
return tarPath;
|
||||
}
|
||||
|
||||
private static string ComputeHash(byte[] data)
|
||||
{
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(data);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
StellaOps.Verifier.Tests.csproj
|
||||
Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
|
||||
Task: GCB-003 - Implement standalone offline verifier
|
||||
Description: Unit tests for standalone bundle verifier
|
||||
-->
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Verifier.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user