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

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