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

@@ -11,7 +11,7 @@ public interface IVexConsensusPolicy
string Version { get; }
/// <summary>
/// Returns the effective weight (0-1) to apply for the provided VEX source.
/// Returns the effective weight (bounded by the policy ceiling) to apply for the provided VEX source.
/// </summary>
double GetProviderWeight(VexProvider provider);

View File

@@ -5,5 +5,5 @@ If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md
|EXCITITOR-CORE-01-001 Canonical VEX domain records|Team Excititor Core & Policy|docs/ARCHITECTURE_EXCITITOR.md|DONE (2025-10-15) Introduced `VexClaim`, `VexConsensus`, provider metadata, export manifest records, and deterministic JSON serialization with tests covering canonical ordering and query signatures.|
|EXCITITOR-CORE-01-002 Trust-weighted consensus resolver|Team Excititor Core & Policy|EXCITITOR-CORE-01-001|DONE (2025-10-15) Added consensus resolver, baseline policy (tier weights + justification gate), telemetry output, and tests covering acceptance, conflict ties, and determinism.|
|EXCITITOR-CORE-01-003 Shared contracts & query signatures|Team Excititor Core & Policy|EXCITITOR-CORE-01-001|DONE (2025-10-15) Published connector/normalizer/exporter/attestation abstractions and expanded deterministic `VexQuerySignature`/hash utilities with test coverage.|
|EXCITITOR-CORE-02-001 Context signal schema prep|Team Excititor Core & Policy|EXCITITOR-POLICY-02-001|TODO Extend `VexClaim`/`VexConsensus` with optional severity/KEV/EPSS payloads, update canonical serializer/hashes, and coordinate migration notes with Storage.|
|EXCITITOR-CORE-02-001 Context signal schema prep|Team Excititor Core & Policy|EXCITITOR-POLICY-02-001|DONE (2025-10-19) Added `VexSignalSnapshot` (severity/KEV/EPSS) to claims/consensus, updated canonical serializer + resolver plumbing, documented storage follow-up, and validated via `dotnet test src/StellaOps.Excititor.Core.Tests/StellaOps.Excititor.Core.Tests.csproj`.|
|EXCITITOR-CORE-02-002 Deterministic risk scoring engine|Team Excititor Core & Policy|EXCITITOR-CORE-02-001, EXCITITOR-POLICY-02-001|BACKLOG Introduce the scoring calculator invoked by consensus, persist score envelopes with audit trails, and add regression fixtures covering gate/boost behaviour before enabling exports.|

View File

@@ -63,6 +63,7 @@ public static class VexCanonicalJsonSerializer
"status",
"justification",
"detail",
"signals",
"document",
"firstSeen",
"lastSeen",
@@ -124,6 +125,7 @@ public static class VexCanonicalJsonSerializer
"calculatedAt",
"sources",
"conflicts",
"signals",
"policyVersion",
"summary",
}
@@ -195,6 +197,25 @@ public static class VexCanonicalJsonSerializer
"diagnostics",
}
},
{
typeof(VexSignalSnapshot),
new[]
{
"severity",
"kev",
"epss",
}
},
{
typeof(VexSeveritySignal),
new[]
{
"scheme",
"score",
"label",
"vector",
}
},
{
typeof(VexExportManifest),
new[]
@@ -208,10 +229,39 @@ public static class VexCanonicalJsonSerializer
"fromCache",
"sourceProviders",
"consensusRevision",
"policyRevisionId",
"policyDigest",
"consensusDigest",
"scoreDigest",
"attestation",
"sizeBytes",
}
},
{
typeof(VexScoreEnvelope),
new[]
{
"generatedAt",
"policyRevisionId",
"policyDigest",
"alpha",
"beta",
"weightCeiling",
"entries",
}
},
{
typeof(VexScoreEntry),
new[]
{
"vulnerabilityId",
"productKey",
"status",
"calculatedAt",
"signals",
"score",
}
},
{
typeof(VexContentAddress),
new[]

View File

@@ -16,6 +16,7 @@ public sealed record VexClaim
VexJustification? justification = null,
string? detail = null,
VexConfidence? confidence = null,
VexSignalSnapshot? signals = null,
ImmutableDictionary<string, string>? additionalMetadata = null)
{
if (string.IsNullOrWhiteSpace(vulnerabilityId))
@@ -43,6 +44,7 @@ public sealed record VexClaim
Justification = justification;
Detail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim();
Confidence = confidence;
Signals = signals;
AdditionalMetadata = NormalizeMetadata(additionalMetadata);
}
@@ -66,6 +68,8 @@ public sealed record VexClaim
public VexConfidence? Confidence { get; }
public VexSignalSnapshot? Signals { get; }
public ImmutableSortedDictionary<string, string> AdditionalMetadata { get; }
private static ImmutableSortedDictionary<string, string> NormalizeMetadata(

View File

@@ -12,6 +12,7 @@ public sealed record VexConsensus
DateTimeOffset calculatedAt,
IEnumerable<VexConsensusSource> sources,
IEnumerable<VexConsensusConflict>? conflicts = null,
VexSignalSnapshot? signals = null,
string? policyVersion = null,
string? summary = null,
string? policyRevisionId = null,
@@ -28,6 +29,7 @@ public sealed record VexConsensus
CalculatedAt = calculatedAt;
Sources = NormalizeSources(sources);
Conflicts = NormalizeConflicts(conflicts);
Signals = signals;
PolicyVersion = string.IsNullOrWhiteSpace(policyVersion) ? null : policyVersion.Trim();
Summary = string.IsNullOrWhiteSpace(summary) ? null : summary.Trim();
PolicyRevisionId = string.IsNullOrWhiteSpace(policyRevisionId) ? null : policyRevisionId.Trim();
@@ -46,6 +48,8 @@ public sealed record VexConsensus
public ImmutableArray<VexConsensusConflict> Conflicts { get; }
public VexSignalSnapshot? Signals { get; }
public string? PolicyVersion { get; }
public string? Summary { get; }

View File

@@ -6,6 +6,12 @@ public sealed record VexConsensusPolicyOptions
{
public const string BaselineVersion = "baseline/v1";
public const double DefaultWeightCeiling = 1.0;
public const double DefaultAlpha = 0.25;
public const double DefaultBeta = 0.5;
public const double MaxSupportedCeiling = 5.0;
public const double MaxSupportedCoefficient = 5.0;
public VexConsensusPolicyOptions(
string? version = null,
double vendorWeight = 1.0,
@@ -13,15 +19,21 @@ public sealed record VexConsensusPolicyOptions
double platformWeight = 0.7,
double hubWeight = 0.5,
double attestationWeight = 0.6,
IEnumerable<KeyValuePair<string, double>>? providerOverrides = null)
IEnumerable<KeyValuePair<string, double>>? providerOverrides = null,
double weightCeiling = DefaultWeightCeiling,
double alpha = DefaultAlpha,
double beta = DefaultBeta)
{
Version = string.IsNullOrWhiteSpace(version) ? BaselineVersion : version.Trim();
VendorWeight = NormalizeWeight(vendorWeight);
DistroWeight = NormalizeWeight(distroWeight);
PlatformWeight = NormalizeWeight(platformWeight);
HubWeight = NormalizeWeight(hubWeight);
AttestationWeight = NormalizeWeight(attestationWeight);
ProviderOverrides = NormalizeOverrides(providerOverrides);
WeightCeiling = NormalizeWeightCeiling(weightCeiling);
VendorWeight = NormalizeWeight(vendorWeight, WeightCeiling);
DistroWeight = NormalizeWeight(distroWeight, WeightCeiling);
PlatformWeight = NormalizeWeight(platformWeight, WeightCeiling);
HubWeight = NormalizeWeight(hubWeight, WeightCeiling);
AttestationWeight = NormalizeWeight(attestationWeight, WeightCeiling);
ProviderOverrides = NormalizeOverrides(providerOverrides, WeightCeiling);
Alpha = NormalizeCoefficient(alpha, nameof(alpha));
Beta = NormalizeCoefficient(beta, nameof(beta));
}
public string Version { get; }
@@ -36,9 +48,15 @@ public sealed record VexConsensusPolicyOptions
public double AttestationWeight { get; }
public double WeightCeiling { get; }
public double Alpha { get; }
public double Beta { get; }
public ImmutableDictionary<string, double> ProviderOverrides { get; }
private static double NormalizeWeight(double weight)
private static double NormalizeWeight(double weight, double ceiling)
{
if (double.IsNaN(weight) || double.IsInfinity(weight))
{
@@ -50,16 +68,17 @@ public sealed record VexConsensusPolicyOptions
return 0;
}
if (weight >= 1)
if (weight >= ceiling)
{
return 1;
return ceiling;
}
return weight;
}
private static ImmutableDictionary<string, double> NormalizeOverrides(
IEnumerable<KeyValuePair<string, double>>? overrides)
IEnumerable<KeyValuePair<string, double>>? overrides,
double ceiling)
{
if (overrides is null)
{
@@ -74,9 +93,54 @@ public sealed record VexConsensusPolicyOptions
continue;
}
builder[key.Trim()] = NormalizeWeight(weight);
builder[key.Trim()] = NormalizeWeight(weight, ceiling);
}
return builder.ToImmutable();
}
private static double NormalizeWeightCeiling(double value)
{
if (double.IsNaN(value) || double.IsInfinity(value))
{
throw new ArgumentOutOfRangeException(nameof(value), "Weight ceiling must be a finite number.");
}
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "Weight ceiling must be greater than zero.");
}
if (value < 1)
{
return 1;
}
if (value > MaxSupportedCeiling)
{
return MaxSupportedCeiling;
}
return value;
}
private static double NormalizeCoefficient(double value, string name)
{
if (double.IsNaN(value) || double.IsInfinity(value))
{
throw new ArgumentOutOfRangeException(name, "Coefficient must be a finite number.");
}
if (value < 0)
{
throw new ArgumentOutOfRangeException(name, "Coefficient must be non-negative.");
}
if (value > MaxSupportedCoefficient)
{
return MaxSupportedCoefficient;
}
return value;
}
}

View File

@@ -39,18 +39,21 @@ public sealed class VexConsensusResolver
double weight = 0;
var included = false;
if (provider is null)
{
rejectionReason = "provider_not_registered";
}
else
{
weight = NormalizeWeight(_policy.GetProviderWeight(provider));
if (weight <= 0)
if (provider is null)
{
rejectionReason = "weight_not_positive";
rejectionReason = "provider_not_registered";
}
else if (!_policy.IsClaimEligible(claim, provider, out rejectionReason))
else
{
var ceiling = request.WeightCeiling <= 0 || double.IsNaN(request.WeightCeiling) || double.IsInfinity(request.WeightCeiling)
? VexConsensusPolicyOptions.DefaultWeightCeiling
: Math.Clamp(request.WeightCeiling, 0.1, VexConsensusPolicyOptions.MaxSupportedCeiling);
weight = NormalizeWeight(_policy.GetProviderWeight(provider), ceiling);
if (weight <= 0)
{
rejectionReason = "weight_not_positive";
}
else if (!_policy.IsClaimEligible(claim, provider, out rejectionReason))
{
rejectionReason ??= "rejected_by_policy";
}
@@ -105,6 +108,7 @@ public sealed class VexConsensusResolver
request.CalculatedAt,
acceptedSources,
AttachConflictDetails(conflicts, acceptedSources, consensusStatus, conflictKeys),
request.Signals,
_policy.Version,
summary,
request.PolicyRevisionId,
@@ -130,16 +134,16 @@ public sealed class VexConsensusResolver
return accumulator;
}
private static double NormalizeWeight(double weight)
private static double NormalizeWeight(double weight, double ceiling)
{
if (double.IsNaN(weight) || double.IsInfinity(weight) || weight <= 0)
{
return 0;
}
if (weight >= 1)
if (weight >= ceiling)
{
return 1;
return ceiling;
}
return weight;
@@ -275,6 +279,8 @@ public sealed record VexConsensusRequest(
IReadOnlyList<VexClaim> Claims,
IReadOnlyDictionary<string, VexProvider> Providers,
DateTimeOffset CalculatedAt,
double WeightCeiling = VexConsensusPolicyOptions.DefaultWeightCeiling,
VexSignalSnapshot? Signals = null,
string? PolicyRevisionId = null,
string? PolicyDigest = null);

View File

@@ -16,6 +16,10 @@ public sealed record VexExportManifest
IEnumerable<string> sourceProviders,
bool fromCache = false,
string? consensusRevision = null,
string? policyRevisionId = null,
string? policyDigest = null,
VexContentAddress? consensusDigest = null,
VexContentAddress? scoreDigest = null,
VexAttestationMetadata? attestation = null,
long sizeBytes = 0)
{
@@ -43,6 +47,10 @@ public sealed record VexExportManifest
FromCache = fromCache;
SourceProviders = NormalizeProviders(sourceProviders);
ConsensusRevision = string.IsNullOrWhiteSpace(consensusRevision) ? null : consensusRevision.Trim();
PolicyRevisionId = string.IsNullOrWhiteSpace(policyRevisionId) ? null : policyRevisionId.Trim();
PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
ConsensusDigest = consensusDigest;
ScoreDigest = scoreDigest;
Attestation = attestation;
SizeBytes = sizeBytes;
}
@@ -65,6 +73,14 @@ public sealed record VexExportManifest
public string? ConsensusRevision { get; }
public string? PolicyRevisionId { get; }
public string? PolicyDigest { get; }
public VexContentAddress? ConsensusDigest { get; }
public VexContentAddress? ScoreDigest { get; }
public VexAttestationMetadata? Attestation { get; }
public long SizeBytes { get; }

View File

@@ -0,0 +1,187 @@
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Excititor.Core;
public sealed record VexScoreEnvelope(
DateTimeOffset GeneratedAt,
string PolicyRevisionId,
string? PolicyDigest,
double Alpha,
double Beta,
double WeightCeiling,
ImmutableArray<VexScoreEntry> Entries)
{
public VexScoreEnvelope(
DateTimeOffset GeneratedAt,
string PolicyRevisionId,
string? PolicyDigest,
double Alpha,
double Beta,
double WeightCeiling,
IEnumerable<VexScoreEntry> Entries)
: this(
GeneratedAt,
PolicyRevisionId,
PolicyDigest,
Alpha,
Beta,
WeightCeiling,
NormalizeEntries(Entries))
{
}
private VexScoreEnvelope(
DateTimeOffset generatedAt,
string policyRevisionId,
string? policyDigest,
double alpha,
double beta,
double weightCeiling,
ImmutableArray<VexScoreEntry> entries)
{
if (string.IsNullOrWhiteSpace(policyRevisionId))
{
throw new ArgumentException("Policy revision id must be provided.", nameof(policyRevisionId));
}
if (double.IsNaN(alpha) || double.IsInfinity(alpha) || alpha < 0)
{
throw new ArgumentOutOfRangeException(nameof(alpha), "Alpha must be a finite, non-negative number.");
}
if (double.IsNaN(beta) || double.IsInfinity(beta) || beta < 0)
{
throw new ArgumentOutOfRangeException(nameof(beta), "Beta must be a finite, non-negative number.");
}
if (double.IsNaN(weightCeiling) || double.IsInfinity(weightCeiling) || weightCeiling <= 0)
{
throw new ArgumentOutOfRangeException(nameof(weightCeiling), "Weight ceiling must be a finite number greater than zero.");
}
this.GeneratedAt = generatedAt;
this.PolicyRevisionId = policyRevisionId.Trim();
this.PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
this.Alpha = alpha;
this.Beta = beta;
this.WeightCeiling = weightCeiling;
this.Entries = entries;
}
public DateTimeOffset GeneratedAt { get; }
public string PolicyRevisionId { get; }
public string? PolicyDigest { get; }
public double Alpha { get; }
public double Beta { get; }
public double WeightCeiling { get; }
public ImmutableArray<VexScoreEntry> Entries { get; }
private static ImmutableArray<VexScoreEntry> NormalizeEntries(IEnumerable<VexScoreEntry> entries)
{
if (entries is null)
{
throw new ArgumentNullException(nameof(entries));
}
return entries
.OrderBy(static entry => entry.VulnerabilityId, StringComparer.Ordinal)
.ThenBy(static entry => entry.ProductKey, StringComparer.Ordinal)
.ToImmutableArray();
}
}
public sealed record VexScoreEntry(
string VulnerabilityId,
string ProductKey,
VexConsensusStatus Status,
DateTimeOffset CalculatedAt,
VexSignalSnapshot? Signals,
double? Score)
{
public VexScoreEntry(
string VulnerabilityId,
string ProductKey,
VexConsensusStatus Status,
DateTimeOffset CalculatedAt,
VexSignalSnapshot? Signals,
double? Score)
: this(
ValidateVulnerability(VulnerabilityId),
ValidateProduct(ProductKey),
Status,
CalculatedAt,
Signals,
ValidateScore(Score))
{
}
private VexScoreEntry(
string vulnerabilityId,
string productKey,
VexConsensusStatus status,
DateTimeOffset calculatedAt,
VexSignalSnapshot? signals,
double? score)
{
VulnerabilityId = vulnerabilityId;
ProductKey = productKey;
Status = status;
CalculatedAt = calculatedAt;
Signals = signals;
Score = score;
}
public string VulnerabilityId { get; }
public string ProductKey { get; }
public VexConsensusStatus Status { get; }
public DateTimeOffset CalculatedAt { get; }
public VexSignalSnapshot? Signals { get; }
public double? Score { get; }
private static string ValidateVulnerability(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Vulnerability id must be provided.", nameof(value));
}
return value.Trim();
}
private static string ValidateProduct(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Product key must be provided.", nameof(value));
}
return value.Trim();
}
private static double? ValidateScore(double? score)
{
if (score is null)
{
return null;
}
if (double.IsNaN(score.Value) || double.IsInfinity(score.Value) || score.Value < 0)
{
throw new ArgumentOutOfRangeException(nameof(score), "Score must be a finite, non-negative number.");
}
return score;
}
}

View File

@@ -0,0 +1,64 @@
namespace StellaOps.Excititor.Core;
public sealed record VexSignalSnapshot
{
public VexSignalSnapshot(
VexSeveritySignal? severity = null,
bool? kev = null,
double? epss = null)
{
if (epss is { } epssValue)
{
if (double.IsNaN(epssValue) || double.IsInfinity(epssValue) || epssValue < 0 || epssValue > 1)
{
throw new ArgumentOutOfRangeException(nameof(epss), "EPSS probability must be between 0 and 1.");
}
}
Severity = severity;
Kev = kev;
Epss = epss;
}
public VexSeveritySignal? Severity { get; }
public bool? Kev { get; }
public double? Epss { get; }
}
public sealed record VexSeveritySignal
{
public VexSeveritySignal(
string scheme,
double? score = null,
string? label = null,
string? vector = null)
{
if (string.IsNullOrWhiteSpace(scheme))
{
throw new ArgumentException("Severity scheme must be provided.", nameof(scheme));
}
if (score is { } scoreValue)
{
if (double.IsNaN(scoreValue) || double.IsInfinity(scoreValue) || scoreValue < 0)
{
throw new ArgumentOutOfRangeException(nameof(score), "Severity score must be a finite, non-negative number.");
}
}
Scheme = scheme.Trim();
Score = score;
Label = string.IsNullOrWhiteSpace(label) ? null : label.Trim();
Vector = string.IsNullOrWhiteSpace(vector) ? null : vector.Trim();
}
public string Scheme { get; }
public double? Score { get; }
public string? Label { get; }
public string? Vector { get; }
}

View File

@@ -0,0 +1,17 @@
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Excititor.Core;
/// <summary>
/// Signature verifier implementation that trusts ingress sources without performing verification.
/// Useful for offline development flows and ingestion pipelines that perform verification upstream.
/// </summary>
public sealed class NoopVexSignatureVerifier : IVexSignatureVerifier
{
public ValueTask<VexSignatureMetadata?> VerifyAsync(VexRawDocument document, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(document);
return ValueTask.FromResult<VexSignatureMetadata?>(null);
}
}