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,148 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Emit.Composition;
using Xunit;
namespace StellaOps.Scanner.Emit.Tests.Composition;
public sealed class CycloneDxComposerTests
{
[Fact]
public void Compose_ProducesInventoryAndUsageArtifacts()
{
var request = BuildRequest();
var composer = new CycloneDxComposer();
var result = composer.Compose(request);
Assert.NotNull(result.Inventory);
Assert.StartsWith("urn:uuid:", result.Inventory.SerialNumber, StringComparison.Ordinal);
Assert.Equal("application/vnd.cyclonedx+json; version=1.5", result.Inventory.JsonMediaType);
Assert.Equal("application/vnd.cyclonedx+protobuf; version=1.5", result.Inventory.ProtobufMediaType);
Assert.Equal(2, result.Inventory.Components.Length);
Assert.NotNull(result.Usage);
Assert.Equal("application/vnd.cyclonedx+json; version=1.5; view=usage", result.Usage!.JsonMediaType);
Assert.Single(result.Usage.Components);
Assert.Equal("pkg:npm/a", result.Usage.Components[0].Identity.Key);
ValidateJson(result.Inventory.JsonBytes, expectedComponentCount: 2, expectedView: "inventory");
ValidateJson(result.Usage.JsonBytes, expectedComponentCount: 1, expectedView: "usage");
}
[Fact]
public void Compose_IsDeterministic()
{
var request = BuildRequest();
var composer = new CycloneDxComposer();
var first = composer.Compose(request);
var second = composer.Compose(request);
Assert.Equal(first.Inventory.JsonSha256, second.Inventory.JsonSha256);
Assert.Equal(first.Inventory.ProtobufSha256, second.Inventory.ProtobufSha256);
Assert.Equal(first.Inventory.SerialNumber, second.Inventory.SerialNumber);
Assert.NotNull(first.Usage);
Assert.NotNull(second.Usage);
Assert.Equal(first.Usage!.JsonSha256, second.Usage!.JsonSha256);
Assert.Equal(first.Usage.ProtobufSha256, second.Usage.ProtobufSha256);
Assert.Equal(first.Usage.SerialNumber, second.Usage.SerialNumber);
}
private static SbomCompositionRequest BuildRequest()
{
var fragments = new[]
{
LayerComponentFragment.Create("sha256:layer1", new[]
{
new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/a", "component-a", "1.0.0", "pkg:npm/a@1.0.0", "library"),
LayerDigest = "sha256:layer1",
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/node_modules/a/package.json")),
Dependencies = ImmutableArray.Create("pkg:npm/b"),
Usage = ComponentUsage.Create(true, new[] { "/app/start.sh" }),
Metadata = new ComponentMetadata
{
Scope = "runtime",
Licenses = new[] { "MIT" },
Properties = new Dictionary<string, string>
{
["stellaops:source"] = "package-lock.json",
["stellaops.os.analyzer"] = "apk",
["stellaops.os.architecture"] = "x86_64",
},
},
}
}),
LayerComponentFragment.Create("sha256:layer2", new[]
{
new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:npm/b", "component-b", "2.0.0", "pkg:npm/b@2.0.0", "library"),
LayerDigest = "sha256:layer2",
Evidence = ImmutableArray.Create(ComponentEvidence.FromPath("/app/node_modules/b/package.json")),
Usage = ComponentUsage.Create(false),
Metadata = new ComponentMetadata
{
Scope = "development",
Properties = new Dictionary<string, string>
{
["stellaops.os.analyzer"] = "language-node",
},
},
}
})
};
var image = new ImageArtifactDescriptor
{
ImageDigest = "sha256:1234567890abcdef",
ImageReference = "registry.example.com/app/service:1.2.3",
Repository = "registry.example.com/app/service",
Tag = "1.2.3",
Architecture = "amd64",
};
return SbomCompositionRequest.Create(
image,
fragments,
new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
generatorName: "StellaOps.Scanner",
generatorVersion: "0.10.0",
properties: new Dictionary<string, string>
{
["stellaops:scanId"] = "scan-1234",
});
}
private static void ValidateJson(byte[] data, int expectedComponentCount, string expectedView)
{
using var document = JsonDocument.Parse(data);
var root = document.RootElement;
Assert.True(root.TryGetProperty("metadata", out var metadata), "metadata property missing");
var properties = metadata.GetProperty("properties");
var viewProperty = properties.EnumerateArray()
.Single(prop => prop.GetProperty("name").GetString() == "stellaops:sbom.view");
Assert.Equal(expectedView, viewProperty.GetProperty("value").GetString());
var components = root.GetProperty("components").EnumerateArray().ToArray();
Assert.Equal(expectedComponentCount, components.Length);
var names = components.Select(component => component.GetProperty("name").GetString()).ToArray();
Assert.Equal(names, names.OrderBy(n => n, StringComparer.Ordinal).ToArray());
var firstComponentProperties = components[0].GetProperty("properties").EnumerateArray().ToDictionary(
element => element.GetProperty("name").GetString(),
element => element.GetProperty("value").GetString());
Assert.Equal("apk", firstComponentProperties["stellaops.os.analyzer"]);
Assert.Equal("x86_64", firstComponentProperties["stellaops.os.architecture"]);
}
}

View File

@@ -0,0 +1,52 @@
using System.Collections.Immutable;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Emit.Composition;
using Xunit;
namespace StellaOps.Scanner.Emit.Tests.Composition;
public class ScanAnalysisCompositionBuilderTests
{
[Fact]
public void FromAnalysis_BuildsRequest_WhenFragmentsPresent()
{
var analysis = new ScanAnalysisStore();
var fragment = LayerComponentFragment.Create(
"sha256:layer",
new[]
{
new ComponentRecord
{
Identity = ComponentIdentity.Create("pkg:test/a", "a", "1.0.0", "pkg:test/a@1.0.0", "library"),
LayerDigest = "sha256:layer",
Evidence = ImmutableArray<ComponentEvidence>.Empty,
Dependencies = ImmutableArray<string>.Empty,
Metadata = null,
Usage = ComponentUsage.Unused,
}
});
analysis.AppendLayerFragments(new[] { fragment });
var request = ScanAnalysisCompositionBuilder.FromAnalysis(
analysis,
new ImageArtifactDescriptor { ImageDigest = "sha256:image" },
DateTimeOffset.UtcNow,
generatorName: "test",
generatorVersion: "1.0.0");
Assert.Equal("sha256:image", request.Image.ImageDigest);
Assert.Single(request.LayerFragments);
Assert.Equal(fragment.LayerDigest, request.LayerFragments[0].LayerDigest);
}
[Fact]
public void BuildComponentGraph_ReturnsEmpty_WhenNoFragments()
{
var analysis = new ScanAnalysisStore();
var graph = ScanAnalysisCompositionBuilder.BuildComponentGraph(analysis);
Assert.Empty(graph.Components);
Assert.Empty(graph.Layers);
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using Collections.Special;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Emit.Index;
namespace StellaOps.Scanner.Emit.Tests.Index;
public sealed class BomIndexBuilderTests
{
[Fact]
public void Build_GeneratesDeterministicBinaryIndex_WithUsageBitmaps()
{
var graph = ComponentGraphBuilder.Build(new[]
{
LayerComponentFragment.Create("sha256:layer1", new[]
{
CreateComponent("pkg:npm/a", "1.0.0", "sha256:layer1", usageEntrypoints: new[] { "/app/start.sh" }),
CreateComponent("pkg:npm/b", "2.0.0", "sha256:layer1"),
}),
LayerComponentFragment.Create("sha256:layer2", new[]
{
CreateComponent("pkg:npm/b", "2.0.0", "sha256:layer2"),
CreateComponent("pkg:npm/c", "3.1.0", "sha256:layer2", usageEntrypoints: new[] { "/app/init.sh" }),
}),
});
var request = new BomIndexBuildRequest
{
ImageDigest = "sha256:image",
Graph = graph,
GeneratedAt = new DateTimeOffset(2025, 10, 19, 9, 45, 0, TimeSpan.Zero),
};
var builder = new BomIndexBuilder();
var artifact = builder.Build(request);
var second = builder.Build(request);
Assert.Equal(artifact.Sha256, second.Sha256);
Assert.Equal(artifact.Bytes, second.Bytes);
Assert.Equal(2, artifact.LayerCount);
Assert.Equal(3, artifact.ComponentCount);
Assert.Equal(2, artifact.EntrypointCount);
using var reader = new BinaryReader(new MemoryStream(artifact.Bytes), System.Text.Encoding.UTF8, leaveOpen: false);
ValidateHeader(reader, request);
var layers = ReadTable(reader, artifact.LayerCount);
Assert.Equal(new[] { "sha256:layer1", "sha256:layer2" }, layers);
var purls = ReadTable(reader, artifact.ComponentCount);
Assert.Equal(new[] { "pkg:npm/a", "pkg:npm/b", "pkg:npm/c" }, purls);
var componentBitmaps = ReadBitmaps(reader, artifact.ComponentCount);
Assert.Equal(new[] { new[] { 0 }, new[] { 0, 1 }, new[] { 1 } }, componentBitmaps);
var entrypoints = ReadTable(reader, artifact.EntrypointCount);
Assert.Equal(new[] { "/app/init.sh", "/app/start.sh" }, entrypoints);
var usageBitmaps = ReadBitmaps(reader, artifact.ComponentCount);
Assert.Equal(new[] { new[] { 1 }, Array.Empty<int>(), new[] { 0 } }, usageBitmaps);
}
private static void ValidateHeader(BinaryReader reader, BomIndexBuildRequest request)
{
var magic = reader.ReadBytes(7);
Assert.Equal("BOMIDX1", System.Text.Encoding.ASCII.GetString(magic));
var version = reader.ReadUInt16();
Assert.Equal(1u, version);
var flags = reader.ReadUInt16();
Assert.Equal(0x1, flags);
var digestLength = reader.ReadUInt16();
var digestBytes = reader.ReadBytes(digestLength);
Assert.Equal(request.ImageDigest, System.Text.Encoding.UTF8.GetString(digestBytes));
var unixMicroseconds = reader.ReadInt64();
var expectedMicroseconds = request.GeneratedAt.ToUniversalTime().ToUnixTimeMilliseconds() * 1000L;
expectedMicroseconds += request.GeneratedAt.ToUniversalTime().Ticks % TimeSpan.TicksPerMillisecond / 10;
Assert.Equal(expectedMicroseconds, unixMicroseconds);
var layers = reader.ReadUInt32();
var components = reader.ReadUInt32();
var entrypoints = reader.ReadUInt32();
Assert.Equal(2u, layers);
Assert.Equal(3u, components);
Assert.Equal(2u, entrypoints);
}
private static string[] ReadTable(BinaryReader reader, int count)
{
var values = new string[count];
for (var i = 0; i < count; i++)
{
var length = reader.ReadUInt16();
var bytes = reader.ReadBytes(length);
values[i] = System.Text.Encoding.UTF8.GetString(bytes);
}
return values;
}
private static int[][] ReadBitmaps(BinaryReader reader, int count)
{
var result = new int[count][];
for (var i = 0; i < count; i++)
{
var length = reader.ReadUInt32();
if (length == 0)
{
result[i] = Array.Empty<int>();
continue;
}
var bytes = reader.ReadBytes((int)length);
using var ms = new MemoryStream(bytes, writable: false);
var bitmap = RoaringBitmap.Deserialize(ms);
result[i] = bitmap.ToArray();
}
return result;
}
private static ComponentRecord CreateComponent(string key, string version, string layerDigest, string[]? usageEntrypoints = null)
{
var usage = usageEntrypoints is null
? ComponentUsage.Unused
: ComponentUsage.Create(true, usageEntrypoints);
return new ComponentRecord
{
Identity = ComponentIdentity.Create(key, key.Split('/', 2)[^1], version, key, "library"),
LayerDigest = layerDigest,
Usage = usage,
};
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Emit.Composition;
using StellaOps.Scanner.Emit.Index;
using StellaOps.Scanner.Emit.Packaging;
namespace StellaOps.Scanner.Emit.Tests.Packaging;
public sealed class ScannerArtifactPackageBuilderTests
{
[Fact]
public void BuildPackage_ProducesDescriptorsAndManifest()
{
var fragments = new[]
{
LayerComponentFragment.Create("sha256:layer1", new[]
{
CreateComponent(
"pkg:npm/a",
"1.0.0",
"sha256:layer1",
usage: ComponentUsage.Create(true, new[] { "/app/start.sh" }),
metadata: new Dictionary<string, string>
{
["stellaops.os.analyzer"] = "apk",
["stellaops.os.architecture"] = "x86_64",
}),
CreateComponent("pkg:npm/b", "2.0.0", "sha256:layer1"),
}),
LayerComponentFragment.Create("sha256:layer2", new[]
{
CreateComponent("pkg:npm/b", "2.0.0", "sha256:layer2"),
CreateComponent("pkg:npm/c", "3.0.0", "sha256:layer2", usage: ComponentUsage.Create(true, new[] { "/app/init.sh" })),
})
};
var request = SbomCompositionRequest.Create(
new ImageArtifactDescriptor
{
ImageDigest = "sha256:image",
ImageReference = "registry.example/app:latest",
Repository = "registry.example/app",
Tag = "latest",
},
fragments,
new DateTimeOffset(2025, 10, 19, 12, 30, 0, TimeSpan.Zero),
generatorName: "StellaOps.Scanner",
generatorVersion: "0.10.0");
var composer = new CycloneDxComposer();
var composition = composer.Compose(request);
var indexBuilder = new BomIndexBuilder();
var bomIndex = indexBuilder.Build(new BomIndexBuildRequest
{
ImageDigest = request.Image.ImageDigest,
Graph = composition.Graph,
GeneratedAt = request.GeneratedAt,
});
var packageBuilder = new ScannerArtifactPackageBuilder();
var package = packageBuilder.Build(request.Image.ImageDigest, request.GeneratedAt, composition, bomIndex);
Assert.Equal(5, package.Artifacts.Length); // inventory JSON+PB, usage JSON+PB, index
var kinds = package.Manifest.Artifacts.Select(entry => entry.Kind).ToArray();
Assert.Equal(new[] { "bom-index", "sbom-inventory", "sbom-inventory", "sbom-usage", "sbom-usage" }, kinds);
var manifestJson = package.Manifest.ToJsonBytes();
using var document = JsonDocument.Parse(manifestJson);
var root = document.RootElement;
Assert.Equal("sha256:image", root.GetProperty("imageDigest").GetString());
Assert.Equal(5, root.GetProperty("artifacts").GetArrayLength());
var usageEntry = root.GetProperty("artifacts").EnumerateArray().First(element => element.GetProperty("kind").GetString() == "sbom-usage");
Assert.Equal("application/vnd.cyclonedx+json; version=1.5; view=usage", usageEntry.GetProperty("mediaType").GetString());
}
private static ComponentRecord CreateComponent(string key, string version, string layerDigest, ComponentUsage? usage = null, IReadOnlyDictionary<string, string>? metadata = null)
{
return new ComponentRecord
{
Identity = ComponentIdentity.Create(key, key.Split('/', 2)[^1], version, key, "library"),
LayerDigest = layerDigest,
Usage = usage ?? ComponentUsage.Unused,
Metadata = metadata is null
? null
: new ComponentMetadata
{
Properties = metadata,
},
};
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Scanner.Emit\StellaOps.Scanner.Emit.csproj" />
</ItemGroup>
</Project>