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
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:
@@ -13,11 +13,15 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AirGap.Policy;
|
||||
using StellaOps.TaskRunner.Core.AirGap;
|
||||
using StellaOps.TaskRunner.Core.Attestation;
|
||||
using StellaOps.TaskRunner.Core.Configuration;
|
||||
using StellaOps.TaskRunner.Core.Events;
|
||||
using StellaOps.TaskRunner.Core.Execution;
|
||||
using StellaOps.TaskRunner.Core.Execution.Simulation;
|
||||
using StellaOps.TaskRunner.Core.Planning;
|
||||
using StellaOps.TaskRunner.Core.TaskPacks;
|
||||
using StellaOps.TaskRunner.Infrastructure.AirGap;
|
||||
using StellaOps.TaskRunner.Infrastructure.Execution;
|
||||
using StellaOps.TaskRunner.WebService;
|
||||
using StellaOps.TaskRunner.WebService.Deprecation;
|
||||
@@ -101,6 +105,28 @@ builder.Services.AddSingleton<IPackRunJobScheduler>(sp => sp.GetRequiredService<
|
||||
builder.Services.AddSingleton<PackRunApprovalDecisionService>();
|
||||
builder.Services.AddApiDeprecation(builder.Configuration);
|
||||
builder.Services.AddSingleton<IDeprecationNotificationService, LoggingDeprecationNotificationService>();
|
||||
|
||||
// Sealed install enforcement (TASKRUN-AIRGAP-57-001)
|
||||
builder.Services.Configure<SealedInstallEnforcementOptions>(
|
||||
builder.Configuration.GetSection("TaskRunner:Enforcement:SealedInstall"));
|
||||
builder.Services.Configure<AirGapStatusProviderOptions>(
|
||||
builder.Configuration.GetSection("TaskRunner:AirGap"));
|
||||
builder.Services.AddHttpClient<IAirGapStatusProvider, HttpAirGapStatusProvider>((sp, client) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<AirGapStatusProviderOptions>>().Value;
|
||||
client.BaseAddress = new Uri(options.BaseUrl);
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
});
|
||||
builder.Services.AddSingleton<ISealedInstallEnforcer, SealedInstallEnforcer>();
|
||||
builder.Services.AddSingleton<IPackRunTimelineEventSink, InMemoryPackRunTimelineEventSink>();
|
||||
builder.Services.AddSingleton<IPackRunTimelineEventEmitter, PackRunTimelineEventEmitter>();
|
||||
builder.Services.AddSingleton<ISealedInstallAuditLogger, SealedInstallAuditLogger>();
|
||||
|
||||
// Pack run attestations (TASKRUN-OBS-54-001)
|
||||
builder.Services.AddSingleton<IPackRunAttestationStore, InMemoryPackRunAttestationStore>();
|
||||
builder.Services.AddSingleton<IPackRunAttestationSigner, StubPackRunAttestationSigner>();
|
||||
builder.Services.AddSingleton<IPackRunAttestationService, PackRunAttestationService>();
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -191,6 +217,19 @@ app.MapPost("/api/runs/{runId}/approvals/{approvalId}", HandleApplyApprovalDecis
|
||||
app.MapPost("/v1/task-runner/runs/{runId}/cancel", HandleCancelRun).WithName("CancelRun");
|
||||
app.MapPost("/api/runs/{runId}/cancel", HandleCancelRun).WithName("CancelRunApi");
|
||||
|
||||
// Attestation endpoints (TASKRUN-OBS-54-001)
|
||||
app.MapGet("/v1/task-runner/runs/{runId}/attestations", HandleListAttestations).WithName("ListRunAttestations");
|
||||
app.MapGet("/api/runs/{runId}/attestations", HandleListAttestations).WithName("ListRunAttestationsApi");
|
||||
|
||||
app.MapGet("/v1/task-runner/attestations/{attestationId}", HandleGetAttestation).WithName("GetAttestation");
|
||||
app.MapGet("/api/attestations/{attestationId}", HandleGetAttestation).WithName("GetAttestationApi");
|
||||
|
||||
app.MapGet("/v1/task-runner/attestations/{attestationId}/envelope", HandleGetAttestationEnvelope).WithName("GetAttestationEnvelope");
|
||||
app.MapGet("/api/attestations/{attestationId}/envelope", HandleGetAttestationEnvelope).WithName("GetAttestationEnvelopeApi");
|
||||
|
||||
app.MapPost("/v1/task-runner/attestations/{attestationId}/verify", HandleVerifyAttestation).WithName("VerifyAttestation");
|
||||
app.MapPost("/api/attestations/{attestationId}/verify", HandleVerifyAttestation).WithName("VerifyAttestationApi");
|
||||
|
||||
app.MapGet("/.well-known/openapi", (HttpResponse response) =>
|
||||
{
|
||||
var metadata = OpenApiMetadataFactory.Create("/openapi");
|
||||
@@ -212,6 +251,8 @@ async Task<IResult> HandleCreateRun(
|
||||
IPackRunStateStore stateStore,
|
||||
IPackRunLogStore logStore,
|
||||
IPackRunJobScheduler scheduler,
|
||||
ISealedInstallEnforcer sealedInstallEnforcer,
|
||||
ISealedInstallAuditLogger auditLogger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request is null || string.IsNullOrWhiteSpace(request.Manifest))
|
||||
@@ -229,6 +270,49 @@ async Task<IResult> HandleCreateRun(
|
||||
return Results.BadRequest(new { error = "Invalid manifest", detail = ex.Message });
|
||||
}
|
||||
|
||||
// TASKRUN-AIRGAP-57-001: Sealed install enforcement
|
||||
var enforcementResult = await sealedInstallEnforcer.EnforceAsync(
|
||||
manifest,
|
||||
request.TenantId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Log the enforcement decision
|
||||
await auditLogger.LogEnforcementAsync(
|
||||
manifest,
|
||||
enforcementResult,
|
||||
request.TenantId,
|
||||
request.RunId,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!enforcementResult.Allowed)
|
||||
{
|
||||
return Results.Json(new
|
||||
{
|
||||
error = new
|
||||
{
|
||||
code = enforcementResult.ErrorCode,
|
||||
message = enforcementResult.Message,
|
||||
details = new
|
||||
{
|
||||
pack_id = manifest.Metadata.Name,
|
||||
pack_version = manifest.Metadata.Version,
|
||||
sealed_install_required = manifest.Spec.SealedInstall,
|
||||
environment_sealed = enforcementResult.Violation?.ActualSealed ?? false,
|
||||
violations = enforcementResult.RequirementViolations?.Select(v => new
|
||||
{
|
||||
requirement = v.Requirement,
|
||||
expected = v.Expected,
|
||||
actual = v.Actual,
|
||||
message = v.Message
|
||||
}),
|
||||
recommendation = enforcementResult.Violation?.Recommendation
|
||||
}
|
||||
},
|
||||
status = "rejected",
|
||||
rejected_at = DateTimeOffset.UtcNow.ToString("O")
|
||||
}, statusCode: StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
var inputs = ConvertInputs(request.Inputs);
|
||||
var planResult = planner.Plan(manifest, inputs);
|
||||
if (!planResult.Success || planResult.Plan is null)
|
||||
@@ -465,6 +549,138 @@ async Task<IResult> HandleCancelRun(
|
||||
return Results.Accepted($"/v1/task-runner/runs/{runId}", new { status = "cancelled" });
|
||||
}
|
||||
|
||||
// Attestation handlers (TASKRUN-OBS-54-001)
|
||||
async Task<IResult> HandleListAttestations(
|
||||
string runId,
|
||||
[FromHeader(Name = "X-Tenant-ID")] string? tenantId,
|
||||
IPackRunAttestationService attestationService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(runId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "runId is required." });
|
||||
}
|
||||
|
||||
var effectiveTenantId = tenantId ?? "default";
|
||||
var attestations = await attestationService.ListByRunAsync(effectiveTenantId, runId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
runId,
|
||||
count = attestations.Count,
|
||||
attestations = attestations.Select(a => new
|
||||
{
|
||||
attestationId = a.AttestationId,
|
||||
status = a.Status.ToString().ToLowerInvariant(),
|
||||
predicateType = a.PredicateType,
|
||||
subjectCount = a.Subjects.Count,
|
||||
createdAt = a.CreatedAt.ToString("O"),
|
||||
hasEnvelope = a.Envelope is not null
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async Task<IResult> HandleGetAttestation(
|
||||
string attestationId,
|
||||
IPackRunAttestationService attestationService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Guid.TryParse(attestationId, out var id))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Invalid attestationId format." });
|
||||
}
|
||||
|
||||
var attestation = await attestationService.GetAsync(id, cancellationToken).ConfigureAwait(false);
|
||||
if (attestation is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
attestationId = attestation.AttestationId,
|
||||
tenantId = attestation.TenantId,
|
||||
runId = attestation.RunId,
|
||||
planHash = attestation.PlanHash,
|
||||
status = attestation.Status.ToString().ToLowerInvariant(),
|
||||
predicateType = attestation.PredicateType,
|
||||
subjects = attestation.Subjects.Select(s => new
|
||||
{
|
||||
name = s.Name,
|
||||
digest = s.Digest
|
||||
}),
|
||||
createdAt = attestation.CreatedAt.ToString("O"),
|
||||
evidenceSnapshotId = attestation.EvidenceSnapshotId,
|
||||
error = attestation.Error,
|
||||
metadata = attestation.Metadata
|
||||
});
|
||||
}
|
||||
|
||||
async Task<IResult> HandleGetAttestationEnvelope(
|
||||
string attestationId,
|
||||
IPackRunAttestationService attestationService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Guid.TryParse(attestationId, out var id))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Invalid attestationId format." });
|
||||
}
|
||||
|
||||
var envelope = await attestationService.GetEnvelopeAsync(id, cancellationToken).ConfigureAwait(false);
|
||||
if (envelope is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
payloadType = envelope.PayloadType,
|
||||
payload = envelope.Payload,
|
||||
signatures = envelope.Signatures.Select(s => new
|
||||
{
|
||||
keyid = s.KeyId,
|
||||
sig = s.Sig
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async Task<IResult> HandleVerifyAttestation(
|
||||
string attestationId,
|
||||
[FromBody] VerifyAttestationRequest? request,
|
||||
IPackRunAttestationService attestationService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Guid.TryParse(attestationId, out var id))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Invalid attestationId format." });
|
||||
}
|
||||
|
||||
var expectedSubjects = request?.ExpectedSubjects?.Select(s =>
|
||||
new PackRunAttestationSubject(s.Name, s.Digest ?? new Dictionary<string, string>())).ToList();
|
||||
|
||||
var verifyRequest = new PackRunAttestationVerificationRequest(
|
||||
AttestationId: id,
|
||||
ExpectedSubjects: expectedSubjects,
|
||||
VerifySignature: request?.VerifySignature ?? true,
|
||||
VerifySubjects: request?.VerifySubjects ?? (expectedSubjects is not null),
|
||||
CheckRevocation: request?.CheckRevocation ?? true);
|
||||
|
||||
var result = await attestationService.VerifyAsync(verifyRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var statusCode = result.Valid ? 200 : 400;
|
||||
return Results.Json(new
|
||||
{
|
||||
valid = result.Valid,
|
||||
attestationId = result.AttestationId,
|
||||
signatureStatus = result.SignatureStatus.ToString().ToLowerInvariant(),
|
||||
subjectStatus = result.SubjectStatus.ToString().ToLowerInvariant(),
|
||||
revocationStatus = result.RevocationStatus.ToString().ToLowerInvariant(),
|
||||
errors = result.Errors,
|
||||
verifiedAt = result.VerifiedAt.ToString("O")
|
||||
}, statusCode: statusCode);
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
static IDictionary<string, JsonNode?>? ConvertInputs(JsonObject? node)
|
||||
@@ -487,6 +703,15 @@ internal sealed record CreateRunRequest(string? RunId, string Manifest, JsonObje
|
||||
|
||||
internal sealed record SimulationRequest(string Manifest, JsonObject? Inputs);
|
||||
|
||||
// Attestation API request models (TASKRUN-OBS-54-001)
|
||||
internal sealed record VerifyAttestationRequest(
|
||||
IReadOnlyList<VerifyAttestationSubject>? ExpectedSubjects,
|
||||
bool VerifySignature = true,
|
||||
bool VerifySubjects = false,
|
||||
bool CheckRevocation = true);
|
||||
|
||||
internal sealed record VerifyAttestationSubject(string Name, IReadOnlyDictionary<string, string>? Digest);
|
||||
|
||||
internal sealed record SimulationResponse(
|
||||
string PlanHash,
|
||||
FailurePolicyResponse FailurePolicy,
|
||||
|
||||
Reference in New Issue
Block a user