test fixes and new product advisories work

This commit is contained in:
master
2026-01-28 02:30:48 +02:00
parent 82caceba56
commit 644887997c
288 changed files with 69101 additions and 375 deletions

View File

@@ -1,7 +1,8 @@
// -----------------------------------------------------------------------------
// SignalsCommandTests.cs
// Sprint: SPRINT_20260117_006_CLI_reachability_analysis (RCA-006, RCA-007)
// Description: Unit tests for signals inspect command
// Sprint: SPRINT_0127_0002_Signals_ebpf_syscall_reachability_proofs (SIGNING-002)
// Description: Unit tests for signals inspect and verify-chain commands
// -----------------------------------------------------------------------------
using System.CommandLine;
@@ -13,8 +14,24 @@ using Xunit;
namespace StellaOps.Cli.Tests.Commands;
public sealed class SignalsCommandTests
public sealed class SignalsCommandTests : IDisposable
{
private readonly string _testDir;
public SignalsCommandTests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"signals-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDir);
}
public void Dispose()
{
if (Directory.Exists(_testDir))
{
Directory.Delete(_testDir, recursive: true);
}
}
private static RootCommand BuildSignalsRoot()
{
var services = new ServiceCollection().BuildServiceProvider();
@@ -47,4 +64,397 @@ public sealed class SignalsCommandTests
using var doc = JsonDocument.Parse(writer.ToString());
Assert.True(doc.RootElement.GetArrayLength() > 0);
}
#region Verify-Chain Tests (SIGNING-002)
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyChain_DirectoryNotFound_ReturnsError()
{
var root = BuildSignalsRoot();
var nonExistentPath = Path.Combine(_testDir, "nonexistent");
var errorWriter = new StringWriter();
var originalErr = Console.Error;
int exitCode;
try
{
Console.SetError(errorWriter);
exitCode = await root.Parse($"signals verify-chain \"{nonExistentPath}\"").InvokeAsync();
}
finally
{
Console.SetError(originalErr);
}
Assert.Equal(1, exitCode);
Assert.Contains("Directory not found", errorWriter.ToString());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyChain_NoChunksFound_ReturnsError()
{
var root = BuildSignalsRoot();
var emptyDir = Path.Combine(_testDir, "empty");
Directory.CreateDirectory(emptyDir);
var errorWriter = new StringWriter();
var originalErr = Console.Error;
int exitCode;
try
{
Console.SetError(errorWriter);
exitCode = await root.Parse($"signals verify-chain \"{emptyDir}\"").InvokeAsync();
}
finally
{
Console.SetError(originalErr);
}
Assert.Equal(1, exitCode);
Assert.Contains("No signed chunks found", errorWriter.ToString());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyChain_ValidChain_ReturnsSuccess()
{
var root = BuildSignalsRoot();
var chainDir = Path.Combine(_testDir, "valid-chain");
Directory.CreateDirectory(chainDir);
// Create a valid 3-chunk chain
await CreateValidChainAsync(chainDir, chunkCount: 3);
var writer = new StringWriter();
var originalOut = Console.Out;
int exitCode;
try
{
Console.SetOut(writer);
exitCode = await root.Parse($"signals verify-chain \"{chainDir}\"").InvokeAsync();
}
finally
{
Console.SetOut(originalOut);
}
Assert.Equal(0, exitCode);
Assert.Contains("VALID", writer.ToString());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyChain_BrokenChain_ReturnsFailure()
{
var root = BuildSignalsRoot();
var chainDir = Path.Combine(_testDir, "broken-chain");
Directory.CreateDirectory(chainDir);
// Create chain with broken linkage
await CreateBrokenChainAsync(chainDir);
var writer = new StringWriter();
var originalOut = Console.Out;
int exitCode;
try
{
Console.SetOut(writer);
exitCode = await root.Parse($"signals verify-chain \"{chainDir}\" --verbose").InvokeAsync();
}
finally
{
Console.SetOut(originalOut);
}
Assert.Equal(1, exitCode);
Assert.Contains("INVALID", writer.ToString());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyChain_JsonFormat_ReturnsReport()
{
var root = BuildSignalsRoot();
var chainDir = Path.Combine(_testDir, "json-chain");
Directory.CreateDirectory(chainDir);
await CreateValidChainAsync(chainDir, chunkCount: 2);
var writer = new StringWriter();
var originalOut = Console.Out;
int exitCode;
try
{
Console.SetOut(writer);
exitCode = await root.Parse($"signals verify-chain \"{chainDir}\" --format json").InvokeAsync();
}
finally
{
Console.SetOut(originalOut);
}
Assert.Equal(0, exitCode);
using var doc = JsonDocument.Parse(writer.ToString());
var root2 = doc.RootElement;
Assert.True(root2.GetProperty("isValid").GetBoolean());
Assert.Equal(2, root2.GetProperty("totalChunks").GetInt32());
Assert.Equal(2, root2.GetProperty("passedChunks").GetInt32());
Assert.Equal(0, root2.GetProperty("failedChunks").GetInt32());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyChain_SaveReport_WritesFile()
{
var root = BuildSignalsRoot();
var chainDir = Path.Combine(_testDir, "report-chain");
Directory.CreateDirectory(chainDir);
var reportPath = Path.Combine(_testDir, "report.json");
await CreateValidChainAsync(chainDir, chunkCount: 2);
var writer = new StringWriter();
var originalOut = Console.Out;
int exitCode;
try
{
Console.SetOut(writer);
exitCode = await root.Parse($"signals verify-chain \"{chainDir}\" --report \"{reportPath}\"").InvokeAsync();
}
finally
{
Console.SetOut(originalOut);
}
Assert.Equal(0, exitCode);
Assert.True(File.Exists(reportPath));
var reportJson = await File.ReadAllTextAsync(reportPath);
using var doc = JsonDocument.Parse(reportJson);
Assert.True(doc.RootElement.GetProperty("isValid").GetBoolean());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyChain_SequenceGap_ReturnsFailure()
{
var root = BuildSignalsRoot();
var chainDir = Path.Combine(_testDir, "sequence-gap");
Directory.CreateDirectory(chainDir);
// Create chain with sequence gap (1, 3 instead of 1, 2)
await CreateChainWithSequenceGapAsync(chainDir);
var writer = new StringWriter();
var originalOut = Console.Out;
int exitCode;
try
{
Console.SetOut(writer);
exitCode = await root.Parse($"signals verify-chain \"{chainDir}\" --verbose").InvokeAsync();
}
finally
{
Console.SetOut(originalOut);
}
Assert.Equal(1, exitCode);
Assert.Contains("Sequence gap", writer.ToString());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyChain_TimeOverlap_ReturnsFailure()
{
var root = BuildSignalsRoot();
var chainDir = Path.Combine(_testDir, "time-overlap");
Directory.CreateDirectory(chainDir);
// Create chain with time overlap
await CreateChainWithTimeOverlapAsync(chainDir);
var writer = new StringWriter();
var originalOut = Console.Out;
int exitCode;
try
{
Console.SetOut(writer);
exitCode = await root.Parse($"signals verify-chain \"{chainDir}\" --verbose").InvokeAsync();
}
finally
{
Console.SetOut(originalOut);
}
Assert.Equal(1, exitCode);
Assert.Contains("Time overlap", writer.ToString());
}
#endregion
#region Test Helpers
private static async Task CreateValidChainAsync(string dir, int chunkCount)
{
string? previousChunkId = null;
var baseTime = DateTimeOffset.UtcNow.AddHours(-chunkCount);
for (int i = 1; i <= chunkCount; i++)
{
var chunkId = $"sha256:{new string((char)('a' + i - 1), 64)}";
var startTime = baseTime.AddMinutes((i - 1) * 10);
var endTime = startTime.AddMinutes(5);
var envelope = CreateDsseEnvelope(
chunkId: chunkId,
chunkSequence: i,
previousChunkId: previousChunkId,
eventCount: 100 * i,
startTime: startTime,
endTime: endTime);
var filePath = Path.Combine(dir, $"chunk-{i:D4}.dsse.json");
await File.WriteAllTextAsync(filePath, envelope);
previousChunkId = chunkId;
}
}
private static async Task CreateBrokenChainAsync(string dir)
{
var baseTime = DateTimeOffset.UtcNow.AddHours(-2);
// First chunk
var envelope1 = CreateDsseEnvelope(
chunkId: "sha256:" + new string('a', 64),
chunkSequence: 1,
previousChunkId: null,
eventCount: 100,
startTime: baseTime,
endTime: baseTime.AddMinutes(5));
await File.WriteAllTextAsync(Path.Combine(dir, "chunk-0001.dsse.json"), envelope1);
// Second chunk with wrong previous_chunk_id (broken chain)
var envelope2 = CreateDsseEnvelope(
chunkId: "sha256:" + new string('b', 64),
chunkSequence: 2,
previousChunkId: "sha256:" + new string('x', 64), // Wrong! Should be 'a's
eventCount: 200,
startTime: baseTime.AddMinutes(10),
endTime: baseTime.AddMinutes(15));
await File.WriteAllTextAsync(Path.Combine(dir, "chunk-0002.dsse.json"), envelope2);
}
private static async Task CreateChainWithSequenceGapAsync(string dir)
{
var baseTime = DateTimeOffset.UtcNow.AddHours(-2);
// Chunk 1
var envelope1 = CreateDsseEnvelope(
chunkId: "sha256:" + new string('a', 64),
chunkSequence: 1,
previousChunkId: null,
eventCount: 100,
startTime: baseTime,
endTime: baseTime.AddMinutes(5));
await File.WriteAllTextAsync(Path.Combine(dir, "chunk-0001.dsse.json"), envelope1);
// Chunk 3 (sequence gap - skipped 2)
var envelope2 = CreateDsseEnvelope(
chunkId: "sha256:" + new string('b', 64),
chunkSequence: 3, // Should be 2
previousChunkId: "sha256:" + new string('a', 64),
eventCount: 200,
startTime: baseTime.AddMinutes(10),
endTime: baseTime.AddMinutes(15));
await File.WriteAllTextAsync(Path.Combine(dir, "chunk-0002.dsse.json"), envelope2);
}
private static async Task CreateChainWithTimeOverlapAsync(string dir)
{
var baseTime = DateTimeOffset.UtcNow.AddHours(-2);
// Chunk 1: ends at baseTime + 10 min
var envelope1 = CreateDsseEnvelope(
chunkId: "sha256:" + new string('a', 64),
chunkSequence: 1,
previousChunkId: null,
eventCount: 100,
startTime: baseTime,
endTime: baseTime.AddMinutes(10));
await File.WriteAllTextAsync(Path.Combine(dir, "chunk-0001.dsse.json"), envelope1);
// Chunk 2: starts at baseTime + 5 min (overlaps with chunk 1)
var envelope2 = CreateDsseEnvelope(
chunkId: "sha256:" + new string('b', 64),
chunkSequence: 2,
previousChunkId: "sha256:" + new string('a', 64),
eventCount: 200,
startTime: baseTime.AddMinutes(5), // Overlaps! Should be >= baseTime + 10
endTime: baseTime.AddMinutes(15));
await File.WriteAllTextAsync(Path.Combine(dir, "chunk-0002.dsse.json"), envelope2);
}
private static string CreateDsseEnvelope(
string chunkId,
int chunkSequence,
string? previousChunkId,
long eventCount,
DateTimeOffset startTime,
DateTimeOffset endTime)
{
// Build the in-toto statement predicate
var predicate = new
{
chunk_id = chunkId,
chunk_sequence = chunkSequence,
previous_chunk_id = previousChunkId,
event_count = eventCount,
time_range = new
{
start = startTime,
end = endTime
}
};
var statement = new
{
_type = "https://in-toto.io/Statement/v0.1",
predicateType = "stella.ops/runtime-evidence@v1",
predicate
};
var statementJson = JsonSerializer.Serialize(statement, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
});
var payloadBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(statementJson));
// Build DSSE envelope
var envelope = new
{
payloadType = "application/vnd.in-toto+json",
payload = payloadBase64,
signatures = new[]
{
new
{
keyid = "test-key",
sig = Convert.ToBase64String(new byte[64]) // Dummy signature
}
}
};
return JsonSerializer.Serialize(envelope, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
});
}
#endregion
}