Add unit tests for AST parsing and security sink detection

- Created `StellaOps.AuditPack.Tests.csproj` for unit testing the AuditPack library.
- Implemented comprehensive unit tests in `index.test.js` for AST parsing, covering various JavaScript and TypeScript constructs including functions, classes, decorators, and JSX.
- Added `sink-detect.test.js` to test security sink detection patterns, validating command injection, SQL injection, file write, deserialization, SSRF, NoSQL injection, and more.
- Included tests for taint source detection in various contexts such as Express, Koa, and AWS Lambda.
This commit is contained in:
StellaOps Bot
2025-12-23 09:23:42 +02:00
parent 7e384ab610
commit 56e2dc01ee
96 changed files with 8555 additions and 1455 deletions

View File

@@ -0,0 +1,326 @@
// -----------------------------------------------------------------------------
// AirGapTrustStoreIntegrationTests.cs
// Sprint: SPRINT_4300_0001_0002 (One-Command Audit Replay CLI)
// Description: Unit tests for AirGapTrustStoreIntegration.
// -----------------------------------------------------------------------------
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.AuditPack.Services;
namespace StellaOps.AuditPack.Tests;
public class AirGapTrustStoreIntegrationTests : IDisposable
{
private readonly string _tempDir;
public AirGapTrustStoreIntegrationTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"trust-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
[Fact]
public async Task LoadFromDirectoryAsync_LoadsPemFiles()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
var keyPem = GenerateEcdsaPublicKeyPem();
await File.WriteAllTextAsync(Path.Combine(_tempDir, "test-key.pem"), keyPem);
// Act
var result = await integration.LoadFromDirectoryAsync(_tempDir);
// Assert
Assert.True(result.Success);
Assert.Equal(1, result.LoadedCount);
Assert.Contains("test-key", result.KeyIds!);
}
[Fact]
public async Task LoadFromDirectoryAsync_FailsWithNonExistentDirectory()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
// Act
var result = await integration.LoadFromDirectoryAsync("/nonexistent/path");
// Assert
Assert.False(result.Success);
Assert.Contains("not found", result.Error);
}
[Fact]
public async Task LoadFromDirectoryAsync_FailsWithEmptyPath()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
// Act
var result = await integration.LoadFromDirectoryAsync("");
// Assert
Assert.False(result.Success);
Assert.Contains("required", result.Error);
}
[Fact]
public async Task LoadFromDirectoryAsync_LoadsFromManifest()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
var keyPem = GenerateEcdsaPublicKeyPem();
await File.WriteAllTextAsync(Path.Combine(_tempDir, "signing-key.pem"), keyPem);
var manifest = new
{
roots = new[]
{
new
{
keyId = "stella-signing-key-001",
relativePath = "signing-key.pem",
algorithm = "ES256",
purpose = "signing"
}
}
};
await File.WriteAllTextAsync(
Path.Combine(_tempDir, "trust-manifest.json"),
JsonSerializer.Serialize(manifest, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
// Act
var result = await integration.LoadFromDirectoryAsync(_tempDir);
// Assert
Assert.True(result.Success);
Assert.Equal(1, result.LoadedCount);
Assert.Contains("stella-signing-key-001", result.KeyIds!);
}
[Fact]
public void LoadFromBundle_ParsesJsonBundle()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
var keyPem = GenerateEcdsaPublicKeyPem();
var bundle = new
{
roots = new[]
{
new
{
keyId = "bundle-key-001",
publicKeyPem = keyPem,
algorithm = "ES256",
purpose = "signing"
}
}
};
var bundleBytes = Encoding.UTF8.GetBytes(
JsonSerializer.Serialize(bundle, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
// Act
var result = integration.LoadFromBundle(bundleBytes);
// Assert
Assert.True(result.Success);
Assert.Equal(1, result.LoadedCount);
Assert.Contains("bundle-key-001", result.KeyIds!);
}
[Fact]
public void LoadFromBundle_FailsWithEmptyContent()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
// Act
var result = integration.LoadFromBundle([]);
// Assert
Assert.False(result.Success);
Assert.Contains("empty", result.Error);
}
[Fact]
public void LoadFromBundle_FailsWithInvalidJson()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
var invalidJson = Encoding.UTF8.GetBytes("not valid json");
// Act
var result = integration.LoadFromBundle(invalidJson);
// Assert
Assert.False(result.Success);
}
[Fact]
public async Task GetPublicKey_ReturnsKey()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
var keyPem = GenerateEcdsaPublicKeyPem();
await File.WriteAllTextAsync(Path.Combine(_tempDir, "test-key.pem"), keyPem);
await integration.LoadFromDirectoryAsync(_tempDir);
// Act
var result = integration.GetPublicKey("test-key");
// Assert
Assert.True(result.Found);
Assert.Equal("test-key", result.KeyId);
Assert.NotNull(result.KeyBytes);
}
[Fact]
public async Task GetPublicKey_ReturnsNotFound()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
await integration.LoadFromDirectoryAsync(_tempDir);
// Act
var result = integration.GetPublicKey("nonexistent-key");
// Assert
Assert.False(result.Found);
Assert.Equal("nonexistent-key", result.KeyId);
}
[Fact]
public async Task GetPublicKey_DetectsExpiredKey()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
var keyPem = GenerateEcdsaPublicKeyPem();
await File.WriteAllTextAsync(Path.Combine(_tempDir, "expired-key.pem"), keyPem);
var manifest = new
{
roots = new[]
{
new
{
keyId = "expired-key",
relativePath = "expired-key.pem",
algorithm = "ES256",
expiresAt = DateTimeOffset.UtcNow.AddDays(-1)
}
}
};
await File.WriteAllTextAsync(
Path.Combine(_tempDir, "trust-manifest.json"),
JsonSerializer.Serialize(manifest, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
await integration.LoadFromDirectoryAsync(_tempDir);
// Act
var result = integration.GetPublicKey("expired-key");
// Assert
Assert.True(result.Found);
Assert.True(result.Expired);
Assert.Contains("expired", result.Warning);
}
[Fact]
public async Task CreateVerificationKey_ReturnsEcdsaKey()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
var keyPem = GenerateEcdsaPublicKeyPem();
await File.WriteAllTextAsync(Path.Combine(_tempDir, "ecdsa-key.pem"), keyPem);
// Use manifest to explicitly set algorithm (SPKI format doesn't include algorithm in PEM header)
var manifest = new
{
roots = new[]
{
new
{
keyId = "ecdsa-key",
relativePath = "ecdsa-key.pem",
algorithm = "ES256",
purpose = "signing"
}
}
};
await File.WriteAllTextAsync(
Path.Combine(_tempDir, "trust-manifest.json"),
JsonSerializer.Serialize(manifest, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
await integration.LoadFromDirectoryAsync(_tempDir);
// Act
var key = integration.CreateVerificationKey("ecdsa-key");
// Assert
Assert.NotNull(key);
Assert.IsAssignableFrom<ECDsa>(key);
key.Dispose();
}
[Fact]
public async Task CreateVerificationKey_ReturnsNullForMissingKey()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
await integration.LoadFromDirectoryAsync(_tempDir);
// Act
var key = integration.CreateVerificationKey("nonexistent");
// Assert
Assert.Null(key);
}
[Fact]
public async Task GetAvailableKeyIds_ReturnsAllKeys()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
await File.WriteAllTextAsync(Path.Combine(_tempDir, "key1.pem"), GenerateEcdsaPublicKeyPem());
await File.WriteAllTextAsync(Path.Combine(_tempDir, "key2.pem"), GenerateEcdsaPublicKeyPem());
await integration.LoadFromDirectoryAsync(_tempDir);
// Act
var keyIds = integration.GetAvailableKeyIds();
// Assert
Assert.Equal(2, keyIds.Count);
Assert.Contains("key1", keyIds);
Assert.Contains("key2", keyIds);
}
[Fact]
public async Task Count_ReturnsCorrectValue()
{
// Arrange
var integration = new AirGapTrustStoreIntegration();
await File.WriteAllTextAsync(Path.Combine(_tempDir, "key1.pem"), GenerateEcdsaPublicKeyPem());
await File.WriteAllTextAsync(Path.Combine(_tempDir, "key2.pem"), GenerateEcdsaPublicKeyPem());
await integration.LoadFromDirectoryAsync(_tempDir);
// Act & Assert
Assert.Equal(2, integration.Count);
}
private static string GenerateEcdsaPublicKeyPem()
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
return ecdsa.ExportSubjectPublicKeyInfoPem();
}
}

View File

@@ -0,0 +1,276 @@
// -----------------------------------------------------------------------------
// AuditBundleWriterTests.cs
// Sprint: SPRINT_4300_0001_0002 (One-Command Audit Replay CLI)
// Description: Unit tests for AuditBundleWriter.
// -----------------------------------------------------------------------------
using System.Text;
using System.Text.Json;
using StellaOps.AuditPack.Services;
namespace StellaOps.AuditPack.Tests;
public class AuditBundleWriterTests : IDisposable
{
private readonly string _tempDir;
public AuditBundleWriterTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"audit-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
[Fact]
public async Task WriteAsync_CreatesValidBundle()
{
// Arrange
var writer = new AuditBundleWriter();
var outputPath = Path.Combine(_tempDir, "test-bundle.tar.gz");
var request = CreateValidRequest(outputPath);
// Act
var result = await writer.WriteAsync(request);
// Assert
Assert.True(result.Success, result.Error);
Assert.True(File.Exists(outputPath));
Assert.NotNull(result.BundleId);
Assert.NotNull(result.MerkleRoot);
Assert.NotNull(result.BundleDigest);
Assert.True(result.TotalSizeBytes > 0);
Assert.True(result.FileCount > 0);
}
[Fact]
public async Task WriteAsync_ComputesMerkleRoot()
{
// Arrange
var writer = new AuditBundleWriter();
var outputPath = Path.Combine(_tempDir, "merkle-test.tar.gz");
var request = CreateValidRequest(outputPath);
// Act
var result = await writer.WriteAsync(request);
// Assert
Assert.True(result.Success);
Assert.NotNull(result.MerkleRoot);
Assert.StartsWith("sha256:", result.MerkleRoot);
Assert.Equal(71, result.MerkleRoot.Length); // sha256: + 64 hex chars
}
[Fact]
public async Task WriteAsync_SignsManifest_WhenSignIsTrue()
{
// Arrange
var writer = new AuditBundleWriter();
var outputPath = Path.Combine(_tempDir, "signed-test.tar.gz");
var request = CreateValidRequest(outputPath) with { Sign = true };
// Act
var result = await writer.WriteAsync(request);
// Assert
Assert.True(result.Success);
Assert.True(result.Signed);
Assert.NotNull(result.SigningKeyId);
Assert.NotNull(result.SigningAlgorithm);
}
[Fact]
public async Task WriteAsync_DoesNotSign_WhenSignIsFalse()
{
// Arrange
var writer = new AuditBundleWriter();
var outputPath = Path.Combine(_tempDir, "unsigned-test.tar.gz");
var request = CreateValidRequest(outputPath) with { Sign = false };
// Act
var result = await writer.WriteAsync(request);
// Assert
Assert.True(result.Success);
Assert.False(result.Signed);
Assert.Null(result.SigningKeyId);
}
[Fact]
public async Task WriteAsync_FailsWithoutSbom()
{
// Arrange
var writer = new AuditBundleWriter();
var outputPath = Path.Combine(_tempDir, "no-sbom.tar.gz");
var request = new AuditBundleWriteRequest
{
OutputPath = outputPath,
ScanId = "scan-001",
ImageRef = "test:latest",
ImageDigest = "sha256:abc123",
Decision = "pass",
Sbom = null!,
FeedsSnapshot = CreateFeedsSnapshot(),
PolicyBundle = CreatePolicyBundle(),
Verdict = CreateVerdict()
};
// Act
var result = await writer.WriteAsync(request);
// Assert
Assert.False(result.Success);
Assert.Contains("SBOM", result.Error);
}
[Fact]
public async Task WriteAsync_IncludesOptionalVex()
{
// Arrange
var writer = new AuditBundleWriter();
var outputPath = Path.Combine(_tempDir, "with-vex.tar.gz");
var vexContent = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
type = "https://openvex.dev/ns/v0.2.0",
statements = new[]
{
new { vulnerability = "CVE-2024-1234", status = "not_affected" }
}
}));
var request = CreateValidRequest(outputPath) with
{
VexStatements = vexContent
};
// Act
var result = await writer.WriteAsync(request);
// Assert
Assert.True(result.Success);
Assert.True(result.FileCount >= 5); // sbom, feeds, policy, verdict, vex
}
[Fact]
public async Task WriteAsync_AddsTimeAnchor()
{
// Arrange
var writer = new AuditBundleWriter();
var outputPath = Path.Combine(_tempDir, "with-anchor.tar.gz");
var request = CreateValidRequest(outputPath) with
{
TimeAnchor = new TimeAnchorInput
{
Timestamp = DateTimeOffset.UtcNow,
Source = "local"
}
};
// Act
var result = await writer.WriteAsync(request);
// Assert
Assert.True(result.Success);
}
[Fact]
public async Task WriteAsync_DeterministicMerkleRoot()
{
// Arrange
var writer = new AuditBundleWriter();
var sbom = CreateSbom();
var feeds = CreateFeedsSnapshot();
var policy = CreatePolicyBundle();
var verdict = CreateVerdict();
var request1 = new AuditBundleWriteRequest
{
OutputPath = Path.Combine(_tempDir, "det-1.tar.gz"),
ScanId = "scan-001",
ImageRef = "test:latest",
ImageDigest = "sha256:abc123",
Decision = "pass",
Sbom = sbom,
FeedsSnapshot = feeds,
PolicyBundle = policy,
Verdict = verdict,
Sign = false
};
var request2 = request1 with
{
OutputPath = Path.Combine(_tempDir, "det-2.tar.gz")
};
// Act
var result1 = await writer.WriteAsync(request1);
var result2 = await writer.WriteAsync(request2);
// Assert
Assert.True(result1.Success);
Assert.True(result2.Success);
Assert.Equal(result1.MerkleRoot, result2.MerkleRoot);
}
private AuditBundleWriteRequest CreateValidRequest(string outputPath)
{
return new AuditBundleWriteRequest
{
OutputPath = outputPath,
ScanId = "scan-001",
ImageRef = "test:latest",
ImageDigest = "sha256:abc123def456",
Decision = "pass",
Sbom = CreateSbom(),
FeedsSnapshot = CreateFeedsSnapshot(),
PolicyBundle = CreatePolicyBundle(),
Verdict = CreateVerdict(),
Sign = true
};
}
private static byte[] CreateSbom()
{
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
bomFormat = "CycloneDX",
specVersion = "1.6",
version = 1,
components = Array.Empty<object>()
}));
}
private static byte[] CreateFeedsSnapshot()
{
return Encoding.UTF8.GetBytes("{\"type\":\"feed-snapshot\"}\n");
}
private static byte[] CreatePolicyBundle()
{
// Minimal gzip content
return new byte[] { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
}
private static byte[] CreateVerdict()
{
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new
{
decision = "pass",
evaluatedAt = DateTimeOffset.UtcNow
}));
}
}

View File

@@ -0,0 +1,514 @@
// -----------------------------------------------------------------------------
// AuditReplayE2ETests.cs
// Sprint: SPRINT_4300_0001_0002 (One-Command Audit Replay CLI)
// Task: REPLAY-028 - E2E test: export -> transfer -> replay offline
// Description: End-to-end integration tests for audit bundle export and replay.
// -----------------------------------------------------------------------------
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.AuditPack.Models;
using StellaOps.AuditPack.Services;
namespace StellaOps.AuditPack.Tests;
/// <summary>
/// End-to-end integration tests that verify the complete audit bundle workflow:
/// export -> transfer -> replay offline.
/// </summary>
public class AuditReplayE2ETests : IDisposable
{
private readonly string _tempDir;
private readonly string _exportDir;
private readonly string _importDir;
public AuditReplayE2ETests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"e2e-test-{Guid.NewGuid():N}");
_exportDir = Path.Combine(_tempDir, "export");
_importDir = Path.Combine(_tempDir, "import");
Directory.CreateDirectory(_exportDir);
Directory.CreateDirectory(_importDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
[Fact]
public async Task E2E_ExportTransferReplayOffline_MatchingVerdict()
{
// ===== PHASE 1: EXPORT =====
// Create scan data
var scanId = $"scan-{Guid.NewGuid():N}";
var imageRef = "registry.example.com/app:v1.2.3";
var imageDigest = "sha256:abc123def456789";
var decision = "pass";
var sbom = CreateCycloneDxSbom(imageRef);
var feeds = CreateFeedsSnapshot();
var policy = CreatePolicyBundle();
var verdict = CreateVerdict(decision, scanId);
var vex = CreateVexStatements();
// Create audit bundle (unsigned for E2E test simplicity)
var writer = new AuditBundleWriter();
var bundlePath = Path.Combine(_exportDir, "audit-bundle.tar.gz");
var writeRequest = new AuditBundleWriteRequest
{
OutputPath = bundlePath,
ScanId = scanId,
ImageRef = imageRef,
ImageDigest = imageDigest,
Decision = decision,
Sbom = sbom,
FeedsSnapshot = feeds,
PolicyBundle = policy,
Verdict = verdict,
VexStatements = vex,
Sign = false, // Skip signing for unit test
TimeAnchor = new TimeAnchorInput
{
Timestamp = DateTimeOffset.UtcNow,
Source = "local-test"
}
};
var writeResult = await writer.WriteAsync(writeRequest);
// Assert export succeeded
Assert.True(writeResult.Success, $"Export failed: {writeResult.Error}");
Assert.True(File.Exists(bundlePath), "Bundle file not created");
Assert.NotNull(writeResult.MerkleRoot);
Assert.NotNull(writeResult.BundleDigest);
// ===== PHASE 2: TRANSFER (simulate by copying) =====
var transferredBundlePath = Path.Combine(_importDir, "transferred-bundle.tar.gz");
File.Copy(bundlePath, transferredBundlePath);
// Verify transfer integrity
var originalHash = await ComputeFileHashAsync(bundlePath);
var transferredHash = await ComputeFileHashAsync(transferredBundlePath);
Assert.Equal(originalHash, transferredHash);
// ===== PHASE 3: REPLAY OFFLINE =====
// Read the bundle
var reader = new AuditBundleReader();
var readRequest = new AuditBundleReadRequest
{
BundlePath = transferredBundlePath,
VerifySignature = false, // No signature in this test
VerifyMerkleRoot = true,
VerifyInputDigests = true,
LoadReplayInputs = true
};
var readResult = await reader.ReadAsync(readRequest);
// Assert read succeeded
Assert.True(readResult.Success, $"Read failed: {readResult.Error}");
Assert.True(readResult.MerkleRootVerified ?? false, "Merkle root validation failed");
Assert.True(readResult.InputDigestsVerified ?? false, "Input digests validation failed");
// Create isolated replay context
using var replayContext = new IsolatedReplayContext(new IsolatedReplayContextOptions
{
CleanupOnDispose = true,
EnforceOffline = true
});
var initResult = await replayContext.InitializeAsync(readResult);
Assert.True(initResult.Success, $"Replay context init failed: {initResult.Error}");
// Execute replay
var executor = new ReplayExecutor();
var replayResult = await executor.ExecuteAsync(
replayContext,
readResult.Manifest!,
new ReplayExecutionOptions
{
FailOnInputDrift = false,
DetailedDriftDetection = true
});
// Assert replay succeeded with matching verdict
Assert.True(replayResult.Success, $"Replay failed: {replayResult.Error}");
Assert.Equal(ReplayStatus.Match, replayResult.Status);
Assert.True(replayResult.InputsVerified, "Inputs should be verified");
Assert.True(replayResult.DecisionMatches, "Decision should match");
Assert.Equal(decision, replayResult.OriginalDecision);
}
[Fact]
public async Task E2E_ReplayDetectsTamperedSbom()
{
// Setup
var scanId = $"scan-{Guid.NewGuid():N}";
var sbom = CreateCycloneDxSbom("app:v1");
var feeds = CreateFeedsSnapshot();
var policy = CreatePolicyBundle();
var verdict = CreateVerdict("pass", scanId);
// Export original bundle
var writer = new AuditBundleWriter();
var bundlePath = Path.Combine(_exportDir, "original.tar.gz");
var writeResult = await writer.WriteAsync(new AuditBundleWriteRequest
{
OutputPath = bundlePath,
ScanId = scanId,
ImageRef = "app:v1",
ImageDigest = "sha256:abc",
Decision = "pass",
Sbom = sbom,
FeedsSnapshot = feeds,
PolicyBundle = policy,
Verdict = verdict,
Sign = false
});
Assert.True(writeResult.Success);
// Export tampered bundle with modified SBOM
var tamperedSbom = CreateCycloneDxSbom("app:v1", addMaliciousComponent: true);
var tamperedBundlePath = Path.Combine(_importDir, "tampered.tar.gz");
var tamperedResult = await writer.WriteAsync(new AuditBundleWriteRequest
{
OutputPath = tamperedBundlePath,
ScanId = scanId,
ImageRef = "app:v1",
ImageDigest = "sha256:abc",
Decision = "pass",
Sbom = tamperedSbom, // Different SBOM
FeedsSnapshot = feeds,
PolicyBundle = policy,
Verdict = verdict,
Sign = false
});
Assert.True(tamperedResult.Success);
// Read both bundles
var reader = new AuditBundleReader();
var originalRead = await reader.ReadAsync(new AuditBundleReadRequest
{
BundlePath = bundlePath,
VerifySignature = false,
LoadReplayInputs = true
});
var tamperedRead = await reader.ReadAsync(new AuditBundleReadRequest
{
BundlePath = tamperedBundlePath,
VerifySignature = false,
LoadReplayInputs = true
});
// The merkle roots should differ
Assert.NotEqual(originalRead.Manifest?.MerkleRoot, tamperedRead.Manifest?.MerkleRoot);
// Input digests should differ
Assert.NotEqual(
originalRead.Manifest?.Inputs.SbomDigest,
tamperedRead.Manifest?.Inputs.SbomDigest);
}
[Fact]
public async Task E2E_DeterministicMerkleRoot_SameInputs()
{
// Create identical inputs
var sbom = CreateCycloneDxSbom("app:deterministic");
var feeds = CreateFeedsSnapshot();
var policy = CreatePolicyBundle();
var verdict = CreateVerdict("pass", "scan-deterministic");
var writer = new AuditBundleWriter();
// Write bundle 1
var bundle1Path = Path.Combine(_exportDir, "deterministic-1.tar.gz");
var result1 = await writer.WriteAsync(new AuditBundleWriteRequest
{
OutputPath = bundle1Path,
ScanId = "scan-deterministic",
ImageRef = "app:deterministic",
ImageDigest = "sha256:deterministic123",
Decision = "pass",
Sbom = sbom,
FeedsSnapshot = feeds,
PolicyBundle = policy,
Verdict = verdict,
Sign = false
});
// Write bundle 2 with same inputs
var bundle2Path = Path.Combine(_exportDir, "deterministic-2.tar.gz");
var result2 = await writer.WriteAsync(new AuditBundleWriteRequest
{
OutputPath = bundle2Path,
ScanId = "scan-deterministic",
ImageRef = "app:deterministic",
ImageDigest = "sha256:deterministic123",
Decision = "pass",
Sbom = sbom,
FeedsSnapshot = feeds,
PolicyBundle = policy,
Verdict = verdict,
Sign = false
});
// Merkle roots must be identical
Assert.True(result1.Success);
Assert.True(result2.Success);
Assert.Equal(result1.MerkleRoot, result2.MerkleRoot);
}
[Fact]
public async Task E2E_BundleContainsAllRequiredFiles()
{
// Setup
var sbom = CreateCycloneDxSbom("app:v1");
var feeds = CreateFeedsSnapshot();
var policy = CreatePolicyBundle();
var verdict = CreateVerdict("pass", "scan-files-test");
var vex = CreateVexStatements();
var writer = new AuditBundleWriter();
var bundlePath = Path.Combine(_exportDir, "files-test.tar.gz");
var writeResult = await writer.WriteAsync(new AuditBundleWriteRequest
{
OutputPath = bundlePath,
ScanId = "scan-files-test",
ImageRef = "app:v1",
ImageDigest = "sha256:abc",
Decision = "pass",
Sbom = sbom,
FeedsSnapshot = feeds,
PolicyBundle = policy,
Verdict = verdict,
VexStatements = vex,
Sign = false
});
Assert.True(writeResult.Success);
Assert.True(writeResult.FileCount >= 5, $"Expected at least 5 files, got {writeResult.FileCount}");
// Read and verify manifest contains all files
var reader = new AuditBundleReader();
var readResult = await reader.ReadAsync(new AuditBundleReadRequest
{
BundlePath = bundlePath,
VerifySignature = false
});
Assert.True(readResult.Success);
Assert.NotNull(readResult.Manifest);
Assert.NotEmpty(readResult.Manifest.Files);
// Verify essential files are present
var filePaths = readResult.Manifest.Files.Select(f => f.RelativePath).ToList();
Assert.Contains(filePaths, p => p.Contains("sbom"));
Assert.Contains(filePaths, p => p.Contains("feeds"));
Assert.Contains(filePaths, p => p.Contains("policy"));
Assert.Contains(filePaths, p => p.Contains("verdict"));
Assert.Contains(filePaths, p => p.Contains("vex"));
}
[Fact]
public async Task E2E_FullCycleWithTimeAnchor()
{
// Setup with explicit time anchor
var timestamp = new DateTimeOffset(2024, 6, 15, 12, 0, 0, TimeSpan.Zero);
var sbom = CreateCycloneDxSbom("app:time-test");
var feeds = CreateFeedsSnapshot();
var policy = CreatePolicyBundle();
var verdict = CreateVerdict("pass", "scan-time-test");
var writer = new AuditBundleWriter();
var bundlePath = Path.Combine(_exportDir, "time-anchor-test.tar.gz");
var writeResult = await writer.WriteAsync(new AuditBundleWriteRequest
{
OutputPath = bundlePath,
ScanId = "scan-time-test",
ImageRef = "app:time-test",
ImageDigest = "sha256:abc",
Decision = "pass",
Sbom = sbom,
FeedsSnapshot = feeds,
PolicyBundle = policy,
Verdict = verdict,
Sign = false,
TimeAnchor = new TimeAnchorInput
{
Timestamp = timestamp,
Source = "test-time-server"
}
});
Assert.True(writeResult.Success);
// Read and verify time anchor
var reader = new AuditBundleReader();
var readResult = await reader.ReadAsync(new AuditBundleReadRequest
{
BundlePath = bundlePath,
VerifySignature = false,
LoadReplayInputs = true
});
Assert.True(readResult.Success);
Assert.NotNull(readResult.Manifest?.TimeAnchor);
Assert.Equal(timestamp, readResult.Manifest.TimeAnchor.Timestamp);
Assert.Equal("test-time-server", readResult.Manifest.TimeAnchor.Source);
// Replay with time anchor context
using var context = new IsolatedReplayContext(new IsolatedReplayContextOptions
{
EvaluationTime = timestamp,
CleanupOnDispose = true
});
var initResult = await context.InitializeAsync(readResult);
Assert.True(initResult.Success);
Assert.Equal(timestamp, context.EvaluationTime);
}
#region Test Data Factories
private static byte[] CreateCycloneDxSbom(string imageRef, bool addMaliciousComponent = false)
{
var components = new List<object>
{
new { type = "library", name = "lodash", version = "4.17.21", purl = "pkg:npm/lodash@4.17.21" },
new { type = "library", name = "express", version = "4.18.2", purl = "pkg:npm/express@4.18.2" }
};
if (addMaliciousComponent)
{
components.Add(new { type = "library", name = "evil-package", version = "1.0.0", purl = "pkg:npm/evil-package@1.0.0" });
}
var sbom = new
{
bomFormat = "CycloneDX",
specVersion = "1.6",
version = 1,
serialNumber = $"urn:uuid:{Guid.NewGuid()}",
metadata = new
{
timestamp = DateTimeOffset.UtcNow.ToString("o"),
component = new { type = "container", name = imageRef }
},
components = components.ToArray()
};
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(sbom, new JsonSerializerOptions
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}));
}
private static byte[] CreateFeedsSnapshot()
{
var snapshot = new
{
type = "feed-snapshot",
version = "1.0",
timestamp = DateTimeOffset.UtcNow.ToString("o"),
sources = new[]
{
new { name = "nvd", lastSync = DateTimeOffset.UtcNow.AddHours(-1).ToString("o") },
new { name = "ghsa", lastSync = DateTimeOffset.UtcNow.AddHours(-2).ToString("o") }
},
advisoryCount = 150000
};
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(snapshot) + "\n");
}
private static byte[] CreatePolicyBundle()
{
// Minimal valid gzip content (empty archive)
return new byte[]
{
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
}
private static byte[] CreateVerdict(string decision, string scanId)
{
var verdict = new
{
version = "1.0",
scanId = scanId,
decision = decision,
evaluatedAt = DateTimeOffset.UtcNow.ToString("o"),
policyVersion = "2024.1",
findings = new
{
critical = 0,
high = 2,
medium = 5,
low = 10,
unknown = 0
},
attestation = new
{
type = "https://stellaops.io/verdict/v1",
predicateType = "https://stellaops.io/attestation/verdict/v1"
}
};
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(verdict, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}));
}
private static byte[] CreateVexStatements()
{
var vex = new
{
type = "https://openvex.dev/ns/v0.2.0",
id = $"https://stellaops.io/vex/{Guid.NewGuid()}",
author = "security-team@example.com",
timestamp = DateTimeOffset.UtcNow.ToString("o"),
statements = new[]
{
new
{
vulnerability = new { id = "CVE-2024-1234" },
status = "not_affected",
justification = "vulnerable_code_not_present"
}
}
};
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(vex, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}));
}
private static async Task<string> ComputeFileHashAsync(string filePath)
{
await using var stream = File.OpenRead(filePath);
var hash = await SHA256.HashDataAsync(stream);
return Convert.ToHexString(hash).ToLowerInvariant();
}
#endregion
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.AuditPack/StellaOps.AuditPack.csproj" />
</ItemGroup>
</Project>