up
This commit is contained in:
128
src/Policy/StellaOps.Policy.Scoring/Engine/CvssEngineFactory.cs
Normal file
128
src/Policy/StellaOps.Policy.Scoring/Engine/CvssEngineFactory.cs
Normal 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();
|
||||
}
|
||||
211
src/Policy/StellaOps.Policy.Scoring/Engine/CvssV2Engine.cs
Normal file
211
src/Policy/StellaOps.Policy.Scoring/Engine/CvssV2Engine.cs
Normal 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
|
||||
};
|
||||
}
|
||||
350
src/Policy/StellaOps.Policy.Scoring/Engine/CvssV3Engine.cs
Normal file
350
src/Policy/StellaOps.Policy.Scoring/Engine/CvssV3Engine.cs
Normal 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
|
||||
};
|
||||
}
|
||||
102
src/Policy/StellaOps.Policy.Scoring/Engine/CvssVersion.cs
Normal file
102
src/Policy/StellaOps.Policy.Scoring/Engine/CvssVersion.cs
Normal 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
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user