feat: Implement approvals workflow and notifications integration
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added approvals orchestration with persistence and workflow scaffolding. - Integrated notifications insights and staged resume hooks. - Introduced approval coordinator and policy notification bridge with unit tests. - Added approval decision API with resume requeue and persisted plan snapshots. - Documented the Excitor consensus API beta and provided JSON sample payload. - Created analyzers to flag usage of deprecated merge service APIs. - Implemented logging for artifact uploads and approval decision service. - Added tests for PackRunApprovalDecisionService and related components.
This commit is contained in:
@@ -85,6 +85,6 @@ public sealed class VexAttestationClientTests
|
||||
private sealed class FakeVerifier : IVexAttestationVerifier
|
||||
{
|
||||
public ValueTask<VexAttestationVerification> VerifyAsync(VexAttestationVerificationRequest request, CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(new VexAttestationVerification(true, ImmutableDictionary<string, string>.Empty));
|
||||
=> ValueTask.FromResult(new VexAttestationVerification(true, VexAttestationDiagnostics.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,42 +16,44 @@ public sealed class VexAttestationVerifierTests : IDisposable
|
||||
{
|
||||
private readonly VexAttestationMetrics _metrics = new();
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ReturnsValid_WhenEnvelopeMatches()
|
||||
{
|
||||
var (request, metadata, envelope) = await CreateSignedAttestationAsync();
|
||||
var verifier = CreateVerifier(options => options.RequireTransparencyLog = false);
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ReturnsValid_WhenEnvelopeMatches()
|
||||
{
|
||||
var (request, metadata, envelope) = await CreateSignedAttestationAsync();
|
||||
var verifier = CreateVerifier(options => options.RequireTransparencyLog = false);
|
||||
|
||||
var verification = await verifier.VerifyAsync(
|
||||
new VexAttestationVerificationRequest(request, metadata, envelope),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(verification.IsValid);
|
||||
Assert.Equal("valid", verification.Diagnostics.Result);
|
||||
Assert.Null(verification.Diagnostics.FailureReason);
|
||||
}
|
||||
|
||||
var verification = await verifier.VerifyAsync(
|
||||
new VexAttestationVerificationRequest(request, metadata, envelope),
|
||||
CancellationToken.None);
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ReturnsInvalid_WhenDigestMismatch()
|
||||
{
|
||||
var (request, metadata, envelope) = await CreateSignedAttestationAsync();
|
||||
var verifier = CreateVerifier(options => options.RequireTransparencyLog = false);
|
||||
|
||||
var tamperedMetadata = new VexAttestationMetadata(
|
||||
metadata.PredicateType,
|
||||
metadata.Rekor,
|
||||
"sha256:deadbeef",
|
||||
metadata.SignedAt);
|
||||
|
||||
var verification = await verifier.VerifyAsync(
|
||||
new VexAttestationVerificationRequest(request, tamperedMetadata, envelope),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(verification.IsValid);
|
||||
Assert.Equal("invalid", verification.Diagnostics.Result);
|
||||
Assert.Equal("sha256:deadbeef", verification.Diagnostics["metadata.envelopeDigest"]);
|
||||
Assert.Equal("envelope_digest_mismatch", verification.Diagnostics.FailureReason);
|
||||
}
|
||||
|
||||
Assert.True(verification.IsValid);
|
||||
Assert.Equal("valid", verification.Diagnostics["result"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ReturnsInvalid_WhenDigestMismatch()
|
||||
{
|
||||
var (request, metadata, envelope) = await CreateSignedAttestationAsync();
|
||||
var verifier = CreateVerifier(options => options.RequireTransparencyLog = false);
|
||||
|
||||
var tamperedMetadata = new VexAttestationMetadata(
|
||||
metadata.PredicateType,
|
||||
metadata.Rekor,
|
||||
"sha256:deadbeef",
|
||||
metadata.SignedAt);
|
||||
|
||||
var verification = await verifier.VerifyAsync(
|
||||
new VexAttestationVerificationRequest(request, tamperedMetadata, envelope),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(verification.IsValid);
|
||||
Assert.Equal("invalid", verification.Diagnostics["result"]);
|
||||
Assert.Equal("sha256:deadbeef", verification.Diagnostics["metadata.envelopeDigest"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_AllowsOfflineTransparency_WhenConfigured()
|
||||
{
|
||||
var (request, metadata, envelope) = await CreateSignedAttestationAsync(includeRekor: true);
|
||||
@@ -67,47 +69,50 @@ public sealed class VexAttestationVerifierTests : IDisposable
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(verification.IsValid);
|
||||
Assert.Equal("offline", verification.Diagnostics["rekor.state"]);
|
||||
Assert.Equal("degraded", verification.Diagnostics["result"]);
|
||||
Assert.Equal("offline", verification.Diagnostics.RekorState);
|
||||
Assert.Equal("degraded", verification.Diagnostics.Result);
|
||||
Assert.Null(verification.Diagnostics.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ReturnsInvalid_WhenTransparencyRequiredAndMissing()
|
||||
{
|
||||
var (request, metadata, envelope) = await CreateSignedAttestationAsync(includeRekor: false);
|
||||
var verifier = CreateVerifier(options =>
|
||||
{
|
||||
options.RequireTransparencyLog = true;
|
||||
options.AllowOfflineTransparency = false;
|
||||
});
|
||||
|
||||
var verification = await verifier.VerifyAsync(
|
||||
new VexAttestationVerificationRequest(request, metadata, envelope),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(verification.IsValid);
|
||||
Assert.Equal("missing", verification.Diagnostics["rekor.state"]);
|
||||
Assert.Equal("invalid", verification.Diagnostics["result"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ReturnsInvalid_WhenTransparencyUnavailableAndOfflineDisallowed()
|
||||
{
|
||||
var (request, metadata, envelope) = await CreateSignedAttestationAsync(includeRekor: true);
|
||||
var transparency = new ThrowingTransparencyLogClient();
|
||||
var verifier = CreateVerifier(options =>
|
||||
{
|
||||
options.RequireTransparencyLog = true;
|
||||
options.AllowOfflineTransparency = false;
|
||||
}, transparency);
|
||||
|
||||
var verification = await verifier.VerifyAsync(
|
||||
new VexAttestationVerificationRequest(request, metadata, envelope),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(verification.IsValid);
|
||||
Assert.Equal("unreachable", verification.Diagnostics["rekor.state"]);
|
||||
Assert.Equal("invalid", verification.Diagnostics["result"]);
|
||||
{
|
||||
var (request, metadata, envelope) = await CreateSignedAttestationAsync(includeRekor: false);
|
||||
var verifier = CreateVerifier(options =>
|
||||
{
|
||||
options.RequireTransparencyLog = true;
|
||||
options.AllowOfflineTransparency = false;
|
||||
});
|
||||
|
||||
var verification = await verifier.VerifyAsync(
|
||||
new VexAttestationVerificationRequest(request, metadata, envelope),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(verification.IsValid);
|
||||
Assert.Equal("missing", verification.Diagnostics.RekorState);
|
||||
Assert.Equal("invalid", verification.Diagnostics.Result);
|
||||
Assert.Equal("missing", verification.Diagnostics.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ReturnsInvalid_WhenTransparencyUnavailableAndOfflineDisallowed()
|
||||
{
|
||||
var (request, metadata, envelope) = await CreateSignedAttestationAsync(includeRekor: true);
|
||||
var transparency = new ThrowingTransparencyLogClient();
|
||||
var verifier = CreateVerifier(options =>
|
||||
{
|
||||
options.RequireTransparencyLog = true;
|
||||
options.AllowOfflineTransparency = false;
|
||||
}, transparency);
|
||||
|
||||
var verification = await verifier.VerifyAsync(
|
||||
new VexAttestationVerificationRequest(request, metadata, envelope),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(verification.IsValid);
|
||||
Assert.Equal("unreachable", verification.Diagnostics.RekorState);
|
||||
Assert.Equal("invalid", verification.Diagnostics.Result);
|
||||
Assert.Equal("unreachable", verification.Diagnostics.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -125,7 +130,7 @@ public sealed class VexAttestationVerifierTests : IDisposable
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(verification.IsValid);
|
||||
Assert.Equal("valid", verification.Diagnostics["result"]);
|
||||
Assert.Equal("valid", verification.Diagnostics.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -152,6 +157,8 @@ public sealed class VexAttestationVerifierTests : IDisposable
|
||||
|
||||
Assert.True(verification.IsValid);
|
||||
Assert.Equal("verified", verification.Diagnostics["signature.state"]);
|
||||
Assert.Equal("valid", verification.Diagnostics.Result);
|
||||
Assert.Null(verification.Diagnostics.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -179,6 +186,8 @@ public sealed class VexAttestationVerifierTests : IDisposable
|
||||
Assert.False(verification.IsValid);
|
||||
Assert.Equal("error", verification.Diagnostics["signature.state"]);
|
||||
Assert.Equal("verification_failed", verification.Diagnostics["signature.reason"]);
|
||||
Assert.Equal("verification_failed", verification.Diagnostics.FailureReason);
|
||||
Assert.Equal("invalid", verification.Diagnostics.Result);
|
||||
}
|
||||
|
||||
private async Task<(VexAttestationRequest Request, VexAttestationMetadata Metadata, string Envelope)> CreateSignedAttestationAsync(
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Globalization;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Excititor.Core;
|
||||
using StellaOps.Excititor.Attestation.Verification;
|
||||
using StellaOps.Excititor.Export;
|
||||
using StellaOps.Excititor.Policy;
|
||||
using StellaOps.Excititor.Storage.Mongo;
|
||||
@@ -291,7 +292,7 @@ public sealed class ExportEngineTests
|
||||
}
|
||||
|
||||
public ValueTask<VexAttestationVerification> VerifyAsync(VexAttestationVerificationRequest request, CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(new VexAttestationVerification(true, ImmutableDictionary<string, string>.Empty));
|
||||
=> ValueTask.FromResult(new VexAttestationVerification(true, VexAttestationDiagnostics.Empty));
|
||||
}
|
||||
|
||||
private sealed class RecordingCacheIndex : IVexCacheIndex
|
||||
|
||||
@@ -4,13 +4,14 @@ using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Excititor.Core;
|
||||
using StellaOps.Excititor.Export;
|
||||
using StellaOps.Excititor.Storage.Mongo;
|
||||
using StellaOps.Excititor.WebService.Services;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Excititor.Core;
|
||||
using StellaOps.Excititor.Attestation.Verification;
|
||||
using StellaOps.Excititor.Export;
|
||||
using StellaOps.Excititor.Storage.Mongo;
|
||||
using StellaOps.Excititor.WebService.Services;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Excititor.Attestation.Dsse;
|
||||
|
||||
@@ -162,7 +163,7 @@ internal static class TestServiceOverrides
|
||||
|
||||
public ValueTask<VexAttestationVerification> VerifyAsync(VexAttestationVerificationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var verification = new VexAttestationVerification(true, ImmutableDictionary<string, string>.Empty);
|
||||
var verification = new VexAttestationVerification(true, VexAttestationDiagnostics.Empty);
|
||||
return ValueTask.FromResult(verification);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,6 +504,21 @@ public sealed class DefaultVexProviderRunnerTests
|
||||
bool includeGlobal,
|
||||
CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(DefaultTrust);
|
||||
|
||||
public ValueTask<IssuerTrustResponseModel> SetIssuerTrustAsync(
|
||||
string tenantId,
|
||||
string issuerId,
|
||||
decimal weight,
|
||||
string? reason,
|
||||
CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(DefaultTrust);
|
||||
|
||||
public ValueTask DeleteIssuerTrustAsync(
|
||||
string tenantId,
|
||||
string issuerId,
|
||||
string? reason,
|
||||
CancellationToken cancellationToken)
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class NoopSignatureVerifier : IVexSignatureVerifier
|
||||
@@ -655,25 +670,25 @@ public sealed class DefaultVexProviderRunnerTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubAttestationVerifier : IVexAttestationVerifier
|
||||
{
|
||||
private readonly bool _isValid;
|
||||
private readonly ImmutableDictionary<string, string> _diagnostics;
|
||||
|
||||
public StubAttestationVerifier(bool isValid, ImmutableDictionary<string, string> diagnostics)
|
||||
{
|
||||
_isValid = isValid;
|
||||
_diagnostics = diagnostics;
|
||||
}
|
||||
|
||||
public int Invocations { get; private set; }
|
||||
|
||||
public ValueTask<VexAttestationVerification> VerifyAsync(VexAttestationVerificationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
Invocations++;
|
||||
return ValueTask.FromResult(new VexAttestationVerification(_isValid, _diagnostics));
|
||||
}
|
||||
}
|
||||
private sealed class StubAttestationVerifier : IVexAttestationVerifier
|
||||
{
|
||||
private readonly bool _isValid;
|
||||
private readonly VexAttestationDiagnostics _diagnostics;
|
||||
|
||||
public StubAttestationVerifier(bool isValid, ImmutableDictionary<string, string> diagnostics)
|
||||
{
|
||||
_isValid = isValid;
|
||||
_diagnostics = VexAttestationDiagnostics.FromBuilder(diagnostics.ToBuilder());
|
||||
}
|
||||
|
||||
public int Invocations { get; private set; }
|
||||
|
||||
public ValueTask<VexAttestationVerification> VerifyAsync(VexAttestationVerificationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
Invocations++;
|
||||
return ValueTask.FromResult(new VexAttestationVerification(_isValid, _diagnostics));
|
||||
}
|
||||
}
|
||||
|
||||
private static VexRawDocument CreateAttestationRawDocument(DateTimeOffset observedAt)
|
||||
{
|
||||
|
||||
@@ -249,19 +249,21 @@ public sealed class WorkerSignatureVerifierTests
|
||||
private sealed class StubAttestationVerifier : IVexAttestationVerifier
|
||||
{
|
||||
private readonly bool _isValid;
|
||||
private readonly ImmutableDictionary<string, string> _diagnostics;
|
||||
private readonly VexAttestationDiagnostics _diagnostics;
|
||||
|
||||
public StubAttestationVerifier(bool isValid, ImmutableDictionary<string, string>? diagnostics = null)
|
||||
{
|
||||
_isValid = isValid;
|
||||
_diagnostics = diagnostics ?? ImmutableDictionary<string, string>.Empty;
|
||||
}
|
||||
|
||||
public int Invocations { get; private set; }
|
||||
|
||||
public ValueTask<VexAttestationVerification> VerifyAsync(VexAttestationVerificationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
Invocations++;
|
||||
{
|
||||
_isValid = isValid;
|
||||
_diagnostics = diagnostics is null
|
||||
? VexAttestationDiagnostics.Empty
|
||||
: VexAttestationDiagnostics.FromBuilder(diagnostics.ToBuilder());
|
||||
}
|
||||
|
||||
public int Invocations { get; private set; }
|
||||
|
||||
public ValueTask<VexAttestationVerification> VerifyAsync(VexAttestationVerificationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
Invocations++;
|
||||
return ValueTask.FromResult(new VexAttestationVerification(_isValid, _diagnostics));
|
||||
}
|
||||
}
|
||||
@@ -269,7 +271,7 @@ public sealed class WorkerSignatureVerifierTests
|
||||
private sealed class StubIssuerDirectoryClient : IIssuerDirectoryClient
|
||||
{
|
||||
private readonly IReadOnlyList<IssuerKeyModel> _keys;
|
||||
private readonly IssuerTrustResponseModel _trust;
|
||||
private IssuerTrustResponseModel _trust;
|
||||
|
||||
private StubIssuerDirectoryClient(
|
||||
IReadOnlyList<IssuerKeyModel> keys,
|
||||
@@ -302,7 +304,7 @@ public sealed class WorkerSignatureVerifierTests
|
||||
null,
|
||||
null);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = DateTimeOffset.UnixEpoch;
|
||||
var overrideModel = new IssuerTrustOverrideModel(weight, "stub", now, "test", now, "test");
|
||||
return new StubIssuerDirectoryClient(
|
||||
new[] { key },
|
||||
@@ -322,6 +324,29 @@ public sealed class WorkerSignatureVerifierTests
|
||||
bool includeGlobal,
|
||||
CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(_trust);
|
||||
|
||||
public ValueTask<IssuerTrustResponseModel> SetIssuerTrustAsync(
|
||||
string tenantId,
|
||||
string issuerId,
|
||||
decimal weight,
|
||||
string? reason,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var now = DateTimeOffset.UnixEpoch;
|
||||
var overrideModel = new IssuerTrustOverrideModel(weight, "stub-set", now, "test", now, "test");
|
||||
_trust = new IssuerTrustResponseModel(overrideModel, null, weight);
|
||||
return ValueTask.FromResult(_trust);
|
||||
}
|
||||
|
||||
public ValueTask DeleteIssuerTrustAsync(
|
||||
string tenantId,
|
||||
string issuerId,
|
||||
string? reason,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_trust = new IssuerTrustResponseModel(null, null, 0m);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
|
||||
Reference in New Issue
Block a user