This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Zastava.Core.Configuration;
|
||||
using StellaOps.Zastava.Core.Diagnostics;
|
||||
using StellaOps.Zastava.Core.Security;
|
||||
using StellaOps.Zastava.Webhook.Backend;
|
||||
using StellaOps.Zastava.Webhook.Configuration;
|
||||
|
||||
namespace StellaOps.Zastava.Webhook.Tests.Backend;
|
||||
|
||||
public sealed class RuntimePolicyClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_SendsDpOpHeaderAndParsesResponse()
|
||||
{
|
||||
var requestCapture = new List<HttpRequestMessage>();
|
||||
var handler = new StubHttpMessageHandler(message =>
|
||||
{
|
||||
requestCapture.Add(message);
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(new
|
||||
{
|
||||
ttlSeconds = 120,
|
||||
results = new
|
||||
{
|
||||
image = new
|
||||
{
|
||||
signed = true,
|
||||
hasSbom = true,
|
||||
policyVerdict = "pass",
|
||||
reasons = Array.Empty<string>()
|
||||
}
|
||||
}
|
||||
}), Encoding.UTF8, "application/json")
|
||||
};
|
||||
return response;
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://scanner.internal")
|
||||
};
|
||||
|
||||
var runtimeOptions = Options.Create(new ZastavaRuntimeOptions
|
||||
{
|
||||
Tenant = "tenant-1",
|
||||
Environment = "test",
|
||||
Component = "webhook",
|
||||
Authority = new ZastavaAuthorityOptions
|
||||
{
|
||||
Audience = new[] { "scanner" },
|
||||
Scopes = new[] { "aud:scanner" }
|
||||
},
|
||||
Logging = new ZastavaRuntimeLoggingOptions(),
|
||||
Metrics = new ZastavaRuntimeMetricsOptions()
|
||||
});
|
||||
|
||||
var webhookOptions = Options.Create(new ZastavaWebhookOptions
|
||||
{
|
||||
Backend = new ZastavaWebhookBackendOptions
|
||||
{
|
||||
BaseAddress = new Uri("https://scanner.internal"),
|
||||
PolicyPath = "/api/v1/scanner/policy/runtime"
|
||||
}
|
||||
});
|
||||
|
||||
using var metrics = new StubRuntimeMetrics();
|
||||
var client = new RuntimePolicyClient(
|
||||
httpClient,
|
||||
new StubAuthorityTokenProvider(),
|
||||
new StaticOptionsMonitor<ZastavaRuntimeOptions>(runtimeOptions.Value),
|
||||
new StaticOptionsMonitor<ZastavaWebhookOptions>(webhookOptions.Value),
|
||||
metrics,
|
||||
NullLogger<RuntimePolicyClient>.Instance);
|
||||
|
||||
var response = await client.EvaluateAsync(new RuntimePolicyRequest
|
||||
{
|
||||
Namespace = "payments",
|
||||
Labels = new Dictionary<string, string> { ["app"] = "api" },
|
||||
Images = new[] { "image" }
|
||||
});
|
||||
|
||||
Assert.Equal(120, response.TtlSeconds);
|
||||
Assert.True(response.Results.ContainsKey("image"));
|
||||
var request = Assert.Single(requestCapture);
|
||||
Assert.Equal("DPoP", request.Headers.Authorization?.Scheme);
|
||||
Assert.Equal("runtime-token", request.Headers.Authorization?.Parameter);
|
||||
Assert.Equal("/api/v1/scanner/policy/runtime", request.RequestUri?.PathAndQuery);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_NonSuccess_ThrowsRuntimePolicyException()
|
||||
{
|
||||
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.BadGateway)
|
||||
{
|
||||
Content = new StringContent("upstream error")
|
||||
});
|
||||
var client = new RuntimePolicyClient(
|
||||
new HttpClient(handler) { BaseAddress = new Uri("https://scanner.internal") },
|
||||
new StubAuthorityTokenProvider(),
|
||||
new StaticOptionsMonitor<ZastavaRuntimeOptions>(new ZastavaRuntimeOptions
|
||||
{
|
||||
Tenant = "tenant",
|
||||
Environment = "test",
|
||||
Component = "webhook",
|
||||
Authority = new ZastavaAuthorityOptions { Audience = new[] { "scanner" } },
|
||||
Logging = new ZastavaRuntimeLoggingOptions(),
|
||||
Metrics = new ZastavaRuntimeMetricsOptions()
|
||||
}),
|
||||
new StaticOptionsMonitor<ZastavaWebhookOptions>(new ZastavaWebhookOptions()),
|
||||
new StubRuntimeMetrics(),
|
||||
NullLogger<RuntimePolicyClient>.Instance);
|
||||
|
||||
await Assert.ThrowsAsync<RuntimePolicyException>(() => client.EvaluateAsync(new RuntimePolicyRequest
|
||||
{
|
||||
Namespace = "payments",
|
||||
Labels = null,
|
||||
Images = new[] { "image" }
|
||||
}));
|
||||
}
|
||||
|
||||
private sealed class StubAuthorityTokenProvider : IZastavaAuthorityTokenProvider
|
||||
{
|
||||
public ValueTask InvalidateAsync(string audience, IEnumerable<string>? additionalScopes = null, CancellationToken cancellationToken = default)
|
||||
=> ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask<ZastavaOperationalToken> GetAsync(string audience, IEnumerable<string>? additionalScopes = null, CancellationToken cancellationToken = default)
|
||||
=> ValueTask.FromResult(new ZastavaOperationalToken("runtime-token", "DPoP", DateTimeOffset.UtcNow.AddMinutes(5), Array.Empty<string>()));
|
||||
}
|
||||
|
||||
private sealed class StubRuntimeMetrics : IZastavaRuntimeMetrics
|
||||
{
|
||||
public StubRuntimeMetrics()
|
||||
{
|
||||
Meter = new Meter("Test.Zastava.Webhook");
|
||||
RuntimeEvents = Meter.CreateCounter<long>("test.events");
|
||||
AdmissionDecisions = Meter.CreateCounter<long>("test.decisions");
|
||||
BackendLatencyMs = Meter.CreateHistogram<double>("test.backend.latency");
|
||||
DefaultTags = Array.Empty<KeyValuePair<string, object?>>();
|
||||
}
|
||||
|
||||
public Meter Meter { get; }
|
||||
|
||||
public Counter<long> RuntimeEvents { get; }
|
||||
|
||||
public Counter<long> AdmissionDecisions { get; }
|
||||
|
||||
public Histogram<double> BackendLatencyMs { get; }
|
||||
|
||||
public IReadOnlyList<KeyValuePair<string, object?>> DefaultTags { get; }
|
||||
|
||||
public void Dispose() => Meter.Dispose();
|
||||
}
|
||||
|
||||
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
{
|
||||
public StaticOptionsMonitor(T value)
|
||||
{
|
||||
CurrentValue = value;
|
||||
}
|
||||
|
||||
public T CurrentValue { get; }
|
||||
|
||||
public T Get(string? name) => CurrentValue;
|
||||
|
||||
public IDisposable OnChange(Action<T, string?> listener) => NullDisposable.Instance;
|
||||
|
||||
private sealed class NullDisposable : IDisposable
|
||||
{
|
||||
public static readonly NullDisposable Instance = new();
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, HttpResponseMessage> responder;
|
||||
|
||||
public StubHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responder)
|
||||
{
|
||||
this.responder = responder;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(responder(request));
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,7 @@ public sealed class SecretFileCertificateSourceTests
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest("CN=zastava-webhook", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
|
||||
using var certificateWithKey = certificate.CopyWithPrivateKey(rsa);
|
||||
using var certificateWithKey = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
|
||||
|
||||
var certificatePath = Path.GetTempFileName();
|
||||
var privateKeyPath = Path.GetTempFileName();
|
||||
@@ -23,7 +22,7 @@ public sealed class SecretFileCertificateSourceTests
|
||||
try
|
||||
{
|
||||
File.WriteAllText(certificatePath, certificateWithKey.ExportCertificatePem());
|
||||
using var exportRsa = certificateWithKey.GetRSAPrivateKey() ?? throw new InvalidOperationException("Missing RSA private key");
|
||||
using var exportRsa = certificateWithKey.GetRSAPrivateKey() ?? throw new InvalidOperationException("Missing RSA private key");
|
||||
var privateKeyPem = PemEncoding.Write("PRIVATE KEY", exportRsa.ExportPkcs8PrivateKey());
|
||||
File.WriteAllText(privateKeyPath, privateKeyPem);
|
||||
|
||||
@@ -52,8 +51,7 @@ public sealed class SecretFileCertificateSourceTests
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest("CN=zastava-webhook", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
|
||||
using var certificateWithKey = certificate.CopyWithPrivateKey(rsa);
|
||||
using var certificateWithKey = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
|
||||
|
||||
var pfxPath = Path.GetTempFileName();
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user