Add Canonical JSON serialization library with tests and documentation

- Implemented CanonJson class for deterministic JSON serialization and hashing.
- Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters.
- Created project files for the Canonical JSON library and its tests, including necessary package references.
- Added README.md for library usage and API reference.
- Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
master
2025-12-19 15:35:00 +02:00
parent 43882078a4
commit 951a38d561
192 changed files with 27550 additions and 2611 deletions

View File

@@ -0,0 +1,296 @@
// -----------------------------------------------------------------------------
// AssumptionPenalties.cs
// Sprint: SPRINT_3850_0001_0001 (Competitive Gap Closure)
// Task: D-SCORE-002 - Assumption penalties in score calculation
// Description: Penalties applied when scoring relies on assumptions.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Scoring;
/// <summary>
/// Types of assumptions that incur scoring penalties.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AssumptionType
{
/// <summary>Assumed vulnerable code is reachable (no reachability analysis).</summary>
AssumedReachable,
/// <summary>Assumed VEX status from source without verification.</summary>
AssumedVexStatus,
/// <summary>Assumed SBOM completeness (no SBOM validation).</summary>
AssumedSbomComplete,
/// <summary>Assumed feed is current (stale feed data).</summary>
AssumedFeedCurrent,
/// <summary>Assumed default CVSS metrics (no specific vector).</summary>
AssumedDefaultCvss,
/// <summary>Assumed package version (ambiguous version).</summary>
AssumedPackageVersion,
/// <summary>Assumed deployment context (no runtime info).</summary>
AssumedDeploymentContext,
/// <summary>Assumed transitive dependency (unverified chain).</summary>
AssumedTransitiveDep,
/// <summary>Assumed no compensating controls.</summary>
AssumedNoControls,
/// <summary>Assumed exploit exists (no PoC verification).</summary>
AssumedExploitExists
}
/// <summary>
/// Configuration for assumption penalties.
/// </summary>
public sealed record AssumptionPenaltyConfig
{
/// <summary>
/// Default penalties by assumption type.
/// </summary>
[JsonPropertyName("penalties")]
public ImmutableDictionary<AssumptionType, double> Penalties { get; init; } =
DefaultPenalties;
/// <summary>
/// Whether to compound penalties (multiply) or add them.
/// </summary>
[JsonPropertyName("compoundPenalties")]
public bool CompoundPenalties { get; init; } = true;
/// <summary>
/// Maximum total penalty (floor for confidence).
/// </summary>
[JsonPropertyName("maxTotalPenalty")]
public double MaxTotalPenalty { get; init; } = 0.7;
/// <summary>
/// Minimum confidence score after penalties.
/// </summary>
[JsonPropertyName("minConfidence")]
public double MinConfidence { get; init; } = 0.1;
/// <summary>
/// Default assumption penalties.
/// </summary>
public static readonly ImmutableDictionary<AssumptionType, double> DefaultPenalties =
new Dictionary<AssumptionType, double>
{
[AssumptionType.AssumedReachable] = 0.15,
[AssumptionType.AssumedVexStatus] = 0.10,
[AssumptionType.AssumedSbomComplete] = 0.12,
[AssumptionType.AssumedFeedCurrent] = 0.08,
[AssumptionType.AssumedDefaultCvss] = 0.05,
[AssumptionType.AssumedPackageVersion] = 0.10,
[AssumptionType.AssumedDeploymentContext] = 0.07,
[AssumptionType.AssumedTransitiveDep] = 0.05,
[AssumptionType.AssumedNoControls] = 0.08,
[AssumptionType.AssumedExploitExists] = 0.06
}.ToImmutableDictionary();
}
/// <summary>
/// An assumption made during scoring.
/// </summary>
public sealed record ScoringAssumption
{
/// <summary>
/// Type of assumption.
/// </summary>
[JsonPropertyName("type")]
public required AssumptionType Type { get; init; }
/// <summary>
/// Human-readable description.
/// </summary>
[JsonPropertyName("description")]
public required string Description { get; init; }
/// <summary>
/// Penalty applied for this assumption.
/// </summary>
[JsonPropertyName("penalty")]
public required double Penalty { get; init; }
/// <summary>
/// What would remove this assumption.
/// </summary>
[JsonPropertyName("resolutionHint")]
public string? ResolutionHint { get; init; }
/// <summary>
/// Related finding or component ID.
/// </summary>
[JsonPropertyName("relatedId")]
public string? RelatedId { get; init; }
}
/// <summary>
/// Result of applying assumption penalties.
/// </summary>
public sealed record AssumptionPenaltyResult
{
/// <summary>
/// Original confidence score (before penalties).
/// </summary>
[JsonPropertyName("originalConfidence")]
public required double OriginalConfidence { get; init; }
/// <summary>
/// Adjusted confidence score (after penalties).
/// </summary>
[JsonPropertyName("adjustedConfidence")]
public required double AdjustedConfidence { get; init; }
/// <summary>
/// Total penalty applied.
/// </summary>
[JsonPropertyName("totalPenalty")]
public required double TotalPenalty { get; init; }
/// <summary>
/// Assumptions that contributed to the penalty.
/// </summary>
[JsonPropertyName("assumptions")]
public ImmutableArray<ScoringAssumption> Assumptions { get; init; } = [];
/// <summary>
/// Whether the penalty was capped.
/// </summary>
[JsonPropertyName("penaltyCapped")]
public bool PenaltyCapped { get; init; }
}
/// <summary>
/// Calculator for assumption-based penalties.
/// </summary>
public sealed class AssumptionPenaltyCalculator
{
private readonly AssumptionPenaltyConfig _config;
public AssumptionPenaltyCalculator(AssumptionPenaltyConfig? config = null)
{
_config = config ?? new AssumptionPenaltyConfig();
}
/// <summary>
/// Calculates the penalty result for a set of assumptions.
/// </summary>
public AssumptionPenaltyResult Calculate(
double originalConfidence,
IEnumerable<ScoringAssumption> assumptions)
{
var assumptionList = assumptions.ToImmutableArray();
if (assumptionList.Length == 0)
{
return new AssumptionPenaltyResult
{
OriginalConfidence = originalConfidence,
AdjustedConfidence = originalConfidence,
TotalPenalty = 0,
Assumptions = [],
PenaltyCapped = false
};
}
double adjustedConfidence;
double totalPenalty;
bool capped = false;
if (_config.CompoundPenalties)
{
// Compound: multiply (1 - penalty) factors
var factor = 1.0;
foreach (var assumption in assumptionList)
{
factor *= (1.0 - assumption.Penalty);
}
adjustedConfidence = originalConfidence * factor;
totalPenalty = 1.0 - factor;
}
else
{
// Additive: sum penalties
totalPenalty = assumptionList.Sum(a => a.Penalty);
if (totalPenalty > _config.MaxTotalPenalty)
{
totalPenalty = _config.MaxTotalPenalty;
capped = true;
}
adjustedConfidence = originalConfidence * (1.0 - totalPenalty);
}
// Apply minimum confidence floor
if (adjustedConfidence < _config.MinConfidence)
{
adjustedConfidence = _config.MinConfidence;
capped = true;
}
return new AssumptionPenaltyResult
{
OriginalConfidence = originalConfidence,
AdjustedConfidence = adjustedConfidence,
TotalPenalty = totalPenalty,
Assumptions = assumptionList,
PenaltyCapped = capped
};
}
/// <summary>
/// Creates a scoring assumption with default penalty.
/// </summary>
public ScoringAssumption CreateAssumption(
AssumptionType type,
string description,
string? relatedId = null)
{
var penalty = _config.Penalties.TryGetValue(type, out var p)
? p
: AssumptionPenaltyConfig.DefaultPenalties.GetValueOrDefault(type, 0.05);
return new ScoringAssumption
{
Type = type,
Description = description,
Penalty = penalty,
ResolutionHint = GetResolutionHint(type),
RelatedId = relatedId
};
}
private static string GetResolutionHint(AssumptionType type) => type switch
{
AssumptionType.AssumedReachable =>
"Run reachability analysis to determine actual code path",
AssumptionType.AssumedVexStatus =>
"Obtain signed VEX statement from vendor",
AssumptionType.AssumedSbomComplete =>
"Generate verified SBOM with attestation",
AssumptionType.AssumedFeedCurrent =>
"Update vulnerability feeds to latest version",
AssumptionType.AssumedDefaultCvss =>
"Obtain environment-specific CVSS vector",
AssumptionType.AssumedPackageVersion =>
"Verify exact package version from lockfile",
AssumptionType.AssumedDeploymentContext =>
"Provide runtime environment information",
AssumptionType.AssumedTransitiveDep =>
"Verify dependency chain with lockfile",
AssumptionType.AssumedNoControls =>
"Document compensating controls in policy",
AssumptionType.AssumedExploitExists =>
"Check exploit databases for PoC availability",
_ => "Provide additional context to remove assumption"
};
}

View File

@@ -0,0 +1,394 @@
// -----------------------------------------------------------------------------
// ScoreAttestationStatement.cs
// Sprint: SPRINT_3850_0001_0001 (Competitive Gap Closure)
// Task: D-SCORE-005 - DSSE-signed score attestation
// Description: DSSE predicate for attesting to security scores.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using StellaOps.Attestor.ProofChain.Statements;
namespace StellaOps.Policy.Scoring;
/// <summary>
/// DSSE predicate type for score attestation.
/// </summary>
public static class ScoreAttestationPredicateType
{
/// <summary>
/// Predicate type URI for score attestation.
/// </summary>
public const string PredicateType = "https://stellaops.io/attestation/score/v1";
}
/// <summary>
/// Score attestation statement (DSSE predicate payload).
/// </summary>
public sealed record ScoreAttestationStatement
{
/// <summary>
/// Attestation version.
/// </summary>
[JsonPropertyName("version")]
public string Version { get; init; } = "1.0.0";
/// <summary>
/// When the score was computed.
/// </summary>
[JsonPropertyName("scoredAt")]
public required DateTimeOffset ScoredAt { get; init; }
/// <summary>
/// Subject artifact digest.
/// </summary>
[JsonPropertyName("subjectDigest")]
public required string SubjectDigest { get; init; }
/// <summary>
/// Subject artifact name/reference.
/// </summary>
[JsonPropertyName("subjectName")]
public string? SubjectName { get; init; }
/// <summary>
/// Overall security score (0-100).
/// </summary>
[JsonPropertyName("overallScore")]
public required int OverallScore { get; init; }
/// <summary>
/// Score confidence (0.0 to 1.0).
/// </summary>
[JsonPropertyName("confidence")]
public required double Confidence { get; init; }
/// <summary>
/// Score grade (A-F).
/// </summary>
[JsonPropertyName("grade")]
public required string Grade { get; init; }
/// <summary>
/// Score breakdown by category.
/// </summary>
[JsonPropertyName("breakdown")]
public required ScoreBreakdown Breakdown { get; init; }
/// <summary>
/// Scoring policy used.
/// </summary>
[JsonPropertyName("policy")]
public required ScoringPolicyRef Policy { get; init; }
/// <summary>
/// Inputs used for scoring.
/// </summary>
[JsonPropertyName("inputs")]
public required ScoringInputs Inputs { get; init; }
/// <summary>
/// Assumptions made during scoring.
/// </summary>
[JsonPropertyName("assumptions")]
public ImmutableArray<AssumptionSummary> Assumptions { get; init; } = [];
/// <summary>
/// Unknowns that affect the score.
/// </summary>
[JsonPropertyName("unknowns")]
public ImmutableArray<UnknownSummary> Unknowns { get; init; } = [];
/// <summary>
/// Hash of this statement for integrity.
/// </summary>
[JsonPropertyName("statementHash")]
public string? StatementHash { get; init; }
}
/// <summary>
/// Score breakdown by category.
/// </summary>
public sealed record ScoreBreakdown
{
/// <summary>
/// Vulnerability score (0-100).
/// </summary>
[JsonPropertyName("vulnerability")]
public required int Vulnerability { get; init; }
/// <summary>
/// Exploitability score (0-100).
/// </summary>
[JsonPropertyName("exploitability")]
public required int Exploitability { get; init; }
/// <summary>
/// Reachability score (0-100).
/// </summary>
[JsonPropertyName("reachability")]
public required int Reachability { get; init; }
/// <summary>
/// Policy compliance score (0-100).
/// </summary>
[JsonPropertyName("compliance")]
public required int Compliance { get; init; }
/// <summary>
/// Supply chain score (0-100).
/// </summary>
[JsonPropertyName("supplyChain")]
public required int SupplyChain { get; init; }
/// <summary>
/// VEX/mitigation score (0-100).
/// </summary>
[JsonPropertyName("mitigation")]
public required int Mitigation { get; init; }
}
/// <summary>
/// Reference to the scoring policy used.
/// </summary>
public sealed record ScoringPolicyRef
{
/// <summary>
/// Policy identifier.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// Policy version.
/// </summary>
[JsonPropertyName("version")]
public required string Version { get; init; }
/// <summary>
/// Policy digest.
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// Policy name.
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; init; }
}
/// <summary>
/// Inputs used for scoring.
/// </summary>
public sealed record ScoringInputs
{
/// <summary>
/// SBOM digest.
/// </summary>
[JsonPropertyName("sbomDigest")]
public string? SbomDigest { get; init; }
/// <summary>
/// Vulnerability feed version.
/// </summary>
[JsonPropertyName("feedVersion")]
public string? FeedVersion { get; init; }
/// <summary>
/// Feed fetch timestamp.
/// </summary>
[JsonPropertyName("feedFetchedAt")]
public DateTimeOffset? FeedFetchedAt { get; init; }
/// <summary>
/// Reachability analysis digest.
/// </summary>
[JsonPropertyName("reachabilityDigest")]
public string? ReachabilityDigest { get; init; }
/// <summary>
/// VEX documents used.
/// </summary>
[JsonPropertyName("vexDocuments")]
public ImmutableArray<VexDocRef> VexDocuments { get; init; } = [];
/// <summary>
/// Total components analyzed.
/// </summary>
[JsonPropertyName("componentCount")]
public int ComponentCount { get; init; }
/// <summary>
/// Total vulnerabilities found.
/// </summary>
[JsonPropertyName("vulnerabilityCount")]
public int VulnerabilityCount { get; init; }
/// <summary>
/// Total findings after filtering.
/// </summary>
[JsonPropertyName("findingCount")]
public int FindingCount { get; init; }
}
/// <summary>
/// Reference to a VEX document.
/// </summary>
public sealed record VexDocRef
{
/// <summary>
/// VEX document digest.
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// VEX source.
/// </summary>
[JsonPropertyName("source")]
public required string Source { get; init; }
/// <summary>
/// Decisions applied from this document.
/// </summary>
[JsonPropertyName("decisionCount")]
public int DecisionCount { get; init; }
}
/// <summary>
/// Summary of an assumption made.
/// </summary>
public sealed record AssumptionSummary
{
/// <summary>
/// Assumption type.
/// </summary>
[JsonPropertyName("type")]
public required string Type { get; init; }
/// <summary>
/// Count of this assumption type.
/// </summary>
[JsonPropertyName("count")]
public required int Count { get; init; }
/// <summary>
/// Total penalty from this assumption type.
/// </summary>
[JsonPropertyName("totalPenalty")]
public required double TotalPenalty { get; init; }
}
/// <summary>
/// Summary of an unknown.
/// </summary>
public sealed record UnknownSummary
{
/// <summary>
/// Unknown type.
/// </summary>
[JsonPropertyName("type")]
public required string Type { get; init; }
/// <summary>
/// Count of this unknown type.
/// </summary>
[JsonPropertyName("count")]
public required int Count { get; init; }
/// <summary>
/// Score impact from this unknown type.
/// </summary>
[JsonPropertyName("scoreImpact")]
public required int ScoreImpact { get; init; }
}
/// <summary>
/// Builder for score attestation statements.
/// </summary>
public sealed class ScoreAttestationBuilder
{
private readonly ScoreAttestationStatement _statement;
private ScoreAttestationBuilder(ScoreAttestationStatement statement)
{
_statement = statement;
}
/// <summary>
/// Creates a new builder.
/// </summary>
public static ScoreAttestationBuilder Create(
string subjectDigest,
int overallScore,
double confidence,
ScoreBreakdown breakdown,
ScoringPolicyRef policy,
ScoringInputs inputs)
{
return new ScoreAttestationBuilder(new ScoreAttestationStatement
{
ScoredAt = DateTimeOffset.UtcNow,
SubjectDigest = subjectDigest,
OverallScore = overallScore,
Confidence = confidence,
Grade = ComputeGrade(overallScore),
Breakdown = breakdown,
Policy = policy,
Inputs = inputs
});
}
/// <summary>
/// Sets the subject name.
/// </summary>
public ScoreAttestationBuilder WithSubjectName(string name)
{
return new ScoreAttestationBuilder(_statement with { SubjectName = name });
}
/// <summary>
/// Adds assumptions.
/// </summary>
public ScoreAttestationBuilder WithAssumptions(IEnumerable<AssumptionSummary> assumptions)
{
return new ScoreAttestationBuilder(_statement with
{
Assumptions = assumptions.ToImmutableArray()
});
}
/// <summary>
/// Adds unknowns.
/// </summary>
public ScoreAttestationBuilder WithUnknowns(IEnumerable<UnknownSummary> unknowns)
{
return new ScoreAttestationBuilder(_statement with
{
Unknowns = unknowns.ToImmutableArray()
});
}
/// <summary>
/// Builds the statement.
/// </summary>
public ScoreAttestationStatement Build()
{
// Compute statement hash
var canonical = StellaOps.Canonical.Json.CanonJson.Canonicalize(_statement);
var hash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(canonical);
return _statement with { StatementHash = hash };
}
private static string ComputeGrade(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
}

View File

@@ -0,0 +1,477 @@
// -----------------------------------------------------------------------------
// ScoringRulesSnapshot.cs
// Sprint: SPRINT_3850_0001_0001 (Competitive Gap Closure)
// Task: E-OFF-003 - Scoring rules snapshot with digest
// Description: Immutable snapshot of scoring rules for offline/audit use.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Scoring;
/// <summary>
/// Immutable snapshot of scoring rules with cryptographic digest.
/// Used for offline operation and audit trail.
/// </summary>
public sealed record ScoringRulesSnapshot
{
/// <summary>
/// Snapshot identifier.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// Snapshot version.
/// </summary>
[JsonPropertyName("version")]
public required int Version { get; init; }
/// <summary>
/// When the snapshot was created.
/// </summary>
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Content digest of the snapshot (sha256:...).
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// Description of this snapshot.
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; init; }
/// <summary>
/// Source policy IDs that contributed to this snapshot.
/// </summary>
[JsonPropertyName("sourcePolicies")]
public ImmutableArray<string> SourcePolicies { get; init; } = [];
/// <summary>
/// Scoring weights configuration.
/// </summary>
[JsonPropertyName("weights")]
public required ScoringWeights Weights { get; init; }
/// <summary>
/// Thresholds for grade boundaries.
/// </summary>
[JsonPropertyName("thresholds")]
public required GradeThresholds Thresholds { get; init; }
/// <summary>
/// Severity multipliers.
/// </summary>
[JsonPropertyName("severityMultipliers")]
public required SeverityMultipliers SeverityMultipliers { get; init; }
/// <summary>
/// Assumption penalty configuration.
/// </summary>
[JsonPropertyName("assumptionPenalties")]
public required AssumptionPenaltyConfig AssumptionPenalties { get; init; }
/// <summary>
/// Trust source weights.
/// </summary>
[JsonPropertyName("trustSourceWeights")]
public required TrustSourceWeightConfig TrustSourceWeights { get; init; }
/// <summary>
/// Freshness decay configuration.
/// </summary>
[JsonPropertyName("freshnessDecay")]
public required FreshnessDecayConfig FreshnessDecay { get; init; }
/// <summary>
/// Custom rules (Rego/SPL).
/// </summary>
[JsonPropertyName("customRules")]
public ImmutableArray<CustomScoringRule> CustomRules { get; init; } = [];
/// <summary>
/// Whether this snapshot is signed.
/// </summary>
[JsonPropertyName("isSigned")]
public bool IsSigned { get; init; }
/// <summary>
/// Signature if signed.
/// </summary>
[JsonPropertyName("signature")]
public string? Signature { get; init; }
/// <summary>
/// Key ID used for signing.
/// </summary>
[JsonPropertyName("signingKeyId")]
public string? SigningKeyId { get; init; }
}
/// <summary>
/// Scoring category weights (must sum to 1.0).
/// </summary>
public sealed record ScoringWeights
{
/// <summary>
/// Weight for vulnerability severity (0.0 to 1.0).
/// </summary>
[JsonPropertyName("vulnerability")]
public double Vulnerability { get; init; } = 0.25;
/// <summary>
/// Weight for exploitability factors (0.0 to 1.0).
/// </summary>
[JsonPropertyName("exploitability")]
public double Exploitability { get; init; } = 0.20;
/// <summary>
/// Weight for reachability analysis (0.0 to 1.0).
/// </summary>
[JsonPropertyName("reachability")]
public double Reachability { get; init; } = 0.20;
/// <summary>
/// Weight for policy compliance (0.0 to 1.0).
/// </summary>
[JsonPropertyName("compliance")]
public double Compliance { get; init; } = 0.15;
/// <summary>
/// Weight for supply chain factors (0.0 to 1.0).
/// </summary>
[JsonPropertyName("supplyChain")]
public double SupplyChain { get; init; } = 0.10;
/// <summary>
/// Weight for mitigation/VEX status (0.0 to 1.0).
/// </summary>
[JsonPropertyName("mitigation")]
public double Mitigation { get; init; } = 0.10;
/// <summary>
/// Validates that weights sum to 1.0.
/// </summary>
public bool Validate()
{
var sum = Vulnerability + Exploitability + Reachability +
Compliance + SupplyChain + Mitigation;
return Math.Abs(sum - 1.0) < 0.001;
}
}
/// <summary>
/// Grade threshold configuration.
/// </summary>
public sealed record GradeThresholds
{
/// <summary>
/// Minimum score for grade A.
/// </summary>
[JsonPropertyName("a")]
public int A { get; init; } = 90;
/// <summary>
/// Minimum score for grade B.
/// </summary>
[JsonPropertyName("b")]
public int B { get; init; } = 80;
/// <summary>
/// Minimum score for grade C.
/// </summary>
[JsonPropertyName("c")]
public int C { get; init; } = 70;
/// <summary>
/// Minimum score for grade D.
/// </summary>
[JsonPropertyName("d")]
public int D { get; init; } = 60;
// Below D threshold is grade F
/// <summary>
/// Gets the grade for a score.
/// </summary>
public string GetGrade(int score) => score switch
{
_ when score >= A => "A",
_ when score >= B => "B",
_ when score >= C => "C",
_ when score >= D => "D",
_ => "F"
};
}
/// <summary>
/// Severity multipliers for scoring.
/// </summary>
public sealed record SeverityMultipliers
{
/// <summary>
/// Multiplier for critical severity.
/// </summary>
[JsonPropertyName("critical")]
public double Critical { get; init; } = 1.5;
/// <summary>
/// Multiplier for high severity.
/// </summary>
[JsonPropertyName("high")]
public double High { get; init; } = 1.2;
/// <summary>
/// Multiplier for medium severity.
/// </summary>
[JsonPropertyName("medium")]
public double Medium { get; init; } = 1.0;
/// <summary>
/// Multiplier for low severity.
/// </summary>
[JsonPropertyName("low")]
public double Low { get; init; } = 0.8;
/// <summary>
/// Multiplier for informational.
/// </summary>
[JsonPropertyName("informational")]
public double Informational { get; init; } = 0.5;
/// <summary>
/// Gets multiplier for a severity string.
/// </summary>
public double GetMultiplier(string severity) => severity?.ToUpperInvariant() switch
{
"CRITICAL" => Critical,
"HIGH" => High,
"MEDIUM" => Medium,
"LOW" => Low,
"INFORMATIONAL" or "INFO" => Informational,
_ => Medium
};
}
/// <summary>
/// Freshness decay configuration.
/// </summary>
public sealed record FreshnessDecayConfig
{
/// <summary>
/// Hours after which SBOM starts to decay.
/// </summary>
[JsonPropertyName("sbomDecayStartHours")]
public int SbomDecayStartHours { get; init; } = 168; // 7 days
/// <summary>
/// Hours after which feeds start to decay.
/// </summary>
[JsonPropertyName("feedDecayStartHours")]
public int FeedDecayStartHours { get; init; } = 24;
/// <summary>
/// Decay rate per hour after start.
/// </summary>
[JsonPropertyName("decayRatePerHour")]
public double DecayRatePerHour { get; init; } = 0.001;
/// <summary>
/// Minimum freshness score.
/// </summary>
[JsonPropertyName("minimumFreshness")]
public double MinimumFreshness { get; init; } = 0.5;
}
/// <summary>
/// Custom scoring rule.
/// </summary>
public sealed record CustomScoringRule
{
/// <summary>
/// Rule identifier.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// Rule name.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Rule language (rego, spl).
/// </summary>
[JsonPropertyName("language")]
public required string Language { get; init; }
/// <summary>
/// Rule content.
/// </summary>
[JsonPropertyName("content")]
public required string Content { get; init; }
/// <summary>
/// Rule priority (higher = evaluated first).
/// </summary>
[JsonPropertyName("priority")]
public int Priority { get; init; }
/// <summary>
/// Whether rule is enabled.
/// </summary>
[JsonPropertyName("enabled")]
public bool Enabled { get; init; } = true;
}
/// <summary>
/// Builder for scoring rules snapshots.
/// </summary>
public sealed class ScoringRulesSnapshotBuilder
{
private ScoringRulesSnapshot _snapshot;
private ScoringRulesSnapshotBuilder(ScoringRulesSnapshot snapshot)
{
_snapshot = snapshot;
}
/// <summary>
/// Creates a new builder with defaults.
/// </summary>
public static ScoringRulesSnapshotBuilder Create(string id, int version)
{
return new ScoringRulesSnapshotBuilder(new ScoringRulesSnapshot
{
Id = id,
Version = version,
CreatedAt = DateTimeOffset.UtcNow,
Digest = "", // Will be computed on build
Weights = new ScoringWeights(),
Thresholds = new GradeThresholds(),
SeverityMultipliers = new SeverityMultipliers(),
AssumptionPenalties = new AssumptionPenaltyConfig(),
TrustSourceWeights = new TrustSourceWeightConfig(),
FreshnessDecay = new FreshnessDecayConfig()
});
}
public ScoringRulesSnapshotBuilder WithDescription(string description)
{
_snapshot = _snapshot with { Description = description };
return this;
}
public ScoringRulesSnapshotBuilder WithWeights(ScoringWeights weights)
{
_snapshot = _snapshot with { Weights = weights };
return this;
}
public ScoringRulesSnapshotBuilder WithThresholds(GradeThresholds thresholds)
{
_snapshot = _snapshot with { Thresholds = thresholds };
return this;
}
public ScoringRulesSnapshotBuilder WithSeverityMultipliers(SeverityMultipliers multipliers)
{
_snapshot = _snapshot with { SeverityMultipliers = multipliers };
return this;
}
public ScoringRulesSnapshotBuilder WithAssumptionPenalties(AssumptionPenaltyConfig penalties)
{
_snapshot = _snapshot with { AssumptionPenalties = penalties };
return this;
}
public ScoringRulesSnapshotBuilder WithTrustSourceWeights(TrustSourceWeightConfig weights)
{
_snapshot = _snapshot with { TrustSourceWeights = weights };
return this;
}
public ScoringRulesSnapshotBuilder WithFreshnessDecay(FreshnessDecayConfig decay)
{
_snapshot = _snapshot with { FreshnessDecay = decay };
return this;
}
public ScoringRulesSnapshotBuilder WithCustomRules(IEnumerable<CustomScoringRule> rules)
{
_snapshot = _snapshot with { CustomRules = rules.ToImmutableArray() };
return this;
}
public ScoringRulesSnapshotBuilder WithSourcePolicies(IEnumerable<string> policyIds)
{
_snapshot = _snapshot with { SourcePolicies = policyIds.ToImmutableArray() };
return this;
}
/// <summary>
/// Builds the snapshot with computed digest.
/// </summary>
public ScoringRulesSnapshot Build()
{
// Validate weights
if (!_snapshot.Weights.Validate())
{
throw new InvalidOperationException("Scoring weights must sum to 1.0");
}
// Compute digest
var canonical = StellaOps.Canonical.Json.CanonJson.Canonicalize(_snapshot with { Digest = "" });
var digest = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(canonical);
return _snapshot with { Digest = digest };
}
}
/// <summary>
/// Service for managing scoring rules snapshots.
/// </summary>
public interface IScoringRulesSnapshotService
{
/// <summary>
/// Creates a new snapshot from current rules.
/// </summary>
Task<ScoringRulesSnapshot> CreateSnapshotAsync(
string description,
CancellationToken ct = default);
/// <summary>
/// Gets a snapshot by ID.
/// </summary>
Task<ScoringRulesSnapshot?> GetSnapshotAsync(
string id,
CancellationToken ct = default);
/// <summary>
/// Gets the latest snapshot.
/// </summary>
Task<ScoringRulesSnapshot?> GetLatestSnapshotAsync(
CancellationToken ct = default);
/// <summary>
/// Validates a snapshot against its digest.
/// </summary>
Task<bool> ValidateSnapshotAsync(
ScoringRulesSnapshot snapshot,
CancellationToken ct = default);
/// <summary>
/// Lists all snapshots.
/// </summary>
Task<IReadOnlyList<ScoringRulesSnapshot>> ListSnapshotsAsync(
int limit = 100,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,412 @@
// -----------------------------------------------------------------------------
// TrustSourceWeights.cs
// Sprint: SPRINT_3850_0001_0001 (Competitive Gap Closure)
// Task: D-SCORE-003 - Configurable trust source weights
// Description: Configurable weights for different vulnerability data sources.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Scoring;
/// <summary>
/// Known vulnerability data sources.
/// </summary>
public static class KnownSources
{
public const string NvdNist = "nvd-nist";
public const string CisaKev = "cisa-kev";
public const string Osv = "osv";
public const string GithubAdvisory = "github-advisory";
public const string VendorAdvisory = "vendor";
public const string RedHatCve = "redhat-cve";
public const string DebianSecurity = "debian-security";
public const string AlpineSecdb = "alpine-secdb";
public const string UbuntuOval = "ubuntu-oval";
public const string Epss = "epss";
public const string ExploitDb = "exploit-db";
public const string VulnDb = "vulndb";
public const string Snyk = "snyk";
public const string Internal = "internal";
}
/// <summary>
/// Configuration for trust source weights.
/// </summary>
public sealed record TrustSourceWeightConfig
{
/// <summary>
/// Weights by source ID (0.0 to 1.0).
/// </summary>
[JsonPropertyName("weights")]
public ImmutableDictionary<string, double> Weights { get; init; } =
DefaultWeights;
/// <summary>
/// Default weight for unknown sources.
/// </summary>
[JsonPropertyName("defaultWeight")]
public double DefaultWeight { get; init; } = 0.5;
/// <summary>
/// Source categories and their base weights.
/// </summary>
[JsonPropertyName("categoryWeights")]
public ImmutableDictionary<SourceCategory, double> CategoryWeights { get; init; } =
DefaultCategoryWeights;
/// <summary>
/// Whether to boost sources with corroborating data.
/// </summary>
[JsonPropertyName("enableCorroborationBoost")]
public bool EnableCorroborationBoost { get; init; } = true;
/// <summary>
/// Boost multiplier when multiple sources agree.
/// </summary>
[JsonPropertyName("corroborationBoostFactor")]
public double CorroborationBoostFactor { get; init; } = 1.1;
/// <summary>
/// Maximum number of corroborating sources to count.
/// </summary>
[JsonPropertyName("maxCorroborationCount")]
public int MaxCorroborationCount { get; init; } = 3;
/// <summary>
/// Default source weights.
/// </summary>
public static readonly ImmutableDictionary<string, double> DefaultWeights =
new Dictionary<string, double>
{
[KnownSources.NvdNist] = 0.90,
[KnownSources.CisaKev] = 0.98,
[KnownSources.Osv] = 0.75,
[KnownSources.GithubAdvisory] = 0.72,
[KnownSources.VendorAdvisory] = 0.88,
[KnownSources.RedHatCve] = 0.85,
[KnownSources.DebianSecurity] = 0.82,
[KnownSources.AlpineSecdb] = 0.80,
[KnownSources.UbuntuOval] = 0.82,
[KnownSources.Epss] = 0.70,
[KnownSources.ExploitDb] = 0.65,
[KnownSources.VulnDb] = 0.68,
[KnownSources.Snyk] = 0.70,
[KnownSources.Internal] = 0.60
}.ToImmutableDictionary();
/// <summary>
/// Default category weights.
/// </summary>
public static readonly ImmutableDictionary<SourceCategory, double> DefaultCategoryWeights =
new Dictionary<SourceCategory, double>
{
[SourceCategory.Government] = 0.95,
[SourceCategory.Vendor] = 0.85,
[SourceCategory.Coordinator] = 0.80,
[SourceCategory.Distro] = 0.82,
[SourceCategory.Community] = 0.70,
[SourceCategory.Commercial] = 0.68,
[SourceCategory.Internal] = 0.60
}.ToImmutableDictionary();
}
/// <summary>
/// Source categories.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SourceCategory
{
/// <summary>Government agency (NIST, CISA, BSI).</summary>
Government,
/// <summary>Software vendor.</summary>
Vendor,
/// <summary>Vulnerability coordinator (CERT).</summary>
Coordinator,
/// <summary>Linux distribution security team.</summary>
Distro,
/// <summary>Open source community.</summary>
Community,
/// <summary>Commercial security vendor.</summary>
Commercial,
/// <summary>Internal organization sources.</summary>
Internal
}
/// <summary>
/// Metadata about a vulnerability source.
/// </summary>
public sealed record SourceMetadata
{
/// <summary>
/// Source identifier.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// Source category.
/// </summary>
[JsonPropertyName("category")]
public required SourceCategory Category { get; init; }
/// <summary>
/// When data was fetched from this source.
/// </summary>
[JsonPropertyName("fetchedAt")]
public DateTimeOffset? FetchedAt { get; init; }
/// <summary>
/// Source data version/timestamp.
/// </summary>
[JsonPropertyName("dataVersion")]
public string? DataVersion { get; init; }
/// <summary>
/// Whether data is signed.
/// </summary>
[JsonPropertyName("isSigned")]
public bool IsSigned { get; init; }
}
/// <summary>
/// Finding data from a source.
/// </summary>
public sealed record SourceFinding
{
/// <summary>
/// Source metadata.
/// </summary>
[JsonPropertyName("source")]
public required SourceMetadata Source { get; init; }
/// <summary>
/// Severity from this source.
/// </summary>
[JsonPropertyName("severity")]
public string? Severity { get; init; }
/// <summary>
/// CVSS score from this source.
/// </summary>
[JsonPropertyName("cvssScore")]
public double? CvssScore { get; init; }
/// <summary>
/// VEX status from this source.
/// </summary>
[JsonPropertyName("vexStatus")]
public string? VexStatus { get; init; }
/// <summary>
/// Whether this source confirms exploitability.
/// </summary>
[JsonPropertyName("confirmsExploit")]
public bool? ConfirmsExploit { get; init; }
/// <summary>
/// Fix version from this source.
/// </summary>
[JsonPropertyName("fixVersion")]
public string? FixVersion { get; init; }
}
/// <summary>
/// Result of merging findings from multiple sources.
/// </summary>
public sealed record WeightedMergeResult
{
/// <summary>
/// Merged severity (highest trust source).
/// </summary>
[JsonPropertyName("severity")]
public string? Severity { get; init; }
/// <summary>
/// Weighted average CVSS score.
/// </summary>
[JsonPropertyName("cvssScore")]
public double? CvssScore { get; init; }
/// <summary>
/// VEX status from highest trust source.
/// </summary>
[JsonPropertyName("vexStatus")]
public string? VexStatus { get; init; }
/// <summary>
/// Fix version (earliest reported).
/// </summary>
[JsonPropertyName("fixVersion")]
public string? FixVersion { get; init; }
/// <summary>
/// Overall confidence in the merged result.
/// </summary>
[JsonPropertyName("confidence")]
public required double Confidence { get; init; }
/// <summary>
/// Sources that contributed (ordered by weight).
/// </summary>
[JsonPropertyName("contributingSources")]
public ImmutableArray<string> ContributingSources { get; init; } = [];
/// <summary>
/// Whether sources corroborated each other.
/// </summary>
[JsonPropertyName("corroborated")]
public bool Corroborated { get; init; }
/// <summary>
/// Corroboration boost applied.
/// </summary>
[JsonPropertyName("corroborationBoost")]
public double CorroborationBoost { get; init; }
}
/// <summary>
/// Service for weighted source merging.
/// </summary>
public sealed class TrustSourceWeightService
{
private readonly TrustSourceWeightConfig _config;
public TrustSourceWeightService(TrustSourceWeightConfig? config = null)
{
_config = config ?? new TrustSourceWeightConfig();
}
/// <summary>
/// Gets the effective weight for a source.
/// </summary>
public double GetSourceWeight(SourceMetadata source)
{
// Check for explicit weight
if (_config.Weights.TryGetValue(source.Id, out var explicitWeight))
{
return ApplyModifiers(explicitWeight, source);
}
// Fall back to category weight
if (_config.CategoryWeights.TryGetValue(source.Category, out var categoryWeight))
{
return ApplyModifiers(categoryWeight, source);
}
return ApplyModifiers(_config.DefaultWeight, source);
}
private double ApplyModifiers(double baseWeight, SourceMetadata source)
{
var weight = baseWeight;
// Boost for signed data
if (source.IsSigned)
{
weight *= 1.05;
}
// Penalty for stale data (>7 days old)
if (source.FetchedAt.HasValue)
{
var age = DateTimeOffset.UtcNow - source.FetchedAt.Value;
if (age.TotalDays > 7)
{
weight *= 0.95;
}
if (age.TotalDays > 30)
{
weight *= 0.90;
}
}
return Math.Clamp(weight, 0.0, 1.0);
}
/// <summary>
/// Merges findings from multiple sources using weights.
/// </summary>
public WeightedMergeResult MergeFindings(IEnumerable<SourceFinding> findings)
{
var findingList = findings.ToList();
if (findingList.Count == 0)
{
return new WeightedMergeResult { Confidence = 0 };
}
// Sort by weight descending
var weighted = findingList
.Select(f => (Finding: f, Weight: GetSourceWeight(f.Source)))
.OrderByDescending(x => x.Weight)
.ToList();
var topFinding = weighted[0].Finding;
var topWeight = weighted[0].Weight;
// Calculate weighted CVSS
double? weightedCvss = null;
var cvssFindings = weighted.Where(w => w.Finding.CvssScore.HasValue).ToList();
if (cvssFindings.Count > 0)
{
var totalWeight = cvssFindings.Sum(w => w.Weight);
weightedCvss = cvssFindings.Sum(w => w.Finding.CvssScore!.Value * w.Weight) / totalWeight;
}
// Check for corroboration
var corroborated = false;
var corroborationBoost = 0.0;
if (_config.EnableCorroborationBoost && weighted.Count > 1)
{
// Check if multiple sources agree on severity
var severities = weighted
.Where(w => !string.IsNullOrEmpty(w.Finding.Severity))
.Select(w => w.Finding.Severity)
.Distinct()
.ToList();
if (severities.Count == 1)
{
var corroboratingCount = Math.Min(
weighted.Count(w => w.Finding.Severity == severities[0]),
_config.MaxCorroborationCount);
if (corroboratingCount > 1)
{
corroborated = true;
corroborationBoost = Math.Pow(
_config.CorroborationBoostFactor,
corroboratingCount - 1) - 1.0;
}
}
}
var confidence = Math.Clamp(topWeight + corroborationBoost, 0.0, 1.0);
return new WeightedMergeResult
{
Severity = topFinding.Severity,
CvssScore = weightedCvss,
VexStatus = topFinding.VexStatus,
FixVersion = findingList
.Where(f => !string.IsNullOrEmpty(f.FixVersion))
.OrderBy(f => f.FixVersion)
.FirstOrDefault()?.FixVersion,
Confidence = confidence,
ContributingSources = weighted.Select(w => w.Finding.Source.Id).ToImmutableArray(),
Corroborated = corroborated,
CorroborationBoost = corroborationBoost
};
}
}

View File

@@ -0,0 +1,429 @@
// -----------------------------------------------------------------------------
// JurisdictionTrustRules.cs
// Sprint: SPRINT_3850_0001_0001 (Competitive Gap Closure)
// Task: VEX-L-003 - Jurisdiction-specific trust rules (US/EU/RU/CN)
// Description: VEX source trust rules by regulatory jurisdiction.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Vex;
/// <summary>
/// Jurisdiction codes for regulatory regions.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Jurisdiction
{
/// <summary>United States (FDA, NIST, CISA).</summary>
US,
/// <summary>European Union (ENISA, BSI, ANSSI).</summary>
EU,
/// <summary>Russian Federation (FSTEC, FSB).</summary>
RU,
/// <summary>China (CNVD, CNNVD).</summary>
CN,
/// <summary>Japan (JPCERT, IPA).</summary>
JP,
/// <summary>Global (no specific jurisdiction).</summary>
Global
}
/// <summary>
/// VEX source identity.
/// </summary>
public sealed record VexSource
{
/// <summary>
/// Unique source identifier.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// Human-readable source name.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Source type (vendor, coordinator, government, community).
/// </summary>
[JsonPropertyName("type")]
public required VexSourceType Type { get; init; }
/// <summary>
/// Jurisdictions where this source is authoritative.
/// </summary>
[JsonPropertyName("jurisdictions")]
public ImmutableArray<Jurisdiction> Jurisdictions { get; init; } = [];
/// <summary>
/// Base trust weight (0.0 to 1.0).
/// </summary>
[JsonPropertyName("baseTrustWeight")]
public double BaseTrustWeight { get; init; } = 0.5;
/// <summary>
/// Whether this source is a government authority.
/// </summary>
[JsonPropertyName("isGovernmentAuthority")]
public bool IsGovernmentAuthority { get; init; }
/// <summary>
/// Signing key identifiers for this source.
/// </summary>
[JsonPropertyName("keyIds")]
public ImmutableArray<string> KeyIds { get; init; } = [];
}
/// <summary>
/// VEX source types.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum VexSourceType
{
/// <summary>Product vendor.</summary>
Vendor,
/// <summary>Vulnerability coordinator (CERT).</summary>
Coordinator,
/// <summary>Government authority.</summary>
Government,
/// <summary>Community/open source.</summary>
Community,
/// <summary>Commercial security vendor.</summary>
Commercial
}
/// <summary>
/// Jurisdiction-specific trust configuration.
/// </summary>
public sealed record JurisdictionTrustConfig
{
/// <summary>
/// Jurisdiction this config applies to.
/// </summary>
[JsonPropertyName("jurisdiction")]
public required Jurisdiction Jurisdiction { get; init; }
/// <summary>
/// Ordered list of preferred sources (highest priority first).
/// </summary>
[JsonPropertyName("preferredSources")]
public ImmutableArray<string> PreferredSources { get; init; } = [];
/// <summary>
/// Trust weight overrides for specific sources.
/// </summary>
[JsonPropertyName("trustWeightOverrides")]
public ImmutableDictionary<string, double> TrustWeightOverrides { get; init; } =
ImmutableDictionary<string, double>.Empty;
/// <summary>
/// Whether government sources must be preferred.
/// </summary>
[JsonPropertyName("preferGovernmentSources")]
public bool PreferGovernmentSources { get; init; }
/// <summary>
/// Minimum trust weight for acceptance.
/// </summary>
[JsonPropertyName("minimumTrustWeight")]
public double MinimumTrustWeight { get; init; } = 0.3;
/// <summary>
/// Required source types for VEX acceptance.
/// </summary>
[JsonPropertyName("requiredSourceTypes")]
public ImmutableArray<VexSourceType> RequiredSourceTypes { get; init; } = [];
}
/// <summary>
/// Service for jurisdiction-aware VEX trust evaluation.
/// </summary>
public interface IJurisdictionTrustService
{
/// <summary>
/// Gets the effective trust weight for a source in a jurisdiction.
/// </summary>
double GetEffectiveTrustWeight(VexSource source, Jurisdiction jurisdiction);
/// <summary>
/// Ranks sources by trust for a jurisdiction.
/// </summary>
IReadOnlyList<VexSource> RankSourcesByTrust(
IEnumerable<VexSource> sources,
Jurisdiction jurisdiction);
/// <summary>
/// Validates that a VEX decision meets jurisdiction requirements.
/// </summary>
JurisdictionValidationResult ValidateForJurisdiction(
VexDecisionContext decision,
Jurisdiction jurisdiction);
}
/// <summary>
/// Context for a VEX decision being validated.
/// </summary>
public sealed record VexDecisionContext
{
/// <summary>
/// VEX status.
/// </summary>
[JsonPropertyName("status")]
public required string Status { get; init; }
/// <summary>
/// Source that provided this decision.
/// </summary>
[JsonPropertyName("source")]
public required VexSource Source { get; init; }
/// <summary>
/// Justification provided.
/// </summary>
[JsonPropertyName("justification")]
public string? Justification { get; init; }
/// <summary>
/// When the decision was made.
/// </summary>
[JsonPropertyName("timestamp")]
public required DateTimeOffset Timestamp { get; init; }
/// <summary>
/// Whether the decision is cryptographically signed.
/// </summary>
[JsonPropertyName("isSigned")]
public bool IsSigned { get; init; }
}
/// <summary>
/// Result of jurisdiction validation.
/// </summary>
public sealed record JurisdictionValidationResult
{
/// <summary>
/// Whether the decision is valid for the jurisdiction.
/// </summary>
[JsonPropertyName("isValid")]
public required bool IsValid { get; init; }
/// <summary>
/// Effective trust weight.
/// </summary>
[JsonPropertyName("effectiveTrustWeight")]
public required double EffectiveTrustWeight { get; init; }
/// <summary>
/// Validation issues.
/// </summary>
[JsonPropertyName("issues")]
public ImmutableArray<string> Issues { get; init; } = [];
/// <summary>
/// Suggested actions to improve trust.
/// </summary>
[JsonPropertyName("suggestions")]
public ImmutableArray<string> Suggestions { get; init; } = [];
}
/// <summary>
/// Default implementation of jurisdiction trust service.
/// </summary>
public sealed class JurisdictionTrustService : IJurisdictionTrustService
{
private readonly IReadOnlyDictionary<Jurisdiction, JurisdictionTrustConfig> _configs;
/// <summary>
/// Default jurisdiction configurations.
/// </summary>
public static readonly ImmutableDictionary<Jurisdiction, JurisdictionTrustConfig> DefaultConfigs =
new Dictionary<Jurisdiction, JurisdictionTrustConfig>
{
[Jurisdiction.US] = new()
{
Jurisdiction = Jurisdiction.US,
PreferredSources = ["nist-nvd", "cisa-kev", "fda-medical", "vendor"],
PreferGovernmentSources = true,
MinimumTrustWeight = 0.4,
TrustWeightOverrides = new Dictionary<string, double>
{
["nist-nvd"] = 0.95,
["cisa-kev"] = 0.98,
["vendor"] = 0.85
}.ToImmutableDictionary()
},
[Jurisdiction.EU] = new()
{
Jurisdiction = Jurisdiction.EU,
PreferredSources = ["enisa", "bsi", "anssi", "cert-eu", "vendor"],
PreferGovernmentSources = true,
MinimumTrustWeight = 0.4,
TrustWeightOverrides = new Dictionary<string, double>
{
["enisa"] = 0.95,
["bsi"] = 0.92,
["anssi"] = 0.92,
["vendor"] = 0.85
}.ToImmutableDictionary()
},
[Jurisdiction.RU] = new()
{
Jurisdiction = Jurisdiction.RU,
PreferredSources = ["fstec", "fsb-cert", "vendor"],
PreferGovernmentSources = true,
MinimumTrustWeight = 0.5,
TrustWeightOverrides = new Dictionary<string, double>
{
["fstec"] = 0.98,
["vendor"] = 0.80
}.ToImmutableDictionary()
},
[Jurisdiction.CN] = new()
{
Jurisdiction = Jurisdiction.CN,
PreferredSources = ["cnvd", "cnnvd", "vendor"],
PreferGovernmentSources = true,
MinimumTrustWeight = 0.5,
TrustWeightOverrides = new Dictionary<string, double>
{
["cnvd"] = 0.95,
["cnnvd"] = 0.95,
["vendor"] = 0.80
}.ToImmutableDictionary()
},
[Jurisdiction.Global] = new()
{
Jurisdiction = Jurisdiction.Global,
PreferredSources = ["vendor", "osv", "github-advisory"],
PreferGovernmentSources = false,
MinimumTrustWeight = 0.3,
TrustWeightOverrides = new Dictionary<string, double>
{
["vendor"] = 0.90,
["osv"] = 0.75,
["github-advisory"] = 0.70
}.ToImmutableDictionary()
}
}.ToImmutableDictionary();
public JurisdictionTrustService(
IReadOnlyDictionary<Jurisdiction, JurisdictionTrustConfig>? configs = null)
{
_configs = configs ?? DefaultConfigs;
}
public double GetEffectiveTrustWeight(VexSource source, Jurisdiction jurisdiction)
{
if (!_configs.TryGetValue(jurisdiction, out var config))
{
config = DefaultConfigs[Jurisdiction.Global];
}
// Check for explicit override
if (config.TrustWeightOverrides.TryGetValue(source.Id, out var overrideWeight))
{
return overrideWeight;
}
var weight = source.BaseTrustWeight;
// Bonus for government sources in jurisdictions that prefer them
if (config.PreferGovernmentSources && source.IsGovernmentAuthority)
{
weight *= 1.2;
}
// Bonus for sources that list this jurisdiction as authoritative
if (source.Jurisdictions.Contains(jurisdiction))
{
weight *= 1.1;
}
// Penalty for non-preferred sources
var preferenceIndex = config.PreferredSources
.Select((id, i) => (id, i))
.FirstOrDefault(x => x.id == source.Id).i;
if (preferenceIndex > 0)
{
weight *= 1.0 - (preferenceIndex * 0.05);
}
return Math.Clamp(weight, 0.0, 1.0);
}
public IReadOnlyList<VexSource> RankSourcesByTrust(
IEnumerable<VexSource> sources,
Jurisdiction jurisdiction)
{
return sources
.OrderByDescending(s => GetEffectiveTrustWeight(s, jurisdiction))
.ToList();
}
public JurisdictionValidationResult ValidateForJurisdiction(
VexDecisionContext decision,
Jurisdiction jurisdiction)
{
if (!_configs.TryGetValue(jurisdiction, out var config))
{
config = DefaultConfigs[Jurisdiction.Global];
}
var issues = new List<string>();
var suggestions = new List<string>();
var effectiveWeight = GetEffectiveTrustWeight(decision.Source, jurisdiction);
// Check minimum trust weight
if (effectiveWeight < config.MinimumTrustWeight)
{
issues.Add($"Source trust weight ({effectiveWeight:P0}) below minimum ({config.MinimumTrustWeight:P0})");
suggestions.Add("Consider obtaining VEX from a higher-trust source");
}
// Check government preference
if (config.PreferGovernmentSources && !decision.Source.IsGovernmentAuthority)
{
suggestions.Add($"Jurisdiction {jurisdiction} prefers government sources");
}
// Check signature requirement for high-trust decisions
if (effectiveWeight >= 0.8 && !decision.IsSigned)
{
issues.Add("High-trust VEX decisions should be cryptographically signed");
suggestions.Add("Request signed VEX statement from source");
}
// Check required source types
if (config.RequiredSourceTypes.Length > 0 &&
!config.RequiredSourceTypes.Contains(decision.Source.Type))
{
issues.Add($"Source type {decision.Source.Type} not in required types");
}
return new JurisdictionValidationResult
{
IsValid = issues.Count == 0,
EffectiveTrustWeight = effectiveWeight,
Issues = issues.ToImmutableArray(),
Suggestions = suggestions.ToImmutableArray()
};
}
}

View File

@@ -0,0 +1,571 @@
// -----------------------------------------------------------------------------
// VexCustomerOverride.cs
// Sprint: SPRINT_3850_0001_0001 (Competitive Gap Closure)
// Task: VEX-L-004 - Customer override with signed audit trail
// Description: Customer-initiated VEX overrides with cryptographic audit trail.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Vex;
/// <summary>
/// Customer-initiated VEX override with full audit trail.
/// </summary>
public sealed record VexCustomerOverride
{
/// <summary>
/// Unique override identifier.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// CVE or vulnerability ID being overridden.
/// </summary>
[JsonPropertyName("vulnerabilityId")]
public required string VulnerabilityId { get; init; }
/// <summary>
/// Product or component PURL.
/// </summary>
[JsonPropertyName("productPurl")]
public required string ProductPurl { get; init; }
/// <summary>
/// Original VEX status from source.
/// </summary>
[JsonPropertyName("originalStatus")]
public required string OriginalStatus { get; init; }
/// <summary>
/// Overridden VEX status.
/// </summary>
[JsonPropertyName("overrideStatus")]
public required string OverrideStatus { get; init; }
/// <summary>
/// Justification for the override.
/// </summary>
[JsonPropertyName("justification")]
public required VexOverrideJustification Justification { get; init; }
/// <summary>
/// User who created the override.
/// </summary>
[JsonPropertyName("createdBy")]
public required OverrideActor CreatedBy { get; init; }
/// <summary>
/// When the override was created.
/// </summary>
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Approvers (for multi-party approval).
/// </summary>
[JsonPropertyName("approvers")]
public ImmutableArray<OverrideApproval> Approvers { get; init; } = [];
/// <summary>
/// Expiration time for the override.
/// </summary>
[JsonPropertyName("expiresAt")]
public DateTimeOffset? ExpiresAt { get; init; }
/// <summary>
/// Whether the override is currently active.
/// </summary>
[JsonPropertyName("isActive")]
public bool IsActive { get; init; } = true;
/// <summary>
/// Scope of the override.
/// </summary>
[JsonPropertyName("scope")]
public required OverrideScope Scope { get; init; }
/// <summary>
/// Cryptographic signature of the override.
/// </summary>
[JsonPropertyName("signature")]
public OverrideSignature? Signature { get; init; }
/// <summary>
/// Evidence references supporting the override.
/// </summary>
[JsonPropertyName("evidenceRefs")]
public ImmutableArray<string> EvidenceRefs { get; init; } = [];
/// <summary>
/// Tags for categorization.
/// </summary>
[JsonPropertyName("tags")]
public ImmutableArray<string> Tags { get; init; } = [];
/// <summary>
/// Audit events for this override.
/// </summary>
[JsonPropertyName("auditTrail")]
public ImmutableArray<OverrideAuditEvent> AuditTrail { get; init; } = [];
}
/// <summary>
/// Justification for a VEX override.
/// </summary>
public sealed record VexOverrideJustification
{
/// <summary>
/// Justification category.
/// </summary>
[JsonPropertyName("category")]
public required OverrideJustificationCategory Category { get; init; }
/// <summary>
/// Detailed explanation.
/// </summary>
[JsonPropertyName("explanation")]
public required string Explanation { get; init; }
/// <summary>
/// Compensating controls in place.
/// </summary>
[JsonPropertyName("compensatingControls")]
public ImmutableArray<string> CompensatingControls { get; init; } = [];
/// <summary>
/// Risk acceptance level.
/// </summary>
[JsonPropertyName("riskAcceptanceLevel")]
public RiskAcceptanceLevel? RiskAcceptanceLevel { get; init; }
/// <summary>
/// Remediation plan if applicable.
/// </summary>
[JsonPropertyName("remediationPlan")]
public RemediationPlan? RemediationPlan { get; init; }
}
/// <summary>
/// Categories for override justification.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum OverrideJustificationCategory
{
/// <summary>Vendor analysis incorrect.</summary>
VendorAnalysisIncorrect,
/// <summary>Compensating controls in place.</summary>
CompensatingControls,
/// <summary>Not applicable to deployment context.</summary>
NotApplicableToContext,
/// <summary>Risk accepted per policy.</summary>
RiskAccepted,
/// <summary>False positive confirmed.</summary>
FalsePositive,
/// <summary>Component not in use.</summary>
ComponentNotInUse,
/// <summary>Vulnerable code path not reachable.</summary>
CodePathNotReachable,
/// <summary>Already mitigated by other means.</summary>
AlreadyMitigated,
/// <summary>Business critical exception.</summary>
BusinessException
}
/// <summary>
/// Risk acceptance levels.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum RiskAcceptanceLevel
{
/// <summary>Low risk accepted.</summary>
Low,
/// <summary>Medium risk accepted.</summary>
Medium,
/// <summary>High risk accepted (requires senior approval).</summary>
High,
/// <summary>Critical risk accepted (requires executive approval).</summary>
Critical
}
/// <summary>
/// Remediation plan for accepted risk.
/// </summary>
public sealed record RemediationPlan
{
/// <summary>
/// Target remediation date.
/// </summary>
[JsonPropertyName("targetDate")]
public required DateTimeOffset TargetDate { get; init; }
/// <summary>
/// Remediation steps.
/// </summary>
[JsonPropertyName("steps")]
public ImmutableArray<string> Steps { get; init; } = [];
/// <summary>
/// Ticket/issue reference.
/// </summary>
[JsonPropertyName("ticketRef")]
public string? TicketRef { get; init; }
/// <summary>
/// Assigned owner.
/// </summary>
[JsonPropertyName("owner")]
public string? Owner { get; init; }
}
/// <summary>
/// Actor who created or modified an override.
/// </summary>
public sealed record OverrideActor
{
/// <summary>
/// User identifier.
/// </summary>
[JsonPropertyName("userId")]
public required string UserId { get; init; }
/// <summary>
/// User display name.
/// </summary>
[JsonPropertyName("displayName")]
public required string DisplayName { get; init; }
/// <summary>
/// User email.
/// </summary>
[JsonPropertyName("email")]
public string? Email { get; init; }
/// <summary>
/// User role at time of action.
/// </summary>
[JsonPropertyName("role")]
public string? Role { get; init; }
/// <summary>
/// Organization/tenant.
/// </summary>
[JsonPropertyName("organization")]
public string? Organization { get; init; }
}
/// <summary>
/// Approval for an override.
/// </summary>
public sealed record OverrideApproval
{
/// <summary>
/// Approver details.
/// </summary>
[JsonPropertyName("approver")]
public required OverrideActor Approver { get; init; }
/// <summary>
/// When approved.
/// </summary>
[JsonPropertyName("approvedAt")]
public required DateTimeOffset ApprovedAt { get; init; }
/// <summary>
/// Approval comment.
/// </summary>
[JsonPropertyName("comment")]
public string? Comment { get; init; }
/// <summary>
/// Signature of approval.
/// </summary>
[JsonPropertyName("signature")]
public OverrideSignature? Signature { get; init; }
}
/// <summary>
/// Scope of an override.
/// </summary>
public sealed record OverrideScope
{
/// <summary>
/// Scope type.
/// </summary>
[JsonPropertyName("type")]
public required OverrideScopeType Type { get; init; }
/// <summary>
/// Specific artifact digests if scoped.
/// </summary>
[JsonPropertyName("artifactDigests")]
public ImmutableArray<string> ArtifactDigests { get; init; } = [];
/// <summary>
/// Environment names if scoped.
/// </summary>
[JsonPropertyName("environments")]
public ImmutableArray<string> Environments { get; init; } = [];
/// <summary>
/// Version range if scoped.
/// </summary>
[JsonPropertyName("versionRange")]
public string? VersionRange { get; init; }
}
/// <summary>
/// Scope types for overrides.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum OverrideScopeType
{
/// <summary>Applies to all versions of the product.</summary>
AllVersions,
/// <summary>Applies to specific version range.</summary>
VersionRange,
/// <summary>Applies to specific artifacts only.</summary>
SpecificArtifacts,
/// <summary>Applies to specific environments only.</summary>
EnvironmentScoped
}
/// <summary>
/// Cryptographic signature for override.
/// </summary>
public sealed record OverrideSignature
{
/// <summary>
/// Signature algorithm.
/// </summary>
[JsonPropertyName("algorithm")]
public required string Algorithm { get; init; }
/// <summary>
/// Key identifier.
/// </summary>
[JsonPropertyName("keyId")]
public required string KeyId { get; init; }
/// <summary>
/// Signature value (base64).
/// </summary>
[JsonPropertyName("signature")]
public required string Signature { get; init; }
/// <summary>
/// Timestamp of signing.
/// </summary>
[JsonPropertyName("signedAt")]
public required DateTimeOffset SignedAt { get; init; }
/// <summary>
/// Certificate chain (PEM, if available).
/// </summary>
[JsonPropertyName("certificateChain")]
public string? CertificateChain { get; init; }
}
/// <summary>
/// Audit event for override lifecycle.
/// </summary>
public sealed record OverrideAuditEvent
{
/// <summary>
/// Event timestamp.
/// </summary>
[JsonPropertyName("timestamp")]
public required DateTimeOffset Timestamp { get; init; }
/// <summary>
/// Event type.
/// </summary>
[JsonPropertyName("eventType")]
public required OverrideAuditEventType EventType { get; init; }
/// <summary>
/// Actor who caused the event.
/// </summary>
[JsonPropertyName("actor")]
public required OverrideActor Actor { get; init; }
/// <summary>
/// Event details.
/// </summary>
[JsonPropertyName("details")]
public string? Details { get; init; }
/// <summary>
/// Previous value (for changes).
/// </summary>
[JsonPropertyName("previousValue")]
public string? PreviousValue { get; init; }
/// <summary>
/// New value (for changes).
/// </summary>
[JsonPropertyName("newValue")]
public string? NewValue { get; init; }
/// <summary>
/// IP address of actor.
/// </summary>
[JsonPropertyName("ipAddress")]
public string? IpAddress { get; init; }
/// <summary>
/// Event signature for tamper-evidence.
/// </summary>
[JsonPropertyName("eventSignature")]
public string? EventSignature { get; init; }
}
/// <summary>
/// Audit event types.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum OverrideAuditEventType
{
/// <summary>Override created.</summary>
Created,
/// <summary>Override approved.</summary>
Approved,
/// <summary>Override rejected.</summary>
Rejected,
/// <summary>Override modified.</summary>
Modified,
/// <summary>Override expired.</summary>
Expired,
/// <summary>Override revoked.</summary>
Revoked,
/// <summary>Override renewed.</summary>
Renewed,
/// <summary>Override applied to scan.</summary>
Applied,
/// <summary>Override viewed.</summary>
Viewed
}
/// <summary>
/// Service for managing customer VEX overrides.
/// </summary>
public interface IVexOverrideService
{
/// <summary>
/// Creates a new override.
/// </summary>
Task<VexCustomerOverride> CreateOverrideAsync(
CreateOverrideRequest request,
CancellationToken ct = default);
/// <summary>
/// Approves an override.
/// </summary>
Task<VexCustomerOverride> ApproveOverrideAsync(
string overrideId,
OverrideApproval approval,
CancellationToken ct = default);
/// <summary>
/// Revokes an override.
/// </summary>
Task<VexCustomerOverride> RevokeOverrideAsync(
string overrideId,
OverrideActor actor,
string reason,
CancellationToken ct = default);
/// <summary>
/// Gets active overrides for a vulnerability.
/// </summary>
Task<IReadOnlyList<VexCustomerOverride>> GetActiveOverridesAsync(
string vulnerabilityId,
string? productPurl = null,
CancellationToken ct = default);
/// <summary>
/// Gets the audit trail for an override.
/// </summary>
Task<IReadOnlyList<OverrideAuditEvent>> GetAuditTrailAsync(
string overrideId,
CancellationToken ct = default);
}
/// <summary>
/// Request to create an override.
/// </summary>
public sealed record CreateOverrideRequest
{
/// <summary>
/// Vulnerability ID.
/// </summary>
[JsonPropertyName("vulnerabilityId")]
public required string VulnerabilityId { get; init; }
/// <summary>
/// Product PURL.
/// </summary>
[JsonPropertyName("productPurl")]
public required string ProductPurl { get; init; }
/// <summary>
/// Override status.
/// </summary>
[JsonPropertyName("overrideStatus")]
public required string OverrideStatus { get; init; }
/// <summary>
/// Justification.
/// </summary>
[JsonPropertyName("justification")]
public required VexOverrideJustification Justification { get; init; }
/// <summary>
/// Scope.
/// </summary>
[JsonPropertyName("scope")]
public required OverrideScope Scope { get; init; }
/// <summary>
/// Expiration.
/// </summary>
[JsonPropertyName("expiresAt")]
public DateTimeOffset? ExpiresAt { get; init; }
/// <summary>
/// Evidence references.
/// </summary>
[JsonPropertyName("evidenceRefs")]
public ImmutableArray<string> EvidenceRefs { get; init; } = [];
/// <summary>
/// Tags.
/// </summary>
[JsonPropertyName("tags")]
public ImmutableArray<string> Tags { get; init; } = [];
}