up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
2025-10-24 09:15:37 +03:00
parent f4d7a15a00
commit 17d861e4ab
163 changed files with 14269 additions and 452 deletions

View File

@@ -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));
}
}

View File

@@ -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