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
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:
93
src/Policy/StellaOps.Policy.Scoring/AGENTS.md
Normal file
93
src/Policy/StellaOps.Policy.Scoring/AGENTS.md
Normal 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
|
||||
354
src/Policy/StellaOps.Policy.Scoring/CvssMetrics.cs
Normal file
354
src/Policy/StellaOps.Policy.Scoring/CvssMetrics.cs
Normal 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
|
||||
223
src/Policy/StellaOps.Policy.Scoring/CvssPolicy.cs
Normal file
223
src/Policy/StellaOps.Policy.Scoring/CvssPolicy.cs
Normal 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; }
|
||||
}
|
||||
297
src/Policy/StellaOps.Policy.Scoring/CvssScoreReceipt.cs
Normal file
297
src/Policy/StellaOps.Policy.Scoring/CvssScoreReceipt.cs
Normal 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
|
||||
}
|
||||
809
src/Policy/StellaOps.Policy.Scoring/Engine/CvssV4Engine.cs
Normal file
809
src/Policy/StellaOps.Policy.Scoring/Engine/CvssV4Engine.cs
Normal 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
|
||||
}
|
||||
68
src/Policy/StellaOps.Policy.Scoring/Engine/ICvssV4Engine.cs
Normal file
68
src/Policy/StellaOps.Policy.Scoring/Engine/ICvssV4Engine.cs
Normal 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; }
|
||||
}
|
||||
244
src/Policy/StellaOps.Policy.Scoring/Engine/MacroVectorLookup.cs
Normal file
244
src/Policy/StellaOps.Policy.Scoring/Engine/MacroVectorLookup.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user