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:
master
2025-10-19 18:36:22 +03:00
parent 2062da7a8b
commit d099a90f9b
966 changed files with 91038 additions and 1850 deletions

View File

@@ -0,0 +1,93 @@
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Core.Tests.Contracts;
public sealed class ComponentGraphBuilderTests
{
[Fact]
public void Build_AggregatesComponentsAcrossLayers()
{
var layer1 = LayerComponentFragment.Create("sha256:layer1", new[]
{
new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/a", "a", "1.0.0"),
LayerDigest = "sha256:layer1",
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/node_modules/a/package.json")),
Dependencies = ImmutableArray.Create("pkg:npm/x"),
Usage = ComponentUsage.Create(false),
Metadata = new ComponentMetadata
{
Scope = "runtime",
},
}
});
var layer2 = LayerComponentFragment.Create("sha256:layer2", new[]
{
new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/a", "a", "1.0.0"),
LayerDigest = "sha256:layer2",
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/node_modules/a/index.js")),
Dependencies = ImmutableArray.Create("pkg:npm/y"),
Usage = ComponentUsage.Create(true, new[] { "/app/start.sh" }),
},
new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/b", "b", "2.0.0"),
LayerDigest = "sha256:layer2",
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/node_modules/b/package.json")),
}
});
var graph = ComponentGraphBuilder.Build(new[] { layer1, layer2 });
Assert.Equal(new[] { "sha256:layer1", "sha256:layer2" }, graph.Layers.Select(layer => layer.LayerDigest));
Assert.Equal(new[] { "pkg:npm/a", "pkg:npm/b" }, graph.Components.Select(component => component.Identity.Key));
var componentA = graph.ComponentMap["pkg:npm/a"];
Assert.Equal("sha256:layer1", componentA.FirstLayerDigest);
Assert.Equal("sha256:layer2", componentA.LastLayerDigest);
Assert.Equal(new[] { "sha256:layer1", "sha256:layer2" }, componentA.LayerDigests);
Assert.True(componentA.Usage.UsedByEntrypoint);
Assert.Contains("/app/start.sh", componentA.Usage.Entrypoints);
Assert.Equal(new[] { "pkg:npm/x", "pkg:npm/y" }, componentA.Dependencies);
Assert.Equal("runtime", componentA.Metadata?.Scope);
Assert.Equal(2, componentA.Evidence.Length);
var componentB = graph.ComponentMap["pkg:npm/b"];
Assert.Equal("sha256:layer2", componentB.FirstLayerDigest);
Assert.Null(componentB.LastLayerDigest);
Assert.Single(componentB.LayerDigests, "sha256:layer2");
Assert.False(componentB.Usage.UsedByEntrypoint);
}
[Fact]
public void Build_DeterministicOrdering()
{
var fragments = new[]
{
LayerComponentFragment.Create("sha256:layer1", new[]
{
new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/c", "c"),
LayerDigest = "sha256:layer1",
},
new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/a", "a"),
LayerDigest = "sha256:layer1",
}
})
};
var graph1 = ComponentGraphBuilder.Build(fragments);
var graph2 = ComponentGraphBuilder.Build(fragments);
Assert.Equal(graph1.Components.Select(c => c.Identity.Key), graph2.Components.Select(c => c.Identity.Key));
}
}

View File

@@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.Serialization;
namespace StellaOps.Scanner.Core.Tests.Contracts;
public sealed class ComponentModelsTests
{
[Fact]
public void ComponentIdentity_Create_Trimmed()
{
var identity = ComponentIdentity.Create(" pkg:npm/foo ", " Foo ", " 1.0.0 ", " pkg:npm/foo@1.0.0 ", " library ", " group ");
Assert.Equal("pkg:npm/foo", identity.Key);
Assert.Equal("Foo", identity.Name);
Assert.Equal("1.0.0", identity.Version);
Assert.Equal("pkg:npm/foo@1.0.0", identity.Purl);
Assert.Equal("library", identity.ComponentType);
Assert.Equal("group", identity.Group);
}
[Fact]
public void ComponentUsage_Create_SortsEntrypoints()
{
var usage = ComponentUsage.Create(true, new[] { "/app/start.sh", "/app/start.sh", "/bin/init", " ", null! });
Assert.True(usage.UsedByEntrypoint);
Assert.Equal(new[] { "/app/start.sh", "/bin/init" }, usage.Entrypoints);
}
[Fact]
public void LayerComponentFragment_Create_SortsComponents()
{
var compB = new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/b", "b"),
LayerDigest = "sha256:layer2",
};
var compA = new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/a", "a"),
LayerDigest = "sha256:layer2",
};
var fragment = LayerComponentFragment.Create("sha256:layer2", new[] { compB, compA });
Assert.Equal("sha256:layer2", fragment.LayerDigest);
Assert.Equal(new[] { compA.Identity.Key, compB.Identity.Key }, fragment.Components.Select(c => c.Identity.Key));
}
[Fact]
public void ComponentRecord_Serializes_WithScannerDefaults()
{
var record = new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/test", "test", "1.0.0"),
LayerDigest = "sha256:layer",
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/package.json")),
Dependencies = ImmutableArray.Create("pkg:npm/dep"),
Usage = ComponentUsage.Create(true, new[] { "/app/start.sh" }),
Metadata = new ComponentMetadata
{
Scope = "runtime",
Licenses = new[] { "MIT" },
Properties = new Dictionary<string, string>
{
["source"] = "package-lock.json",
},
},
};
var json = JsonSerializer.Serialize(record, ScannerJsonOptions.Default);
var deserialized = JsonSerializer.Deserialize<ComponentRecord>(json, ScannerJsonOptions.Default);
Assert.NotNull(deserialized);
Assert.Equal(record.Identity.Key, deserialized!.Identity.Key);
Assert.Equal(record.Metadata?.Scope, deserialized.Metadata?.Scope);
Assert.True(deserialized.Usage.UsedByEntrypoint);
Assert.Equal(record.Usage.Entrypoints.AsSpan(), deserialized.Usage.Entrypoints.AsSpan());
}
}

View File

@@ -0,0 +1,130 @@
using System.Text.Json;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.Serialization;
using StellaOps.Scanner.Core.Utility;
using Xunit;
namespace StellaOps.Scanner.Core.Tests.Contracts;
public sealed class ScannerCoreContractsTests
{
private static readonly JsonSerializerOptions Options = ScannerJsonOptions.CreateDefault();
private static readonly ScanJobId SampleJobId = ScanJobId.From(Guid.Parse("8f4cc9c5-8245-4b9d-9b4f-5ae049631b7d"));
private static readonly DateTimeOffset SampleCreatedAt = new DateTimeOffset(2025, 10, 18, 14, 30, 15, TimeSpan.Zero).AddTicks(1_234_560);
[Fact]
public void ScanJob_RoundTripMatchesGoldenFixture()
{
var job = CreateSampleJob();
var json = JsonSerializer.Serialize(job, Options);
var expected = LoadFixture("scan-job.json");
Assert.Equal(expected, json);
var deserialized = JsonSerializer.Deserialize<ScanJob>(expected, Options);
Assert.NotNull(deserialized);
Assert.Equal(job.Id, deserialized!.Id);
Assert.Equal(job.ImageDigest, deserialized.ImageDigest);
Assert.Equal(job.CorrelationId, deserialized.CorrelationId);
Assert.Equal(job.Metadata, deserialized.Metadata);
Assert.Equal(job.Failure?.Message, deserialized.Failure?.Message);
Assert.Equal(job.Failure?.Details, deserialized.Failure?.Details);
}
[Fact]
public void ScanProgressEvent_RoundTripMatchesGoldenFixture()
{
var progress = CreateSampleProgressEvent();
var json = JsonSerializer.Serialize(progress, Options);
var expected = LoadFixture("scan-progress-event.json");
Assert.Equal(expected, json);
var deserialized = JsonSerializer.Deserialize<ScanProgressEvent>(expected, Options);
Assert.NotNull(deserialized);
Assert.Equal(progress.JobId, deserialized!.JobId);
Assert.Equal(progress.Stage, deserialized.Stage);
Assert.Equal(progress.Kind, deserialized.Kind);
Assert.Equal(progress.Sequence, deserialized.Sequence);
Assert.Equal(progress.Error?.Details, deserialized.Error?.Details);
}
[Fact]
public void ScannerError_RoundTripMatchesGoldenFixture()
{
var error = CreateSampleError();
var json = JsonSerializer.Serialize(error, Options);
var expected = LoadFixture("scanner-error.json");
Assert.Equal(expected, json);
var deserialized = JsonSerializer.Deserialize<ScannerError>(expected, Options);
Assert.NotNull(deserialized);
Assert.Equal(error.Code, deserialized!.Code);
Assert.Equal(error.Severity, deserialized.Severity);
Assert.Equal(error.Details, deserialized.Details);
}
private static ScanJob CreateSampleJob()
{
var updatedAt = SampleCreatedAt.AddSeconds(5);
var correlationId = ScannerIdentifiers.CreateCorrelationId(SampleJobId, nameof(ScanStage.AnalyzeOperatingSystem));
return new ScanJob(
SampleJobId,
ScanJobStatus.Running,
"registry.example.com/stellaops/scanner:1.2.3",
"SHA256:ABCDEF",
SampleCreatedAt,
updatedAt,
correlationId,
"tenant-a",
new Dictionary<string, string>
{
["requestId"] = "req-1234",
["source"] = "ci"
},
CreateSampleError());
}
private static ScanProgressEvent CreateSampleProgressEvent()
{
return new ScanProgressEvent(
SampleJobId,
ScanStage.AnalyzeOperatingSystem,
ScanProgressEventKind.Warning,
sequence: 3,
timestamp: SampleCreatedAt.AddSeconds(1),
percentComplete: 42.5,
message: "OS analyzer reported missing packages",
attributes: new Dictionary<string, string>
{
["package"] = "openssl",
["version"] = "1.1.1w"
},
error: CreateSampleError());
}
private static ScannerError CreateSampleError()
{
return new ScannerError(
ScannerErrorCode.AnalyzerFailure,
ScannerErrorSeverity.Error,
"Analyzer failed to parse layer",
SampleCreatedAt,
retryable: false,
details: new Dictionary<string, string>
{
["layerDigest"] = "sha256:deadbeef",
["attempt"] = "1"
},
stage: nameof(ScanStage.AnalyzeOperatingSystem),
component: "os-analyzer");
}
private static string LoadFixture(string fileName)
{
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", fileName);
return File.ReadAllText(path).Trim();
}
}

View File

@@ -0,0 +1 @@
{"id":"8f4cc9c582454b9d9b4f5ae049631b7d","status":"running","imageReference":"registry.example.com/stellaops/scanner:1.2.3","imageDigest":"sha256:abcdef","createdAt":"2025-10-18T14:30:15.123456+00:00","updatedAt":"2025-10-18T14:30:20.123456+00:00","correlationId":"scan-analyzeoperatingsystem-8f4cc9c582454b9d9b4f5ae049631b7d","tenantId":"tenant-a","metadata":{"requestId":"req-1234","source":"ci"},"failure":{"code":"analyzerFailure","severity":"error","message":"Analyzer failed to parse layer","timestamp":"2025-10-18T14:30:15.123456+00:00","retryable":false,"stage":"AnalyzeOperatingSystem","component":"os-analyzer","details":{"layerDigest":"sha256:deadbeef","attempt":"1"}}}

View File

@@ -0,0 +1 @@
{"jobId":"8f4cc9c582454b9d9b4f5ae049631b7d","stage":"analyzeOperatingSystem","kind":"warning","sequence":3,"timestamp":"2025-10-18T14:30:16.123456+00:00","percentComplete":42.5,"message":"OS analyzer reported missing packages","attributes":{"package":"openssl","version":"1.1.1w"},"error":{"code":"analyzerFailure","severity":"error","message":"Analyzer failed to parse layer","timestamp":"2025-10-18T14:30:15.123456+00:00","retryable":false,"stage":"AnalyzeOperatingSystem","component":"os-analyzer","details":{"layerDigest":"sha256:deadbeef","attempt":"1"}}}

View File

@@ -0,0 +1 @@
{"code":"analyzerFailure","severity":"error","message":"Analyzer failed to parse layer","timestamp":"2025-10-18T14:30:15.123456+00:00","retryable":false,"stage":"AnalyzeOperatingSystem","component":"os-analyzer","details":{"layerDigest":"sha256:deadbeef","attempt":"1"}}

View File

@@ -0,0 +1,103 @@
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.Observability;
using StellaOps.Scanner.Core.Utility;
using Xunit;
namespace StellaOps.Scanner.Core.Tests.Observability;
public sealed class ScannerLogExtensionsPerformanceTests
{
private const double ThresholdMicroseconds = 5.0;
private const int WarmupIterations = 5_000;
private const int MeasuredIterations = 200_000;
private static readonly DateTimeOffset Timestamp = ScannerTimestamps.Normalize(new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero));
private static readonly string Stage = nameof(ScanStage.AnalyzeOperatingSystem);
private static readonly string Component = "os-analyzer";
[Fact]
public void BeginScanScope_CompletesWithinThreshold()
{
using var factory = LoggerFactory.Create(builder => builder.AddFilter(static _ => false));
var logger = factory.CreateLogger("ScannerPerformance");
var job = CreateScanJob();
var microseconds = Measure(() => logger.BeginScanScope(job, Stage, Component));
Assert.True(microseconds <= ThresholdMicroseconds, $"Expected BeginScanScope to stay ≤ {ThresholdMicroseconds} µs but measured {microseconds:F3} µs.");
}
[Fact]
public void BeginProgressScope_CompletesWithinThreshold()
{
using var factory = LoggerFactory.Create(builder => builder.AddFilter(static _ => false));
var logger = factory.CreateLogger("ScannerPerformance");
var progress = CreateProgressEvent();
var microseconds = Measure(() => logger.BeginProgressScope(progress, Component));
Assert.True(microseconds <= ThresholdMicroseconds, $"Expected BeginProgressScope to stay ≤ {ThresholdMicroseconds} µs but measured {microseconds:F3} µs.");
}
private static double Measure(Func<IDisposable> scopeFactory)
{
for (var i = 0; i < WarmupIterations; i++)
{
using var scope = scopeFactory();
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var stopwatch = Stopwatch.StartNew();
for (var i = 0; i < MeasuredIterations; i++)
{
using var scope = scopeFactory();
}
stopwatch.Stop();
return stopwatch.Elapsed.TotalSeconds * 1_000_000 / MeasuredIterations;
}
private static ScanJob CreateScanJob()
{
var jobId = ScannerIdentifiers.CreateJobId("registry.example.com/stellaops/scanner:1.2.3", "sha256:abcdef", "tenant-a", "perf");
var correlationId = ScannerIdentifiers.CreateCorrelationId(jobId, Stage, Component);
return new ScanJob(
jobId,
ScanJobStatus.Running,
"registry.example.com/stellaops/scanner:1.2.3",
"sha256:abcdef",
Timestamp,
Timestamp,
correlationId,
"tenant-a",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["requestId"] = "req-perf"
});
}
private static ScanProgressEvent CreateProgressEvent()
{
var jobId = ScannerIdentifiers.CreateJobId("registry.example.com/stellaops/scanner:1.2.3", "sha256:abcdef", "tenant-a", "perf");
return new ScanProgressEvent(
jobId,
ScanStage.AnalyzeOperatingSystem,
ScanProgressEventKind.Progress,
sequence: 42,
Timestamp,
percentComplete: 10.5,
message: "performance check",
attributes: new Dictionary<string, string>(StringComparer.Ordinal)
{
["sample"] = "true"
});
}
}

View File

@@ -2,9 +2,9 @@ using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using Microsoft.Extensions.Time.Testing;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Scanner.Core.Security;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Security.Dpop;
using Xunit;
namespace StellaOps.Scanner.Core.Tests.Security;

View File

@@ -9,4 +9,7 @@
<ProjectReference Include="../StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="../StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Fixtures\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>