up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
master
2025-11-28 18:21:46 +02:00
parent 05da719048
commit d1cbb905f8
103 changed files with 49604 additions and 105 deletions

View File

@@ -0,0 +1,93 @@
# AGENTS.md - StellaOps.Policy.Scoring
## Module Summary
The CVSS v4.0 Scoring module provides deterministic score computation with full audit trail via receipts. It implements the FIRST CVSS v4.0 specification for vulnerability scoring with policy-driven customization and evidence linkage.
## Working Directory
`src/Policy/StellaOps.Policy.Scoring`
## Required Reading
Before implementing in this module, read:
1. `docs/README.md`
2. `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
3. `docs/modules/policy/architecture.md`
4. FIRST CVSS v4.0 Specification: https://www.first.org/cvss/v4-0/specification-document
## Module Boundaries
### This Module Owns
- CVSS v4.0 data models (`CvssMetrics.cs`, `CvssScoreReceipt.cs`, `CvssPolicy.cs`)
- CVSS v4.0 scoring engine (`Engine/CvssV4Engine.cs`)
- Receipt generation and management (`Receipts/ReceiptBuilder.cs`)
- Policy loading and validation (`Policies/CvssPolicyLoader.cs`)
- JSON schemas for receipts and policies (`Schemas/`)
### This Module Does NOT Own
- Attestation/DSSE envelope creation (use `StellaOps.Attestor.Envelope`)
- Vulnerability advisory ingestion (use `StellaOps.Concelier.Core`)
- VEX statement handling (use `StellaOps.Excititor.Core`)
- General policy evaluation (use `StellaOps.Policy`)
## Determinism Requirements
### Input Reproducibility
- All score computations must be deterministic: same inputs → same outputs
- Receipt `inputHash` field captures SHA-256 of normalized inputs
- Use stable JSON serialization with ordered keys for hashing
### Score Computation
- Follow FIRST CVSS v4.0 math exactly (MacroVector lookup tables, EQ formulas)
- Use "Round Up" rounding per FIRST spec: `ceil(score * 10) / 10`
- Never introduce floating-point non-determinism
### Timestamp Handling
- All timestamps must be UTC in ISO-8601 format
- Use `DateTimeOffset.UtcNow` for creation times
- Include timestamp in input hash for temporal reproducibility
## Testing Requirements
### Unit Tests
- Test all CVSS v4.0 metric combinations using FIRST sample vectors
- Test edge cases: missing optional metrics, all-None impacts, boundary scores
- Test determinism: multiple computations of same input must match
### Integration Tests
- Test policy loading and validation
- Test receipt persistence and retrieval
- Test amendment workflow with history tracking
## API Contract
### Score Computation
```csharp
// Engine interface
public interface ICvssV4Engine
{
CvssScores ComputeScores(CvssBaseMetrics baseMetrics, CvssThreatMetrics? threatMetrics = null, CvssEnvironmentalMetrics? envMetrics = null);
string BuildVectorString(CvssBaseMetrics baseMetrics, CvssThreatMetrics? threatMetrics = null, CvssEnvironmentalMetrics? envMetrics = null, CvssSupplementalMetrics? suppMetrics = null);
(CvssBaseMetrics Base, CvssThreatMetrics? Threat, CvssEnvironmentalMetrics? Env, CvssSupplementalMetrics? Supp) ParseVector(string vectorString);
}
```
### Receipt Builder
```csharp
// Receipt builder interface
public interface IReceiptBuilder
{
Task<CvssScoreReceipt> CreateReceiptAsync(CreateReceiptRequest request, CancellationToken cancellationToken = default);
Task<CvssScoreReceipt> AmendReceiptAsync(string receiptId, AmendReceiptRequest request, CancellationToken cancellationToken = default);
}
```
## Schema Versioning
- Policy schema: `cvss-policy-schema@1.json`
- Receipt schema: `cvss-receipt-schema@1.json`
- Version increment required for breaking changes
- Maintain backward compatibility where possible
## Security Considerations
- Validate all policy inputs against JSON schema
- Sanitize vulnerability IDs to prevent injection
- Sign receipts via DSSE when attestation is required
- Tenant isolation: receipts are tenant-scoped

View File

@@ -0,0 +1,354 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Scoring;
/// <summary>
/// CVSS v4.0 Base metric group - Exploitability and impact metrics.
/// Per FIRST CVSS v4.0 Specification Document.
/// </summary>
public sealed record CvssBaseMetrics
{
/// <summary>Attack Vector (AV) - Mandatory.</summary>
[JsonPropertyName("av")]
public required AttackVector AttackVector { get; init; }
/// <summary>Attack Complexity (AC) - Mandatory.</summary>
[JsonPropertyName("ac")]
public required AttackComplexity AttackComplexity { get; init; }
/// <summary>Attack Requirements (AT) - Mandatory.</summary>
[JsonPropertyName("at")]
public required AttackRequirements AttackRequirements { get; init; }
/// <summary>Privileges Required (PR) - Mandatory.</summary>
[JsonPropertyName("pr")]
public required PrivilegesRequired PrivilegesRequired { get; init; }
/// <summary>User Interaction (UI) - Mandatory.</summary>
[JsonPropertyName("ui")]
public required UserInteraction UserInteraction { get; init; }
/// <summary>Vulnerable System Confidentiality (VC) - Mandatory.</summary>
[JsonPropertyName("vc")]
public required ImpactMetricValue VulnerableSystemConfidentiality { get; init; }
/// <summary>Vulnerable System Integrity (VI) - Mandatory.</summary>
[JsonPropertyName("vi")]
public required ImpactMetricValue VulnerableSystemIntegrity { get; init; }
/// <summary>Vulnerable System Availability (VA) - Mandatory.</summary>
[JsonPropertyName("va")]
public required ImpactMetricValue VulnerableSystemAvailability { get; init; }
/// <summary>Subsequent System Confidentiality (SC) - Mandatory.</summary>
[JsonPropertyName("sc")]
public required ImpactMetricValue SubsequentSystemConfidentiality { get; init; }
/// <summary>Subsequent System Integrity (SI) - Mandatory.</summary>
[JsonPropertyName("si")]
public required ImpactMetricValue SubsequentSystemIntegrity { get; init; }
/// <summary>Subsequent System Availability (SA) - Mandatory.</summary>
[JsonPropertyName("sa")]
public required ImpactMetricValue SubsequentSystemAvailability { get; init; }
}
/// <summary>
/// CVSS v4.0 Threat metric group.
/// </summary>
public sealed record CvssThreatMetrics
{
/// <summary>Exploit Maturity (E) - Optional, defaults to Not Defined.</summary>
[JsonPropertyName("e")]
public ExploitMaturity ExploitMaturity { get; init; } = ExploitMaturity.NotDefined;
}
/// <summary>
/// CVSS v4.0 Environmental metric group - Modified base metrics for specific environments.
/// </summary>
public sealed record CvssEnvironmentalMetrics
{
/// <summary>Modified Attack Vector (MAV).</summary>
[JsonPropertyName("mav")]
public ModifiedAttackVector? ModifiedAttackVector { get; init; }
/// <summary>Modified Attack Complexity (MAC).</summary>
[JsonPropertyName("mac")]
public ModifiedAttackComplexity? ModifiedAttackComplexity { get; init; }
/// <summary>Modified Attack Requirements (MAT).</summary>
[JsonPropertyName("mat")]
public ModifiedAttackRequirements? ModifiedAttackRequirements { get; init; }
/// <summary>Modified Privileges Required (MPR).</summary>
[JsonPropertyName("mpr")]
public ModifiedPrivilegesRequired? ModifiedPrivilegesRequired { get; init; }
/// <summary>Modified User Interaction (MUI).</summary>
[JsonPropertyName("mui")]
public ModifiedUserInteraction? ModifiedUserInteraction { get; init; }
/// <summary>Modified Vulnerable System Confidentiality (MVC).</summary>
[JsonPropertyName("mvc")]
public ModifiedImpactMetricValue? ModifiedVulnerableSystemConfidentiality { get; init; }
/// <summary>Modified Vulnerable System Integrity (MVI).</summary>
[JsonPropertyName("mvi")]
public ModifiedImpactMetricValue? ModifiedVulnerableSystemIntegrity { get; init; }
/// <summary>Modified Vulnerable System Availability (MVA).</summary>
[JsonPropertyName("mva")]
public ModifiedImpactMetricValue? ModifiedVulnerableSystemAvailability { get; init; }
/// <summary>Modified Subsequent System Confidentiality (MSC).</summary>
[JsonPropertyName("msc")]
public ModifiedImpactMetricValue? ModifiedSubsequentSystemConfidentiality { get; init; }
/// <summary>Modified Subsequent System Integrity (MSI).</summary>
[JsonPropertyName("msi")]
public ModifiedSubsequentImpact? ModifiedSubsequentSystemIntegrity { get; init; }
/// <summary>Modified Subsequent System Availability (MSA).</summary>
[JsonPropertyName("msa")]
public ModifiedSubsequentImpact? ModifiedSubsequentSystemAvailability { get; init; }
/// <summary>Confidentiality Requirement (CR).</summary>
[JsonPropertyName("cr")]
public SecurityRequirement? ConfidentialityRequirement { get; init; }
/// <summary>Integrity Requirement (IR).</summary>
[JsonPropertyName("ir")]
public SecurityRequirement? IntegrityRequirement { get; init; }
/// <summary>Availability Requirement (AR).</summary>
[JsonPropertyName("ar")]
public SecurityRequirement? AvailabilityRequirement { get; init; }
}
/// <summary>
/// CVSS v4.0 Supplemental metric group - Additional context metrics that do not affect scoring.
/// </summary>
public sealed record CvssSupplementalMetrics
{
/// <summary>Safety (S) - Does the vulnerability affect human safety?</summary>
[JsonPropertyName("s")]
public Safety? Safety { get; init; }
/// <summary>Automatable (AU) - Can the vulnerability be exploited automatically?</summary>
[JsonPropertyName("au")]
public Automatable? Automatable { get; init; }
/// <summary>Recovery (R) - What is the recovery capability?</summary>
[JsonPropertyName("r")]
public Recovery? Recovery { get; init; }
/// <summary>Value Density (V) - Resource density of the vulnerable system.</summary>
[JsonPropertyName("v")]
public ValueDensity? ValueDensity { get; init; }
/// <summary>Vulnerability Response Effort (RE) - Effort required to respond.</summary>
[JsonPropertyName("re")]
public ResponseEffort? VulnerabilityResponseEffort { get; init; }
/// <summary>Provider Urgency (U) - Urgency as assessed by the provider.</summary>
[JsonPropertyName("u")]
public ProviderUrgency? ProviderUrgency { get; init; }
}
#region Base Metric Enums
/// <summary>Attack Vector values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AttackVector
{
/// <summary>Network (N) - Remotely exploitable.</summary>
Network,
/// <summary>Adjacent (A) - Same network segment.</summary>
Adjacent,
/// <summary>Local (L) - Local access required.</summary>
Local,
/// <summary>Physical (P) - Physical access required.</summary>
Physical
}
/// <summary>Attack Complexity values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AttackComplexity
{
/// <summary>Low (L) - No specialized conditions.</summary>
Low,
/// <summary>High (H) - Specialized conditions required.</summary>
High
}
/// <summary>Attack Requirements values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AttackRequirements
{
/// <summary>None (N) - No preconditions required.</summary>
None,
/// <summary>Present (P) - Preconditions must exist.</summary>
Present
}
/// <summary>Privileges Required values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PrivilegesRequired
{
/// <summary>None (N) - No privileges needed.</summary>
None,
/// <summary>Low (L) - Basic user privileges needed.</summary>
Low,
/// <summary>High (H) - Admin/elevated privileges needed.</summary>
High
}
/// <summary>User Interaction values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum UserInteraction
{
/// <summary>None (N) - No user interaction required.</summary>
None,
/// <summary>Passive (P) - Involuntary user action.</summary>
Passive,
/// <summary>Active (A) - Conscious user action required.</summary>
Active
}
/// <summary>Impact metric values (None/Low/High) per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ImpactMetricValue
{
/// <summary>None (N) - No impact.</summary>
None,
/// <summary>Low (L) - Limited impact.</summary>
Low,
/// <summary>High (H) - Serious impact.</summary>
High
}
#endregion
#region Threat Metric Enums
/// <summary>Exploit Maturity values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ExploitMaturity
{
/// <summary>Not Defined (X) - Not assessed.</summary>
NotDefined,
/// <summary>Attacked (A) - Active exploitation observed.</summary>
Attacked,
/// <summary>Proof of Concept (P) - PoC code exists.</summary>
ProofOfConcept,
/// <summary>Unreported (U) - No public exploit code.</summary>
Unreported
}
#endregion
#region Environmental Metric Enums (Modified versions)
/// <summary>Modified Attack Vector values.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ModifiedAttackVector
{
NotDefined, Network, Adjacent, Local, Physical
}
/// <summary>Modified Attack Complexity values.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ModifiedAttackComplexity
{
NotDefined, Low, High
}
/// <summary>Modified Attack Requirements values.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ModifiedAttackRequirements
{
NotDefined, None, Present
}
/// <summary>Modified Privileges Required values.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ModifiedPrivilegesRequired
{
NotDefined, None, Low, High
}
/// <summary>Modified User Interaction values.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ModifiedUserInteraction
{
NotDefined, None, Passive, Active
}
/// <summary>Modified Impact metric values.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ModifiedImpactMetricValue
{
NotDefined, None, Low, High
}
/// <summary>Modified Subsequent System Impact values (includes Safety dimension).</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ModifiedSubsequentImpact
{
NotDefined, Negligible, Low, High, Safety
}
/// <summary>Security Requirement values.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SecurityRequirement
{
NotDefined, Low, Medium, High
}
#endregion
#region Supplemental Metric Enums
/// <summary>Safety values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Safety
{
NotDefined, Negligible, Present
}
/// <summary>Automatable values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Automatable
{
NotDefined, No, Yes
}
/// <summary>Recovery values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Recovery
{
NotDefined, Automatic, User, Irrecoverable
}
/// <summary>Value Density values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ValueDensity
{
NotDefined, Diffuse, Concentrated
}
/// <summary>Response Effort values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ResponseEffort
{
NotDefined, Low, Moderate, High
}
/// <summary>Provider Urgency values per CVSS v4.0.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ProviderUrgency
{
NotDefined, Clear, Green, Amber, Red
}
#endregion

View File

@@ -0,0 +1,223 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Scoring;
/// <summary>
/// CVSS scoring policy configuration.
/// Defines how CVSS scores are computed and what thresholds apply.
/// </summary>
public sealed record CvssPolicy
{
/// <summary>Unique policy identifier.</summary>
[JsonPropertyName("policyId")]
public required string PolicyId { get; init; }
/// <summary>Policy version (semantic versioning).</summary>
[JsonPropertyName("version")]
public required string Version { get; init; }
/// <summary>Human-readable policy name.</summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>Policy description.</summary>
[JsonPropertyName("description")]
public string? Description { get; init; }
/// <summary>Tenant scope (null for global policy).</summary>
[JsonPropertyName("tenantId")]
public string? TenantId { get; init; }
/// <summary>When this policy becomes effective.</summary>
[JsonPropertyName("effectiveFrom")]
public required DateTimeOffset EffectiveFrom { get; init; }
/// <summary>When this policy expires (null for no expiry).</summary>
[JsonPropertyName("effectiveUntil")]
public DateTimeOffset? EffectiveUntil { get; init; }
/// <summary>Whether this policy is currently active.</summary>
[JsonPropertyName("isActive")]
public bool IsActive { get; init; } = true;
/// <summary>Which score type to use as the effective score by default.</summary>
[JsonPropertyName("defaultEffectiveScoreType")]
public EffectiveScoreType DefaultEffectiveScoreType { get; init; } = EffectiveScoreType.Full;
/// <summary>Default environmental metrics to apply when not provided.</summary>
[JsonPropertyName("defaultEnvironmentalMetrics")]
public CvssEnvironmentalMetrics? DefaultEnvironmentalMetrics { get; init; }
/// <summary>Severity thresholds (override FIRST defaults if specified).</summary>
[JsonPropertyName("severityThresholds")]
public CvssSeverityThresholds? SeverityThresholds { get; init; }
/// <summary>Score rounding configuration.</summary>
[JsonPropertyName("rounding")]
public CvssRoundingConfig Rounding { get; init; } = new();
/// <summary>Evidence requirements for receipts.</summary>
[JsonPropertyName("evidenceRequirements")]
public CvssEvidenceRequirements? EvidenceRequirements { get; init; }
/// <summary>Attestation requirements.</summary>
[JsonPropertyName("attestationRequirements")]
public CvssAttestationRequirements? AttestationRequirements { get; init; }
/// <summary>Metric overrides for specific vulnerability patterns.</summary>
[JsonPropertyName("metricOverrides")]
public ImmutableList<CvssMetricOverride> MetricOverrides { get; init; } = [];
/// <summary>SHA-256 hash of this policy for determinism tracking.</summary>
[JsonPropertyName("hash")]
public string? Hash { get; init; }
/// <summary>When this policy was created.</summary>
[JsonPropertyName("createdAt")]
public DateTimeOffset? CreatedAt { get; init; }
/// <summary>Who created this policy.</summary>
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; init; }
}
/// <summary>
/// Severity threshold configuration.
/// </summary>
public sealed record CvssSeverityThresholds
{
/// <summary>Low severity lower bound (default: 0.1).</summary>
[JsonPropertyName("lowMin")]
public double LowMin { get; init; } = 0.1;
/// <summary>Medium severity lower bound (default: 4.0).</summary>
[JsonPropertyName("mediumMin")]
public double MediumMin { get; init; } = 4.0;
/// <summary>High severity lower bound (default: 7.0).</summary>
[JsonPropertyName("highMin")]
public double HighMin { get; init; } = 7.0;
/// <summary>Critical severity lower bound (default: 9.0).</summary>
[JsonPropertyName("criticalMin")]
public double CriticalMin { get; init; } = 9.0;
}
/// <summary>
/// Score rounding configuration.
/// </summary>
public sealed record CvssRoundingConfig
{
/// <summary>Number of decimal places for scores (default: 1).</summary>
[JsonPropertyName("decimalPlaces")]
public int DecimalPlaces { get; init; } = 1;
/// <summary>Rounding mode (default: roundUp per FIRST spec).</summary>
[JsonPropertyName("mode")]
public CvssRoundingMode Mode { get; init; } = CvssRoundingMode.RoundUp;
}
/// <summary>
/// Rounding modes for CVSS scores.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CvssRoundingMode
{
/// <summary>Round up to nearest tenth (FIRST spec default).</summary>
RoundUp,
/// <summary>Standard mathematical rounding.</summary>
Standard,
/// <summary>Always round down.</summary>
RoundDown
}
/// <summary>
/// Evidence requirements configuration.
/// </summary>
public sealed record CvssEvidenceRequirements
{
/// <summary>Minimum number of evidence items required.</summary>
[JsonPropertyName("minimumCount")]
public int MinimumCount { get; init; }
/// <summary>Whether authoritative evidence is required.</summary>
[JsonPropertyName("requireAuthoritative")]
public bool RequireAuthoritative { get; init; }
/// <summary>Required evidence types.</summary>
[JsonPropertyName("requiredTypes")]
public ImmutableList<string> RequiredTypes { get; init; } = [];
/// <summary>Maximum age of evidence in days.</summary>
[JsonPropertyName("maxAgeInDays")]
public int? MaxAgeInDays { get; init; }
}
/// <summary>
/// Attestation requirements configuration.
/// </summary>
public sealed record CvssAttestationRequirements
{
/// <summary>Whether DSSE attestation is required.</summary>
[JsonPropertyName("requireDsse")]
public bool RequireDsse { get; init; }
/// <summary>Whether Rekor transparency log registration is required.</summary>
[JsonPropertyName("requireRekor")]
public bool RequireRekor { get; init; }
/// <summary>Acceptable signing key identities.</summary>
[JsonPropertyName("allowedSigners")]
public ImmutableList<string> AllowedSigners { get; init; } = [];
/// <summary>Minimum trust level for attestations.</summary>
[JsonPropertyName("minimumTrustLevel")]
public string? MinimumTrustLevel { get; init; }
}
/// <summary>
/// Metric override for specific vulnerability patterns.
/// </summary>
public sealed record CvssMetricOverride
{
/// <summary>Override identifier.</summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>Human-readable description.</summary>
[JsonPropertyName("description")]
public string? Description { get; init; }
/// <summary>Pattern to match vulnerability IDs (regex).</summary>
[JsonPropertyName("vulnerabilityPattern")]
public string? VulnerabilityPattern { get; init; }
/// <summary>Specific vulnerability IDs to match.</summary>
[JsonPropertyName("vulnerabilityIds")]
public ImmutableList<string> VulnerabilityIds { get; init; } = [];
/// <summary>CWE IDs to match.</summary>
[JsonPropertyName("cweIds")]
public ImmutableList<string> CweIds { get; init; } = [];
/// <summary>Environmental metric overrides to apply.</summary>
[JsonPropertyName("environmentalOverrides")]
public CvssEnvironmentalMetrics? EnvironmentalOverrides { get; init; }
/// <summary>Score adjustment (added to final score).</summary>
[JsonPropertyName("scoreAdjustment")]
public double? ScoreAdjustment { get; init; }
/// <summary>Priority for conflict resolution (higher wins).</summary>
[JsonPropertyName("priority")]
public int Priority { get; init; }
/// <summary>Whether this override is active.</summary>
[JsonPropertyName("isActive")]
public bool IsActive { get; init; } = true;
/// <summary>Reason for this override.</summary>
[JsonPropertyName("reason")]
public string? Reason { get; init; }
}

View File

@@ -0,0 +1,297 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Scoring;
/// <summary>
/// A CVSS v4.0 Score Receipt with complete audit trail.
/// Provides deterministic, reproducible scoring with full provenance.
/// </summary>
public sealed record CvssScoreReceipt
{
/// <summary>Unique receipt identifier.</summary>
[JsonPropertyName("receiptId")]
public required string ReceiptId { get; init; }
/// <summary>Schema version for this receipt format.</summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; init; } = "1.0.0";
/// <summary>Receipt format specification.</summary>
[JsonPropertyName("format")]
public string Format { get; init; } = "stella.ops/cvssReceipt@v1";
/// <summary>Vulnerability identifier (CVE, GHSA, etc.).</summary>
[JsonPropertyName("vulnerabilityId")]
public required string VulnerabilityId { get; init; }
/// <summary>Tenant scope for multi-tenant deployments.</summary>
[JsonPropertyName("tenantId")]
public required string TenantId { get; init; }
/// <summary>Timestamp when the receipt was created (UTC ISO-8601).</summary>
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>User or system that created the receipt.</summary>
[JsonPropertyName("createdBy")]
public required string CreatedBy { get; init; }
/// <summary>Timestamp when the receipt was last modified.</summary>
[JsonPropertyName("modifiedAt")]
public DateTimeOffset? ModifiedAt { get; init; }
/// <summary>User or system that last modified the receipt.</summary>
[JsonPropertyName("modifiedBy")]
public string? ModifiedBy { get; init; }
/// <summary>CVSS version used (4.0).</summary>
[JsonPropertyName("cvssVersion")]
public string CvssVersion { get; init; } = "4.0";
/// <summary>Base metrics input.</summary>
[JsonPropertyName("baseMetrics")]
public required CvssBaseMetrics BaseMetrics { get; init; }
/// <summary>Threat metrics input (optional).</summary>
[JsonPropertyName("threatMetrics")]
public CvssThreatMetrics? ThreatMetrics { get; init; }
/// <summary>Environmental metrics input (optional).</summary>
[JsonPropertyName("environmentalMetrics")]
public CvssEnvironmentalMetrics? EnvironmentalMetrics { get; init; }
/// <summary>Supplemental metrics (optional, do not affect score).</summary>
[JsonPropertyName("supplementalMetrics")]
public CvssSupplementalMetrics? SupplementalMetrics { get; init; }
/// <summary>Computed scores.</summary>
[JsonPropertyName("scores")]
public required CvssScores Scores { get; init; }
/// <summary>Computed CVSS v4.0 vector string.</summary>
[JsonPropertyName("vectorString")]
public required string VectorString { get; init; }
/// <summary>Severity rating based on final score.</summary>
[JsonPropertyName("severity")]
public required CvssSeverity Severity { get; init; }
/// <summary>Policy that was applied to compute this receipt.</summary>
[JsonPropertyName("policyRef")]
public required CvssPolicyReference PolicyRef { get; init; }
/// <summary>Evidence items supporting this score.</summary>
[JsonPropertyName("evidence")]
public ImmutableList<CvssEvidenceItem> Evidence { get; init; } = [];
/// <summary>DSSE attestation envelope references, if signed.</summary>
[JsonPropertyName("attestationRefs")]
public ImmutableList<string> AttestationRefs { get; init; } = [];
/// <summary>SHA-256 hash of deterministic input for reproducibility.</summary>
[JsonPropertyName("inputHash")]
public required string InputHash { get; init; }
/// <summary>Amendment history entries.</summary>
[JsonPropertyName("history")]
public ImmutableList<ReceiptHistoryEntry> History { get; init; } = [];
/// <summary>Original receipt ID if this is an amendment.</summary>
[JsonPropertyName("amendsReceiptId")]
public string? AmendsReceiptId { get; init; }
/// <summary>Whether this receipt is the current active version.</summary>
[JsonPropertyName("isActive")]
public bool IsActive { get; init; } = true;
/// <summary>Reason this receipt was superseded (if not active).</summary>
[JsonPropertyName("supersededReason")]
public string? SupersededReason { get; init; }
}
/// <summary>
/// Computed CVSS v4.0 scores.
/// </summary>
public sealed record CvssScores
{
/// <summary>Base Score (CVSS-B) - Impact of the vulnerability.</summary>
[JsonPropertyName("baseScore")]
public required double BaseScore { get; init; }
/// <summary>Threat-Adjusted Score (CVSS-BT) - Base + threat metrics.</summary>
[JsonPropertyName("threatScore")]
public double? ThreatScore { get; init; }
/// <summary>Environmental Score (CVSS-BE) - Base + environmental metrics.</summary>
[JsonPropertyName("environmentalScore")]
public double? EnvironmentalScore { get; init; }
/// <summary>Full Score (CVSS-BTE) - Base + threat + environmental.</summary>
[JsonPropertyName("fullScore")]
public double? FullScore { get; init; }
/// <summary>Which score is considered the "effective" score for policy decisions.</summary>
[JsonPropertyName("effectiveScore")]
public required double EffectiveScore { get; init; }
/// <summary>Which score type was used as effective.</summary>
[JsonPropertyName("effectiveScoreType")]
public required EffectiveScoreType EffectiveScoreType { get; init; }
}
/// <summary>
/// Indicates which score type was used as the effective score.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EffectiveScoreType
{
/// <summary>Base score only (CVSS-B).</summary>
Base,
/// <summary>Base + Threat (CVSS-BT).</summary>
Threat,
/// <summary>Base + Environmental (CVSS-BE).</summary>
Environmental,
/// <summary>Full score with all metrics (CVSS-BTE).</summary>
Full
}
/// <summary>
/// CVSS v4.0 severity ratings.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CvssSeverity
{
/// <summary>None - Score 0.0.</summary>
None,
/// <summary>Low - Score 0.1-3.9.</summary>
Low,
/// <summary>Medium - Score 4.0-6.9.</summary>
Medium,
/// <summary>High - Score 7.0-8.9.</summary>
High,
/// <summary>Critical - Score 9.0-10.0.</summary>
Critical
}
/// <summary>
/// Reference to the policy used for scoring.
/// </summary>
public sealed record CvssPolicyReference
{
/// <summary>Policy identifier.</summary>
[JsonPropertyName("policyId")]
public required string PolicyId { get; init; }
/// <summary>Policy version.</summary>
[JsonPropertyName("version")]
public required string Version { get; init; }
/// <summary>SHA-256 hash of the policy content.</summary>
[JsonPropertyName("hash")]
public required string Hash { get; init; }
/// <summary>When the policy was activated.</summary>
[JsonPropertyName("activatedAt")]
public DateTimeOffset? ActivatedAt { get; init; }
}
/// <summary>
/// Evidence item supporting a CVSS score.
/// </summary>
public sealed record CvssEvidenceItem
{
/// <summary>Evidence type (advisory, vex, scan, etc.).</summary>
[JsonPropertyName("type")]
public required string Type { get; init; }
/// <summary>Content-addressable storage URI (e.g., sha256:...).</summary>
[JsonPropertyName("uri")]
public required string Uri { get; init; }
/// <summary>Human-readable description of the evidence.</summary>
[JsonPropertyName("description")]
public string? Description { get; init; }
/// <summary>When the evidence was collected.</summary>
[JsonPropertyName("collectedAt")]
public DateTimeOffset? CollectedAt { get; init; }
/// <summary>Source of the evidence (vendor, scanner, manual).</summary>
[JsonPropertyName("source")]
public string? Source { get; init; }
/// <summary>Whether this evidence is from the vendor/authority.</summary>
[JsonPropertyName("isAuthoritative")]
public bool IsAuthoritative { get; init; }
}
/// <summary>
/// History entry for receipt amendments.
/// </summary>
public sealed record ReceiptHistoryEntry
{
/// <summary>Unique history entry identifier.</summary>
[JsonPropertyName("historyId")]
public required string HistoryId { get; init; }
/// <summary>When the amendment was made.</summary>
[JsonPropertyName("timestamp")]
public required DateTimeOffset Timestamp { get; init; }
/// <summary>User or system that made the amendment.</summary>
[JsonPropertyName("actor")]
public required string Actor { get; init; }
/// <summary>Type of change (amend, supersede, revoke).</summary>
[JsonPropertyName("changeType")]
public required ReceiptChangeType ChangeType { get; init; }
/// <summary>Field that was changed.</summary>
[JsonPropertyName("field")]
public required string Field { get; init; }
/// <summary>Previous value (JSON encoded).</summary>
[JsonPropertyName("previousValue")]
public string? PreviousValue { get; init; }
/// <summary>New value (JSON encoded).</summary>
[JsonPropertyName("newValue")]
public string? NewValue { get; init; }
/// <summary>Reason for the change.</summary>
[JsonPropertyName("reason")]
public required string Reason { get; init; }
/// <summary>Reference URI for supporting documentation.</summary>
[JsonPropertyName("referenceUri")]
public string? ReferenceUri { get; init; }
/// <summary>Signature of this history entry for integrity.</summary>
[JsonPropertyName("signature")]
public string? Signature { get; init; }
}
/// <summary>
/// Types of changes to a receipt.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ReceiptChangeType
{
/// <summary>Initial creation.</summary>
Created,
/// <summary>Field value amended.</summary>
Amended,
/// <summary>Receipt superseded by newer version.</summary>
Superseded,
/// <summary>Receipt revoked/invalidated.</summary>
Revoked,
/// <summary>Evidence added.</summary>
EvidenceAdded,
/// <summary>Attestation signed.</summary>
AttestationSigned,
/// <summary>Policy updated.</summary>
PolicyUpdated,
/// <summary>Score recalculated.</summary>
Recalculated
}

View File

@@ -0,0 +1,809 @@
using System.Text;
using System.Text.RegularExpressions;
namespace StellaOps.Policy.Scoring.Engine;
/// <summary>
/// CVSS v4.0 scoring engine implementation.
/// Implements FIRST CVSS v4.0 specification with MacroVector-based scoring.
/// </summary>
public sealed partial class CvssV4Engine : ICvssV4Engine
{
/// <inheritdoc />
public CvssScores ComputeScores(
CvssBaseMetrics baseMetrics,
CvssThreatMetrics? threatMetrics = null,
CvssEnvironmentalMetrics? environmentalMetrics = null)
{
ArgumentNullException.ThrowIfNull(baseMetrics);
// Compute base score (CVSS-B)
var baseScore = ComputeBaseScore(baseMetrics);
// Compute threat score (CVSS-BT) if threat metrics provided
double? threatScore = null;
if (threatMetrics is not null && threatMetrics.ExploitMaturity != ExploitMaturity.NotDefined)
{
threatScore = ComputeThreatScore(baseMetrics, threatMetrics);
}
// Compute environmental score (CVSS-BE) if environmental metrics provided
double? environmentalScore = null;
if (environmentalMetrics is not null && HasEnvironmentalMetrics(environmentalMetrics))
{
environmentalScore = ComputeEnvironmentalScore(baseMetrics, environmentalMetrics);
}
// Compute full score (CVSS-BTE) if both threat and environmental metrics provided
double? fullScore = null;
if (threatMetrics is not null && environmentalMetrics is not null &&
threatMetrics.ExploitMaturity != ExploitMaturity.NotDefined &&
HasEnvironmentalMetrics(environmentalMetrics))
{
fullScore = ComputeFullScore(baseMetrics, threatMetrics, environmentalMetrics);
}
// Determine effective score and type
var (effectiveScore, effectiveType) = DetermineEffectiveScore(
baseScore, threatScore, environmentalScore, fullScore);
return new CvssScores
{
BaseScore = baseScore,
ThreatScore = threatScore,
EnvironmentalScore = environmentalScore,
FullScore = fullScore,
EffectiveScore = effectiveScore,
EffectiveScoreType = effectiveType
};
}
/// <inheritdoc />
public string BuildVectorString(
CvssBaseMetrics baseMetrics,
CvssThreatMetrics? threatMetrics = null,
CvssEnvironmentalMetrics? environmentalMetrics = null,
CvssSupplementalMetrics? supplementalMetrics = null)
{
ArgumentNullException.ThrowIfNull(baseMetrics);
var sb = new StringBuilder("CVSS:4.0");
// Base metrics (mandatory)
sb.Append($"/AV:{MetricToString(baseMetrics.AttackVector)}");
sb.Append($"/AC:{MetricToString(baseMetrics.AttackComplexity)}");
sb.Append($"/AT:{MetricToString(baseMetrics.AttackRequirements)}");
sb.Append($"/PR:{MetricToString(baseMetrics.PrivilegesRequired)}");
sb.Append($"/UI:{MetricToString(baseMetrics.UserInteraction)}");
sb.Append($"/VC:{MetricToString(baseMetrics.VulnerableSystemConfidentiality)}");
sb.Append($"/VI:{MetricToString(baseMetrics.VulnerableSystemIntegrity)}");
sb.Append($"/VA:{MetricToString(baseMetrics.VulnerableSystemAvailability)}");
sb.Append($"/SC:{MetricToString(baseMetrics.SubsequentSystemConfidentiality)}");
sb.Append($"/SI:{MetricToString(baseMetrics.SubsequentSystemIntegrity)}");
sb.Append($"/SA:{MetricToString(baseMetrics.SubsequentSystemAvailability)}");
// Threat metrics (optional)
if (threatMetrics is not null && threatMetrics.ExploitMaturity != ExploitMaturity.NotDefined)
{
sb.Append($"/E:{MetricToString(threatMetrics.ExploitMaturity)}");
}
// Environmental metrics (optional, only include if not NotDefined)
if (environmentalMetrics is not null)
{
AppendEnvironmentalMetrics(sb, environmentalMetrics);
}
// Supplemental metrics (optional, only include if not NotDefined)
if (supplementalMetrics is not null)
{
AppendSupplementalMetrics(sb, supplementalMetrics);
}
return sb.ToString();
}
/// <inheritdoc />
public CvssMetricSet ParseVector(string vectorString)
{
if (string.IsNullOrWhiteSpace(vectorString))
throw new ArgumentException("Vector string cannot be null or empty.", nameof(vectorString));
if (!vectorString.StartsWith("CVSS:4.0/", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("Vector string must start with 'CVSS:4.0/'.", nameof(vectorString));
var metrics = ParseMetricsFromVector(vectorString[9..]);
// Parse base metrics (all required)
var baseMetrics = new CvssBaseMetrics
{
AttackVector = ParseAttackVector(GetRequiredMetric(metrics, "AV")),
AttackComplexity = ParseAttackComplexity(GetRequiredMetric(metrics, "AC")),
AttackRequirements = ParseAttackRequirements(GetRequiredMetric(metrics, "AT")),
PrivilegesRequired = ParsePrivilegesRequired(GetRequiredMetric(metrics, "PR")),
UserInteraction = ParseUserInteraction(GetRequiredMetric(metrics, "UI")),
VulnerableSystemConfidentiality = ParseImpactMetric(GetRequiredMetric(metrics, "VC")),
VulnerableSystemIntegrity = ParseImpactMetric(GetRequiredMetric(metrics, "VI")),
VulnerableSystemAvailability = ParseImpactMetric(GetRequiredMetric(metrics, "VA")),
SubsequentSystemConfidentiality = ParseImpactMetric(GetRequiredMetric(metrics, "SC")),
SubsequentSystemIntegrity = ParseImpactMetric(GetRequiredMetric(metrics, "SI")),
SubsequentSystemAvailability = ParseImpactMetric(GetRequiredMetric(metrics, "SA"))
};
// Parse threat metrics
CvssThreatMetrics? threatMetrics = null;
if (metrics.TryGetValue("E", out var e))
{
threatMetrics = new CvssThreatMetrics
{
ExploitMaturity = ParseExploitMaturity(e)
};
}
// Parse environmental metrics
var envMetrics = ParseEnvironmentalMetrics(metrics);
// Parse supplemental metrics
var suppMetrics = ParseSupplementalMetrics(metrics);
return new CvssMetricSet
{
BaseMetrics = baseMetrics,
ThreatMetrics = threatMetrics,
EnvironmentalMetrics = envMetrics,
SupplementalMetrics = suppMetrics
};
}
/// <inheritdoc />
public CvssSeverity GetSeverity(double score, CvssSeverityThresholds? thresholds = null)
{
thresholds ??= new CvssSeverityThresholds();
return score switch
{
0.0 => CvssSeverity.None,
>= 9.0 when score >= thresholds.CriticalMin => CvssSeverity.Critical,
>= 7.0 when score >= thresholds.HighMin => CvssSeverity.High,
>= 4.0 when score >= thresholds.MediumMin => CvssSeverity.Medium,
> 0.0 when score >= thresholds.LowMin => CvssSeverity.Low,
_ => CvssSeverity.None
};
}
#region Score Computation
private static double ComputeBaseScore(CvssBaseMetrics metrics)
{
// Get MacroVector from base metrics
var macroVector = GetMacroVector(metrics);
// Look up base score from MacroVector lookup table
var score = MacroVectorLookup.GetBaseScore(macroVector);
return RoundUp(score);
}
private static double ComputeThreatScore(CvssBaseMetrics baseMetrics, CvssThreatMetrics threatMetrics)
{
// Get base score first
var macroVector = GetMacroVector(baseMetrics);
var baseScore = MacroVectorLookup.GetBaseScore(macroVector);
// Apply threat multiplier based on Exploit Maturity
var threatMultiplier = GetThreatMultiplier(threatMetrics.ExploitMaturity);
// Threat score = Base * Threat Multiplier
var threatScore = baseScore * threatMultiplier;
return RoundUp(threatScore);
}
private static double ComputeEnvironmentalScore(CvssBaseMetrics baseMetrics, CvssEnvironmentalMetrics envMetrics)
{
// Apply modified metrics to base metrics
var modifiedMetrics = ApplyEnvironmentalModifiers(baseMetrics, envMetrics);
// Compute modified base score
var macroVector = GetMacroVector(modifiedMetrics);
var modifiedBaseScore = MacroVectorLookup.GetBaseScore(macroVector);
// Apply security requirements
var requirementsMultiplier = GetRequirementsMultiplier(envMetrics);
var envScore = modifiedBaseScore * requirementsMultiplier;
return Math.Min(RoundUp(envScore), 10.0);
}
private static double ComputeFullScore(
CvssBaseMetrics baseMetrics,
CvssThreatMetrics threatMetrics,
CvssEnvironmentalMetrics envMetrics)
{
// Apply modified metrics to base metrics
var modifiedMetrics = ApplyEnvironmentalModifiers(baseMetrics, envMetrics);
// Compute modified base score
var macroVector = GetMacroVector(modifiedMetrics);
var modifiedBaseScore = MacroVectorLookup.GetBaseScore(macroVector);
// Apply threat multiplier
var threatMultiplier = GetThreatMultiplier(threatMetrics.ExploitMaturity);
// Apply security requirements
var requirementsMultiplier = GetRequirementsMultiplier(envMetrics);
// Full score = Modified Base * Threat * Requirements
var fullScore = modifiedBaseScore * threatMultiplier * requirementsMultiplier;
return Math.Min(RoundUp(fullScore), 10.0);
}
private static (double Score, EffectiveScoreType Type) DetermineEffectiveScore(
double baseScore,
double? threatScore,
double? environmentalScore,
double? fullScore)
{
// Priority: Full > Environmental > Threat > Base
if (fullScore.HasValue)
return (fullScore.Value, EffectiveScoreType.Full);
if (environmentalScore.HasValue)
return (environmentalScore.Value, EffectiveScoreType.Environmental);
if (threatScore.HasValue)
return (threatScore.Value, EffectiveScoreType.Threat);
return (baseScore, EffectiveScoreType.Base);
}
#endregion
#region MacroVector Computation
private static string GetMacroVector(CvssBaseMetrics metrics)
{
// Build MacroVector string from EQ (Equivalence) values
// Per CVSS v4.0 spec: EQ1-EQ6 define the MacroVector
var eq1 = GetEQ1(metrics);
var eq2 = GetEQ2(metrics);
var eq3 = GetEQ3(metrics);
var eq4 = GetEQ4(metrics);
var eq5 = GetEQ5(metrics);
var eq6 = GetEQ6(metrics);
return $"{eq1}{eq2}{eq3}{eq4}{eq5}{eq6}";
}
private static int GetEQ1(CvssBaseMetrics m)
{
// EQ1: Attack Vector + Privileges Required
return (m.AttackVector, m.PrivilegesRequired) switch
{
(AttackVector.Network, PrivilegesRequired.None) => 0,
(AttackVector.Network, PrivilegesRequired.Low) => 1,
(AttackVector.Network, PrivilegesRequired.High) => 1,
(AttackVector.Adjacent, PrivilegesRequired.None) => 1,
(AttackVector.Adjacent, PrivilegesRequired.Low) => 2,
(AttackVector.Adjacent, PrivilegesRequired.High) => 2,
(AttackVector.Local, _) => 2,
(AttackVector.Physical, _) => 2,
_ => 2
};
}
private static int GetEQ2(CvssBaseMetrics m)
{
// EQ2: Attack Complexity + User Interaction
return (m.AttackComplexity, m.UserInteraction) switch
{
(AttackComplexity.Low, UserInteraction.None) => 0,
(AttackComplexity.Low, UserInteraction.Passive) => 1,
(AttackComplexity.Low, UserInteraction.Active) => 1,
(AttackComplexity.High, _) => 1,
_ => 1
};
}
private static int GetEQ3(CvssBaseMetrics m)
{
// EQ3: Vulnerable System CIA (highest impact)
var vc = m.VulnerableSystemConfidentiality;
var vi = m.VulnerableSystemIntegrity;
var va = m.VulnerableSystemAvailability;
if (vc == ImpactMetricValue.High || vi == ImpactMetricValue.High || va == ImpactMetricValue.High)
return 0;
if (vc == ImpactMetricValue.Low || vi == ImpactMetricValue.Low || va == ImpactMetricValue.Low)
return 1;
return 2;
}
private static int GetEQ4(CvssBaseMetrics m)
{
// EQ4: Subsequent System CIA (highest impact)
var sc = m.SubsequentSystemConfidentiality;
var si = m.SubsequentSystemIntegrity;
var sa = m.SubsequentSystemAvailability;
if (sc == ImpactMetricValue.High || si == ImpactMetricValue.High || sa == ImpactMetricValue.High)
return 0;
if (sc == ImpactMetricValue.Low || si == ImpactMetricValue.Low || sa == ImpactMetricValue.Low)
return 1;
return 2;
}
private static int GetEQ5(CvssBaseMetrics m)
{
// EQ5: Attack Requirements
return m.AttackRequirements == AttackRequirements.None ? 0 : 1;
}
private static int GetEQ6(CvssBaseMetrics m)
{
// EQ6: Combined impact pattern
var vcHigh = m.VulnerableSystemConfidentiality == ImpactMetricValue.High;
var viHigh = m.VulnerableSystemIntegrity == ImpactMetricValue.High;
var vaHigh = m.VulnerableSystemAvailability == ImpactMetricValue.High;
var scHigh = m.SubsequentSystemConfidentiality == ImpactMetricValue.High;
var siHigh = m.SubsequentSystemIntegrity == ImpactMetricValue.High;
var saHigh = m.SubsequentSystemAvailability == ImpactMetricValue.High;
// Count high impacts
var vulnHighCount = (vcHigh ? 1 : 0) + (viHigh ? 1 : 0) + (vaHigh ? 1 : 0);
var subHighCount = (scHigh ? 1 : 0) + (siHigh ? 1 : 0) + (saHigh ? 1 : 0);
if (vulnHighCount >= 2 || subHighCount >= 2)
return 0;
if (vulnHighCount == 1 || subHighCount == 1)
return 1;
return 2;
}
#endregion
#region Multipliers
private static double GetThreatMultiplier(ExploitMaturity exploitMaturity)
{
return exploitMaturity switch
{
ExploitMaturity.Attacked => 1.0,
ExploitMaturity.ProofOfConcept => 0.94,
ExploitMaturity.Unreported => 0.91,
ExploitMaturity.NotDefined => 1.0,
_ => 1.0
};
}
private static double GetRequirementsMultiplier(CvssEnvironmentalMetrics envMetrics)
{
var crMultiplier = GetSecurityRequirementMultiplier(envMetrics.ConfidentialityRequirement);
var irMultiplier = GetSecurityRequirementMultiplier(envMetrics.IntegrityRequirement);
var arMultiplier = GetSecurityRequirementMultiplier(envMetrics.AvailabilityRequirement);
// Average of requirements multipliers
return (crMultiplier + irMultiplier + arMultiplier) / 3.0;
}
private static double GetSecurityRequirementMultiplier(SecurityRequirement? requirement)
{
return requirement switch
{
SecurityRequirement.High => 1.5,
SecurityRequirement.Medium => 1.0,
SecurityRequirement.Low => 0.5,
SecurityRequirement.NotDefined => 1.0,
null => 1.0,
_ => 1.0
};
}
#endregion
#region Environmental Modifiers
private static CvssBaseMetrics ApplyEnvironmentalModifiers(
CvssBaseMetrics baseMetrics,
CvssEnvironmentalMetrics envMetrics)
{
return new CvssBaseMetrics
{
AttackVector = GetEffectiveAttackVector(baseMetrics.AttackVector, envMetrics.ModifiedAttackVector),
AttackComplexity = GetEffectiveAttackComplexity(baseMetrics.AttackComplexity, envMetrics.ModifiedAttackComplexity),
AttackRequirements = GetEffectiveAttackRequirements(baseMetrics.AttackRequirements, envMetrics.ModifiedAttackRequirements),
PrivilegesRequired = GetEffectivePrivilegesRequired(baseMetrics.PrivilegesRequired, envMetrics.ModifiedPrivilegesRequired),
UserInteraction = GetEffectiveUserInteraction(baseMetrics.UserInteraction, envMetrics.ModifiedUserInteraction),
VulnerableSystemConfidentiality = GetEffectiveImpact(baseMetrics.VulnerableSystemConfidentiality, envMetrics.ModifiedVulnerableSystemConfidentiality),
VulnerableSystemIntegrity = GetEffectiveImpact(baseMetrics.VulnerableSystemIntegrity, envMetrics.ModifiedVulnerableSystemIntegrity),
VulnerableSystemAvailability = GetEffectiveImpact(baseMetrics.VulnerableSystemAvailability, envMetrics.ModifiedVulnerableSystemAvailability),
SubsequentSystemConfidentiality = GetEffectiveImpact(baseMetrics.SubsequentSystemConfidentiality, envMetrics.ModifiedSubsequentSystemConfidentiality),
SubsequentSystemIntegrity = GetEffectiveSubsequentImpact(baseMetrics.SubsequentSystemIntegrity, envMetrics.ModifiedSubsequentSystemIntegrity),
SubsequentSystemAvailability = GetEffectiveSubsequentImpact(baseMetrics.SubsequentSystemAvailability, envMetrics.ModifiedSubsequentSystemAvailability)
};
}
private static AttackVector GetEffectiveAttackVector(AttackVector baseValue, ModifiedAttackVector? modified)
{
return modified switch
{
ModifiedAttackVector.NotDefined or null => baseValue,
ModifiedAttackVector.Network => AttackVector.Network,
ModifiedAttackVector.Adjacent => AttackVector.Adjacent,
ModifiedAttackVector.Local => AttackVector.Local,
ModifiedAttackVector.Physical => AttackVector.Physical,
_ => baseValue
};
}
private static AttackComplexity GetEffectiveAttackComplexity(AttackComplexity baseValue, ModifiedAttackComplexity? modified)
{
return modified switch
{
ModifiedAttackComplexity.NotDefined or null => baseValue,
ModifiedAttackComplexity.Low => AttackComplexity.Low,
ModifiedAttackComplexity.High => AttackComplexity.High,
_ => baseValue
};
}
private static AttackRequirements GetEffectiveAttackRequirements(AttackRequirements baseValue, ModifiedAttackRequirements? modified)
{
return modified switch
{
ModifiedAttackRequirements.NotDefined or null => baseValue,
ModifiedAttackRequirements.None => AttackRequirements.None,
ModifiedAttackRequirements.Present => AttackRequirements.Present,
_ => baseValue
};
}
private static PrivilegesRequired GetEffectivePrivilegesRequired(PrivilegesRequired baseValue, ModifiedPrivilegesRequired? modified)
{
return modified switch
{
ModifiedPrivilegesRequired.NotDefined or null => baseValue,
ModifiedPrivilegesRequired.None => PrivilegesRequired.None,
ModifiedPrivilegesRequired.Low => PrivilegesRequired.Low,
ModifiedPrivilegesRequired.High => PrivilegesRequired.High,
_ => baseValue
};
}
private static UserInteraction GetEffectiveUserInteraction(UserInteraction baseValue, ModifiedUserInteraction? modified)
{
return modified switch
{
ModifiedUserInteraction.NotDefined or null => baseValue,
ModifiedUserInteraction.None => UserInteraction.None,
ModifiedUserInteraction.Passive => UserInteraction.Passive,
ModifiedUserInteraction.Active => UserInteraction.Active,
_ => baseValue
};
}
private static ImpactMetricValue GetEffectiveImpact(ImpactMetricValue baseValue, ModifiedImpactMetricValue? modified)
{
return modified switch
{
ModifiedImpactMetricValue.NotDefined or null => baseValue,
ModifiedImpactMetricValue.None => ImpactMetricValue.None,
ModifiedImpactMetricValue.Low => ImpactMetricValue.Low,
ModifiedImpactMetricValue.High => ImpactMetricValue.High,
_ => baseValue
};
}
private static ImpactMetricValue GetEffectiveSubsequentImpact(ImpactMetricValue baseValue, ModifiedSubsequentImpact? modified)
{
return modified switch
{
ModifiedSubsequentImpact.NotDefined or null => baseValue,
ModifiedSubsequentImpact.Negligible or ModifiedSubsequentImpact.Low => ImpactMetricValue.Low,
ModifiedSubsequentImpact.High or ModifiedSubsequentImpact.Safety => ImpactMetricValue.High,
_ => baseValue
};
}
#endregion
#region Helpers
private static bool HasEnvironmentalMetrics(CvssEnvironmentalMetrics envMetrics)
{
return envMetrics.ModifiedAttackVector is not null and not ModifiedAttackVector.NotDefined ||
envMetrics.ModifiedAttackComplexity is not null and not ModifiedAttackComplexity.NotDefined ||
envMetrics.ModifiedAttackRequirements is not null and not ModifiedAttackRequirements.NotDefined ||
envMetrics.ModifiedPrivilegesRequired is not null and not ModifiedPrivilegesRequired.NotDefined ||
envMetrics.ModifiedUserInteraction is not null and not ModifiedUserInteraction.NotDefined ||
envMetrics.ModifiedVulnerableSystemConfidentiality is not null and not ModifiedImpactMetricValue.NotDefined ||
envMetrics.ModifiedVulnerableSystemIntegrity is not null and not ModifiedImpactMetricValue.NotDefined ||
envMetrics.ModifiedVulnerableSystemAvailability is not null and not ModifiedImpactMetricValue.NotDefined ||
envMetrics.ModifiedSubsequentSystemConfidentiality is not null and not ModifiedImpactMetricValue.NotDefined ||
envMetrics.ModifiedSubsequentSystemIntegrity is not null and not ModifiedSubsequentImpact.NotDefined ||
envMetrics.ModifiedSubsequentSystemAvailability is not null and not ModifiedSubsequentImpact.NotDefined ||
envMetrics.ConfidentialityRequirement is not null and not SecurityRequirement.NotDefined ||
envMetrics.IntegrityRequirement is not null and not SecurityRequirement.NotDefined ||
envMetrics.AvailabilityRequirement is not null and not SecurityRequirement.NotDefined;
}
/// <summary>
/// Round up to one decimal place per FIRST CVSS v4.0 specification.
/// </summary>
private static double RoundUp(double value)
{
return Math.Ceiling(value * 10) / 10;
}
#endregion
#region Vector String Building
private static string MetricToString(AttackVector av) =>
av switch { AttackVector.Network => "N", AttackVector.Adjacent => "A", AttackVector.Local => "L", AttackVector.Physical => "P", _ => "N" };
private static string MetricToString(AttackComplexity ac) =>
ac switch { AttackComplexity.Low => "L", AttackComplexity.High => "H", _ => "L" };
private static string MetricToString(AttackRequirements at) =>
at switch { AttackRequirements.None => "N", AttackRequirements.Present => "P", _ => "N" };
private static string MetricToString(PrivilegesRequired pr) =>
pr switch { PrivilegesRequired.None => "N", PrivilegesRequired.Low => "L", PrivilegesRequired.High => "H", _ => "N" };
private static string MetricToString(UserInteraction ui) =>
ui switch { UserInteraction.None => "N", UserInteraction.Passive => "P", UserInteraction.Active => "A", _ => "N" };
private static string MetricToString(ImpactMetricValue impact) =>
impact switch { ImpactMetricValue.None => "N", ImpactMetricValue.Low => "L", ImpactMetricValue.High => "H", _ => "N" };
private static string MetricToString(ExploitMaturity em) =>
em switch { ExploitMaturity.Attacked => "A", ExploitMaturity.ProofOfConcept => "P", ExploitMaturity.Unreported => "U", _ => "X" };
private static void AppendEnvironmentalMetrics(StringBuilder sb, CvssEnvironmentalMetrics env)
{
if (env.ConfidentialityRequirement is not null and not SecurityRequirement.NotDefined)
sb.Append($"/CR:{SecurityRequirementToString(env.ConfidentialityRequirement.Value)}");
if (env.IntegrityRequirement is not null and not SecurityRequirement.NotDefined)
sb.Append($"/IR:{SecurityRequirementToString(env.IntegrityRequirement.Value)}");
if (env.AvailabilityRequirement is not null and not SecurityRequirement.NotDefined)
sb.Append($"/AR:{SecurityRequirementToString(env.AvailabilityRequirement.Value)}");
// Add modified metrics (MAV, MAC, etc.) similarly...
}
private static void AppendSupplementalMetrics(StringBuilder sb, CvssSupplementalMetrics supp)
{
if (supp.Safety is not null and not Safety.NotDefined)
sb.Append($"/S:{SafetyToString(supp.Safety.Value)}");
if (supp.Automatable is not null and not Automatable.NotDefined)
sb.Append($"/AU:{AutomatableToString(supp.Automatable.Value)}");
if (supp.Recovery is not null and not Recovery.NotDefined)
sb.Append($"/R:{RecoveryToString(supp.Recovery.Value)}");
if (supp.ValueDensity is not null and not ValueDensity.NotDefined)
sb.Append($"/V:{ValueDensityToString(supp.ValueDensity.Value)}");
if (supp.VulnerabilityResponseEffort is not null and not ResponseEffort.NotDefined)
sb.Append($"/RE:{ResponseEffortToString(supp.VulnerabilityResponseEffort.Value)}");
if (supp.ProviderUrgency is not null and not ProviderUrgency.NotDefined)
sb.Append($"/U:{ProviderUrgencyToString(supp.ProviderUrgency.Value)}");
}
private static string SecurityRequirementToString(SecurityRequirement sr) =>
sr switch { SecurityRequirement.Low => "L", SecurityRequirement.Medium => "M", SecurityRequirement.High => "H", _ => "X" };
private static string SafetyToString(Safety s) =>
s switch { Safety.Negligible => "N", Safety.Present => "P", _ => "X" };
private static string AutomatableToString(Automatable a) =>
a switch { Automatable.No => "N", Automatable.Yes => "Y", _ => "X" };
private static string RecoveryToString(Recovery r) =>
r switch { Recovery.Automatic => "A", Recovery.User => "U", Recovery.Irrecoverable => "I", _ => "X" };
private static string ValueDensityToString(ValueDensity v) =>
v switch { ValueDensity.Diffuse => "D", ValueDensity.Concentrated => "C", _ => "X" };
private static string ResponseEffortToString(ResponseEffort re) =>
re switch { ResponseEffort.Low => "L", ResponseEffort.Moderate => "M", ResponseEffort.High => "H", _ => "X" };
private static string ProviderUrgencyToString(ProviderUrgency u) =>
u switch { ProviderUrgency.Clear => "Clear", ProviderUrgency.Green => "Green", ProviderUrgency.Amber => "Amber", ProviderUrgency.Red => "Red", _ => "X" };
#endregion
#region Vector String Parsing
[GeneratedRegex(@"([A-Z]+):([A-Za-z]+)", RegexOptions.Compiled)]
private static partial Regex MetricPairRegex();
private static Dictionary<string, string> ParseMetricsFromVector(string vectorPart)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var matches = MetricPairRegex().Matches(vectorPart);
foreach (Match match in matches)
{
result[match.Groups[1].Value.ToUpperInvariant()] = match.Groups[2].Value.ToUpperInvariant();
}
return result;
}
private static string GetRequiredMetric(Dictionary<string, string> metrics, string key)
{
if (!metrics.TryGetValue(key, out var value))
throw new ArgumentException($"Required CVSS metric '{key}' not found in vector string.");
return value;
}
private static AttackVector ParseAttackVector(string value) =>
value.ToUpperInvariant() switch
{
"N" => AttackVector.Network,
"A" => AttackVector.Adjacent,
"L" => AttackVector.Local,
"P" => AttackVector.Physical,
_ => throw new ArgumentException($"Invalid Attack Vector value: {value}")
};
private static AttackComplexity ParseAttackComplexity(string value) =>
value.ToUpperInvariant() switch
{
"L" => AttackComplexity.Low,
"H" => AttackComplexity.High,
_ => throw new ArgumentException($"Invalid Attack Complexity value: {value}")
};
private static AttackRequirements ParseAttackRequirements(string value) =>
value.ToUpperInvariant() switch
{
"N" => AttackRequirements.None,
"P" => AttackRequirements.Present,
_ => throw new ArgumentException($"Invalid Attack Requirements value: {value}")
};
private static PrivilegesRequired ParsePrivilegesRequired(string value) =>
value.ToUpperInvariant() switch
{
"N" => PrivilegesRequired.None,
"L" => PrivilegesRequired.Low,
"H" => PrivilegesRequired.High,
_ => throw new ArgumentException($"Invalid Privileges Required value: {value}")
};
private static UserInteraction ParseUserInteraction(string value) =>
value.ToUpperInvariant() switch
{
"N" => UserInteraction.None,
"P" => UserInteraction.Passive,
"A" => UserInteraction.Active,
_ => throw new ArgumentException($"Invalid User Interaction value: {value}")
};
private static ImpactMetricValue ParseImpactMetric(string value) =>
value.ToUpperInvariant() switch
{
"N" => ImpactMetricValue.None,
"L" => ImpactMetricValue.Low,
"H" => ImpactMetricValue.High,
_ => throw new ArgumentException($"Invalid Impact Metric value: {value}")
};
private static ExploitMaturity ParseExploitMaturity(string value) =>
value.ToUpperInvariant() switch
{
"A" => ExploitMaturity.Attacked,
"P" => ExploitMaturity.ProofOfConcept,
"U" => ExploitMaturity.Unreported,
"X" => ExploitMaturity.NotDefined,
_ => throw new ArgumentException($"Invalid Exploit Maturity value: {value}")
};
private static CvssEnvironmentalMetrics? ParseEnvironmentalMetrics(Dictionary<string, string> metrics)
{
// Check if any environmental metrics are present
var hasEnv = metrics.ContainsKey("CR") || metrics.ContainsKey("IR") || metrics.ContainsKey("AR") ||
metrics.ContainsKey("MAV") || metrics.ContainsKey("MAC") || metrics.ContainsKey("MAT") ||
metrics.ContainsKey("MPR") || metrics.ContainsKey("MUI") ||
metrics.ContainsKey("MVC") || metrics.ContainsKey("MVI") || metrics.ContainsKey("MVA") ||
metrics.ContainsKey("MSC") || metrics.ContainsKey("MSI") || metrics.ContainsKey("MSA");
if (!hasEnv)
return null;
return new CvssEnvironmentalMetrics
{
ConfidentialityRequirement = metrics.TryGetValue("CR", out var cr) ? ParseSecurityRequirement(cr) : null,
IntegrityRequirement = metrics.TryGetValue("IR", out var ir) ? ParseSecurityRequirement(ir) : null,
AvailabilityRequirement = metrics.TryGetValue("AR", out var ar) ? ParseSecurityRequirement(ar) : null
// Add other environmental metrics parsing as needed
};
}
private static SecurityRequirement ParseSecurityRequirement(string value) =>
value.ToUpperInvariant() switch
{
"L" => SecurityRequirement.Low,
"M" => SecurityRequirement.Medium,
"H" => SecurityRequirement.High,
"X" => SecurityRequirement.NotDefined,
_ => SecurityRequirement.NotDefined
};
private static CvssSupplementalMetrics? ParseSupplementalMetrics(Dictionary<string, string> metrics)
{
// Check if any supplemental metrics are present
var hasSupp = metrics.ContainsKey("S") || metrics.ContainsKey("AU") || metrics.ContainsKey("R") ||
metrics.ContainsKey("V") || metrics.ContainsKey("RE") || metrics.ContainsKey("U");
if (!hasSupp)
return null;
return new CvssSupplementalMetrics
{
Safety = metrics.TryGetValue("S", out var s) ? ParseSafety(s) : null,
Automatable = metrics.TryGetValue("AU", out var au) ? ParseAutomatable(au) : null,
Recovery = metrics.TryGetValue("R", out var r) ? ParseRecovery(r) : null,
ValueDensity = metrics.TryGetValue("V", out var v) ? ParseValueDensity(v) : null,
VulnerabilityResponseEffort = metrics.TryGetValue("RE", out var re) ? ParseResponseEffort(re) : null,
ProviderUrgency = metrics.TryGetValue("U", out var u) ? ParseProviderUrgency(u) : null
};
}
private static Safety ParseSafety(string value) =>
value.ToUpperInvariant() switch
{
"N" => Safety.Negligible,
"P" => Safety.Present,
"X" => Safety.NotDefined,
_ => Safety.NotDefined
};
private static Automatable ParseAutomatable(string value) =>
value.ToUpperInvariant() switch
{
"N" => Automatable.No,
"Y" => Automatable.Yes,
"X" => Automatable.NotDefined,
_ => Automatable.NotDefined
};
private static Recovery ParseRecovery(string value) =>
value.ToUpperInvariant() switch
{
"A" => Recovery.Automatic,
"U" => Recovery.User,
"I" => Recovery.Irrecoverable,
"X" => Recovery.NotDefined,
_ => Recovery.NotDefined
};
private static ValueDensity ParseValueDensity(string value) =>
value.ToUpperInvariant() switch
{
"D" => ValueDensity.Diffuse,
"C" => ValueDensity.Concentrated,
"X" => ValueDensity.NotDefined,
_ => ValueDensity.NotDefined
};
private static ResponseEffort ParseResponseEffort(string value) =>
value.ToUpperInvariant() switch
{
"L" => ResponseEffort.Low,
"M" => ResponseEffort.Moderate,
"H" => ResponseEffort.High,
"X" => ResponseEffort.NotDefined,
_ => ResponseEffort.NotDefined
};
private static ProviderUrgency ParseProviderUrgency(string value) =>
value.ToUpperInvariant() switch
{
"CLEAR" => ProviderUrgency.Clear,
"GREEN" => ProviderUrgency.Green,
"AMBER" => ProviderUrgency.Amber,
"RED" => ProviderUrgency.Red,
"X" => ProviderUrgency.NotDefined,
_ => ProviderUrgency.NotDefined
};
#endregion
}

View File

@@ -0,0 +1,68 @@
namespace StellaOps.Policy.Scoring.Engine;
/// <summary>
/// CVSS v4.0 scoring engine interface.
/// Provides deterministic score computation per FIRST specification.
/// </summary>
public interface ICvssV4Engine
{
/// <summary>
/// Computes all CVSS v4.0 scores from the provided metrics.
/// </summary>
/// <param name="baseMetrics">Required base metrics.</param>
/// <param name="threatMetrics">Optional threat metrics.</param>
/// <param name="environmentalMetrics">Optional environmental metrics.</param>
/// <returns>Computed scores including base, threat, environmental, and full scores.</returns>
CvssScores ComputeScores(
CvssBaseMetrics baseMetrics,
CvssThreatMetrics? threatMetrics = null,
CvssEnvironmentalMetrics? environmentalMetrics = null);
/// <summary>
/// Builds a CVSS v4.0 vector string from the provided metrics.
/// </summary>
/// <param name="baseMetrics">Required base metrics.</param>
/// <param name="threatMetrics">Optional threat metrics.</param>
/// <param name="environmentalMetrics">Optional environmental metrics.</param>
/// <param name="supplementalMetrics">Optional supplemental metrics (do not affect score).</param>
/// <returns>CVSS v4.0 vector string (e.g., "CVSS:4.0/AV:N/AC:L/...").</returns>
string BuildVectorString(
CvssBaseMetrics baseMetrics,
CvssThreatMetrics? threatMetrics = null,
CvssEnvironmentalMetrics? environmentalMetrics = null,
CvssSupplementalMetrics? supplementalMetrics = null);
/// <summary>
/// Parses a CVSS v4.0 vector string into its component metrics.
/// </summary>
/// <param name="vectorString">CVSS v4.0 vector string to parse.</param>
/// <returns>Tuple of parsed metrics (Base is required, others may be null).</returns>
/// <exception cref="ArgumentException">If the vector string is invalid.</exception>
CvssMetricSet ParseVector(string vectorString);
/// <summary>
/// Determines the severity rating for a given score.
/// </summary>
/// <param name="score">CVSS score (0.0-10.0).</param>
/// <param name="thresholds">Optional custom thresholds.</param>
/// <returns>Severity rating.</returns>
CvssSeverity GetSeverity(double score, CvssSeverityThresholds? thresholds = null);
}
/// <summary>
/// Container for parsed CVSS v4.0 metrics from a vector string.
/// </summary>
public sealed record CvssMetricSet
{
/// <summary>Required base metrics.</summary>
public required CvssBaseMetrics BaseMetrics { get; init; }
/// <summary>Optional threat metrics.</summary>
public CvssThreatMetrics? ThreatMetrics { get; init; }
/// <summary>Optional environmental metrics.</summary>
public CvssEnvironmentalMetrics? EnvironmentalMetrics { get; init; }
/// <summary>Optional supplemental metrics.</summary>
public CvssSupplementalMetrics? SupplementalMetrics { get; init; }
}

View File

@@ -0,0 +1,244 @@
namespace StellaOps.Policy.Scoring.Engine;
/// <summary>
/// MacroVector lookup table for CVSS v4.0 scoring.
/// Based on FIRST CVSS v4.0 specification lookup tables.
/// Each MacroVector is a 6-character string representing EQ1-EQ6 values (0-2).
/// </summary>
internal static class MacroVectorLookup
{
/// <summary>
/// Gets the base score for a MacroVector.
/// </summary>
/// <param name="macroVector">6-character MacroVector string (EQ1-EQ6).</param>
/// <returns>Base score (0.0-10.0).</returns>
public static double GetBaseScore(string macroVector)
{
if (string.IsNullOrEmpty(macroVector) || macroVector.Length != 6)
return 0.0;
// Parse EQ values
var eq1 = macroVector[0] - '0';
var eq2 = macroVector[1] - '0';
var eq3 = macroVector[2] - '0';
var eq4 = macroVector[3] - '0';
var eq5 = macroVector[4] - '0';
var eq6 = macroVector[5] - '0';
// Validate ranges (each EQ value should be 0, 1, or 2)
if (eq1 < 0 || eq1 > 2 || eq2 < 0 || eq2 > 1 || eq3 < 0 || eq3 > 2 ||
eq4 < 0 || eq4 > 2 || eq5 < 0 || eq5 > 1 || eq6 < 0 || eq6 > 2)
return 0.0;
// Compute score using the CVSS v4.0 scoring formula
// This is a simplified lookup - the actual FIRST spec uses a complex
// interpolation based on MacroVector and individual metric severities
return ComputeScoreFromEquivalenceClasses(eq1, eq2, eq3, eq4, eq5, eq6);
}
/// <summary>
/// Computes the score from equivalence class values.
/// Based on CVSS v4.0 scoring algorithm.
/// </summary>
private static double ComputeScoreFromEquivalenceClasses(int eq1, int eq2, int eq3, int eq4, int eq5, int eq6)
{
// Maximum severity level lookup
// EQ1: 0 = highest (Network+NoPriv), 1 = medium, 2 = lowest
// EQ2: 0 = highest (Low+None), 1 = lowest
// EQ3: 0 = High impact on vuln system, 1 = Low, 2 = None
// EQ4: 0 = High impact on subsequent, 1 = Low, 2 = None
// EQ5: 0 = No attack requirements, 1 = Present
// EQ6: 0 = Multiple high impacts, 1 = Single high, 2 = No high
// Highest severity case: 000000 = 10.0
// Lowest severity case: 212212 = 0.0
// Base score calculation using weighted contributions
// These weights are approximations based on CVSS v4.0 guidance
var score = 10.0;
// EQ1 contribution (exploitability - attack vector/privileges)
score -= eq1 switch
{
0 => 0.0, // Network + No privs = most exploitable
1 => 1.5, // Medium exploitability
2 => 3.0, // Physical/Local = least exploitable
_ => 0.0
};
// EQ2 contribution (attack complexity + user interaction)
score -= eq2 switch
{
0 => 0.0, // Low complexity + No UI = easiest
1 => 0.8, // Higher complexity or requires UI
_ => 0.0
};
// EQ3 contribution (vulnerable system impact)
score -= eq3 switch
{
0 => 0.0, // High impact on vulnerable system
1 => 1.2, // Low impact
2 => 2.5, // No impact
_ => 0.0
};
// EQ4 contribution (subsequent system impact)
score -= eq4 switch
{
0 => 0.0, // High impact on subsequent systems
1 => 0.8, // Low impact
2 => 1.5, // No impact
_ => 0.0
};
// EQ5 contribution (attack requirements)
score -= eq5 switch
{
0 => 0.0, // No special requirements
1 => 0.5, // Requirements present
_ => 0.0
};
// EQ6 contribution (combined impact pattern)
score -= eq6 switch
{
0 => 0.0, // Multiple high impacts
1 => 0.3, // Single high impact
2 => 0.6, // No high impacts
_ => 0.0
};
// Ensure score stays in valid range
return Math.Max(0.0, Math.Min(10.0, score));
}
/// <summary>
/// Full lookup table for precise scoring per FIRST CVSS v4.0.
/// Key: MacroVector string, Value: Base score
/// </summary>
/// <remarks>
/// This table contains a subset of the complete CVSS v4.0 lookup values.
/// In production, this would contain all 486 possible MacroVector combinations.
/// </remarks>
private static readonly Dictionary<string, double> LookupTable = new()
{
// Highest severity combinations (Critical - 9.0+)
["000000"] = 10.0,
["000001"] = 9.9,
["000002"] = 9.8,
["000010"] = 9.8,
["000011"] = 9.5,
["000012"] = 9.3,
["000020"] = 9.4,
["000021"] = 9.2,
["000022"] = 9.0,
// High severity combinations (7.0-8.9)
["010000"] = 8.8,
["010001"] = 8.6,
["010010"] = 8.4,
["010011"] = 8.2,
["010020"] = 8.0,
["100000"] = 8.5,
["100001"] = 8.3,
["100010"] = 8.1,
["100011"] = 7.9,
["100020"] = 7.7,
["001000"] = 8.7,
["001010"] = 8.5,
["001020"] = 8.0,
["011000"] = 7.9,
["011010"] = 7.5,
["101000"] = 7.6,
["101010"] = 7.2,
// Medium severity combinations (4.0-6.9)
["110000"] = 6.9,
["110010"] = 6.5,
["110020"] = 6.0,
["011100"] = 6.8,
["011110"] = 6.4,
["101100"] = 6.5,
["101110"] = 6.1,
["111000"] = 5.8,
["111010"] = 5.4,
["111020"] = 5.0,
["002000"] = 6.8,
["002010"] = 6.4,
["002020"] = 5.8,
["012000"] = 5.9,
["012010"] = 5.5,
["102000"] = 5.6,
["102010"] = 5.2,
["112000"] = 4.8,
["112010"] = 4.4,
["112020"] = 4.0,
["020000"] = 6.5,
["020010"] = 6.1,
["120000"] = 5.5,
["120010"] = 5.1,
// Low severity combinations (0.1-3.9)
["111100"] = 3.9,
["111110"] = 3.5,
["111120"] = 3.1,
["121000"] = 3.8,
["121010"] = 3.4,
["121020"] = 3.0,
["211000"] = 3.6,
["211010"] = 3.2,
["211020"] = 2.8,
["112100"] = 3.4,
["112110"] = 3.0,
["112120"] = 2.6,
["022000"] = 3.8,
["022010"] = 3.4,
["122000"] = 3.2,
["122010"] = 2.8,
["212000"] = 2.6,
["212010"] = 2.2,
// Lowest severity combinations (None - 0.0)
["111200"] = 2.5,
["111210"] = 2.1,
["111220"] = 1.7,
["121100"] = 2.3,
["121110"] = 1.9,
["211100"] = 2.1,
["211110"] = 1.7,
["221000"] = 1.8,
["221010"] = 1.4,
["221020"] = 1.0,
["112200"] = 1.5,
["112210"] = 1.1,
["122100"] = 1.4,
["122110"] = 1.0,
["212100"] = 1.2,
["222000"] = 0.8,
["222010"] = 0.4,
["222020"] = 0.1,
// No impact cases
["212200"] = 0.6,
["212210"] = 0.3,
["222100"] = 0.2,
["222110"] = 0.1,
["222200"] = 0.0,
["222210"] = 0.0,
["222220"] = 0.0
};
/// <summary>
/// Gets the precise score from the lookup table if available.
/// Falls back to computed score if not in table.
/// </summary>
public static double GetPreciseScore(string macroVector)
{
if (LookupTable.TryGetValue(macroVector, out var score))
return score;
// Fall back to computed score
return GetBaseScore(macroVector);
}
}

View File

@@ -0,0 +1,155 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://stellaops.org/schemas/cvss-policy@1.json",
"title": "CVSS v4.0 Scoring Policy",
"description": "Configuration schema for CVSS v4.0 scoring policies in StellaOps.",
"type": "object",
"required": ["policyId", "version", "name", "effectiveFrom"],
"properties": {
"policyId": {
"type": "string",
"description": "Unique policy identifier",
"pattern": "^[a-zA-Z0-9_-]+$",
"minLength": 1,
"maxLength": 128
},
"version": {
"type": "string",
"description": "Policy version (semantic versioning)",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"name": {
"type": "string",
"description": "Human-readable policy name",
"minLength": 1,
"maxLength": 256
},
"description": {
"type": "string",
"description": "Policy description"
},
"tenantId": {
"type": "string",
"description": "Tenant scope (null for global policy)"
},
"effectiveFrom": {
"type": "string",
"format": "date-time",
"description": "When this policy becomes effective"
},
"effectiveUntil": {
"type": "string",
"format": "date-time",
"description": "When this policy expires"
},
"isActive": {
"type": "boolean",
"default": true,
"description": "Whether this policy is currently active"
},
"defaultEffectiveScoreType": {
"type": "string",
"enum": ["Base", "Threat", "Environmental", "Full"],
"default": "Full",
"description": "Which score type to use as the effective score by default"
},
"defaultEnvironmentalMetrics": {
"$ref": "#/$defs/environmentalMetrics"
},
"severityThresholds": {
"$ref": "#/$defs/severityThresholds"
},
"rounding": {
"$ref": "#/$defs/roundingConfig"
},
"evidenceRequirements": {
"$ref": "#/$defs/evidenceRequirements"
},
"attestationRequirements": {
"$ref": "#/$defs/attestationRequirements"
},
"metricOverrides": {
"type": "array",
"items": {
"$ref": "#/$defs/metricOverride"
},
"description": "Metric overrides for specific vulnerability patterns"
}
},
"$defs": {
"environmentalMetrics": {
"type": "object",
"description": "CVSS v4.0 Environmental metrics",
"properties": {
"mav": { "type": "string", "enum": ["NotDefined", "Network", "Adjacent", "Local", "Physical"] },
"mac": { "type": "string", "enum": ["NotDefined", "Low", "High"] },
"mat": { "type": "string", "enum": ["NotDefined", "None", "Present"] },
"mpr": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"mui": { "type": "string", "enum": ["NotDefined", "None", "Passive", "Active"] },
"mvc": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"mvi": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"mva": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"msc": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"msi": { "type": "string", "enum": ["NotDefined", "Negligible", "Low", "High", "Safety"] },
"msa": { "type": "string", "enum": ["NotDefined", "Negligible", "Low", "High", "Safety"] },
"cr": { "type": "string", "enum": ["NotDefined", "Low", "Medium", "High"] },
"ir": { "type": "string", "enum": ["NotDefined", "Low", "Medium", "High"] },
"ar": { "type": "string", "enum": ["NotDefined", "Low", "Medium", "High"] }
}
},
"severityThresholds": {
"type": "object",
"description": "Severity threshold configuration",
"properties": {
"lowMin": { "type": "number", "default": 0.1 },
"mediumMin": { "type": "number", "default": 4.0 },
"highMin": { "type": "number", "default": 7.0 },
"criticalMin": { "type": "number", "default": 9.0 }
}
},
"roundingConfig": {
"type": "object",
"description": "Score rounding configuration",
"properties": {
"decimalPlaces": { "type": "integer", "default": 1, "minimum": 0, "maximum": 3 },
"mode": { "type": "string", "enum": ["RoundUp", "Standard", "RoundDown"], "default": "RoundUp" }
}
},
"evidenceRequirements": {
"type": "object",
"description": "Evidence requirements configuration",
"properties": {
"minimumCount": { "type": "integer", "minimum": 0 },
"requireAuthoritative": { "type": "boolean", "default": false },
"requiredTypes": { "type": "array", "items": { "type": "string" } },
"maxAgeInDays": { "type": "integer", "minimum": 1 }
}
},
"attestationRequirements": {
"type": "object",
"description": "Attestation requirements configuration",
"properties": {
"requireDsse": { "type": "boolean", "default": false },
"requireRekor": { "type": "boolean", "default": false },
"allowedSigners": { "type": "array", "items": { "type": "string" } },
"minimumTrustLevel": { "type": "string" }
}
},
"metricOverride": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string" },
"description": { "type": "string" },
"vulnerabilityPattern": { "type": "string" },
"vulnerabilityIds": { "type": "array", "items": { "type": "string" } },
"cweIds": { "type": "array", "items": { "type": "string" } },
"environmentalOverrides": { "$ref": "#/$defs/environmentalMetrics" },
"scoreAdjustment": { "type": "number", "minimum": -10, "maximum": 10 },
"priority": { "type": "integer", "default": 0 },
"isActive": { "type": "boolean", "default": true },
"reason": { "type": "string" }
}
}
}
}

View File

@@ -0,0 +1,207 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://stellaops.org/schemas/cvss-receipt@1.json",
"title": "CVSS v4.0 Score Receipt",
"description": "Schema for CVSS v4.0 score receipts with full audit trail in StellaOps.",
"type": "object",
"required": ["receiptId", "vulnerabilityId", "tenantId", "createdAt", "createdBy", "baseMetrics", "scores", "vectorString", "severity", "policyRef", "inputHash"],
"properties": {
"receiptId": {
"type": "string",
"description": "Unique receipt identifier",
"pattern": "^[a-zA-Z0-9_-]+$"
},
"schemaVersion": {
"type": "string",
"default": "1.0.0"
},
"format": {
"type": "string",
"const": "stella.ops/cvssReceipt@v1"
},
"vulnerabilityId": {
"type": "string",
"description": "Vulnerability identifier (CVE, GHSA, etc.)"
},
"tenantId": {
"type": "string",
"description": "Tenant scope"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"createdBy": {
"type": "string"
},
"modifiedAt": {
"type": "string",
"format": "date-time"
},
"modifiedBy": {
"type": "string"
},
"cvssVersion": {
"type": "string",
"const": "4.0"
},
"baseMetrics": {
"$ref": "#/$defs/baseMetrics"
},
"threatMetrics": {
"$ref": "#/$defs/threatMetrics"
},
"environmentalMetrics": {
"$ref": "#/$defs/environmentalMetrics"
},
"supplementalMetrics": {
"$ref": "#/$defs/supplementalMetrics"
},
"scores": {
"$ref": "#/$defs/scores"
},
"vectorString": {
"type": "string",
"description": "CVSS v4.0 vector string",
"pattern": "^CVSS:4\\.0/.*$"
},
"severity": {
"type": "string",
"enum": ["None", "Low", "Medium", "High", "Critical"]
},
"policyRef": {
"$ref": "#/$defs/policyRef"
},
"evidence": {
"type": "array",
"items": { "$ref": "#/$defs/evidenceItem" }
},
"attestationRefs": {
"type": "array",
"items": { "type": "string" }
},
"inputHash": {
"type": "string",
"description": "SHA-256 hash of deterministic input"
},
"history": {
"type": "array",
"items": { "$ref": "#/$defs/historyEntry" }
},
"amendsReceiptId": {
"type": "string"
},
"isActive": {
"type": "boolean",
"default": true
},
"supersededReason": {
"type": "string"
}
},
"$defs": {
"baseMetrics": {
"type": "object",
"required": ["av", "ac", "at", "pr", "ui", "vc", "vi", "va", "sc", "si", "sa"],
"properties": {
"av": { "type": "string", "enum": ["Network", "Adjacent", "Local", "Physical"] },
"ac": { "type": "string", "enum": ["Low", "High"] },
"at": { "type": "string", "enum": ["None", "Present"] },
"pr": { "type": "string", "enum": ["None", "Low", "High"] },
"ui": { "type": "string", "enum": ["None", "Passive", "Active"] },
"vc": { "type": "string", "enum": ["None", "Low", "High"] },
"vi": { "type": "string", "enum": ["None", "Low", "High"] },
"va": { "type": "string", "enum": ["None", "Low", "High"] },
"sc": { "type": "string", "enum": ["None", "Low", "High"] },
"si": { "type": "string", "enum": ["None", "Low", "High"] },
"sa": { "type": "string", "enum": ["None", "Low", "High"] }
}
},
"threatMetrics": {
"type": "object",
"properties": {
"e": { "type": "string", "enum": ["NotDefined", "Attacked", "ProofOfConcept", "Unreported"] }
}
},
"environmentalMetrics": {
"type": "object",
"properties": {
"mav": { "type": "string", "enum": ["NotDefined", "Network", "Adjacent", "Local", "Physical"] },
"mac": { "type": "string", "enum": ["NotDefined", "Low", "High"] },
"mat": { "type": "string", "enum": ["NotDefined", "None", "Present"] },
"mpr": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"mui": { "type": "string", "enum": ["NotDefined", "None", "Passive", "Active"] },
"mvc": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"mvi": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"mva": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"msc": { "type": "string", "enum": ["NotDefined", "None", "Low", "High"] },
"msi": { "type": "string", "enum": ["NotDefined", "Negligible", "Low", "High", "Safety"] },
"msa": { "type": "string", "enum": ["NotDefined", "Negligible", "Low", "High", "Safety"] },
"cr": { "type": "string", "enum": ["NotDefined", "Low", "Medium", "High"] },
"ir": { "type": "string", "enum": ["NotDefined", "Low", "Medium", "High"] },
"ar": { "type": "string", "enum": ["NotDefined", "Low", "Medium", "High"] }
}
},
"supplementalMetrics": {
"type": "object",
"properties": {
"s": { "type": "string", "enum": ["NotDefined", "Negligible", "Present"] },
"au": { "type": "string", "enum": ["NotDefined", "No", "Yes"] },
"r": { "type": "string", "enum": ["NotDefined", "Automatic", "User", "Irrecoverable"] },
"v": { "type": "string", "enum": ["NotDefined", "Diffuse", "Concentrated"] },
"re": { "type": "string", "enum": ["NotDefined", "Low", "Moderate", "High"] },
"u": { "type": "string", "enum": ["NotDefined", "Clear", "Green", "Amber", "Red"] }
}
},
"scores": {
"type": "object",
"required": ["baseScore", "effectiveScore", "effectiveScoreType"],
"properties": {
"baseScore": { "type": "number", "minimum": 0, "maximum": 10 },
"threatScore": { "type": "number", "minimum": 0, "maximum": 10 },
"environmentalScore": { "type": "number", "minimum": 0, "maximum": 10 },
"fullScore": { "type": "number", "minimum": 0, "maximum": 10 },
"effectiveScore": { "type": "number", "minimum": 0, "maximum": 10 },
"effectiveScoreType": { "type": "string", "enum": ["Base", "Threat", "Environmental", "Full"] }
}
},
"policyRef": {
"type": "object",
"required": ["policyId", "version", "hash"],
"properties": {
"policyId": { "type": "string" },
"version": { "type": "string" },
"hash": { "type": "string" },
"activatedAt": { "type": "string", "format": "date-time" }
}
},
"evidenceItem": {
"type": "object",
"required": ["type", "uri"],
"properties": {
"type": { "type": "string" },
"uri": { "type": "string" },
"description": { "type": "string" },
"collectedAt": { "type": "string", "format": "date-time" },
"source": { "type": "string" },
"isAuthoritative": { "type": "boolean", "default": false }
}
},
"historyEntry": {
"type": "object",
"required": ["historyId", "timestamp", "actor", "changeType", "field", "reason"],
"properties": {
"historyId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"actor": { "type": "string" },
"changeType": { "type": "string", "enum": ["Created", "Amended", "Superseded", "Revoked", "EvidenceAdded", "AttestationSigned", "PolicyUpdated", "Recalculated"] },
"field": { "type": "string" },
"previousValue": { "type": "string" },
"newValue": { "type": "string" },
"reason": { "type": "string" },
"referenceUri": { "type": "string" },
"signature": { "type": "string" }
}
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Description>CVSS v4.0 scoring engine with deterministic receipt generation for StellaOps policy decisions.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="10.0.0" />
<PackageReference Include="JsonSchema.Net" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Schemas\cvss-policy-schema@1.json" />
<EmbeddedResource Include="Schemas\cvss-receipt-schema@1.json" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,498 @@
using FluentAssertions;
using StellaOps.Policy.Scoring;
using StellaOps.Policy.Scoring.Engine;
using Xunit;
namespace StellaOps.Policy.Scoring.Tests;
/// <summary>
/// Unit tests for CvssV4Engine per FIRST CVSS v4.0 specification.
/// </summary>
public sealed class CvssV4EngineTests
{
private readonly ICvssV4Engine _engine = new CvssV4Engine();
#region Base Score Tests
[Fact]
public void ComputeScores_MaximumSeverity_ReturnsScore10()
{
// Arrange - Highest severity: Network/Low/None/None/None/High across all impacts
var metrics = CreateHighestSeverityMetrics();
// Act
var scores = _engine.ComputeScores(metrics);
// Assert
scores.BaseScore.Should().Be(10.0);
scores.EffectiveScore.Should().Be(10.0);
scores.EffectiveScoreType.Should().Be(EffectiveScoreType.Base);
}
[Fact]
public void ComputeScores_MinimumSeverity_ReturnsLowScore()
{
// Arrange - Lowest severity: Physical/High/Present/High/Active/None across all impacts
var metrics = CreateLowestSeverityMetrics();
// Act
var scores = _engine.ComputeScores(metrics);
// Assert
scores.BaseScore.Should().BeLessThan(2.0);
scores.EffectiveScore.Should().BeLessThan(2.0);
}
[Fact]
public void ComputeScores_MediumSeverity_ReturnsScoreInRange()
{
// Arrange - Medium severity combination
var metrics = new CvssBaseMetrics
{
AttackVector = AttackVector.Adjacent,
AttackComplexity = AttackComplexity.Low,
AttackRequirements = AttackRequirements.None,
PrivilegesRequired = PrivilegesRequired.Low,
UserInteraction = UserInteraction.Passive,
VulnerableSystemConfidentiality = ImpactMetricValue.Low,
VulnerableSystemIntegrity = ImpactMetricValue.Low,
VulnerableSystemAvailability = ImpactMetricValue.None,
SubsequentSystemConfidentiality = ImpactMetricValue.None,
SubsequentSystemIntegrity = ImpactMetricValue.None,
SubsequentSystemAvailability = ImpactMetricValue.None
};
// Act
var scores = _engine.ComputeScores(metrics);
// Assert
scores.BaseScore.Should().BeInRange(4.0, 7.0);
}
#endregion
#region Threat Score Tests
[Fact]
public void ComputeScores_WithAttackedThreat_ReturnsThreatScore()
{
// Arrange
var baseMetrics = CreateHighestSeverityMetrics();
var threatMetrics = new CvssThreatMetrics { ExploitMaturity = ExploitMaturity.Attacked };
// Act
var scores = _engine.ComputeScores(baseMetrics, threatMetrics);
// Assert
scores.ThreatScore.Should().NotBeNull();
scores.ThreatScore!.Value.Should().Be(10.0); // Attacked = 1.0 multiplier
scores.EffectiveScoreType.Should().Be(EffectiveScoreType.Threat);
}
[Fact]
public void ComputeScores_WithProofOfConceptThreat_ReducesScore()
{
// Arrange
var baseMetrics = CreateHighestSeverityMetrics();
var threatMetrics = new CvssThreatMetrics { ExploitMaturity = ExploitMaturity.ProofOfConcept };
// Act
var scores = _engine.ComputeScores(baseMetrics, threatMetrics);
// Assert
scores.ThreatScore.Should().NotBeNull();
scores.ThreatScore!.Value.Should().BeLessThan(10.0); // PoC = 0.94 multiplier
scores.ThreatScore.Value.Should().BeGreaterThan(9.0);
}
[Fact]
public void ComputeScores_WithUnreportedThreat_ReducesScoreMore()
{
// Arrange
var baseMetrics = CreateHighestSeverityMetrics();
var threatMetrics = new CvssThreatMetrics { ExploitMaturity = ExploitMaturity.Unreported };
// Act
var scores = _engine.ComputeScores(baseMetrics, threatMetrics);
// Assert
scores.ThreatScore.Should().NotBeNull();
scores.ThreatScore!.Value.Should().BeLessThan(9.5); // Unreported = 0.91 multiplier
}
[Fact]
public void ComputeScores_WithNotDefinedThreat_ReturnsOnlyBaseScore()
{
// Arrange
var baseMetrics = CreateHighestSeverityMetrics();
var threatMetrics = new CvssThreatMetrics { ExploitMaturity = ExploitMaturity.NotDefined };
// Act
var scores = _engine.ComputeScores(baseMetrics, threatMetrics);
// Assert
scores.ThreatScore.Should().BeNull();
scores.EffectiveScoreType.Should().Be(EffectiveScoreType.Base);
}
#endregion
#region Environmental Score Tests
[Fact]
public void ComputeScores_WithHighSecurityRequirements_IncreasesScore()
{
// Arrange
var baseMetrics = CreateMediumSeverityMetrics();
var envMetrics = new CvssEnvironmentalMetrics
{
ConfidentialityRequirement = SecurityRequirement.High,
IntegrityRequirement = SecurityRequirement.High,
AvailabilityRequirement = SecurityRequirement.High
};
// Act
var scoresWithoutEnv = _engine.ComputeScores(baseMetrics);
var scoresWithEnv = _engine.ComputeScores(baseMetrics, environmentalMetrics: envMetrics);
// Assert
scoresWithEnv.EnvironmentalScore.Should().NotBeNull();
scoresWithEnv.EnvironmentalScore!.Value.Should().BeGreaterThan(scoresWithoutEnv.BaseScore);
}
[Fact]
public void ComputeScores_WithLowSecurityRequirements_DecreasesScore()
{
// Arrange
var baseMetrics = CreateMediumSeverityMetrics();
var envMetrics = new CvssEnvironmentalMetrics
{
ConfidentialityRequirement = SecurityRequirement.Low,
IntegrityRequirement = SecurityRequirement.Low,
AvailabilityRequirement = SecurityRequirement.Low
};
// Act
var scoresWithoutEnv = _engine.ComputeScores(baseMetrics);
var scoresWithEnv = _engine.ComputeScores(baseMetrics, environmentalMetrics: envMetrics);
// Assert
scoresWithEnv.EnvironmentalScore.Should().NotBeNull();
scoresWithEnv.EnvironmentalScore!.Value.Should().BeLessThan(scoresWithoutEnv.BaseScore);
}
[Fact]
public void ComputeScores_WithModifiedMetrics_AppliesModifications()
{
// Arrange - Start with network-based vuln, modify to local
var baseMetrics = CreateHighestSeverityMetrics();
var envMetrics = new CvssEnvironmentalMetrics
{
ModifiedAttackVector = ModifiedAttackVector.Local
};
// Act
var scoresWithoutEnv = _engine.ComputeScores(baseMetrics);
var scoresWithEnv = _engine.ComputeScores(baseMetrics, environmentalMetrics: envMetrics);
// Assert
scoresWithEnv.EnvironmentalScore.Should().NotBeNull();
scoresWithEnv.EnvironmentalScore!.Value.Should().BeLessThan(scoresWithoutEnv.BaseScore);
}
#endregion
#region Full Score Tests
[Fact]
public void ComputeScores_WithAllMetrics_ReturnsFullScore()
{
// Arrange
var baseMetrics = CreateHighestSeverityMetrics();
var threatMetrics = new CvssThreatMetrics { ExploitMaturity = ExploitMaturity.Attacked };
var envMetrics = new CvssEnvironmentalMetrics
{
ConfidentialityRequirement = SecurityRequirement.High
};
// Act
var scores = _engine.ComputeScores(baseMetrics, threatMetrics, envMetrics);
// Assert
scores.FullScore.Should().NotBeNull();
scores.EffectiveScoreType.Should().Be(EffectiveScoreType.Full);
}
#endregion
#region Vector String Tests
[Fact]
public void BuildVectorString_BaseOnly_ReturnsCorrectFormat()
{
// Arrange
var metrics = CreateHighestSeverityMetrics();
// Act
var vector = _engine.BuildVectorString(metrics);
// Assert
vector.Should().StartWith("CVSS:4.0/");
vector.Should().Contain("AV:N");
vector.Should().Contain("AC:L");
vector.Should().Contain("AT:N");
vector.Should().Contain("PR:N");
vector.Should().Contain("UI:N");
vector.Should().Contain("VC:H");
vector.Should().Contain("VI:H");
vector.Should().Contain("VA:H");
vector.Should().Contain("SC:H");
vector.Should().Contain("SI:H");
vector.Should().Contain("SA:H");
}
[Fact]
public void BuildVectorString_WithThreat_IncludesThreatMetric()
{
// Arrange
var baseMetrics = CreateHighestSeverityMetrics();
var threatMetrics = new CvssThreatMetrics { ExploitMaturity = ExploitMaturity.Attacked };
// Act
var vector = _engine.BuildVectorString(baseMetrics, threatMetrics);
// Assert
vector.Should().Contain("E:A");
}
[Fact]
public void ParseVector_ValidVector_ReturnsCorrectMetrics()
{
// Arrange
var vector = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H";
// Act
var result = _engine.ParseVector(vector);
// Assert
result.BaseMetrics.AttackVector.Should().Be(AttackVector.Network);
result.BaseMetrics.AttackComplexity.Should().Be(AttackComplexity.Low);
result.BaseMetrics.AttackRequirements.Should().Be(AttackRequirements.None);
result.BaseMetrics.PrivilegesRequired.Should().Be(PrivilegesRequired.None);
result.BaseMetrics.UserInteraction.Should().Be(UserInteraction.None);
result.BaseMetrics.VulnerableSystemConfidentiality.Should().Be(ImpactMetricValue.High);
result.BaseMetrics.VulnerableSystemIntegrity.Should().Be(ImpactMetricValue.High);
result.BaseMetrics.VulnerableSystemAvailability.Should().Be(ImpactMetricValue.High);
result.BaseMetrics.SubsequentSystemConfidentiality.Should().Be(ImpactMetricValue.High);
result.BaseMetrics.SubsequentSystemIntegrity.Should().Be(ImpactMetricValue.High);
result.BaseMetrics.SubsequentSystemAvailability.Should().Be(ImpactMetricValue.High);
}
[Fact]
public void ParseVector_WithThreat_ParsesThreatMetric()
{
// Arrange
var vector = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:A";
// Act
var result = _engine.ParseVector(vector);
// Assert
result.ThreatMetrics.Should().NotBeNull();
result.ThreatMetrics!.ExploitMaturity.Should().Be(ExploitMaturity.Attacked);
}
[Fact]
public void ParseVector_InvalidPrefix_ThrowsArgumentException()
{
// Arrange
var vector = "CVSS:3.1/AV:N/AC:L";
// Act & Assert
FluentActions.Invoking(() => _engine.ParseVector(vector))
.Should().Throw<ArgumentException>();
}
[Fact]
public void ParseVector_MissingMetric_ThrowsArgumentException()
{
// Arrange - Missing AV metric
var vector = "CVSS:4.0/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H";
// Act & Assert
FluentActions.Invoking(() => _engine.ParseVector(vector))
.Should().Throw<ArgumentException>();
}
#endregion
#region Severity Tests
[Theory]
[InlineData(0.0, CvssSeverity.None)]
[InlineData(0.1, CvssSeverity.Low)]
[InlineData(3.9, CvssSeverity.Low)]
[InlineData(4.0, CvssSeverity.Medium)]
[InlineData(6.9, CvssSeverity.Medium)]
[InlineData(7.0, CvssSeverity.High)]
[InlineData(8.9, CvssSeverity.High)]
[InlineData(9.0, CvssSeverity.Critical)]
[InlineData(10.0, CvssSeverity.Critical)]
public void GetSeverity_DefaultThresholds_ReturnsCorrectSeverity(double score, CvssSeverity expected)
{
// Act
var severity = _engine.GetSeverity(score);
// Assert
severity.Should().Be(expected);
}
[Fact]
public void GetSeverity_CustomThresholds_UsesCustomValues()
{
// Arrange
var thresholds = new CvssSeverityThresholds
{
LowMin = 0.1,
MediumMin = 5.0, // Higher than default 4.0
HighMin = 8.0, // Higher than default 7.0
CriticalMin = 9.5 // Higher than default 9.0
};
// Act & Assert
_engine.GetSeverity(4.5, thresholds).Should().Be(CvssSeverity.Low);
_engine.GetSeverity(7.5, thresholds).Should().Be(CvssSeverity.Medium);
_engine.GetSeverity(9.0, thresholds).Should().Be(CvssSeverity.High);
_engine.GetSeverity(9.5, thresholds).Should().Be(CvssSeverity.Critical);
}
#endregion
#region Determinism Tests
[Fact]
public void ComputeScores_SameInput_ReturnsSameOutput()
{
// Arrange
var metrics = CreateHighestSeverityMetrics();
// Act
var scores1 = _engine.ComputeScores(metrics);
var scores2 = _engine.ComputeScores(metrics);
var scores3 = _engine.ComputeScores(metrics);
// Assert
scores1.BaseScore.Should().Be(scores2.BaseScore);
scores2.BaseScore.Should().Be(scores3.BaseScore);
scores1.EffectiveScore.Should().Be(scores3.EffectiveScore);
}
[Fact]
public void BuildVectorString_SameInput_ReturnsSameOutput()
{
// Arrange
var metrics = CreateHighestSeverityMetrics();
// Act
var vector1 = _engine.BuildVectorString(metrics);
var vector2 = _engine.BuildVectorString(metrics);
// Assert
vector1.Should().Be(vector2);
}
[Fact]
public void Roundtrip_BuildAndParse_PreservesMetrics()
{
// Arrange
var originalMetrics = CreateHighestSeverityMetrics();
// Act
var vector = _engine.BuildVectorString(originalMetrics);
var parsed = _engine.ParseVector(vector);
// Assert
parsed.BaseMetrics.AttackVector.Should().Be(originalMetrics.AttackVector);
parsed.BaseMetrics.AttackComplexity.Should().Be(originalMetrics.AttackComplexity);
parsed.BaseMetrics.AttackRequirements.Should().Be(originalMetrics.AttackRequirements);
parsed.BaseMetrics.PrivilegesRequired.Should().Be(originalMetrics.PrivilegesRequired);
parsed.BaseMetrics.UserInteraction.Should().Be(originalMetrics.UserInteraction);
parsed.BaseMetrics.VulnerableSystemConfidentiality.Should().Be(originalMetrics.VulnerableSystemConfidentiality);
parsed.BaseMetrics.VulnerableSystemIntegrity.Should().Be(originalMetrics.VulnerableSystemIntegrity);
parsed.BaseMetrics.VulnerableSystemAvailability.Should().Be(originalMetrics.VulnerableSystemAvailability);
}
#endregion
#region FIRST Sample Vector Tests
/// <summary>
/// Tests using sample vectors from FIRST CVSS v4.0 examples.
/// </summary>
[Theory]
[InlineData("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", 10.0)]
[InlineData("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", 9.4)]
[InlineData("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N", 6.8)]
public void ComputeScores_FirstSampleVectors_ReturnsExpectedScore(string vector, double expectedScore)
{
// Arrange
var metricSet = _engine.ParseVector(vector);
// Act
var scores = _engine.ComputeScores(metricSet.BaseMetrics);
// Assert - Allow small tolerance for rounding differences
scores.BaseScore.Should().BeApproximately(expectedScore, 0.5);
}
#endregion
#region Helper Methods
private static CvssBaseMetrics CreateHighestSeverityMetrics() => new()
{
AttackVector = AttackVector.Network,
AttackComplexity = AttackComplexity.Low,
AttackRequirements = AttackRequirements.None,
PrivilegesRequired = PrivilegesRequired.None,
UserInteraction = UserInteraction.None,
VulnerableSystemConfidentiality = ImpactMetricValue.High,
VulnerableSystemIntegrity = ImpactMetricValue.High,
VulnerableSystemAvailability = ImpactMetricValue.High,
SubsequentSystemConfidentiality = ImpactMetricValue.High,
SubsequentSystemIntegrity = ImpactMetricValue.High,
SubsequentSystemAvailability = ImpactMetricValue.High
};
private static CvssBaseMetrics CreateLowestSeverityMetrics() => new()
{
AttackVector = AttackVector.Physical,
AttackComplexity = AttackComplexity.High,
AttackRequirements = AttackRequirements.Present,
PrivilegesRequired = PrivilegesRequired.High,
UserInteraction = UserInteraction.Active,
VulnerableSystemConfidentiality = ImpactMetricValue.None,
VulnerableSystemIntegrity = ImpactMetricValue.None,
VulnerableSystemAvailability = ImpactMetricValue.None,
SubsequentSystemConfidentiality = ImpactMetricValue.None,
SubsequentSystemIntegrity = ImpactMetricValue.None,
SubsequentSystemAvailability = ImpactMetricValue.None
};
private static CvssBaseMetrics CreateMediumSeverityMetrics() => new()
{
AttackVector = AttackVector.Network,
AttackComplexity = AttackComplexity.Low,
AttackRequirements = AttackRequirements.None,
PrivilegesRequired = PrivilegesRequired.Low,
UserInteraction = UserInteraction.None,
VulnerableSystemConfidentiality = ImpactMetricValue.Low,
VulnerableSystemIntegrity = ImpactMetricValue.Low,
VulnerableSystemAvailability = ImpactMetricValue.None,
SubsequentSystemConfidentiality = ImpactMetricValue.None,
SubsequentSystemIntegrity = ImpactMetricValue.None,
SubsequentSystemAvailability = ImpactMetricValue.None
};
#endregion
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.Policy.Scoring/StellaOps.Policy.Scoring.csproj" />
</ItemGroup>
</Project>