Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
// Task: TEST-STRAT-5100-005 - Introduce one Pact contract test for critical API
|
||||
// =============================================================================
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using PactNet;
|
||||
@@ -11,7 +12,6 @@ using PactNet.Matchers;
|
||||
using StellaOps.Policy.Engine.Scoring;
|
||||
using StellaOps.Policy.Scoring;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Contract.Tests;
|
||||
|
||||
@@ -176,8 +176,8 @@ public sealed class ScoringApiContractTests : IAsyncLifetime
|
||||
response.IsSuccessStatusCode.Should().BeTrue();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
result.GetProperty("finalScore").GetInt32().Should().BeGreaterOrEqualTo(0);
|
||||
result.GetProperty("finalScore").GetInt32().Should().BeLessOrEqualTo(100);
|
||||
result.GetProperty("finalScore").GetInt32().Should().BeGreaterThanOrEqualTo(0);
|
||||
result.GetProperty("finalScore").GetInt32().Should().BeLessThanOrEqualTo(100);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,23 +11,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="PactNet" Version="5.0.0" />
|
||||
<PackageReference Include="PactNet.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="PactNet" />
|
||||
<PackageReference Include="PactNet.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj" />
|
||||
<ProjectReference Include="../../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -0,0 +1,370 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy.Crypto;
|
||||
using StellaOps.Policy.Engine.Crypto;
|
||||
using Xunit;
|
||||
using EngineCryptoRiskEvaluator = StellaOps.Policy.Engine.Crypto.CryptoRiskEvaluator;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Crypto;
|
||||
|
||||
public sealed class CryptoRiskEvaluatorTests
|
||||
{
|
||||
private readonly EngineCryptoRiskEvaluator _evaluator = new();
|
||||
|
||||
#region DEPRECATED_CRYPTO Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateDeprecatedCrypto_WithMD5_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("MD5"));
|
||||
|
||||
var result = _evaluator.EvaluateDeprecatedCrypto(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.Critical, result.Severity);
|
||||
Assert.Contains("MD5", result.TriggeringAlgorithms);
|
||||
Assert.Contains("MD5", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateDeprecatedCrypto_WithSHA1_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("SHA-1"));
|
||||
|
||||
var result = _evaluator.EvaluateDeprecatedCrypto(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.Critical, result.Severity);
|
||||
Assert.Contains("SHA-1", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateDeprecatedCrypto_With3DES_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("3DES"));
|
||||
|
||||
var result = _evaluator.EvaluateDeprecatedCrypto(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("3DES", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateDeprecatedCrypto_WithAES256_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"));
|
||||
|
||||
var result = _evaluator.EvaluateDeprecatedCrypto(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.Info, result.Severity);
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WEAK_CRYPTO Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateWeakCrypto_WithECB_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-ECB"));
|
||||
|
||||
var result = _evaluator.EvaluateWeakCrypto(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.High, result.Severity);
|
||||
Assert.Contains("AES-256-ECB", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateWeakCrypto_WithGCM_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"));
|
||||
|
||||
var result = _evaluator.EvaluateWeakCrypto(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.Info, result.Severity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region QUANTUM_VULNERABLE Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithRSA_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("RSA-2048"));
|
||||
|
||||
var result = _evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("RSA-2048", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithECDSA_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ECDSA-P256"));
|
||||
|
||||
var result = _evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("ECDSA-P256", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithMLKEM_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ML-KEM-768"));
|
||||
|
||||
var result = _evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithSymmetricOnly_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"),
|
||||
CreateAlgorithmAsset("ChaCha20-Poly1305"));
|
||||
|
||||
var result = _evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithDisabledWarnings_DoesNotTrigger()
|
||||
{
|
||||
var evaluator = new EngineCryptoRiskEvaluator(new CryptoRiskOptions
|
||||
{
|
||||
EnableQuantumRiskWarnings = false
|
||||
});
|
||||
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("RSA-2048"));
|
||||
|
||||
var result = evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region INSUFFICIENT_KEY_SIZE Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateInsufficientKeySize_WithRSA1024_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
new CryptoAsset
|
||||
{
|
||||
Id = "test",
|
||||
ComponentKey = "test",
|
||||
AlgorithmName = "RSA",
|
||||
KeySizeBits = 1024
|
||||
});
|
||||
|
||||
var result = _evaluator.EvaluateInsufficientKeySize(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.High, result.Severity);
|
||||
Assert.Single(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateInsufficientKeySize_WithRSA2048_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
new CryptoAsset
|
||||
{
|
||||
Id = "test",
|
||||
ComponentKey = "test",
|
||||
AlgorithmName = "RSA",
|
||||
KeySizeBits = 2048
|
||||
});
|
||||
|
||||
var result = _evaluator.EvaluateInsufficientKeySize(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateInsufficientKeySize_WithECDSA192_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
new CryptoAsset
|
||||
{
|
||||
Id = "test",
|
||||
ComponentKey = "test",
|
||||
AlgorithmName = "ECDSA",
|
||||
KeySizeBits = 192
|
||||
});
|
||||
|
||||
var result = _evaluator.EvaluateInsufficientKeySize(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region POST_QUANTUM_READY Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePostQuantumReady_WithMLKEM_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ML-KEM-768"));
|
||||
|
||||
var result = _evaluator.EvaluatePostQuantumReady(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("ML-KEM-768", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePostQuantumReady_WithKyber_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("KYBER-1024"));
|
||||
|
||||
var result = _evaluator.EvaluatePostQuantumReady(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("KYBER-1024", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePostQuantumReady_WithDilithium_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ML-DSA-65"));
|
||||
|
||||
var result = _evaluator.EvaluatePostQuantumReady(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("ML-DSA-65", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePostQuantumReady_WithRSAOnly_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("RSA-2048"));
|
||||
|
||||
var result = _evaluator.EvaluatePostQuantumReady(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FIPS_COMPLIANT Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFipsCompliance_WithFipsAlgorithms_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"),
|
||||
CreateAlgorithmAsset("SHA-256"));
|
||||
|
||||
var result = _evaluator.EvaluateFipsCompliance(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFipsCompliance_WithNonFipsAlgorithm_ReportsNonCompliant()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ChaCha20-Poly1305"));
|
||||
|
||||
var evaluator = new EngineCryptoRiskEvaluator(new CryptoRiskOptions
|
||||
{
|
||||
RequireFipsCompliance = true
|
||||
});
|
||||
|
||||
var result = evaluator.EvaluateFipsCompliance(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("ChaCha20-Poly1305", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Full Evaluation Tests
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ReturnsAllAtomResults()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"),
|
||||
CreateAlgorithmAsset("RSA-2048"));
|
||||
|
||||
var results = _evaluator.Evaluate(assets);
|
||||
|
||||
Assert.Equal(6, results.Length);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.DeprecatedCrypto);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.WeakCrypto);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.QuantumVulnerable);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.InsufficientKeySize);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.PostQuantumReady);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.FipsCompliant);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_WithMixedAlgorithms_ReportsCorrectly()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("MD5"), // deprecated
|
||||
CreateAlgorithmAsset("AES-128-ECB"), // weak
|
||||
CreateAlgorithmAsset("RSA-2048"), // quantum vulnerable
|
||||
CreateAlgorithmAsset("ML-KEM-768"), // post-quantum ready
|
||||
CreateAlgorithmAsset("AES-256-GCM")); // good
|
||||
|
||||
var results = _evaluator.Evaluate(assets);
|
||||
|
||||
var deprecatedResult = results.First(r => r.AtomName == CryptoRiskAtoms.DeprecatedCrypto);
|
||||
Assert.True(deprecatedResult.Triggered);
|
||||
Assert.Contains("MD5", deprecatedResult.TriggeringAlgorithms);
|
||||
|
||||
var weakResult = results.First(r => r.AtomName == CryptoRiskAtoms.WeakCrypto);
|
||||
Assert.True(weakResult.Triggered);
|
||||
Assert.Contains("AES-128-ECB", weakResult.TriggeringAlgorithms);
|
||||
|
||||
var quantumResult = results.First(r => r.AtomName == CryptoRiskAtoms.QuantumVulnerable);
|
||||
Assert.True(quantumResult.Triggered);
|
||||
Assert.Contains("RSA-2048", quantumResult.TriggeringAlgorithms);
|
||||
|
||||
var pqResult = results.First(r => r.AtomName == CryptoRiskAtoms.PostQuantumReady);
|
||||
Assert.True(pqResult.Triggered);
|
||||
Assert.Contains("ML-KEM-768", pqResult.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static CryptoAsset CreateAlgorithmAsset(string algorithmName) => new()
|
||||
{
|
||||
Id = $"asset-{algorithmName}",
|
||||
ComponentKey = "test-component",
|
||||
AlgorithmName = algorithmName
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -115,7 +115,7 @@ public sealed class PolicyEngineDeterminismTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcurrentEvaluations_ProduceIdenticalResults()
|
||||
public async Task ConcurrentEvaluations_ProduceIdenticalResults()
|
||||
{
|
||||
// Arrange
|
||||
var policy = CreateTestPolicy();
|
||||
@@ -127,7 +127,7 @@ public sealed class PolicyEngineDeterminismTests
|
||||
.Select(_ => Task.Run(() => evaluator.Evaluate(policy, input)))
|
||||
.ToArray();
|
||||
|
||||
var results = Task.WhenAll(tasks).GetAwaiter().GetResult();
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
// Assert
|
||||
var firstHash = results[0].VerdictHash;
|
||||
@@ -500,6 +500,8 @@ public record PolicyEvaluatorOptions
|
||||
|
||||
public class PolicyEvaluator(PolicyEvaluatorOptions options)
|
||||
{
|
||||
private readonly PolicyEvaluatorOptions _options = options;
|
||||
|
||||
public EvaluationResult Evaluate(PolicyDefinition policy, EvaluationInput input)
|
||||
{
|
||||
// Stub implementation - actual implementation would come from Policy.Engine
|
||||
@@ -509,6 +511,7 @@ public class PolicyEvaluator(PolicyEvaluatorOptions options)
|
||||
// Count unknowns
|
||||
var unknownsCount = input.Findings.Count(f => f.Severity == "unknown" || f.ImpactUnknown);
|
||||
metrics["unknowns_count"] = unknownsCount;
|
||||
metrics["deterministic_mode"] = _options.DeterministicMode ? 1 : 0;
|
||||
|
||||
// Check unknowns budget
|
||||
if (policy.UnknownsBudget is not null && policy.UnknownsBudget.FailOnExceed)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.Policy.Engine.Evaluation;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
@@ -28,9 +29,11 @@ public class CicdGateIntegrationTests
|
||||
_options = new PolicyGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultTimeout = TimeSpan.FromSeconds(60),
|
||||
AllowBypassWithJustification = true,
|
||||
MinimumJustificationLength = 20
|
||||
Override = new OverrideOptions
|
||||
{
|
||||
RequireJustification = true,
|
||||
MinJustificationLength = 20
|
||||
}
|
||||
};
|
||||
|
||||
var monitor = Substitute.For<IOptionsMonitor<PolicyGateOptions>>();
|
||||
@@ -366,9 +369,8 @@ public class CicdGateIntegrationTests
|
||||
RequestedStatus = "affected",
|
||||
LatticeState = "CR",
|
||||
UncertaintyTier = "T1",
|
||||
RiskScore = 9.5,
|
||||
RiskScore = 9.5
|
||||
// No baseline reference = new finding
|
||||
IsNewFinding = true
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -391,8 +393,8 @@ public class CicdGateIntegrationTests
|
||||
RequestedStatus = "affected",
|
||||
LatticeState = "CR",
|
||||
UncertaintyTier = "T4",
|
||||
RiskScore = 7.0,
|
||||
IsNewFinding = false // Already in baseline
|
||||
RiskScore = 7.0
|
||||
// Already in baseline
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
// VexTrustConfidenceFactorProviderTests - Unit tests for confidence factor provider
|
||||
// Part of SPRINT_1227_0004_0003: VexTrustGate Policy Integration
|
||||
|
||||
using StellaOps.Policy.Confidence.Models;
|
||||
using StellaOps.Policy.Engine.Confidence;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for VexTrustConfidenceFactorProvider.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Sprint", "1227.0004.0003")]
|
||||
public sealed class VexTrustConfidenceFactorProviderTests
|
||||
{
|
||||
private readonly VexTrustConfidenceFactorProvider _provider = new();
|
||||
private readonly ConfidenceFactorOptions _defaultOptions = new();
|
||||
|
||||
[Fact]
|
||||
public void Type_ReturnsVex()
|
||||
{
|
||||
Assert.Equal(ConfidenceFactorType.Vex, _provider.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_NullTrustStatus_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = null,
|
||||
Environment = "production"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_ValidTrustStatus_ReturnsFactor()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.80m,
|
||||
SignatureVerified = false,
|
||||
Freshness = "fresh",
|
||||
IssuerName = "Test Issuer"
|
||||
},
|
||||
Environment = "production"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ConfidenceFactorType.Vex, result.Type);
|
||||
Assert.Equal(0.20m, result.Weight); // Default weight
|
||||
Assert.Equal(0.80m, result.RawValue);
|
||||
Assert.Contains("VEX trust: High", result.Reason);
|
||||
Assert.Contains("Test Issuer", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_SignatureVerified_AddsBonus()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.80m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new ConfidenceFactorOptions
|
||||
{
|
||||
VexTrustWeight = 0.20m,
|
||||
SignatureVerifiedBonus = 0.10m
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0.90m, result.RawValue); // 0.80 + 0.10 bonus
|
||||
Assert.Contains("signature verified", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_RekorTransparency_AddsBonus()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.80m,
|
||||
SignatureVerified = false,
|
||||
RekorLogIndex = 12345678,
|
||||
RekorLogId = "rekor.sigstore.dev",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new ConfidenceFactorOptions
|
||||
{
|
||||
VexTrustWeight = 0.20m,
|
||||
TransparencyBonus = 0.05m
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0.85m, result.RawValue); // 0.80 + 0.05 bonus
|
||||
Assert.Contains("Rekor: #12345678", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_AllBonuses_ClampedToOne()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.95m, // Already high
|
||||
SignatureVerified = true,
|
||||
RekorLogIndex = 12345678,
|
||||
RekorLogId = "rekor.sigstore.dev",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new ConfidenceFactorOptions
|
||||
{
|
||||
SignatureVerifiedBonus = 0.10m,
|
||||
TransparencyBonus = 0.05m
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(1.0m, result.RawValue); // Clamped to 1.0
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.95, "VeryHigh")]
|
||||
[InlineData(0.75, "High")]
|
||||
[InlineData(0.55, "Medium")]
|
||||
[InlineData(0.35, "Low")]
|
||||
[InlineData(0.15, "VeryLow")]
|
||||
public void ComputeFactor_TierInReason(double score, string expectedTier)
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = (decimal)score,
|
||||
SignatureVerified = false,
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains($"VEX trust: {expectedTier}", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_FreshnessInReason()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.70m,
|
||||
SignatureVerified = false,
|
||||
Freshness = "stale"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("freshness: stale", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_SignatureMethodInReason()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.70m,
|
||||
SignatureVerified = true,
|
||||
SignatureMethod = "dsse",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("method: dsse", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_EvidenceDigests_IncludesRekor()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.70m,
|
||||
SignatureVerified = false,
|
||||
RekorLogIndex = 99999,
|
||||
RekorLogId = "test-rekor",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("rekor:test-rekor@99999", result.EvidenceDigests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_EvidenceDigests_IncludesIssuer()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.70m,
|
||||
SignatureVerified = false,
|
||||
IssuerId = "redhat.com",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("issuer:redhat.com", result.EvidenceDigests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_Contribution_CalculatedCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.80m,
|
||||
SignatureVerified = false,
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new ConfidenceFactorOptions
|
||||
{
|
||||
VexTrustWeight = 0.25m
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0.25m, result.Weight);
|
||||
Assert.Equal(0.80m, result.RawValue);
|
||||
Assert.Equal(0.20m, result.Contribution); // 0.25 * 0.80 = 0.20
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
// VexTrustGateTests - Unit tests for VEX trust gate
|
||||
// Part of SPRINT_1227_0004_0003: VexTrustGate Policy Integration
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for VexTrustGate.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Sprint", "1227.0004.0003")]
|
||||
public sealed class VexTrustGateTests
|
||||
{
|
||||
private readonly TimeProvider _fixedTimeProvider = new FixedTimeProvider(
|
||||
new DateTimeOffset(2024, 12, 27, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
#region Gate Disabled Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WhenDisabled_ReturnsAllow()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions(enabled: false);
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production");
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
Assert.Equal("gate_disabled", result.Reason);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Status Applicability Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_StatusNotInApplyList_ReturnsAllow()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("under_investigation", "production");
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
Assert.Equal("status_not_applicable", result.Reason);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("not_affected")]
|
||||
[InlineData("fixed")]
|
||||
[InlineData("NOT_AFFECTED")] // Case insensitive
|
||||
public async Task EvaluateAsync_StatusInApplyList_Evaluates(string status)
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest(status, "production", CreateHighTrustStatus());
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
// Should evaluate, not skip
|
||||
Assert.NotEqual("status_not_applicable", result.Reason);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Missing Trust Data Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(MissingTrustBehavior.Allow, VexTrustGateDecision.Allow)]
|
||||
[InlineData(MissingTrustBehavior.Warn, VexTrustGateDecision.Warn)]
|
||||
[InlineData(MissingTrustBehavior.Block, VexTrustGateDecision.Block)]
|
||||
public async Task EvaluateAsync_MissingTrustData_RespectsConfiguredBehavior(
|
||||
MissingTrustBehavior behavior,
|
||||
VexTrustGateDecision expectedDecision)
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions(missingTrustBehavior: behavior);
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", trustStatus: null);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedDecision, result.Decision);
|
||||
Assert.Equal("missing_vex_trust_data", result.Reason);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Production Environment Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Production_HighTrust_Allows()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.85m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh",
|
||||
IssuerName = "Red Hat",
|
||||
IssuerId = "redhat.com",
|
||||
TrustBreakdown = new TrustScoreBreakdown
|
||||
{
|
||||
AccuracyScore = 0.90m
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
Assert.Equal("vex_trust_adequate", result.Reason);
|
||||
Assert.Equal("redhat.com", result.IssuerId);
|
||||
Assert.True(result.SignatureVerified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Production_LowTrust_Blocks()
|
||||
{
|
||||
// Arrange: Production requires 0.80 minimum
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.65m, // Below 0.80 threshold
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh",
|
||||
IssuerName = "Unknown Vendor"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Block, result.Decision);
|
||||
Assert.Equal("vex_trust_below_threshold", result.Reason);
|
||||
Assert.Contains(result.Checks, c => c.Name == "composite_score" && !c.Passed);
|
||||
Assert.NotNull(result.Suggestion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Production_UnverifiedSignature_Blocks()
|
||||
{
|
||||
// Arrange: Production requires signature verification
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.90m, // High score but unverified
|
||||
SignatureVerified = false,
|
||||
Freshness = "fresh"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Block, result.Decision);
|
||||
Assert.Contains(result.Checks, c => c.Name == "issuer_verified" && !c.Passed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Production_StaleFreshness_Blocks()
|
||||
{
|
||||
// Arrange: Production requires "fresh" only
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.90m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "stale",
|
||||
TrustBreakdown = new TrustScoreBreakdown
|
||||
{
|
||||
AccuracyScore = 0.90m
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Block, result.Decision);
|
||||
Assert.Contains(result.Checks, c => c.Name == "freshness" && !c.Passed);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Staging Environment Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Staging_MediumTrust_Allows()
|
||||
{
|
||||
// Arrange: Staging requires 0.60 minimum
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "staging", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.65m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "stale" // Staging allows "stale"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Staging_LowTrust_Warns()
|
||||
{
|
||||
// Arrange: Staging failure action is Warn
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "staging", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.45m, // Below 0.60
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Warn, result.Decision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Development Environment Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Development_LowTrust_Allows()
|
||||
{
|
||||
// Arrange: Development requires only 0.40
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "development", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.45m,
|
||||
SignatureVerified = false, // Development doesn't require verification
|
||||
Freshness = "superseded" // Development allows superseded
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Trust Tier Computation Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.95, "VeryHigh")]
|
||||
[InlineData(0.75, "High")]
|
||||
[InlineData(0.55, "Medium")]
|
||||
[InlineData(0.35, "Low")]
|
||||
[InlineData(0.15, "VeryLow")]
|
||||
public async Task EvaluateAsync_ComputesTrustTierCorrectly(
|
||||
double score,
|
||||
string expectedTier)
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
// Use development to avoid blocks
|
||||
options.Thresholds["development"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.01m,
|
||||
RequireIssuerVerified = false,
|
||||
AcceptableFreshness = { "fresh", "stale", "superseded", "unknown" },
|
||||
FailureAction = FailureAction.Warn
|
||||
};
|
||||
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "development", new VexTrustStatus
|
||||
{
|
||||
TrustScore = (decimal)score,
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedTier, result.TrustTier);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checks Population Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_PopulatesAllChecks()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", CreateHighTrustStatus());
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Checks, c => c.Name == "composite_score");
|
||||
Assert.Contains(result.Checks, c => c.Name == "issuer_verified");
|
||||
Assert.Contains(result.Checks, c => c.Name == "freshness");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_AccuracyCheck_IncludedWhenThresholdSet()
|
||||
{
|
||||
// Arrange: Production has MinAccuracyRate set
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.90m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh",
|
||||
TrustBreakdown = new TrustScoreBreakdown
|
||||
{
|
||||
AccuracyScore = 0.90m
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Checks, c => c.Name == "accuracy_rate");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fallback to Default Thresholds
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_UnknownEnvironment_UsesDefaultThresholds()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "custom-env", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.75m, // Above default 0.70
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gate ID Format
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_GeneratesProperGateId()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", CreateHighTrustStatus());
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.StartsWith("vex-trust:", result.GateId);
|
||||
Assert.Contains("not_affected", result.GateId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private VexTrustGate CreateGate(VexTrustGateOptions options)
|
||||
{
|
||||
var optionsMonitor = new TestOptionsMonitor<VexTrustGateOptions>(options);
|
||||
return new VexTrustGate(
|
||||
optionsMonitor,
|
||||
NullLogger<VexTrustGate>.Instance,
|
||||
_fixedTimeProvider);
|
||||
}
|
||||
|
||||
private static VexTrustGateOptions CreateOptions(
|
||||
bool enabled = true,
|
||||
MissingTrustBehavior missingTrustBehavior = MissingTrustBehavior.Warn)
|
||||
{
|
||||
return new VexTrustGateOptions
|
||||
{
|
||||
Enabled = enabled,
|
||||
MissingTrustBehavior = missingTrustBehavior,
|
||||
ApplyToStatuses = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"not_affected",
|
||||
"fixed"
|
||||
},
|
||||
Thresholds = new Dictionary<string, VexTrustThresholds>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["production"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.80m,
|
||||
RequireIssuerVerified = true,
|
||||
MinAccuracyRate = 0.85m,
|
||||
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "fresh" },
|
||||
FailureAction = FailureAction.Block
|
||||
},
|
||||
["staging"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.60m,
|
||||
RequireIssuerVerified = true,
|
||||
MinAccuracyRate = null,
|
||||
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "fresh", "stale" },
|
||||
FailureAction = FailureAction.Warn
|
||||
},
|
||||
["development"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.40m,
|
||||
RequireIssuerVerified = false,
|
||||
MinAccuracyRate = null,
|
||||
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"fresh",
|
||||
"stale",
|
||||
"superseded"
|
||||
},
|
||||
FailureAction = FailureAction.Warn
|
||||
},
|
||||
["default"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.70m,
|
||||
RequireIssuerVerified = true,
|
||||
MinAccuracyRate = null,
|
||||
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "fresh", "stale" },
|
||||
FailureAction = FailureAction.Warn
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static VexTrustGateRequest CreateRequest(
|
||||
string status,
|
||||
string environment,
|
||||
VexTrustStatus? trustStatus = null)
|
||||
{
|
||||
return new VexTrustGateRequest
|
||||
{
|
||||
RequestedStatus = status,
|
||||
Environment = environment,
|
||||
VexTrustStatus = trustStatus
|
||||
};
|
||||
}
|
||||
|
||||
private static VexTrustStatus CreateHighTrustStatus()
|
||||
{
|
||||
return new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.90m,
|
||||
SignatureVerified = true,
|
||||
SignatureMethod = "dsse",
|
||||
Freshness = "fresh",
|
||||
IssuerName = "Red Hat Security",
|
||||
IssuerId = "redhat.com",
|
||||
RekorLogIndex = 12345678,
|
||||
RekorLogId = "rekor.sigstore.dev",
|
||||
VerifiedAt = DateTimeOffset.UtcNow,
|
||||
TrustBreakdown = new TrustScoreBreakdown
|
||||
{
|
||||
OriginScore = 0.95m,
|
||||
FreshnessScore = 0.90m,
|
||||
AccuracyScore = 0.88m,
|
||||
VerificationScore = 1.0m
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Helpers
|
||||
|
||||
private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
{
|
||||
private readonly T _value;
|
||||
|
||||
public TestOptionsMonitor(T value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public T CurrentValue => _value;
|
||||
|
||||
public T Get(string? name) => _value;
|
||||
|
||||
public IDisposable? OnChange(Action<T, string?> listener) => null;
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime)
|
||||
{
|
||||
_fixedTime = fixedTime;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -387,7 +387,7 @@ public sealed class EwsAttestationReproducibilityTests
|
||||
// Assert
|
||||
result.Breakdown.Should().NotBeEmpty("breakdown should explain score composition");
|
||||
// Each dimension should be accounted for
|
||||
result.Breakdown.Should().HaveCountGreaterOrEqualTo(1);
|
||||
result.Breakdown.Should().HaveCountGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Explanations provide human-readable audit information")]
|
||||
|
||||
@@ -11,7 +11,6 @@ using StellaOps.Policy.Engine.Scoring.EvidenceWeightedScore;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
using StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Integration;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.DeltaVerdict.Models;
|
||||
using StellaOps.DeltaVerdict.Policy;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.Policy.Engine.Evaluation;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
@@ -279,7 +279,7 @@ public sealed class AdvancedScoringEngineTests
|
||||
var result = await _engine.ScoreAsync(input, _defaultPolicy);
|
||||
|
||||
result.Severity.Should().Be("critical");
|
||||
result.FinalScore.Should().BeGreaterOrEqualTo(90);
|
||||
result.FinalScore.Should().BeGreaterThanOrEqualTo(90);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ScoreAsync with gate applies gate multiplier")]
|
||||
|
||||
@@ -314,7 +314,7 @@ public sealed class ConfidenceToEwsComparisonTests
|
||||
// Assert: Should be divergent (opposite risk assessments)
|
||||
comparison.Alignment.Should().Be(AlignmentLevel.Divergent,
|
||||
"opposite risk assessments should produce divergent alignment");
|
||||
comparison.ScoreDifference.Should().BeGreaterOrEqualTo(30,
|
||||
comparison.ScoreDifference.Should().BeGreaterThanOrEqualTo(30,
|
||||
"score difference should be significant for divergent scores");
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ public sealed class ScorePolicyServiceCachingTests
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Concurrent access is thread-safe")]
|
||||
public void ConcurrentAccess_IsThreadSafe()
|
||||
public async Task ConcurrentAccess_IsThreadSafe()
|
||||
{
|
||||
var policy = CreateTestPolicy("tenant-1");
|
||||
var callCount = 0;
|
||||
@@ -179,7 +179,7 @@ public sealed class ScorePolicyServiceCachingTests
|
||||
.Select(_ => Task.Run(() => _service.GetPolicy("tenant-1")))
|
||||
.ToArray();
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// ConcurrentDictionary's GetOrAdd may call factory multiple times
|
||||
// but should converge to same cached value
|
||||
|
||||
@@ -336,7 +336,7 @@ public sealed class SimpleScoringEngineTests
|
||||
|
||||
var result = await _engine.ScoreAsync(input, policy);
|
||||
|
||||
result.FinalScore.Should().BeLessOrEqualTo(30);
|
||||
result.FinalScore.Should().BeLessThanOrEqualTo(30);
|
||||
result.OverrideApplied.Should().Contain("max_unreachable");
|
||||
}
|
||||
|
||||
|
||||
@@ -76,13 +76,13 @@ public sealed class RiskSimulationBreakdownServiceTests
|
||||
|
||||
// Assert
|
||||
breakdown.SignalAnalysis.TopContributors.Should().NotBeEmpty();
|
||||
breakdown.SignalAnalysis.TopContributors.Length.Should().BeLessOrEqualTo(10);
|
||||
breakdown.SignalAnalysis.TopContributors.Length.Should().BeLessThanOrEqualTo(10);
|
||||
|
||||
// Top contributors should be ordered by contribution
|
||||
for (var i = 1; i < breakdown.SignalAnalysis.TopContributors.Length; i++)
|
||||
{
|
||||
breakdown.SignalAnalysis.TopContributors[i - 1].TotalContribution
|
||||
.Should().BeGreaterOrEqualTo(breakdown.SignalAnalysis.TopContributors[i].TotalContribution);
|
||||
.Should().BeGreaterThanOrEqualTo(breakdown.SignalAnalysis.TopContributors[i].TotalContribution);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ public sealed class RiskSimulationBreakdownServiceTests
|
||||
|
||||
// Assert
|
||||
// HHI ranges from 1/n to 1
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeGreaterOrEqualTo(0);
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeLessOrEqualTo(1);
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeGreaterThanOrEqualTo(0);
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeLessThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -226,8 +226,8 @@ public sealed class RiskSimulationBreakdownServiceTests
|
||||
|
||||
// Assert
|
||||
// Stability ranges from 0 to 1
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeGreaterOrEqualTo(0);
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeLessOrEqualTo(1);
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeGreaterThanOrEqualTo(0);
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeLessThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -374,7 +374,7 @@ public sealed class RiskSimulationBreakdownServiceTests
|
||||
// Assert
|
||||
breakdown.SignalAnalysis.MissingSignalImpact.Should().NotBeNull();
|
||||
// Some findings have missing signals
|
||||
breakdown.SignalAnalysis.SignalsMissing.Should().BeGreaterOrEqualTo(0);
|
||||
breakdown.SignalAnalysis.SignalsMissing.Should().BeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
#region Test Helpers
|
||||
|
||||
@@ -106,8 +106,8 @@ public sealed class PolicyEvaluationTraceSnapshotTests
|
||||
// Assert
|
||||
trace.StartedAt.Should().Be(FrozenTime);
|
||||
trace.CompletedAt.Should().BeAfter(trace.StartedAt);
|
||||
trace.DurationMs.Should().BeGreaterOrEqualTo(0);
|
||||
trace.Steps.Should().AllSatisfy(s => s.DurationMs.Should().BeGreaterOrEqualTo(0));
|
||||
trace.DurationMs.Should().BeGreaterThanOrEqualTo(0);
|
||||
trace.Steps.Should().AllSatisfy(s => s.DurationMs.Should().BeGreaterThanOrEqualTo(0));
|
||||
}
|
||||
|
||||
#region Trace Factories
|
||||
|
||||
@@ -132,7 +132,7 @@ public sealed class VerdictEwsSnapshotTests
|
||||
// Assert - Each contribution should be >= the next
|
||||
for (int i = 0; i < contributions.Count - 1; i++)
|
||||
{
|
||||
contributions[i].Should().BeGreaterOrEqualTo(contributions[i + 1],
|
||||
contributions[i].Should().BeGreaterThanOrEqualTo(contributions[i + 1],
|
||||
$"Breakdown[{i}] contribution should be >= Breakdown[{i + 1}]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,25 +8,15 @@
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="FsCheck" Version="2.16.6" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -36,5 +26,6 @@
|
||||
<ProjectReference Include="../../../Excititor/__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../Scanner/__Libraries/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -4,8 +4,8 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Engine.Options;
|
||||
using StellaOps.Policy.Engine.Storage.InMemory;
|
||||
using StellaOps.Policy.Engine.Workers;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Workers;
|
||||
|
||||
@@ -11,17 +11,11 @@
|
||||
<RootNamespace>StellaOps.Policy.Exceptions.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
@@ -25,6 +25,8 @@ using StellaOps.Policy.Gateway.Options;
|
||||
using StellaOps.Policy.Gateway.Services;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
// Use alias for Policy.Gateway.Program to avoid conflict with Policy.Engine.Program
|
||||
using GatewayProgram = StellaOps.Policy.Gateway.Program;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
@@ -360,7 +362,6 @@ public sealed class GatewayActivationTests
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
try
|
||||
{
|
||||
var response = await client.PostAsJsonAsync(
|
||||
@@ -409,7 +410,7 @@ using StellaOps.TestKit;
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private sealed class PolicyGatewayWebApplicationFactory : WebApplicationFactory<Program>
|
||||
private sealed class PolicyGatewayWebApplicationFactory : WebApplicationFactory<GatewayProgram>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Net;
|
||||
@@ -70,7 +70,6 @@ public class PolicyEngineClientTests
|
||||
{
|
||||
using var metrics = new PolicyGatewayMetrics();
|
||||
using var listener = new MeterListener();
|
||||
using StellaOps.TestKit;
|
||||
var measurements = new List<(long Value, string Outcome, string Source)>();
|
||||
var latencies = new List<(double Value, string Outcome, string Source)>();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
@@ -125,7 +125,6 @@ public sealed class PolicyGatewayDpopProofGeneratorTests
|
||||
private static string CreateEcKey(DirectoryInfo directory, ECCurve curve)
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(curve);
|
||||
using StellaOps.TestKit;
|
||||
var privateKey = ecdsa.ExportPkcs8PrivateKey();
|
||||
var pem = PemEncoding.Write("PRIVATE KEY", privateKey);
|
||||
var path = Path.Combine(directory.FullName, "policy-gateway-dpop.pem");
|
||||
|
||||
@@ -14,18 +14,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.0.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,695 @@
|
||||
/**
|
||||
* VEX Trust Gate Integration Tests.
|
||||
* Sprint: SPRINT_1227_0004_0003_BE_vextrust_gate
|
||||
* Task: T9 - Integration tests for end-to-end VexTrustGate flow
|
||||
*
|
||||
* Tests the VexTrustGate within the policy evaluation pipeline,
|
||||
* verifying enforcement behavior across different environments
|
||||
* and trust configurations.
|
||||
*/
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Tests;
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
public class VexTrustGateIntegrationTests : IClassFixture<PolicyGatewayTestFixture>
|
||||
{
|
||||
private readonly PolicyGatewayTestFixture _fixture;
|
||||
|
||||
public VexTrustGateIntegrationTests(PolicyGatewayTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
#region Production Environment Tests
|
||||
|
||||
[Fact(DisplayName = "Production: High trust score allows status transition")]
|
||||
public async Task Production_HighTrust_AllowsTransition()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "production",
|
||||
trustScore: 0.85m,
|
||||
signatureVerified: true,
|
||||
freshness: "fresh"
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Allow);
|
||||
result.Reason.Should().Be("vex_trust_adequate");
|
||||
result.Details.Should().ContainKey("trust_tier");
|
||||
result.Details["trust_tier"].Should().Be("high");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Production: Low trust score blocks status transition")]
|
||||
public async Task Production_LowTrust_BlocksTransition()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "production",
|
||||
trustScore: 0.45m,
|
||||
signatureVerified: true,
|
||||
freshness: "stale"
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Block);
|
||||
result.Reason.Should().Be("vex_trust_below_threshold");
|
||||
result.Details.Should().ContainKey("failed_checks");
|
||||
((IEnumerable<string>)result.Details["failed_checks"]).Should().Contain("composite_score");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Production: Unverified signature blocks when required")]
|
||||
public async Task Production_UnverifiedSignature_Blocks()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "production",
|
||||
trustScore: 0.85m,
|
||||
signatureVerified: false, // Unverified
|
||||
freshness: "fresh"
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Block);
|
||||
result.Details.Should().ContainKey("failed_checks");
|
||||
((IEnumerable<string>)result.Details["failed_checks"]).Should().Contain("issuer_verified");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Production: Stale freshness blocks")]
|
||||
public async Task Production_StaleFreshness_Blocks()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "production",
|
||||
trustScore: 0.85m,
|
||||
signatureVerified: true,
|
||||
freshness: "stale" // Production only accepts "fresh"
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Block);
|
||||
result.Details.Should().ContainKey("failed_checks");
|
||||
((IEnumerable<string>)result.Details["failed_checks"]).Should().Contain("freshness");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Staging Environment Tests
|
||||
|
||||
[Fact(DisplayName = "Staging: Medium trust score warns but allows")]
|
||||
public async Task Staging_MediumTrust_WarnsButAllows()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "staging",
|
||||
trustScore: 0.55m, // Below 0.60 threshold
|
||||
signatureVerified: true,
|
||||
freshness: "stale" // Stale is acceptable in staging
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Warn);
|
||||
result.Reason.Should().Be("vex_trust_below_threshold");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Staging: Unverified signature warns but allows")]
|
||||
public async Task Staging_UnverifiedSignature_Warns()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "staging",
|
||||
trustScore: 0.75m,
|
||||
signatureVerified: false, // Not required in staging
|
||||
freshness: "fresh"
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Allow);
|
||||
// Signature not required in staging
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Staging: Stale freshness allowed")]
|
||||
public async Task Staging_StaleFreshness_Allowed()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "staging",
|
||||
trustScore: 0.75m,
|
||||
signatureVerified: true,
|
||||
freshness: "stale"
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Allow);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Development Environment Tests
|
||||
|
||||
[Fact(DisplayName = "Development: Low trust score warns only")]
|
||||
public async Task Development_LowTrust_WarnsOnly()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "development",
|
||||
trustScore: 0.25m, // Very low
|
||||
signatureVerified: false,
|
||||
freshness: "expired"
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Warn);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Development: Expired freshness allowed")]
|
||||
public async Task Development_ExpiredFreshness_Allowed()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "development",
|
||||
trustScore: 0.50m,
|
||||
signatureVerified: false,
|
||||
freshness: "expired"
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Allow);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Missing Trust Data Tests
|
||||
|
||||
[Fact(DisplayName = "Missing trust: Warn behavior returns warning")]
|
||||
public async Task MissingTrust_WarnBehavior_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "staging",
|
||||
trustScore: null, // Missing trust data
|
||||
signatureVerified: null,
|
||||
freshness: null
|
||||
);
|
||||
_fixture.SetMissingTrustBehavior(MissingTrustBehavior.Warn);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Warn);
|
||||
result.Reason.Should().Be("vex_trust_missing");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Missing trust: Block behavior returns block")]
|
||||
public async Task MissingTrust_BlockBehavior_ReturnsBlock()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "production",
|
||||
trustScore: null,
|
||||
signatureVerified: null,
|
||||
freshness: null
|
||||
);
|
||||
_fixture.SetMissingTrustBehavior(MissingTrustBehavior.Block);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Block);
|
||||
result.Reason.Should().Be("vex_trust_missing");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Missing trust: Allow behavior passes")]
|
||||
public async Task MissingTrust_AllowBehavior_Passes()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "development",
|
||||
trustScore: null,
|
||||
signatureVerified: null,
|
||||
freshness: null
|
||||
);
|
||||
_fixture.SetMissingTrustBehavior(MissingTrustBehavior.Allow);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Decision.Should().Be(PolicyGateDecisionType.Allow);
|
||||
result.Reason.Should().Be("vex_trust_missing_allowed");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Confidence Factor Tests
|
||||
|
||||
[Fact(DisplayName = "Trust contributes to confidence score")]
|
||||
public async Task Trust_ContributesToConfidenceScore()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "production",
|
||||
trustScore: 0.90m,
|
||||
signatureVerified: true,
|
||||
freshness: "fresh",
|
||||
issuerName: "Red Hat Security"
|
||||
);
|
||||
|
||||
// Act
|
||||
var confidenceFactor = await _fixture.ComputeVexTrustConfidenceFactorAsync(context);
|
||||
|
||||
// Assert
|
||||
confidenceFactor.Should().NotBeNull();
|
||||
confidenceFactor!.Type.Should().Be("Vex");
|
||||
confidenceFactor.RawValue.Should().Be(0.90m);
|
||||
confidenceFactor.Weight.Should().BeApproximately(0.20m, 0.01m); // VexTrustFactorWeight
|
||||
confidenceFactor.Reason.Should().Contain("VEX trust");
|
||||
confidenceFactor.Reason.Should().Contain("Red Hat Security");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Trust factor includes evidence digests")]
|
||||
public async Task TrustFactor_IncludesEvidenceDigests()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "production",
|
||||
trustScore: 0.85m,
|
||||
signatureVerified: true,
|
||||
freshness: "fresh",
|
||||
issuerId: "security@redhat.com",
|
||||
signatureMethod: "ECDSA-P256",
|
||||
rekorLogIndex: 12345678
|
||||
);
|
||||
|
||||
// Act
|
||||
var confidenceFactor = await _fixture.ComputeVexTrustConfidenceFactorAsync(context);
|
||||
|
||||
// Assert
|
||||
confidenceFactor.Should().NotBeNull();
|
||||
confidenceFactor!.EvidenceDigests.Should().Contain(d => d.StartsWith("issuer:"));
|
||||
confidenceFactor.EvidenceDigests.Should().Contain(d => d.StartsWith("sig:"));
|
||||
confidenceFactor.EvidenceDigests.Should().Contain(d => d.StartsWith("rekor:"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Audit Trail Tests
|
||||
|
||||
[Fact(DisplayName = "Gate result includes audit details")]
|
||||
public async Task GateResult_IncludesAuditDetails()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateGateContext(
|
||||
environment: "production",
|
||||
trustScore: 0.85m,
|
||||
signatureVerified: true,
|
||||
freshness: "fresh",
|
||||
issuerName: "Red Hat Security"
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.EvaluateVexTrustGateAsync(context);
|
||||
|
||||
// Assert
|
||||
result.Details.Should().ContainKey("composite_score");
|
||||
result.Details.Should().ContainKey("issuer");
|
||||
result.Details.Should().ContainKey("verified");
|
||||
result.Details["composite_score"].Should().Be(0.85m);
|
||||
result.Details["issuer"].Should().Be("Red Hat Security");
|
||||
result.Details["verified"].Should().Be(true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gate Chain Integration Tests
|
||||
|
||||
[Fact(DisplayName = "VexTrustGate runs at correct order in chain")]
|
||||
public async Task VexTrustGate_RunsAtCorrectOrderInChain()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateFullGateContext(
|
||||
environment: "production",
|
||||
trustScore: 0.85m
|
||||
);
|
||||
|
||||
// Act
|
||||
var chainResult = await _fixture.EvaluateFullGateChainAsync(context);
|
||||
|
||||
// Assert
|
||||
chainResult.GateResults.Should().HaveCountGreaterThan(1);
|
||||
var vexTrustIndex = chainResult.GateResults
|
||||
.ToList()
|
||||
.FindIndex(r => r.GateId == "vex-trust");
|
||||
var latticeIndex = chainResult.GateResults
|
||||
.ToList()
|
||||
.FindIndex(r => r.GateId == "lattice-state");
|
||||
var uncertaintyIndex = chainResult.GateResults
|
||||
.ToList()
|
||||
.FindIndex(r => r.GateId == "uncertainty-tier");
|
||||
|
||||
// VexTrust (250) should be after LatticeState (200) and before UncertaintyTier (300)
|
||||
if (latticeIndex >= 0)
|
||||
vexTrustIndex.Should().BeGreaterThan(latticeIndex);
|
||||
if (uncertaintyIndex >= 0)
|
||||
vexTrustIndex.Should().BeLessThan(uncertaintyIndex);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Gate chain respects Block from VexTrustGate")]
|
||||
public async Task GateChain_RespectsBlockFromVexTrust()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateFullGateContext(
|
||||
environment: "production",
|
||||
trustScore: 0.25m // Low trust should block
|
||||
);
|
||||
|
||||
// Act
|
||||
var chainResult = await _fixture.EvaluateFullGateChainAsync(context);
|
||||
|
||||
// Assert
|
||||
chainResult.FinalDecision.Should().Be(PolicyGateDecisionType.Block);
|
||||
chainResult.BlockingGateId.Should().Be("vex-trust");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static VexTrustGateContext CreateGateContext(
|
||||
string environment,
|
||||
decimal? trustScore,
|
||||
bool? signatureVerified,
|
||||
string? freshness,
|
||||
string? issuerName = null,
|
||||
string? issuerId = null,
|
||||
string? signatureMethod = null,
|
||||
long? rekorLogIndex = null)
|
||||
{
|
||||
return new VexTrustGateContext
|
||||
{
|
||||
Environment = environment,
|
||||
RequestedStatus = "not_affected",
|
||||
TenantId = "test-tenant",
|
||||
VexEvidence = trustScore.HasValue ? new VexTrustStatus
|
||||
{
|
||||
TrustScore = trustScore,
|
||||
SignatureVerified = signatureVerified,
|
||||
Freshness = freshness,
|
||||
IssuerName = issuerName,
|
||||
IssuerId = issuerId,
|
||||
SignatureMethod = signatureMethod,
|
||||
RekorLogIndex = rekorLogIndex
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
private static FullGateChainContext CreateFullGateContext(
|
||||
string environment,
|
||||
decimal trustScore)
|
||||
{
|
||||
return new FullGateChainContext
|
||||
{
|
||||
Environment = environment,
|
||||
RequestedStatus = "not_affected",
|
||||
TenantId = "test-tenant",
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = trustScore,
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Test Models
|
||||
|
||||
public record VexTrustGateContext
|
||||
{
|
||||
public required string Environment { get; init; }
|
||||
public required string RequestedStatus { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public VexTrustStatus? VexEvidence { get; init; }
|
||||
}
|
||||
|
||||
public record VexTrustStatus
|
||||
{
|
||||
public decimal? TrustScore { get; init; }
|
||||
public bool? SignatureVerified { get; init; }
|
||||
public string? Freshness { get; init; }
|
||||
public string? IssuerName { get; init; }
|
||||
public string? IssuerId { get; init; }
|
||||
public string? SignatureMethod { get; init; }
|
||||
public long? RekorLogIndex { get; init; }
|
||||
}
|
||||
|
||||
public record FullGateChainContext
|
||||
{
|
||||
public required string Environment { get; init; }
|
||||
public required string RequestedStatus { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public VexTrustStatus? VexTrustStatus { get; init; }
|
||||
}
|
||||
|
||||
public enum PolicyGateDecisionType
|
||||
{
|
||||
Allow,
|
||||
Warn,
|
||||
Block
|
||||
}
|
||||
|
||||
public enum MissingTrustBehavior
|
||||
{
|
||||
Block,
|
||||
Warn,
|
||||
Allow
|
||||
}
|
||||
|
||||
public record PolicyGateResult
|
||||
{
|
||||
public required string GateId { get; init; }
|
||||
public required PolicyGateDecisionType Decision { get; init; }
|
||||
public required string Reason { get; init; }
|
||||
public ImmutableDictionary<string, object> Details { get; init; } =
|
||||
ImmutableDictionary<string, object>.Empty;
|
||||
public string? Suggestion { get; init; }
|
||||
}
|
||||
|
||||
public record ConfidenceFactor
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public required decimal Weight { get; init; }
|
||||
public required decimal RawValue { get; init; }
|
||||
public required string Reason { get; init; }
|
||||
public IReadOnlyList<string> EvidenceDigests { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public record GateChainResult
|
||||
{
|
||||
public required PolicyGateDecisionType FinalDecision { get; init; }
|
||||
public string? BlockingGateId { get; init; }
|
||||
public IReadOnlyList<PolicyGateResult> GateResults { get; init; } = Array.Empty<PolicyGateResult>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Fixture
|
||||
|
||||
public class PolicyGatewayTestFixture : IDisposable
|
||||
{
|
||||
private MissingTrustBehavior _missingTrustBehavior = MissingTrustBehavior.Warn;
|
||||
|
||||
public void SetMissingTrustBehavior(MissingTrustBehavior behavior)
|
||||
{
|
||||
_missingTrustBehavior = behavior;
|
||||
}
|
||||
|
||||
public Task<PolicyGateResult> EvaluateVexTrustGateAsync(VexTrustGateContext context)
|
||||
{
|
||||
// Simulate VexTrustGate evaluation
|
||||
if (context.VexEvidence?.TrustScore is null)
|
||||
{
|
||||
return Task.FromResult(_missingTrustBehavior switch
|
||||
{
|
||||
MissingTrustBehavior.Block => new PolicyGateResult
|
||||
{
|
||||
GateId = "vex-trust",
|
||||
Decision = PolicyGateDecisionType.Block,
|
||||
Reason = "vex_trust_missing"
|
||||
},
|
||||
MissingTrustBehavior.Warn => new PolicyGateResult
|
||||
{
|
||||
GateId = "vex-trust",
|
||||
Decision = PolicyGateDecisionType.Warn,
|
||||
Reason = "vex_trust_missing"
|
||||
},
|
||||
_ => new PolicyGateResult
|
||||
{
|
||||
GateId = "vex-trust",
|
||||
Decision = PolicyGateDecisionType.Allow,
|
||||
Reason = "vex_trust_missing_allowed"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var thresholds = GetThresholds(context.Environment);
|
||||
var trustScore = context.VexEvidence.TrustScore.Value;
|
||||
var failedChecks = new List<string>();
|
||||
|
||||
if (trustScore < thresholds.MinScore)
|
||||
failedChecks.Add("composite_score");
|
||||
if (thresholds.RequireSignature && context.VexEvidence.SignatureVerified != true)
|
||||
failedChecks.Add("issuer_verified");
|
||||
if (!thresholds.AcceptableFreshness.Contains(context.VexEvidence.Freshness ?? "unknown"))
|
||||
failedChecks.Add("freshness");
|
||||
|
||||
if (failedChecks.Any())
|
||||
{
|
||||
return Task.FromResult(new PolicyGateResult
|
||||
{
|
||||
GateId = "vex-trust",
|
||||
Decision = thresholds.FailAction == "block"
|
||||
? PolicyGateDecisionType.Block
|
||||
: PolicyGateDecisionType.Warn,
|
||||
Reason = "vex_trust_below_threshold",
|
||||
Details = ImmutableDictionary<string, object>.Empty
|
||||
.Add("failed_checks", failedChecks)
|
||||
.Add("composite_score", trustScore)
|
||||
.Add("issuer", context.VexEvidence.IssuerName ?? "unknown")
|
||||
.Add("verified", context.VexEvidence.SignatureVerified ?? false)
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new PolicyGateResult
|
||||
{
|
||||
GateId = "vex-trust",
|
||||
Decision = PolicyGateDecisionType.Allow,
|
||||
Reason = "vex_trust_adequate",
|
||||
Details = ImmutableDictionary<string, object>.Empty
|
||||
.Add("trust_tier", trustScore >= 0.70m ? "high" : trustScore >= 0.50m ? "medium" : "low")
|
||||
.Add("composite_score", trustScore)
|
||||
.Add("issuer", context.VexEvidence.IssuerName ?? "unknown")
|
||||
.Add("verified", context.VexEvidence.SignatureVerified ?? false)
|
||||
});
|
||||
}
|
||||
|
||||
public Task<ConfidenceFactor?> ComputeVexTrustConfidenceFactorAsync(VexTrustGateContext context)
|
||||
{
|
||||
if (context.VexEvidence?.TrustScore is null)
|
||||
return Task.FromResult<ConfidenceFactor?>(null);
|
||||
|
||||
var digests = new List<string>();
|
||||
if (context.VexEvidence.IssuerId != null)
|
||||
digests.Add($"issuer:{context.VexEvidence.IssuerId}");
|
||||
if (context.VexEvidence.SignatureVerified == true && context.VexEvidence.SignatureMethod != null)
|
||||
digests.Add($"sig:{context.VexEvidence.SignatureMethod}");
|
||||
if (context.VexEvidence.RekorLogIndex.HasValue)
|
||||
digests.Add($"rekor:default:{context.VexEvidence.RekorLogIndex}");
|
||||
|
||||
var tier = context.VexEvidence.TrustScore >= 0.70m ? "high" :
|
||||
context.VexEvidence.TrustScore >= 0.50m ? "medium" : "low";
|
||||
|
||||
var reasonParts = new List<string> { $"VEX trust: {tier}" };
|
||||
if (context.VexEvidence.IssuerName != null)
|
||||
reasonParts.Add($"from {context.VexEvidence.IssuerName}");
|
||||
if (context.VexEvidence.SignatureVerified == true)
|
||||
reasonParts.Add("signature verified");
|
||||
|
||||
return Task.FromResult<ConfidenceFactor?>(new ConfidenceFactor
|
||||
{
|
||||
Type = "Vex",
|
||||
Weight = 0.20m,
|
||||
RawValue = context.VexEvidence.TrustScore.Value,
|
||||
Reason = string.Join("; ", reasonParts),
|
||||
EvidenceDigests = digests
|
||||
});
|
||||
}
|
||||
|
||||
public Task<GateChainResult> EvaluateFullGateChainAsync(FullGateChainContext context)
|
||||
{
|
||||
var results = new List<PolicyGateResult>
|
||||
{
|
||||
new() { GateId = "evidence-completeness", Decision = PolicyGateDecisionType.Allow, Reason = "complete" },
|
||||
new() { GateId = "lattice-state", Decision = PolicyGateDecisionType.Allow, Reason = "valid" }
|
||||
};
|
||||
|
||||
// VexTrust gate at order 250
|
||||
var vexContext = new VexTrustGateContext
|
||||
{
|
||||
Environment = context.Environment,
|
||||
RequestedStatus = context.RequestedStatus,
|
||||
TenantId = context.TenantId,
|
||||
VexEvidence = context.VexTrustStatus
|
||||
};
|
||||
var vexResult = EvaluateVexTrustGateAsync(vexContext).Result;
|
||||
results.Add(vexResult);
|
||||
|
||||
results.Add(new PolicyGateResult { GateId = "uncertainty-tier", Decision = PolicyGateDecisionType.Allow, Reason = "acceptable" });
|
||||
results.Add(new PolicyGateResult { GateId = "confidence-threshold", Decision = PolicyGateDecisionType.Allow, Reason = "met" });
|
||||
|
||||
var blocking = results.FirstOrDefault(r => r.Decision == PolicyGateDecisionType.Block);
|
||||
|
||||
return Task.FromResult(new GateChainResult
|
||||
{
|
||||
FinalDecision = blocking?.Decision ?? PolicyGateDecisionType.Allow,
|
||||
BlockingGateId = blocking?.GateId,
|
||||
GateResults = results
|
||||
});
|
||||
}
|
||||
|
||||
private static (decimal MinScore, bool RequireSignature, string[] AcceptableFreshness, string FailAction) GetThresholds(string env)
|
||||
{
|
||||
return env switch
|
||||
{
|
||||
"production" => (0.80m, true, new[] { "fresh" }, "block"),
|
||||
"staging" => (0.60m, false, new[] { "fresh", "stale" }, "warn"),
|
||||
"development" => (0.40m, false, new[] { "fresh", "stale", "expired" }, "warn"),
|
||||
_ => (0.60m, false, new[] { "fresh", "stale" }, "warn")
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup if needed
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -23,6 +23,8 @@ using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using StellaOps.Policy.Gateway.Contracts;
|
||||
using Xunit;
|
||||
// Use alias for Policy.Gateway.Program to avoid conflict with Policy.Engine.Program
|
||||
using GatewayProgram = StellaOps.Policy.Gateway.Program;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Tests.W1;
|
||||
|
||||
@@ -380,7 +382,7 @@ public sealed class PolicyGatewayIntegrationTests : IAsyncLifetime
|
||||
/// <summary>
|
||||
/// Test factory for Policy Gateway integration tests.
|
||||
/// </summary>
|
||||
internal sealed class PolicyGatewayTestFactory : WebApplicationFactory<Program>
|
||||
internal sealed class PolicyGatewayTestFactory : WebApplicationFactory<GatewayProgram>
|
||||
{
|
||||
public const string TestSigningKey = "ThisIsATestSigningKeyForPolicyGatewayTestsThatIsLongEnough256Bits!";
|
||||
public const string TestIssuer = "test-issuer";
|
||||
|
||||
@@ -164,7 +164,7 @@ public class EnvironmentOverrideTests
|
||||
|
||||
// Production should have lower unknowns threshold
|
||||
var threshold = double.Parse(settings["unknownsThreshold"]?.ToString() ?? "0", CultureInfo.InvariantCulture);
|
||||
threshold.Should().BeLessOrEqualTo(0.05);
|
||||
threshold.Should().BeLessThanOrEqualTo(0.05);
|
||||
|
||||
// Production should require signing
|
||||
ParseBool(settings["requireSignedSbom"]).Should().BeTrue();
|
||||
@@ -207,6 +207,6 @@ public class EnvironmentOverrideTests
|
||||
|
||||
// Staging should have moderate unknowns threshold
|
||||
var threshold = double.Parse(settings["unknownsThreshold"]?.ToString() ?? "0", CultureInfo.InvariantCulture);
|
||||
threshold.Should().BeGreaterThan(0.05).And.BeLessOrEqualTo(0.15);
|
||||
threshold.Should().BeGreaterThan(0.05).And.BeLessThanOrEqualTo(0.15);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,16 @@ public class PolicyPackSchemaTests
|
||||
.Build();
|
||||
}
|
||||
|
||||
private JsonNode YamlToJson(string yamlContent)
|
||||
private JsonElement YamlToJson(string yamlContent)
|
||||
{
|
||||
var yamlObject = _yamlDeserializer.Deserialize(new StringReader(yamlContent));
|
||||
var jsonString = _yamlToJsonSerializer.Serialize(yamlObject);
|
||||
return JsonNode.Parse(jsonString)!;
|
||||
return JsonDocument.Parse(jsonString).RootElement;
|
||||
}
|
||||
|
||||
private static JsonElement ParseJson(string json)
|
||||
{
|
||||
return JsonDocument.Parse(json).RootElement;
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -96,7 +101,7 @@ public class PolicyPackSchemaTests
|
||||
[Fact]
|
||||
public void Schema_RequiresApiVersion()
|
||||
{
|
||||
var invalidPolicy = JsonNode.Parse("""
|
||||
var invalidPolicy = ParseJson("""
|
||||
{
|
||||
"kind": "PolicyPack",
|
||||
"metadata": { "name": "test-policy", "version": "1.0.0" },
|
||||
@@ -112,7 +117,7 @@ public class PolicyPackSchemaTests
|
||||
[Fact]
|
||||
public void Schema_RequiresKind()
|
||||
{
|
||||
var invalidPolicy = JsonNode.Parse("""
|
||||
var invalidPolicy = ParseJson("""
|
||||
{
|
||||
"apiVersion": "policy.stellaops.io/v1",
|
||||
"metadata": { "name": "test-policy", "version": "1.0.0" },
|
||||
@@ -128,7 +133,7 @@ public class PolicyPackSchemaTests
|
||||
[Fact]
|
||||
public void Schema_RequiresMetadata()
|
||||
{
|
||||
var invalidPolicy = JsonNode.Parse("""
|
||||
var invalidPolicy = ParseJson("""
|
||||
{
|
||||
"apiVersion": "policy.stellaops.io/v1",
|
||||
"kind": "PolicyPack",
|
||||
@@ -144,7 +149,7 @@ public class PolicyPackSchemaTests
|
||||
[Fact]
|
||||
public void Schema_RequiresSpec()
|
||||
{
|
||||
var invalidPolicy = JsonNode.Parse("""
|
||||
var invalidPolicy = ParseJson("""
|
||||
{
|
||||
"apiVersion": "policy.stellaops.io/v1",
|
||||
"kind": "PolicyPack",
|
||||
@@ -160,7 +165,7 @@ public class PolicyPackSchemaTests
|
||||
[Fact]
|
||||
public void Schema_ValidatesApiVersionFormat()
|
||||
{
|
||||
var invalidPolicy = JsonNode.Parse("""
|
||||
var invalidPolicy = ParseJson("""
|
||||
{
|
||||
"apiVersion": "invalid-version",
|
||||
"kind": "PolicyPack",
|
||||
@@ -177,7 +182,7 @@ public class PolicyPackSchemaTests
|
||||
[Fact]
|
||||
public void Schema_ValidatesKindEnum()
|
||||
{
|
||||
var invalidPolicy = JsonNode.Parse("""
|
||||
var invalidPolicy = ParseJson("""
|
||||
{
|
||||
"apiVersion": "policy.stellaops.io/v1",
|
||||
"kind": "InvalidKind",
|
||||
@@ -194,7 +199,7 @@ public class PolicyPackSchemaTests
|
||||
[Fact]
|
||||
public void Schema_AcceptsValidPolicyPack()
|
||||
{
|
||||
var validPolicy = JsonNode.Parse("""
|
||||
var validPolicy = ParseJson("""
|
||||
{
|
||||
"apiVersion": "policy.stellaops.io/v1",
|
||||
"kind": "PolicyPack",
|
||||
@@ -228,7 +233,7 @@ public class PolicyPackSchemaTests
|
||||
[Fact]
|
||||
public void Schema_AcceptsValidPolicyOverride()
|
||||
{
|
||||
var validOverride = JsonNode.Parse("""
|
||||
var validOverride = ParseJson("""
|
||||
{
|
||||
"apiVersion": "policy.stellaops.io/v1",
|
||||
"kind": "PolicyOverride",
|
||||
@@ -264,7 +269,7 @@ public class PolicyPackSchemaTests
|
||||
[InlineData("block")]
|
||||
public void Schema_AcceptsValidRuleActions(string action)
|
||||
{
|
||||
var policy = JsonNode.Parse($$"""
|
||||
var policy = ParseJson($$"""
|
||||
{
|
||||
"apiVersion": "policy.stellaops.io/v1",
|
||||
"kind": "PolicyPack",
|
||||
@@ -295,7 +300,7 @@ public class PolicyPackSchemaTests
|
||||
|
||||
private static void CollectErrors(EvaluationResults result, List<string> errors)
|
||||
{
|
||||
if (result.Errors != null && result.Errors.Count > 0)
|
||||
if (result.Errors is { Count: > 0 })
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
@@ -303,12 +308,12 @@ public class PolicyPackSchemaTests
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.IsValid && result.HasErrors && errors.Count == 0)
|
||||
if (!result.IsValid && result.Errors is { Count: > 0 } && errors.Count == 0)
|
||||
{
|
||||
errors.Add($"At {result.InstanceLocation}: validation failed with no specific error message");
|
||||
}
|
||||
|
||||
if (result.HasDetails)
|
||||
if (result.Details is { Count: > 0 })
|
||||
{
|
||||
foreach (var detail in result.Details)
|
||||
{
|
||||
|
||||
@@ -9,19 +9,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.2.1" />
|
||||
<PackageReference Include="JsonSchema.Net" Version="7.3.4" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
<PackageReference Include="JsonSchema.Net" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -38,4 +28,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
[Collection(PolicyPostgresCollection.Name)]
|
||||
public sealed class EvaluationRunRepositoryTests : IAsyncLifetime
|
||||
@@ -4,11 +4,12 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
using StellaOps.Policy.Exceptions.Repositories;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for PostgresExceptionObjectRepository.
|
||||
@@ -360,7 +361,7 @@ public sealed class ExceptionObjectRepositoryTests : IAsyncLifetime
|
||||
counts.Proposed.Should().Be(2);
|
||||
counts.Active.Should().Be(2);
|
||||
counts.Revoked.Should().Be(1);
|
||||
counts.ExpiringSoon.Should().BeGreaterOrEqualTo(1); // At least the one expiring in 3 days
|
||||
counts.ExpiringSoon.Should().BeGreaterThanOrEqualTo(1); // At least the one expiring in 3 days
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
[Collection(PolicyPostgresCollection.Name)]
|
||||
public sealed class ExceptionRepositoryTests : IAsyncLifetime
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
[Collection(PolicyPostgresCollection.Name)]
|
||||
public sealed class PackRepositoryTests : IAsyncLifetime
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for pack versioning workflow scenarios (PG-T4.8.2).
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
[Collection(PolicyPostgresCollection.Name)]
|
||||
public sealed class PolicyAuditRepositoryTests : IAsyncLifetime
|
||||
@@ -9,11 +9,12 @@ using System.Reflection;
|
||||
using Dapper;
|
||||
using FluentAssertions;
|
||||
using Npgsql;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.TestKit;
|
||||
using Testcontainers.PostgreSql;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Migration tests for Policy.Storage.
|
||||
@@ -7,14 +7,15 @@
|
||||
|
||||
using System.Reflection;
|
||||
using StellaOps.Infrastructure.Postgres.Testing;
|
||||
using StellaOps.Policy.Storage.Postgres;
|
||||
using StellaOps.Policy.Persistence;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using Xunit;
|
||||
|
||||
// Type aliases to disambiguate TestKit and Infrastructure.Postgres.Testing fixtures
|
||||
using TestKitPostgresFixture = StellaOps.TestKit.Fixtures.PostgresFixture;
|
||||
using TestKitPostgresIsolationMode = StellaOps.TestKit.Fixtures.PostgresIsolationMode;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL integration test fixture for the Policy module.
|
||||
@@ -52,9 +53,10 @@ public sealed class PolicyTestKitPostgresFixture : IAsyncLifetime
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_fixture = new TestKitPostgresFixture(TestKitPostgresIsolationMode.Truncation);
|
||||
_fixture = new TestKitPostgresFixture();
|
||||
_fixture.IsolationMode = TestKitPostgresIsolationMode.Truncation;
|
||||
await _fixture.InitializeAsync();
|
||||
await _fixture.ApplyMigrationsFromAssemblyAsync(MigrationAssembly);
|
||||
await _fixture.ApplyMigrationsFromAssemblyAsync(MigrationAssembly, "public");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => _fixture.DisposeAsync();
|
||||
@@ -8,12 +8,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Query determinism tests for Policy storage operations.
|
||||
@@ -158,9 +159,9 @@ public sealed class PolicyQueryDeterminismTests : IAsyncLifetime
|
||||
};
|
||||
|
||||
// Act - Query multiple times
|
||||
var results1 = await _ruleRepository.GetByVersionIdAsync(version.Id);
|
||||
var results2 = await _ruleRepository.GetByVersionIdAsync(version.Id);
|
||||
var results3 = await _ruleRepository.GetByVersionIdAsync(version.Id);
|
||||
var results1 = await _ruleRepository.GetByPackVersionIdAsync(version.Id);
|
||||
var results2 = await _ruleRepository.GetByPackVersionIdAsync(version.Id);
|
||||
var results3 = await _ruleRepository.GetByPackVersionIdAsync(version.Id);
|
||||
|
||||
// Assert - All queries should return same order
|
||||
var ids1 = results1.Select(r => r.Id).ToList();
|
||||
@@ -181,9 +182,9 @@ public sealed class PolicyQueryDeterminismTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
// Act - Query multiple times
|
||||
var results1 = await _auditRepository.GetRecentAsync(_tenantId, 10);
|
||||
var results2 = await _auditRepository.GetRecentAsync(_tenantId, 10);
|
||||
var results3 = await _auditRepository.GetRecentAsync(_tenantId, 10);
|
||||
var results1 = await _auditRepository.ListAsync(_tenantId, 10);
|
||||
var results2 = await _auditRepository.ListAsync(_tenantId, 10);
|
||||
var results3 = await _auditRepository.ListAsync(_tenantId, 10);
|
||||
|
||||
// Assert - All queries should return same order
|
||||
var ids1 = results1.Select(a => a.Id).ToList();
|
||||
@@ -369,8 +370,10 @@ public sealed class PolicyQueryDeterminismTests : IAsyncLifetime
|
||||
Name = name,
|
||||
DisplayName = $"Display {name}",
|
||||
Version = 1,
|
||||
PolicyContent = """{"rules": []}""",
|
||||
ContentHash = $"hash-{Guid.NewGuid():N}"
|
||||
Thresholds = """{"critical": 9.0, "high": 7.0}""",
|
||||
ScoringWeights = """{"cvss": 1.0}""",
|
||||
Exemptions = """[]""",
|
||||
Metadata = """{}"""
|
||||
};
|
||||
await _riskProfileRepository.CreateAsync(profile);
|
||||
return profile;
|
||||
@@ -383,29 +386,27 @@ public sealed class PolicyQueryDeterminismTests : IAsyncLifetime
|
||||
Id = Guid.NewGuid(),
|
||||
PackVersionId = versionId,
|
||||
Name = name,
|
||||
DisplayName = $"Display {name}",
|
||||
Severity = "HIGH",
|
||||
RuleContent = """{"condition": "always"}""",
|
||||
Description = $"Description for {name}",
|
||||
Severity = RuleSeverity.High,
|
||||
Content = """{"condition": "always"}""",
|
||||
ContentHash = $"hash-{Guid.NewGuid():N}"
|
||||
};
|
||||
await _ruleRepository.CreateAsync(rule);
|
||||
return rule;
|
||||
}
|
||||
|
||||
private async Task<PolicyAuditEntity> CreateAuditEntryAsync(string action)
|
||||
private async Task<long> CreateAuditEntryAsync(string action)
|
||||
{
|
||||
var audit = new PolicyAuditEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Action = action,
|
||||
Actor = "test-user",
|
||||
EntityType = "Pack",
|
||||
EntityId = Guid.NewGuid().ToString(),
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Details = """{"test": true}"""
|
||||
UserId = Guid.NewGuid(),
|
||||
ResourceType = "Pack",
|
||||
ResourceId = Guid.NewGuid().ToString(),
|
||||
OldValue = null,
|
||||
NewValue = """{"test": true}"""
|
||||
};
|
||||
await _auditRepository.CreateAsync(audit);
|
||||
return audit;
|
||||
return await _auditRepository.CreateAsync(audit);
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Immutability tests for Policy versioning storage operations.
|
||||
@@ -112,19 +113,24 @@ public sealed class PolicyVersioningImmutabilityTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnpublishedVersion_CanBeModified()
|
||||
public async Task UnpublishedVersion_CanBePublished()
|
||||
{
|
||||
// Arrange
|
||||
var pack = await CreatePackAsync("mutable-unpublished-test");
|
||||
var version = await CreatePackVersionAsync(pack.Id, 1, rulesHash: "initial-hash", publish: false);
|
||||
|
||||
// Act - Unpublished versions can be updated
|
||||
version.Description = "Updated description";
|
||||
var updated = await _packVersionRepository.UpdateAsync(version);
|
||||
version.IsPublished.Should().BeFalse();
|
||||
version.PublishedAt.Should().BeNull();
|
||||
|
||||
// Act - Unpublished versions can be published
|
||||
var publishResult = await _packVersionRepository.PublishAsync(version.Id, "test-publisher");
|
||||
|
||||
// Assert
|
||||
publishResult.Should().BeTrue();
|
||||
var queried = await _packVersionRepository.GetByIdAsync(version.Id);
|
||||
queried!.Description.Should().Be("Updated description");
|
||||
queried!.IsPublished.Should().BeTrue();
|
||||
queried.PublishedAt.Should().NotBeNull();
|
||||
queried.PublishedBy.Should().Be("test-publisher");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -12,7 +12,7 @@ using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for PostgresExceptionApplicationRepository.
|
||||
@@ -42,7 +42,6 @@ public sealed class PostgresExceptionApplicationRepositoryTests : IAsyncLifetime
|
||||
|
||||
// Set search path to include the test schema
|
||||
await using var conn = await _dataSource.OpenConnectionAsync();
|
||||
using StellaOps.TestKit;
|
||||
await using var cmd = new NpgsqlCommand($"SET search_path TO {_fixture.SchemaName}, public;", conn);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
@@ -175,4 +174,4 @@ using StellaOps.TestKit;
|
||||
string? vulnId = null,
|
||||
string eff = "suppress") =>
|
||||
ExceptionApplication.Create(_tenantId, excId, findId, "affected", "not_affected", "test", eff, vulnId);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,12 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
using StellaOps.Policy.Exceptions.Repositories;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for PostgresExceptionObjectRepository.
|
||||
@@ -4,13 +4,14 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Scoring;
|
||||
using StellaOps.Policy.Scoring.Receipts;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Storage.Postgres.Tests;
|
||||
using StellaOps.Policy.Storage.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Tests;
|
||||
using StellaOps.Policy.Persistence;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
[Collection(PolicyPostgresCollection.Name)]
|
||||
public sealed class PostgresReceiptRepositoryTests : IAsyncLifetime
|
||||
@@ -2,12 +2,13 @@ using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using StellaOps.Policy.Storage.Postgres;
|
||||
using StellaOps.Policy.Persistence;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
[Collection(PolicyPostgresCollection.Name)]
|
||||
public sealed class RecheckEvidenceMigrationTests : IAsyncLifetime
|
||||
@@ -42,7 +43,6 @@ public sealed class RecheckEvidenceMigrationTests : IAsyncLifetime
|
||||
private static async Task AssertTableExistsAsync(NpgsqlConnection connection, string tableName)
|
||||
{
|
||||
await using var command = new NpgsqlCommand("SELECT to_regclass(@name)", connection);
|
||||
using StellaOps.TestKit;
|
||||
command.Parameters.AddWithValue("name", tableName);
|
||||
var result = await command.ExecuteScalarAsync();
|
||||
result.Should().NotBeNull($"{tableName} should exist after migrations");
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
[Collection(PolicyPostgresCollection.Name)]
|
||||
public sealed class RiskProfileRepositoryTests : IAsyncLifetime
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for risk profile version history scenarios (PG-T4.8.3).
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
[Collection(PolicyPostgresCollection.Name)]
|
||||
public sealed class RuleRepositoryTests : IAsyncLifetime
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -11,28 +11,18 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="4.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PackageReference Include="Dapper" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Policy.Storage.Postgres\StellaOps.Policy.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Policy.Persistence\StellaOps.Policy.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Policy.Exceptions\StellaOps.Policy.Exceptions.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Policy.Unknowns\StellaOps.Policy.Unknowns.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Policy.Scoring\StellaOps.Policy.Scoring.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,14 +1,15 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Storage.Postgres;
|
||||
using StellaOps.Policy.Persistence;
|
||||
using StellaOps.Policy.Persistence.Postgres;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
using StellaOps.Policy.Unknowns.Repositories;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Storage.Postgres.Tests;
|
||||
namespace StellaOps.Policy.Persistence.Tests;
|
||||
|
||||
[Collection(PolicyPostgresCollection.Name)]
|
||||
public sealed class UnknownsRepositoryTests : IAsyncLifetime
|
||||
@@ -64,7 +65,6 @@ public sealed class UnknownsRepositoryTests : IAsyncLifetime
|
||||
public async Task UpdateAsync_PersistsReasonCodeAndAssumptions()
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId.ToString());
|
||||
using StellaOps.TestKit;
|
||||
var repository = new UnknownsRepository(connection);
|
||||
var now = new DateTimeOffset(2025, 2, 3, 4, 5, 6, TimeSpan.Zero);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Policy.RiskProfile.Canonicalization;
|
||||
using Xunit;
|
||||
@@ -73,7 +73,6 @@ public class RiskProfileCanonicalizerTests
|
||||
|
||||
var merged = RiskProfileCanonicalizer.Merge(baseProfile, overlay);
|
||||
using var doc = JsonDocument.Parse(merged);
|
||||
using StellaOps.TestKit;
|
||||
var root = doc.RootElement;
|
||||
|
||||
Assert.Equal(2, root.GetProperty("signals").GetArrayLength());
|
||||
|
||||
@@ -42,7 +42,7 @@ public class RiskProfileValidatorTests
|
||||
|
||||
var result = _validator.Validate(profile);
|
||||
|
||||
Assert.True(result.IsValid, string.Join(" | ", result.Errors ?? Array.Empty<string>()));
|
||||
Assert.True(result.IsValid, result.Errors != null ? string.Join(" | ", result.Errors.Values) : string.Empty);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -56,7 +56,7 @@ public class RiskProfileValidatorTests
|
||||
var result = _validator.Validate(invalidProfile);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.NotEmpty(result.Errors);
|
||||
Assert.NotEmpty(result.Errors!);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
|
||||
@@ -11,9 +11,4 @@
|
||||
<ProjectReference Include="../../StellaOps.Policy.RiskProfile/StellaOps.Policy.RiskProfile.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring.Policies;
|
||||
@@ -80,7 +80,6 @@ public sealed class CvssPolicyLoaderTests
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
using var finalDoc = JsonDocument.Parse(stream);
|
||||
using StellaOps.TestKit;
|
||||
return finalDoc.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Tests;
|
||||
@@ -461,7 +462,6 @@ public sealed class CvssV4EngineTests
|
||||
// Arrange
|
||||
var metricSet = _engine.ParseVector(vector);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act
|
||||
var scores = _engine.ComputeScores(metricSet.BaseMetrics);
|
||||
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for CVSS v4.0 Environmental Metrics parsing and scoring.
|
||||
/// Tests cover Modified Attack Metrics (MAV, MAC, MAT, MPR, MUI) and
|
||||
/// Modified Impact Metrics (MVC, MVI, MVA, MSC, MSI, MSA).
|
||||
/// </summary>
|
||||
public sealed class CvssV4EnvironmentalTests
|
||||
{
|
||||
private readonly ICvssV4Engine _engine = new CvssV4Engine();
|
||||
|
||||
#region Modified Attack Vector Parsing Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedAttackVector.Network)]
|
||||
[InlineData("A", ModifiedAttackVector.Adjacent)]
|
||||
[InlineData("L", ModifiedAttackVector.Local)]
|
||||
[InlineData("P", ModifiedAttackVector.Physical)]
|
||||
[InlineData("X", ModifiedAttackVector.NotDefined)]
|
||||
public void ParseVector_WithMAV_ParsesCorrectly(string mavValue, ModifiedAttackVector expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MAV:{mavValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedAttackVector.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("L", ModifiedAttackComplexity.Low)]
|
||||
[InlineData("H", ModifiedAttackComplexity.High)]
|
||||
[InlineData("X", ModifiedAttackComplexity.NotDefined)]
|
||||
public void ParseVector_WithMAC_ParsesCorrectly(string macValue, ModifiedAttackComplexity expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MAC:{macValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedAttackComplexity.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedAttackRequirements.None)]
|
||||
[InlineData("P", ModifiedAttackRequirements.Present)]
|
||||
[InlineData("X", ModifiedAttackRequirements.NotDefined)]
|
||||
public void ParseVector_WithMAT_ParsesCorrectly(string matValue, ModifiedAttackRequirements expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MAT:{matValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedAttackRequirements.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedPrivilegesRequired.None)]
|
||||
[InlineData("L", ModifiedPrivilegesRequired.Low)]
|
||||
[InlineData("H", ModifiedPrivilegesRequired.High)]
|
||||
[InlineData("X", ModifiedPrivilegesRequired.NotDefined)]
|
||||
public void ParseVector_WithMPR_ParsesCorrectly(string mprValue, ModifiedPrivilegesRequired expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MPR:{mprValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedPrivilegesRequired.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedUserInteraction.None)]
|
||||
[InlineData("P", ModifiedUserInteraction.Passive)]
|
||||
[InlineData("A", ModifiedUserInteraction.Active)]
|
||||
[InlineData("X", ModifiedUserInteraction.NotDefined)]
|
||||
public void ParseVector_WithMUI_ParsesCorrectly(string muiValue, ModifiedUserInteraction expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MUI:{muiValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedUserInteraction.Should().Be(expected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Modified Impact Metrics Parsing Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedImpactMetricValue.None)]
|
||||
[InlineData("L", ModifiedImpactMetricValue.Low)]
|
||||
[InlineData("H", ModifiedImpactMetricValue.High)]
|
||||
[InlineData("X", ModifiedImpactMetricValue.NotDefined)]
|
||||
public void ParseVector_WithMVC_ParsesCorrectly(string mvcValue, ModifiedImpactMetricValue expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MVC:{mvcValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedVulnerableSystemConfidentiality.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedImpactMetricValue.None)]
|
||||
[InlineData("L", ModifiedImpactMetricValue.Low)]
|
||||
[InlineData("H", ModifiedImpactMetricValue.High)]
|
||||
public void ParseVector_WithMVI_ParsesCorrectly(string mviValue, ModifiedImpactMetricValue expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MVI:{mviValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedVulnerableSystemIntegrity.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedImpactMetricValue.None)]
|
||||
[InlineData("L", ModifiedImpactMetricValue.Low)]
|
||||
[InlineData("H", ModifiedImpactMetricValue.High)]
|
||||
public void ParseVector_WithMVA_ParsesCorrectly(string mvaValue, ModifiedImpactMetricValue expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MVA:{mvaValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedVulnerableSystemAvailability.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedImpactMetricValue.None)]
|
||||
[InlineData("L", ModifiedImpactMetricValue.Low)]
|
||||
[InlineData("H", ModifiedImpactMetricValue.High)]
|
||||
public void ParseVector_WithMSC_ParsesCorrectly(string mscValue, ModifiedImpactMetricValue expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MSC:{mscValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedSubsequentSystemConfidentiality.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedSubsequentImpact.Negligible)]
|
||||
[InlineData("L", ModifiedSubsequentImpact.Low)]
|
||||
[InlineData("H", ModifiedSubsequentImpact.High)]
|
||||
[InlineData("S", ModifiedSubsequentImpact.Safety)]
|
||||
[InlineData("X", ModifiedSubsequentImpact.NotDefined)]
|
||||
public void ParseVector_WithMSI_ParsesCorrectly(string msiValue, ModifiedSubsequentImpact expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MSI:{msiValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedSubsequentSystemIntegrity.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("N", ModifiedSubsequentImpact.Negligible)]
|
||||
[InlineData("L", ModifiedSubsequentImpact.Low)]
|
||||
[InlineData("H", ModifiedSubsequentImpact.High)]
|
||||
[InlineData("S", ModifiedSubsequentImpact.Safety)]
|
||||
public void ParseVector_WithMSA_ParsesCorrectly(string msaValue, ModifiedSubsequentImpact expected)
|
||||
{
|
||||
// Arrange
|
||||
var vector = $"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/MSA:{msaValue}";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedSubsequentSystemAvailability.Should().Be(expected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Vector String Building Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildVectorString_WithAllModifiedMetrics_IncludesAllMetrics()
|
||||
{
|
||||
// Arrange
|
||||
var baseMetrics = CreateHighSeverityBaseMetrics();
|
||||
var envMetrics = new CvssEnvironmentalMetrics
|
||||
{
|
||||
ModifiedAttackVector = ModifiedAttackVector.Local,
|
||||
ModifiedAttackComplexity = ModifiedAttackComplexity.High,
|
||||
ModifiedAttackRequirements = ModifiedAttackRequirements.Present,
|
||||
ModifiedPrivilegesRequired = ModifiedPrivilegesRequired.High,
|
||||
ModifiedUserInteraction = ModifiedUserInteraction.Active,
|
||||
ModifiedVulnerableSystemConfidentiality = ModifiedImpactMetricValue.Low,
|
||||
ModifiedVulnerableSystemIntegrity = ModifiedImpactMetricValue.Low,
|
||||
ModifiedVulnerableSystemAvailability = ModifiedImpactMetricValue.None,
|
||||
ModifiedSubsequentSystemConfidentiality = ModifiedImpactMetricValue.None,
|
||||
ModifiedSubsequentSystemIntegrity = ModifiedSubsequentImpact.Safety,
|
||||
ModifiedSubsequentSystemAvailability = ModifiedSubsequentImpact.High,
|
||||
ConfidentialityRequirement = SecurityRequirement.High,
|
||||
IntegrityRequirement = SecurityRequirement.Medium,
|
||||
AvailabilityRequirement = SecurityRequirement.Low
|
||||
};
|
||||
|
||||
// Act
|
||||
var vector = _engine.BuildVectorString(baseMetrics, environmentalMetrics: envMetrics);
|
||||
|
||||
// Assert
|
||||
vector.Should().Contain("/MAV:L");
|
||||
vector.Should().Contain("/MAC:H");
|
||||
vector.Should().Contain("/MAT:P");
|
||||
vector.Should().Contain("/MPR:H");
|
||||
vector.Should().Contain("/MUI:A");
|
||||
vector.Should().Contain("/MVC:L");
|
||||
vector.Should().Contain("/MVI:L");
|
||||
vector.Should().Contain("/MVA:N");
|
||||
vector.Should().Contain("/MSC:N");
|
||||
vector.Should().Contain("/MSI:S");
|
||||
vector.Should().Contain("/MSA:H");
|
||||
vector.Should().Contain("/CR:H");
|
||||
vector.Should().Contain("/IR:M");
|
||||
vector.Should().Contain("/AR:L");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildVectorString_WithNotDefinedMetrics_ExcludesNotDefinedMetrics()
|
||||
{
|
||||
// Arrange
|
||||
var baseMetrics = CreateHighSeverityBaseMetrics();
|
||||
var envMetrics = new CvssEnvironmentalMetrics
|
||||
{
|
||||
ModifiedAttackVector = ModifiedAttackVector.NotDefined,
|
||||
ModifiedAttackComplexity = ModifiedAttackComplexity.High, // Only this should appear
|
||||
ConfidentialityRequirement = SecurityRequirement.NotDefined
|
||||
};
|
||||
|
||||
// Act
|
||||
var vector = _engine.BuildVectorString(baseMetrics, environmentalMetrics: envMetrics);
|
||||
|
||||
// Assert
|
||||
vector.Should().Contain("/MAC:H");
|
||||
vector.Should().NotContain("/MAV:");
|
||||
vector.Should().NotContain("/CR:");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Roundtrip Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Roundtrip_WithAllModifiedMetrics_PreservesValues()
|
||||
{
|
||||
// Arrange
|
||||
var baseMetrics = CreateHighSeverityBaseMetrics();
|
||||
var envMetrics = new CvssEnvironmentalMetrics
|
||||
{
|
||||
ModifiedAttackVector = ModifiedAttackVector.Adjacent,
|
||||
ModifiedAttackComplexity = ModifiedAttackComplexity.High,
|
||||
ModifiedAttackRequirements = ModifiedAttackRequirements.Present,
|
||||
ModifiedPrivilegesRequired = ModifiedPrivilegesRequired.Low,
|
||||
ModifiedUserInteraction = ModifiedUserInteraction.Passive,
|
||||
ModifiedVulnerableSystemConfidentiality = ModifiedImpactMetricValue.Low,
|
||||
ModifiedVulnerableSystemIntegrity = ModifiedImpactMetricValue.High,
|
||||
ModifiedVulnerableSystemAvailability = ModifiedImpactMetricValue.None,
|
||||
ConfidentialityRequirement = SecurityRequirement.High,
|
||||
IntegrityRequirement = SecurityRequirement.Low
|
||||
};
|
||||
|
||||
// Act
|
||||
var vector = _engine.BuildVectorString(baseMetrics, environmentalMetrics: envMetrics);
|
||||
var parsed = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
parsed.EnvironmentalMetrics.Should().NotBeNull();
|
||||
var parsedEnv = parsed.EnvironmentalMetrics!;
|
||||
parsedEnv.ModifiedAttackVector.Should().Be(ModifiedAttackVector.Adjacent);
|
||||
parsedEnv.ModifiedAttackComplexity.Should().Be(ModifiedAttackComplexity.High);
|
||||
parsedEnv.ModifiedAttackRequirements.Should().Be(ModifiedAttackRequirements.Present);
|
||||
parsedEnv.ModifiedPrivilegesRequired.Should().Be(ModifiedPrivilegesRequired.Low);
|
||||
parsedEnv.ModifiedUserInteraction.Should().Be(ModifiedUserInteraction.Passive);
|
||||
parsedEnv.ModifiedVulnerableSystemConfidentiality.Should().Be(ModifiedImpactMetricValue.Low);
|
||||
parsedEnv.ModifiedVulnerableSystemIntegrity.Should().Be(ModifiedImpactMetricValue.High);
|
||||
parsedEnv.ModifiedVulnerableSystemAvailability.Should().Be(ModifiedImpactMetricValue.None);
|
||||
parsedEnv.ConfidentialityRequirement.Should().Be(SecurityRequirement.High);
|
||||
parsedEnv.IntegrityRequirement.Should().Be(SecurityRequirement.Low);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Environmental Score Computation Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeScores_WithMAVLocal_ReducesScoreFromNetwork()
|
||||
{
|
||||
// Arrange - Network attack vector base, modified to Local
|
||||
var baseMetrics = CreateHighSeverityBaseMetrics();
|
||||
var envMetrics = new CvssEnvironmentalMetrics
|
||||
{
|
||||
ModifiedAttackVector = ModifiedAttackVector.Local
|
||||
};
|
||||
|
||||
// Act
|
||||
var baseScores = _engine.ComputeScores(baseMetrics);
|
||||
var envScores = _engine.ComputeScores(baseMetrics, environmentalMetrics: envMetrics);
|
||||
|
||||
// Assert - Local should have lower score than Network
|
||||
envScores.EnvironmentalScore.Should().NotBeNull();
|
||||
envScores.EnvironmentalScore!.Value.Should().BeLessThan(baseScores.BaseScore);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeScores_WithMACHigh_ReducesScore()
|
||||
{
|
||||
// Arrange - Low complexity base, modified to High
|
||||
var baseMetrics = CreateHighSeverityBaseMetrics(); // AC:L
|
||||
var envMetrics = new CvssEnvironmentalMetrics
|
||||
{
|
||||
ModifiedAttackComplexity = ModifiedAttackComplexity.High
|
||||
};
|
||||
|
||||
// Act
|
||||
var baseScores = _engine.ComputeScores(baseMetrics);
|
||||
var envScores = _engine.ComputeScores(baseMetrics, environmentalMetrics: envMetrics);
|
||||
|
||||
// Assert
|
||||
envScores.EnvironmentalScore.Should().NotBeNull();
|
||||
envScores.EnvironmentalScore!.Value.Should().BeLessThan(baseScores.BaseScore);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeScores_WithMVCNone_ReducesImpact()
|
||||
{
|
||||
// Arrange - High confidentiality impact base, modified to None
|
||||
var baseMetrics = CreateHighSeverityBaseMetrics(); // VC:H
|
||||
var envMetrics = new CvssEnvironmentalMetrics
|
||||
{
|
||||
ModifiedVulnerableSystemConfidentiality = ModifiedImpactMetricValue.None,
|
||||
ModifiedVulnerableSystemIntegrity = ModifiedImpactMetricValue.None,
|
||||
ModifiedVulnerableSystemAvailability = ModifiedImpactMetricValue.None
|
||||
};
|
||||
|
||||
// Act
|
||||
var baseScores = _engine.ComputeScores(baseMetrics);
|
||||
var envScores = _engine.ComputeScores(baseMetrics, environmentalMetrics: envMetrics);
|
||||
|
||||
// Assert - Removing all vulnerable system impact should significantly reduce score
|
||||
envScores.EnvironmentalScore.Should().NotBeNull();
|
||||
envScores.EnvironmentalScore!.Value.Should().BeLessThan(baseScores.BaseScore);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeScores_WithMSISafety_AffectsScore()
|
||||
{
|
||||
// Arrange - Modify subsequent system integrity to Safety (highest impact)
|
||||
var baseMetrics = CreateMediumSeverityBaseMetrics();
|
||||
var envMetrics = new CvssEnvironmentalMetrics
|
||||
{
|
||||
ModifiedSubsequentSystemIntegrity = ModifiedSubsequentImpact.Safety
|
||||
};
|
||||
|
||||
// Act
|
||||
var envScores = _engine.ComputeScores(baseMetrics, environmentalMetrics: envMetrics);
|
||||
|
||||
// Assert
|
||||
envScores.EnvironmentalScore.Should().NotBeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FIRST Sample Vector Tests
|
||||
|
||||
/// <summary>
|
||||
/// Test vectors from FIRST CVSS v4.0 specification examples.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[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/MAV:L/MAC:H",
|
||||
4.0, 6.0)] // Modified to local + high complexity significantly reduces score
|
||||
[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/CR:H/IR:H/AR:H",
|
||||
9.0, 10.0)] // High security requirements should increase score
|
||||
public void ComputeScores_FirstEnvironmentalVectors_ReturnsExpectedScoreRange(
|
||||
string vector, double minExpected, double maxExpected)
|
||||
{
|
||||
// Arrange
|
||||
var metricSet = _engine.ParseVector(vector);
|
||||
|
||||
// Act
|
||||
var scores = _engine.ComputeScores(
|
||||
metricSet.BaseMetrics,
|
||||
metricSet.ThreatMetrics,
|
||||
metricSet.EnvironmentalMetrics);
|
||||
|
||||
// Assert
|
||||
scores.EnvironmentalScore.Should().NotBeNull();
|
||||
scores.EnvironmentalScore!.Value.Should().BeInRange(minExpected, maxExpected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Full Score Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeScores_WithThreatAndEnvironmental_ReturnsFullScore()
|
||||
{
|
||||
// Arrange
|
||||
var baseMetrics = CreateHighSeverityBaseMetrics();
|
||||
var threatMetrics = new CvssThreatMetrics { ExploitMaturity = ExploitMaturity.Attacked };
|
||||
var envMetrics = new CvssEnvironmentalMetrics
|
||||
{
|
||||
ModifiedAttackVector = ModifiedAttackVector.Local,
|
||||
ConfidentialityRequirement = SecurityRequirement.High
|
||||
};
|
||||
|
||||
// Act
|
||||
var scores = _engine.ComputeScores(baseMetrics, threatMetrics, envMetrics);
|
||||
|
||||
// Assert
|
||||
scores.FullScore.Should().NotBeNull();
|
||||
scores.EffectiveScoreType.Should().Be(EffectiveScoreType.Full);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Case Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseVector_WithAllXValues_ReturnsNotDefinedForAll()
|
||||
{
|
||||
// Arrange - All environmental metrics set to X (Not Defined)
|
||||
var vector = "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" +
|
||||
"/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X" +
|
||||
"/CR:X/IR:X/AR:X";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
var env = result.EnvironmentalMetrics!;
|
||||
env.ModifiedAttackVector.Should().Be(ModifiedAttackVector.NotDefined);
|
||||
env.ModifiedAttackComplexity.Should().Be(ModifiedAttackComplexity.NotDefined);
|
||||
env.ModifiedAttackRequirements.Should().Be(ModifiedAttackRequirements.NotDefined);
|
||||
env.ModifiedPrivilegesRequired.Should().Be(ModifiedPrivilegesRequired.NotDefined);
|
||||
env.ModifiedUserInteraction.Should().Be(ModifiedUserInteraction.NotDefined);
|
||||
env.ModifiedVulnerableSystemConfidentiality.Should().Be(ModifiedImpactMetricValue.NotDefined);
|
||||
env.ModifiedVulnerableSystemIntegrity.Should().Be(ModifiedImpactMetricValue.NotDefined);
|
||||
env.ModifiedVulnerableSystemAvailability.Should().Be(ModifiedImpactMetricValue.NotDefined);
|
||||
env.ModifiedSubsequentSystemConfidentiality.Should().Be(ModifiedImpactMetricValue.NotDefined);
|
||||
env.ModifiedSubsequentSystemIntegrity.Should().Be(ModifiedSubsequentImpact.NotDefined);
|
||||
env.ModifiedSubsequentSystemAvailability.Should().Be(ModifiedSubsequentImpact.NotDefined);
|
||||
env.ConfidentialityRequirement.Should().Be(SecurityRequirement.NotDefined);
|
||||
env.IntegrityRequirement.Should().Be(SecurityRequirement.NotDefined);
|
||||
env.AvailabilityRequirement.Should().Be(SecurityRequirement.NotDefined);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeScores_WithOnlyNotDefinedEnvMetrics_ReturnsNullEnvironmentalScore()
|
||||
{
|
||||
// Arrange - All environmental metrics are X (should not trigger environmental scoring)
|
||||
var baseMetrics = CreateHighSeverityBaseMetrics();
|
||||
var envMetrics = new CvssEnvironmentalMetrics
|
||||
{
|
||||
ModifiedAttackVector = ModifiedAttackVector.NotDefined,
|
||||
ModifiedAttackComplexity = ModifiedAttackComplexity.NotDefined,
|
||||
ConfidentialityRequirement = SecurityRequirement.NotDefined
|
||||
};
|
||||
|
||||
// Act
|
||||
var scores = _engine.ComputeScores(baseMetrics, environmentalMetrics: envMetrics);
|
||||
|
||||
// Assert
|
||||
scores.EnvironmentalScore.Should().BeNull();
|
||||
scores.EffectiveScoreType.Should().Be(EffectiveScoreType.Base);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseVector_CaseInsensitive_ParsesCorrectly()
|
||||
{
|
||||
// Arrange - Mixed case values
|
||||
var vector = "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/mav:l/mac:h";
|
||||
|
||||
// Act
|
||||
var result = _engine.ParseVector(vector);
|
||||
|
||||
// Assert
|
||||
result.EnvironmentalMetrics.Should().NotBeNull();
|
||||
result.EnvironmentalMetrics!.ModifiedAttackVector.Should().Be(ModifiedAttackVector.Local);
|
||||
result.EnvironmentalMetrics!.ModifiedAttackComplexity.Should().Be(ModifiedAttackComplexity.High);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static CvssBaseMetrics CreateHighSeverityBaseMetrics() => 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.None,
|
||||
SubsequentSystemIntegrity = ImpactMetricValue.None,
|
||||
SubsequentSystemAvailability = ImpactMetricValue.None
|
||||
};
|
||||
|
||||
private static CvssBaseMetrics CreateMediumSeverityBaseMetrics() => new()
|
||||
{
|
||||
AttackVector = AttackVector.Network,
|
||||
AttackComplexity = AttackComplexity.Low,
|
||||
AttackRequirements = AttackRequirements.None,
|
||||
PrivilegesRequired = PrivilegesRequired.Low,
|
||||
UserInteraction = UserInteraction.None,
|
||||
VulnerableSystemConfidentiality = ImpactMetricValue.Low,
|
||||
VulnerableSystemIntegrity = ImpactMetricValue.Low,
|
||||
VulnerableSystemAvailability = ImpactMetricValue.Low,
|
||||
SubsequentSystemConfidentiality = ImpactMetricValue.None,
|
||||
SubsequentSystemIntegrity = ImpactMetricValue.None,
|
||||
SubsequentSystemAvailability = ImpactMetricValue.None
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,6 @@ using System.Diagnostics;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Scoring.Tests;
|
||||
|
||||
@@ -9,22 +9,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<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" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Policy.Scoring/StellaOps.Policy.Scoring.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -141,7 +141,6 @@ public sealed class PolicyBinderTests
|
||||
{
|
||||
using var output = new StringWriter();
|
||||
using var error = new StringWriter();
|
||||
using StellaOps.TestKit;
|
||||
var cli = new PolicyValidationCli(output, error);
|
||||
var options = new PolicyValidationCliOptions
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Policy.Tests;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
|
||||
@@ -60,7 +60,6 @@ public sealed class PolicyScoringConfigTests
|
||||
using var stream = assembly.GetManifestResourceStream("StellaOps.Policy.Schemas.policy-scoring-default.json")
|
||||
?? throw new InvalidOperationException("Unable to locate embedded scoring default resource.");
|
||||
using var reader = new StreamReader(stream);
|
||||
using StellaOps.TestKit;
|
||||
var json = reader.ReadToEnd();
|
||||
|
||||
var binding = PolicyScoringConfigBinder.Bind(json, PolicyDocumentFormat.Json);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Policy;
|
||||
@@ -44,7 +44,6 @@ public class PolicyValidationCliTests
|
||||
|
||||
using var output = new StringWriter();
|
||||
using var error = new StringWriter();
|
||||
using StellaOps.TestKit;
|
||||
var cli = new PolicyValidationCli(output, error);
|
||||
|
||||
var exit = await cli.RunAsync(options);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Policy;
|
||||
using Xunit;
|
||||
|
||||
@@ -61,7 +61,6 @@ public class SplLayeringEngineTests
|
||||
var merged = SplLayeringEngine.Merge(baseDoc, overlay);
|
||||
|
||||
using var doc = JsonDocument.Parse(merged);
|
||||
using StellaOps.TestKit;
|
||||
var root = doc.RootElement;
|
||||
|
||||
Assert.True(root.TryGetProperty("extras", out var extras) && extras.TryGetProperty("foo", out var foo) && foo.GetInt32() == 1);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Policy;
|
||||
using Xunit;
|
||||
|
||||
@@ -14,7 +14,6 @@ public class SplSchemaResourceTests
|
||||
{
|
||||
var schema = SplSchemaResource.GetSchema();
|
||||
using var doc = JsonDocument.Parse(schema);
|
||||
using StellaOps.TestKit;
|
||||
var match = doc.RootElement
|
||||
.GetProperty("properties")
|
||||
.GetProperty("spec")
|
||||
|
||||
@@ -10,22 +10,15 @@
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
||||
<PackageReference Include="FsCheck" Version="2.16.6" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -7,6 +7,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
|
||||
@@ -11,17 +11,11 @@
|
||||
<RootNamespace>StellaOps.Policy.Unknowns.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.PolicyDsl;
|
||||
using Xunit;
|
||||
@@ -32,7 +33,7 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
|
||||
if (!result1.Success || result1.Document is null)
|
||||
{
|
||||
return true.Label("Skip: Source doesn't parse cleanly");
|
||||
return true.ToProperty().Label("Skip: Source doesn't parse cleanly");
|
||||
}
|
||||
|
||||
// Print the document back to source
|
||||
@@ -44,14 +45,14 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
// Both should succeed
|
||||
if (!result2.Success || result2.Document is null)
|
||||
{
|
||||
return false.Label($"Roundtrip failed: {string.Join("; ", result2.Diagnostics.Select(d => d.Message))}");
|
||||
return false.ToProperty().Label($"Roundtrip failed: {string.Join("; ", result2.Diagnostics.Select(d => d.Message))}");
|
||||
}
|
||||
|
||||
// Documents should be semantically equivalent
|
||||
var equivalent = AreDocumentsEquivalent(result1.Document, result2.Document);
|
||||
|
||||
return equivalent
|
||||
.Label($"Documents should be equivalent after roundtrip");
|
||||
.ToProperty().Label($"Documents should be equivalent after roundtrip");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,7 +79,7 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
var result1 = _compiler.Compile(source);
|
||||
if (!result1.Success || result1.Document is null)
|
||||
{
|
||||
return true.Label("Skip: Name causes parse failure");
|
||||
return true.ToProperty().Label("Skip: Name causes parse failure");
|
||||
}
|
||||
|
||||
var printed = PolicyIrPrinter.Print(result1.Document);
|
||||
@@ -86,11 +87,11 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
|
||||
if (!result2.Success || result2.Document is null)
|
||||
{
|
||||
return false.Label("Roundtrip parse failed");
|
||||
return false.ToProperty().Label("Roundtrip parse failed");
|
||||
}
|
||||
|
||||
return (result1.Document.Name == result2.Document.Name)
|
||||
.Label($"Name should be preserved: '{result1.Document.Name}' vs '{result2.Document.Name}'");
|
||||
.ToProperty().Label($"Name should be preserved: '{result1.Document.Name}' vs '{result2.Document.Name}'");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +108,7 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
var result1 = _compiler.Compile(source);
|
||||
if (!result1.Success || result1.Document is null)
|
||||
{
|
||||
return true.Label("Skip: Source doesn't parse");
|
||||
return true.ToProperty().Label("Skip: Source doesn't parse");
|
||||
}
|
||||
|
||||
var printed = PolicyIrPrinter.Print(result1.Document);
|
||||
@@ -115,11 +116,11 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
|
||||
if (!result2.Success || result2.Document is null)
|
||||
{
|
||||
return false.Label("Roundtrip parse failed");
|
||||
return false.ToProperty().Label("Roundtrip parse failed");
|
||||
}
|
||||
|
||||
return (result1.Document.Rules.Length == result2.Document.Rules.Length)
|
||||
.Label($"Rule count should be preserved: {result1.Document.Rules.Length} vs {result2.Document.Rules.Length}");
|
||||
.ToProperty().Label($"Rule count should be preserved: {result1.Document.Rules.Length} vs {result2.Document.Rules.Length}");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,7 +137,7 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
var result1 = _compiler.Compile(source);
|
||||
if (!result1.Success || result1.Document is null)
|
||||
{
|
||||
return true.Label("Skip: Source doesn't parse");
|
||||
return true.ToProperty().Label("Skip: Source doesn't parse");
|
||||
}
|
||||
|
||||
var printed = PolicyIrPrinter.Print(result1.Document);
|
||||
@@ -144,7 +145,7 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
|
||||
if (!result2.Success || result2.Document is null)
|
||||
{
|
||||
return false.Label("Roundtrip parse failed");
|
||||
return false.ToProperty().Label("Roundtrip parse failed");
|
||||
}
|
||||
|
||||
var keysMatch = result1.Document.Metadata.Keys
|
||||
@@ -152,7 +153,7 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
.SequenceEqual(result2.Document.Metadata.Keys.OrderBy(k => k));
|
||||
|
||||
return keysMatch
|
||||
.Label($"Metadata keys should be preserved");
|
||||
.ToProperty().Label($"Metadata keys should be preserved");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,11 +172,11 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
|
||||
if (!result1.Success || !result2.Success)
|
||||
{
|
||||
return true.Label("Skip: Parse failures");
|
||||
return true.ToProperty().Label("Skip: Parse failures");
|
||||
}
|
||||
|
||||
return (result1.Checksum == result2.Checksum)
|
||||
.Label($"Checksum should be stable: {result1.Checksum} vs {result2.Checksum}");
|
||||
.ToProperty().Label($"Checksum should be stable: {result1.Checksum} vs {result2.Checksum}");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -192,7 +193,7 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
var result1 = _compiler.Compile(source);
|
||||
if (!result1.Success || result1.Document is null)
|
||||
{
|
||||
return true.Label("Skip: Score policy doesn't parse");
|
||||
return true.ToProperty().Label("Skip: Score policy doesn't parse");
|
||||
}
|
||||
|
||||
var printed = PolicyIrPrinter.Print(result1.Document);
|
||||
@@ -200,11 +201,11 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
|
||||
if (!result2.Success || result2.Document is null)
|
||||
{
|
||||
return false.Label($"Score policy roundtrip failed: {string.Join("; ", result2.Diagnostics.Select(d => d.Message))}");
|
||||
return false.ToProperty().Label($"Score policy roundtrip failed: {string.Join("; ", result2.Diagnostics.Select(d => d.Message))}");
|
||||
}
|
||||
|
||||
return AreDocumentsEquivalent(result1.Document, result2.Document)
|
||||
.Label("Score policy documents should be equivalent after roundtrip");
|
||||
.ToProperty().Label("Score policy documents should be equivalent after roundtrip");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,7 +232,7 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
var result = _compiler.Compile(source);
|
||||
|
||||
return (result.Success && result.Document is not null)
|
||||
.Label($"Score condition '{condition}' should parse successfully");
|
||||
.ToProperty().Label($"Score condition '{condition}' should parse successfully");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -258,7 +259,7 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
var result1 = _compiler.Compile(source);
|
||||
if (!result1.Success || result1.Document is null)
|
||||
{
|
||||
return true.Label($"Skip: Condition '{condition}' doesn't parse");
|
||||
return true.ToProperty().Label($"Skip: Condition '{condition}' doesn't parse");
|
||||
}
|
||||
|
||||
var printed = PolicyIrPrinter.Print(result1.Document);
|
||||
@@ -266,12 +267,12 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
|
||||
if (!result2.Success || result2.Document is null)
|
||||
{
|
||||
return false.Label($"Roundtrip failed for '{condition}'");
|
||||
return false.ToProperty().Label($"Roundtrip failed for '{condition}'");
|
||||
}
|
||||
|
||||
// Verify rule count matches
|
||||
return (result1.Document.Rules.Length == result2.Document.Rules.Length)
|
||||
.Label($"Rule count preserved for condition '{condition}'");
|
||||
.ToProperty().Label($"Rule count preserved for condition '{condition}'");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -291,18 +292,18 @@ public sealed class PolicyDslRoundtripPropertyTests
|
||||
|
||||
if (!result1.Success || !result2.Success)
|
||||
{
|
||||
return true.Label("Skip: Parse failures");
|
||||
return true.ToProperty().Label("Skip: Parse failures");
|
||||
}
|
||||
|
||||
// If sources are identical, checksums should match
|
||||
if (source1 == source2)
|
||||
{
|
||||
return (result1.Checksum == result2.Checksum)
|
||||
.Label("Identical sources should have same checksum");
|
||||
.ToProperty().Label("Identical sources should have same checksum");
|
||||
}
|
||||
|
||||
// Different sources may produce different checksums (not guaranteed if semantically equal)
|
||||
return true.Label("Different sources checked");
|
||||
return true.ToProperty().Label("Different sources checked");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -397,22 +398,22 @@ internal static class PolicyDslArbs
|
||||
Arb.From(
|
||||
from name in Gen.Elements(ValidIdentifiers)
|
||||
from ruleCount in Gen.Choose(1, 3)
|
||||
from rules in Gen.ArrayOf(ruleCount, GenRule())
|
||||
from rules in Gen.ArrayOf<string>(GenRule(), ruleCount)
|
||||
select BuildPolicy(name, rules));
|
||||
|
||||
public static Arbitrary<string> ValidPolicyWithRules() =>
|
||||
Arb.From(
|
||||
from name in Gen.Elements(ValidIdentifiers)
|
||||
from ruleCount in Gen.Choose(1, 5)
|
||||
from rules in Gen.ArrayOf(ruleCount, GenRule())
|
||||
from rules in Gen.ArrayOf<string>(GenRule(), ruleCount)
|
||||
select BuildPolicy(name, rules));
|
||||
|
||||
public static Arbitrary<string> ValidPolicyWithMetadata() =>
|
||||
Arb.From(
|
||||
from name in Gen.Elements(ValidIdentifiers)
|
||||
from hasVersion in Arb.Generate<bool>()
|
||||
from hasAuthor in Arb.Generate<bool>()
|
||||
from rules in Gen.ArrayOf(1, GenRule())
|
||||
from hasVersion in ArbMap.Default.GeneratorFor<bool>()
|
||||
from hasAuthor in ArbMap.Default.GeneratorFor<bool>()
|
||||
from rules in Gen.ArrayOf<string>(GenRule(), 1)
|
||||
select BuildPolicyWithMetadata(name, hasVersion, hasAuthor, rules));
|
||||
|
||||
/// <summary>
|
||||
@@ -422,7 +423,7 @@ internal static class PolicyDslArbs
|
||||
Arb.From(
|
||||
from name in Gen.Elements(ValidIdentifiers)
|
||||
from ruleCount in Gen.Choose(1, 3)
|
||||
from rules in Gen.ArrayOf(ruleCount, GenScoreRule())
|
||||
from rules in Gen.ArrayOf<string>(GenScoreRule(), ruleCount)
|
||||
select BuildPolicy(name, rules));
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -9,23 +9,13 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<!-- Disable Concelier test infra to avoid duplicate package references -->
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="FsCheck" Version="2.16.6" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.PolicyDsl/StellaOps.PolicyDsl.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
@@ -35,4 +25,4 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user