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:
@@ -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
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user