- 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.
277 lines
7.8 KiB
C#
277 lines
7.8 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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
|
|
}));
|
|
}
|
|
}
|