feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Assumptions;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single assumption made during vulnerability analysis.
|
||||
/// Assumptions capture the conditions under which a finding is considered valid.
|
||||
/// </summary>
|
||||
/// <param name="Category">The category of assumption (compiler flag, runtime config, etc.)</param>
|
||||
/// <param name="Key">The specific assumption key (e.g., "-fstack-protector", "DEBUG_MODE")</param>
|
||||
/// <param name="AssumedValue">The value assumed during analysis</param>
|
||||
/// <param name="ObservedValue">The actual observed value, if available</param>
|
||||
/// <param name="Source">How the assumption was derived</param>
|
||||
/// <param name="Confidence">The confidence level in this assumption</param>
|
||||
public sealed record Assumption(
|
||||
AssumptionCategory Category,
|
||||
string Key,
|
||||
string AssumedValue,
|
||||
string? ObservedValue,
|
||||
AssumptionSource Source,
|
||||
ConfidenceLevel Confidence
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if the observed value matches the assumed value.
|
||||
/// </summary>
|
||||
public bool IsValidated => ObservedValue is not null &&
|
||||
string.Equals(AssumedValue, ObservedValue, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the observed value contradicts the assumed value.
|
||||
/// </summary>
|
||||
public bool IsContradicted => ObservedValue is not null &&
|
||||
!string.Equals(AssumedValue, ObservedValue, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Categories of assumptions that affect vulnerability exploitability.
|
||||
/// </summary>
|
||||
public enum AssumptionCategory
|
||||
{
|
||||
/// <summary>Compiler flags like -fstack-protector, -D_FORTIFY_SOURCE</summary>
|
||||
CompilerFlag,
|
||||
|
||||
/// <summary>Environment variables, config files, runtime settings</summary>
|
||||
RuntimeConfig,
|
||||
|
||||
/// <summary>Feature flags, build variants, conditional compilation</summary>
|
||||
FeatureGate,
|
||||
|
||||
/// <summary>LD_PRELOAD, RPATH, symbol versioning, loader behavior</summary>
|
||||
LoaderBehavior,
|
||||
|
||||
/// <summary>Port bindings, firewall rules, network exposure</summary>
|
||||
NetworkExposure,
|
||||
|
||||
/// <summary>Capabilities, seccomp profiles, AppArmor/SELinux policies</summary>
|
||||
ProcessPrivilege,
|
||||
|
||||
/// <summary>Memory layout assumptions (ASLR, PIE)</summary>
|
||||
MemoryProtection,
|
||||
|
||||
/// <summary>System call availability and filtering</summary>
|
||||
SyscallAvailability
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How an assumption was derived.
|
||||
/// </summary>
|
||||
public enum AssumptionSource
|
||||
{
|
||||
/// <summary>Default assumption when no evidence available</summary>
|
||||
Default,
|
||||
|
||||
/// <summary>Inferred from static analysis of binaries/code</summary>
|
||||
StaticAnalysis,
|
||||
|
||||
/// <summary>Observed from runtime telemetry</summary>
|
||||
RuntimeObservation,
|
||||
|
||||
/// <summary>Derived from container/image manifest</summary>
|
||||
ImageManifest,
|
||||
|
||||
/// <summary>Provided by user configuration</summary>
|
||||
UserProvided,
|
||||
|
||||
/// <summary>Extracted from Dockerfile or build configuration</summary>
|
||||
BuildConfig
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confidence level in an assumption.
|
||||
/// </summary>
|
||||
public enum ConfidenceLevel
|
||||
{
|
||||
/// <summary>No evidence, using defaults</summary>
|
||||
Low = 1,
|
||||
|
||||
/// <summary>Some indirect evidence</summary>
|
||||
Medium = 2,
|
||||
|
||||
/// <summary>Strong evidence from static analysis</summary>
|
||||
High = 3,
|
||||
|
||||
/// <summary>Direct runtime observation</summary>
|
||||
Verified = 4
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Assumptions;
|
||||
|
||||
/// <summary>
|
||||
/// A collection of assumptions associated with a finding or analysis context.
|
||||
/// Provides methods for querying and validating assumptions.
|
||||
/// </summary>
|
||||
public sealed record AssumptionSet
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier for this assumption set.
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The assumptions in this set, keyed by category and key.
|
||||
/// </summary>
|
||||
public ImmutableArray<Assumption> Assumptions { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// When this assumption set was created.
|
||||
/// </summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional context identifier (e.g., finding ID, image digest).
|
||||
/// </summary>
|
||||
public string? ContextId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all assumptions of a specific category.
|
||||
/// </summary>
|
||||
public IEnumerable<Assumption> GetByCategory(AssumptionCategory category) =>
|
||||
Assumptions.Where(a => a.Category == category);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific assumption by category and key.
|
||||
/// </summary>
|
||||
public Assumption? Get(AssumptionCategory category, string key) =>
|
||||
Assumptions.FirstOrDefault(a => a.Category == category &&
|
||||
string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the overall confidence level (minimum of all assumptions).
|
||||
/// </summary>
|
||||
public ConfidenceLevel OverallConfidence =>
|
||||
Assumptions.Length == 0
|
||||
? ConfidenceLevel.Low
|
||||
: Assumptions.Min(a => a.Confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the count of validated assumptions.
|
||||
/// </summary>
|
||||
public int ValidatedCount => Assumptions.Count(a => a.IsValidated);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the count of contradicted assumptions.
|
||||
/// </summary>
|
||||
public int ContradictedCount => Assumptions.Count(a => a.IsContradicted);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if any assumption is contradicted by observed evidence.
|
||||
/// </summary>
|
||||
public bool HasContradictions => Assumptions.Any(a => a.IsContradicted);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the validation ratio (validated / total with observations).
|
||||
/// </summary>
|
||||
public double ValidationRatio
|
||||
{
|
||||
get
|
||||
{
|
||||
var withObservations = Assumptions.Count(a => a.ObservedValue is not null);
|
||||
return withObservations == 0 ? 0.0 : (double)ValidatedCount / withObservations;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AssumptionSet with an additional assumption.
|
||||
/// </summary>
|
||||
public AssumptionSet WithAssumption(Assumption assumption) =>
|
||||
this with { Assumptions = Assumptions.Add(assumption) };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AssumptionSet with updated observation for an assumption.
|
||||
/// </summary>
|
||||
public AssumptionSet WithObservation(AssumptionCategory category, string key, string observedValue)
|
||||
{
|
||||
var index = Assumptions.FindIndex(a =>
|
||||
a.Category == category &&
|
||||
string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (index < 0)
|
||||
return this;
|
||||
|
||||
var updated = Assumptions[index] with { ObservedValue = observedValue };
|
||||
return this with { Assumptions = Assumptions.SetItem(index, updated) };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for ImmutableArray to support FindIndex.
|
||||
/// </summary>
|
||||
internal static class ImmutableArrayExtensions
|
||||
{
|
||||
public static int FindIndex<T>(this ImmutableArray<T> array, Func<T, bool> predicate)
|
||||
{
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
{
|
||||
if (predicate(array[i]))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Assumptions;
|
||||
|
||||
/// <summary>
|
||||
/// Collects assumptions from various sources during vulnerability analysis.
|
||||
/// </summary>
|
||||
public interface IAssumptionCollector
|
||||
{
|
||||
/// <summary>
|
||||
/// Records an assumption made during analysis.
|
||||
/// </summary>
|
||||
/// <param name="category">The category of assumption</param>
|
||||
/// <param name="key">The assumption key</param>
|
||||
/// <param name="assumedValue">The assumed value</param>
|
||||
/// <param name="source">How the assumption was derived</param>
|
||||
/// <param name="confidence">Confidence level</param>
|
||||
void Record(
|
||||
AssumptionCategory category,
|
||||
string key,
|
||||
string assumedValue,
|
||||
AssumptionSource source,
|
||||
ConfidenceLevel confidence = ConfidenceLevel.Low);
|
||||
|
||||
/// <summary>
|
||||
/// Records an observation that validates or contradicts an assumption.
|
||||
/// </summary>
|
||||
/// <param name="category">The category of assumption</param>
|
||||
/// <param name="key">The assumption key</param>
|
||||
/// <param name="observedValue">The observed value</param>
|
||||
void RecordObservation(AssumptionCategory category, string key, string observedValue);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the final assumption set from collected assumptions.
|
||||
/// </summary>
|
||||
/// <param name="contextId">Optional context identifier</param>
|
||||
/// <returns>The completed assumption set</returns>
|
||||
AssumptionSet Build(string? contextId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all collected assumptions.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IAssumptionCollector"/>.
|
||||
/// </summary>
|
||||
public sealed class AssumptionCollector : IAssumptionCollector
|
||||
{
|
||||
private readonly Dictionary<(AssumptionCategory, string), Assumption> _assumptions = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Record(
|
||||
AssumptionCategory category,
|
||||
string key,
|
||||
string assumedValue,
|
||||
AssumptionSource source,
|
||||
ConfidenceLevel confidence = ConfidenceLevel.Low)
|
||||
{
|
||||
var normalizedKey = key.ToLowerInvariant();
|
||||
var existing = _assumptions.GetValueOrDefault((category, normalizedKey));
|
||||
|
||||
// Keep assumption with higher confidence
|
||||
if (existing is null || confidence > existing.Confidence)
|
||||
{
|
||||
_assumptions[(category, normalizedKey)] = new Assumption(
|
||||
category,
|
||||
key,
|
||||
assumedValue,
|
||||
existing?.ObservedValue,
|
||||
source,
|
||||
confidence);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RecordObservation(AssumptionCategory category, string key, string observedValue)
|
||||
{
|
||||
var normalizedKey = key.ToLowerInvariant();
|
||||
if (_assumptions.TryGetValue((category, normalizedKey), out var existing))
|
||||
{
|
||||
_assumptions[(category, normalizedKey)] = existing with
|
||||
{
|
||||
ObservedValue = observedValue,
|
||||
Confidence = ConfidenceLevel.Verified
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Record observation even without prior assumption
|
||||
_assumptions[(category, normalizedKey)] = new Assumption(
|
||||
category,
|
||||
key,
|
||||
observedValue, // Use observed as assumed when no prior assumption
|
||||
observedValue,
|
||||
AssumptionSource.RuntimeObservation,
|
||||
ConfidenceLevel.Verified);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AssumptionSet Build(string? contextId = null)
|
||||
{
|
||||
return new AssumptionSet
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Assumptions = [.. _assumptions.Values],
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
ContextId = contextId
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear() => _assumptions.Clear();
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Explainability.Falsifiability;
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Confidence;
|
||||
|
||||
/// <summary>
|
||||
/// Evidence factors that contribute to confidence scoring.
|
||||
/// </summary>
|
||||
public sealed record EvidenceFactors
|
||||
{
|
||||
/// <summary>Assumption set for the finding</summary>
|
||||
public AssumptionSet? Assumptions { get; init; }
|
||||
|
||||
/// <summary>Falsifiability criteria for the finding</summary>
|
||||
public FalsifiabilityCriteria? Falsifiability { get; init; }
|
||||
|
||||
/// <summary>Whether static reachability analysis was performed</summary>
|
||||
public bool HasStaticReachability { get; init; }
|
||||
|
||||
/// <summary>Whether runtime observations are available</summary>
|
||||
public bool HasRuntimeObservations { get; init; }
|
||||
|
||||
/// <summary>Whether SBOM lineage is tracked</summary>
|
||||
public bool HasSbomLineage { get; init; }
|
||||
|
||||
/// <summary>Number of corroborating vulnerability sources</summary>
|
||||
public int SourceCount { get; init; } = 1;
|
||||
|
||||
/// <summary>Whether VEX assessment is available</summary>
|
||||
public bool HasVexAssessment { get; init; }
|
||||
|
||||
/// <summary>Whether exploit code is known to exist</summary>
|
||||
public bool HasKnownExploit { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of evidence density scoring.
|
||||
/// </summary>
|
||||
public sealed record EvidenceDensityScore
|
||||
{
|
||||
/// <summary>Overall confidence score (0.0 to 1.0)</summary>
|
||||
public required double Score { get; init; }
|
||||
|
||||
/// <summary>Confidence level derived from score</summary>
|
||||
public required ConfidenceLevel Level { get; init; }
|
||||
|
||||
/// <summary>Individual factor contributions</summary>
|
||||
public required IReadOnlyDictionary<string, double> FactorBreakdown { get; init; }
|
||||
|
||||
/// <summary>Human-readable explanation</summary>
|
||||
public required string Explanation { get; init; }
|
||||
|
||||
/// <summary>Recommendations to improve confidence</summary>
|
||||
public required IReadOnlyList<string> ImprovementRecommendations { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates confidence scores based on evidence density.
|
||||
/// More evidence types and validation = higher confidence in the finding accuracy.
|
||||
/// </summary>
|
||||
public interface IEvidenceDensityScorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates an evidence density score for a finding.
|
||||
/// </summary>
|
||||
EvidenceDensityScore Calculate(EvidenceFactors factors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IEvidenceDensityScorer"/>.
|
||||
/// </summary>
|
||||
public sealed class EvidenceDensityScorer : IEvidenceDensityScorer
|
||||
{
|
||||
// Weights for different evidence factors
|
||||
private const double WeightAssumptionValidation = 0.20;
|
||||
private const double WeightFalsifiabilityEval = 0.15;
|
||||
private const double WeightStaticReachability = 0.15;
|
||||
private const double WeightRuntimeObservation = 0.20;
|
||||
private const double WeightSbomLineage = 0.05;
|
||||
private const double WeightMultipleSources = 0.10;
|
||||
private const double WeightVexAssessment = 0.10;
|
||||
private const double WeightKnownExploit = 0.05;
|
||||
|
||||
/// <inheritdoc />
|
||||
public EvidenceDensityScore Calculate(EvidenceFactors factors)
|
||||
{
|
||||
var breakdown = new Dictionary<string, double>();
|
||||
var recommendations = new List<string>();
|
||||
|
||||
// Factor 1: Assumption validation ratio
|
||||
double assumptionScore = 0.0;
|
||||
if (factors.Assumptions is not null && factors.Assumptions.Assumptions.Length > 0)
|
||||
{
|
||||
assumptionScore = factors.Assumptions.ValidationRatio * WeightAssumptionValidation;
|
||||
if (factors.Assumptions.ValidationRatio < 0.5)
|
||||
{
|
||||
recommendations.Add("Validate more assumptions with runtime observations or static analysis");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
recommendations.Add("Add assumption tracking to understand analysis context");
|
||||
}
|
||||
breakdown["assumption_validation"] = assumptionScore;
|
||||
|
||||
// Factor 2: Falsifiability evaluation
|
||||
double falsifiabilityScore = 0.0;
|
||||
if (factors.Falsifiability is not null)
|
||||
{
|
||||
var evaluatedCount = factors.Falsifiability.Criteria
|
||||
.Count(c => c.Status is CriterionStatus.Satisfied or CriterionStatus.NotSatisfied);
|
||||
var totalCount = factors.Falsifiability.Criteria.Length;
|
||||
|
||||
if (totalCount > 0)
|
||||
{
|
||||
falsifiabilityScore = ((double)evaluatedCount / totalCount) * WeightFalsifiabilityEval;
|
||||
}
|
||||
|
||||
if (factors.Falsifiability.Status == FalsifiabilityStatus.PartiallyEvaluated)
|
||||
{
|
||||
recommendations.Add("Complete evaluation of pending falsifiability criteria");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
recommendations.Add("Generate falsifiability criteria to understand what would disprove this finding");
|
||||
}
|
||||
breakdown["falsifiability_evaluation"] = falsifiabilityScore;
|
||||
|
||||
// Factor 3: Static reachability
|
||||
double staticReachScore = factors.HasStaticReachability ? WeightStaticReachability : 0.0;
|
||||
if (!factors.HasStaticReachability)
|
||||
{
|
||||
recommendations.Add("Perform static reachability analysis to verify code paths");
|
||||
}
|
||||
breakdown["static_reachability"] = staticReachScore;
|
||||
|
||||
// Factor 4: Runtime observations
|
||||
double runtimeScore = factors.HasRuntimeObservations ? WeightRuntimeObservation : 0.0;
|
||||
if (!factors.HasRuntimeObservations)
|
||||
{
|
||||
recommendations.Add("Collect runtime observations to verify actual behavior");
|
||||
}
|
||||
breakdown["runtime_observations"] = runtimeScore;
|
||||
|
||||
// Factor 5: SBOM lineage
|
||||
double lineageScore = factors.HasSbomLineage ? WeightSbomLineage : 0.0;
|
||||
if (!factors.HasSbomLineage)
|
||||
{
|
||||
recommendations.Add("Track SBOM lineage for reproducibility");
|
||||
}
|
||||
breakdown["sbom_lineage"] = lineageScore;
|
||||
|
||||
// Factor 6: Multiple sources
|
||||
double sourceScore = Math.Min(factors.SourceCount, 3) / 3.0 * WeightMultipleSources;
|
||||
if (factors.SourceCount < 2)
|
||||
{
|
||||
recommendations.Add("Cross-reference with additional vulnerability databases");
|
||||
}
|
||||
breakdown["multiple_sources"] = sourceScore;
|
||||
|
||||
// Factor 7: VEX assessment
|
||||
double vexScore = factors.HasVexAssessment ? WeightVexAssessment : 0.0;
|
||||
if (!factors.HasVexAssessment)
|
||||
{
|
||||
recommendations.Add("Obtain vendor VEX assessment for authoritative status");
|
||||
}
|
||||
breakdown["vex_assessment"] = vexScore;
|
||||
|
||||
// Factor 8: Known exploit
|
||||
double exploitScore = factors.HasKnownExploit ? WeightKnownExploit : 0.0;
|
||||
// Not having a known exploit is not a negative - don't recommend
|
||||
breakdown["known_exploit"] = exploitScore;
|
||||
|
||||
// Calculate total score
|
||||
double totalScore = breakdown.Values.Sum();
|
||||
var level = ScoreToLevel(totalScore);
|
||||
var explanation = GenerateExplanation(totalScore, level, breakdown);
|
||||
|
||||
return new EvidenceDensityScore
|
||||
{
|
||||
Score = Math.Round(totalScore, 3),
|
||||
Level = level,
|
||||
FactorBreakdown = breakdown,
|
||||
Explanation = explanation,
|
||||
ImprovementRecommendations = recommendations
|
||||
};
|
||||
}
|
||||
|
||||
private static ConfidenceLevel ScoreToLevel(double score) => score switch
|
||||
{
|
||||
>= 0.75 => ConfidenceLevel.Verified,
|
||||
>= 0.50 => ConfidenceLevel.High,
|
||||
>= 0.25 => ConfidenceLevel.Medium,
|
||||
_ => ConfidenceLevel.Low
|
||||
};
|
||||
|
||||
private static string GenerateExplanation(
|
||||
double score,
|
||||
ConfidenceLevel level,
|
||||
Dictionary<string, double> breakdown)
|
||||
{
|
||||
var topFactors = breakdown
|
||||
.Where(kv => kv.Value > 0)
|
||||
.OrderByDescending(kv => kv.Value)
|
||||
.Take(3)
|
||||
.Select(kv => kv.Key.Replace("_", " "));
|
||||
|
||||
var factorList = string.Join(", ", topFactors);
|
||||
|
||||
return level switch
|
||||
{
|
||||
ConfidenceLevel.Verified =>
|
||||
$"Very high confidence ({score:P0}). Strong evidence from: {factorList}.",
|
||||
ConfidenceLevel.High =>
|
||||
$"High confidence ({score:P0}). Good evidence from: {factorList}.",
|
||||
ConfidenceLevel.Medium =>
|
||||
$"Medium confidence ({score:P0}). Some evidence from: {factorList}.",
|
||||
_ =>
|
||||
$"Low confidence ({score:P0}). Limited evidence available. Consider gathering more data."
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Explainability.Confidence;
|
||||
using StellaOps.Scanner.Explainability.Falsifiability;
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Dsse;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes explainability data to DSSE predicate format.
|
||||
/// </summary>
|
||||
public interface IExplainabilityPredicateSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// The predicate type URI for finding explainability predicates.
|
||||
/// </summary>
|
||||
const string PredicateType = "https://stella-ops.org/predicates/finding-explainability/v2";
|
||||
|
||||
/// <summary>
|
||||
/// Converts a risk report to DSSE predicate format.
|
||||
/// </summary>
|
||||
byte[] Serialize(RiskReport report);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a risk report to a predicate object that can be embedded in DSSE.
|
||||
/// </summary>
|
||||
FindingExplainabilityPredicate ToPredicate(RiskReport report);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IExplainabilityPredicateSerializer"/>.
|
||||
/// </summary>
|
||||
public sealed class ExplainabilityPredicateSerializer : IExplainabilityPredicateSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] Serialize(RiskReport report)
|
||||
{
|
||||
var predicate = ToPredicate(report);
|
||||
return JsonSerializer.SerializeToUtf8Bytes(predicate, SerializerOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public FindingExplainabilityPredicate ToPredicate(RiskReport report)
|
||||
{
|
||||
return new FindingExplainabilityPredicate
|
||||
{
|
||||
FindingId = report.FindingId,
|
||||
VulnerabilityId = report.VulnerabilityId,
|
||||
PackageName = report.PackageName,
|
||||
PackageVersion = report.PackageVersion,
|
||||
GeneratedAt = report.GeneratedAt,
|
||||
EngineVersion = report.EngineVersion,
|
||||
Explanation = report.Explanation,
|
||||
DetailedNarrative = report.DetailedNarrative,
|
||||
Assumptions = report.Assumptions is not null ? ToPredicateAssumptions(report.Assumptions) : null,
|
||||
Falsifiability = report.Falsifiability is not null ? ToPredicateFalsifiability(report.Falsifiability) : null,
|
||||
ConfidenceScore = report.ConfidenceScore is not null ? ToPredicateConfidence(report.ConfidenceScore) : null,
|
||||
RecommendedActions = report.RecommendedActions
|
||||
.Select(a => new PredicateRecommendedAction
|
||||
{
|
||||
Priority = a.Priority,
|
||||
Action = a.Action,
|
||||
Rationale = a.Rationale,
|
||||
Effort = a.Effort.ToString()
|
||||
})
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static PredicateAssumptionSet ToPredicateAssumptions(AssumptionSet assumptions)
|
||||
{
|
||||
return new PredicateAssumptionSet
|
||||
{
|
||||
Id = assumptions.Id,
|
||||
ContextId = assumptions.ContextId,
|
||||
CreatedAt = assumptions.CreatedAt,
|
||||
Assumptions = assumptions.Assumptions
|
||||
.Select(a => new PredicateAssumption
|
||||
{
|
||||
Category = a.Category.ToString(),
|
||||
Key = a.Key,
|
||||
AssumedValue = a.AssumedValue,
|
||||
ObservedValue = a.ObservedValue,
|
||||
Source = a.Source.ToString(),
|
||||
Confidence = a.Confidence.ToString()
|
||||
})
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static PredicateFalsifiabilityCriteria ToPredicateFalsifiability(FalsifiabilityCriteria falsifiability)
|
||||
{
|
||||
return new PredicateFalsifiabilityCriteria
|
||||
{
|
||||
Id = falsifiability.Id,
|
||||
FindingId = falsifiability.FindingId,
|
||||
GeneratedAt = falsifiability.GeneratedAt,
|
||||
Status = falsifiability.Status.ToString(),
|
||||
Summary = falsifiability.Summary,
|
||||
Criteria = falsifiability.Criteria
|
||||
.Select(c => new PredicateFalsificationCriterion
|
||||
{
|
||||
Type = c.Type.ToString(),
|
||||
Description = c.Description,
|
||||
CheckExpression = c.CheckExpression,
|
||||
Evidence = c.Evidence,
|
||||
Status = c.Status.ToString()
|
||||
})
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static PredicateEvidenceDensityScore ToPredicateConfidence(EvidenceDensityScore score)
|
||||
{
|
||||
return new PredicateEvidenceDensityScore
|
||||
{
|
||||
Score = score.Score,
|
||||
Level = score.Level.ToString(),
|
||||
FactorBreakdown = score.FactorBreakdown.ToDictionary(kv => kv.Key, kv => kv.Value),
|
||||
Explanation = score.Explanation,
|
||||
ImprovementRecommendations = score.ImprovementRecommendations.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#region Predicate DTOs
|
||||
|
||||
/// <summary>
|
||||
/// DSSE predicate DTO for finding explainability.
|
||||
/// </summary>
|
||||
public sealed class FindingExplainabilityPredicate
|
||||
{
|
||||
public required string FindingId { get; init; }
|
||||
public required string VulnerabilityId { get; init; }
|
||||
public required string PackageName { get; init; }
|
||||
public required string PackageVersion { get; init; }
|
||||
public string? Severity { get; init; }
|
||||
public string? FixedVersion { get; init; }
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
public required string EngineVersion { get; init; }
|
||||
public string? Explanation { get; init; }
|
||||
public string? DetailedNarrative { get; init; }
|
||||
public PredicateAssumptionSet? Assumptions { get; init; }
|
||||
public PredicateFalsifiabilityCriteria? Falsifiability { get; init; }
|
||||
public PredicateEvidenceDensityScore? ConfidenceScore { get; init; }
|
||||
public PredicateRecommendedAction[]? RecommendedActions { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicate DTO for assumption set.
|
||||
/// </summary>
|
||||
public sealed class PredicateAssumptionSet
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public string? ContextId { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public required PredicateAssumption[] Assumptions { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicate DTO for individual assumption.
|
||||
/// </summary>
|
||||
public sealed class PredicateAssumption
|
||||
{
|
||||
public required string Category { get; init; }
|
||||
public required string Key { get; init; }
|
||||
public required string AssumedValue { get; init; }
|
||||
public string? ObservedValue { get; init; }
|
||||
public required string Source { get; init; }
|
||||
public required string Confidence { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicate DTO for falsifiability criteria.
|
||||
/// </summary>
|
||||
public sealed class PredicateFalsifiabilityCriteria
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string FindingId { get; init; }
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public string? Summary { get; init; }
|
||||
public required PredicateFalsificationCriterion[] Criteria { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicate DTO for individual falsification criterion.
|
||||
/// </summary>
|
||||
public sealed class PredicateFalsificationCriterion
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public string? CheckExpression { get; init; }
|
||||
public string? Evidence { get; init; }
|
||||
public required string Status { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicate DTO for evidence density score.
|
||||
/// </summary>
|
||||
public sealed class PredicateEvidenceDensityScore
|
||||
{
|
||||
public required double Score { get; init; }
|
||||
public required string Level { get; init; }
|
||||
public Dictionary<string, double>? FactorBreakdown { get; init; }
|
||||
public string? Explanation { get; init; }
|
||||
public string[]? ImprovementRecommendations { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicate DTO for recommended action.
|
||||
/// </summary>
|
||||
public sealed class PredicateRecommendedAction
|
||||
{
|
||||
public required int Priority { get; init; }
|
||||
public required string Action { get; init; }
|
||||
public required string Rationale { get; init; }
|
||||
public required string Effort { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Falsifiability;
|
||||
|
||||
/// <summary>
|
||||
/// Represents criteria that would falsify (disprove) a vulnerability finding.
|
||||
/// Answers the question: "What would prove this finding wrong?"
|
||||
/// </summary>
|
||||
public sealed record FalsifiabilityCriteria
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this criteria set.
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The finding ID these criteria apply to.
|
||||
/// </summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Individual criteria that would disprove the finding.
|
||||
/// </summary>
|
||||
public ImmutableArray<FalsificationCriterion> Criteria { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Overall falsifiability status.
|
||||
/// </summary>
|
||||
public FalsifiabilityStatus Status { get; init; } = FalsifiabilityStatus.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable summary of what would disprove this finding.
|
||||
/// </summary>
|
||||
public string? Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When these criteria were generated.
|
||||
/// </summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single criterion that would falsify a finding.
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of falsification check</param>
|
||||
/// <param name="Description">Human-readable description of the criterion</param>
|
||||
/// <param name="CheckExpression">Machine-evaluable expression (e.g., CEL, Rego)</param>
|
||||
/// <param name="Evidence">Evidence that supports or refutes this criterion</param>
|
||||
/// <param name="Status">Current evaluation status</param>
|
||||
public sealed record FalsificationCriterion(
|
||||
FalsificationType Type,
|
||||
string Description,
|
||||
string? CheckExpression,
|
||||
string? Evidence,
|
||||
CriterionStatus Status
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Types of falsification criteria.
|
||||
/// </summary>
|
||||
public enum FalsificationType
|
||||
{
|
||||
/// <summary>Package is not actually installed</summary>
|
||||
PackageNotPresent,
|
||||
|
||||
/// <summary>Vulnerable version is not the installed version</summary>
|
||||
VersionMismatch,
|
||||
|
||||
/// <summary>Vulnerable code path is not reachable</summary>
|
||||
CodeUnreachable,
|
||||
|
||||
/// <summary>Required feature/function is disabled</summary>
|
||||
FeatureDisabled,
|
||||
|
||||
/// <summary>Mitigation is in place (ASLR, stack canaries, etc.)</summary>
|
||||
MitigationPresent,
|
||||
|
||||
/// <summary>Network exposure required but not present</summary>
|
||||
NoNetworkExposure,
|
||||
|
||||
/// <summary>Required privileges not available</summary>
|
||||
InsufficientPrivileges,
|
||||
|
||||
/// <summary>Vulnerability is already patched</summary>
|
||||
PatchApplied,
|
||||
|
||||
/// <summary>Configuration prevents exploitation</summary>
|
||||
ConfigurationPrevents,
|
||||
|
||||
/// <summary>Runtime environment prevents exploitation</summary>
|
||||
RuntimePrevents
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of a falsification criterion evaluation.
|
||||
/// </summary>
|
||||
public enum CriterionStatus
|
||||
{
|
||||
/// <summary>Not yet evaluated</summary>
|
||||
Pending,
|
||||
|
||||
/// <summary>Criterion is satisfied (finding is falsified)</summary>
|
||||
Satisfied,
|
||||
|
||||
/// <summary>Criterion is not satisfied (finding stands)</summary>
|
||||
NotSatisfied,
|
||||
|
||||
/// <summary>Could not be evaluated (insufficient data)</summary>
|
||||
Inconclusive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overall falsifiability status.
|
||||
/// </summary>
|
||||
public enum FalsifiabilityStatus
|
||||
{
|
||||
/// <summary>Status not determined</summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>Finding has been falsified (at least one criterion satisfied)</summary>
|
||||
Falsified,
|
||||
|
||||
/// <summary>Finding stands (all criteria not satisfied)</summary>
|
||||
NotFalsified,
|
||||
|
||||
/// <summary>Some criteria inconclusive</summary>
|
||||
PartiallyEvaluated
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
|
||||
namespace StellaOps.Scanner.Explainability.Falsifiability;
|
||||
|
||||
/// <summary>
|
||||
/// Input data for generating falsifiability criteria.
|
||||
/// </summary>
|
||||
public sealed record FalsifiabilityInput
|
||||
{
|
||||
/// <summary>The finding ID</summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>The CVE or vulnerability ID</summary>
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>Package name</summary>
|
||||
public required string PackageName { get; init; }
|
||||
|
||||
/// <summary>Installed version</summary>
|
||||
public required string InstalledVersion { get; init; }
|
||||
|
||||
/// <summary>Vulnerable version range</summary>
|
||||
public string? VulnerableRange { get; init; }
|
||||
|
||||
/// <summary>Fixed version, if available</summary>
|
||||
public string? FixedVersion { get; init; }
|
||||
|
||||
/// <summary>Assumptions made during analysis</summary>
|
||||
public AssumptionSet? Assumptions { get; init; }
|
||||
|
||||
/// <summary>Whether reachability analysis was performed</summary>
|
||||
public bool HasReachabilityData { get; init; }
|
||||
|
||||
/// <summary>Whether code is reachable (if analysis was performed)</summary>
|
||||
public bool? IsReachable { get; init; }
|
||||
|
||||
/// <summary>Known mitigations in place</summary>
|
||||
public ImmutableArray<string> Mitigations { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates falsifiability criteria for vulnerability findings.
|
||||
/// </summary>
|
||||
public interface IFalsifiabilityGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates falsifiability criteria for a finding.
|
||||
/// </summary>
|
||||
FalsifiabilityCriteria Generate(FalsifiabilityInput input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IFalsifiabilityGenerator"/>.
|
||||
/// </summary>
|
||||
public sealed class FalsifiabilityGenerator : IFalsifiabilityGenerator
|
||||
{
|
||||
private readonly ILogger<FalsifiabilityGenerator> _logger;
|
||||
|
||||
public FalsifiabilityGenerator(ILogger<FalsifiabilityGenerator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public FalsifiabilityCriteria Generate(FalsifiabilityInput input)
|
||||
{
|
||||
_logger.LogDebug("Generating falsifiability criteria for finding {FindingId}", input.FindingId);
|
||||
|
||||
var criteria = new List<FalsificationCriterion>();
|
||||
|
||||
// Criterion 1: Package presence
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.PackageNotPresent,
|
||||
$"Package '{input.PackageName}' is not actually installed or is a false positive from manifest parsing",
|
||||
$"package.exists(\"{input.PackageName}\") == false",
|
||||
null,
|
||||
CriterionStatus.Pending));
|
||||
|
||||
// Criterion 2: Version mismatch
|
||||
if (input.VulnerableRange is not null)
|
||||
{
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.VersionMismatch,
|
||||
$"Installed version '{input.InstalledVersion}' is not within vulnerable range '{input.VulnerableRange}'",
|
||||
$"version.inRange(\"{input.InstalledVersion}\", \"{input.VulnerableRange}\") == false",
|
||||
null,
|
||||
CriterionStatus.Pending));
|
||||
}
|
||||
|
||||
// Criterion 3: Patch applied
|
||||
if (input.FixedVersion is not null)
|
||||
{
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.PatchApplied,
|
||||
$"Version '{input.InstalledVersion}' is at or above fixed version '{input.FixedVersion}'",
|
||||
$"version.gte(\"{input.InstalledVersion}\", \"{input.FixedVersion}\")",
|
||||
null,
|
||||
CriterionStatus.Pending));
|
||||
}
|
||||
|
||||
// Criterion 4: Code unreachable
|
||||
if (input.HasReachabilityData)
|
||||
{
|
||||
var reachabilityStatus = input.IsReachable switch
|
||||
{
|
||||
false => CriterionStatus.Satisfied,
|
||||
true => CriterionStatus.NotSatisfied,
|
||||
null => CriterionStatus.Inconclusive
|
||||
};
|
||||
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.CodeUnreachable,
|
||||
"Vulnerable code path is not reachable from application entry points",
|
||||
"reachability.isReachable() == false",
|
||||
input.IsReachable.HasValue ? $"Reachability analysis: {(input.IsReachable.Value ? "reachable" : "unreachable")}" : null,
|
||||
reachabilityStatus));
|
||||
}
|
||||
|
||||
// Criterion 5: Mitigations
|
||||
foreach (var mitigation in input.Mitigations)
|
||||
{
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
FalsificationType.MitigationPresent,
|
||||
$"Mitigation '{mitigation}' prevents exploitation",
|
||||
null,
|
||||
$"Mitigation present: {mitigation}",
|
||||
CriterionStatus.Satisfied));
|
||||
}
|
||||
|
||||
// Criterion 6: Assumption-based criteria
|
||||
if (input.Assumptions is not null)
|
||||
{
|
||||
foreach (var assumption in input.Assumptions.Assumptions.Where(a => a.IsContradicted))
|
||||
{
|
||||
var type = assumption.Category switch
|
||||
{
|
||||
AssumptionCategory.NetworkExposure => FalsificationType.NoNetworkExposure,
|
||||
AssumptionCategory.ProcessPrivilege => FalsificationType.InsufficientPrivileges,
|
||||
AssumptionCategory.FeatureGate => FalsificationType.FeatureDisabled,
|
||||
AssumptionCategory.RuntimeConfig => FalsificationType.ConfigurationPrevents,
|
||||
AssumptionCategory.CompilerFlag => FalsificationType.MitigationPresent,
|
||||
_ => FalsificationType.RuntimePrevents
|
||||
};
|
||||
|
||||
criteria.Add(new FalsificationCriterion(
|
||||
type,
|
||||
$"Assumption '{assumption.Key}' was contradicted: assumed '{assumption.AssumedValue}', observed '{assumption.ObservedValue}'",
|
||||
null,
|
||||
$"Observed: {assumption.ObservedValue}",
|
||||
CriterionStatus.Satisfied));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
var status = DetermineOverallStatus(criteria);
|
||||
|
||||
// Generate summary
|
||||
var summary = GenerateSummary(input, criteria, status);
|
||||
|
||||
return new FalsifiabilityCriteria
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
FindingId = input.FindingId,
|
||||
Criteria = [.. criteria],
|
||||
Status = status,
|
||||
Summary = summary,
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static FalsifiabilityStatus DetermineOverallStatus(List<FalsificationCriterion> criteria)
|
||||
{
|
||||
if (criteria.Count == 0)
|
||||
return FalsifiabilityStatus.Unknown;
|
||||
|
||||
if (criteria.Any(c => c.Status == CriterionStatus.Satisfied))
|
||||
return FalsifiabilityStatus.Falsified;
|
||||
|
||||
if (criteria.All(c => c.Status == CriterionStatus.NotSatisfied))
|
||||
return FalsifiabilityStatus.NotFalsified;
|
||||
|
||||
if (criteria.Any(c => c.Status is CriterionStatus.Pending or CriterionStatus.Inconclusive))
|
||||
return FalsifiabilityStatus.PartiallyEvaluated;
|
||||
|
||||
return FalsifiabilityStatus.Unknown;
|
||||
}
|
||||
|
||||
private static string GenerateSummary(
|
||||
FalsifiabilityInput input,
|
||||
List<FalsificationCriterion> criteria,
|
||||
FalsifiabilityStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
FalsifiabilityStatus.Falsified =>
|
||||
$"Finding {input.FindingId} can be falsified. " +
|
||||
$"Criteria satisfied: {string.Join(", ", criteria.Where(c => c.Status == CriterionStatus.Satisfied).Select(c => c.Type))}",
|
||||
|
||||
FalsifiabilityStatus.NotFalsified =>
|
||||
$"Finding {input.FindingId} has not been falsified. All {criteria.Count} criteria evaluated negative.",
|
||||
|
||||
FalsifiabilityStatus.PartiallyEvaluated =>
|
||||
$"Finding {input.FindingId} is partially evaluated. " +
|
||||
$"{criteria.Count(c => c.Status == CriterionStatus.Pending)} pending, " +
|
||||
$"{criteria.Count(c => c.Status == CriterionStatus.Inconclusive)} inconclusive.",
|
||||
|
||||
_ => $"Finding {input.FindingId} falsifiability status unknown."
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Explainability.Confidence;
|
||||
using StellaOps.Scanner.Explainability.Falsifiability;
|
||||
|
||||
namespace StellaOps.Scanner.Explainability;
|
||||
|
||||
/// <summary>
|
||||
/// A comprehensive risk report that includes all explainability data for a finding.
|
||||
/// </summary>
|
||||
public sealed record RiskReport
|
||||
{
|
||||
/// <summary>Unique report identifier</summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>The finding this report explains</summary>
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
/// <summary>The vulnerability ID (CVE, GHSA, etc.)</summary>
|
||||
public required string VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>Package name</summary>
|
||||
public required string PackageName { get; init; }
|
||||
|
||||
/// <summary>Package version</summary>
|
||||
public required string PackageVersion { get; init; }
|
||||
|
||||
/// <summary>Assumptions made during analysis</summary>
|
||||
public AssumptionSet? Assumptions { get; init; }
|
||||
|
||||
/// <summary>Falsifiability criteria and status</summary>
|
||||
public FalsifiabilityCriteria? Falsifiability { get; init; }
|
||||
|
||||
/// <summary>Evidence density confidence score</summary>
|
||||
public EvidenceDensityScore? ConfidenceScore { get; init; }
|
||||
|
||||
/// <summary>Human-readable explanation of the finding</summary>
|
||||
public required string Explanation { get; init; }
|
||||
|
||||
/// <summary>Detailed narrative explaining the risk</summary>
|
||||
public string? DetailedNarrative { get; init; }
|
||||
|
||||
/// <summary>Recommended actions</summary>
|
||||
public ImmutableArray<RecommendedAction> RecommendedActions { get; init; } = [];
|
||||
|
||||
/// <summary>When this report was generated</summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
/// <summary>Version of the explainability engine</summary>
|
||||
public required string EngineVersion { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A recommended action to address a finding.
|
||||
/// </summary>
|
||||
/// <param name="Priority">Action priority (1 = highest)</param>
|
||||
/// <param name="Action">The recommended action</param>
|
||||
/// <param name="Rationale">Why this action is recommended</param>
|
||||
/// <param name="Effort">Estimated effort level</param>
|
||||
public sealed record RecommendedAction(
|
||||
int Priority,
|
||||
string Action,
|
||||
string Rationale,
|
||||
EffortLevel Effort
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Effort level for a recommended action.
|
||||
/// </summary>
|
||||
public enum EffortLevel
|
||||
{
|
||||
/// <summary>Quick configuration change or update</summary>
|
||||
Low,
|
||||
|
||||
/// <summary>Moderate code changes or testing required</summary>
|
||||
Medium,
|
||||
|
||||
/// <summary>Significant refactoring or architectural changes</summary>
|
||||
High
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates comprehensive risk reports.
|
||||
/// </summary>
|
||||
public interface IRiskReportGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a risk report for a finding.
|
||||
/// </summary>
|
||||
RiskReport Generate(RiskReportInput input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input for generating a risk report.
|
||||
/// </summary>
|
||||
public sealed record RiskReportInput
|
||||
{
|
||||
public required string FindingId { get; init; }
|
||||
public required string VulnerabilityId { get; init; }
|
||||
public required string PackageName { get; init; }
|
||||
public required string PackageVersion { get; init; }
|
||||
public string? Severity { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? FixedVersion { get; init; }
|
||||
public AssumptionSet? Assumptions { get; init; }
|
||||
public FalsifiabilityCriteria? Falsifiability { get; init; }
|
||||
public EvidenceFactors? EvidenceFactors { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IRiskReportGenerator"/>.
|
||||
/// </summary>
|
||||
public sealed class RiskReportGenerator : IRiskReportGenerator
|
||||
{
|
||||
private const string EngineVersionValue = "1.0.0";
|
||||
|
||||
private readonly IEvidenceDensityScorer _scorer;
|
||||
|
||||
public RiskReportGenerator(IEvidenceDensityScorer scorer)
|
||||
{
|
||||
_scorer = scorer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RiskReport Generate(RiskReportInput input)
|
||||
{
|
||||
// Calculate confidence score if evidence factors provided
|
||||
EvidenceDensityScore? confidenceScore = null;
|
||||
if (input.EvidenceFactors is not null)
|
||||
{
|
||||
confidenceScore = _scorer.Calculate(input.EvidenceFactors);
|
||||
}
|
||||
|
||||
var explanation = GenerateExplanation(input);
|
||||
var narrative = GenerateNarrative(input, confidenceScore);
|
||||
var actions = GenerateRecommendedActions(input);
|
||||
|
||||
return new RiskReport
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
FindingId = input.FindingId,
|
||||
VulnerabilityId = input.VulnerabilityId,
|
||||
PackageName = input.PackageName,
|
||||
PackageVersion = input.PackageVersion,
|
||||
Assumptions = input.Assumptions,
|
||||
Falsifiability = input.Falsifiability,
|
||||
ConfidenceScore = confidenceScore,
|
||||
Explanation = explanation,
|
||||
DetailedNarrative = narrative,
|
||||
RecommendedActions = [.. actions],
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
EngineVersion = EngineVersionValue
|
||||
};
|
||||
}
|
||||
|
||||
private static string GenerateExplanation(RiskReportInput input)
|
||||
{
|
||||
var parts = new List<string>
|
||||
{
|
||||
$"Vulnerability {input.VulnerabilityId} affects {input.PackageName}@{input.PackageVersion}."
|
||||
};
|
||||
|
||||
if (input.Severity is not null)
|
||||
{
|
||||
parts.Add($"Severity: {input.Severity}.");
|
||||
}
|
||||
|
||||
if (input.Falsifiability?.Status == FalsifiabilityStatus.Falsified)
|
||||
{
|
||||
parts.Add("This finding has been falsified and may not be exploitable in your environment.");
|
||||
}
|
||||
else if (input.Assumptions?.HasContradictions == true)
|
||||
{
|
||||
parts.Add("Some analysis assumptions have been contradicted by observed evidence.");
|
||||
}
|
||||
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
private static string GenerateNarrative(RiskReportInput input, EvidenceDensityScore? score)
|
||||
{
|
||||
var sections = new List<string>();
|
||||
|
||||
// Overview
|
||||
sections.Add($"## Overview\n{input.Description ?? "No description available."}");
|
||||
|
||||
// Assumptions section
|
||||
if (input.Assumptions is not null && input.Assumptions.Assumptions.Length > 0)
|
||||
{
|
||||
var assumptionLines = input.Assumptions.Assumptions
|
||||
.Select(a => $"- **{a.Category}**: {a.Key} = {a.AssumedValue}" +
|
||||
(a.ObservedValue is not null ? $" (observed: {a.ObservedValue})" : ""));
|
||||
|
||||
sections.Add($"## Assumptions\n{string.Join("\n", assumptionLines)}");
|
||||
}
|
||||
|
||||
// Falsifiability section
|
||||
if (input.Falsifiability is not null)
|
||||
{
|
||||
sections.Add($"## Falsifiability\n**Status**: {input.Falsifiability.Status}\n\n{input.Falsifiability.Summary}");
|
||||
}
|
||||
|
||||
// Confidence section
|
||||
if (score is not null)
|
||||
{
|
||||
sections.Add($"## Confidence Assessment\n{score.Explanation}");
|
||||
|
||||
if (score.ImprovementRecommendations.Count > 0)
|
||||
{
|
||||
var recs = score.ImprovementRecommendations.Select(r => $"- {r}");
|
||||
sections.Add($"### Recommendations to Improve Confidence\n{string.Join("\n", recs)}");
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("\n\n", sections);
|
||||
}
|
||||
|
||||
private static List<RecommendedAction> GenerateRecommendedActions(RiskReportInput input)
|
||||
{
|
||||
var actions = new List<RecommendedAction>();
|
||||
int priority = 1;
|
||||
|
||||
// Action: Update package if fix available
|
||||
if (input.FixedVersion is not null)
|
||||
{
|
||||
actions.Add(new RecommendedAction(
|
||||
priority++,
|
||||
$"Update {input.PackageName} to version {input.FixedVersion} or later",
|
||||
"A fixed version is available that addresses this vulnerability",
|
||||
EffortLevel.Low));
|
||||
}
|
||||
|
||||
// Action: Validate assumptions
|
||||
if (input.Assumptions is not null && input.Assumptions.ValidatedCount < input.Assumptions.Assumptions.Length)
|
||||
{
|
||||
actions.Add(new RecommendedAction(
|
||||
priority++,
|
||||
"Validate analysis assumptions with runtime observations",
|
||||
$"Only {input.Assumptions.ValidatedCount}/{input.Assumptions.Assumptions.Length} assumptions are validated",
|
||||
EffortLevel.Medium));
|
||||
}
|
||||
|
||||
// Action: Evaluate falsifiability criteria
|
||||
if (input.Falsifiability?.Status == FalsifiabilityStatus.PartiallyEvaluated)
|
||||
{
|
||||
var pendingCount = input.Falsifiability.Criteria.Count(c => c.Status == CriterionStatus.Pending);
|
||||
actions.Add(new RecommendedAction(
|
||||
priority++,
|
||||
"Complete falsifiability evaluation",
|
||||
$"{pendingCount} criteria are pending evaluation",
|
||||
EffortLevel.Medium));
|
||||
}
|
||||
|
||||
// Default action if no fix available
|
||||
if (input.FixedVersion is null)
|
||||
{
|
||||
actions.Add(new RecommendedAction(
|
||||
priority,
|
||||
"Monitor for vendor patch or implement compensating controls",
|
||||
"No fixed version is currently available",
|
||||
EffortLevel.High));
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user