sprints work
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering Evidence-Weighted Scoring services.
|
||||
/// </summary>
|
||||
public static class EvidenceWeightedScoringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Evidence-Weighted Scoring services to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddEvidenceWeightedScoring(this IServiceCollection services)
|
||||
{
|
||||
return services.AddEvidenceWeightedScoring(_ => { });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Evidence-Weighted Scoring services to the service collection with configuration.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configure">Configuration action for options.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddEvidenceWeightedScoring(
|
||||
this IServiceCollection services,
|
||||
Action<EvidenceWeightPolicyOptions> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// Register options with hot-reload support
|
||||
services.AddOptions<EvidenceWeightPolicyOptions>()
|
||||
.Configure(configure);
|
||||
|
||||
// Register calculator as singleton (stateless, thread-safe)
|
||||
services.TryAddSingleton<IEvidenceWeightedScoreCalculator, EvidenceWeightedScoreCalculator>();
|
||||
|
||||
// Register policy provider
|
||||
services.TryAddSingleton<IEvidenceWeightPolicyProvider>(sp =>
|
||||
{
|
||||
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<EvidenceWeightPolicyOptions>>();
|
||||
return new OptionsEvidenceWeightPolicyProvider(optionsMonitor);
|
||||
});
|
||||
|
||||
// Register TimeProvider if not already registered
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Evidence-Weighted Scoring services with a custom policy provider.
|
||||
/// </summary>
|
||||
/// <typeparam name="TProvider">The policy provider type.</typeparam>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddEvidenceWeightedScoring<TProvider>(this IServiceCollection services)
|
||||
where TProvider : class, IEvidenceWeightPolicyProvider
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
// Register calculator as singleton
|
||||
services.TryAddSingleton<IEvidenceWeightedScoreCalculator, EvidenceWeightedScoreCalculator>();
|
||||
|
||||
// Register custom policy provider
|
||||
services.TryAddSingleton<IEvidenceWeightPolicyProvider, TProvider>();
|
||||
|
||||
// Register TimeProvider if not already registered
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Evidence-Weighted Scoring services with an in-memory policy.
|
||||
/// Useful for testing or simple deployments.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="policy">The policy to use.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddEvidenceWeightedScoringWithPolicy(
|
||||
this IServiceCollection services,
|
||||
EvidenceWeightPolicy policy)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(policy);
|
||||
|
||||
// Register calculator as singleton
|
||||
services.TryAddSingleton<IEvidenceWeightedScoreCalculator, EvidenceWeightedScoreCalculator>();
|
||||
|
||||
// Register in-memory provider with the given policy
|
||||
var provider = new InMemoryEvidenceWeightPolicyProvider();
|
||||
provider.SetPolicy(policy);
|
||||
services.TryAddSingleton<IEvidenceWeightPolicyProvider>(provider);
|
||||
|
||||
// Register TimeProvider if not already registered
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Evidence-Weighted Scoring services with default production policy.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddEvidenceWeightedScoringWithDefaults(this IServiceCollection services)
|
||||
{
|
||||
return services.AddEvidenceWeightedScoringWithPolicy(EvidenceWeightPolicy.DefaultProduction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes backport evidence to a [0, 1] BKP score.
|
||||
/// Higher scores indicate stronger evidence that a vulnerability has been fixed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Evidence tiers (from weakest to strongest):
|
||||
/// - None: No backport evidence (0.00)
|
||||
/// - Heuristic: Changelog mention, commit patterns (0.45-0.60)
|
||||
/// - PatchSignature: Patch-graph signature match (0.70-0.85)
|
||||
/// - BinaryDiff: Binary-level diff confirmation (0.80-0.92)
|
||||
/// - VendorVex: Vendor-issued VEX statement (0.85-0.95)
|
||||
/// - SignedProof: Cryptographically signed proof (0.90-1.00)
|
||||
///
|
||||
/// Multiple evidence tiers provide a combination bonus (up to 0.05).
|
||||
/// </remarks>
|
||||
public sealed class BackportEvidenceNormalizer : IEvidenceNormalizer<BackportInput>
|
||||
{
|
||||
private readonly BackportNormalizerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="BackportEvidenceNormalizer"/>.
|
||||
/// </summary>
|
||||
public BackportEvidenceNormalizer(IOptionsMonitor<NormalizerOptions> options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options.CurrentValue.Backport;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance with explicit options (for testing).
|
||||
/// </summary>
|
||||
internal BackportEvidenceNormalizer(BackportNormalizerOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Dimension => "BKP";
|
||||
|
||||
/// <inheritdoc />
|
||||
public double Normalize(BackportInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
return CalculateScore(input);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NormalizationResult NormalizeWithDetails(BackportInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
var score = CalculateScore(input);
|
||||
var explanation = GenerateExplanation(input, score);
|
||||
var components = BuildComponents(input);
|
||||
|
||||
return NormalizationResult.WithComponents(score, Dimension, explanation, components);
|
||||
}
|
||||
|
||||
private double CalculateScore(BackportInput input)
|
||||
{
|
||||
// Status handling: Fixed or NotAffected = high confidence
|
||||
if (input.Status == BackportStatus.NotAffected)
|
||||
{
|
||||
return CalculateNotAffectedScore(input);
|
||||
}
|
||||
|
||||
if (input.Status == BackportStatus.Fixed)
|
||||
{
|
||||
return CalculateFixedScore(input);
|
||||
}
|
||||
|
||||
if (input.Status == BackportStatus.Affected || input.Status == BackportStatus.UnderInvestigation)
|
||||
{
|
||||
// Affected = no backport protection; use base score from evidence tier
|
||||
return CalculateTierBaseScore(input.EvidenceTier, input.Confidence);
|
||||
}
|
||||
|
||||
// Unknown status - rely on evidence tier and confidence
|
||||
return CalculateTierBaseScore(input.EvidenceTier, input.Confidence);
|
||||
}
|
||||
|
||||
private double CalculateNotAffectedScore(BackportInput input)
|
||||
{
|
||||
// NotAffected with high-tier evidence = very high score
|
||||
var baseScore = GetTierRange(input.EvidenceTier).Min;
|
||||
var tierBonus = (GetTierRange(input.EvidenceTier).Max - baseScore) * input.Confidence;
|
||||
var statusBonus = 0.10; // Bonus for NotAffected status
|
||||
|
||||
return Math.Min(1.0, baseScore + tierBonus + statusBonus);
|
||||
}
|
||||
|
||||
private double CalculateFixedScore(BackportInput input)
|
||||
{
|
||||
// Fixed status = confirmed backport; score based on evidence tier
|
||||
var (min, max) = GetTierRange(input.EvidenceTier);
|
||||
var baseScore = min;
|
||||
var tierBonus = (max - min) * input.Confidence;
|
||||
|
||||
return Math.Min(1.0, baseScore + tierBonus);
|
||||
}
|
||||
|
||||
private double CalculateTierBaseScore(BackportEvidenceTier tier, double confidence)
|
||||
{
|
||||
if (tier == BackportEvidenceTier.None)
|
||||
return 0.0;
|
||||
|
||||
var (min, max) = GetTierRange(tier);
|
||||
return min + (max - min) * confidence;
|
||||
}
|
||||
|
||||
private (double Min, double Max) GetTierRange(BackportEvidenceTier tier)
|
||||
{
|
||||
return tier switch
|
||||
{
|
||||
BackportEvidenceTier.None => _options.Tier0Range, // (0.00, 0.10)
|
||||
BackportEvidenceTier.Heuristic => _options.Tier1Range, // (0.45, 0.60)
|
||||
BackportEvidenceTier.PatchSignature => _options.Tier2Range, // (0.70, 0.85)
|
||||
BackportEvidenceTier.BinaryDiff => _options.Tier3Range, // (0.80, 0.92)
|
||||
BackportEvidenceTier.VendorVex => _options.Tier4Range, // (0.85, 0.95)
|
||||
BackportEvidenceTier.SignedProof => _options.Tier5Range, // (0.90, 1.00)
|
||||
_ => _options.Tier0Range
|
||||
};
|
||||
}
|
||||
|
||||
private string GenerateExplanation(BackportInput input, double score)
|
||||
{
|
||||
if (input.EvidenceTier == BackportEvidenceTier.None)
|
||||
return "No backport evidence available.";
|
||||
|
||||
var statusDesc = input.Status switch
|
||||
{
|
||||
BackportStatus.Fixed => "Fixed",
|
||||
BackportStatus.NotAffected => "Not affected",
|
||||
BackportStatus.Affected => "Affected",
|
||||
BackportStatus.UnderInvestigation => "Under investigation",
|
||||
_ => "Unknown status"
|
||||
};
|
||||
|
||||
var tierDesc = input.EvidenceTier switch
|
||||
{
|
||||
BackportEvidenceTier.Heuristic => "heuristic detection (changelog/commit patterns)",
|
||||
BackportEvidenceTier.PatchSignature => "patch signature match",
|
||||
BackportEvidenceTier.BinaryDiff => "binary diff confirmation",
|
||||
BackportEvidenceTier.VendorVex => "vendor VEX statement",
|
||||
BackportEvidenceTier.SignedProof => "cryptographically signed proof",
|
||||
_ => "unknown evidence"
|
||||
};
|
||||
|
||||
var confidenceDesc = input.Confidence switch
|
||||
{
|
||||
>= 0.9 => "very high",
|
||||
>= 0.7 => "high",
|
||||
>= 0.5 => "moderate",
|
||||
>= 0.3 => "low",
|
||||
_ => "very low"
|
||||
};
|
||||
|
||||
var proofInfo = !string.IsNullOrEmpty(input.ProofId)
|
||||
? $" (proof: {input.ProofId})"
|
||||
: "";
|
||||
|
||||
return $"{statusDesc} via {tierDesc} with {confidenceDesc} confidence ({input.Confidence:P0}){proofInfo}. BKP = {score:F2}.";
|
||||
}
|
||||
|
||||
private Dictionary<string, double> BuildComponents(BackportInput input)
|
||||
{
|
||||
var components = new Dictionary<string, double>
|
||||
{
|
||||
["tier_base"] = GetTierRange(input.EvidenceTier).Min,
|
||||
["confidence"] = input.Confidence,
|
||||
["tier_ordinal"] = (int)input.EvidenceTier
|
||||
};
|
||||
|
||||
if (input.Status == BackportStatus.NotAffected)
|
||||
{
|
||||
components["status_bonus"] = 0.10;
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering evidence normalizer services.
|
||||
/// </summary>
|
||||
public static class EvidenceNormalizersServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds all evidence normalizer services to the DI container.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddEvidenceNormalizers(this IServiceCollection services)
|
||||
{
|
||||
return services.AddEvidenceNormalizers(_ => { });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all evidence normalizer services to the DI container with custom options configuration.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configure">Action to configure normalizer options.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddEvidenceNormalizers(
|
||||
this IServiceCollection services,
|
||||
Action<NormalizerOptions> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// Register options with default values and apply configuration
|
||||
services.AddOptions<NormalizerOptions>()
|
||||
.Configure(configure);
|
||||
|
||||
// Register individual normalizers
|
||||
services.TryAddSingleton<IEvidenceNormalizer<ReachabilityInput>, ReachabilityNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<RuntimeInput>, RuntimeSignalNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<BackportInput>, BackportEvidenceNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<ExploitInput>, ExploitLikelihoodNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<SourceTrustInput>, SourceTrustNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<MitigationInput>, MitigationNormalizer>();
|
||||
|
||||
// Register the aggregator
|
||||
services.TryAddSingleton<INormalizerAggregator, NormalizerAggregator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all evidence normalizer services with configuration binding from appsettings.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configuration">The configuration root.</param>
|
||||
/// <param name="sectionName">The configuration section name (default: "EvidenceNormalizers").</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddEvidenceNormalizers(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string sectionName = "EvidenceNormalizers")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
// Bind options from configuration
|
||||
var section = configuration.GetSection(sectionName);
|
||||
services.AddOptions<NormalizerOptions>()
|
||||
.Bind(section)
|
||||
.ValidateOnStart();
|
||||
|
||||
// Register individual normalizers
|
||||
services.TryAddSingleton<IEvidenceNormalizer<ReachabilityInput>, ReachabilityNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<RuntimeInput>, RuntimeSignalNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<BackportInput>, BackportEvidenceNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<ExploitInput>, ExploitLikelihoodNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<SourceTrustInput>, SourceTrustNormalizer>();
|
||||
services.TryAddSingleton<IEvidenceNormalizer<MitigationInput>, MitigationNormalizer>();
|
||||
|
||||
// Register the aggregator
|
||||
services.TryAddSingleton<INormalizerAggregator, NormalizerAggregator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the evidence normalizer aggregator only.
|
||||
/// Use this when individual normalizers are already registered.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddNormalizerAggregator(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.TryAddSingleton<INormalizerAggregator, NormalizerAggregator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes exploit likelihood evidence to a [0, 1] XPL score.
|
||||
/// Combines EPSS (Exploit Prediction Scoring System) with KEV (Known Exploited Vulnerabilities) status.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Scoring logic:
|
||||
/// - KEV presence establishes a floor (default 0.40) - actively exploited vulnerabilities are high risk
|
||||
/// - EPSS percentile maps to score bands:
|
||||
/// - Top 1% (≥99th percentile): 0.90–1.00
|
||||
/// - Top 5% (≥95th percentile): 0.70–0.89
|
||||
/// - Top 25% (≥75th percentile): 0.40–0.69
|
||||
/// - Below 75th percentile: 0.20–0.39
|
||||
/// - Missing EPSS data: neutral score (default 0.30)
|
||||
/// - Public exploit availability adds a bonus
|
||||
/// - Final score is max(KEV floor, EPSS-based score)
|
||||
/// </remarks>
|
||||
public sealed class ExploitLikelihoodNormalizer : IEvidenceNormalizer<ExploitInput>
|
||||
{
|
||||
private readonly ExploitNormalizerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ExploitLikelihoodNormalizer"/>.
|
||||
/// </summary>
|
||||
public ExploitLikelihoodNormalizer(IOptionsMonitor<NormalizerOptions> options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options.CurrentValue.Exploit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance with explicit options (for testing).
|
||||
/// </summary>
|
||||
internal ExploitLikelihoodNormalizer(ExploitNormalizerOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Dimension => "XPL";
|
||||
|
||||
/// <inheritdoc />
|
||||
public double Normalize(ExploitInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
return CalculateScore(input);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NormalizationResult NormalizeWithDetails(ExploitInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
var score = CalculateScore(input);
|
||||
var explanation = GenerateExplanation(input, score);
|
||||
var components = BuildComponents(input);
|
||||
|
||||
return NormalizationResult.WithComponents(score, Dimension, explanation, components);
|
||||
}
|
||||
|
||||
private double CalculateScore(ExploitInput input)
|
||||
{
|
||||
var epssScore = CalculateEpssScore(input);
|
||||
var kevFloor = GetKevFloor(input);
|
||||
var exploitBonus = input.PublicExploitAvailable ? 0.10 : 0.0;
|
||||
|
||||
// Final score is max of KEV floor and EPSS score, plus exploit availability bonus
|
||||
return Math.Min(1.0, Math.Max(kevFloor, epssScore) + exploitBonus);
|
||||
}
|
||||
|
||||
private double CalculateEpssScore(ExploitInput input)
|
||||
{
|
||||
// EPSS percentile is in range [0, 100]
|
||||
var percentile = input.EpssPercentile;
|
||||
|
||||
// Convert percentile (0-100) to fraction (0-1) for threshold comparison
|
||||
var percentileFraction = percentile / 100.0;
|
||||
|
||||
if (percentileFraction >= _options.Top1PercentThreshold)
|
||||
{
|
||||
// Top 1%: highest risk band
|
||||
return InterpolateInRange(percentileFraction, _options.Top1PercentThreshold, 1.0, _options.Top1PercentRange);
|
||||
}
|
||||
|
||||
if (percentileFraction >= _options.Top5PercentThreshold)
|
||||
{
|
||||
// Top 5%: high risk band
|
||||
return InterpolateInRange(percentileFraction, _options.Top5PercentThreshold, _options.Top1PercentThreshold, _options.Top5PercentRange);
|
||||
}
|
||||
|
||||
if (percentileFraction >= _options.Top25PercentThreshold)
|
||||
{
|
||||
// Top 25%: moderate risk band
|
||||
return InterpolateInRange(percentileFraction, _options.Top25PercentThreshold, _options.Top5PercentThreshold, _options.Top25PercentRange);
|
||||
}
|
||||
|
||||
// Below 75th percentile: lower risk
|
||||
return InterpolateInRange(percentileFraction, 0.0, _options.Top25PercentThreshold, _options.LowerPercentRange);
|
||||
}
|
||||
|
||||
private static double InterpolateInRange(double value, double rangeMin, double rangeMax, (double Low, double High) scoreRange)
|
||||
{
|
||||
if (rangeMax <= rangeMin)
|
||||
return scoreRange.Low;
|
||||
|
||||
var normalizedPosition = (value - rangeMin) / (rangeMax - rangeMin);
|
||||
return scoreRange.Low + (scoreRange.High - scoreRange.Low) * normalizedPosition;
|
||||
}
|
||||
|
||||
private double GetKevFloor(ExploitInput input)
|
||||
{
|
||||
return input.KevStatus switch
|
||||
{
|
||||
KevStatus.InKev => _options.KevFloor,
|
||||
KevStatus.RemovedFromKev => _options.KevFloor * 0.5, // Reduced but still elevated
|
||||
KevStatus.NotInKev => 0.0,
|
||||
_ => 0.0
|
||||
};
|
||||
}
|
||||
|
||||
private string GenerateExplanation(ExploitInput input, double score)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
// EPSS description
|
||||
var epssDesc = input.EpssPercentile switch
|
||||
{
|
||||
>= 99.0 => $"Very high EPSS ({input.EpssScore:P1}, top 1%)",
|
||||
>= 95.0 => $"High EPSS ({input.EpssScore:P1}, top 5%)",
|
||||
>= 75.0 => $"Moderate EPSS ({input.EpssScore:P1}, top 25%)",
|
||||
>= 50.0 => $"Low EPSS ({input.EpssScore:P1})",
|
||||
_ => $"Very low EPSS ({input.EpssScore:P1})"
|
||||
};
|
||||
parts.Add(epssDesc);
|
||||
|
||||
// KEV status
|
||||
if (input.KevStatus == KevStatus.InKev)
|
||||
{
|
||||
var kevInfo = "actively exploited (KEV)";
|
||||
if (input.KevAddedDate.HasValue)
|
||||
kevInfo += $", added {input.KevAddedDate.Value:yyyy-MM-dd}";
|
||||
if (input.KevDueDate.HasValue)
|
||||
kevInfo += $", due {input.KevDueDate.Value:yyyy-MM-dd}";
|
||||
parts.Add(kevInfo);
|
||||
}
|
||||
else if (input.KevStatus == KevStatus.RemovedFromKev)
|
||||
{
|
||||
parts.Add("previously in KEV (removed)");
|
||||
}
|
||||
|
||||
// Public exploit
|
||||
if (input.PublicExploitAvailable)
|
||||
{
|
||||
var maturityInfo = !string.IsNullOrEmpty(input.ExploitMaturity)
|
||||
? $" ({input.ExploitMaturity})"
|
||||
: "";
|
||||
parts.Add($"public exploit available{maturityInfo}");
|
||||
}
|
||||
|
||||
var explanation = string.Join("; ", parts);
|
||||
return $"{explanation}. XPL = {score:F2}.";
|
||||
}
|
||||
|
||||
private Dictionary<string, double> BuildComponents(ExploitInput input)
|
||||
{
|
||||
var components = new Dictionary<string, double>
|
||||
{
|
||||
["epss_score"] = input.EpssScore,
|
||||
["epss_percentile"] = input.EpssPercentile,
|
||||
["epss_based_score"] = CalculateEpssScore(input),
|
||||
["kev_floor"] = GetKevFloor(input),
|
||||
["kev_status"] = (int)input.KevStatus
|
||||
};
|
||||
|
||||
if (input.PublicExploitAvailable)
|
||||
{
|
||||
components["exploit_bonus"] = 0.10;
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a normalization operation with detailed breakdown.
|
||||
/// </summary>
|
||||
/// <param name="Score">Normalized score [0, 1].</param>
|
||||
/// <param name="Dimension">Dimension name (e.g., "Reachability", "Runtime").</param>
|
||||
/// <param name="Explanation">Human-readable explanation of the normalization.</param>
|
||||
/// <param name="Components">Breakdown of individual contributing factors.</param>
|
||||
public sealed record NormalizationResult(
|
||||
double Score,
|
||||
string Dimension,
|
||||
string Explanation,
|
||||
IReadOnlyDictionary<string, double> Components)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a simple result with no component breakdown.
|
||||
/// </summary>
|
||||
public static NormalizationResult Simple(double score, string dimension, string explanation) =>
|
||||
new(score, dimension, explanation, new Dictionary<string, double>());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a result with component breakdown.
|
||||
/// </summary>
|
||||
public static NormalizationResult WithComponents(
|
||||
double score,
|
||||
string dimension,
|
||||
string explanation,
|
||||
Dictionary<string, double> components) =>
|
||||
new(score, dimension, explanation, new Dictionary<string, double>(components));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes raw evidence to a [0, 1] score for evidence-weighted scoring.
|
||||
/// Each implementation bridges a specific data source to the unified scoring model.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The raw evidence input type.</typeparam>
|
||||
public interface IEvidenceNormalizer<in TInput>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the dimension name this normalizer produces (e.g., "RCH", "RTS", "BKP").
|
||||
/// </summary>
|
||||
string Dimension { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes raw evidence to a [0, 1] score.
|
||||
/// </summary>
|
||||
/// <param name="input">The raw evidence to normalize.</param>
|
||||
/// <returns>A score in range [0, 1] where higher = stronger evidence.</returns>
|
||||
double Normalize(TInput input);
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes raw evidence with detailed breakdown.
|
||||
/// </summary>
|
||||
/// <param name="input">The raw evidence to normalize.</param>
|
||||
/// <returns>Detailed normalization result including explanation and components.</returns>
|
||||
NormalizationResult NormalizeWithDetails(TInput input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for normalizers.
|
||||
/// </summary>
|
||||
public static class NormalizerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes input and clamps result to [0, 1].
|
||||
/// </summary>
|
||||
public static double NormalizeClamped<TInput>(this IEvidenceNormalizer<TInput> normalizer, TInput input) =>
|
||||
Math.Clamp(normalizer.Normalize(input), 0.0, 1.0);
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes multiple inputs and returns average.
|
||||
/// </summary>
|
||||
public static double NormalizeAverage<TInput>(this IEvidenceNormalizer<TInput> normalizer, IEnumerable<TInput> inputs)
|
||||
{
|
||||
var scores = inputs.Select(normalizer.NormalizeClamped).ToList();
|
||||
return scores.Count == 0 ? 0.0 : scores.Average();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes multiple inputs and returns maximum.
|
||||
/// </summary>
|
||||
public static double NormalizeMax<TInput>(this IEvidenceNormalizer<TInput> normalizer, IEnumerable<TInput> inputs)
|
||||
{
|
||||
var scores = inputs.Select(normalizer.NormalizeClamped).ToList();
|
||||
return scores.Count == 0 ? 0.0 : scores.Max();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Aggregated evidence from all sources for a single finding.
|
||||
/// Used as input to the normalizer aggregator.
|
||||
/// Maps to existing detailed input types from EvidenceWeightedScoreInput.
|
||||
/// </summary>
|
||||
public sealed record FindingEvidence
|
||||
{
|
||||
/// <summary>Finding identifier (CVE@PURL format).</summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>Reachability evidence (maps to ReachabilityInput).</summary>
|
||||
public ReachabilityInput? Reachability { get; init; }
|
||||
|
||||
/// <summary>Runtime signal evidence (maps to RuntimeInput).</summary>
|
||||
public RuntimeInput? Runtime { get; init; }
|
||||
|
||||
/// <summary>Backport/patch evidence (maps to BackportInput).</summary>
|
||||
public BackportInput? Backport { get; init; }
|
||||
|
||||
/// <summary>Exploit likelihood evidence (maps to ExploitInput).</summary>
|
||||
public ExploitInput? Exploit { get; init; }
|
||||
|
||||
/// <summary>Source trust evidence (maps to SourceTrustInput).</summary>
|
||||
public SourceTrustInput? SourceTrust { get; init; }
|
||||
|
||||
/// <summary>Active mitigations evidence (maps to MitigationInput).</summary>
|
||||
public MitigationInput? Mitigations { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates FindingEvidence from an existing EvidenceWeightedScoreInput.
|
||||
/// Extracts the detailed input records if present.
|
||||
/// </summary>
|
||||
public static FindingEvidence FromScoreInput(EvidenceWeightedScoreInput input) =>
|
||||
new()
|
||||
{
|
||||
FindingId = input.FindingId,
|
||||
Reachability = input.ReachabilityDetails,
|
||||
Runtime = input.RuntimeDetails,
|
||||
Backport = input.BackportDetails,
|
||||
Exploit = input.ExploitDetails,
|
||||
SourceTrust = input.SourceTrustDetails,
|
||||
Mitigations = input.MitigationDetails
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates all normalizers to produce unified evidence-weighted score input.
|
||||
/// </summary>
|
||||
public interface INormalizerAggregator
|
||||
{
|
||||
/// <summary>
|
||||
/// Aggregates all evidence for a finding into normalized input.
|
||||
/// Retrieves evidence data asynchronously from configured sources.
|
||||
/// </summary>
|
||||
/// <param name="findingId">The finding identifier (CVE@PURL format).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Fully populated evidence-weighted score input.</returns>
|
||||
Task<EvidenceWeightedScoreInput> AggregateAsync(
|
||||
string findingId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates pre-loaded evidence into normalized input.
|
||||
/// Use when evidence has already been retrieved.
|
||||
/// </summary>
|
||||
/// <param name="evidence">Pre-loaded evidence for the finding.</param>
|
||||
/// <returns>Fully populated evidence-weighted score input.</returns>
|
||||
EvidenceWeightedScoreInput Aggregate(FindingEvidence evidence);
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates with detailed breakdown for all dimensions.
|
||||
/// </summary>
|
||||
/// <param name="evidence">Pre-loaded evidence for the finding.</param>
|
||||
/// <returns>Input with detailed normalization results.</returns>
|
||||
AggregationResult AggregateWithDetails(FindingEvidence evidence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detailed aggregation result including all normalization breakdowns.
|
||||
/// </summary>
|
||||
public sealed record AggregationResult
|
||||
{
|
||||
/// <summary>The normalized input values.</summary>
|
||||
public required EvidenceWeightedScoreInput Input { get; init; }
|
||||
|
||||
/// <summary>Detailed normalization results per dimension.</summary>
|
||||
public required IReadOnlyDictionary<string, NormalizationResult> Details { get; init; }
|
||||
|
||||
/// <summary>Any warnings or issues during normalization.</summary>
|
||||
public IReadOnlyList<string> Warnings { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes mitigation evidence to a [0, 1] MIT score.
|
||||
/// Higher scores indicate stronger mitigations that reduce exploitability.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Mitigation types and typical effectiveness:
|
||||
/// - FeatureFlag: Code disabled (0.20-0.40)
|
||||
/// - AuthRequired: Authentication requirement (0.10-0.20)
|
||||
/// - AdminOnly: Admin-only access (0.15-0.25)
|
||||
/// - NonDefaultConfig: Non-default configuration (0.15-0.30)
|
||||
/// - SecurityPolicy: Seccomp/AppArmor/SELinux (0.10-0.25)
|
||||
/// - Isolation: Container/sandbox isolation (0.10-0.20)
|
||||
/// - NetworkControl: Network-level controls (0.05-0.15)
|
||||
/// - InputValidation: Rate limiting/validation (0.05-0.10)
|
||||
/// - VirtualPatch: IDS/IPS rules (0.10-0.20)
|
||||
/// - ComponentRemoval: Vulnerable component removed (0.80-1.00)
|
||||
///
|
||||
/// Multiple mitigations are summed, capped at 1.0.
|
||||
/// Verified mitigations receive a confidence bonus.
|
||||
/// </remarks>
|
||||
public sealed class MitigationNormalizer : IEvidenceNormalizer<MitigationInput>
|
||||
{
|
||||
private readonly MitigationNormalizerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="MitigationNormalizer"/>.
|
||||
/// </summary>
|
||||
public MitigationNormalizer(IOptionsMonitor<NormalizerOptions> options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options.CurrentValue.Mitigation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance with explicit options (for testing).
|
||||
/// </summary>
|
||||
internal MitigationNormalizer(MitigationNormalizerOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Dimension => "MIT";
|
||||
|
||||
/// <inheritdoc />
|
||||
public double Normalize(MitigationInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
return CalculateScore(input);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NormalizationResult NormalizeWithDetails(MitigationInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
var score = CalculateScore(input);
|
||||
var explanation = GenerateExplanation(input, score);
|
||||
var components = BuildComponents(input);
|
||||
|
||||
return NormalizationResult.WithComponents(score, Dimension, explanation, components);
|
||||
}
|
||||
|
||||
private double CalculateScore(MitigationInput input)
|
||||
{
|
||||
var runtimeBonus = input.RuntimeVerified ? _options.VerificationBonus : 0.0;
|
||||
|
||||
// If pre-computed combined effectiveness is provided, validate and use it
|
||||
if (input.CombinedEffectiveness > 0.0)
|
||||
{
|
||||
var validatedEffectiveness = Math.Min(input.CombinedEffectiveness, _options.MaxTotalMitigation);
|
||||
return Math.Min(1.0, validatedEffectiveness + runtimeBonus);
|
||||
}
|
||||
|
||||
// Calculate from active mitigations
|
||||
if (input.ActiveMitigations.Count == 0)
|
||||
return 0.0;
|
||||
|
||||
var totalEffectiveness = CalculateTotalEffectiveness(input.ActiveMitigations);
|
||||
|
||||
return Math.Min(1.0, totalEffectiveness + runtimeBonus);
|
||||
}
|
||||
|
||||
private double CalculateTotalEffectiveness(IReadOnlyList<ActiveMitigation> mitigations)
|
||||
{
|
||||
var total = 0.0;
|
||||
|
||||
foreach (var mitigation in mitigations)
|
||||
{
|
||||
var effectiveness = mitigation.Effectiveness;
|
||||
|
||||
// Apply verification bonus at individual mitigation level
|
||||
if (mitigation.Verified)
|
||||
{
|
||||
effectiveness += _options.VerificationBonus * 0.5; // Half bonus at individual level
|
||||
}
|
||||
|
||||
total += effectiveness;
|
||||
}
|
||||
|
||||
// Cap at max total mitigation
|
||||
return Math.Min(total, _options.MaxTotalMitigation);
|
||||
}
|
||||
|
||||
private (double Low, double High) GetEffectivenessRange(MitigationType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MitigationType.FeatureFlag => _options.FeatureFlagEffectiveness,
|
||||
MitigationType.AuthRequired => _options.AuthRequiredEffectiveness,
|
||||
MitigationType.SecurityPolicy => _options.SeccompEffectiveness, // SELinux/AppArmor/seccomp
|
||||
MitigationType.Isolation => _options.NetworkIsolationEffectiveness, // Reuse range
|
||||
MitigationType.InputValidation => _options.ReadOnlyFsEffectiveness, // Reuse range
|
||||
MitigationType.NetworkControl => _options.NetworkIsolationEffectiveness,
|
||||
MitigationType.VirtualPatch => _options.AuthRequiredEffectiveness, // Similar range
|
||||
MitigationType.ComponentRemoval => (0.80, 1.00), // Complete removal is very effective
|
||||
MitigationType.Unknown => (0.0, 0.10),
|
||||
_ => (0.0, 0.10)
|
||||
};
|
||||
}
|
||||
|
||||
private string GenerateExplanation(MitigationInput input, double score)
|
||||
{
|
||||
if (input.ActiveMitigations.Count == 0 && input.CombinedEffectiveness <= 0.0)
|
||||
{
|
||||
return "No active mitigations identified.";
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
|
||||
if (input.ActiveMitigations.Count > 0)
|
||||
{
|
||||
var mitigationDescriptions = input.ActiveMitigations
|
||||
.Select(m => FormatMitigation(m))
|
||||
.ToList();
|
||||
|
||||
parts.Add($"{input.ActiveMitigations.Count} mitigation(s): {string.Join(", ", mitigationDescriptions)}");
|
||||
}
|
||||
else if (input.CombinedEffectiveness > 0.0)
|
||||
{
|
||||
parts.Add($"Combined effectiveness: {input.CombinedEffectiveness:P0}");
|
||||
}
|
||||
|
||||
if (input.RuntimeVerified)
|
||||
{
|
||||
parts.Add("runtime verified");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(input.AssessmentSource))
|
||||
{
|
||||
parts.Add($"source: {input.AssessmentSource}");
|
||||
}
|
||||
|
||||
var description = string.Join("; ", parts);
|
||||
return $"{description}. MIT = {score:F2}.";
|
||||
}
|
||||
|
||||
private static string FormatMitigation(ActiveMitigation mitigation)
|
||||
{
|
||||
var name = !string.IsNullOrEmpty(mitigation.Name) ? mitigation.Name : mitigation.Type.ToString();
|
||||
var verified = mitigation.Verified ? " ✓" : "";
|
||||
return $"{name} ({mitigation.Effectiveness:P0}{verified})";
|
||||
}
|
||||
|
||||
private Dictionary<string, double> BuildComponents(MitigationInput input)
|
||||
{
|
||||
var components = new Dictionary<string, double>
|
||||
{
|
||||
["mitigation_count"] = input.ActiveMitigations.Count,
|
||||
["combined_effectiveness"] = input.CombinedEffectiveness,
|
||||
["runtime_verified"] = input.RuntimeVerified ? 1.0 : 0.0
|
||||
};
|
||||
|
||||
// Add individual mitigation contributions
|
||||
for (int i = 0; i < Math.Min(input.ActiveMitigations.Count, 5); i++)
|
||||
{
|
||||
var m = input.ActiveMitigations[i];
|
||||
components[$"mitigation_{i}_type"] = (int)m.Type;
|
||||
components[$"mitigation_{i}_effectiveness"] = m.Effectiveness;
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates all evidence normalizers to produce unified evidence-weighted score input.
|
||||
/// Orchestrates the normalization of all dimensions for a finding.
|
||||
/// </summary>
|
||||
public sealed class NormalizerAggregator : INormalizerAggregator
|
||||
{
|
||||
private readonly IEvidenceNormalizer<ReachabilityInput> _reachabilityNormalizer;
|
||||
private readonly IEvidenceNormalizer<RuntimeInput> _runtimeNormalizer;
|
||||
private readonly IEvidenceNormalizer<BackportInput> _backportNormalizer;
|
||||
private readonly IEvidenceNormalizer<ExploitInput> _exploitNormalizer;
|
||||
private readonly IEvidenceNormalizer<SourceTrustInput> _sourceTrustNormalizer;
|
||||
private readonly IEvidenceNormalizer<MitigationInput> _mitigationNormalizer;
|
||||
private readonly NormalizerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Create an aggregator with default normalizers and options.
|
||||
/// </summary>
|
||||
public NormalizerAggregator()
|
||||
: this(new NormalizerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an aggregator with specific options.
|
||||
/// </summary>
|
||||
public NormalizerAggregator(NormalizerOptions options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_reachabilityNormalizer = new ReachabilityNormalizer(_options.Reachability);
|
||||
_runtimeNormalizer = new RuntimeSignalNormalizer(_options.Runtime);
|
||||
_backportNormalizer = new BackportEvidenceNormalizer(_options.Backport);
|
||||
_exploitNormalizer = new ExploitLikelihoodNormalizer(_options.Exploit);
|
||||
_sourceTrustNormalizer = new SourceTrustNormalizer(_options.SourceTrust);
|
||||
_mitigationNormalizer = new MitigationNormalizer(_options.Mitigation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an aggregator with custom normalizers.
|
||||
/// </summary>
|
||||
public NormalizerAggregator(
|
||||
IEvidenceNormalizer<ReachabilityInput> reachabilityNormalizer,
|
||||
IEvidenceNormalizer<RuntimeInput> runtimeNormalizer,
|
||||
IEvidenceNormalizer<BackportInput> backportNormalizer,
|
||||
IEvidenceNormalizer<ExploitInput> exploitNormalizer,
|
||||
IEvidenceNormalizer<SourceTrustInput> sourceTrustNormalizer,
|
||||
IEvidenceNormalizer<MitigationInput> mitigationNormalizer,
|
||||
NormalizerOptions options)
|
||||
{
|
||||
_reachabilityNormalizer = reachabilityNormalizer ?? throw new ArgumentNullException(nameof(reachabilityNormalizer));
|
||||
_runtimeNormalizer = runtimeNormalizer ?? throw new ArgumentNullException(nameof(runtimeNormalizer));
|
||||
_backportNormalizer = backportNormalizer ?? throw new ArgumentNullException(nameof(backportNormalizer));
|
||||
_exploitNormalizer = exploitNormalizer ?? throw new ArgumentNullException(nameof(exploitNormalizer));
|
||||
_sourceTrustNormalizer = sourceTrustNormalizer ?? throw new ArgumentNullException(nameof(sourceTrustNormalizer));
|
||||
_mitigationNormalizer = mitigationNormalizer ?? throw new ArgumentNullException(nameof(mitigationNormalizer));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an aggregator with DI-provided options.
|
||||
/// </summary>
|
||||
public NormalizerAggregator(IOptionsMonitor<NormalizerOptions> optionsMonitor)
|
||||
: this(optionsMonitor?.CurrentValue ?? new NormalizerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<EvidenceWeightedScoreInput> AggregateAsync(
|
||||
string findingId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(findingId);
|
||||
|
||||
// In a real implementation, this would fetch evidence from various sources
|
||||
// For now, return a default input with neutral values
|
||||
// The actual evidence retrieval should be implemented in a higher-level service
|
||||
|
||||
var defaultEvidence = new FindingEvidence
|
||||
{
|
||||
FindingId = findingId,
|
||||
// All evidence is null - will use defaults
|
||||
};
|
||||
|
||||
var result = Aggregate(defaultEvidence);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public EvidenceWeightedScoreInput Aggregate(FindingEvidence evidence)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evidence);
|
||||
|
||||
var reachability = NormalizeReachability(evidence.Reachability);
|
||||
var runtime = NormalizeRuntime(evidence.Runtime);
|
||||
var backport = NormalizeBackport(evidence.Backport);
|
||||
var exploit = NormalizeExploit(evidence.Exploit);
|
||||
var sourceTrust = NormalizeSourceTrust(evidence.SourceTrust);
|
||||
var mitigation = NormalizeMitigation(evidence.Mitigations);
|
||||
|
||||
return new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = evidence.FindingId,
|
||||
Rch = reachability,
|
||||
Rts = runtime,
|
||||
Bkp = backport,
|
||||
Xpl = exploit,
|
||||
Src = sourceTrust,
|
||||
Mit = mitigation,
|
||||
ReachabilityDetails = evidence.Reachability,
|
||||
RuntimeDetails = evidence.Runtime,
|
||||
BackportDetails = evidence.Backport,
|
||||
ExploitDetails = evidence.Exploit,
|
||||
SourceTrustDetails = evidence.SourceTrust,
|
||||
MitigationDetails = evidence.Mitigations
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AggregationResult AggregateWithDetails(FindingEvidence evidence)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evidence);
|
||||
|
||||
var warnings = new List<string>();
|
||||
var details = new Dictionary<string, NormalizationResult>();
|
||||
|
||||
// Normalize each dimension with details
|
||||
var (reachability, reachabilityDetails) = NormalizeReachabilityWithDetails(evidence.Reachability, warnings);
|
||||
var (runtime, runtimeDetails) = NormalizeRuntimeWithDetails(evidence.Runtime, warnings);
|
||||
var (backport, backportDetails) = NormalizeBackportWithDetails(evidence.Backport, warnings);
|
||||
var (exploit, exploitDetails) = NormalizeExploitWithDetails(evidence.Exploit, warnings);
|
||||
var (sourceTrust, sourceTrustDetails) = NormalizeSourceTrustWithDetails(evidence.SourceTrust, warnings);
|
||||
var (mitigation, mitigationDetails) = NormalizeMitigationWithDetails(evidence.Mitigations, warnings);
|
||||
|
||||
// Collect all details
|
||||
if (reachabilityDetails != null)
|
||||
details["RCH"] = reachabilityDetails;
|
||||
if (runtimeDetails != null)
|
||||
details["RTS"] = runtimeDetails;
|
||||
if (backportDetails != null)
|
||||
details["BKP"] = backportDetails;
|
||||
if (exploitDetails != null)
|
||||
details["XPL"] = exploitDetails;
|
||||
if (sourceTrustDetails != null)
|
||||
details["SRC"] = sourceTrustDetails;
|
||||
if (mitigationDetails != null)
|
||||
details["MIT"] = mitigationDetails;
|
||||
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = evidence.FindingId,
|
||||
Rch = reachability,
|
||||
Rts = runtime,
|
||||
Bkp = backport,
|
||||
Xpl = exploit,
|
||||
Src = sourceTrust,
|
||||
Mit = mitigation,
|
||||
ReachabilityDetails = evidence.Reachability,
|
||||
RuntimeDetails = evidence.Runtime,
|
||||
BackportDetails = evidence.Backport,
|
||||
ExploitDetails = evidence.Exploit,
|
||||
SourceTrustDetails = evidence.SourceTrust,
|
||||
MitigationDetails = evidence.Mitigations
|
||||
};
|
||||
|
||||
return new AggregationResult
|
||||
{
|
||||
Input = input,
|
||||
Details = details,
|
||||
Warnings = warnings
|
||||
};
|
||||
}
|
||||
|
||||
#region Simple Normalization Methods
|
||||
|
||||
private double NormalizeReachability(ReachabilityInput? input)
|
||||
{
|
||||
if (input == null)
|
||||
return _options.Reachability.UnknownScore; // Default for unknown
|
||||
|
||||
return _reachabilityNormalizer.Normalize(input);
|
||||
}
|
||||
|
||||
private double NormalizeRuntime(RuntimeInput? input)
|
||||
{
|
||||
if (input == null)
|
||||
return _options.Runtime.UnknownScore; // Default for no runtime data
|
||||
|
||||
return _runtimeNormalizer.Normalize(input);
|
||||
}
|
||||
|
||||
private double NormalizeBackport(BackportInput? input)
|
||||
{
|
||||
if (input == null)
|
||||
return _options.Backport.Tier0Range.Min; // Default for no backport evidence
|
||||
|
||||
return _backportNormalizer.Normalize(input);
|
||||
}
|
||||
|
||||
private double NormalizeExploit(ExploitInput? input)
|
||||
{
|
||||
if (input == null)
|
||||
return _options.Exploit.NoEpssScore; // Default for no EPSS data
|
||||
|
||||
return _exploitNormalizer.Normalize(input);
|
||||
}
|
||||
|
||||
private double NormalizeSourceTrust(SourceTrustInput? input)
|
||||
{
|
||||
if (input == null)
|
||||
return 0.50; // Neutral trust for unknown sources
|
||||
|
||||
return _sourceTrustNormalizer.Normalize(input);
|
||||
}
|
||||
|
||||
private double NormalizeMitigation(MitigationInput? input)
|
||||
{
|
||||
if (input == null)
|
||||
return 0.0; // No mitigation by default
|
||||
|
||||
return _mitigationNormalizer.Normalize(input);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Detailed Normalization Methods
|
||||
|
||||
private (double Score, NormalizationResult? Details) NormalizeReachabilityWithDetails(
|
||||
ReachabilityInput? input, List<string> warnings)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
warnings.Add("No reachability evidence provided; using neutral score.");
|
||||
return (_options.Reachability.UnknownScore, null);
|
||||
}
|
||||
|
||||
var validationErrors = input.Validate();
|
||||
if (validationErrors.Count > 0)
|
||||
{
|
||||
warnings.AddRange(validationErrors.Select(e => $"RCH validation: {e}"));
|
||||
}
|
||||
|
||||
var details = _reachabilityNormalizer.NormalizeWithDetails(input);
|
||||
return (details.Score, details);
|
||||
}
|
||||
|
||||
private (double Score, NormalizationResult? Details) NormalizeRuntimeWithDetails(
|
||||
RuntimeInput? input, List<string> warnings)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
warnings.Add("No runtime evidence provided; using zero score.");
|
||||
return (_options.Runtime.UnknownScore, null);
|
||||
}
|
||||
|
||||
var validationErrors = input.Validate();
|
||||
if (validationErrors.Count > 0)
|
||||
{
|
||||
warnings.AddRange(validationErrors.Select(e => $"RTS validation: {e}"));
|
||||
}
|
||||
|
||||
var details = _runtimeNormalizer.NormalizeWithDetails(input);
|
||||
return (details.Score, details);
|
||||
}
|
||||
|
||||
private (double Score, NormalizationResult? Details) NormalizeBackportWithDetails(
|
||||
BackportInput? input, List<string> warnings)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
warnings.Add("No backport evidence provided; using minimal score.");
|
||||
return (_options.Backport.Tier0Range.Min, null);
|
||||
}
|
||||
|
||||
var validationErrors = input.Validate();
|
||||
if (validationErrors.Count > 0)
|
||||
{
|
||||
warnings.AddRange(validationErrors.Select(e => $"BKP validation: {e}"));
|
||||
}
|
||||
|
||||
var details = _backportNormalizer.NormalizeWithDetails(input);
|
||||
return (details.Score, details);
|
||||
}
|
||||
|
||||
private (double Score, NormalizationResult? Details) NormalizeExploitWithDetails(
|
||||
ExploitInput? input, List<string> warnings)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
warnings.Add("No exploit likelihood evidence provided; using neutral score.");
|
||||
return (_options.Exploit.NoEpssScore, null);
|
||||
}
|
||||
|
||||
var validationErrors = input.Validate();
|
||||
if (validationErrors.Count > 0)
|
||||
{
|
||||
warnings.AddRange(validationErrors.Select(e => $"XPL validation: {e}"));
|
||||
}
|
||||
|
||||
var details = _exploitNormalizer.NormalizeWithDetails(input);
|
||||
return (details.Score, details);
|
||||
}
|
||||
|
||||
private (double Score, NormalizationResult? Details) NormalizeSourceTrustWithDetails(
|
||||
SourceTrustInput? input, List<string> warnings)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
warnings.Add("No source trust evidence provided; using neutral score.");
|
||||
return (0.50, null);
|
||||
}
|
||||
|
||||
var validationErrors = input.Validate();
|
||||
if (validationErrors.Count > 0)
|
||||
{
|
||||
warnings.AddRange(validationErrors.Select(e => $"SRC validation: {e}"));
|
||||
}
|
||||
|
||||
var details = _sourceTrustNormalizer.NormalizeWithDetails(input);
|
||||
return (details.Score, details);
|
||||
}
|
||||
|
||||
private (double Score, NormalizationResult? Details) NormalizeMitigationWithDetails(
|
||||
MitigationInput? input, List<string> warnings)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
warnings.Add("No mitigation evidence provided; using zero score.");
|
||||
return (0.0, null);
|
||||
}
|
||||
|
||||
var validationErrors = input.Validate();
|
||||
if (validationErrors.Count > 0)
|
||||
{
|
||||
warnings.AddRange(validationErrors.Select(e => $"MIT validation: {e}"));
|
||||
}
|
||||
|
||||
var details = _mitigationNormalizer.NormalizeWithDetails(input);
|
||||
return (details.Score, details);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for evidence normalization.
|
||||
/// </summary>
|
||||
public sealed class NormalizerOptions
|
||||
{
|
||||
/// <summary>Configuration section name.</summary>
|
||||
public const string SectionName = "EvidenceNormalization";
|
||||
|
||||
/// <summary>Reachability normalization options.</summary>
|
||||
public ReachabilityNormalizerOptions Reachability { get; set; } = new();
|
||||
|
||||
/// <summary>Runtime signal normalization options.</summary>
|
||||
public RuntimeNormalizerOptions Runtime { get; set; } = new();
|
||||
|
||||
/// <summary>Backport evidence normalization options.</summary>
|
||||
public BackportNormalizerOptions Backport { get; set; } = new();
|
||||
|
||||
/// <summary>Exploit likelihood normalization options.</summary>
|
||||
public ExploitNormalizerOptions Exploit { get; set; } = new();
|
||||
|
||||
/// <summary>Source trust normalization options.</summary>
|
||||
public SourceTrustNormalizerOptions SourceTrust { get; set; } = new();
|
||||
|
||||
/// <summary>Mitigation normalization options.</summary>
|
||||
public MitigationNormalizerOptions Mitigation { get; set; } = new();
|
||||
|
||||
/// <summary>Default values for missing evidence.</summary>
|
||||
public DefaultValuesOptions Defaults { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reachability normalization configuration.
|
||||
/// </summary>
|
||||
public sealed class ReachabilityNormalizerOptions
|
||||
{
|
||||
/// <summary>Score for ConfirmedReachable state.</summary>
|
||||
public double ConfirmedReachableBase { get; set; } = 0.95;
|
||||
|
||||
/// <summary>Maximum bonus for confidence on ConfirmedReachable.</summary>
|
||||
public double ConfirmedReachableBonus { get; set; } = 0.05;
|
||||
|
||||
/// <summary>Base score for StaticReachable state.</summary>
|
||||
public double StaticReachableBase { get; set; } = 0.40;
|
||||
|
||||
/// <summary>Maximum bonus range for StaticReachable confidence.</summary>
|
||||
public double StaticReachableRange { get; set; } = 0.50;
|
||||
|
||||
/// <summary>Score for Unknown state.</summary>
|
||||
public double UnknownScore { get; set; } = 0.50;
|
||||
|
||||
/// <summary>Base score for StaticUnreachable state.</summary>
|
||||
public double StaticUnreachableBase { get; set; } = 0.25;
|
||||
|
||||
/// <summary>Maximum reduction for StaticUnreachable confidence.</summary>
|
||||
public double StaticUnreachableRange { get; set; } = 0.20;
|
||||
|
||||
/// <summary>Base score for ConfirmedUnreachable state.</summary>
|
||||
public double ConfirmedUnreachableBase { get; set; } = 0.05;
|
||||
|
||||
/// <summary>Maximum reduction for ConfirmedUnreachable confidence.</summary>
|
||||
public double ConfirmedUnreachableRange { get; set; } = 0.05;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime signal normalization configuration.
|
||||
/// </summary>
|
||||
public sealed class RuntimeNormalizerOptions
|
||||
{
|
||||
/// <summary>Threshold for high observation count.</summary>
|
||||
public int HighObservationThreshold { get; set; } = 10;
|
||||
|
||||
/// <summary>Threshold for medium observation count.</summary>
|
||||
public int MediumObservationThreshold { get; set; } = 5;
|
||||
|
||||
/// <summary>Base score for high observations.</summary>
|
||||
public double HighObservationScore { get; set; } = 0.90;
|
||||
|
||||
/// <summary>Base score for medium observations.</summary>
|
||||
public double MediumObservationScore { get; set; } = 0.75;
|
||||
|
||||
/// <summary>Base score for low observations.</summary>
|
||||
public double LowObservationScore { get; set; } = 0.60;
|
||||
|
||||
/// <summary>Base score for minimal observations.</summary>
|
||||
public double MinimalObservationScore { get; set; } = 0.50;
|
||||
|
||||
/// <summary>Bonus for very recent observations (< 1 hour).</summary>
|
||||
public double VeryRecentBonus { get; set; } = 0.10;
|
||||
|
||||
/// <summary>Bonus for recent observations (< 6 hours).</summary>
|
||||
public double RecentBonus { get; set; } = 0.05;
|
||||
|
||||
/// <summary>Hours threshold for very recent.</summary>
|
||||
public double VeryRecentHours { get; set; } = 1.0;
|
||||
|
||||
/// <summary>Hours threshold for recent.</summary>
|
||||
public double RecentHours { get; set; } = 6.0;
|
||||
|
||||
/// <summary>Score for Unknown posture (no runtime data).</summary>
|
||||
public double UnknownScore { get; set; } = 0.0;
|
||||
|
||||
/// <summary>Score for Contradicts posture.</summary>
|
||||
public double ContradictsScore { get; set; } = 0.10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backport evidence normalization configuration.
|
||||
/// </summary>
|
||||
public sealed class BackportNormalizerOptions
|
||||
{
|
||||
/// <summary>Score range for Tier 0 (None): [min, max].</summary>
|
||||
public (double Min, double Max) Tier0Range { get; set; } = (0.00, 0.10);
|
||||
|
||||
/// <summary>Score range for Tier 1 (Heuristic): [min, max].</summary>
|
||||
public (double Min, double Max) Tier1Range { get; set; } = (0.45, 0.60);
|
||||
|
||||
/// <summary>Score range for Tier 2 (PatchSignature): [min, max].</summary>
|
||||
public (double Min, double Max) Tier2Range { get; set; } = (0.70, 0.85);
|
||||
|
||||
/// <summary>Score range for Tier 3 (BinaryDiff): [min, max].</summary>
|
||||
public (double Min, double Max) Tier3Range { get; set; } = (0.80, 0.92);
|
||||
|
||||
/// <summary>Score range for Tier 4 (VendorVex): [min, max].</summary>
|
||||
public (double Min, double Max) Tier4Range { get; set; } = (0.85, 0.95);
|
||||
|
||||
/// <summary>Score range for Tier 5 (SignedProof): [min, max].</summary>
|
||||
public (double Min, double Max) Tier5Range { get; set; } = (0.90, 1.00);
|
||||
|
||||
/// <summary>Bonus when multiple evidence tiers are present.</summary>
|
||||
public double CombinationBonus { get; set; } = 0.05;
|
||||
|
||||
/// <summary>Score for no evidence.</summary>
|
||||
public double NoEvidenceScore { get; set; } = 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exploit likelihood normalization configuration.
|
||||
/// </summary>
|
||||
public sealed class ExploitNormalizerOptions
|
||||
{
|
||||
/// <summary>Floor score when CVE is in KEV catalog.</summary>
|
||||
public double KevFloor { get; set; } = 0.40;
|
||||
|
||||
/// <summary>EPSS percentile threshold for top 1%.</summary>
|
||||
public double Top1PercentThreshold { get; set; } = 0.99;
|
||||
|
||||
/// <summary>EPSS percentile threshold for top 5%.</summary>
|
||||
public double Top5PercentThreshold { get; set; } = 0.95;
|
||||
|
||||
/// <summary>EPSS percentile threshold for top 25%.</summary>
|
||||
public double Top25PercentThreshold { get; set; } = 0.75;
|
||||
|
||||
/// <summary>Score range for top 1% percentile.</summary>
|
||||
public (double Low, double High) Top1PercentRange { get; set; } = (0.90, 1.00);
|
||||
|
||||
/// <summary>Score range for top 5% percentile.</summary>
|
||||
public (double Low, double High) Top5PercentRange { get; set; } = (0.70, 0.89);
|
||||
|
||||
/// <summary>Score range for top 25% percentile.</summary>
|
||||
public (double Low, double High) Top25PercentRange { get; set; } = (0.40, 0.69);
|
||||
|
||||
/// <summary>Score range for below top 25% percentile.</summary>
|
||||
public (double Low, double High) LowerPercentRange { get; set; } = (0.20, 0.39);
|
||||
|
||||
/// <summary>Score when no EPSS data available.</summary>
|
||||
public double NoEpssScore { get; set; } = 0.30;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source trust normalization configuration.
|
||||
/// </summary>
|
||||
public sealed class SourceTrustNormalizerOptions
|
||||
{
|
||||
/// <summary>Multiplier for Vendor issuer type.</summary>
|
||||
public double VendorMultiplier { get; set; } = 1.0;
|
||||
|
||||
/// <summary>Multiplier for Distribution issuer type.</summary>
|
||||
public double DistributionMultiplier { get; set; } = 0.85;
|
||||
|
||||
/// <summary>Multiplier for TrustedThirdParty issuer type.</summary>
|
||||
public double TrustedThirdPartyMultiplier { get; set; } = 0.80;
|
||||
|
||||
/// <summary>Multiplier for Community issuer type.</summary>
|
||||
public double CommunityMultiplier { get; set; } = 0.60;
|
||||
|
||||
/// <summary>Multiplier for Unknown issuer type.</summary>
|
||||
public double UnknownMultiplier { get; set; } = 0.30;
|
||||
|
||||
/// <summary>Bonus multiplier for signed sources.</summary>
|
||||
public double SignedBonus { get; set; } = 0.10;
|
||||
|
||||
/// <summary>Weight for provenance in trust calculation.</summary>
|
||||
public double ProvenanceWeight { get; set; } = 0.40;
|
||||
|
||||
/// <summary>Weight for coverage in trust calculation.</summary>
|
||||
public double CoverageWeight { get; set; } = 0.35;
|
||||
|
||||
/// <summary>Weight for replayability in trust calculation.</summary>
|
||||
public double ReplayabilityWeight { get; set; } = 0.25;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mitigation normalization configuration.
|
||||
/// </summary>
|
||||
public sealed class MitigationNormalizerOptions
|
||||
{
|
||||
/// <summary>Effectiveness for FeatureFlag mitigation.</summary>
|
||||
public (double Low, double High) FeatureFlagEffectiveness { get; set; } = (0.20, 0.40);
|
||||
|
||||
/// <summary>Effectiveness for AuthRequired mitigation.</summary>
|
||||
public (double Low, double High) AuthRequiredEffectiveness { get; set; } = (0.10, 0.20);
|
||||
|
||||
/// <summary>Effectiveness for AdminOnly mitigation.</summary>
|
||||
public (double Low, double High) AdminOnlyEffectiveness { get; set; } = (0.15, 0.25);
|
||||
|
||||
/// <summary>Effectiveness for NonDefaultConfig mitigation.</summary>
|
||||
public (double Low, double High) NonDefaultConfigEffectiveness { get; set; } = (0.15, 0.30);
|
||||
|
||||
/// <summary>Effectiveness for SeccompProfile mitigation.</summary>
|
||||
public (double Low, double High) SeccompEffectiveness { get; set; } = (0.10, 0.25);
|
||||
|
||||
/// <summary>Effectiveness for MandatoryAccessControl mitigation.</summary>
|
||||
public (double Low, double High) MacEffectiveness { get; set; } = (0.10, 0.20);
|
||||
|
||||
/// <summary>Effectiveness for NetworkIsolation mitigation.</summary>
|
||||
public (double Low, double High) NetworkIsolationEffectiveness { get; set; } = (0.05, 0.15);
|
||||
|
||||
/// <summary>Effectiveness for ReadOnlyFilesystem mitigation.</summary>
|
||||
public (double Low, double High) ReadOnlyFsEffectiveness { get; set; } = (0.05, 0.10);
|
||||
|
||||
/// <summary>Maximum total mitigation score (cap).</summary>
|
||||
public double MaxTotalMitigation { get; set; } = 1.0;
|
||||
|
||||
/// <summary>Bonus for runtime-verified mitigations.</summary>
|
||||
public double VerificationBonus { get; set; } = 0.05;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default values for missing evidence.
|
||||
/// </summary>
|
||||
public sealed class DefaultValuesOptions
|
||||
{
|
||||
/// <summary>Default RCH when no reachability evidence.</summary>
|
||||
public double Rch { get; set; } = 0.50;
|
||||
|
||||
/// <summary>Default RTS when no runtime evidence.</summary>
|
||||
public double Rts { get; set; } = 0.0;
|
||||
|
||||
/// <summary>Default BKP when no backport evidence.</summary>
|
||||
public double Bkp { get; set; } = 0.0;
|
||||
|
||||
/// <summary>Default XPL when no exploit evidence.</summary>
|
||||
public double Xpl { get; set; } = 0.30;
|
||||
|
||||
/// <summary>Default SRC when no source trust evidence.</summary>
|
||||
public double Src { get; set; } = 0.30;
|
||||
|
||||
/// <summary>Default MIT when no mitigation evidence.</summary>
|
||||
public double Mit { get; set; } = 0.0;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes reachability evidence to a [0, 1] RCH score.
|
||||
/// Higher scores indicate greater reachability risk.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Maps ReachabilityState + confidence to normalized scores:
|
||||
/// - LiveExploitPath: 0.95-1.00 (highest risk)
|
||||
/// - DynamicReachable: 0.90-0.98 (confirmed reachable via runtime)
|
||||
/// - StaticReachable: 0.40-0.90 (depends on confidence)
|
||||
/// - PotentiallyReachable: 0.30-0.60 (conservative analysis)
|
||||
/// - Unknown: 0.50 (neutral)
|
||||
/// - NotReachable: 0.00-0.15 (depends on confidence)
|
||||
/// </remarks>
|
||||
public sealed class ReachabilityNormalizer : IEvidenceNormalizer<ReachabilityInput>
|
||||
{
|
||||
private readonly ReachabilityNormalizerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with default options.
|
||||
/// </summary>
|
||||
public ReachabilityNormalizer()
|
||||
: this(new ReachabilityNormalizerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with specific options.
|
||||
/// </summary>
|
||||
public ReachabilityNormalizer(ReachabilityNormalizerOptions options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with DI-provided options.
|
||||
/// </summary>
|
||||
public ReachabilityNormalizer(IOptionsMonitor<NormalizerOptions> optionsMonitor)
|
||||
: this(optionsMonitor?.CurrentValue?.Reachability ?? new ReachabilityNormalizerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Dimension => "RCH";
|
||||
|
||||
/// <inheritdoc />
|
||||
public double Normalize(ReachabilityInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
return CalculateScore(input);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NormalizationResult NormalizeWithDetails(ReachabilityInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
var score = CalculateScore(input);
|
||||
var explanation = GenerateExplanation(input, score);
|
||||
var components = BuildComponents(input);
|
||||
|
||||
return NormalizationResult.WithComponents(score, Dimension, explanation, components);
|
||||
}
|
||||
|
||||
private double CalculateScore(ReachabilityInput input)
|
||||
{
|
||||
var baseScore = GetBaseScore(input.State);
|
||||
var confidenceModifier = CalculateConfidenceModifier(input.State, input.Confidence);
|
||||
var analysisBonus = CalculateAnalysisBonus(input);
|
||||
var hopPenalty = CalculateHopPenalty(input.HopCount, input.State);
|
||||
|
||||
var rawScore = baseScore + confidenceModifier + analysisBonus - hopPenalty;
|
||||
|
||||
return Math.Clamp(rawScore, 0.0, 1.0);
|
||||
}
|
||||
|
||||
private double GetBaseScore(ReachabilityState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
ReachabilityState.LiveExploitPath => _options.ConfirmedReachableBase,
|
||||
ReachabilityState.DynamicReachable => _options.ConfirmedReachableBase - 0.05, // 0.90
|
||||
ReachabilityState.StaticReachable => _options.StaticReachableBase,
|
||||
ReachabilityState.PotentiallyReachable => 0.35, // Conservative base
|
||||
ReachabilityState.Unknown => _options.UnknownScore,
|
||||
ReachabilityState.NotReachable => _options.ConfirmedUnreachableBase,
|
||||
_ => _options.UnknownScore
|
||||
};
|
||||
}
|
||||
|
||||
private double CalculateConfidenceModifier(ReachabilityState state, double confidence)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
// For reachable states: higher confidence = higher risk
|
||||
ReachabilityState.LiveExploitPath => confidence * _options.ConfirmedReachableBonus,
|
||||
ReachabilityState.DynamicReachable => confidence * 0.08, // Up to 0.98
|
||||
ReachabilityState.StaticReachable => confidence * _options.StaticReachableRange,
|
||||
ReachabilityState.PotentiallyReachable => confidence * 0.25, // Up to 0.60
|
||||
|
||||
// For unreachable states: higher confidence = lower risk (subtract more)
|
||||
ReachabilityState.NotReachable => -(confidence * _options.ConfirmedUnreachableRange),
|
||||
|
||||
// Unknown: no confidence modifier
|
||||
ReachabilityState.Unknown => 0.0,
|
||||
|
||||
_ => 0.0
|
||||
};
|
||||
}
|
||||
|
||||
private double CalculateAnalysisBonus(ReachabilityInput input)
|
||||
{
|
||||
// Better analysis methods get a small bonus (more trustworthy results)
|
||||
var bonus = 0.0;
|
||||
|
||||
if (input.HasInterproceduralFlow)
|
||||
bonus += 0.02;
|
||||
|
||||
if (input.HasTaintTracking)
|
||||
bonus += 0.02;
|
||||
|
||||
if (input.HasDataFlowSensitivity)
|
||||
bonus += 0.01;
|
||||
|
||||
// Only apply bonus for positive reachability findings
|
||||
return input.State is ReachabilityState.StaticReachable
|
||||
or ReachabilityState.DynamicReachable
|
||||
or ReachabilityState.LiveExploitPath
|
||||
? bonus
|
||||
: 0.0;
|
||||
}
|
||||
|
||||
private double CalculateHopPenalty(int hopCount, ReachabilityState state)
|
||||
{
|
||||
// Only penalize high hop counts for static analysis
|
||||
if (state != ReachabilityState.StaticReachable)
|
||||
return 0.0;
|
||||
|
||||
// More hops = less confident in reachability
|
||||
// 0 hops = 0 penalty, 10+ hops = max 0.10 penalty
|
||||
return hopCount switch
|
||||
{
|
||||
0 => 0.0,
|
||||
1 => 0.01,
|
||||
2 => 0.02,
|
||||
3 => 0.03,
|
||||
<= 5 => 0.05,
|
||||
<= 10 => 0.08,
|
||||
_ => 0.10
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<string, double> BuildComponents(ReachabilityInput input)
|
||||
{
|
||||
var components = new Dictionary<string, double>
|
||||
{
|
||||
["state"] = (double)input.State,
|
||||
["confidence"] = input.Confidence,
|
||||
["hop_count"] = input.HopCount,
|
||||
["base_score"] = GetBaseScore(input.State),
|
||||
["confidence_modifier"] = CalculateConfidenceModifier(input.State, input.Confidence),
|
||||
["analysis_bonus"] = CalculateAnalysisBonus(input),
|
||||
["hop_penalty"] = CalculateHopPenalty(input.HopCount, input.State),
|
||||
["interprocedural_flow"] = input.HasInterproceduralFlow ? 1.0 : 0.0,
|
||||
["taint_tracking"] = input.HasTaintTracking ? 1.0 : 0.0,
|
||||
["data_flow_sensitivity"] = input.HasDataFlowSensitivity ? 1.0 : 0.0
|
||||
};
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
private string GenerateExplanation(ReachabilityInput input, double score)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var stateDesc = input.State switch
|
||||
{
|
||||
ReachabilityState.LiveExploitPath => "Live exploit path observed",
|
||||
ReachabilityState.DynamicReachable => "Dynamically confirmed reachable",
|
||||
ReachabilityState.StaticReachable => "Statically determined reachable",
|
||||
ReachabilityState.PotentiallyReachable => "Potentially reachable (conservative)",
|
||||
ReachabilityState.Unknown => "Reachability unknown",
|
||||
ReachabilityState.NotReachable => "Confirmed not reachable",
|
||||
_ => $"Unknown state ({input.State})"
|
||||
};
|
||||
|
||||
sb.Append($"{stateDesc} with {input.Confidence:P0} confidence");
|
||||
|
||||
if (input.HopCount > 0)
|
||||
sb.Append($", {input.HopCount} hop(s) from entry point");
|
||||
|
||||
var analysisFlags = new List<string>();
|
||||
if (input.HasInterproceduralFlow) analysisFlags.Add("interprocedural");
|
||||
if (input.HasTaintTracking) analysisFlags.Add("taint-tracked");
|
||||
if (input.HasDataFlowSensitivity) analysisFlags.Add("data-flow");
|
||||
|
||||
if (analysisFlags.Count > 0)
|
||||
sb.Append($" ({string.Join(", ", analysisFlags)} analysis)");
|
||||
|
||||
if (!string.IsNullOrEmpty(input.AnalysisMethod))
|
||||
sb.Append($" via {input.AnalysisMethod}");
|
||||
|
||||
if (!string.IsNullOrEmpty(input.EvidenceSource))
|
||||
sb.Append($" from {input.EvidenceSource}");
|
||||
|
||||
sb.Append($" → RCH={score:F2}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes runtime signal evidence to a [0, 1] RTS score.
|
||||
/// Higher scores indicate stronger runtime evidence that the code path is exercised.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Maps RuntimePosture + observation count + recency to normalized scores:
|
||||
/// - FullInstrumentation with high observations: 0.90-1.00
|
||||
/// - EbpfDeep with medium observations: 0.75-0.90
|
||||
/// - ActiveTracing with some observations: 0.60-0.75
|
||||
/// - Passive with minimal observations: 0.50-0.60
|
||||
/// - None/Unknown: 0.00
|
||||
/// </remarks>
|
||||
public sealed class RuntimeSignalNormalizer : IEvidenceNormalizer<RuntimeInput>
|
||||
{
|
||||
private readonly RuntimeNormalizerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with default options.
|
||||
/// </summary>
|
||||
public RuntimeSignalNormalizer()
|
||||
: this(new RuntimeNormalizerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with specific options.
|
||||
/// </summary>
|
||||
public RuntimeSignalNormalizer(RuntimeNormalizerOptions options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with DI-provided options.
|
||||
/// </summary>
|
||||
public RuntimeSignalNormalizer(IOptionsMonitor<NormalizerOptions> optionsMonitor)
|
||||
: this(optionsMonitor?.CurrentValue?.Runtime ?? new RuntimeNormalizerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Dimension => "RTS";
|
||||
|
||||
/// <inheritdoc />
|
||||
public double Normalize(RuntimeInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
return CalculateScore(input);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NormalizationResult NormalizeWithDetails(RuntimeInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
var score = CalculateScore(input);
|
||||
var explanation = GenerateExplanation(input, score);
|
||||
var components = BuildComponents(input);
|
||||
|
||||
return NormalizationResult.WithComponents(score, Dimension, explanation, components);
|
||||
}
|
||||
|
||||
private double CalculateScore(RuntimeInput input)
|
||||
{
|
||||
// No runtime observation = no evidence
|
||||
if (input.Posture == RuntimePosture.None || input.ObservationCount == 0)
|
||||
return _options.UnknownScore;
|
||||
|
||||
var observationScore = CalculateObservationScore(input.ObservationCount);
|
||||
var postureMultiplier = GetPostureMultiplier(input.Posture);
|
||||
var recencyBonus = CalculateRecencyBonus(input);
|
||||
var qualityBonus = CalculateQualityBonus(input);
|
||||
|
||||
var rawScore = observationScore * postureMultiplier + recencyBonus + qualityBonus;
|
||||
|
||||
return Math.Clamp(rawScore, 0.0, 1.0);
|
||||
}
|
||||
|
||||
private double CalculateObservationScore(int observationCount)
|
||||
{
|
||||
return observationCount switch
|
||||
{
|
||||
>= 10 when observationCount >= _options.HighObservationThreshold => _options.HighObservationScore,
|
||||
>= 5 when observationCount >= _options.MediumObservationThreshold => _options.MediumObservationScore,
|
||||
>= 1 => _options.LowObservationScore,
|
||||
_ => _options.MinimalObservationScore
|
||||
};
|
||||
}
|
||||
|
||||
private double GetPostureMultiplier(RuntimePosture posture)
|
||||
{
|
||||
// Higher quality observation methods get a multiplier
|
||||
return posture switch
|
||||
{
|
||||
RuntimePosture.FullInstrumentation => 1.10, // Best quality, 10% bonus
|
||||
RuntimePosture.EbpfDeep => 1.05, // eBPF = excellent
|
||||
RuntimePosture.ActiveTracing => 1.00, // Baseline
|
||||
RuntimePosture.Passive => 0.90, // Passive = less confidence
|
||||
RuntimePosture.None => 0.0,
|
||||
_ => 0.90
|
||||
};
|
||||
}
|
||||
|
||||
private double CalculateRecencyBonus(RuntimeInput input)
|
||||
{
|
||||
// Use RecencyFactor directly if available
|
||||
if (input.RecencyFactor > 0.0)
|
||||
{
|
||||
// High recency factor (close to 1.0) = recent observations
|
||||
return input.RecencyFactor switch
|
||||
{
|
||||
>= 0.9 => _options.VeryRecentBonus, // Very recent
|
||||
>= 0.5 => _options.RecentBonus, // Moderately recent
|
||||
_ => 0.0 // Old observations
|
||||
};
|
||||
}
|
||||
|
||||
// Fall back to LastObservation timestamp if available
|
||||
if (input.LastObservation.HasValue)
|
||||
{
|
||||
var hoursSince = (DateTimeOffset.UtcNow - input.LastObservation.Value).TotalHours;
|
||||
return hoursSince switch
|
||||
{
|
||||
< 1.0 when hoursSince < _options.VeryRecentHours => _options.VeryRecentBonus,
|
||||
< 6.0 when hoursSince < _options.RecentHours => _options.RecentBonus,
|
||||
_ => 0.0
|
||||
};
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
private double CalculateQualityBonus(RuntimeInput input)
|
||||
{
|
||||
var bonus = 0.0;
|
||||
|
||||
// Direct path observation is strong evidence
|
||||
if (input.DirectPathObserved)
|
||||
bonus += 0.05;
|
||||
|
||||
// Production traffic is more meaningful
|
||||
if (input.IsProductionTraffic)
|
||||
bonus += 0.03;
|
||||
|
||||
return bonus;
|
||||
}
|
||||
|
||||
private Dictionary<string, double> BuildComponents(RuntimeInput input)
|
||||
{
|
||||
var components = new Dictionary<string, double>
|
||||
{
|
||||
["posture"] = (double)input.Posture,
|
||||
["observation_count"] = input.ObservationCount,
|
||||
["recency_factor"] = input.RecencyFactor,
|
||||
["observation_score"] = CalculateObservationScore(input.ObservationCount),
|
||||
["posture_multiplier"] = GetPostureMultiplier(input.Posture),
|
||||
["recency_bonus"] = CalculateRecencyBonus(input),
|
||||
["quality_bonus"] = CalculateQualityBonus(input),
|
||||
["direct_path_observed"] = input.DirectPathObserved ? 1.0 : 0.0,
|
||||
["is_production_traffic"] = input.IsProductionTraffic ? 1.0 : 0.0
|
||||
};
|
||||
|
||||
if (input.SessionDigests?.Count > 0)
|
||||
components["session_count"] = input.SessionDigests.Count;
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
private string GenerateExplanation(RuntimeInput input, double score)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (input.Posture == RuntimePosture.None || input.ObservationCount == 0)
|
||||
{
|
||||
sb.Append("No runtime observations available");
|
||||
sb.Append($" → RTS={score:F2}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
var postureDesc = input.Posture switch
|
||||
{
|
||||
RuntimePosture.FullInstrumentation => "full instrumentation",
|
||||
RuntimePosture.EbpfDeep => "eBPF deep observation",
|
||||
RuntimePosture.ActiveTracing => "active tracing",
|
||||
RuntimePosture.Passive => "passive monitoring",
|
||||
_ => $"unknown posture ({input.Posture})"
|
||||
};
|
||||
|
||||
sb.Append($"{input.ObservationCount} observation(s) via {postureDesc}");
|
||||
|
||||
if (input.DirectPathObserved)
|
||||
sb.Append(", vulnerable path directly observed");
|
||||
|
||||
if (input.IsProductionTraffic)
|
||||
sb.Append(" in production traffic");
|
||||
|
||||
// Recency description
|
||||
var recencyDesc = input.RecencyFactor switch
|
||||
{
|
||||
>= 0.9 => " (very recent)",
|
||||
>= 0.5 => " (moderately recent)",
|
||||
> 0 => " (aging)",
|
||||
_ => ""
|
||||
};
|
||||
sb.Append(recencyDesc);
|
||||
|
||||
if (!string.IsNullOrEmpty(input.EvidenceSource))
|
||||
sb.Append($" from {input.EvidenceSource}");
|
||||
|
||||
sb.Append($" → RTS={score:F2}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright © 2025 StellaOps
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes source trust evidence to a [0, 1] SRC score.
|
||||
/// Higher scores indicate higher trust in the advisory/VEX source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Combines issuer type multiplier with trust vector components:
|
||||
/// - GovernmentAgency/CNA: highest multiplier
|
||||
/// - Vendor: high trust (1.0)
|
||||
/// - Distribution: good trust (0.85)
|
||||
/// - Upstream: good trust (0.80)
|
||||
/// - SecurityResearcher: moderate trust (0.70)
|
||||
/// - Community: lower trust (0.60)
|
||||
/// - Unknown: minimal trust (0.30)
|
||||
///
|
||||
/// Trust vector weighted: provenance (40%) + coverage (35%) + replayability (25%)
|
||||
/// Bonuses for cryptographic attestation and corroborating sources
|
||||
/// </remarks>
|
||||
public sealed class SourceTrustNormalizer : IEvidenceNormalizer<SourceTrustInput>
|
||||
{
|
||||
private readonly SourceTrustNormalizerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with default options.
|
||||
/// </summary>
|
||||
public SourceTrustNormalizer()
|
||||
: this(new SourceTrustNormalizerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with specific options.
|
||||
/// </summary>
|
||||
public SourceTrustNormalizer(SourceTrustNormalizerOptions options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a normalizer with DI-provided options.
|
||||
/// </summary>
|
||||
public SourceTrustNormalizer(IOptionsMonitor<NormalizerOptions> optionsMonitor)
|
||||
: this(optionsMonitor?.CurrentValue?.SourceTrust ?? new SourceTrustNormalizerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Dimension => "SRC";
|
||||
|
||||
/// <inheritdoc />
|
||||
public double Normalize(SourceTrustInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
return CalculateScore(input);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NormalizationResult NormalizeWithDetails(SourceTrustInput input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
var score = CalculateScore(input);
|
||||
var explanation = GenerateExplanation(input, score);
|
||||
var components = BuildComponents(input);
|
||||
|
||||
return NormalizationResult.WithComponents(score, Dimension, explanation, components);
|
||||
}
|
||||
|
||||
private double CalculateScore(SourceTrustInput input)
|
||||
{
|
||||
var issuerMultiplier = GetIssuerMultiplier(input.IssuerType);
|
||||
var trustVectorScore = CalculateTrustVectorScore(input);
|
||||
var attestationBonus = CalculateAttestationBonus(input);
|
||||
var corroborationBonus = CalculateCorroborationBonus(input);
|
||||
var historicalBonus = CalculateHistoricalBonus(input);
|
||||
|
||||
var rawScore = trustVectorScore * issuerMultiplier + attestationBonus + corroborationBonus + historicalBonus;
|
||||
|
||||
return Math.Clamp(rawScore, 0.0, 1.0);
|
||||
}
|
||||
|
||||
private double GetIssuerMultiplier(IssuerType issuerType)
|
||||
{
|
||||
return issuerType switch
|
||||
{
|
||||
IssuerType.GovernmentAgency => 1.05, // CISA, etc.
|
||||
IssuerType.Cna => 1.02, // CVE Numbering Authority
|
||||
IssuerType.Vendor => _options.VendorMultiplier,
|
||||
IssuerType.Distribution => _options.DistributionMultiplier,
|
||||
IssuerType.Upstream => 0.82, // Upstream maintainers
|
||||
IssuerType.SecurityResearcher => 0.75,
|
||||
IssuerType.Community => _options.CommunityMultiplier,
|
||||
IssuerType.Unknown => _options.UnknownMultiplier,
|
||||
_ => _options.UnknownMultiplier
|
||||
};
|
||||
}
|
||||
|
||||
private double CalculateTrustVectorScore(SourceTrustInput input)
|
||||
{
|
||||
// Weighted combination of trust vector components
|
||||
return _options.ProvenanceWeight * input.ProvenanceTrust +
|
||||
_options.CoverageWeight * input.CoverageCompleteness +
|
||||
_options.ReplayabilityWeight * input.Replayability;
|
||||
}
|
||||
|
||||
private double CalculateAttestationBonus(SourceTrustInput input)
|
||||
{
|
||||
var bonus = 0.0;
|
||||
|
||||
// Cryptographic attestation (DSSE/in-toto) is a strong signal
|
||||
if (input.IsCryptographicallyAttested)
|
||||
bonus += _options.SignedBonus;
|
||||
|
||||
// Independent verification adds confidence
|
||||
if (input.IndependentlyVerified)
|
||||
bonus += 0.05;
|
||||
|
||||
return bonus;
|
||||
}
|
||||
|
||||
private double CalculateCorroborationBonus(SourceTrustInput input)
|
||||
{
|
||||
// Multiple independent sources increase trust
|
||||
return input.CorroboratingSourceCount switch
|
||||
{
|
||||
0 => 0.0,
|
||||
1 => 0.02,
|
||||
2 => 0.04,
|
||||
>= 3 => 0.06,
|
||||
_ => 0.0
|
||||
};
|
||||
}
|
||||
|
||||
private double CalculateHistoricalBonus(SourceTrustInput input)
|
||||
{
|
||||
// Good track record earns a small bonus
|
||||
if (!input.HistoricalAccuracy.HasValue)
|
||||
return 0.0;
|
||||
|
||||
return input.HistoricalAccuracy.Value switch
|
||||
{
|
||||
>= 0.95 => 0.05, // Excellent track record
|
||||
>= 0.85 => 0.03, // Good track record
|
||||
>= 0.70 => 0.01, // Acceptable track record
|
||||
_ => -0.02 // Poor track record = penalty
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<string, double> BuildComponents(SourceTrustInput input)
|
||||
{
|
||||
var components = new Dictionary<string, double>
|
||||
{
|
||||
["issuer_type"] = (double)input.IssuerType,
|
||||
["issuer_multiplier"] = GetIssuerMultiplier(input.IssuerType),
|
||||
["provenance_trust"] = input.ProvenanceTrust,
|
||||
["coverage_completeness"] = input.CoverageCompleteness,
|
||||
["replayability"] = input.Replayability,
|
||||
["trust_vector_score"] = CalculateTrustVectorScore(input),
|
||||
["attestation_bonus"] = CalculateAttestationBonus(input),
|
||||
["corroboration_bonus"] = CalculateCorroborationBonus(input),
|
||||
["historical_bonus"] = CalculateHistoricalBonus(input),
|
||||
["cryptographically_attested"] = input.IsCryptographicallyAttested ? 1.0 : 0.0,
|
||||
["independently_verified"] = input.IndependentlyVerified ? 1.0 : 0.0,
|
||||
["corroborating_sources"] = input.CorroboratingSourceCount
|
||||
};
|
||||
|
||||
if (input.HistoricalAccuracy.HasValue)
|
||||
components["historical_accuracy"] = input.HistoricalAccuracy.Value;
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
private string GenerateExplanation(SourceTrustInput input, double score)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var issuerDesc = input.IssuerType switch
|
||||
{
|
||||
IssuerType.GovernmentAgency => "government agency",
|
||||
IssuerType.Cna => "CVE Numbering Authority",
|
||||
IssuerType.Vendor => "software vendor",
|
||||
IssuerType.Distribution => "distribution maintainer",
|
||||
IssuerType.Upstream => "upstream project",
|
||||
IssuerType.SecurityResearcher => "security researcher",
|
||||
IssuerType.Community => "community source",
|
||||
IssuerType.Unknown => "unknown source",
|
||||
_ => $"unknown type ({input.IssuerType})"
|
||||
};
|
||||
|
||||
sb.Append($"From {issuerDesc}");
|
||||
|
||||
if (!string.IsNullOrEmpty(input.IssuerId))
|
||||
sb.Append($" ({input.IssuerId})");
|
||||
|
||||
// Trust vector summary
|
||||
var trustVectorScore = CalculateTrustVectorScore(input);
|
||||
sb.Append($" with trust vector {trustVectorScore:P0}");
|
||||
|
||||
// Attestation
|
||||
if (input.IsCryptographicallyAttested)
|
||||
sb.Append(", cryptographically attested");
|
||||
|
||||
if (input.IndependentlyVerified)
|
||||
sb.Append(", independently verified");
|
||||
|
||||
// Corroboration
|
||||
if (input.CorroboratingSourceCount > 0)
|
||||
sb.Append($", {input.CorroboratingSourceCount} corroborating source(s)");
|
||||
|
||||
// Historical accuracy
|
||||
if (input.HistoricalAccuracy.HasValue)
|
||||
sb.Append($", {input.HistoricalAccuracy.Value:P0} historical accuracy");
|
||||
|
||||
sb.Append($" → SRC={score:F2}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user