This commit is contained in:
StellaOps Bot
2025-12-14 23:20:14 +02:00
parent 3411e825cd
commit b058dbe031
356 changed files with 68310 additions and 1108 deletions

View File

@@ -0,0 +1,128 @@
namespace StellaOps.Policy.Scoring.Engine;
/// <summary>
/// Factory for creating CVSS engines and detecting versions from vector strings.
/// </summary>
public sealed class CvssEngineFactory : ICvssEngineFactory
{
private readonly ICvssV4Engine _v4Engine;
private readonly CvssV3Engine _v31Engine;
private readonly CvssV3Engine _v30Engine;
private readonly CvssV2Engine _v2Engine;
public CvssEngineFactory(ICvssV4Engine? v4Engine = null)
{
_v4Engine = v4Engine ?? new CvssV4Engine();
_v31Engine = new CvssV3Engine(CvssVersion.V3_1);
_v30Engine = new CvssV3Engine(CvssVersion.V3_0);
_v2Engine = new CvssV2Engine();
}
public ICvssEngine Create(CvssVersion version) => version switch
{
CvssVersion.V2 => _v2Engine,
CvssVersion.V3_0 => _v30Engine,
CvssVersion.V3_1 => _v31Engine,
CvssVersion.V4_0 => new CvssV4EngineAdapter(_v4Engine),
_ => throw new ArgumentOutOfRangeException(nameof(version), version, "Unsupported CVSS version")
};
public CvssVersion? DetectVersion(string vectorString)
{
if (string.IsNullOrWhiteSpace(vectorString))
return null;
var trimmed = vectorString.Trim();
// CVSS v4.0: "CVSS:4.0/..."
if (trimmed.StartsWith("CVSS:4.0/", StringComparison.OrdinalIgnoreCase))
return CvssVersion.V4_0;
// CVSS v3.1: "CVSS:3.1/..."
if (trimmed.StartsWith("CVSS:3.1/", StringComparison.OrdinalIgnoreCase))
return CvssVersion.V3_1;
// CVSS v3.0: "CVSS:3.0/..."
if (trimmed.StartsWith("CVSS:3.0/", StringComparison.OrdinalIgnoreCase))
return CvssVersion.V3_0;
// CVSS v2.0: No prefix or "CVSS2#", contains "Au:" (Authentication)
if (trimmed.Contains("Au:", StringComparison.OrdinalIgnoreCase) ||
trimmed.StartsWith("CVSS2#", StringComparison.OrdinalIgnoreCase))
return CvssVersion.V2;
// Try to detect by metric patterns
// v4.0 unique: AT: (Attack Requirements)
if (trimmed.Contains("/AT:", StringComparison.OrdinalIgnoreCase))
return CvssVersion.V4_0;
// v3.x unique: PR: (Privileges Required), S: (Scope)
if (trimmed.Contains("/PR:", StringComparison.OrdinalIgnoreCase) &&
trimmed.Contains("/S:", StringComparison.OrdinalIgnoreCase))
return CvssVersion.V3_1; // Default to 3.1 if unspecified
return null;
}
public CvssVersionedScore ComputeFromVector(string vectorString)
{
var version = DetectVersion(vectorString);
if (version is null)
throw new ArgumentException($"Unable to detect CVSS version from vector: {vectorString}", nameof(vectorString));
var engine = Create(version.Value);
return engine.ComputeFromVector(vectorString);
}
}
/// <summary>
/// Adapter to make ICvssV4Engine compatible with ICvssEngine interface.
/// </summary>
internal sealed class CvssV4EngineAdapter : ICvssEngine
{
private readonly ICvssV4Engine _engine;
public CvssV4EngineAdapter(ICvssV4Engine engine)
{
_engine = engine ?? throw new ArgumentNullException(nameof(engine));
}
public CvssVersion Version => CvssVersion.V4_0;
public CvssVersionedScore ComputeFromVector(string vectorString)
{
var metrics = _engine.ParseVector(vectorString);
var scores = _engine.ComputeScores(metrics.BaseMetrics, metrics.ThreatMetrics, metrics.EnvironmentalMetrics);
var vector = _engine.BuildVectorString(metrics.BaseMetrics, metrics.ThreatMetrics, metrics.EnvironmentalMetrics, metrics.SupplementalMetrics);
var severity = _engine.GetSeverity(scores.EffectiveScore);
return new CvssVersionedScore
{
Version = CvssVersion.V4_0,
BaseScore = scores.BaseScore,
TemporalScore = scores.ThreatScore > 0 && scores.ThreatScore != scores.BaseScore ? scores.ThreatScore : null,
EnvironmentalScore = scores.EnvironmentalScore > 0 && scores.EnvironmentalScore != scores.BaseScore ? scores.EnvironmentalScore : null,
EffectiveScore = scores.EffectiveScore,
Severity = severity.ToString(),
VectorString = vector
};
}
public bool IsValidVector(string vectorString)
{
if (string.IsNullOrWhiteSpace(vectorString))
return false;
try
{
_engine.ParseVector(vectorString);
return true;
}
catch
{
return false;
}
}
public string GetSeverityLabel(double score) => _engine.GetSeverity(score).ToString();
}

View File

@@ -0,0 +1,211 @@
using System.Text.RegularExpressions;
namespace StellaOps.Policy.Scoring.Engine;
/// <summary>
/// CVSS v2.0 scoring engine per FIRST specification.
/// https://www.first.org/cvss/v2/guide
/// </summary>
public sealed partial class CvssV2Engine : ICvssEngine
{
public CvssVersion Version => CvssVersion.V2;
// CVSS v2 vector pattern - supports base, temporal, and environmental metric groups
// Base: AV:N/AC:L/Au:N/C:C/I:C/A:C
// Temporal: E:POC/RL:OF/RC:C (E can be U/POC/F/H/ND, RL can be OF/TF/W/U/ND, RC can be UC/UR/C/ND)
// Environmental: CDP:N/TD:N/CR:M/IR:M/AR:M
[GeneratedRegex(@"^(?:CVSS2#)?AV:([LAN])/AC:([HML])/Au:([MSN])/C:([NPC])/I:([NPC])/A:([NPC])(?:/E:(U|POC|F|H|ND)/RL:(OF|TF|W|U|ND)/RC:(UC|UR|C|ND))?(?:/CDP:(N|L|LM|MH|H|ND)/TD:(N|L|M|H|ND)/CR:(L|M|H|ND)/IR:(L|M|H|ND)/AR:(L|M|H|ND))?$", RegexOptions.IgnoreCase)]
private static partial Regex VectorPattern();
public CvssVersionedScore ComputeFromVector(string vectorString)
{
ArgumentNullException.ThrowIfNull(vectorString);
var match = VectorPattern().Match(vectorString.Trim());
if (!match.Success)
throw new ArgumentException($"Invalid CVSS v2.0 vector string: {vectorString}", nameof(vectorString));
// Parse base metrics
var av = ParseAccessVector(match.Groups[1].Value);
var ac = ParseAccessComplexity(match.Groups[2].Value);
var au = ParseAuthentication(match.Groups[3].Value);
var c = ParseImpact(match.Groups[4].Value);
var i = ParseImpact(match.Groups[5].Value);
var a = ParseImpact(match.Groups[6].Value);
// Compute base score
var impact = 10.41 * (1 - (1 - c) * (1 - i) * (1 - a));
var exploitability = 20 * av * ac * au;
var fImpact = impact == 0 ? 0 : 1.176;
var baseScore = Math.Round(((0.6 * impact) + (0.4 * exploitability) - 1.5) * fImpact, 1, MidpointRounding.AwayFromZero);
baseScore = Math.Clamp(baseScore, 0, 10);
// Parse temporal metrics if present
double? temporalScore = null;
if (match.Groups[7].Success)
{
var e = ParseExploitability(match.Groups[7].Value);
var rl = ParseRemediationLevel(match.Groups[8].Value);
var rc = ParseReportConfidence(match.Groups[9].Value);
temporalScore = Math.Round(baseScore * e * rl * rc, 1, MidpointRounding.AwayFromZero);
}
// Parse environmental metrics if present
double? environmentalScore = null;
if (match.Groups[10].Success)
{
var cdp = ParseCollateralDamagePotential(match.Groups[10].Value);
var td = ParseTargetDistribution(match.Groups[11].Value);
var cr = ParseRequirement(match.Groups[12].Value);
var ir = ParseRequirement(match.Groups[13].Value);
var ar = ParseRequirement(match.Groups[14].Value);
var adjustedImpact = Math.Min(10, 10.41 * (1 - (1 - c * cr) * (1 - i * ir) * (1 - a * ar)));
var adjustedBase = Math.Round(((0.6 * adjustedImpact) + (0.4 * exploitability) - 1.5) * fImpact, 1, MidpointRounding.AwayFromZero);
var tempScoreForEnv = temporalScore ?? baseScore;
if (match.Groups[7].Success)
{
var e = ParseExploitability(match.Groups[7].Value);
var rl = ParseRemediationLevel(match.Groups[8].Value);
var rc = ParseReportConfidence(match.Groups[9].Value);
adjustedBase = Math.Round(adjustedBase * e * rl * rc, 1, MidpointRounding.AwayFromZero);
}
environmentalScore = Math.Round((adjustedBase + (10 - adjustedBase) * cdp) * td, 1, MidpointRounding.AwayFromZero);
environmentalScore = Math.Clamp(environmentalScore.Value, 0, 10);
}
var effectiveScore = environmentalScore ?? temporalScore ?? baseScore;
return new CvssVersionedScore
{
Version = CvssVersion.V2,
BaseScore = baseScore,
TemporalScore = temporalScore,
EnvironmentalScore = environmentalScore,
EffectiveScore = effectiveScore,
Severity = GetSeverityLabel(effectiveScore),
VectorString = NormalizeVector(vectorString)
};
}
public bool IsValidVector(string vectorString)
{
if (string.IsNullOrWhiteSpace(vectorString))
return false;
return VectorPattern().IsMatch(vectorString.Trim());
}
public string GetSeverityLabel(double score) => score switch
{
>= 7.0 => "High",
>= 4.0 => "Medium",
> 0 => "Low",
_ => "None"
};
private static string NormalizeVector(string vector)
{
// Ensure consistent casing and format
var normalized = vector.Trim().ToUpperInvariant();
if (!normalized.StartsWith("CVSS2#", StringComparison.Ordinal))
normalized = "CVSS2#" + normalized;
return normalized;
}
// Access Vector (AV)
private static double ParseAccessVector(string value) => value.ToUpperInvariant() switch
{
"L" => 0.395, // Local
"A" => 0.646, // Adjacent Network
"N" => 1.0, // Network
_ => throw new ArgumentException($"Invalid Access Vector: {value}")
};
// Access Complexity (AC)
private static double ParseAccessComplexity(string value) => value.ToUpperInvariant() switch
{
"H" => 0.35, // High
"M" => 0.61, // Medium
"L" => 0.71, // Low
_ => throw new ArgumentException($"Invalid Access Complexity: {value}")
};
// Authentication (Au) - v2 specific
private static double ParseAuthentication(string value) => value.ToUpperInvariant() switch
{
"M" => 0.45, // Multiple
"S" => 0.56, // Single
"N" => 0.704, // None
_ => throw new ArgumentException($"Invalid Authentication: {value}")
};
// Impact (C/I/A)
private static double ParseImpact(string value) => value.ToUpperInvariant() switch
{
"N" => 0, // None
"P" => 0.275, // Partial
"C" => 0.660, // Complete
_ => throw new ArgumentException($"Invalid Impact: {value}")
};
// Exploitability (E)
private static double ParseExploitability(string value) => value.ToUpperInvariant() switch
{
"U" or "ND" => 1.0, // Unproven / Not Defined
"POC" or "P" => 0.9, // Proof of Concept
"F" => 0.95, // Functional
"H" => 1.0, // High
_ => 1.0
};
// Remediation Level (RL)
private static double ParseRemediationLevel(string value) => value.ToUpperInvariant() switch
{
"OF" or "O" => 0.87, // Official Fix
"TF" or "T" => 0.90, // Temporary Fix
"W" => 0.95, // Workaround
"U" or "ND" => 1.0, // Unavailable / Not Defined
_ => 1.0
};
// Report Confidence (RC)
private static double ParseReportConfidence(string value) => value.ToUpperInvariant() switch
{
"UC" or "U" => 0.9, // Unconfirmed
"UR" => 0.95, // Uncorroborated
"C" or "ND" => 1.0, // Confirmed / Not Defined
_ => 1.0
};
// Collateral Damage Potential (CDP)
private static double ParseCollateralDamagePotential(string value) => value.ToUpperInvariant() switch
{
"N" or "ND" => 0,
"L" => 0.1,
"LM" => 0.3,
"MH" => 0.4,
"H" => 0.5,
_ => 0
};
// Target Distribution (TD)
private static double ParseTargetDistribution(string value) => value.ToUpperInvariant() switch
{
"N" or "ND" => 1.0,
"L" => 0.25,
"M" => 0.75,
"H" => 1.0,
_ => 1.0
};
// Security Requirements (CR/IR/AR)
private static double ParseRequirement(string value) => value.ToUpperInvariant() switch
{
"L" => 0.5,
"M" or "ND" => 1.0,
"H" => 1.51,
_ => 1.0
};
}

View File

@@ -0,0 +1,350 @@
using System.Text.RegularExpressions;
namespace StellaOps.Policy.Scoring.Engine;
/// <summary>
/// CVSS v3.0/v3.1 scoring engine per FIRST specification.
/// https://www.first.org/cvss/v3.1/specification-document
/// </summary>
public sealed partial class CvssV3Engine : ICvssEngine
{
private readonly CvssVersion _version;
public CvssV3Engine(CvssVersion version = CvssVersion.V3_1)
{
if (version != CvssVersion.V3_0 && version != CvssVersion.V3_1)
throw new ArgumentException("Version must be V3_0 or V3_1", nameof(version));
_version = version;
}
public CvssVersion Version => _version;
// CVSS v3 vector pattern
[GeneratedRegex(@"^CVSS:3\.[01]/AV:([NALP])/AC:([LH])/PR:([NLH])/UI:([NR])/S:([UC])/C:([NLH])/I:([NLH])/A:([NLH])(?:/E:([XUPFH])/RL:([XOTWU])/RC:([XURC]))?(?:/CR:([XLMH])/IR:([XLMH])/AR:([XLMH]))?(?:/MAV:([XNALP])/MAC:([XLH])/MPR:([XNLH])/MUI:([XNR])/MS:([XUC])/MC:([XNLH])/MI:([XNLH])/MA:([XNLH]))?$", RegexOptions.IgnoreCase)]
private static partial Regex VectorPattern();
public CvssVersionedScore ComputeFromVector(string vectorString)
{
ArgumentNullException.ThrowIfNull(vectorString);
var match = VectorPattern().Match(vectorString.Trim());
if (!match.Success)
throw new ArgumentException($"Invalid CVSS v3.x vector string: {vectorString}", nameof(vectorString));
// Parse base metrics
var av = ParseAttackVector(match.Groups[1].Value);
var ac = ParseAttackComplexity(match.Groups[2].Value);
var pr = ParsePrivilegesRequired(match.Groups[3].Value, match.Groups[5].Value);
var ui = ParseUserInteraction(match.Groups[4].Value);
var scope = match.Groups[5].Value.ToUpperInvariant() == "C";
var c = ParseImpact(match.Groups[6].Value);
var i = ParseImpact(match.Groups[7].Value);
var a = ParseImpact(match.Groups[8].Value);
// Compute base score
var baseScore = ComputeBaseScore(av, ac, pr, ui, scope, c, i, a);
// Parse temporal metrics if present
double? temporalScore = null;
if (match.Groups[9].Success && !string.IsNullOrEmpty(match.Groups[9].Value))
{
var e = ParseExploitCodeMaturity(match.Groups[9].Value);
var rl = ParseRemediationLevel(match.Groups[10].Value);
var rc = ParseReportConfidence(match.Groups[11].Value);
temporalScore = RoundUp(baseScore * e * rl * rc);
}
// Parse environmental metrics if present
double? environmentalScore = null;
if (match.Groups[12].Success && !string.IsNullOrEmpty(match.Groups[12].Value))
{
var cr = ParseRequirement(match.Groups[12].Value);
var ir = ParseRequirement(match.Groups[13].Value);
var ar = ParseRequirement(match.Groups[14].Value);
// Modified base metrics (use base values if not specified)
var mav = match.Groups[15].Success ? ParseModifiedAttackVector(match.Groups[15].Value) ?? av : av;
var mac = match.Groups[16].Success ? ParseModifiedAttackComplexity(match.Groups[16].Value) ?? ac : ac;
var mpr = match.Groups[17].Success ? ParseModifiedPrivilegesRequired(match.Groups[17].Value, match.Groups[19].Value) ?? pr : pr;
var mui = match.Groups[18].Success ? ParseModifiedUserInteraction(match.Groups[18].Value) ?? ui : ui;
var ms = match.Groups[19].Success ? ParseModifiedScope(match.Groups[19].Value) ?? scope : scope;
var mc = match.Groups[20].Success ? ParseModifiedImpact(match.Groups[20].Value) ?? c : c;
var mi = match.Groups[21].Success ? ParseModifiedImpact(match.Groups[21].Value) ?? i : i;
var ma = match.Groups[22].Success ? ParseModifiedImpact(match.Groups[22].Value) ?? a : a;
environmentalScore = ComputeEnvironmentalScore(mav, mac, mpr, mui, ms, mc, mi, ma, cr, ir, ar);
// Apply temporal to environmental if temporal present
if (temporalScore.HasValue && match.Groups[9].Success)
{
var e = ParseExploitCodeMaturity(match.Groups[9].Value);
var rl = ParseRemediationLevel(match.Groups[10].Value);
var rc = ParseReportConfidence(match.Groups[11].Value);
environmentalScore = RoundUp(environmentalScore.Value * e * rl * rc);
}
}
var effectiveScore = environmentalScore ?? temporalScore ?? baseScore;
return new CvssVersionedScore
{
Version = _version,
BaseScore = baseScore,
TemporalScore = temporalScore,
EnvironmentalScore = environmentalScore,
EffectiveScore = effectiveScore,
Severity = GetSeverityLabel(effectiveScore),
VectorString = NormalizeVector(vectorString)
};
}
public bool IsValidVector(string vectorString)
{
if (string.IsNullOrWhiteSpace(vectorString))
return false;
return VectorPattern().IsMatch(vectorString.Trim());
}
public string GetSeverityLabel(double score) => score switch
{
>= 9.0 => "Critical",
>= 7.0 => "High",
>= 4.0 => "Medium",
> 0 => "Low",
_ => "None"
};
private double ComputeBaseScore(double av, double ac, double pr, double ui, bool scope, double c, double i, double a)
{
var iss = 1 - (1 - c) * (1 - i) * (1 - a);
double impact;
if (scope)
{
// Changed scope
impact = 7.52 * (iss - 0.029) - 3.25 * Math.Pow(iss - 0.02, 15);
}
else
{
// Unchanged scope
impact = 6.42 * iss;
}
var exploitability = 8.22 * av * ac * pr * ui;
if (impact <= 0)
return 0;
double baseScore;
if (scope)
{
baseScore = Math.Min(1.08 * (impact + exploitability), 10);
}
else
{
baseScore = Math.Min(impact + exploitability, 10);
}
return RoundUp(baseScore);
}
private double ComputeEnvironmentalScore(double mav, double mac, double mpr, double mui, bool ms,
double mc, double mi, double ma, double cr, double ir, double ar)
{
var miss = Math.Min(1 - (1 - mc * cr) * (1 - mi * ir) * (1 - ma * ar), 0.915);
double modifiedImpact;
if (ms)
{
modifiedImpact = 7.52 * (miss - 0.029) - 3.25 * Math.Pow(miss * 0.9731 - 0.02, 13);
}
else
{
modifiedImpact = 6.42 * miss;
}
var modifiedExploitability = 8.22 * mav * mac * mpr * mui;
if (modifiedImpact <= 0)
return 0;
double envScore;
if (ms)
{
envScore = Math.Min(1.08 * (modifiedImpact + modifiedExploitability), 10);
}
else
{
envScore = Math.Min(modifiedImpact + modifiedExploitability, 10);
}
return RoundUp(envScore);
}
private static string NormalizeVector(string vector)
{
var normalized = vector.Trim().ToUpperInvariant();
// Ensure proper prefix
if (!normalized.StartsWith("CVSS:3.", StringComparison.Ordinal))
{
normalized = "CVSS:3.1/" + normalized;
}
return normalized;
}
private static double RoundUp(double value)
{
// CVSS v3 uses "round up" to nearest 0.1
var intValue = (int)Math.Round(value * 100000);
if (intValue % 10000 == 0)
return intValue / 100000.0;
return (Math.Floor((double)intValue / 10000) + 1) / 10.0;
}
// Attack Vector (AV)
private static double ParseAttackVector(string value) => value.ToUpperInvariant() switch
{
"N" => 0.85, // Network
"A" => 0.62, // Adjacent
"L" => 0.55, // Local
"P" => 0.2, // Physical
_ => throw new ArgumentException($"Invalid Attack Vector: {value}")
};
// Attack Complexity (AC)
private static double ParseAttackComplexity(string value) => value.ToUpperInvariant() switch
{
"L" => 0.77, // Low
"H" => 0.44, // High
_ => throw new ArgumentException($"Invalid Attack Complexity: {value}")
};
// Privileges Required (PR) - depends on Scope
private static double ParsePrivilegesRequired(string value, string scopeValue)
{
var scopeChanged = scopeValue.ToUpperInvariant() == "C";
return value.ToUpperInvariant() switch
{
"N" => 0.85, // None
"L" => scopeChanged ? 0.68 : 0.62, // Low
"H" => scopeChanged ? 0.5 : 0.27, // High
_ => throw new ArgumentException($"Invalid Privileges Required: {value}")
};
}
// User Interaction (UI)
private static double ParseUserInteraction(string value) => value.ToUpperInvariant() switch
{
"N" => 0.85, // None
"R" => 0.62, // Required
_ => throw new ArgumentException($"Invalid User Interaction: {value}")
};
// Impact (C/I/A)
private static double ParseImpact(string value) => value.ToUpperInvariant() switch
{
"N" => 0, // None
"L" => 0.22, // Low
"H" => 0.56, // High
_ => throw new ArgumentException($"Invalid Impact: {value}")
};
// Exploit Code Maturity (E)
private static double ParseExploitCodeMaturity(string value) => value.ToUpperInvariant() switch
{
"X" => 1.0, // Not Defined
"U" => 0.91, // Unproven
"P" => 0.94, // Proof of Concept
"F" => 0.97, // Functional
"H" => 1.0, // High
_ => 1.0
};
// Remediation Level (RL)
private static double ParseRemediationLevel(string value) => value.ToUpperInvariant() switch
{
"X" => 1.0, // Not Defined
"O" => 0.95, // Official Fix
"T" => 0.96, // Temporary Fix
"W" => 0.97, // Workaround
"U" => 1.0, // Unavailable
_ => 1.0
};
// Report Confidence (RC)
private static double ParseReportConfidence(string value) => value.ToUpperInvariant() switch
{
"X" => 1.0, // Not Defined
"U" => 0.92, // Unknown
"R" => 0.96, // Reasonable
"C" => 1.0, // Confirmed
_ => 1.0
};
// Security Requirements (CR/IR/AR)
private static double ParseRequirement(string value) => value.ToUpperInvariant() switch
{
"X" => 1.0, // Not Defined
"L" => 0.5, // Low
"M" => 1.0, // Medium
"H" => 1.5, // High
_ => 1.0
};
// Modified metrics - return null if "X" (Not Defined) to use base value
private static double? ParseModifiedAttackVector(string value) => value.ToUpperInvariant() switch
{
"X" => null,
"N" => 0.85,
"A" => 0.62,
"L" => 0.55,
"P" => 0.2,
_ => null
};
private static double? ParseModifiedAttackComplexity(string value) => value.ToUpperInvariant() switch
{
"X" => null,
"L" => 0.77,
"H" => 0.44,
_ => null
};
private static double? ParseModifiedPrivilegesRequired(string value, string scopeValue)
{
if (value.ToUpperInvariant() == "X") return null;
var scopeChanged = scopeValue.ToUpperInvariant() == "C";
return value.ToUpperInvariant() switch
{
"N" => 0.85,
"L" => scopeChanged ? 0.68 : 0.62,
"H" => scopeChanged ? 0.5 : 0.27,
_ => null
};
}
private static double? ParseModifiedUserInteraction(string value) => value.ToUpperInvariant() switch
{
"X" => null,
"N" => 0.85,
"R" => 0.62,
_ => null
};
private static bool? ParseModifiedScope(string value) => value.ToUpperInvariant() switch
{
"X" => null,
"U" => false,
"C" => true,
_ => null
};
private static double? ParseModifiedImpact(string value) => value.ToUpperInvariant() switch
{
"X" => null,
"N" => 0,
"L" => 0.22,
"H" => 0.56,
_ => null
};
}

View File

@@ -0,0 +1,102 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Scoring.Engine;
/// <summary>
/// CVSS specification version.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CvssVersion
{
/// <summary>CVSS v2.0</summary>
V2,
/// <summary>CVSS v3.0</summary>
V3_0,
/// <summary>CVSS v3.1</summary>
V3_1,
/// <summary>CVSS v4.0</summary>
V4_0
}
/// <summary>
/// Version-agnostic CVSS score result.
/// </summary>
public sealed record CvssVersionedScore
{
/// <summary>The CVSS version used for scoring.</summary>
public required CvssVersion Version { get; init; }
/// <summary>Base score (0.0-10.0).</summary>
public required double BaseScore { get; init; }
/// <summary>Temporal score (v2/v3) or Threat score (v4).</summary>
public double? TemporalScore { get; init; }
/// <summary>Environmental score.</summary>
public double? EnvironmentalScore { get; init; }
/// <summary>The effective score to use for prioritization.</summary>
public required double EffectiveScore { get; init; }
/// <summary>Severity label (None/Low/Medium/High/Critical).</summary>
public required string Severity { get; init; }
/// <summary>Vector string in version-appropriate format.</summary>
public required string VectorString { get; init; }
}
/// <summary>
/// Universal CVSS engine interface supporting all versions.
/// </summary>
public interface ICvssEngine
{
/// <summary>The CVSS version this engine implements.</summary>
CvssVersion Version { get; }
/// <summary>
/// Computes scores from a vector string.
/// </summary>
/// <param name="vectorString">CVSS vector string.</param>
/// <returns>Computed score with version information.</returns>
CvssVersionedScore ComputeFromVector(string vectorString);
/// <summary>
/// Validates a vector string format.
/// </summary>
/// <param name="vectorString">Vector string to validate.</param>
/// <returns>True if valid for this version.</returns>
bool IsValidVector(string vectorString);
/// <summary>
/// Gets severity label for a score.
/// </summary>
/// <param name="score">CVSS score (0.0-10.0).</param>
/// <returns>Severity label.</returns>
string GetSeverityLabel(double score);
}
/// <summary>
/// Factory for creating version-appropriate CVSS engines.
/// </summary>
public interface ICvssEngineFactory
{
/// <summary>
/// Creates an engine for the specified version.
/// </summary>
ICvssEngine Create(CvssVersion version);
/// <summary>
/// Detects the CVSS version from a vector string.
/// </summary>
/// <param name="vectorString">Vector string to analyze.</param>
/// <returns>Detected version, or null if unrecognized.</returns>
CvssVersion? DetectVersion(string vectorString);
/// <summary>
/// Computes scores automatically detecting version from vector string.
/// </summary>
CvssVersionedScore ComputeFromVector(string vectorString);
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,10 @@
<Description>CVSS v4.0 scoring engine with deterministic receipt generation for StellaOps policy decisions.</Description>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Policy.Scoring.Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JsonSchema.Net" Version="7.3.2" />
<ProjectReference Include="..\..\Attestor\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />