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

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

View File

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

View File

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