Add unit tests for PackRunAttestation and SealedInstallEnforcer
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (push) Has been cancelled

- Implement comprehensive tests for PackRunAttestationService, covering attestation generation, verification, and event emission.
- Add tests for SealedInstallEnforcer to validate sealed install requirements and enforcement logic.
- Introduce a MonacoLoaderService stub for testing purposes to prevent Monaco workers/styles from loading during Karma runs.
This commit is contained in:
StellaOps Bot
2025-12-06 22:25:30 +02:00
parent dd0067ea0b
commit 4042fc2184
110 changed files with 20084 additions and 639 deletions

View File

@@ -0,0 +1,345 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.TaskRunner.Core.Evidence;
using StellaOps.TaskRunner.Core.Events;
namespace StellaOps.TaskRunner.Tests;
public sealed class BundleImportEvidenceTests
{
[Fact]
public void BundleImportHashChain_Compute_CreatesDeterministicHash()
{
var input = new BundleImportInputManifest(
FormatVersion: "1.0.0",
BundleId: "test-bundle",
BundleVersion: "2025.10.0",
CreatedAt: DateTimeOffset.Parse("2025-12-06T00:00:00Z"),
CreatedBy: "test@example.com",
TotalSizeBytes: 1024,
ItemCount: 5,
ManifestSha256: "sha256:abc123",
Signature: null,
SignatureValid: null);
var outputs = new List<BundleImportOutputFile>
{
new("file1.json", "sha256:aaa", 100, "application/json", DateTimeOffset.UtcNow, "item1"),
new("file2.json", "sha256:bbb", 200, "application/json", DateTimeOffset.UtcNow, "item2")
};
var transcript = new List<BundleImportTranscriptEntry>
{
new(DateTimeOffset.UtcNow, "info", "import.started", "Import started", null)
};
var chain1 = BundleImportHashChain.Compute(input, outputs, transcript);
var chain2 = BundleImportHashChain.Compute(input, outputs, transcript);
Assert.Equal(chain1.RootHash, chain2.RootHash);
Assert.Equal(chain1.InputsHash, chain2.InputsHash);
Assert.Equal(chain1.OutputsHash, chain2.OutputsHash);
Assert.StartsWith("sha256:", chain1.RootHash);
}
[Fact]
public void BundleImportHashChain_Compute_DifferentInputsProduceDifferentHashes()
{
var input1 = new BundleImportInputManifest(
FormatVersion: "1.0.0",
BundleId: "bundle-1",
BundleVersion: "2025.10.0",
CreatedAt: DateTimeOffset.UtcNow,
CreatedBy: null,
TotalSizeBytes: 1024,
ItemCount: 5,
ManifestSha256: "sha256:abc123",
Signature: null,
SignatureValid: null);
var input2 = new BundleImportInputManifest(
FormatVersion: "1.0.0",
BundleId: "bundle-2",
BundleVersion: "2025.10.0",
CreatedAt: DateTimeOffset.UtcNow,
CreatedBy: null,
TotalSizeBytes: 1024,
ItemCount: 5,
ManifestSha256: "sha256:def456",
Signature: null,
SignatureValid: null);
var outputs = new List<BundleImportOutputFile>();
var transcript = new List<BundleImportTranscriptEntry>();
var chain1 = BundleImportHashChain.Compute(input1, outputs, transcript);
var chain2 = BundleImportHashChain.Compute(input2, outputs, transcript);
Assert.NotEqual(chain1.RootHash, chain2.RootHash);
Assert.NotEqual(chain1.InputsHash, chain2.InputsHash);
}
[Fact]
public async Task BundleImportEvidenceService_CaptureAsync_StoresEvidence()
{
var store = new InMemoryPackRunEvidenceStore();
var service = new BundleImportEvidenceService(
store,
NullLogger<BundleImportEvidenceService>.Instance);
var evidence = CreateTestEvidence();
var result = await service.CaptureAsync(evidence, TestContext.Current.CancellationToken);
Assert.True(result.Success);
Assert.NotNull(result.Snapshot);
Assert.NotNull(result.EvidencePointer);
Assert.Equal(1, store.Count);
}
[Fact]
public async Task BundleImportEvidenceService_CaptureAsync_CreatesCorrectMaterials()
{
var store = new InMemoryPackRunEvidenceStore();
var service = new BundleImportEvidenceService(
store,
NullLogger<BundleImportEvidenceService>.Instance);
var evidence = CreateTestEvidence();
var result = await service.CaptureAsync(evidence, TestContext.Current.CancellationToken);
Assert.True(result.Success);
var snapshot = result.Snapshot!;
// Should have: input manifest, 2 outputs, transcript, validation, hashchain = 6 materials
Assert.Equal(6, snapshot.Materials.Count);
Assert.Contains(snapshot.Materials, m => m.Section == "input");
Assert.Contains(snapshot.Materials, m => m.Section == "output");
Assert.Contains(snapshot.Materials, m => m.Section == "transcript");
Assert.Contains(snapshot.Materials, m => m.Section == "validation");
Assert.Contains(snapshot.Materials, m => m.Section == "hashchain");
}
[Fact]
public async Task BundleImportEvidenceService_CaptureAsync_SetsCorrectMetadata()
{
var store = new InMemoryPackRunEvidenceStore();
var service = new BundleImportEvidenceService(
store,
NullLogger<BundleImportEvidenceService>.Instance);
var evidence = CreateTestEvidence();
var result = await service.CaptureAsync(evidence, TestContext.Current.CancellationToken);
Assert.True(result.Success);
var snapshot = result.Snapshot!;
Assert.Equal(evidence.JobId, snapshot.Metadata!["jobId"]);
Assert.Equal(evidence.Status.ToString(), snapshot.Metadata["status"]);
Assert.Equal(evidence.SourcePath, snapshot.Metadata["sourcePath"]);
Assert.Equal("2", snapshot.Metadata["outputCount"]);
}
[Fact]
public async Task BundleImportEvidenceService_CaptureAsync_EmitsTimelineEvent()
{
var store = new InMemoryPackRunEvidenceStore();
var timelineSink = new InMemoryPackRunTimelineEventSink();
var emitter = new PackRunTimelineEventEmitter(
timelineSink,
TimeProvider.System,
NullLogger<PackRunTimelineEventEmitter>.Instance);
var service = new BundleImportEvidenceService(
store,
NullLogger<BundleImportEvidenceService>.Instance,
emitter);
var evidence = CreateTestEvidence();
var result = await service.CaptureAsync(evidence, TestContext.Current.CancellationToken);
Assert.True(result.Success);
Assert.Equal(1, timelineSink.Count);
var evt = timelineSink.GetEvents()[0];
Assert.Equal("bundle.import.evidence_captured", evt.EventType);
}
[Fact]
public async Task BundleImportEvidenceService_GetAsync_ReturnsEvidence()
{
var store = new InMemoryPackRunEvidenceStore();
var service = new BundleImportEvidenceService(
store,
NullLogger<BundleImportEvidenceService>.Instance);
var evidence = CreateTestEvidence();
await service.CaptureAsync(evidence, TestContext.Current.CancellationToken);
var retrieved = await service.GetAsync(evidence.JobId, TestContext.Current.CancellationToken);
Assert.NotNull(retrieved);
Assert.Equal(evidence.JobId, retrieved.JobId);
Assert.Equal(evidence.TenantId, retrieved.TenantId);
}
[Fact]
public async Task BundleImportEvidenceService_GetAsync_ReturnsNullForMissingJob()
{
var store = new InMemoryPackRunEvidenceStore();
var service = new BundleImportEvidenceService(
store,
NullLogger<BundleImportEvidenceService>.Instance);
var retrieved = await service.GetAsync("non-existent-job", TestContext.Current.CancellationToken);
Assert.Null(retrieved);
}
[Fact]
public async Task BundleImportEvidenceService_ExportToPortableBundleAsync_CreatesFile()
{
var store = new InMemoryPackRunEvidenceStore();
var service = new BundleImportEvidenceService(
store,
NullLogger<BundleImportEvidenceService>.Instance);
var evidence = CreateTestEvidence();
await service.CaptureAsync(evidence, TestContext.Current.CancellationToken);
var outputPath = Path.Combine(Path.GetTempPath(), $"evidence-{Guid.NewGuid():N}.json");
try
{
var result = await service.ExportToPortableBundleAsync(
evidence.JobId,
outputPath,
TestContext.Current.CancellationToken);
Assert.True(result.Success);
Assert.Equal(outputPath, result.OutputPath);
Assert.True(File.Exists(outputPath));
Assert.True(result.SizeBytes > 0);
Assert.StartsWith("sha256:", result.BundleSha256);
}
finally
{
if (File.Exists(outputPath))
{
File.Delete(outputPath);
}
}
}
[Fact]
public async Task BundleImportEvidenceService_ExportToPortableBundleAsync_FailsForMissingJob()
{
var store = new InMemoryPackRunEvidenceStore();
var service = new BundleImportEvidenceService(
store,
NullLogger<BundleImportEvidenceService>.Instance);
var outputPath = Path.Combine(Path.GetTempPath(), $"evidence-{Guid.NewGuid():N}.json");
var result = await service.ExportToPortableBundleAsync(
"non-existent-job",
outputPath,
TestContext.Current.CancellationToken);
Assert.False(result.Success);
Assert.Contains("No evidence found", result.Error);
}
[Fact]
public void BundleImportEvidence_RecordProperties_AreAccessible()
{
var evidence = CreateTestEvidence();
Assert.Equal("test-job-123", evidence.JobId);
Assert.Equal("tenant-1", evidence.TenantId);
Assert.Equal("/path/to/bundle.tar.gz", evidence.SourcePath);
Assert.Equal(BundleImportStatus.Completed, evidence.Status);
Assert.NotNull(evidence.InputManifest);
Assert.Equal(2, evidence.OutputFiles.Count);
Assert.Equal(2, evidence.Transcript.Count);
Assert.NotNull(evidence.ValidationResult);
}
[Fact]
public void BundleImportValidationResult_RecordProperties_AreAccessible()
{
var result = new BundleImportValidationResult(
Valid: true,
ChecksumValid: true,
SignatureValid: true,
FormatValid: true,
Errors: null,
Warnings: ["Advisory data may be stale"]);
Assert.True(result.Valid);
Assert.True(result.ChecksumValid);
Assert.True(result.SignatureValid);
Assert.True(result.FormatValid);
Assert.Null(result.Errors);
Assert.Single(result.Warnings!);
}
private static BundleImportEvidence CreateTestEvidence()
{
var now = DateTimeOffset.UtcNow;
var input = new BundleImportInputManifest(
FormatVersion: "1.0.0",
BundleId: "test-bundle-001",
BundleVersion: "2025.10.0",
CreatedAt: now.AddHours(-1),
CreatedBy: "bundle-builder@example.com",
TotalSizeBytes: 10240,
ItemCount: 5,
ManifestSha256: "sha256:abcdef1234567890",
Signature: "base64sig...",
SignatureValid: true);
var outputs = new List<BundleImportOutputFile>
{
new("advisories/CVE-2025-0001.json", "sha256:output1hash", 512, "application/json", now, "item1"),
new("advisories/CVE-2025-0002.json", "sha256:output2hash", 1024, "application/json", now, "item2")
};
var transcript = new List<BundleImportTranscriptEntry>
{
new(now.AddMinutes(-5), "info", "import.started", "Bundle import started", new Dictionary<string, string>
{
["sourcePath"] = "/path/to/bundle.tar.gz"
}),
new(now, "info", "import.completed", "Bundle import completed successfully", new Dictionary<string, string>
{
["itemsImported"] = "5"
})
};
var validation = new BundleImportValidationResult(
Valid: true,
ChecksumValid: true,
SignatureValid: true,
FormatValid: true,
Errors: null,
Warnings: null);
var hashChain = BundleImportHashChain.Compute(input, outputs, transcript);
return new BundleImportEvidence(
JobId: "test-job-123",
TenantId: "tenant-1",
SourcePath: "/path/to/bundle.tar.gz",
StartedAt: now.AddMinutes(-5),
CompletedAt: now,
Status: BundleImportStatus.Completed,
ErrorMessage: null,
InitiatedBy: "admin@example.com",
InputManifest: input,
OutputFiles: outputs,
Transcript: transcript,
ValidationResult: validation,
HashChain: hashChain);
}
}

View File

@@ -0,0 +1,491 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.TaskRunner.Core.Attestation;
using StellaOps.TaskRunner.Core.Events;
using StellaOps.TaskRunner.Core.Evidence;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunAttestationTests
{
[Fact]
public async Task GenerateAsync_CreatesAttestationWithSubjects()
{
var store = new InMemoryPackRunAttestationStore();
var signer = new StubPackRunAttestationSigner();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance,
signer);
var subjects = new List<PackRunAttestationSubject>
{
new("artifact/output.tar.gz", new Dictionary<string, string> { ["sha256"] = "abc123" }),
new("artifact/sbom.json", new Dictionary<string, string> { ["sha256"] = "def456" })
};
var request = new PackRunAttestationRequest(
RunId: "run-001",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: subjects,
EvidenceSnapshotId: Guid.NewGuid(),
StartedAt: DateTimeOffset.UtcNow.AddMinutes(-5),
CompletedAt: DateTimeOffset.UtcNow,
BuilderId: null,
ExternalParameters: null,
ResolvedDependencies: null,
Metadata: null);
var result = await service.GenerateAsync(request, TestContext.Current.CancellationToken);
Assert.True(result.Success);
Assert.NotNull(result.Attestation);
Assert.Equal(PackRunAttestationStatus.Signed, result.Attestation.Status);
Assert.Equal(2, result.Attestation.Subjects.Count);
Assert.NotNull(result.Attestation.Envelope);
}
[Fact]
public async Task GenerateAsync_WithoutSigner_CreatesPendingAttestation()
{
var store = new InMemoryPackRunAttestationStore();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance);
var subjects = new List<PackRunAttestationSubject>
{
new("artifact/output.tar.gz", new Dictionary<string, string> { ["sha256"] = "abc123" })
};
var request = new PackRunAttestationRequest(
RunId: "run-002",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: subjects,
EvidenceSnapshotId: null,
StartedAt: DateTimeOffset.UtcNow,
CompletedAt: null,
BuilderId: null,
ExternalParameters: null,
ResolvedDependencies: null,
Metadata: null);
var result = await service.GenerateAsync(request, TestContext.Current.CancellationToken);
Assert.True(result.Success);
Assert.NotNull(result.Attestation);
Assert.Equal(PackRunAttestationStatus.Pending, result.Attestation.Status);
Assert.Null(result.Attestation.Envelope);
}
[Fact]
public async Task GenerateAsync_EmitsTimelineEvent()
{
var store = new InMemoryPackRunAttestationStore();
var signer = new StubPackRunAttestationSigner();
var timelineSink = new InMemoryPackRunTimelineEventSink();
var emitter = new PackRunTimelineEventEmitter(
timelineSink,
TimeProvider.System,
NullLogger<PackRunTimelineEventEmitter>.Instance);
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance,
signer,
emitter);
var request = new PackRunAttestationRequest(
RunId: "run-003",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: [new("artifact/test.json", new Dictionary<string, string> { ["sha256"] = "abc" })],
EvidenceSnapshotId: null,
StartedAt: DateTimeOffset.UtcNow,
CompletedAt: DateTimeOffset.UtcNow,
BuilderId: null,
ExternalParameters: null,
ResolvedDependencies: null,
Metadata: null);
await service.GenerateAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(1, timelineSink.Count);
var evt = timelineSink.GetEvents()[0];
Assert.Equal(PackRunAttestationEventTypes.AttestationCreated, evt.EventType);
}
[Fact]
public async Task VerifyAsync_ValidatesSubjectsMatch()
{
var store = new InMemoryPackRunAttestationStore();
var signer = new StubPackRunAttestationSigner();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance,
signer);
var subjects = new List<PackRunAttestationSubject>
{
new("artifact/output.tar.gz", new Dictionary<string, string> { ["sha256"] = "abc123" })
};
var request = new PackRunAttestationRequest(
RunId: "run-004",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: subjects,
EvidenceSnapshotId: null,
StartedAt: DateTimeOffset.UtcNow,
CompletedAt: DateTimeOffset.UtcNow,
BuilderId: null,
ExternalParameters: null,
ResolvedDependencies: null,
Metadata: null);
var genResult = await service.GenerateAsync(request, TestContext.Current.CancellationToken);
Assert.NotNull(genResult.Attestation);
var verifyResult = await service.VerifyAsync(
new PackRunAttestationVerificationRequest(
AttestationId: genResult.Attestation.AttestationId,
ExpectedSubjects: subjects,
VerifySignature: true,
VerifySubjects: true,
CheckRevocation: true),
TestContext.Current.CancellationToken);
Assert.True(verifyResult.Valid);
Assert.Equal(PackRunSignatureVerificationStatus.Valid, verifyResult.SignatureStatus);
Assert.Equal(PackRunSubjectVerificationStatus.Match, verifyResult.SubjectStatus);
Assert.Equal(PackRunRevocationStatus.NotRevoked, verifyResult.RevocationStatus);
}
[Fact]
public async Task VerifyAsync_DetectsMismatchedSubjects()
{
var store = new InMemoryPackRunAttestationStore();
var signer = new StubPackRunAttestationSigner();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance,
signer);
var subjects = new List<PackRunAttestationSubject>
{
new("artifact/output.tar.gz", new Dictionary<string, string> { ["sha256"] = "abc123" })
};
var request = new PackRunAttestationRequest(
RunId: "run-005",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: subjects,
EvidenceSnapshotId: null,
StartedAt: DateTimeOffset.UtcNow,
CompletedAt: DateTimeOffset.UtcNow,
BuilderId: null,
ExternalParameters: null,
ResolvedDependencies: null,
Metadata: null);
var genResult = await service.GenerateAsync(request, TestContext.Current.CancellationToken);
Assert.NotNull(genResult.Attestation);
// Verify with different expected subjects
var differentSubjects = new List<PackRunAttestationSubject>
{
new("artifact/different.tar.gz", new Dictionary<string, string> { ["sha256"] = "xyz789" })
};
var verifyResult = await service.VerifyAsync(
new PackRunAttestationVerificationRequest(
AttestationId: genResult.Attestation.AttestationId,
ExpectedSubjects: differentSubjects,
VerifySignature: false,
VerifySubjects: true,
CheckRevocation: false),
TestContext.Current.CancellationToken);
Assert.False(verifyResult.Valid);
Assert.Equal(PackRunSubjectVerificationStatus.Missing, verifyResult.SubjectStatus);
Assert.NotNull(verifyResult.Errors);
Assert.Contains(verifyResult.Errors, e => e.Contains("Missing subjects"));
}
[Fact]
public async Task VerifyAsync_DetectsRevokedAttestation()
{
var store = new InMemoryPackRunAttestationStore();
var signer = new StubPackRunAttestationSigner();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance,
signer);
var subjects = new List<PackRunAttestationSubject>
{
new("artifact/output.tar.gz", new Dictionary<string, string> { ["sha256"] = "abc123" })
};
var request = new PackRunAttestationRequest(
RunId: "run-006",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: subjects,
EvidenceSnapshotId: null,
StartedAt: DateTimeOffset.UtcNow,
CompletedAt: DateTimeOffset.UtcNow,
BuilderId: null,
ExternalParameters: null,
ResolvedDependencies: null,
Metadata: null);
var genResult = await service.GenerateAsync(request, TestContext.Current.CancellationToken);
Assert.NotNull(genResult.Attestation);
// Revoke the attestation
await store.UpdateStatusAsync(
genResult.Attestation.AttestationId,
PackRunAttestationStatus.Revoked,
"Compromised key",
TestContext.Current.CancellationToken);
var verifyResult = await service.VerifyAsync(
new PackRunAttestationVerificationRequest(
AttestationId: genResult.Attestation.AttestationId,
ExpectedSubjects: null,
VerifySignature: false,
VerifySubjects: false,
CheckRevocation: true),
TestContext.Current.CancellationToken);
Assert.False(verifyResult.Valid);
Assert.Equal(PackRunRevocationStatus.Revoked, verifyResult.RevocationStatus);
}
[Fact]
public async Task VerifyAsync_ReturnsErrorForNonExistentAttestation()
{
var store = new InMemoryPackRunAttestationStore();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance);
var verifyResult = await service.VerifyAsync(
new PackRunAttestationVerificationRequest(
AttestationId: Guid.NewGuid(),
ExpectedSubjects: null,
VerifySignature: false,
VerifySubjects: false,
CheckRevocation: false),
TestContext.Current.CancellationToken);
Assert.False(verifyResult.Valid);
Assert.NotNull(verifyResult.Errors);
Assert.Contains(verifyResult.Errors, e => e.Contains("not found"));
}
[Fact]
public async Task ListByRunAsync_ReturnsAttestationsForRun()
{
var store = new InMemoryPackRunAttestationStore();
var signer = new StubPackRunAttestationSigner();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance,
signer);
// Create two attestations for the same run
for (var i = 0; i < 2; i++)
{
var request = new PackRunAttestationRequest(
RunId: "run-007",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: [new($"artifact/output{i}.tar.gz", new Dictionary<string, string> { ["sha256"] = $"hash{i}" })],
EvidenceSnapshotId: null,
StartedAt: DateTimeOffset.UtcNow,
CompletedAt: DateTimeOffset.UtcNow,
BuilderId: null,
ExternalParameters: null,
ResolvedDependencies: null,
Metadata: null);
await service.GenerateAsync(request, TestContext.Current.CancellationToken);
}
var attestations = await service.ListByRunAsync("tenant-1", "run-007", TestContext.Current.CancellationToken);
Assert.Equal(2, attestations.Count);
Assert.All(attestations, a => Assert.Equal("run-007", a.RunId));
}
[Fact]
public async Task GetEnvelopeAsync_ReturnsEnvelopeForSignedAttestation()
{
var store = new InMemoryPackRunAttestationStore();
var signer = new StubPackRunAttestationSigner();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance,
signer);
var request = new PackRunAttestationRequest(
RunId: "run-008",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: [new("artifact/output.tar.gz", new Dictionary<string, string> { ["sha256"] = "abc123" })],
EvidenceSnapshotId: null,
StartedAt: DateTimeOffset.UtcNow,
CompletedAt: DateTimeOffset.UtcNow,
BuilderId: null,
ExternalParameters: null,
ResolvedDependencies: null,
Metadata: null);
var genResult = await service.GenerateAsync(request, TestContext.Current.CancellationToken);
Assert.NotNull(genResult.Attestation);
var envelope = await service.GetEnvelopeAsync(genResult.Attestation.AttestationId, TestContext.Current.CancellationToken);
Assert.NotNull(envelope);
Assert.Equal(PackRunDsseEnvelope.InTotoPayloadType, envelope.PayloadType);
Assert.Single(envelope.Signatures);
}
[Fact]
public void PackRunAttestationSubject_FromArtifact_ParsesSha256Prefix()
{
var artifact = new PackRunArtifactReference(
Name: "output.tar.gz",
Sha256: "sha256:abcdef123456",
SizeBytes: 1024,
MediaType: "application/gzip");
var subject = PackRunAttestationSubject.FromArtifact(artifact);
Assert.Equal("output.tar.gz", subject.Name);
Assert.Equal("abcdef123456", subject.Digest["sha256"]);
}
[Fact]
public void PackRunAttestation_ComputeStatementDigest_IsDeterministic()
{
var subjects = new List<PackRunAttestationSubject>
{
new("artifact/output.tar.gz", new Dictionary<string, string> { ["sha256"] = "abc123" })
};
var attestation = new PackRunAttestation(
AttestationId: Guid.NewGuid(),
TenantId: "tenant-1",
RunId: "run-001",
PlanHash: "sha256:plan123",
CreatedAt: DateTimeOffset.Parse("2025-12-06T00:00:00Z"),
Subjects: subjects,
PredicateType: PredicateTypes.PackRunProvenance,
PredicateJson: "{\"test\":true}",
Envelope: null,
Status: PackRunAttestationStatus.Pending,
Error: null,
EvidenceSnapshotId: null,
Metadata: null);
var digest1 = attestation.ComputeStatementDigest();
var digest2 = attestation.ComputeStatementDigest();
Assert.Equal(digest1, digest2);
Assert.StartsWith("sha256:", digest1);
}
[Fact]
public void PackRunDsseEnvelope_ComputeDigest_IsDeterministic()
{
var envelope = new PackRunDsseEnvelope(
PayloadType: PackRunDsseEnvelope.InTotoPayloadType,
Payload: Convert.ToBase64String([1, 2, 3]),
Signatures: [new PackRunDsseSignature("key-001", "sig123")]);
var digest1 = envelope.ComputeDigest();
var digest2 = envelope.ComputeDigest();
Assert.Equal(digest1, digest2);
Assert.StartsWith("sha256:", digest1);
}
[Fact]
public async Task GenerateAsync_WithExternalParameters_IncludesInPredicate()
{
var store = new InMemoryPackRunAttestationStore();
var signer = new StubPackRunAttestationSigner();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance,
signer);
var externalParams = new Dictionary<string, object>
{
["manifestUrl"] = "https://registry.example.com/pack/v1",
["version"] = "1.0.0"
};
var request = new PackRunAttestationRequest(
RunId: "run-009",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: [new("artifact/output.tar.gz", new Dictionary<string, string> { ["sha256"] = "abc" })],
EvidenceSnapshotId: null,
StartedAt: DateTimeOffset.UtcNow,
CompletedAt: DateTimeOffset.UtcNow,
BuilderId: "https://stellaops.io/task-runner/custom",
ExternalParameters: externalParams,
ResolvedDependencies: null,
Metadata: null);
var result = await service.GenerateAsync(request, TestContext.Current.CancellationToken);
Assert.True(result.Success);
Assert.NotNull(result.Attestation);
Assert.Contains("manifestUrl", result.Attestation.PredicateJson);
}
[Fact]
public async Task GenerateAsync_WithResolvedDependencies_IncludesInPredicate()
{
var store = new InMemoryPackRunAttestationStore();
var signer = new StubPackRunAttestationSigner();
var service = new PackRunAttestationService(
store,
NullLogger<PackRunAttestationService>.Instance,
signer);
var dependencies = new List<PackRunDependency>
{
new("https://registry.example.com/tool/scanner:v1",
new Dictionary<string, string> { ["sha256"] = "scanner123" },
"scanner",
"application/vnd.oci.image.index.v1+json")
};
var request = new PackRunAttestationRequest(
RunId: "run-010",
TenantId: "tenant-1",
PlanHash: "sha256:plan123",
Subjects: [new("artifact/output.tar.gz", new Dictionary<string, string> { ["sha256"] = "abc" })],
EvidenceSnapshotId: null,
StartedAt: DateTimeOffset.UtcNow,
CompletedAt: DateTimeOffset.UtcNow,
BuilderId: null,
ExternalParameters: null,
ResolvedDependencies: dependencies,
Metadata: null);
var result = await service.GenerateAsync(request, TestContext.Current.CancellationToken);
Assert.True(result.Success);
Assert.NotNull(result.Attestation);
Assert.Contains("resolvedDependencies", result.Attestation.PredicateJson);
Assert.Contains("scanner", result.Attestation.PredicateJson);
}
}

View File

@@ -0,0 +1,390 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.TaskRunner.Core.AirGap;
using StellaOps.TaskRunner.Core.TaskPacks;
namespace StellaOps.TaskRunner.Tests;
public sealed class SealedInstallEnforcerTests
{
private static TaskPackManifest CreateManifest(bool sealedInstall, SealedRequirements? requirements = null)
{
return new TaskPackManifest
{
ApiVersion = "taskrunner/v1",
Kind = "TaskPack",
Metadata = new TaskPackMetadata
{
Name = "test-pack",
Version = "1.0.0"
},
Spec = new TaskPackSpec
{
SealedInstall = sealedInstall,
SealedRequirements = requirements
}
};
}
[Fact]
public async Task EnforceAsync_WhenPackDoesNotRequireSealedInstall_ReturnsAllowed()
{
var statusProvider = new MockAirGapStatusProvider(SealedModeStatus.Unsealed());
var options = Options.Create(new SealedInstallEnforcementOptions { Enabled = true });
var enforcer = new SealedInstallEnforcer(
statusProvider,
options,
NullLogger<SealedInstallEnforcer>.Instance);
var manifest = CreateManifest(sealedInstall: false);
var result = await enforcer.EnforceAsync(manifest, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(result.Allowed);
Assert.Equal("Pack does not require sealed install", result.Message);
}
[Fact]
public async Task EnforceAsync_WhenEnforcementDisabled_ReturnsAllowed()
{
var statusProvider = new MockAirGapStatusProvider(SealedModeStatus.Unsealed());
var options = Options.Create(new SealedInstallEnforcementOptions { Enabled = false });
var enforcer = new SealedInstallEnforcer(
statusProvider,
options,
NullLogger<SealedInstallEnforcer>.Instance);
var manifest = CreateManifest(sealedInstall: true);
var result = await enforcer.EnforceAsync(manifest, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(result.Allowed);
Assert.Equal("Enforcement disabled", result.Message);
}
[Fact]
public async Task EnforceAsync_WhenSealedRequiredButEnvironmentNotSealed_ReturnsDenied()
{
var statusProvider = new MockAirGapStatusProvider(SealedModeStatus.Unsealed());
var options = Options.Create(new SealedInstallEnforcementOptions { Enabled = true });
var enforcer = new SealedInstallEnforcer(
statusProvider,
options,
NullLogger<SealedInstallEnforcer>.Instance);
var manifest = CreateManifest(sealedInstall: true);
var result = await enforcer.EnforceAsync(manifest, cancellationToken: TestContext.Current.CancellationToken);
Assert.False(result.Allowed);
Assert.Equal(SealedInstallErrorCodes.SealedInstallViolation, result.ErrorCode);
Assert.NotNull(result.Violation);
Assert.True(result.Violation.RequiredSealed);
Assert.False(result.Violation.ActualSealed);
}
[Fact]
public async Task EnforceAsync_WhenSealedRequiredAndEnvironmentSealed_ReturnsAllowed()
{
var status = new SealedModeStatus(
Sealed: true,
Mode: "sealed",
SealedAt: DateTimeOffset.UtcNow.AddDays(-1),
SealedBy: "admin@test.com",
BundleVersion: "2025.10.0",
BundleDigest: "sha256:abc123",
LastAdvisoryUpdate: DateTimeOffset.UtcNow.AddHours(-12),
AdvisoryStalenessHours: 12,
TimeAnchor: new TimeAnchorInfo(
DateTimeOffset.UtcNow.AddHours(-1),
"base64signature",
Valid: true,
ExpiresAt: DateTimeOffset.UtcNow.AddDays(30)),
EgressBlocked: true,
NetworkPolicy: "deny-all");
var statusProvider = new MockAirGapStatusProvider(status);
var options = Options.Create(new SealedInstallEnforcementOptions { Enabled = true });
var enforcer = new SealedInstallEnforcer(
statusProvider,
options,
NullLogger<SealedInstallEnforcer>.Instance);
var manifest = CreateManifest(sealedInstall: true);
var result = await enforcer.EnforceAsync(manifest, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(result.Allowed);
Assert.Equal("Sealed install requirements satisfied", result.Message);
}
[Fact]
public async Task EnforceAsync_WhenBundleVersionBelowMinimum_ReturnsDenied()
{
var status = new SealedModeStatus(
Sealed: true,
Mode: "sealed",
SealedAt: DateTimeOffset.UtcNow,
SealedBy: null,
BundleVersion: "2024.5.0",
BundleDigest: null,
LastAdvisoryUpdate: DateTimeOffset.UtcNow,
AdvisoryStalenessHours: 0,
TimeAnchor: new TimeAnchorInfo(DateTimeOffset.UtcNow, null, true, DateTimeOffset.UtcNow.AddDays(30)),
EgressBlocked: true,
NetworkPolicy: null);
var statusProvider = new MockAirGapStatusProvider(status);
var options = Options.Create(new SealedInstallEnforcementOptions { Enabled = true });
var enforcer = new SealedInstallEnforcer(
statusProvider,
options,
NullLogger<SealedInstallEnforcer>.Instance);
var requirements = new SealedRequirements(
MinBundleVersion: "2025.10.0",
MaxAdvisoryStalenessHours: 168,
RequireTimeAnchor: true,
AllowedOfflineDurationHours: 720,
RequireSignatureVerification: true);
var manifest = CreateManifest(sealedInstall: true, requirements);
var result = await enforcer.EnforceAsync(manifest, cancellationToken: TestContext.Current.CancellationToken);
Assert.False(result.Allowed);
Assert.Equal(SealedInstallErrorCodes.SealedRequirementsViolation, result.ErrorCode);
Assert.NotNull(result.RequirementViolations);
Assert.Single(result.RequirementViolations);
Assert.Equal("min_bundle_version", result.RequirementViolations[0].Requirement);
}
[Fact]
public async Task EnforceAsync_WhenAdvisoryTooStale_ReturnsDenied()
{
var status = new SealedModeStatus(
Sealed: true,
Mode: "sealed",
SealedAt: DateTimeOffset.UtcNow,
SealedBy: null,
BundleVersion: "2025.10.0",
BundleDigest: null,
LastAdvisoryUpdate: DateTimeOffset.UtcNow.AddHours(-200),
AdvisoryStalenessHours: 200,
TimeAnchor: new TimeAnchorInfo(DateTimeOffset.UtcNow, null, true, DateTimeOffset.UtcNow.AddDays(30)),
EgressBlocked: true,
NetworkPolicy: null);
var statusProvider = new MockAirGapStatusProvider(status);
var options = Options.Create(new SealedInstallEnforcementOptions
{
Enabled = true,
DenyOnStaleness = true,
StalenessGracePeriodHours = 0
});
var enforcer = new SealedInstallEnforcer(
statusProvider,
options,
NullLogger<SealedInstallEnforcer>.Instance);
var requirements = new SealedRequirements(
MinBundleVersion: null,
MaxAdvisoryStalenessHours: 168,
RequireTimeAnchor: false,
AllowedOfflineDurationHours: 720,
RequireSignatureVerification: false);
var manifest = CreateManifest(sealedInstall: true, requirements);
var result = await enforcer.EnforceAsync(manifest, cancellationToken: TestContext.Current.CancellationToken);
Assert.False(result.Allowed);
Assert.Equal(SealedInstallErrorCodes.SealedRequirementsViolation, result.ErrorCode);
Assert.NotNull(result.RequirementViolations);
Assert.Single(result.RequirementViolations);
Assert.Equal("max_advisory_staleness_hours", result.RequirementViolations[0].Requirement);
}
[Fact]
public async Task EnforceAsync_WhenTimeAnchorMissing_ReturnsDenied()
{
var status = new SealedModeStatus(
Sealed: true,
Mode: "sealed",
SealedAt: DateTimeOffset.UtcNow,
SealedBy: null,
BundleVersion: "2025.10.0",
BundleDigest: null,
LastAdvisoryUpdate: DateTimeOffset.UtcNow,
AdvisoryStalenessHours: 0,
TimeAnchor: null, // No time anchor
EgressBlocked: true,
NetworkPolicy: null);
var statusProvider = new MockAirGapStatusProvider(status);
var options = Options.Create(new SealedInstallEnforcementOptions { Enabled = true });
var enforcer = new SealedInstallEnforcer(
statusProvider,
options,
NullLogger<SealedInstallEnforcer>.Instance);
var requirements = new SealedRequirements(
MinBundleVersion: null,
MaxAdvisoryStalenessHours: 168,
RequireTimeAnchor: true,
AllowedOfflineDurationHours: 720,
RequireSignatureVerification: false);
var manifest = CreateManifest(sealedInstall: true, requirements);
var result = await enforcer.EnforceAsync(manifest, cancellationToken: TestContext.Current.CancellationToken);
Assert.False(result.Allowed);
Assert.Equal(SealedInstallErrorCodes.SealedRequirementsViolation, result.ErrorCode);
Assert.NotNull(result.RequirementViolations);
Assert.Single(result.RequirementViolations);
Assert.Equal("require_time_anchor", result.RequirementViolations[0].Requirement);
}
[Fact]
public async Task EnforceAsync_WhenTimeAnchorInvalid_ReturnsDenied()
{
var status = new SealedModeStatus(
Sealed: true,
Mode: "sealed",
SealedAt: DateTimeOffset.UtcNow,
SealedBy: null,
BundleVersion: "2025.10.0",
BundleDigest: null,
LastAdvisoryUpdate: DateTimeOffset.UtcNow,
AdvisoryStalenessHours: 0,
TimeAnchor: new TimeAnchorInfo(DateTimeOffset.UtcNow, null, Valid: false, null),
EgressBlocked: true,
NetworkPolicy: null);
var statusProvider = new MockAirGapStatusProvider(status);
var options = Options.Create(new SealedInstallEnforcementOptions { Enabled = true });
var enforcer = new SealedInstallEnforcer(
statusProvider,
options,
NullLogger<SealedInstallEnforcer>.Instance);
var requirements = new SealedRequirements(
MinBundleVersion: null,
MaxAdvisoryStalenessHours: 168,
RequireTimeAnchor: true,
AllowedOfflineDurationHours: 720,
RequireSignatureVerification: false);
var manifest = CreateManifest(sealedInstall: true, requirements);
var result = await enforcer.EnforceAsync(manifest, cancellationToken: TestContext.Current.CancellationToken);
Assert.False(result.Allowed);
Assert.Equal(SealedInstallErrorCodes.SealedRequirementsViolation, result.ErrorCode);
Assert.NotNull(result.RequirementViolations);
Assert.Contains(result.RequirementViolations, v => v.Requirement == "require_time_anchor");
}
[Fact]
public async Task EnforceAsync_WhenStatusProviderFails_ReturnsDenied()
{
var statusProvider = new FailingAirGapStatusProvider();
var options = Options.Create(new SealedInstallEnforcementOptions { Enabled = true });
var enforcer = new SealedInstallEnforcer(
statusProvider,
options,
NullLogger<SealedInstallEnforcer>.Instance);
var manifest = CreateManifest(sealedInstall: true);
var result = await enforcer.EnforceAsync(manifest, cancellationToken: TestContext.Current.CancellationToken);
Assert.False(result.Allowed);
Assert.Equal(SealedInstallErrorCodes.SealedInstallViolation, result.ErrorCode);
Assert.Contains("Failed to verify", result.Message);
}
[Fact]
public void SealedModeStatus_Unsealed_ReturnsCorrectDefaults()
{
var status = SealedModeStatus.Unsealed();
Assert.False(status.Sealed);
Assert.Equal("unsealed", status.Mode);
Assert.Null(status.SealedAt);
Assert.Null(status.BundleVersion);
}
[Fact]
public void SealedModeStatus_Unavailable_ReturnsCorrectDefaults()
{
var status = SealedModeStatus.Unavailable();
Assert.False(status.Sealed);
Assert.Equal("unavailable", status.Mode);
}
[Fact]
public void SealedRequirements_Default_HasExpectedValues()
{
var defaults = SealedRequirements.Default;
Assert.Null(defaults.MinBundleVersion);
Assert.Equal(168, defaults.MaxAdvisoryStalenessHours);
Assert.True(defaults.RequireTimeAnchor);
Assert.Equal(720, defaults.AllowedOfflineDurationHours);
Assert.True(defaults.RequireSignatureVerification);
}
[Fact]
public void EnforcementResult_CreateAllowed_SetsProperties()
{
var result = SealedInstallEnforcementResult.CreateAllowed("Test message");
Assert.True(result.Allowed);
Assert.Null(result.ErrorCode);
Assert.Equal("Test message", result.Message);
Assert.Null(result.Violation);
Assert.Null(result.RequirementViolations);
}
[Fact]
public void EnforcementResult_CreateDenied_SetsProperties()
{
var violation = new SealedInstallViolation("pack-1", "1.0.0", true, false, "Seal the environment");
var result = SealedInstallEnforcementResult.CreateDenied(
SealedInstallErrorCodes.SealedInstallViolation,
"Denied message",
violation);
Assert.False(result.Allowed);
Assert.Equal(SealedInstallErrorCodes.SealedInstallViolation, result.ErrorCode);
Assert.Equal("Denied message", result.Message);
Assert.NotNull(result.Violation);
Assert.Equal("pack-1", result.Violation.PackId);
}
private sealed class MockAirGapStatusProvider : IAirGapStatusProvider
{
private readonly SealedModeStatus _status;
public MockAirGapStatusProvider(SealedModeStatus status)
{
_status = status;
}
public Task<SealedModeStatus> GetStatusAsync(string? tenantId = null, CancellationToken cancellationToken = default)
{
return Task.FromResult(_status);
}
}
private sealed class FailingAirGapStatusProvider : IAirGapStatusProvider
{
public Task<SealedModeStatus> GetStatusAsync(string? tenantId = null, CancellationToken cancellationToken = default)
{
throw new HttpRequestException("Connection refused");
}
}
}