// 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;
///
/// A comprehensive risk report that includes all explainability data for a finding.
///
public sealed record RiskReport
{
/// Unique report identifier
public required string Id { get; init; }
/// The finding this report explains
public required string FindingId { get; init; }
/// The vulnerability ID (CVE, GHSA, etc.)
public required string VulnerabilityId { get; init; }
/// Package name
public required string PackageName { get; init; }
/// Package version
public required string PackageVersion { get; init; }
/// Assumptions made during analysis
public AssumptionSet? Assumptions { get; init; }
/// Falsifiability criteria and status
public FalsifiabilityCriteria? Falsifiability { get; init; }
/// Evidence density confidence score
public EvidenceDensityScore? ConfidenceScore { get; init; }
/// Human-readable explanation of the finding
public required string Explanation { get; init; }
/// Detailed narrative explaining the risk
public string? DetailedNarrative { get; init; }
/// Recommended actions
public ImmutableArray RecommendedActions { get; init; } = [];
/// When this report was generated
public required DateTimeOffset GeneratedAt { get; init; }
/// Version of the explainability engine
public required string EngineVersion { get; init; }
}
///
/// A recommended action to address a finding.
///
/// Action priority (1 = highest)
/// The recommended action
/// Why this action is recommended
/// Estimated effort level
public sealed record RecommendedAction(
int Priority,
string Action,
string Rationale,
EffortLevel Effort
);
///
/// Effort level for a recommended action.
///
public enum EffortLevel
{
/// Quick configuration change or update
Low,
/// Moderate code changes or testing required
Medium,
/// Significant refactoring or architectural changes
High
}
///
/// Generates comprehensive risk reports.
///
public interface IRiskReportGenerator
{
///
/// Generates a risk report for a finding.
///
RiskReport Generate(RiskReportInput input);
}
///
/// Input for generating a risk report.
///
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; }
}
///
/// Default implementation of .
///
public sealed class RiskReportGenerator : IRiskReportGenerator
{
private const string EngineVersionValue = "1.0.0";
private readonly IEvidenceDensityScorer _scorer;
public RiskReportGenerator(IEvidenceDensityScorer scorer)
{
_scorer = scorer;
}
///
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
{
$"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();
// 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 GenerateRecommendedActions(RiskReportInput input)
{
var actions = new List();
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;
}
}