new advisories work and features gaps work
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) 2025 StellaOps
|
||||
// Sprint: SPRINT_20260112_004_LB_attested_reduction_scoring (EWS-ATT-001)
|
||||
// Description: Anchor metadata for attested evidence inputs
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
/// <summary>
|
||||
/// Anchor metadata for cryptographically attested evidence.
|
||||
/// Provides provenance information for VEX, patch proof, reachability, and telemetry inputs.
|
||||
/// </summary>
|
||||
public sealed record AnchorMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this evidence is cryptographically anchored (has valid attestation).
|
||||
/// </summary>
|
||||
public required bool IsAnchored { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope digest (SHA-256) if evidence is signed.
|
||||
/// Format: "sha256:<hex>"
|
||||
/// </summary>
|
||||
public string? DsseEnvelopeDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Predicate type from the attestation (e.g., "https://stellaops.io/attestation/vex-override/v1").
|
||||
/// </summary>
|
||||
public string? PredicateType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor transparency log index (if recorded).
|
||||
/// </summary>
|
||||
public long? RekorLogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor entry UUID (if recorded).
|
||||
/// </summary>
|
||||
public string? RekorEntryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// RFC 3161 timestamp token digest (if timestamped).
|
||||
/// </summary>
|
||||
public string? TimestampTokenDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Key ID used for signing (if known).
|
||||
/// </summary>
|
||||
public string? SigningKeyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the attestation was created (UTC).
|
||||
/// </summary>
|
||||
public DateTimeOffset? AttestationTimestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Attestation verification status.
|
||||
/// </summary>
|
||||
public AnchorVerificationStatus VerificationStatus { get; init; } = AnchorVerificationStatus.Unverified;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an unanchored metadata instance.
|
||||
/// </summary>
|
||||
public static AnchorMetadata Unanchored => new() { IsAnchored = false };
|
||||
|
||||
/// <summary>
|
||||
/// Creates an anchored metadata instance with basic info.
|
||||
/// </summary>
|
||||
public static AnchorMetadata CreateAnchored(
|
||||
string dsseDigest,
|
||||
string predicateType,
|
||||
long? rekorLogIndex = null,
|
||||
string? rekorEntryId = null) => new()
|
||||
{
|
||||
IsAnchored = true,
|
||||
DsseEnvelopeDigest = dsseDigest,
|
||||
PredicateType = predicateType,
|
||||
RekorLogIndex = rekorLogIndex,
|
||||
RekorEntryId = rekorEntryId,
|
||||
VerificationStatus = AnchorVerificationStatus.Verified
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verification status for anchor metadata.
|
||||
/// </summary>
|
||||
public enum AnchorVerificationStatus
|
||||
{
|
||||
/// <summary>Anchor has not been verified.</summary>
|
||||
Unverified = 0,
|
||||
|
||||
/// <summary>Anchor signature and/or inclusion proof verified successfully.</summary>
|
||||
Verified = 1,
|
||||
|
||||
/// <summary>Anchor verification failed (invalid signature, missing proof, etc.).</summary>
|
||||
Failed = 2,
|
||||
|
||||
/// <summary>Anchor verification was skipped (offline mode, policy decision, etc.).</summary>
|
||||
Skipped = 3
|
||||
}
|
||||
@@ -80,6 +80,12 @@ public sealed record BackportInput
|
||||
/// <summary>Distribution/vendor that issued the backport.</summary>
|
||||
public string? Distributor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Anchor metadata for cryptographically attested backport/patch proof.
|
||||
/// Used by attested-reduction scoring profile to determine precedence.
|
||||
/// </summary>
|
||||
public AnchorMetadata? Anchor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the backport input.
|
||||
/// </summary>
|
||||
|
||||
@@ -166,6 +166,83 @@ public sealed record SpeculativeCapConfig
|
||||
public static SpeculativeCapConfig Default => new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attested-reduction scoring configuration.
|
||||
/// Sprint: SPRINT_20260112_004_LB_attested_reduction_scoring (EWS-ATT-002)
|
||||
/// </summary>
|
||||
public sealed record AttestedReductionConfig
|
||||
{
|
||||
/// <summary>Whether attested-reduction scoring is enabled.</summary>
|
||||
public bool Enabled { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Precedence list for anchored evidence types (higher index = higher priority).
|
||||
/// Default order: VEX not_affected/fixed > anchored backport > anchored reachability.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> PrecedenceList { get; init; } = [
|
||||
"vex.not_affected",
|
||||
"vex.fixed",
|
||||
"backport.signed_proof",
|
||||
"backport.vendor_vex",
|
||||
"reachability.not_reachable",
|
||||
"runtime.not_observed"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Reachability bonus (R) for EPSS reduction formula.
|
||||
/// Applied when anchored reachability evidence shows not-reachable.
|
||||
/// </summary>
|
||||
public double ReachabilityBonus { get; init; } = 0.3;
|
||||
|
||||
/// <summary>
|
||||
/// Telemetry bonus (T) for EPSS reduction formula.
|
||||
/// Applied when anchored runtime evidence shows no observation.
|
||||
/// </summary>
|
||||
public double TelemetryBonus { get; init; } = 0.2;
|
||||
|
||||
/// <summary>
|
||||
/// Patch proof reduction (P) for EPSS reduction formula.
|
||||
/// Applied when anchored backport evidence confirms patch.
|
||||
/// </summary>
|
||||
public double PatchProofReduction { get; init; } = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum score for clamp operation.
|
||||
/// </summary>
|
||||
public double ClampMin { get; init; } = 0.0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum score for clamp operation.
|
||||
/// </summary>
|
||||
public double ClampMax { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Hard-fail when anchored affected + runtime telemetry confirms active use.
|
||||
/// </summary>
|
||||
public bool HardFailOnAffectedWithRuntime { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Hard-fail score (typically 1.0 = maximum severity).
|
||||
/// </summary>
|
||||
public double HardFailScore { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Skip EPSS (XPL) dimension when stronger anchored evidence exists.
|
||||
/// </summary>
|
||||
public bool SkipEpssWhenAnchored { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum anchor verification status required for precedence.
|
||||
/// </summary>
|
||||
public AnchorVerificationStatus RequiredVerificationStatus { get; init; } = AnchorVerificationStatus.Verified;
|
||||
|
||||
/// <summary>Default configuration (disabled).</summary>
|
||||
public static AttestedReductionConfig Default => new();
|
||||
|
||||
/// <summary>Enabled configuration with default values.</summary>
|
||||
public static AttestedReductionConfig EnabledDefault => new() { Enabled = true };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Score bucket threshold configuration.
|
||||
/// </summary>
|
||||
@@ -204,6 +281,9 @@ public sealed record EvidenceWeightPolicy
|
||||
/// <summary>Bucket thresholds.</summary>
|
||||
public BucketThresholds Buckets { get; init; } = BucketThresholds.Default;
|
||||
|
||||
/// <summary>Attested-reduction scoring configuration.</summary>
|
||||
public AttestedReductionConfig AttestedReduction { get; init; } = AttestedReductionConfig.Default;
|
||||
|
||||
/// <summary>Optional tenant ID for multi-tenant scenarios.</summary>
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
@@ -285,6 +365,19 @@ public sealed record EvidenceWeightPolicy
|
||||
act_now_min = Buckets.ActNowMin,
|
||||
schedule_next_min = Buckets.ScheduleNextMin,
|
||||
investigate_min = Buckets.InvestigateMin
|
||||
},
|
||||
attested_reduction = new
|
||||
{
|
||||
enabled = AttestedReduction.Enabled,
|
||||
precedence_list = AttestedReduction.PrecedenceList,
|
||||
reachability_bonus = AttestedReduction.ReachabilityBonus,
|
||||
telemetry_bonus = AttestedReduction.TelemetryBonus,
|
||||
patch_proof_reduction = AttestedReduction.PatchProofReduction,
|
||||
clamp_min = AttestedReduction.ClampMin,
|
||||
clamp_max = AttestedReduction.ClampMax,
|
||||
hard_fail_on_affected_with_runtime = AttestedReduction.HardFailOnAffectedWithRuntime,
|
||||
hard_fail_score = AttestedReduction.HardFailScore,
|
||||
skip_epss_when_anchored = AttestedReduction.SkipEpssWhenAnchored
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -161,6 +161,20 @@ public sealed class EvidenceWeightedScoreCalculator : IEvidenceWeightedScoreCalc
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
ArgumentNullException.ThrowIfNull(policy);
|
||||
|
||||
// Check if attested-reduction scoring is enabled
|
||||
if (policy.AttestedReduction.Enabled)
|
||||
{
|
||||
return CalculateAttestedReduction(input, policy);
|
||||
}
|
||||
|
||||
return CalculateStandard(input, policy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Standard EWS calculation path.
|
||||
/// </summary>
|
||||
private EvidenceWeightedScoreResult CalculateStandard(EvidenceWeightedScoreInput input, EvidenceWeightPolicy policy)
|
||||
{
|
||||
// Clamp input values to ensure they're in valid range
|
||||
var clampedInput = input.Clamp();
|
||||
var weights = policy.Weights;
|
||||
@@ -214,6 +228,188 @@ public sealed class EvidenceWeightedScoreCalculator : IEvidenceWeightedScoreCalc
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attested-reduction scoring path.
|
||||
/// Sprint: SPRINT_20260112_004_LB_attested_reduction_scoring (EWS-ATT-003)
|
||||
/// Formula: score = clamp(base_epss * (1 + R + T) - P, 0, 1)
|
||||
/// Short-circuits:
|
||||
/// - Anchored VEX not_affected/fixed -> score 0
|
||||
/// - Anchored affected + runtime telemetry -> hard fail (score 1.0)
|
||||
/// </summary>
|
||||
private EvidenceWeightedScoreResult CalculateAttestedReduction(EvidenceWeightedScoreInput input, EvidenceWeightPolicy policy)
|
||||
{
|
||||
var clampedInput = input.Clamp();
|
||||
var weights = policy.Weights;
|
||||
var config = policy.AttestedReduction;
|
||||
var flags = new List<string>();
|
||||
var explanations = new List<string>();
|
||||
|
||||
// Check for anchored VEX evidence
|
||||
var hasAnchoredVex = IsAnchoredWithStatus(input.VexAnchor, config.RequiredVerificationStatus);
|
||||
var hasAnchoredBackport = IsAnchoredWithStatus(input.BackportDetails?.Anchor, config.RequiredVerificationStatus);
|
||||
var hasAnchoredReachability = IsAnchoredWithStatus(input.ReachabilityDetails?.Anchor, config.RequiredVerificationStatus);
|
||||
var hasAnchoredRuntime = IsAnchoredWithStatus(input.RuntimeDetails?.Anchor, config.RequiredVerificationStatus);
|
||||
|
||||
// Short-circuit 1: Anchored VEX not_affected or fixed -> score 0
|
||||
if (hasAnchoredVex &&
|
||||
(string.Equals(input.VexStatus, "not_affected", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(input.VexStatus, "fixed", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
flags.Add("anchored-vex");
|
||||
flags.Add("vendor-na");
|
||||
explanations.Add($"Anchored VEX statement: {input.VexStatus} - score reduced to 0");
|
||||
|
||||
return CreateAttestedResult(input, policy, clampedInput, weights,
|
||||
score: 0,
|
||||
bucket: ScoreBucket.Watchlist,
|
||||
flags: flags,
|
||||
explanations: explanations,
|
||||
attestedReductionApplied: true,
|
||||
hardFailApplied: false);
|
||||
}
|
||||
|
||||
// Short-circuit 2: Anchored affected + runtime confirmed -> hard fail
|
||||
if (config.HardFailOnAffectedWithRuntime &&
|
||||
hasAnchoredVex &&
|
||||
string.Equals(input.VexStatus, "affected", StringComparison.OrdinalIgnoreCase) &&
|
||||
hasAnchoredRuntime &&
|
||||
input.RuntimeDetails?.DirectPathObserved == true)
|
||||
{
|
||||
flags.Add("anchored-vex");
|
||||
flags.Add("anchored-runtime");
|
||||
flags.Add("hard-fail");
|
||||
explanations.Add("Anchored VEX affected + runtime confirmed vulnerable path - hard fail");
|
||||
|
||||
var hardFailScore = (int)Math.Round(config.HardFailScore * 100);
|
||||
return CreateAttestedResult(input, policy, clampedInput, weights,
|
||||
score: hardFailScore,
|
||||
bucket: ScoreBucket.ActNow,
|
||||
flags: flags,
|
||||
explanations: explanations,
|
||||
attestedReductionApplied: true,
|
||||
hardFailApplied: true);
|
||||
}
|
||||
|
||||
// Calculate reduction formula: score = clamp(base_epss * (1 + R + T) - P, min, max)
|
||||
var baseEpss = clampedInput.Xpl;
|
||||
var reachabilityBonus = 0.0;
|
||||
var telemetryBonus = 0.0;
|
||||
var patchReduction = 0.0;
|
||||
|
||||
// Apply reachability bonus if anchored not-reachable
|
||||
if (hasAnchoredReachability &&
|
||||
input.ReachabilityDetails?.State == ReachabilityState.NotReachable)
|
||||
{
|
||||
reachabilityBonus = config.ReachabilityBonus;
|
||||
flags.Add("anchored-reachability");
|
||||
explanations.Add($"Anchored reachability: not reachable - R bonus {reachabilityBonus:P0}");
|
||||
}
|
||||
|
||||
// Apply telemetry bonus if anchored no-observation
|
||||
if (hasAnchoredRuntime &&
|
||||
(input.RuntimeDetails?.Posture == RuntimePosture.None ||
|
||||
input.RuntimeDetails?.ObservationCount == 0))
|
||||
{
|
||||
telemetryBonus = config.TelemetryBonus;
|
||||
flags.Add("anchored-runtime");
|
||||
explanations.Add($"Anchored runtime: no observations - T bonus {telemetryBonus:P0}");
|
||||
}
|
||||
|
||||
// Apply patch proof reduction if anchored backport
|
||||
if (hasAnchoredBackport &&
|
||||
(input.BackportDetails?.Status == BackportStatus.Fixed ||
|
||||
input.BackportDetails?.Status == BackportStatus.NotAffected))
|
||||
{
|
||||
patchReduction = config.PatchProofReduction;
|
||||
flags.Add("anchored-backport");
|
||||
explanations.Add($"Anchored backport: {input.BackportDetails.Status} - P reduction {patchReduction:P0}");
|
||||
}
|
||||
|
||||
// Apply EPSS-skip behavior
|
||||
var effectiveEpss = baseEpss;
|
||||
if (config.SkipEpssWhenAnchored && (hasAnchoredBackport || hasAnchoredReachability))
|
||||
{
|
||||
// Reduce EPSS influence when stronger anchored evidence exists
|
||||
effectiveEpss *= 0.5;
|
||||
flags.Add("epss-reduced");
|
||||
explanations.Add("EPSS influence reduced due to anchored evidence");
|
||||
}
|
||||
|
||||
// Compute final score using reduction formula
|
||||
var rawReduction = effectiveEpss * (1.0 + reachabilityBonus + telemetryBonus) - patchReduction;
|
||||
var clampedScore = Math.Clamp(rawReduction, config.ClampMin, config.ClampMax);
|
||||
var scaledScore = (int)Math.Round(clampedScore * 100);
|
||||
|
||||
// Apply standard guardrails on top
|
||||
var (finalScore, guardrails) = ApplyGuardrails(scaledScore, clampedInput, policy.Guardrails);
|
||||
|
||||
// Generate standard flags on top
|
||||
var standardFlags = GenerateFlags(clampedInput, guardrails);
|
||||
foreach (var flag in standardFlags)
|
||||
{
|
||||
if (!flags.Contains(flag))
|
||||
flags.Add(flag);
|
||||
}
|
||||
|
||||
// Determine bucket
|
||||
var bucket = GetBucket(finalScore, policy.Buckets);
|
||||
|
||||
return CreateAttestedResult(input, policy, clampedInput, weights,
|
||||
score: finalScore,
|
||||
bucket: bucket,
|
||||
flags: flags,
|
||||
explanations: explanations,
|
||||
attestedReductionApplied: true,
|
||||
hardFailApplied: false,
|
||||
guardrails: guardrails);
|
||||
}
|
||||
|
||||
private static bool IsAnchoredWithStatus(AnchorMetadata? anchor, AnchorVerificationStatus requiredStatus)
|
||||
{
|
||||
if (anchor is null || !anchor.IsAnchored)
|
||||
return false;
|
||||
|
||||
return anchor.VerificationStatus >= requiredStatus;
|
||||
}
|
||||
|
||||
private EvidenceWeightedScoreResult CreateAttestedResult(
|
||||
EvidenceWeightedScoreInput input,
|
||||
EvidenceWeightPolicy policy,
|
||||
EvidenceWeightedScoreInput clampedInput,
|
||||
EvidenceWeights weights,
|
||||
int score,
|
||||
ScoreBucket bucket,
|
||||
List<string> flags,
|
||||
List<string> explanations,
|
||||
bool attestedReductionApplied,
|
||||
bool hardFailApplied,
|
||||
AppliedGuardrails? guardrails = null)
|
||||
{
|
||||
var breakdown = CalculateBreakdown(clampedInput, weights);
|
||||
|
||||
if (attestedReductionApplied)
|
||||
flags.Add("attested-reduction");
|
||||
if (hardFailApplied)
|
||||
flags.Add("hard-fail");
|
||||
|
||||
return new EvidenceWeightedScoreResult
|
||||
{
|
||||
FindingId = input.FindingId,
|
||||
Score = score,
|
||||
Bucket = bucket,
|
||||
Inputs = new EvidenceInputValues(
|
||||
clampedInput.Rch, clampedInput.Rts, clampedInput.Bkp,
|
||||
clampedInput.Xpl, clampedInput.Src, clampedInput.Mit),
|
||||
Weights = weights,
|
||||
Breakdown = breakdown,
|
||||
Flags = flags,
|
||||
Explanations = explanations,
|
||||
Caps = guardrails ?? AppliedGuardrails.None(score),
|
||||
PolicyDigest = policy.ComputeDigest(),
|
||||
CalculatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
private static (int finalScore, AppliedGuardrails guardrails) ApplyGuardrails(
|
||||
int score,
|
||||
EvidenceWeightedScoreInput input,
|
||||
|
||||
@@ -33,6 +33,12 @@ public sealed record EvidenceWeightedScoreInput
|
||||
/// <summary>VEX status for backport guardrail evaluation (e.g., "not_affected", "affected", "fixed").</summary>
|
||||
public string? VexStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Anchor metadata for the primary VEX/advisory evidence.
|
||||
/// Used by attested-reduction scoring profile for precedence determination.
|
||||
/// </summary>
|
||||
public AnchorMetadata? VexAnchor { get; init; }
|
||||
|
||||
/// <summary>Detailed inputs for explanation generation (reachability).</summary>
|
||||
public ReachabilityInput? ReachabilityDetails { get; init; }
|
||||
|
||||
|
||||
@@ -59,6 +59,12 @@ public sealed record ReachabilityInput
|
||||
/// <summary>Evidence timestamp (UTC ISO-8601).</summary>
|
||||
public DateTimeOffset? EvidenceTimestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Anchor metadata for cryptographically attested reachability evidence.
|
||||
/// Used by attested-reduction scoring profile to determine precedence.
|
||||
/// </summary>
|
||||
public AnchorMetadata? Anchor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the reachability input.
|
||||
/// </summary>
|
||||
|
||||
@@ -56,6 +56,12 @@ public sealed record RuntimeInput
|
||||
/// <summary>Correlation ID linking to runtime evidence.</summary>
|
||||
public string? CorrelationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Anchor metadata for cryptographically attested runtime telemetry.
|
||||
/// Used by attested-reduction scoring profile to determine precedence.
|
||||
/// </summary>
|
||||
public AnchorMetadata? Anchor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the runtime input.
|
||||
/// </summary>
|
||||
|
||||
@@ -65,6 +65,12 @@ public sealed record SourceTrustInput
|
||||
/// <summary>Number of corroborating sources.</summary>
|
||||
public int CorroboratingSourceCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Anchor metadata for cryptographically attested VEX/advisory evidence.
|
||||
/// Used by attested-reduction scoring profile to determine precedence.
|
||||
/// </summary>
|
||||
public AnchorMetadata? Anchor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the source trust input.
|
||||
/// </summary>
|
||||
|
||||
330
src/Signals/StellaOps.Signals/Models/RuntimeUpdatedEvent.cs
Normal file
330
src/Signals/StellaOps.Signals/Models/RuntimeUpdatedEvent.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
// <copyright file="RuntimeUpdatedEvent.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_008_SIGNALS_runtime_telemetry_events (SIG-RUN-001)
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Signals.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted when runtime observations change for a CVE and product pair.
|
||||
/// Used to drive policy reanalysis of unknowns.
|
||||
/// </summary>
|
||||
public sealed record RuntimeUpdatedEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique event identifier (deterministic based on content).
|
||||
/// </summary>
|
||||
[JsonPropertyName("eventId")]
|
||||
public required string EventId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event type constant.
|
||||
/// </summary>
|
||||
[JsonPropertyName("eventType")]
|
||||
public string EventType { get; init; } = RuntimeEventTypes.Updated;
|
||||
|
||||
/// <summary>
|
||||
/// Event version for schema compatibility.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Tenant identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tenant")]
|
||||
public required string Tenant { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE identifier affected by this update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cveId")]
|
||||
public string? CveId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Product PURL affected by this update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("purl")]
|
||||
public string? Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject key (canonical identifier for this CVE+product pair).
|
||||
/// </summary>
|
||||
[JsonPropertyName("subjectKey")]
|
||||
public required string SubjectKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Callgraph ID associated with this update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("callgraphId")]
|
||||
public string? CallgraphId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 digest of the runtime evidence that triggered this update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidenceDigest")]
|
||||
public required string EvidenceDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of runtime update.
|
||||
/// </summary>
|
||||
[JsonPropertyName("updateType")]
|
||||
public required RuntimeUpdateType UpdateType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous reachability state (null for new observations).
|
||||
/// </summary>
|
||||
[JsonPropertyName("previousState")]
|
||||
public string? PreviousState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New reachability state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("newState")]
|
||||
public required string NewState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score for the new state (0.0-1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this update is from runtime observation (vs static analysis).
|
||||
/// </summary>
|
||||
[JsonPropertyName("fromRuntime")]
|
||||
public required bool FromRuntime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime observation method (e.g., "ebpf", "agent", "probe").
|
||||
/// </summary>
|
||||
[JsonPropertyName("runtimeMethod")]
|
||||
public string? RuntimeMethod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Node hashes observed at runtime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("observedNodeHashes")]
|
||||
public ImmutableArray<string> ObservedNodeHashes { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Path hash for the observed call path.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pathHash")]
|
||||
public string? PathHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this update should trigger policy reanalysis.
|
||||
/// </summary>
|
||||
[JsonPropertyName("triggerReanalysis")]
|
||||
public required bool TriggerReanalysis { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for reanalysis (if triggered).
|
||||
/// </summary>
|
||||
[JsonPropertyName("reanalysisReason")]
|
||||
public string? ReanalysisReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when this event occurred.
|
||||
/// </summary>
|
||||
[JsonPropertyName("occurredAtUtc")]
|
||||
public required DateTimeOffset OccurredAtUtc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Correlation ID for tracing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("traceId")]
|
||||
public string? TraceId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of runtime update.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum RuntimeUpdateType
|
||||
{
|
||||
/// <summary>New runtime observation.</summary>
|
||||
NewObservation,
|
||||
|
||||
/// <summary>State change from previous observation.</summary>
|
||||
StateChange,
|
||||
|
||||
/// <summary>Confidence increase from new evidence.</summary>
|
||||
ConfidenceIncrease,
|
||||
|
||||
/// <summary>New call path observed.</summary>
|
||||
NewCallPath,
|
||||
|
||||
/// <summary>Exploit telemetry detected.</summary>
|
||||
ExploitTelemetry
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Well-known runtime event types.
|
||||
/// </summary>
|
||||
public static class RuntimeEventTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Runtime observations updated for a subject.
|
||||
/// </summary>
|
||||
public const string Updated = "runtime.updated";
|
||||
|
||||
/// <summary>
|
||||
/// Versioned event type alias for routing.
|
||||
/// </summary>
|
||||
public const string UpdatedV1 = "runtime.updated@1";
|
||||
|
||||
/// <summary>
|
||||
/// New runtime observation ingested.
|
||||
/// </summary>
|
||||
public const string Ingested = "runtime.ingested";
|
||||
|
||||
/// <summary>
|
||||
/// Runtime fact confirmed by new evidence.
|
||||
/// </summary>
|
||||
public const string Confirmed = "runtime.confirmed";
|
||||
|
||||
/// <summary>
|
||||
/// Exploit behavior detected at runtime.
|
||||
/// </summary>
|
||||
public const string ExploitDetected = "runtime.exploit_detected";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating deterministic runtime updated events.
|
||||
/// </summary>
|
||||
public static class RuntimeUpdatedEventFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a runtime updated event with deterministic event ID.
|
||||
/// </summary>
|
||||
public static RuntimeUpdatedEvent Create(
|
||||
string tenant,
|
||||
string subjectKey,
|
||||
string evidenceDigest,
|
||||
RuntimeUpdateType updateType,
|
||||
string newState,
|
||||
double confidence,
|
||||
bool fromRuntime,
|
||||
DateTimeOffset occurredAtUtc,
|
||||
string? cveId = null,
|
||||
string? purl = null,
|
||||
string? callgraphId = null,
|
||||
string? previousState = null,
|
||||
string? runtimeMethod = null,
|
||||
IReadOnlyList<string>? observedNodeHashes = null,
|
||||
string? pathHash = null,
|
||||
string? traceId = null)
|
||||
{
|
||||
// Determine if reanalysis is needed
|
||||
var triggerReanalysis = ShouldTriggerReanalysis(updateType, previousState, newState, confidence, fromRuntime);
|
||||
var reanalysisReason = triggerReanalysis
|
||||
? DetermineReanalysisReason(updateType, previousState, newState, fromRuntime)
|
||||
: null;
|
||||
|
||||
var eventId = ComputeEventId(
|
||||
subjectKey,
|
||||
evidenceDigest,
|
||||
occurredAtUtc);
|
||||
|
||||
return new RuntimeUpdatedEvent
|
||||
{
|
||||
EventId = eventId,
|
||||
Tenant = tenant,
|
||||
CveId = cveId,
|
||||
Purl = purl,
|
||||
SubjectKey = subjectKey,
|
||||
CallgraphId = callgraphId,
|
||||
EvidenceDigest = evidenceDigest,
|
||||
UpdateType = updateType,
|
||||
PreviousState = previousState,
|
||||
NewState = newState,
|
||||
Confidence = confidence,
|
||||
FromRuntime = fromRuntime,
|
||||
RuntimeMethod = runtimeMethod,
|
||||
ObservedNodeHashes = observedNodeHashes?.ToImmutableArray() ?? [],
|
||||
PathHash = pathHash,
|
||||
TriggerReanalysis = triggerReanalysis,
|
||||
ReanalysisReason = reanalysisReason,
|
||||
OccurredAtUtc = occurredAtUtc,
|
||||
TraceId = traceId
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ShouldTriggerReanalysis(
|
||||
RuntimeUpdateType updateType,
|
||||
string? previousState,
|
||||
string newState,
|
||||
double confidence,
|
||||
bool fromRuntime)
|
||||
{
|
||||
// Always trigger for exploit telemetry
|
||||
if (updateType == RuntimeUpdateType.ExploitTelemetry)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Trigger for state changes
|
||||
if (previousState is not null && !string.Equals(previousState, newState, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Trigger for high-confidence runtime observations
|
||||
if (fromRuntime && confidence >= 0.9)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Trigger for new call paths
|
||||
if (updateType == RuntimeUpdateType.NewCallPath)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string DetermineReanalysisReason(
|
||||
RuntimeUpdateType updateType,
|
||||
string? previousState,
|
||||
string newState,
|
||||
bool fromRuntime)
|
||||
{
|
||||
if (updateType == RuntimeUpdateType.ExploitTelemetry)
|
||||
{
|
||||
return "exploit_telemetry_detected";
|
||||
}
|
||||
|
||||
if (previousState is not null && !string.Equals(previousState, newState, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"state_change_{previousState}_to_{newState}";
|
||||
}
|
||||
|
||||
if (fromRuntime)
|
||||
{
|
||||
return "high_confidence_runtime_observation";
|
||||
}
|
||||
|
||||
if (updateType == RuntimeUpdateType.NewCallPath)
|
||||
{
|
||||
return "new_call_path_observed";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private static string ComputeEventId(string subjectKey, string evidenceDigest, DateTimeOffset occurredAtUtc)
|
||||
{
|
||||
var input = $"{subjectKey}|{evidenceDigest}|{occurredAtUtc.ToString("O", CultureInfo.InvariantCulture)}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"runtime-evt-{Convert.ToHexStringLower(hash)[..16]}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user