Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -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);
});
}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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")]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -3,6 +3,7 @@
using FluentAssertions;
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

View File

@@ -3,6 +3,7 @@
using FluentAssertions;
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

View File

@@ -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")]

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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

View File

@@ -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}]");
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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)
{

View File

@@ -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)>();

View File

@@ -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");

View File

@@ -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>

View File

@@ -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

View File

@@ -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";

View File

@@ -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);
}
}

View File

@@ -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)
{

View File

@@ -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>

View File

@@ -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

View File

@@ -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)]

View File

@@ -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

View File

@@ -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

View File

@@ -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).

View File

@@ -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

View File

@@ -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.

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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]

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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");

View File

@@ -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

View File

@@ -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).

View File

@@ -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

View File

@@ -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>

View File

@@ -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);

View File

@@ -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());

View File

@@ -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)]

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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")

View File

@@ -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>

View File

@@ -7,6 +7,7 @@
// -----------------------------------------------------------------------------
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using FluentAssertions;
using StellaOps.Policy.TrustLattice;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>