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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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.|
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
187
src/StellaOps.Excititor.Core/VexScoreEnvelope.cs
Normal file
187
src/StellaOps.Excititor.Core/VexScoreEnvelope.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
64
src/StellaOps.Excititor.Core/VexSignals.cs
Normal file
64
src/StellaOps.Excititor.Core/VexSignals.cs
Normal 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; }
|
||||
}
|
||||
17
src/StellaOps.Excititor.Core/VexSignatureVerifiers.cs
Normal file
17
src/StellaOps.Excititor.Core/VexSignatureVerifiers.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user