Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy.Crypto;
|
||||
using StellaOps.Policy.Engine.Crypto;
|
||||
using Xunit;
|
||||
using EngineCryptoRiskEvaluator = StellaOps.Policy.Engine.Crypto.CryptoRiskEvaluator;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Crypto;
|
||||
|
||||
public sealed class CryptoRiskEvaluatorTests
|
||||
{
|
||||
private readonly EngineCryptoRiskEvaluator _evaluator = new();
|
||||
|
||||
#region DEPRECATED_CRYPTO Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateDeprecatedCrypto_WithMD5_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("MD5"));
|
||||
|
||||
var result = _evaluator.EvaluateDeprecatedCrypto(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.Critical, result.Severity);
|
||||
Assert.Contains("MD5", result.TriggeringAlgorithms);
|
||||
Assert.Contains("MD5", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateDeprecatedCrypto_WithSHA1_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("SHA-1"));
|
||||
|
||||
var result = _evaluator.EvaluateDeprecatedCrypto(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.Critical, result.Severity);
|
||||
Assert.Contains("SHA-1", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateDeprecatedCrypto_With3DES_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("3DES"));
|
||||
|
||||
var result = _evaluator.EvaluateDeprecatedCrypto(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("3DES", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateDeprecatedCrypto_WithAES256_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"));
|
||||
|
||||
var result = _evaluator.EvaluateDeprecatedCrypto(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.Info, result.Severity);
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WEAK_CRYPTO Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateWeakCrypto_WithECB_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-ECB"));
|
||||
|
||||
var result = _evaluator.EvaluateWeakCrypto(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.High, result.Severity);
|
||||
Assert.Contains("AES-256-ECB", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateWeakCrypto_WithGCM_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"));
|
||||
|
||||
var result = _evaluator.EvaluateWeakCrypto(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.Info, result.Severity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region QUANTUM_VULNERABLE Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithRSA_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("RSA-2048"));
|
||||
|
||||
var result = _evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("RSA-2048", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithECDSA_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ECDSA-P256"));
|
||||
|
||||
var result = _evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("ECDSA-P256", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithMLKEM_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ML-KEM-768"));
|
||||
|
||||
var result = _evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithSymmetricOnly_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"),
|
||||
CreateAlgorithmAsset("ChaCha20-Poly1305"));
|
||||
|
||||
var result = _evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateQuantumVulnerable_WithDisabledWarnings_DoesNotTrigger()
|
||||
{
|
||||
var evaluator = new EngineCryptoRiskEvaluator(new CryptoRiskOptions
|
||||
{
|
||||
EnableQuantumRiskWarnings = false
|
||||
});
|
||||
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("RSA-2048"));
|
||||
|
||||
var result = evaluator.EvaluateQuantumVulnerable(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region INSUFFICIENT_KEY_SIZE Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateInsufficientKeySize_WithRSA1024_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
new CryptoAsset
|
||||
{
|
||||
Id = "test",
|
||||
ComponentKey = "test",
|
||||
AlgorithmName = "RSA",
|
||||
KeySizeBits = 1024
|
||||
});
|
||||
|
||||
var result = _evaluator.EvaluateInsufficientKeySize(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Equal(CryptoSeverity.High, result.Severity);
|
||||
Assert.Single(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateInsufficientKeySize_WithRSA2048_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
new CryptoAsset
|
||||
{
|
||||
Id = "test",
|
||||
ComponentKey = "test",
|
||||
AlgorithmName = "RSA",
|
||||
KeySizeBits = 2048
|
||||
});
|
||||
|
||||
var result = _evaluator.EvaluateInsufficientKeySize(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateInsufficientKeySize_WithECDSA192_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
new CryptoAsset
|
||||
{
|
||||
Id = "test",
|
||||
ComponentKey = "test",
|
||||
AlgorithmName = "ECDSA",
|
||||
KeySizeBits = 192
|
||||
});
|
||||
|
||||
var result = _evaluator.EvaluateInsufficientKeySize(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region POST_QUANTUM_READY Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePostQuantumReady_WithMLKEM_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ML-KEM-768"));
|
||||
|
||||
var result = _evaluator.EvaluatePostQuantumReady(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("ML-KEM-768", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePostQuantumReady_WithKyber_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("KYBER-1024"));
|
||||
|
||||
var result = _evaluator.EvaluatePostQuantumReady(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("KYBER-1024", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePostQuantumReady_WithDilithium_TriggersAtom()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ML-DSA-65"));
|
||||
|
||||
var result = _evaluator.EvaluatePostQuantumReady(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("ML-DSA-65", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePostQuantumReady_WithRSAOnly_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("RSA-2048"));
|
||||
|
||||
var result = _evaluator.EvaluatePostQuantumReady(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FIPS_COMPLIANT Tests
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFipsCompliance_WithFipsAlgorithms_DoesNotTrigger()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"),
|
||||
CreateAlgorithmAsset("SHA-256"));
|
||||
|
||||
var result = _evaluator.EvaluateFipsCompliance(assets);
|
||||
|
||||
Assert.False(result.Triggered);
|
||||
Assert.Empty(result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateFipsCompliance_WithNonFipsAlgorithm_ReportsNonCompliant()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("ChaCha20-Poly1305"));
|
||||
|
||||
var evaluator = new EngineCryptoRiskEvaluator(new CryptoRiskOptions
|
||||
{
|
||||
RequireFipsCompliance = true
|
||||
});
|
||||
|
||||
var result = evaluator.EvaluateFipsCompliance(assets);
|
||||
|
||||
Assert.True(result.Triggered);
|
||||
Assert.Contains("ChaCha20-Poly1305", result.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Full Evaluation Tests
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ReturnsAllAtomResults()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("AES-256-GCM"),
|
||||
CreateAlgorithmAsset("RSA-2048"));
|
||||
|
||||
var results = _evaluator.Evaluate(assets);
|
||||
|
||||
Assert.Equal(6, results.Length);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.DeprecatedCrypto);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.WeakCrypto);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.QuantumVulnerable);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.InsufficientKeySize);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.PostQuantumReady);
|
||||
Assert.Contains(results, r => r.AtomName == CryptoRiskAtoms.FipsCompliant);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_WithMixedAlgorithms_ReportsCorrectly()
|
||||
{
|
||||
var assets = ImmutableArray.Create(
|
||||
CreateAlgorithmAsset("MD5"), // deprecated
|
||||
CreateAlgorithmAsset("AES-128-ECB"), // weak
|
||||
CreateAlgorithmAsset("RSA-2048"), // quantum vulnerable
|
||||
CreateAlgorithmAsset("ML-KEM-768"), // post-quantum ready
|
||||
CreateAlgorithmAsset("AES-256-GCM")); // good
|
||||
|
||||
var results = _evaluator.Evaluate(assets);
|
||||
|
||||
var deprecatedResult = results.First(r => r.AtomName == CryptoRiskAtoms.DeprecatedCrypto);
|
||||
Assert.True(deprecatedResult.Triggered);
|
||||
Assert.Contains("MD5", deprecatedResult.TriggeringAlgorithms);
|
||||
|
||||
var weakResult = results.First(r => r.AtomName == CryptoRiskAtoms.WeakCrypto);
|
||||
Assert.True(weakResult.Triggered);
|
||||
Assert.Contains("AES-128-ECB", weakResult.TriggeringAlgorithms);
|
||||
|
||||
var quantumResult = results.First(r => r.AtomName == CryptoRiskAtoms.QuantumVulnerable);
|
||||
Assert.True(quantumResult.Triggered);
|
||||
Assert.Contains("RSA-2048", quantumResult.TriggeringAlgorithms);
|
||||
|
||||
var pqResult = results.First(r => r.AtomName == CryptoRiskAtoms.PostQuantumReady);
|
||||
Assert.True(pqResult.Triggered);
|
||||
Assert.Contains("ML-KEM-768", pqResult.TriggeringAlgorithms);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static CryptoAsset CreateAlgorithmAsset(string algorithmName) => new()
|
||||
{
|
||||
Id = $"asset-{algorithmName}",
|
||||
ComponentKey = "test-component",
|
||||
AlgorithmName = algorithmName
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -115,7 +115,7 @@ public sealed class PolicyEngineDeterminismTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcurrentEvaluations_ProduceIdenticalResults()
|
||||
public async Task ConcurrentEvaluations_ProduceIdenticalResults()
|
||||
{
|
||||
// Arrange
|
||||
var policy = CreateTestPolicy();
|
||||
@@ -127,7 +127,7 @@ public sealed class PolicyEngineDeterminismTests
|
||||
.Select(_ => Task.Run(() => evaluator.Evaluate(policy, input)))
|
||||
.ToArray();
|
||||
|
||||
var results = Task.WhenAll(tasks).GetAwaiter().GetResult();
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
// Assert
|
||||
var firstHash = results[0].VerdictHash;
|
||||
@@ -500,6 +500,8 @@ public record PolicyEvaluatorOptions
|
||||
|
||||
public class PolicyEvaluator(PolicyEvaluatorOptions options)
|
||||
{
|
||||
private readonly PolicyEvaluatorOptions _options = options;
|
||||
|
||||
public EvaluationResult Evaluate(PolicyDefinition policy, EvaluationInput input)
|
||||
{
|
||||
// Stub implementation - actual implementation would come from Policy.Engine
|
||||
@@ -509,6 +511,7 @@ public class PolicyEvaluator(PolicyEvaluatorOptions options)
|
||||
// Count unknowns
|
||||
var unknownsCount = input.Findings.Count(f => f.Severity == "unknown" || f.ImpactUnknown);
|
||||
metrics["unknowns_count"] = unknownsCount;
|
||||
metrics["deterministic_mode"] = _options.DeterministicMode ? 1 : 0;
|
||||
|
||||
// Check unknowns budget
|
||||
if (policy.UnknownsBudget is not null && policy.UnknownsBudget.FailOnExceed)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.Policy.Engine.Evaluation;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
@@ -28,9 +29,11 @@ public class CicdGateIntegrationTests
|
||||
_options = new PolicyGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultTimeout = TimeSpan.FromSeconds(60),
|
||||
AllowBypassWithJustification = true,
|
||||
MinimumJustificationLength = 20
|
||||
Override = new OverrideOptions
|
||||
{
|
||||
RequireJustification = true,
|
||||
MinJustificationLength = 20
|
||||
}
|
||||
};
|
||||
|
||||
var monitor = Substitute.For<IOptionsMonitor<PolicyGateOptions>>();
|
||||
@@ -366,9 +369,8 @@ public class CicdGateIntegrationTests
|
||||
RequestedStatus = "affected",
|
||||
LatticeState = "CR",
|
||||
UncertaintyTier = "T1",
|
||||
RiskScore = 9.5,
|
||||
RiskScore = 9.5
|
||||
// No baseline reference = new finding
|
||||
IsNewFinding = true
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -391,8 +393,8 @@ public class CicdGateIntegrationTests
|
||||
RequestedStatus = "affected",
|
||||
LatticeState = "CR",
|
||||
UncertaintyTier = "T4",
|
||||
RiskScore = 7.0,
|
||||
IsNewFinding = false // Already in baseline
|
||||
RiskScore = 7.0
|
||||
// Already in baseline
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
// VexTrustConfidenceFactorProviderTests - Unit tests for confidence factor provider
|
||||
// Part of SPRINT_1227_0004_0003: VexTrustGate Policy Integration
|
||||
|
||||
using StellaOps.Policy.Confidence.Models;
|
||||
using StellaOps.Policy.Engine.Confidence;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for VexTrustConfidenceFactorProvider.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Sprint", "1227.0004.0003")]
|
||||
public sealed class VexTrustConfidenceFactorProviderTests
|
||||
{
|
||||
private readonly VexTrustConfidenceFactorProvider _provider = new();
|
||||
private readonly ConfidenceFactorOptions _defaultOptions = new();
|
||||
|
||||
[Fact]
|
||||
public void Type_ReturnsVex()
|
||||
{
|
||||
Assert.Equal(ConfidenceFactorType.Vex, _provider.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_NullTrustStatus_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = null,
|
||||
Environment = "production"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_ValidTrustStatus_ReturnsFactor()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.80m,
|
||||
SignatureVerified = false,
|
||||
Freshness = "fresh",
|
||||
IssuerName = "Test Issuer"
|
||||
},
|
||||
Environment = "production"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ConfidenceFactorType.Vex, result.Type);
|
||||
Assert.Equal(0.20m, result.Weight); // Default weight
|
||||
Assert.Equal(0.80m, result.RawValue);
|
||||
Assert.Contains("VEX trust: High", result.Reason);
|
||||
Assert.Contains("Test Issuer", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_SignatureVerified_AddsBonus()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.80m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new ConfidenceFactorOptions
|
||||
{
|
||||
VexTrustWeight = 0.20m,
|
||||
SignatureVerifiedBonus = 0.10m
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0.90m, result.RawValue); // 0.80 + 0.10 bonus
|
||||
Assert.Contains("signature verified", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_RekorTransparency_AddsBonus()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.80m,
|
||||
SignatureVerified = false,
|
||||
RekorLogIndex = 12345678,
|
||||
RekorLogId = "rekor.sigstore.dev",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new ConfidenceFactorOptions
|
||||
{
|
||||
VexTrustWeight = 0.20m,
|
||||
TransparencyBonus = 0.05m
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0.85m, result.RawValue); // 0.80 + 0.05 bonus
|
||||
Assert.Contains("Rekor: #12345678", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_AllBonuses_ClampedToOne()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.95m, // Already high
|
||||
SignatureVerified = true,
|
||||
RekorLogIndex = 12345678,
|
||||
RekorLogId = "rekor.sigstore.dev",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new ConfidenceFactorOptions
|
||||
{
|
||||
SignatureVerifiedBonus = 0.10m,
|
||||
TransparencyBonus = 0.05m
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(1.0m, result.RawValue); // Clamped to 1.0
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.95, "VeryHigh")]
|
||||
[InlineData(0.75, "High")]
|
||||
[InlineData(0.55, "Medium")]
|
||||
[InlineData(0.35, "Low")]
|
||||
[InlineData(0.15, "VeryLow")]
|
||||
public void ComputeFactor_TierInReason(double score, string expectedTier)
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = (decimal)score,
|
||||
SignatureVerified = false,
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains($"VEX trust: {expectedTier}", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_FreshnessInReason()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.70m,
|
||||
SignatureVerified = false,
|
||||
Freshness = "stale"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("freshness: stale", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_SignatureMethodInReason()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.70m,
|
||||
SignatureVerified = true,
|
||||
SignatureMethod = "dsse",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("method: dsse", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_EvidenceDigests_IncludesRekor()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.70m,
|
||||
SignatureVerified = false,
|
||||
RekorLogIndex = 99999,
|
||||
RekorLogId = "test-rekor",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("rekor:test-rekor@99999", result.EvidenceDigests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_EvidenceDigests_IncludesIssuer()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.70m,
|
||||
SignatureVerified = false,
|
||||
IssuerId = "redhat.com",
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, _defaultOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("issuer:redhat.com", result.EvidenceDigests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFactor_Contribution_CalculatedCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var context = new ConfidenceFactorContext
|
||||
{
|
||||
VexTrustStatus = new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.80m,
|
||||
SignatureVerified = false,
|
||||
Freshness = "fresh"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new ConfidenceFactorOptions
|
||||
{
|
||||
VexTrustWeight = 0.25m
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _provider.ComputeFactor(context, options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0.25m, result.Weight);
|
||||
Assert.Equal(0.80m, result.RawValue);
|
||||
Assert.Equal(0.20m, result.Contribution); // 0.25 * 0.80 = 0.20
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
// VexTrustGateTests - Unit tests for VEX trust gate
|
||||
// Part of SPRINT_1227_0004_0003: VexTrustGate Policy Integration
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for VexTrustGate.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Sprint", "1227.0004.0003")]
|
||||
public sealed class VexTrustGateTests
|
||||
{
|
||||
private readonly TimeProvider _fixedTimeProvider = new FixedTimeProvider(
|
||||
new DateTimeOffset(2024, 12, 27, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
#region Gate Disabled Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WhenDisabled_ReturnsAllow()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions(enabled: false);
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production");
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
Assert.Equal("gate_disabled", result.Reason);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Status Applicability Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_StatusNotInApplyList_ReturnsAllow()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("under_investigation", "production");
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
Assert.Equal("status_not_applicable", result.Reason);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("not_affected")]
|
||||
[InlineData("fixed")]
|
||||
[InlineData("NOT_AFFECTED")] // Case insensitive
|
||||
public async Task EvaluateAsync_StatusInApplyList_Evaluates(string status)
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest(status, "production", CreateHighTrustStatus());
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
// Should evaluate, not skip
|
||||
Assert.NotEqual("status_not_applicable", result.Reason);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Missing Trust Data Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(MissingTrustBehavior.Allow, VexTrustGateDecision.Allow)]
|
||||
[InlineData(MissingTrustBehavior.Warn, VexTrustGateDecision.Warn)]
|
||||
[InlineData(MissingTrustBehavior.Block, VexTrustGateDecision.Block)]
|
||||
public async Task EvaluateAsync_MissingTrustData_RespectsConfiguredBehavior(
|
||||
MissingTrustBehavior behavior,
|
||||
VexTrustGateDecision expectedDecision)
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions(missingTrustBehavior: behavior);
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", trustStatus: null);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedDecision, result.Decision);
|
||||
Assert.Equal("missing_vex_trust_data", result.Reason);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Production Environment Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Production_HighTrust_Allows()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.85m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh",
|
||||
IssuerName = "Red Hat",
|
||||
IssuerId = "redhat.com",
|
||||
TrustBreakdown = new TrustScoreBreakdown
|
||||
{
|
||||
AccuracyScore = 0.90m
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
Assert.Equal("vex_trust_adequate", result.Reason);
|
||||
Assert.Equal("redhat.com", result.IssuerId);
|
||||
Assert.True(result.SignatureVerified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Production_LowTrust_Blocks()
|
||||
{
|
||||
// Arrange: Production requires 0.80 minimum
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.65m, // Below 0.80 threshold
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh",
|
||||
IssuerName = "Unknown Vendor"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Block, result.Decision);
|
||||
Assert.Equal("vex_trust_below_threshold", result.Reason);
|
||||
Assert.Contains(result.Checks, c => c.Name == "composite_score" && !c.Passed);
|
||||
Assert.NotNull(result.Suggestion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Production_UnverifiedSignature_Blocks()
|
||||
{
|
||||
// Arrange: Production requires signature verification
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.90m, // High score but unverified
|
||||
SignatureVerified = false,
|
||||
Freshness = "fresh"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Block, result.Decision);
|
||||
Assert.Contains(result.Checks, c => c.Name == "issuer_verified" && !c.Passed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Production_StaleFreshness_Blocks()
|
||||
{
|
||||
// Arrange: Production requires "fresh" only
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.90m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "stale",
|
||||
TrustBreakdown = new TrustScoreBreakdown
|
||||
{
|
||||
AccuracyScore = 0.90m
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Block, result.Decision);
|
||||
Assert.Contains(result.Checks, c => c.Name == "freshness" && !c.Passed);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Staging Environment Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Staging_MediumTrust_Allows()
|
||||
{
|
||||
// Arrange: Staging requires 0.60 minimum
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "staging", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.65m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "stale" // Staging allows "stale"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Staging_LowTrust_Warns()
|
||||
{
|
||||
// Arrange: Staging failure action is Warn
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "staging", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.45m, // Below 0.60
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Warn, result.Decision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Development Environment Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Development_LowTrust_Allows()
|
||||
{
|
||||
// Arrange: Development requires only 0.40
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "development", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.45m,
|
||||
SignatureVerified = false, // Development doesn't require verification
|
||||
Freshness = "superseded" // Development allows superseded
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Trust Tier Computation Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.95, "VeryHigh")]
|
||||
[InlineData(0.75, "High")]
|
||||
[InlineData(0.55, "Medium")]
|
||||
[InlineData(0.35, "Low")]
|
||||
[InlineData(0.15, "VeryLow")]
|
||||
public async Task EvaluateAsync_ComputesTrustTierCorrectly(
|
||||
double score,
|
||||
string expectedTier)
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
// Use development to avoid blocks
|
||||
options.Thresholds["development"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.01m,
|
||||
RequireIssuerVerified = false,
|
||||
AcceptableFreshness = { "fresh", "stale", "superseded", "unknown" },
|
||||
FailureAction = FailureAction.Warn
|
||||
};
|
||||
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "development", new VexTrustStatus
|
||||
{
|
||||
TrustScore = (decimal)score,
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedTier, result.TrustTier);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checks Population Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_PopulatesAllChecks()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", CreateHighTrustStatus());
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Checks, c => c.Name == "composite_score");
|
||||
Assert.Contains(result.Checks, c => c.Name == "issuer_verified");
|
||||
Assert.Contains(result.Checks, c => c.Name == "freshness");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_AccuracyCheck_IncludedWhenThresholdSet()
|
||||
{
|
||||
// Arrange: Production has MinAccuracyRate set
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.90m,
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh",
|
||||
TrustBreakdown = new TrustScoreBreakdown
|
||||
{
|
||||
AccuracyScore = 0.90m
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Checks, c => c.Name == "accuracy_rate");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fallback to Default Thresholds
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_UnknownEnvironment_UsesDefaultThresholds()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "custom-env", new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.75m, // Above default 0.70
|
||||
SignatureVerified = true,
|
||||
Freshness = "fresh"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VexTrustGateDecision.Allow, result.Decision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gate ID Format
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_GeneratesProperGateId()
|
||||
{
|
||||
// Arrange
|
||||
var options = CreateOptions();
|
||||
var gate = CreateGate(options);
|
||||
var request = CreateRequest("not_affected", "production", CreateHighTrustStatus());
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.StartsWith("vex-trust:", result.GateId);
|
||||
Assert.Contains("not_affected", result.GateId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private VexTrustGate CreateGate(VexTrustGateOptions options)
|
||||
{
|
||||
var optionsMonitor = new TestOptionsMonitor<VexTrustGateOptions>(options);
|
||||
return new VexTrustGate(
|
||||
optionsMonitor,
|
||||
NullLogger<VexTrustGate>.Instance,
|
||||
_fixedTimeProvider);
|
||||
}
|
||||
|
||||
private static VexTrustGateOptions CreateOptions(
|
||||
bool enabled = true,
|
||||
MissingTrustBehavior missingTrustBehavior = MissingTrustBehavior.Warn)
|
||||
{
|
||||
return new VexTrustGateOptions
|
||||
{
|
||||
Enabled = enabled,
|
||||
MissingTrustBehavior = missingTrustBehavior,
|
||||
ApplyToStatuses = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"not_affected",
|
||||
"fixed"
|
||||
},
|
||||
Thresholds = new Dictionary<string, VexTrustThresholds>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["production"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.80m,
|
||||
RequireIssuerVerified = true,
|
||||
MinAccuracyRate = 0.85m,
|
||||
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "fresh" },
|
||||
FailureAction = FailureAction.Block
|
||||
},
|
||||
["staging"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.60m,
|
||||
RequireIssuerVerified = true,
|
||||
MinAccuracyRate = null,
|
||||
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "fresh", "stale" },
|
||||
FailureAction = FailureAction.Warn
|
||||
},
|
||||
["development"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.40m,
|
||||
RequireIssuerVerified = false,
|
||||
MinAccuracyRate = null,
|
||||
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"fresh",
|
||||
"stale",
|
||||
"superseded"
|
||||
},
|
||||
FailureAction = FailureAction.Warn
|
||||
},
|
||||
["default"] = new VexTrustThresholds
|
||||
{
|
||||
MinCompositeScore = 0.70m,
|
||||
RequireIssuerVerified = true,
|
||||
MinAccuracyRate = null,
|
||||
AcceptableFreshness = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "fresh", "stale" },
|
||||
FailureAction = FailureAction.Warn
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static VexTrustGateRequest CreateRequest(
|
||||
string status,
|
||||
string environment,
|
||||
VexTrustStatus? trustStatus = null)
|
||||
{
|
||||
return new VexTrustGateRequest
|
||||
{
|
||||
RequestedStatus = status,
|
||||
Environment = environment,
|
||||
VexTrustStatus = trustStatus
|
||||
};
|
||||
}
|
||||
|
||||
private static VexTrustStatus CreateHighTrustStatus()
|
||||
{
|
||||
return new VexTrustStatus
|
||||
{
|
||||
TrustScore = 0.90m,
|
||||
SignatureVerified = true,
|
||||
SignatureMethod = "dsse",
|
||||
Freshness = "fresh",
|
||||
IssuerName = "Red Hat Security",
|
||||
IssuerId = "redhat.com",
|
||||
RekorLogIndex = 12345678,
|
||||
RekorLogId = "rekor.sigstore.dev",
|
||||
VerifiedAt = DateTimeOffset.UtcNow,
|
||||
TrustBreakdown = new TrustScoreBreakdown
|
||||
{
|
||||
OriginScore = 0.95m,
|
||||
FreshnessScore = 0.90m,
|
||||
AccuracyScore = 0.88m,
|
||||
VerificationScore = 1.0m
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Helpers
|
||||
|
||||
private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
{
|
||||
private readonly T _value;
|
||||
|
||||
public TestOptionsMonitor(T value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public T CurrentValue => _value;
|
||||
|
||||
public T Get(string? name) => _value;
|
||||
|
||||
public IDisposable? OnChange(Action<T, string?> listener) => null;
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime)
|
||||
{
|
||||
_fixedTime = fixedTime;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -387,7 +387,7 @@ public sealed class EwsAttestationReproducibilityTests
|
||||
// Assert
|
||||
result.Breakdown.Should().NotBeEmpty("breakdown should explain score composition");
|
||||
// Each dimension should be accounted for
|
||||
result.Breakdown.Should().HaveCountGreaterOrEqualTo(1);
|
||||
result.Breakdown.Should().HaveCountGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Explanations provide human-readable audit information")]
|
||||
|
||||
@@ -11,7 +11,6 @@ using StellaOps.Policy.Engine.Scoring.EvidenceWeightedScore;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
using StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Integration;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.DeltaVerdict.Models;
|
||||
using StellaOps.DeltaVerdict.Policy;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.Policy.Engine.Evaluation;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
@@ -279,7 +279,7 @@ public sealed class AdvancedScoringEngineTests
|
||||
var result = await _engine.ScoreAsync(input, _defaultPolicy);
|
||||
|
||||
result.Severity.Should().Be("critical");
|
||||
result.FinalScore.Should().BeGreaterOrEqualTo(90);
|
||||
result.FinalScore.Should().BeGreaterThanOrEqualTo(90);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ScoreAsync with gate applies gate multiplier")]
|
||||
|
||||
@@ -314,7 +314,7 @@ public sealed class ConfidenceToEwsComparisonTests
|
||||
// Assert: Should be divergent (opposite risk assessments)
|
||||
comparison.Alignment.Should().Be(AlignmentLevel.Divergent,
|
||||
"opposite risk assessments should produce divergent alignment");
|
||||
comparison.ScoreDifference.Should().BeGreaterOrEqualTo(30,
|
||||
comparison.ScoreDifference.Should().BeGreaterThanOrEqualTo(30,
|
||||
"score difference should be significant for divergent scores");
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ public sealed class ScorePolicyServiceCachingTests
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Concurrent access is thread-safe")]
|
||||
public void ConcurrentAccess_IsThreadSafe()
|
||||
public async Task ConcurrentAccess_IsThreadSafe()
|
||||
{
|
||||
var policy = CreateTestPolicy("tenant-1");
|
||||
var callCount = 0;
|
||||
@@ -179,7 +179,7 @@ public sealed class ScorePolicyServiceCachingTests
|
||||
.Select(_ => Task.Run(() => _service.GetPolicy("tenant-1")))
|
||||
.ToArray();
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// ConcurrentDictionary's GetOrAdd may call factory multiple times
|
||||
// but should converge to same cached value
|
||||
|
||||
@@ -336,7 +336,7 @@ public sealed class SimpleScoringEngineTests
|
||||
|
||||
var result = await _engine.ScoreAsync(input, policy);
|
||||
|
||||
result.FinalScore.Should().BeLessOrEqualTo(30);
|
||||
result.FinalScore.Should().BeLessThanOrEqualTo(30);
|
||||
result.OverrideApplied.Should().Contain("max_unreachable");
|
||||
}
|
||||
|
||||
|
||||
@@ -76,13 +76,13 @@ public sealed class RiskSimulationBreakdownServiceTests
|
||||
|
||||
// Assert
|
||||
breakdown.SignalAnalysis.TopContributors.Should().NotBeEmpty();
|
||||
breakdown.SignalAnalysis.TopContributors.Length.Should().BeLessOrEqualTo(10);
|
||||
breakdown.SignalAnalysis.TopContributors.Length.Should().BeLessThanOrEqualTo(10);
|
||||
|
||||
// Top contributors should be ordered by contribution
|
||||
for (var i = 1; i < breakdown.SignalAnalysis.TopContributors.Length; i++)
|
||||
{
|
||||
breakdown.SignalAnalysis.TopContributors[i - 1].TotalContribution
|
||||
.Should().BeGreaterOrEqualTo(breakdown.SignalAnalysis.TopContributors[i].TotalContribution);
|
||||
.Should().BeGreaterThanOrEqualTo(breakdown.SignalAnalysis.TopContributors[i].TotalContribution);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ public sealed class RiskSimulationBreakdownServiceTests
|
||||
|
||||
// Assert
|
||||
// HHI ranges from 1/n to 1
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeGreaterOrEqualTo(0);
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeLessOrEqualTo(1);
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeGreaterThanOrEqualTo(0);
|
||||
breakdown.SeverityBreakdown.SeverityConcentration.Should().BeLessThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -226,8 +226,8 @@ public sealed class RiskSimulationBreakdownServiceTests
|
||||
|
||||
// Assert
|
||||
// Stability ranges from 0 to 1
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeGreaterOrEqualTo(0);
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeLessOrEqualTo(1);
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeGreaterThanOrEqualTo(0);
|
||||
breakdown.ActionBreakdown.DecisionStability.Should().BeLessThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -374,7 +374,7 @@ public sealed class RiskSimulationBreakdownServiceTests
|
||||
// Assert
|
||||
breakdown.SignalAnalysis.MissingSignalImpact.Should().NotBeNull();
|
||||
// Some findings have missing signals
|
||||
breakdown.SignalAnalysis.SignalsMissing.Should().BeGreaterOrEqualTo(0);
|
||||
breakdown.SignalAnalysis.SignalsMissing.Should().BeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
#region Test Helpers
|
||||
|
||||
@@ -106,8 +106,8 @@ public sealed class PolicyEvaluationTraceSnapshotTests
|
||||
// Assert
|
||||
trace.StartedAt.Should().Be(FrozenTime);
|
||||
trace.CompletedAt.Should().BeAfter(trace.StartedAt);
|
||||
trace.DurationMs.Should().BeGreaterOrEqualTo(0);
|
||||
trace.Steps.Should().AllSatisfy(s => s.DurationMs.Should().BeGreaterOrEqualTo(0));
|
||||
trace.DurationMs.Should().BeGreaterThanOrEqualTo(0);
|
||||
trace.Steps.Should().AllSatisfy(s => s.DurationMs.Should().BeGreaterThanOrEqualTo(0));
|
||||
}
|
||||
|
||||
#region Trace Factories
|
||||
|
||||
@@ -132,7 +132,7 @@ public sealed class VerdictEwsSnapshotTests
|
||||
// Assert - Each contribution should be >= the next
|
||||
for (int i = 0; i < contributions.Count - 1; i++)
|
||||
{
|
||||
contributions[i].Should().BeGreaterOrEqualTo(contributions[i + 1],
|
||||
contributions[i].Should().BeGreaterThanOrEqualTo(contributions[i + 1],
|
||||
$"Breakdown[{i}] contribution should be >= Breakdown[{i + 1}]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,25 +8,15 @@
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="FsCheck" Version="2.16.6" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -36,5 +26,6 @@
|
||||
<ProjectReference Include="../../../Excititor/__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../Scanner/__Libraries/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -4,8 +4,8 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Engine.Options;
|
||||
using StellaOps.Policy.Engine.Storage.InMemory;
|
||||
using StellaOps.Policy.Engine.Workers;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
using StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
using StellaOps.Policy.Persistence.Postgres.Models;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Workers;
|
||||
|
||||
Reference in New Issue
Block a user