Files
git.stella-ops.org/src/BinaryIndex/__Tests/StellaOps.BinaryIndex.GroundTruth.Reproducible.Tests/BundleImportServiceTests.cs
2026-01-22 19:08:46 +02:00

653 lines
20 KiB
C#

// -----------------------------------------------------------------------------
// BundleImportServiceTests.cs
// Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification
// Task: GCB-002 - Implement offline corpus bundle import and verification
// Description: Unit tests for BundleImportService corpus bundle import and verification
// -----------------------------------------------------------------------------
using System.IO.Compression;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.BinaryIndex.GroundTruth.Reproducible.Models;
using Xunit;
namespace StellaOps.BinaryIndex.GroundTruth.Reproducible.Tests;
public sealed class BundleImportServiceTests : IDisposable
{
private readonly string _tempDir;
private readonly string _tempBundleDir;
private readonly BundleImportService _sut;
public BundleImportServiceTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"import-test-{Guid.NewGuid():N}");
_tempBundleDir = Path.Combine(Path.GetTempPath(), $"bundle-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
Directory.CreateDirectory(_tempBundleDir);
var options = Options.Create(new BundleImportOptions
{
StagingDirectory = Path.Combine(Path.GetTempPath(), "import-staging-test")
});
_sut = new BundleImportService(
options,
NullLogger<BundleImportService>.Instance);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
if (Directory.Exists(_tempBundleDir))
{
Directory.Delete(_tempBundleDir, recursive: true);
}
}
#region Validation Tests
[Fact]
public async Task ValidateAsync_NonexistentFile_ReturnsInvalid()
{
// Arrange
var bundlePath = Path.Combine(_tempDir, "nonexistent.tar.gz");
// Act
var result = await _sut.ValidateAsync(bundlePath);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("not found"));
}
[Fact]
public async Task ValidateAsync_ValidBundle_ReturnsValid()
{
// Arrange
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
// Act
var result = await _sut.ValidateAsync(bundlePath);
// Assert
result.IsValid.Should().BeTrue();
result.Metadata.Should().NotBeNull();
result.Metadata!.BundleId.Should().NotBeNullOrEmpty();
result.Metadata.SchemaVersion.Should().Be("1.0.0");
result.Metadata.PairCount.Should().Be(1);
}
[Fact]
public async Task ValidateAsync_MissingManifest_ReturnsInvalid()
{
// Arrange
var bundlePath = CreateTestBundleWithoutManifest();
// Act
var result = await _sut.ValidateAsync(bundlePath);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().Contain(e => e.Contains("manifest"));
}
#endregion
#region Import Tests
[Fact]
public async Task ImportAsync_NonexistentFile_ReturnsFailed()
{
// Arrange
var request = new BundleImportRequest
{
InputPath = Path.Combine(_tempDir, "nonexistent.tar.gz")
};
// Act
var result = await _sut.ImportAsync(request);
// Assert
result.Success.Should().BeFalse();
result.OverallStatus.Should().Be(VerificationStatus.Failed);
result.Error.Should().Contain("not found");
}
[Fact]
public async Task ImportAsync_ValidBundle_ReturnsSuccess()
{
// Arrange
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
var request = new BundleImportRequest
{
InputPath = bundlePath,
VerifySignatures = false,
VerifyTimestamps = false,
VerifyDigests = true,
RunMatcher = true
};
// Act
var result = await _sut.ImportAsync(request);
// Assert
result.Success.Should().BeTrue();
result.OverallStatus.Should().Be(VerificationStatus.Passed);
result.Metadata.Should().NotBeNull();
result.DigestResult.Should().NotBeNull();
result.DigestResult!.Passed.Should().BeTrue();
}
[Fact]
public async Task ImportAsync_WithSignatureVerification_FailsForUnsignedBundle()
{
// Arrange
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
var request = new BundleImportRequest
{
InputPath = bundlePath,
VerifySignatures = true,
VerifyTimestamps = false,
VerifyDigests = false,
RunMatcher = false
};
// Act
var result = await _sut.ImportAsync(request);
// Assert
result.SignatureResult.Should().NotBeNull();
result.SignatureResult!.Passed.Should().BeFalse();
result.OverallStatus.Should().Be(VerificationStatus.Warning);
}
[Fact]
public async Task ImportAsync_WithPlaceholderSignature_FailsVerification()
{
// Arrange
var bundlePath = CreateTestBundleWithPlaceholderSignature();
var request = new BundleImportRequest
{
InputPath = bundlePath,
VerifySignatures = true,
VerifyTimestamps = false,
VerifyDigests = false,
RunMatcher = false
};
// Act
var result = await _sut.ImportAsync(request);
// Assert
result.SignatureResult.Should().NotBeNull();
result.SignatureResult!.Passed.Should().BeFalse();
result.SignatureResult.Error.Should().Contain("placeholder");
}
[Fact]
public async Task ImportAsync_DigestMismatch_ReturnsFailed()
{
// Arrange
var bundlePath = CreateTestBundleWithBadDigest();
var request = new BundleImportRequest
{
InputPath = bundlePath,
VerifySignatures = false,
VerifyTimestamps = false,
VerifyDigests = true,
RunMatcher = false
};
// Act
var result = await _sut.ImportAsync(request);
// Assert
result.Success.Should().BeFalse();
result.OverallStatus.Should().Be(VerificationStatus.Failed);
result.DigestResult.Should().NotBeNull();
result.DigestResult!.Passed.Should().BeFalse();
result.DigestResult.Mismatches.Should().NotBeEmpty();
}
[Fact]
public async Task ImportAsync_WithPairVerification_VerifiesPairs()
{
// Arrange
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
var request = new BundleImportRequest
{
InputPath = bundlePath,
VerifySignatures = false,
VerifyTimestamps = false,
VerifyDigests = false,
RunMatcher = true
};
// Act
var result = await _sut.ImportAsync(request);
// Assert
result.PairResults.Should().HaveCount(1);
var pair = result.PairResults[0];
pair.Package.Should().Be("openssl");
pair.AdvisoryId.Should().Be("CVE-2024-1234");
pair.Passed.Should().BeTrue();
}
[Fact]
public async Task ImportAsync_WithProgress_ReportsProgress()
{
// Arrange
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
var progressReports = new List<BundleImportProgress>();
var progress = new Progress<BundleImportProgress>(p => progressReports.Add(p));
var request = new BundleImportRequest
{
InputPath = bundlePath,
VerifySignatures = false,
VerifyTimestamps = false,
VerifyDigests = true,
RunMatcher = true
};
// Act
var result = await _sut.ImportAsync(request, progress);
// Wait for progress reports
await Task.Delay(100);
// Assert
result.Success.Should().BeTrue();
progressReports.Should().NotBeEmpty();
progressReports.Select(p => p.Stage).Should().Contain("Extracting bundle");
}
[Fact]
public async Task ImportAsync_WithCancellation_ThrowsCancelled()
{
// Arrange
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
var request = new BundleImportRequest
{
InputPath = bundlePath
};
using var cts = new CancellationTokenSource();
await cts.CancelAsync();
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(
() => _sut.ImportAsync(request, cancellationToken: cts.Token));
}
#endregion
#region Extract Tests
[Fact]
public async Task ExtractAsync_ValidBundle_ExtractsContents()
{
// Arrange
var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian");
var extractPath = Path.Combine(_tempDir, "extracted");
// Act
var resultPath = await _sut.ExtractAsync(bundlePath, extractPath);
// Assert
resultPath.Should().Be(extractPath);
Directory.Exists(extractPath).Should().BeTrue();
File.Exists(Path.Combine(extractPath, "manifest.json")).Should().BeTrue();
}
[Fact]
public async Task ExtractAsync_NonexistentFile_ThrowsException()
{
// Arrange
var bundlePath = Path.Combine(_tempDir, "nonexistent.tar.gz");
var extractPath = Path.Combine(_tempDir, "extracted");
// Act & Assert
await Assert.ThrowsAsync<FileNotFoundException>(
() => _sut.ExtractAsync(bundlePath, extractPath));
}
#endregion
#region Report Generation Tests
[Fact]
public async Task GenerateReportAsync_MarkdownFormat_GeneratesMarkdown()
{
// Arrange
var result = CreateTestImportResult();
var outputPath = Path.Combine(_tempDir, "report");
// Act
var reportPath = await _sut.GenerateReportAsync(
result,
BundleReportFormat.Markdown,
outputPath);
// Assert
reportPath.Should().EndWith(".md");
File.Exists(reportPath).Should().BeTrue();
var content = await File.ReadAllTextAsync(reportPath);
content.Should().Contain("# Bundle Verification Report");
content.Should().Contain("PASSED");
}
[Fact]
public async Task GenerateReportAsync_JsonFormat_GeneratesJson()
{
// Arrange
var result = CreateTestImportResult();
var outputPath = Path.Combine(_tempDir, "report");
// Act
var reportPath = await _sut.GenerateReportAsync(
result,
BundleReportFormat.Json,
outputPath);
// Assert
reportPath.Should().EndWith(".json");
File.Exists(reportPath).Should().BeTrue();
var content = await File.ReadAllTextAsync(reportPath);
var json = JsonDocument.Parse(content);
json.RootElement.GetProperty("success").GetBoolean().Should().BeTrue();
json.RootElement.GetProperty("overallStatus").GetString().Should().Be("Passed");
}
[Fact]
public async Task GenerateReportAsync_HtmlFormat_GeneratesHtml()
{
// Arrange
var result = CreateTestImportResult();
var outputPath = Path.Combine(_tempDir, "report");
// Act
var reportPath = await _sut.GenerateReportAsync(
result,
BundleReportFormat.Html,
outputPath);
// Assert
reportPath.Should().EndWith(".html");
File.Exists(reportPath).Should().BeTrue();
var content = await File.ReadAllTextAsync(reportPath);
content.Should().Contain("<html");
content.Should().Contain("Bundle Verification Report");
}
[Fact]
public async Task GenerateReportAsync_WithFailedResult_IncludesErrors()
{
// Arrange
var result = BundleImportResult.Failed("Test error message");
var outputPath = Path.Combine(_tempDir, "failed-report");
// Act
var reportPath = await _sut.GenerateReportAsync(
result,
BundleReportFormat.Markdown,
outputPath);
// Assert
var content = await File.ReadAllTextAsync(reportPath);
content.Should().Contain("FAILED");
content.Should().Contain("Test error message");
}
#endregion
#region Helper Methods
private string CreateTestBundle(string package, string advisoryId, string distribution)
{
var stagingDir = Path.Combine(_tempBundleDir, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(stagingDir);
// Create pairs directory
var pairId = $"{package}-{advisoryId}-{distribution}";
var pairDir = Path.Combine(stagingDir, "pairs", pairId);
Directory.CreateDirectory(pairDir);
// Create pre and post binaries
File.WriteAllBytes(Path.Combine(pairDir, "pre.bin"), new byte[] { 1, 2, 3, 4 });
File.WriteAllBytes(Path.Combine(pairDir, "post.bin"), new byte[] { 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 predicate
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 = "BundleImportServiceTests",
pairs = new[]
{
new
{
pairId,
package,
advisoryId,
distribution,
vulnerableVersion = "1.0.0",
patchedVersion = "1.0.1",
debugSymbolsIncluded = false,
sbomDigest,
deltaSigDigest = predicateDigest
}
}
};
File.WriteAllText(
Path.Combine(stagingDir, "manifest.json"),
JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }));
// Create tarball
return CreateTarball(stagingDir);
}
private string CreateTestBundleWithoutManifest()
{
var stagingDir = Path.Combine(_tempBundleDir, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(stagingDir);
// Create some content but no manifest
var pairDir = Path.Combine(stagingDir, "pairs", "test-pair");
Directory.CreateDirectory(pairDir);
File.WriteAllBytes(Path.Combine(pairDir, "pre.bin"), new byte[] { 1, 2, 3, 4 });
return CreateTarball(stagingDir);
}
private string CreateTestBundleWithPlaceholderSignature()
{
var stagingDir = Path.Combine(_tempBundleDir, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(stagingDir);
// Create manifest
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));
// Create placeholder signature
var signature = new
{
signatureType = "cosign",
keyId = "test-key",
placeholder = true,
message = "Signing integration pending"
};
File.WriteAllText(
Path.Combine(stagingDir, "manifest.json.sig"),
JsonSerializer.Serialize(signature));
return CreateTarball(stagingDir);
}
private string CreateTestBundleWithBadDigest()
{
var stagingDir = Path.Combine(_tempBundleDir, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(stagingDir);
// Create pairs directory
var pairId = "openssl-CVE-2024-1234-debian";
var pairDir = Path.Combine(stagingDir, "pairs", pairId);
Directory.CreateDirectory(pairDir);
// Create SBOM with content that won't match the digest
var sbom = new { spdxVersion = "SPDX-3.0.1", name = "openssl-sbom" };
File.WriteAllText(
Path.Combine(pairDir, "sbom.spdx.json"),
JsonSerializer.Serialize(sbom));
// Create manifest with wrong digest
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 CreateTarball(string sourceDir)
{
var tarPath = Path.Combine(_tempBundleDir, $"{Guid.NewGuid():N}.tar.gz");
// Create tar
var tempTar = Path.GetTempFileName();
try
{
using (var tarStream = File.Create(tempTar))
{
System.Formats.Tar.TarFile.CreateFromDirectory(
sourceDir,
tarStream,
includeBaseDirectory: false);
}
// Gzip it
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);
}
// Cleanup staging
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()}";
}
private static BundleImportResult CreateTestImportResult()
{
return new BundleImportResult
{
Success = true,
OverallStatus = VerificationStatus.Passed,
ManifestDigest = "sha256:abc123",
Metadata = new BundleMetadata
{
BundleId = "test-bundle",
SchemaVersion = "1.0.0",
CreatedAt = DateTimeOffset.UtcNow,
Generator = "Test",
PairCount = 1,
TotalSizeBytes = 1024
},
SignatureResult = new SignatureVerificationResult
{
Passed = true,
SignatureCount = 1,
SignerKeyIds = ["test-key"]
},
DigestResult = new DigestVerificationResult
{
Passed = true,
TotalBlobs = 2,
MatchedBlobs = 2
},
PairResults =
[
new PairVerificationResult
{
PairId = "openssl-CVE-2024-1234-debian",
Package = "openssl",
AdvisoryId = "CVE-2024-1234",
Passed = true,
SbomStatus = VerificationStatus.Passed,
DeltaSigStatus = VerificationStatus.Passed,
MatcherStatus = VerificationStatus.Passed,
FunctionMatchRate = 0.95,
Duration = TimeSpan.FromSeconds(1.5)
}
],
Duration = TimeSpan.FromSeconds(5)
};
}
#endregion
}