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

@@ -9,6 +9,7 @@ using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Json.Schema;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
@@ -23,12 +24,11 @@ public static class PolicyScoringConfigBinder
{
private const string DefaultResourceName = "StellaOps.Policy.Schemas.policy-scoring-default.json";
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
private static readonly JsonSchema ScoringSchema = PolicyScoringSchema.Schema;
private static readonly ImmutableDictionary<string, double> DefaultReachabilityBuckets = CreateDefaultReachabilityBuckets();
private static readonly PolicyUnknownConfidenceConfig DefaultUnknownConfidence = CreateDefaultUnknownConfidence();
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
@@ -69,6 +69,13 @@ public static class PolicyScoringConfigBinder
}
var issues = ImmutableArray.CreateBuilder<PolicyIssue>();
var schemaIssues = ValidateAgainstSchema(root);
issues.AddRange(schemaIssues);
if (schemaIssues.Any(static issue => issue.Severity == PolicyIssueSeverity.Error))
{
return new PolicyScoringBindingResult(false, null, issues.ToImmutable());
}
var config = BuildConfig(obj, issues);
var hasErrors = issues.Any(issue => issue.Severity == PolicyIssueSeverity.Error);
return new PolicyScoringBindingResult(!hasErrors, config, issues.ToImmutable());
@@ -101,6 +108,127 @@ public static class PolicyScoringConfigBinder
return PolicyBinderUtilities.ConvertYamlObject(yamlObject);
}
private static ImmutableArray<PolicyIssue> ValidateAgainstSchema(JsonNode root)
{
try
{
using var document = JsonDocument.Parse(root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = false,
}));
var result = ScoringSchema.Evaluate(document.RootElement, new EvaluationOptions
{
OutputFormat = OutputFormat.List,
RequireFormatValidation = true,
});
if (result.IsValid)
{
return ImmutableArray<PolicyIssue>.Empty;
}
var issues = ImmutableArray.CreateBuilder<PolicyIssue>();
var seen = new HashSet<string>(StringComparer.Ordinal);
CollectSchemaIssues(result, issues, seen);
return issues.ToImmutable();
}
catch (JsonException ex)
{
return ImmutableArray.Create(PolicyIssue.Error("scoring.schema.normalize", $"Failed to normalize scoring configuration for schema validation: {ex.Message}", "$"));
}
}
private static void CollectSchemaIssues(EvaluationResults result, ImmutableArray<PolicyIssue>.Builder issues, HashSet<string> seen)
{
if (result.Errors is { Count: > 0 })
{
foreach (var pair in result.Errors)
{
var keyword = SanitizeKeyword(pair.Key);
var path = ConvertPointerToPath(result.InstanceLocation?.ToString() ?? "#");
var message = pair.Value ?? "Schema violation.";
var key = $"{path}|{keyword}|{message}";
if (seen.Add(key))
{
issues.Add(PolicyIssue.Error($"scoring.schema.{keyword}", message, path));
}
}
}
if (result.Details is null)
{
return;
}
foreach (var detail in result.Details)
{
CollectSchemaIssues(detail, issues, seen);
}
}
private static string ConvertPointerToPath(string pointer)
{
if (string.IsNullOrEmpty(pointer) || pointer == "#")
{
return "$";
}
if (pointer[0] == '#')
{
pointer = pointer.Length > 1 ? pointer[1..] : string.Empty;
}
if (string.IsNullOrEmpty(pointer))
{
return "$";
}
var segments = pointer.Split('/', StringSplitOptions.RemoveEmptyEntries);
var builder = new StringBuilder("$");
foreach (var segment in segments)
{
var unescaped = segment.Replace("~1", "/").Replace("~0", "~");
if (int.TryParse(unescaped, out var index))
{
builder.Append('[').Append(index).Append(']');
}
else
{
builder.Append('.').Append(unescaped);
}
}
return builder.ToString();
}
private static string SanitizeKeyword(string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
{
return "unknown";
}
var builder = new StringBuilder(keyword.Length);
foreach (var ch in keyword)
{
if (char.IsLetterOrDigit(ch))
{
builder.Append(char.ToLowerInvariant(ch));
}
else if (ch is '.' or '_' or '-')
{
builder.Append(ch);
}
else
{
builder.Append('_');
}
}
return builder.Length == 0 ? "unknown" : builder.ToString();
}
private static PolicyScoringConfig BuildConfig(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
{
var version = ReadString(obj, "version", issues, required: true) ?? PolicyScoringConfig.BaselineVersion;
@@ -110,6 +238,8 @@ public static class PolicyScoringConfigBinder
var warnPenalty = ReadDouble(obj, "warnPenalty", issues, defaultValue: 15);
var ignorePenalty = ReadDouble(obj, "ignorePenalty", issues, defaultValue: 35);
var trustOverrides = ReadTrustOverrides(obj, issues);
var reachabilityBuckets = ReadReachabilityBuckets(obj, issues);
var unknownConfidence = ReadUnknownConfidence(obj, issues);
return new PolicyScoringConfig(
version,
@@ -117,7 +247,212 @@ public static class PolicyScoringConfigBinder
quietPenalty,
warnPenalty,
ignorePenalty,
trustOverrides);
trustOverrides,
reachabilityBuckets,
unknownConfidence);
}
private static ImmutableDictionary<string, double> CreateDefaultReachabilityBuckets()
{
var builder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
builder["entrypoint"] = 1.0;
builder["direct"] = 0.85;
builder["indirect"] = 0.6;
builder["runtime"] = 0.45;
builder["unreachable"] = 0.25;
builder["unknown"] = 0.5;
return builder.ToImmutable();
}
private static PolicyUnknownConfidenceConfig CreateDefaultUnknownConfidence()
{
var bands = ImmutableArray.Create(
new PolicyUnknownConfidenceBand("high", 0.65, "Fresh unknowns with recent telemetry."),
new PolicyUnknownConfidenceBand("medium", 0.35, "Unknowns aging toward action required."),
new PolicyUnknownConfidenceBand("low", 0.0, "Stale unknowns that must be triaged."));
return new PolicyUnknownConfidenceConfig(0.8, 0.05, 0.2, bands);
}
private static ImmutableDictionary<string, double> ReadReachabilityBuckets(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
{
if (!obj.TryGetPropertyValue("reachabilityBuckets", out var node))
{
issues.Add(PolicyIssue.Warning("scoring.reachability.default", "reachabilityBuckets not specified; defaulting to baseline weights.", "$.reachabilityBuckets"));
return DefaultReachabilityBuckets;
}
if (node is not JsonObject bucketsObj)
{
issues.Add(PolicyIssue.Error("scoring.reachability.type", "reachabilityBuckets must be an object.", "$.reachabilityBuckets"));
return DefaultReachabilityBuckets;
}
var builder = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in bucketsObj)
{
if (pair.Value is null)
{
issues.Add(PolicyIssue.Warning("scoring.reachability.null", $"Bucket '{pair.Key}' is null; defaulting to 0.", $"$.reachabilityBuckets.{pair.Key}"));
builder[pair.Key] = 0;
continue;
}
var value = ExtractDouble(pair.Value, issues, $"$.reachabilityBuckets.{pair.Key}");
builder[pair.Key] = value;
}
if (builder.Count == 0)
{
issues.Add(PolicyIssue.Warning("scoring.reachability.empty", "No reachability buckets defined; using defaults.", "$.reachabilityBuckets"));
return DefaultReachabilityBuckets;
}
return builder.ToImmutable();
}
private static PolicyUnknownConfidenceConfig ReadUnknownConfidence(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
{
if (!obj.TryGetPropertyValue("unknownConfidence", out var node))
{
issues.Add(PolicyIssue.Warning("scoring.unknown.default", "unknownConfidence not specified; defaulting to baseline decay settings.", "$.unknownConfidence"));
return DefaultUnknownConfidence;
}
if (node is not JsonObject configObj)
{
issues.Add(PolicyIssue.Error("scoring.unknown.type", "unknownConfidence must be an object.", "$.unknownConfidence"));
return DefaultUnknownConfidence;
}
var initial = DefaultUnknownConfidence.Initial;
if (configObj.TryGetPropertyValue("initial", out var initialNode))
{
initial = ExtractDouble(initialNode, issues, "$.unknownConfidence.initial");
}
else
{
issues.Add(PolicyIssue.Warning("scoring.unknown.initial.default", "initial not specified; using baseline value.", "$.unknownConfidence.initial"));
}
var decay = DefaultUnknownConfidence.DecayPerDay;
if (configObj.TryGetPropertyValue("decayPerDay", out var decayNode))
{
decay = ExtractDouble(decayNode, issues, "$.unknownConfidence.decayPerDay");
}
else
{
issues.Add(PolicyIssue.Warning("scoring.unknown.decay.default", "decayPerDay not specified; using baseline value.", "$.unknownConfidence.decayPerDay"));
}
var floor = DefaultUnknownConfidence.Floor;
if (configObj.TryGetPropertyValue("floor", out var floorNode))
{
floor = ExtractDouble(floorNode, issues, "$.unknownConfidence.floor");
}
else
{
issues.Add(PolicyIssue.Warning("scoring.unknown.floor.default", "floor not specified; using baseline value.", "$.unknownConfidence.floor"));
}
var bands = ReadConfidenceBands(configObj, issues);
if (bands.IsDefaultOrEmpty)
{
bands = DefaultUnknownConfidence.Bands;
}
if (initial < 0 || initial > 1)
{
issues.Add(PolicyIssue.Warning("scoring.unknown.initial.range", "initial confidence should be between 0 and 1. Clamping to valid range.", "$.unknownConfidence.initial"));
initial = Math.Clamp(initial, 0, 1);
}
if (decay < 0 || decay > 1)
{
issues.Add(PolicyIssue.Warning("scoring.unknown.decay.range", "decayPerDay should be between 0 and 1. Clamping to valid range.", "$.unknownConfidence.decayPerDay"));
decay = Math.Clamp(decay, 0, 1);
}
if (floor < 0 || floor > 1)
{
issues.Add(PolicyIssue.Warning("scoring.unknown.floor.range", "floor should be between 0 and 1. Clamping to valid range.", "$.unknownConfidence.floor"));
floor = Math.Clamp(floor, 0, 1);
}
return new PolicyUnknownConfidenceConfig(initial, decay, floor, bands);
}
private static ImmutableArray<PolicyUnknownConfidenceBand> ReadConfidenceBands(JsonObject configObj, ImmutableArray<PolicyIssue>.Builder issues)
{
if (!configObj.TryGetPropertyValue("bands", out var node))
{
return ImmutableArray<PolicyUnknownConfidenceBand>.Empty;
}
if (node is not JsonArray array)
{
issues.Add(PolicyIssue.Error("scoring.unknown.bands.type", "unknownConfidence.bands must be an array.", "$.unknownConfidence.bands"));
return ImmutableArray<PolicyUnknownConfidenceBand>.Empty;
}
var builder = ImmutableArray.CreateBuilder<PolicyUnknownConfidenceBand>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var index = 0; index < array.Count; index++)
{
var element = array[index];
if (element is not JsonObject bandObj)
{
issues.Add(PolicyIssue.Warning("scoring.unknown.band.type", "Band entry must be an object.", $"$.unknownConfidence.bands[{index}]"));
continue;
}
string? name = null;
if (bandObj.TryGetPropertyValue("name", out var nameNode) && nameNode is JsonValue nameValue && nameValue.TryGetValue(out string? text))
{
name = text?.Trim();
}
if (string.IsNullOrWhiteSpace(name))
{
issues.Add(PolicyIssue.Error("scoring.unknown.band.name", "Band entry requires a non-empty 'name'.", $"$.unknownConfidence.bands[{index}].name"));
continue;
}
if (!seen.Add(name))
{
issues.Add(PolicyIssue.Warning("scoring.unknown.band.duplicate", $"Duplicate band '{name}' encountered.", $"$.unknownConfidence.bands[{index}].name"));
continue;
}
if (!bandObj.TryGetPropertyValue("min", out var minNode))
{
issues.Add(PolicyIssue.Error("scoring.unknown.band.min", $"Band '{name}' is missing 'min'.", $"$.unknownConfidence.bands[{index}].min"));
continue;
}
var min = ExtractDouble(minNode, issues, $"$.unknownConfidence.bands[{index}].min");
if (min < 0 || min > 1)
{
issues.Add(PolicyIssue.Warning("scoring.unknown.band.range", $"Band '{name}' min should be between 0 and 1. Clamping to valid range.", $"$.unknownConfidence.bands[{index}].min"));
min = Math.Clamp(min, 0, 1);
}
string? description = null;
if (bandObj.TryGetPropertyValue("description", out var descriptionNode) && descriptionNode is JsonValue descriptionValue && descriptionValue.TryGetValue(out string? descriptionText))
{
description = descriptionText?.Trim();
}
builder.Add(new PolicyUnknownConfidenceBand(name, min, description));
}
if (builder.Count == 0)
{
return ImmutableArray<PolicyUnknownConfidenceBand>.Empty;
}
return builder.ToImmutable()
.OrderByDescending(static band => band.Min)
.ToImmutableArray();
}
private static ImmutableDictionary<PolicySeverity, double> ReadSeverityWeights(JsonObject obj, ImmutableArray<PolicyIssue>.Builder issues)
@@ -228,6 +563,8 @@ internal static class PolicyBinderUtilities
{
case null:
return null;
case string s when bool.TryParse(s, out var boolValue):
return JsonValue.Create(boolValue);
case string s:
return JsonValue.Create(s);
case bool b: