feat: Initialize Zastava Webhook service with TLS and Authority authentication

- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint.
- Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately.
- Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly.
- Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
This commit is contained in:
2025-10-19 18:36:22 +03:00
parent 7e2fa0a42a
commit 5ce40d2eeb
966 changed files with 91038 additions and 1850 deletions

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
@@ -319,6 +320,61 @@ public sealed class CommandHandlersTests
}
}
[Fact]
public async Task HandleExcititorExportAsync_DownloadsWhenOutputProvided()
{
var original = Environment.ExitCode;
using var tempDir = new TempDirectory();
try
{
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
const string manifestJson = """
{
"exportId": "exports/20251019T101530Z/abcdef1234567890",
"format": "openvex",
"createdAt": "2025-10-19T10:15:30Z",
"artifact": { "algorithm": "sha256", "digest": "abcdef1234567890" },
"fromCache": false,
"sizeBytes": 2048,
"attestation": {
"rekor": {
"location": "https://rekor.example/api/v1/log/entries/123",
"logIndex": "123"
}
}
}
""";
backend.ExcititorResult = new ExcititorOperationResult(true, "ok", null, JsonDocument.Parse(manifestJson).RootElement.Clone());
var provider = BuildServiceProvider(backend);
var outputPath = Path.Combine(tempDir.Path, "export.json");
await CommandHandlers.HandleExcititorExportAsync(
provider,
format: "openvex",
delta: false,
scope: null,
since: null,
provider: null,
outputPath: outputPath,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.Single(backend.ExportDownloads);
var request = backend.ExportDownloads[0];
Assert.Equal("exports/20251019T101530Z/abcdef1234567890", request.ExportId);
Assert.Equal(Path.GetFullPath(outputPath), request.DestinationPath);
Assert.Equal("sha256", request.Algorithm);
Assert.Equal("abcdef1234567890", request.Digest);
}
finally
{
Environment.ExitCode = original;
}
}
[Theory]
[InlineData(null)]
[InlineData("default")]
@@ -624,6 +680,7 @@ public sealed class CommandHandlersTests
public string? LastExcititorRoute { get; private set; }
public HttpMethod? LastExcititorMethod { get; private set; }
public object? LastExcititorPayload { get; private set; }
public List<(string ExportId, string DestinationPath, string? Algorithm, string? Digest)> ExportDownloads { get; } = new();
public ExcititorOperationResult? ExcititorResult { get; set; } = new ExcititorOperationResult(true, "ok", null, null);
public IReadOnlyList<ExcititorProviderSummary> ProviderSummaries { get; set; } = Array.Empty<ExcititorProviderSummary>();
@@ -650,8 +707,29 @@ public sealed class CommandHandlersTests
return Task.FromResult(ExcititorResult ?? new ExcititorOperationResult(true, "ok", null, null));
}
public Task<ExcititorExportDownloadResult> DownloadExcititorExportAsync(string exportId, string destinationPath, string? expectedDigestAlgorithm, string? expectedDigest, CancellationToken cancellationToken)
{
var fullPath = Path.GetFullPath(destinationPath);
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(fullPath, "{}");
var info = new FileInfo(fullPath);
ExportDownloads.Add((exportId, fullPath, expectedDigestAlgorithm, expectedDigest));
return Task.FromResult(new ExcititorExportDownloadResult(fullPath, info.Length, false));
}
public Task<IReadOnlyList<ExcititorProviderSummary>> GetExcititorProvidersAsync(bool includeDisabled, CancellationToken cancellationToken)
=> Task.FromResult(ProviderSummaries);
public Task<RuntimePolicyEvaluationResult> EvaluateRuntimePolicyAsync(RuntimePolicyEvaluationRequest request, CancellationToken cancellationToken)
{
var empty = new ReadOnlyDictionary<string, RuntimePolicyImageDecision>(new Dictionary<string, RuntimePolicyImageDecision>());
return Task.FromResult(new RuntimePolicyEvaluationResult(0, null, null, empty));
}
}
private sealed class StubExecutor : IScannerExecutor

View File

@@ -1,9 +1,11 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
@@ -375,6 +377,107 @@ public sealed class BackendOperationsClientTests
Assert.True(tokenClient.Requests > 0);
}
[Fact]
public async Task EvaluateRuntimePolicyAsync_ParsesDecisionPayload()
{
var handler = new StubHttpMessageHandler((request, _) =>
{
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal("/api/scanner/policy/runtime", request.RequestUri!.AbsolutePath);
var body = request.Content!.ReadAsStringAsync().GetAwaiter().GetResult();
using var document = JsonDocument.Parse(body);
var root = document.RootElement;
Assert.Equal("prod", root.GetProperty("namespace").GetString());
Assert.Equal("payments", root.GetProperty("labels").GetProperty("app").GetString());
var images = root.GetProperty("images");
Assert.Equal(2, images.GetArrayLength());
Assert.Equal("ghcr.io/app@sha256:abc", images[0].GetString());
Assert.Equal("ghcr.io/api@sha256:def", images[1].GetString());
var responseJson = @"{
""ttlSeconds"": 120,
""policyRevision"": ""rev-123"",
""expiresAtUtc"": ""2025-10-19T12:34:56Z"",
""results"": {
""ghcr.io/app@sha256:abc"": {
""policyVerdict"": ""pass"",
""signed"": true,
""hasSbom"": true,
""reasons"": [],
""rekor"": { ""uuid"": ""uuid-1"", ""url"": ""https://rekor.example/uuid-1"" },
""confidence"": 0.87,
""quiet"": false,
""metadata"": { ""note"": ""cached"" }
},
""ghcr.io/api@sha256:def"": {
""policyVerdict"": ""fail"",
""signed"": false,
""hasSbom"": false,
""reasons"": [""unsigned"", ""missing sbom""]
}
}
}";
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(responseJson, Encoding.UTF8, "application/json"),
RequestMessage = request
};
});
var httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://scanner.example/")
};
var options = new StellaOpsCliOptions
{
BackendUrl = "https://scanner.example/"
};
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());
var labels = new ReadOnlyDictionary<string, string>(new Dictionary<string, string> { ["app"] = "payments" });
var imagesList = new ReadOnlyCollection<string>(new List<string>
{
"ghcr.io/app@sha256:abc",
"ghcr.io/app@sha256:abc",
"ghcr.io/api@sha256:def"
});
var requestModel = new RuntimePolicyEvaluationRequest("prod", labels, imagesList);
var result = await client.EvaluateRuntimePolicyAsync(requestModel, CancellationToken.None);
Assert.Equal(120, result.TtlSeconds);
Assert.Equal("rev-123", result.PolicyRevision);
Assert.Equal(DateTimeOffset.Parse("2025-10-19T12:34:56Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal), result.ExpiresAtUtc);
Assert.Equal(2, result.Decisions.Count);
var primary = result.Decisions["ghcr.io/app@sha256:abc"];
Assert.Equal("pass", primary.PolicyVerdict);
Assert.True(primary.Signed);
Assert.True(primary.HasSbom);
Assert.Empty(primary.Reasons);
Assert.NotNull(primary.Rekor);
Assert.Equal("uuid-1", primary.Rekor!.Uuid);
Assert.Equal("https://rekor.example/uuid-1", primary.Rekor.Url);
Assert.Equal(0.87, Assert.IsType<double>(primary.AdditionalProperties["confidence"]), 3);
Assert.False(Assert.IsType<bool>(primary.AdditionalProperties["quiet"]));
var metadataJson = Assert.IsType<string>(primary.AdditionalProperties["metadata"]);
using var metadataDocument = JsonDocument.Parse(metadataJson);
Assert.Equal("cached", metadataDocument.RootElement.GetProperty("note").GetString());
var secondary = result.Decisions["ghcr.io/api@sha256:def"];
Assert.Equal("fail", secondary.PolicyVerdict);
Assert.False(secondary.Signed);
Assert.False(secondary.HasSbom);
Assert.Collection(secondary.Reasons,
item => Assert.Equal("unsigned", item),
item => Assert.Equal("missing sbom", item));
}
private sealed class StubTokenClient : IStellaOpsTokenClient
{
private readonly StellaOpsTokenResult _tokenResult;