sprints work

This commit is contained in:
StellaOps Bot
2025-12-24 21:46:08 +02:00
parent 43e2af88f6
commit b9f71fc7e9
161 changed files with 29566 additions and 527 deletions

View File

@@ -0,0 +1,445 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
using FluentAssertions;
using StellaOps.Signals.EvidenceWeightedScore;
using Xunit;
namespace StellaOps.Signals.Tests.EvidenceWeightedScore;
public class ReachabilityInputTests
{
[Fact]
public void Validate_WithValidInput_ReturnsNoErrors()
{
var input = CreateValidInput();
var errors = input.Validate();
errors.Should().BeEmpty();
}
[Theory]
[InlineData(-0.1)]
[InlineData(1.5)]
public void Validate_WithInvalidConfidence_ReturnsError(double confidence)
{
var input = CreateValidInput() with { Confidence = confidence };
var errors = input.Validate();
errors.Should().ContainSingle(e => e.Contains("Confidence"));
}
[Fact]
public void Validate_WithNegativeHopCount_ReturnsError()
{
var input = CreateValidInput() with { HopCount = -1 };
var errors = input.Validate();
errors.Should().ContainSingle(e => e.Contains("HopCount"));
}
[Theory]
[InlineData(ReachabilityState.Unknown, "No reachability data available")]
[InlineData(ReachabilityState.NotReachable, "Confirmed not reachable")]
[InlineData(ReachabilityState.StaticReachable, "Statically reachable")]
[InlineData(ReachabilityState.DynamicReachable, "Dynamically confirmed reachable")]
[InlineData(ReachabilityState.LiveExploitPath, "Live exploit path observed")]
public void GetExplanation_ReturnsCorrectStateDescription(ReachabilityState state, string expectedFragment)
{
var input = CreateValidInput() with { State = state };
var explanation = input.GetExplanation();
explanation.Should().Contain(expectedFragment);
}
[Theory]
[InlineData(0, "direct path")]
[InlineData(1, "1 hop away")]
[InlineData(5, "5 hops away")]
public void GetExplanation_IncludesHopInfo(int hopCount, string expectedFragment)
{
var input = CreateValidInput() with { HopCount = hopCount };
var explanation = input.GetExplanation();
explanation.Should().Contain(expectedFragment);
}
[Fact]
public void GetExplanation_IncludesAnalysisFlags()
{
var input = CreateValidInput() with
{
HasInterproceduralFlow = true,
HasTaintTracking = true,
HasDataFlowSensitivity = true
};
var explanation = input.GetExplanation();
explanation.Should().Contain("interprocedural");
explanation.Should().Contain("taint-tracked");
explanation.Should().Contain("data-flow");
}
private static ReachabilityInput CreateValidInput() => new()
{
State = ReachabilityState.StaticReachable,
Confidence = 0.8,
HopCount = 2
};
}
public class RuntimeInputTests
{
[Fact]
public void Validate_WithValidInput_ReturnsNoErrors()
{
var input = CreateValidInput();
var errors = input.Validate();
errors.Should().BeEmpty();
}
[Fact]
public void Validate_WithNegativeObservationCount_ReturnsError()
{
var input = CreateValidInput() with { ObservationCount = -1 };
var errors = input.Validate();
errors.Should().ContainSingle(e => e.Contains("ObservationCount"));
}
[Theory]
[InlineData(-0.1)]
[InlineData(1.5)]
public void Validate_WithInvalidRecencyFactor_ReturnsError(double recency)
{
var input = CreateValidInput() with { RecencyFactor = recency };
var errors = input.Validate();
errors.Should().ContainSingle(e => e.Contains("RecencyFactor"));
}
[Theory]
[InlineData(RuntimePosture.None, 0, "No runtime observations")]
[InlineData(RuntimePosture.EbpfDeep, 5, "eBPF deep observation")]
[InlineData(RuntimePosture.ActiveTracing, 10, "active tracing")]
public void GetExplanation_ReturnsCorrectDescription(RuntimePosture posture, int count, string expectedFragment)
{
var input = CreateValidInput() with { Posture = posture, ObservationCount = count };
var explanation = input.GetExplanation();
explanation.Should().Contain(expectedFragment);
}
[Fact]
public void GetExplanation_IncludesProductionInfo()
{
var input = CreateValidInput() with { IsProductionTraffic = true };
var explanation = input.GetExplanation();
explanation.Should().Contain("in production");
}
[Fact]
public void GetExplanation_IncludesDirectPathInfo()
{
var input = CreateValidInput() with { DirectPathObserved = true };
var explanation = input.GetExplanation();
explanation.Should().Contain("vulnerable path directly observed");
}
private static RuntimeInput CreateValidInput() => new()
{
Posture = RuntimePosture.EbpfDeep,
ObservationCount = 5,
RecencyFactor = 0.9
};
}
public class BackportInputTests
{
[Fact]
public void Validate_WithValidInput_ReturnsNoErrors()
{
var input = CreateValidInput();
var errors = input.Validate();
errors.Should().BeEmpty();
}
[Theory]
[InlineData(-0.1)]
[InlineData(1.5)]
public void Validate_WithInvalidConfidence_ReturnsError(double confidence)
{
var input = CreateValidInput() with { Confidence = confidence };
var errors = input.Validate();
errors.Should().ContainSingle(e => e.Contains("Confidence"));
}
[Theory]
[InlineData(BackportStatus.NotAffected, "confirmed not affected")]
[InlineData(BackportStatus.Affected, "confirmed affected")]
[InlineData(BackportStatus.Fixed, "fixed")]
public void GetExplanation_ReturnsCorrectStatusDescription(BackportStatus status, string expectedFragment)
{
var input = CreateValidInput() with { Status = status };
var explanation = input.GetExplanation();
explanation.Should().Contain(expectedFragment);
}
[Theory]
[InlineData(BackportEvidenceTier.VendorVex, "vendor VEX")]
[InlineData(BackportEvidenceTier.SignedProof, "signed proof")]
[InlineData(BackportEvidenceTier.BinaryDiff, "binary-diff")]
public void GetExplanation_ReturnsCorrectTierDescription(BackportEvidenceTier tier, string expectedFragment)
{
var input = CreateValidInput() with { EvidenceTier = tier };
var explanation = input.GetExplanation();
explanation.Should().Contain(expectedFragment);
}
[Fact]
public void GetExplanation_IncludesDistributor()
{
var input = CreateValidInput() with { Distributor = "debian-security" };
var explanation = input.GetExplanation();
explanation.Should().Contain("debian-security");
}
private static BackportInput CreateValidInput() => new()
{
EvidenceTier = BackportEvidenceTier.VendorVex,
Status = BackportStatus.NotAffected,
Confidence = 0.95
};
}
public class ExploitInputTests
{
[Fact]
public void Validate_WithValidInput_ReturnsNoErrors()
{
var input = CreateValidInput();
var errors = input.Validate();
errors.Should().BeEmpty();
}
[Theory]
[InlineData(-0.1)]
[InlineData(1.5)]
public void Validate_WithInvalidEpssScore_ReturnsError(double score)
{
var input = CreateValidInput() with { EpssScore = score };
var errors = input.Validate();
errors.Should().ContainSingle(e => e.Contains("EpssScore"));
}
[Theory]
[InlineData(-1.0)]
[InlineData(101.0)]
public void Validate_WithInvalidEpssPercentile_ReturnsError(double percentile)
{
var input = CreateValidInput() with { EpssPercentile = percentile };
var errors = input.Validate();
errors.Should().ContainSingle(e => e.Contains("EpssPercentile"));
}
[Theory]
[InlineData(0.8, "Very high EPSS")]
[InlineData(0.5, "High EPSS")]
[InlineData(0.15, "Moderate EPSS")]
[InlineData(0.05, "Low EPSS")]
public void GetExplanation_ReturnsCorrectEpssDescription(double score, string expectedFragment)
{
var input = CreateValidInput() with { EpssScore = score };
var explanation = input.GetExplanation();
explanation.Should().Contain(expectedFragment);
}
[Fact]
public void GetExplanation_IncludesKevStatus()
{
var input = CreateValidInput() with
{
KevStatus = KevStatus.InKev,
KevAddedDate = DateTimeOffset.Parse("2024-01-15T00:00:00Z")
};
var explanation = input.GetExplanation();
explanation.Should().Contain("in KEV catalog");
explanation.Should().Contain("2024-01-15");
}
[Fact]
public void GetExplanation_IncludesPublicExploit()
{
var input = CreateValidInput() with
{
PublicExploitAvailable = true,
ExploitMaturity = "weaponized"
};
var explanation = input.GetExplanation();
explanation.Should().Contain("public exploit");
explanation.Should().Contain("weaponized");
}
private static ExploitInput CreateValidInput() => new()
{
EpssScore = 0.3,
EpssPercentile = 85.0,
KevStatus = KevStatus.NotInKev
};
}
public class SourceTrustInputTests
{
[Fact]
public void Validate_WithValidInput_ReturnsNoErrors()
{
var input = CreateValidInput();
var errors = input.Validate();
errors.Should().BeEmpty();
}
[Theory]
[InlineData(-0.1)]
[InlineData(1.5)]
public void Validate_WithInvalidTrustFactors_ReturnsErrors(double value)
{
var input = CreateValidInput() with
{
ProvenanceTrust = value,
CoverageCompleteness = value,
Replayability = value
};
var errors = input.Validate();
errors.Should().HaveCount(3);
}
[Theory]
[InlineData(IssuerType.Vendor, "software vendor")]
[InlineData(IssuerType.Distribution, "distribution maintainer")]
[InlineData(IssuerType.GovernmentAgency, "government agency")]
public void GetExplanation_ReturnsCorrectIssuerDescription(IssuerType issuer, string expectedFragment)
{
var input = CreateValidInput() with { IssuerType = issuer };
var explanation = input.GetExplanation();
explanation.Should().Contain(expectedFragment);
}
[Fact]
public void GetCombinedTrustScore_CalculatesWeightedAverage()
{
var input = new SourceTrustInput
{
IssuerType = IssuerType.Vendor,
ProvenanceTrust = 1.0,
CoverageCompleteness = 1.0,
Replayability = 1.0
};
var score = input.GetCombinedTrustScore();
score.Should().Be(1.0); // All weights sum to 1
}
[Fact]
public void GetExplanation_IncludesAttestationInfo()
{
var input = CreateValidInput() with
{
IsCryptographicallyAttested = true,
IndependentlyVerified = true,
CorroboratingSourceCount = 3
};
var explanation = input.GetExplanation();
explanation.Should().Contain("cryptographically attested");
explanation.Should().Contain("independently verified");
explanation.Should().Contain("3 corroborating");
}
private static SourceTrustInput CreateValidInput() => new()
{
IssuerType = IssuerType.Vendor,
ProvenanceTrust = 0.9,
CoverageCompleteness = 0.8,
Replayability = 0.7
};
}
public class MitigationInputTests
{
[Fact]
public void Validate_WithValidInput_ReturnsNoErrors()
{
var input = CreateValidInput();
var errors = input.Validate();
errors.Should().BeEmpty();
}
[Theory]
[InlineData(-0.1)]
[InlineData(1.5)]
public void Validate_WithInvalidCombinedEffectiveness_ReturnsError(double value)
{
var input = CreateValidInput() with { CombinedEffectiveness = value };
var errors = input.Validate();
errors.Should().ContainSingle(e => e.Contains("CombinedEffectiveness"));
}
[Fact]
public void CalculateCombinedEffectiveness_WithNoMitigations_ReturnsZero()
{
var effectiveness = MitigationInput.CalculateCombinedEffectiveness([]);
effectiveness.Should().Be(0.0);
}
[Fact]
public void CalculateCombinedEffectiveness_WithSingleMitigation_ReturnsMitigationEffectiveness()
{
var mitigations = new[]
{
new ActiveMitigation { Type = MitigationType.FeatureFlag, Effectiveness = 0.8 }
};
var effectiveness = MitigationInput.CalculateCombinedEffectiveness(mitigations);
effectiveness.Should().BeApproximately(0.8, 0.001);
}
[Fact]
public void CalculateCombinedEffectiveness_WithMultipleMitigations_UsesDiminishingReturns()
{
var mitigations = new[]
{
new ActiveMitigation { Type = MitigationType.FeatureFlag, Effectiveness = 0.5 },
new ActiveMitigation { Type = MitigationType.NetworkControl, Effectiveness = 0.5 }
};
// Combined = 1 - (1-0.5)(1-0.5) = 1 - 0.25 = 0.75
var effectiveness = MitigationInput.CalculateCombinedEffectiveness(mitigations);
effectiveness.Should().BeApproximately(0.75, 0.001);
}
[Fact]
public void GetExplanation_WithNoMitigations_ReturnsNoneMessage()
{
var input = new MitigationInput
{
ActiveMitigations = [],
CombinedEffectiveness = 0.0
};
var explanation = input.GetExplanation();
explanation.Should().Contain("No active mitigations");
}
[Fact]
public void GetExplanation_IncludesMitigationSummary()
{
var input = CreateValidInput();
var explanation = input.GetExplanation();
explanation.Should().Contain("2 active mitigation(s)");
explanation.Should().Contain("feature flag");
}
private static MitigationInput CreateValidInput() => new()
{
ActiveMitigations =
[
new ActiveMitigation { Type = MitigationType.FeatureFlag, Name = "disable-feature-x", Effectiveness = 0.7, Verified = true },
new ActiveMitigation { Type = MitigationType.NetworkControl, Name = "waf-rule-123", Effectiveness = 0.5 }
],
CombinedEffectiveness = 0.85,
RuntimeVerified = true
};
}

View File

@@ -0,0 +1,345 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
using FluentAssertions;
using StellaOps.Signals.EvidenceWeightedScore;
using Xunit;
namespace StellaOps.Signals.Tests.EvidenceWeightedScore;
public class EvidenceWeightPolicyTests
{
[Fact]
public void DefaultProduction_HasValidDefaults()
{
var policy = EvidenceWeightPolicy.DefaultProduction;
policy.Version.Should().Be("ews.v1");
policy.Profile.Should().Be("production");
policy.Weights.Should().NotBeNull();
policy.Validate().Should().BeEmpty();
}
[Fact]
public void Validate_WithValidPolicy_ReturnsNoErrors()
{
var policy = new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "test",
Weights = EvidenceWeights.Default
};
var errors = policy.Validate();
errors.Should().BeEmpty();
}
[Fact]
public void Validate_WithMissingVersion_ReturnsError()
{
var policy = new EvidenceWeightPolicy
{
Version = "",
Profile = "test",
Weights = EvidenceWeights.Default
};
var errors = policy.Validate();
errors.Should().ContainSingle(e => e.Contains("Version"));
}
[Fact]
public void Validate_WithMissingProfile_ReturnsError()
{
var policy = new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "",
Weights = EvidenceWeights.Default
};
var errors = policy.Validate();
errors.Should().ContainSingle(e => e.Contains("Profile"));
}
[Fact]
public void Validate_WithInvalidBucketOrdering_ReturnsError()
{
var policy = new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "test",
Weights = EvidenceWeights.Default,
Buckets = new BucketThresholds
{
ActNowMin = 50,
ScheduleNextMin = 70, // Invalid: should be less than ActNowMin
InvestigateMin = 40
}
};
var errors = policy.Validate();
errors.Should().Contain(e => e.Contains("ActNowMin") && e.Contains("ScheduleNextMin"));
}
[Fact]
public void ComputeDigest_IsDeterministic()
{
var policy1 = EvidenceWeightPolicy.DefaultProduction;
var policy2 = EvidenceWeightPolicy.DefaultProduction;
var digest1 = policy1.ComputeDigest();
var digest2 = policy2.ComputeDigest();
digest1.Should().Be(digest2);
}
[Fact]
public void ComputeDigest_IsCached()
{
var policy = EvidenceWeightPolicy.DefaultProduction;
var digest1 = policy.ComputeDigest();
var digest2 = policy.ComputeDigest();
digest1.Should().BeSameAs(digest2);
}
[Fact]
public void ComputeDigest_DiffersForDifferentWeights()
{
var policy1 = new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "test",
Weights = new EvidenceWeights { Rch = 0.5, Rts = 0.2, Bkp = 0.1, Xpl = 0.1, Src = 0.05, Mit = 0.05 }
};
var policy2 = new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "test",
Weights = new EvidenceWeights { Rch = 0.3, Rts = 0.3, Bkp = 0.15, Xpl = 0.15, Src = 0.05, Mit = 0.05 }
};
policy1.ComputeDigest().Should().NotBe(policy2.ComputeDigest());
}
[Fact]
public void GetCanonicalJson_IsValid()
{
var policy = EvidenceWeightPolicy.DefaultProduction;
var json = policy.GetCanonicalJson();
json.Should().NotBeNullOrEmpty();
json.Should().Contain("\"version\"");
json.Should().Contain("\"weights\"");
json.Should().Contain("\"guardrails\"");
}
}
public class EvidenceWeightsTests
{
[Fact]
public void Default_HasCorrectValues()
{
var weights = EvidenceWeights.Default;
weights.Rch.Should().Be(0.30);
weights.Rts.Should().Be(0.25);
weights.Bkp.Should().Be(0.15);
weights.Xpl.Should().Be(0.15);
weights.Src.Should().Be(0.10);
weights.Mit.Should().Be(0.10);
}
[Fact]
public void Default_AdditiveSumIsOne()
{
var weights = EvidenceWeights.Default;
// Sum of additive weights (excludes MIT)
weights.AdditiveSum.Should().BeApproximately(0.95, 0.001);
}
[Fact]
public void Normalize_SumsAdditiveToOne()
{
var weights = new EvidenceWeights
{
Rch = 0.5,
Rts = 0.3,
Bkp = 0.2,
Xpl = 0.1,
Src = 0.1,
Mit = 0.1
};
var normalized = weights.Normalize();
normalized.AdditiveSum.Should().BeApproximately(1.0, 0.001);
}
[Fact]
public void Normalize_PreservesMitWeight()
{
var weights = new EvidenceWeights
{
Rch = 0.5,
Rts = 0.3,
Bkp = 0.2,
Xpl = 0.1,
Src = 0.1,
Mit = 0.15
};
var normalized = weights.Normalize();
normalized.Mit.Should().Be(0.15);
}
[Fact]
public void Validate_WithValidWeights_ReturnsNoErrors()
{
var weights = EvidenceWeights.Default;
var errors = weights.Validate();
errors.Should().BeEmpty();
}
[Theory]
[InlineData(-0.1)]
[InlineData(1.5)]
[InlineData(double.NaN)]
public void Validate_WithInvalidWeight_ReturnsError(double value)
{
var weights = EvidenceWeights.Default with { Rch = value };
var errors = weights.Validate();
errors.Should().NotBeEmpty();
}
}
public class InMemoryEvidenceWeightPolicyProviderTests
{
[Fact]
public async Task GetPolicyAsync_WithNoStoredPolicy_ReturnsDefault()
{
var provider = new InMemoryEvidenceWeightPolicyProvider();
var policy = await provider.GetPolicyAsync(null, "production");
policy.Should().NotBeNull();
policy.Profile.Should().Be("production");
}
[Fact]
public async Task GetPolicyAsync_WithStoredPolicy_ReturnsStored()
{
var provider = new InMemoryEvidenceWeightPolicyProvider();
var customPolicy = new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "production",
Weights = new EvidenceWeights { Rch = 0.5, Rts = 0.2, Bkp = 0.1, Xpl = 0.1, Src = 0.05, Mit = 0.05 }
};
provider.SetPolicy(customPolicy);
var policy = await provider.GetPolicyAsync(null, "production");
policy.Weights.Rch.Should().Be(0.5);
}
[Fact]
public async Task GetPolicyAsync_WithTenantPolicy_ReturnsTenantSpecific()
{
var provider = new InMemoryEvidenceWeightPolicyProvider();
var tenantPolicy = new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "production",
TenantId = "tenant-123",
Weights = new EvidenceWeights { Rch = 0.6, Rts = 0.2, Bkp = 0.1, Xpl = 0.05, Src = 0.025, Mit = 0.025 }
};
provider.SetPolicy(tenantPolicy);
var policy = await provider.GetPolicyAsync("tenant-123", "production");
policy.Weights.Rch.Should().Be(0.6);
}
[Fact]
public async Task GetPolicyAsync_WithTenantFallsBackToGlobal()
{
var provider = new InMemoryEvidenceWeightPolicyProvider();
var globalPolicy = new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "production",
Weights = new EvidenceWeights { Rch = 0.4, Rts = 0.3, Bkp = 0.1, Xpl = 0.1, Src = 0.05, Mit = 0.05 }
};
provider.SetPolicy(globalPolicy);
var policy = await provider.GetPolicyAsync("unknown-tenant", "production");
policy.Weights.Rch.Should().Be(0.4);
}
[Fact]
public async Task PolicyExistsAsync_WithStoredPolicy_ReturnsTrue()
{
var provider = new InMemoryEvidenceWeightPolicyProvider();
provider.SetPolicy(EvidenceWeightPolicy.DefaultProduction);
var exists = await provider.PolicyExistsAsync(null, "production");
exists.Should().BeTrue();
}
[Fact]
public async Task PolicyExistsAsync_WithNoPolicy_ReturnsFalse()
{
var provider = new InMemoryEvidenceWeightPolicyProvider();
var exists = await provider.PolicyExistsAsync("tenant-xyz", "staging");
exists.Should().BeFalse();
}
[Fact]
public void RemovePolicy_RemovesStoredPolicy()
{
var provider = new InMemoryEvidenceWeightPolicyProvider();
provider.SetPolicy(EvidenceWeightPolicy.DefaultProduction);
var removed = provider.RemovePolicy(null, "production");
removed.Should().BeTrue();
}
[Fact]
public void Clear_RemovesAllPolicies()
{
var provider = new InMemoryEvidenceWeightPolicyProvider();
provider.SetPolicy(new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "production",
Weights = EvidenceWeights.Default
});
provider.SetPolicy(new EvidenceWeightPolicy
{
Version = "ews.v1",
Profile = "development",
Weights = EvidenceWeights.Default
});
provider.Clear();
provider.PolicyExistsAsync(null, "production").Result.Should().BeFalse();
provider.PolicyExistsAsync(null, "development").Result.Should().BeFalse();
}
}

View File

@@ -0,0 +1,358 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
using FluentAssertions;
using StellaOps.Signals.EvidenceWeightedScore;
using Xunit;
namespace StellaOps.Signals.Tests.EvidenceWeightedScore;
public class EvidenceWeightedScoreCalculatorTests
{
private readonly EvidenceWeightedScoreCalculator _calculator = new();
private readonly EvidenceWeightPolicy _defaultPolicy = EvidenceWeightPolicy.DefaultProduction;
[Fact]
public void Calculate_WithAllZeros_ReturnsZeroScore()
{
var input = CreateInput(0, 0, 0, 0, 0, 0);
var result = _calculator.Calculate(input, _defaultPolicy);
result.Score.Should().Be(0);
result.Bucket.Should().Be(ScoreBucket.Watchlist);
}
[Fact]
public void Calculate_WithAllOnes_ReturnsNearMaxScore()
{
var input = CreateInput(1, 1, 1, 1, 1, 0); // MIT=0 to get max
var result = _calculator.Calculate(input, _defaultPolicy);
// Without MIT, sum of weights = 0.95 (default) → 95%
result.Score.Should().BeGreaterOrEqualTo(90);
result.Bucket.Should().Be(ScoreBucket.ActNow);
}
[Fact]
public void Calculate_WithHighMit_ReducesScore()
{
var inputNoMit = CreateInput(0.8, 0.8, 0.5, 0.5, 0.5, 0);
var inputWithMit = CreateInput(0.8, 0.8, 0.5, 0.5, 0.5, 1.0);
var resultNoMit = _calculator.Calculate(inputNoMit, _defaultPolicy);
var resultWithMit = _calculator.Calculate(inputWithMit, _defaultPolicy);
resultWithMit.Score.Should().BeLessThan(resultNoMit.Score);
}
[Fact]
public void Calculate_ReturnsCorrectFindingId()
{
var input = CreateInput(0.5, 0.5, 0.5, 0.5, 0.5, 0.1, "CVE-2024-1234@pkg:npm/test@1.0.0");
var result = _calculator.Calculate(input, _defaultPolicy);
result.FindingId.Should().Be("CVE-2024-1234@pkg:npm/test@1.0.0");
}
[Fact]
public void Calculate_ReturnsCorrectInputsEcho()
{
var input = CreateInput(0.7, 0.6, 0.5, 0.4, 0.3, 0.2);
var result = _calculator.Calculate(input, _defaultPolicy);
result.Inputs.Rch.Should().Be(0.7);
result.Inputs.Rts.Should().Be(0.6);
result.Inputs.Bkp.Should().Be(0.5);
result.Inputs.Xpl.Should().Be(0.4);
result.Inputs.Src.Should().Be(0.3);
result.Inputs.Mit.Should().Be(0.2);
}
[Fact]
public void Calculate_ReturnsBreakdown()
{
var input = CreateInput(0.8, 0.6, 0.4, 0.3, 0.2, 0.1);
var result = _calculator.Calculate(input, _defaultPolicy);
result.Breakdown.Should().HaveCount(6);
result.Breakdown.Should().Contain(d => d.Symbol == "RCH");
result.Breakdown.Should().Contain(d => d.Symbol == "MIT" && d.IsSubtractive);
}
[Fact]
public void Calculate_ReturnsFlags()
{
var input = CreateInput(0.8, 0.7, 0.5, 0.6, 0.5, 0.1);
var result = _calculator.Calculate(input, _defaultPolicy);
result.Flags.Should().Contain("live-signal"); // RTS >= 0.6
result.Flags.Should().Contain("proven-path"); // RCH >= 0.7 && RTS >= 0.5
result.Flags.Should().Contain("high-epss"); // XPL >= 0.5
}
[Fact]
public void Calculate_ReturnsExplanations()
{
var input = CreateInput(0.9, 0.8, 0.5, 0.5, 0.5, 0.1);
var result = _calculator.Calculate(input, _defaultPolicy);
result.Explanations.Should().NotBeEmpty();
result.Explanations.Should().Contain(e => e.Contains("Reachability"));
}
[Fact]
public void Calculate_ReturnsPolicyDigest()
{
var input = CreateInput(0.5, 0.5, 0.5, 0.5, 0.5, 0.1);
var result = _calculator.Calculate(input, _defaultPolicy);
result.PolicyDigest.Should().NotBeNullOrEmpty();
result.PolicyDigest.Should().Be(_defaultPolicy.ComputeDigest());
}
[Fact]
public void Calculate_ReturnsTimestamp()
{
var input = CreateInput(0.5, 0.5, 0.5, 0.5, 0.5, 0.1);
var before = DateTimeOffset.UtcNow;
var result = _calculator.Calculate(input, _defaultPolicy);
result.CalculatedAt.Should().BeOnOrAfter(before);
}
[Fact]
public void Calculate_ClampsOutOfRangeInputs()
{
var input = new EvidenceWeightedScoreInput
{
FindingId = "test",
Rch = 1.5, // Out of range
Rts = -0.3, // Out of range
Bkp = 0.5,
Xpl = 0.5,
Src = 0.5,
Mit = 0.1
};
var result = _calculator.Calculate(input, _defaultPolicy);
result.Inputs.Rch.Should().Be(1.0);
result.Inputs.Rts.Should().Be(0.0);
}
[Theory]
[InlineData(0, ScoreBucket.Watchlist)]
[InlineData(39, ScoreBucket.Watchlist)]
[InlineData(40, ScoreBucket.Investigate)]
[InlineData(69, ScoreBucket.Investigate)]
[InlineData(70, ScoreBucket.ScheduleNext)]
[InlineData(89, ScoreBucket.ScheduleNext)]
[InlineData(90, ScoreBucket.ActNow)]
[InlineData(100, ScoreBucket.ActNow)]
public void GetBucket_ReturnsCorrectBucket(int score, ScoreBucket expected)
{
var bucket = EvidenceWeightedScoreCalculator.GetBucket(score, BucketThresholds.Default);
bucket.Should().Be(expected);
}
// Guardrail Tests
[Fact]
public void Calculate_SpeculativeCapApplied_WhenNoReachabilityOrRuntime()
{
// Use high values for other dimensions to get a score > 45, but Rch=0 and Rts=0
// to trigger the speculative cap. We use a custom policy with very low Rch/Rts weight
// so other dimensions drive the score high enough to cap.
var policyWithLowRchRtsWeight = new EvidenceWeightPolicy
{
Profile = "test-speculative",
Version = "ews.v1",
Weights = new EvidenceWeights
{
Rch = 0.05, // Very low weight
Rts = 0.05, // Very low weight
Bkp = 0.30, // High weight
Xpl = 0.30, // High weight
Src = 0.20, // High weight
Mit = 0.05
}
};
// With Rch=0, Rts=0 but Bkp=1.0, Xpl=1.0, Src=1.0:
// Score = 0*0.05 + 0*0.05 + 1*0.30 + 1*0.30 + 1*0.20 - 0*0.05 = 0.80 * 100 = 80
// This should be capped to 45
var input = CreateInput(0, 0, 1.0, 1.0, 1.0, 0);
var result = _calculator.Calculate(input, policyWithLowRchRtsWeight);
result.Score.Should().Be(45);
result.Caps.SpeculativeCap.Should().BeTrue();
result.Flags.Should().Contain("speculative");
}
[Fact]
public void Calculate_NotAffectedCapApplied_WhenVendorSaysNotAffected()
{
var input = new EvidenceWeightedScoreInput
{
FindingId = "test",
Rch = 0.8,
Rts = 0.3, // Below 0.6
Bkp = 1.0, // Vendor backport proof
Xpl = 0.5,
Src = 0.8,
Mit = 0,
VexStatus = "not_affected"
};
var result = _calculator.Calculate(input, _defaultPolicy);
result.Score.Should().BeLessOrEqualTo(15);
result.Caps.NotAffectedCap.Should().BeTrue();
result.Flags.Should().Contain("vendor-na");
}
[Fact]
public void Calculate_RuntimeFloorApplied_WhenStrongLiveSignal()
{
var input = CreateInput(0.1, 0.9, 0.1, 0.1, 0.1, 0.1);
var result = _calculator.Calculate(input, _defaultPolicy);
result.Score.Should().BeGreaterOrEqualTo(60);
result.Caps.RuntimeFloor.Should().BeTrue();
}
[Fact]
public void Calculate_GuardrailsAppliedInOrder_CapsBeforeFloors()
{
// Scenario: speculative cap should apply first, but runtime floor would override
var input = CreateInput(0, 0.85, 0.5, 0.5, 0.5, 0);
var result = _calculator.Calculate(input, _defaultPolicy);
// Since RTS >= 0.8, runtime floor should apply (floor at 60)
result.Score.Should().BeGreaterOrEqualTo(60);
result.Caps.RuntimeFloor.Should().BeTrue();
// Speculative cap shouldn't apply because RTS > 0
result.Caps.SpeculativeCap.Should().BeFalse();
}
[Fact]
public void Calculate_NoGuardrailsApplied_WhenNotTriggered()
{
var input = CreateInput(0.5, 0.5, 0.5, 0.5, 0.5, 0.1);
var result = _calculator.Calculate(input, _defaultPolicy);
result.Caps.AnyApplied.Should().BeFalse();
result.Caps.OriginalScore.Should().Be(result.Caps.AdjustedScore);
}
// Determinism Tests
[Fact]
public void Calculate_IsDeterministic_SameInputsSameResult()
{
var input = CreateInput(0.7, 0.6, 0.5, 0.4, 0.3, 0.2);
var result1 = _calculator.Calculate(input, _defaultPolicy);
var result2 = _calculator.Calculate(input, _defaultPolicy);
result1.Score.Should().Be(result2.Score);
result1.PolicyDigest.Should().Be(result2.PolicyDigest);
}
[Fact]
public void Calculate_IsDeterministic_WithDifferentCalculatorInstances()
{
var calc1 = new EvidenceWeightedScoreCalculator();
var calc2 = new EvidenceWeightedScoreCalculator();
var input = CreateInput(0.7, 0.6, 0.5, 0.4, 0.3, 0.2);
var result1 = calc1.Calculate(input, _defaultPolicy);
var result2 = calc2.Calculate(input, _defaultPolicy);
result1.Score.Should().Be(result2.Score);
}
// Edge Cases
[Fact]
public void Calculate_HandlesNullDetailInputs()
{
var input = new EvidenceWeightedScoreInput
{
FindingId = "test",
Rch = 0.5,
Rts = 0.5,
Bkp = 0.5,
Xpl = 0.5,
Src = 0.5,
Mit = 0.1,
ReachabilityDetails = null,
RuntimeDetails = null,
BackportDetails = null,
ExploitDetails = null,
SourceTrustDetails = null,
MitigationDetails = null
};
var result = _calculator.Calculate(input, _defaultPolicy);
result.Should().NotBeNull();
result.Score.Should().BeGreaterOrEqualTo(0);
}
[Fact]
public void Calculate_WithDetailedInputs_IncludesThemInExplanations()
{
var input = new EvidenceWeightedScoreInput
{
FindingId = "test",
Rch = 0.8,
Rts = 0.7,
Bkp = 0.5,
Xpl = 0.5,
Src = 0.5,
Mit = 0.1,
ReachabilityDetails = new ReachabilityInput
{
State = ReachabilityState.StaticReachable,
Confidence = 0.8,
HopCount = 2
}
};
var result = _calculator.Calculate(input, _defaultPolicy);
result.Explanations.Should().Contain(e => e.Contains("Statically reachable"));
}
// Helper
private static EvidenceWeightedScoreInput CreateInput(
double rch, double rts, double bkp, double xpl, double src, double mit, string findingId = "test")
{
return new EvidenceWeightedScoreInput
{
FindingId = findingId,
Rch = rch,
Rts = rts,
Bkp = bkp,
Xpl = xpl,
Src = src,
Mit = mit
};
}
}

View File

@@ -0,0 +1,179 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
using FluentAssertions;
using StellaOps.Signals.EvidenceWeightedScore;
using Xunit;
namespace StellaOps.Signals.Tests.EvidenceWeightedScore;
public class EvidenceWeightedScoreInputTests
{
[Fact]
public void Validate_WithValidInput_ReturnsNoErrors()
{
// Arrange
var input = CreateValidInput();
// Act
var errors = input.Validate();
// Assert
errors.Should().BeEmpty();
}
[Theory]
[InlineData(-0.1, "Rch")]
[InlineData(1.1, "Rch")]
[InlineData(double.NaN, "Rch")]
[InlineData(double.PositiveInfinity, "Rch")]
[InlineData(double.NegativeInfinity, "Rch")]
public void Validate_WithInvalidRch_ReturnsError(double value, string dimension)
{
// Arrange
var input = CreateValidInput() with { Rch = value };
// Act
var errors = input.Validate();
// Assert
errors.Should().ContainSingle(e => e.Contains(dimension));
}
[Theory]
[InlineData(-0.6)] // 0.5 + -0.6 = -0.1 (invalid)
[InlineData(0.6)] // 0.5 + 0.6 = 1.1 (invalid)
public void Validate_WithInvalidDimensions_ReturnsMultipleErrors(double offset)
{
// Arrange
var input = CreateValidInput() with
{
Rch = 0.5 + offset,
Rts = 0.5 + offset,
Bkp = 0.5 + offset
};
// Act
var errors = input.Validate();
// Assert
errors.Should().HaveCount(3);
}
[Fact]
public void Validate_WithEmptyFindingId_ReturnsError()
{
// Arrange
var input = CreateValidInput() with { FindingId = "" };
// Act
var errors = input.Validate();
// Assert
errors.Should().ContainSingle(e => e.Contains("FindingId"));
}
[Fact]
public void Clamp_WithOutOfRangeValues_ReturnsClampedInput()
{
// Arrange
var input = new EvidenceWeightedScoreInput
{
FindingId = "CVE-2024-1234@pkg:npm/test@1.0.0",
Rch = 1.5,
Rts = -0.3,
Bkp = 0.5,
Xpl = double.PositiveInfinity,
Src = double.NaN,
Mit = 2.0
};
// Act
var clamped = input.Clamp();
// Assert
clamped.Rch.Should().Be(1.0);
clamped.Rts.Should().Be(0.0);
clamped.Bkp.Should().Be(0.5);
clamped.Xpl.Should().Be(1.0);
clamped.Src.Should().Be(0.0);
clamped.Mit.Should().Be(1.0);
}
[Fact]
public void Clamp_PreservesValidValues()
{
// Arrange
var input = CreateValidInput();
// Act
var clamped = input.Clamp();
// Assert
clamped.Should().BeEquivalentTo(input);
}
[Theory]
[InlineData(0.0)]
[InlineData(0.5)]
[InlineData(1.0)]
public void Validate_WithBoundaryValues_ReturnsNoErrors(double value)
{
// Arrange
var input = new EvidenceWeightedScoreInput
{
FindingId = "CVE-2024-1234@pkg:npm/test@1.0.0",
Rch = value,
Rts = value,
Bkp = value,
Xpl = value,
Src = value,
Mit = value
};
// Act
var errors = input.Validate();
// Assert
errors.Should().BeEmpty();
}
[Fact]
public void Input_WithDetailedInputs_PreservesAllProperties()
{
// Arrange
var input = CreateValidInput() with
{
VexStatus = "not_affected",
ReachabilityDetails = new ReachabilityInput
{
State = ReachabilityState.StaticReachable,
Confidence = 0.8
},
RuntimeDetails = new RuntimeInput
{
Posture = RuntimePosture.EbpfDeep,
ObservationCount = 10,
RecencyFactor = 0.9
}
};
// Assert
input.VexStatus.Should().Be("not_affected");
input.ReachabilityDetails.Should().NotBeNull();
input.ReachabilityDetails!.State.Should().Be(ReachabilityState.StaticReachable);
input.RuntimeDetails.Should().NotBeNull();
input.RuntimeDetails!.Posture.Should().Be(RuntimePosture.EbpfDeep);
}
private static EvidenceWeightedScoreInput CreateValidInput() => new()
{
FindingId = "CVE-2024-1234@pkg:npm/test@1.0.0",
Rch = 0.7,
Rts = 0.5,
Bkp = 0.3,
Xpl = 0.4,
Src = 0.6,
Mit = 0.2
};
}

View File

@@ -0,0 +1,290 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright © 2025 StellaOps
using FluentAssertions;
using StellaOps.Signals.EvidenceWeightedScore;
using Xunit;
namespace StellaOps.Signals.Tests.EvidenceWeightedScore;
/// <summary>
/// Property-style tests for score calculation invariants using exhaustive sampling.
/// Uses deterministic sample sets rather than random generation for reproducibility.
/// </summary>
public class EvidenceWeightedScorePropertyTests
{
private static readonly EvidenceWeightedScoreCalculator Calculator = new();
private static readonly EvidenceWeightPolicy Policy = EvidenceWeightPolicy.DefaultProduction;
// Sample grid values for exhaustive testing
private static readonly double[] SampleValues = [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0];
public static IEnumerable<object[]> GetBoundaryTestCases()
{
foreach (var rch in SampleValues)
foreach (var xpl in SampleValues)
foreach (var mit in new[] { 0.0, 0.5, 1.0 })
{
yield return [rch, 0.5, 0.5, xpl, 0.5, mit];
}
}
public static IEnumerable<object[]> GetDeterminismTestCases()
{
yield return [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
yield return [1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
yield return [0.5, 0.5, 0.5, 0.5, 0.5, 0.5];
yield return [0.33, 0.66, 0.25, 0.75, 0.1, 0.9];
yield return [0.123, 0.456, 0.789, 0.012, 0.345, 0.678];
}
public static IEnumerable<object[]> GetMonotonicityTestCases()
{
// Pairs where (base, increment) for increasing input tests
foreach (var baseVal in new[] { 0.1, 0.3, 0.5, 0.7 })
foreach (var increment in new[] { 0.05, 0.1, 0.2 })
{
if (baseVal + increment <= 1.0)
{
yield return [baseVal, increment];
}
}
}
public static IEnumerable<object[]> GetMitigationMonotonicityTestCases()
{
foreach (var mit1 in new[] { 0.0, 0.2, 0.4 })
foreach (var mit2 in new[] { 0.5, 0.7, 0.9 })
{
if (mit1 < mit2)
{
yield return [mit1, mit2];
}
}
}
[Theory]
[MemberData(nameof(GetBoundaryTestCases))]
public void Score_IsAlwaysBetween0And100(double rch, double rts, double bkp, double xpl, double src, double mit)
{
var input = CreateInput(rch, rts, bkp, xpl, src, mit);
var result = Calculator.Calculate(input, Policy);
result.Score.Should().BeGreaterThanOrEqualTo(0);
result.Score.Should().BeLessThanOrEqualTo(100);
}
[Theory]
[MemberData(nameof(GetBoundaryTestCases))]
public void GuardrailsNeverProduceScoreOutsideBounds(double rch, double rts, double bkp, double xpl, double src, double mit)
{
var input = CreateInput(rch, rts, bkp, xpl, src, mit);
var result = Calculator.Calculate(input, Policy);
result.Caps.AdjustedScore.Should().BeGreaterThanOrEqualTo(0);
result.Caps.AdjustedScore.Should().BeLessThanOrEqualTo(100);
}
[Theory]
[MemberData(nameof(GetDeterminismTestCases))]
public void DeterminismProperty_SameInputsSameScore(double rch, double rts, double bkp, double xpl, double src, double mit)
{
var input1 = CreateInput(rch, rts, bkp, xpl, src, mit);
var input2 = CreateInput(rch, rts, bkp, xpl, src, mit);
var result1 = Calculator.Calculate(input1, Policy);
var result2 = Calculator.Calculate(input2, Policy);
result1.Score.Should().Be(result2.Score);
result1.PolicyDigest.Should().Be(result2.PolicyDigest);
}
[Fact]
public void DeterminismProperty_MultipleCalculationsProduceSameResult()
{
var input = CreateInput(0.7, 0.6, 0.5, 0.4, 0.3, 0.2);
var results = Enumerable.Range(0, 100)
.Select(_ => Calculator.Calculate(input, Policy))
.ToList();
var firstScore = results[0].Score;
results.Should().AllSatisfy(r => r.Score.Should().Be(firstScore));
}
[Theory]
[MemberData(nameof(GetMonotonicityTestCases))]
public void IncreasingInputs_IncreaseOrMaintainScore_WhenNoGuardrails(double baseValue, double increment)
{
// Use mid-range values that won't trigger guardrails
var input1 = CreateInput(baseValue, 0.5, 0.3, 0.3, 0.3, 0.1);
var input2 = CreateInput(baseValue + increment, 0.5, 0.3, 0.3, 0.3, 0.1);
var result1 = Calculator.Calculate(input1, Policy);
var result2 = Calculator.Calculate(input2, Policy);
// If no guardrails triggered on either, higher input should give >= score
if (!result1.Caps.AnyApplied && !result2.Caps.AnyApplied)
{
result2.Score.Should().BeGreaterThanOrEqualTo(result1.Score,
"increasing reachability input should increase or maintain score when no guardrails apply");
}
}
[Theory]
[MemberData(nameof(GetMitigationMonotonicityTestCases))]
public void IncreasingMit_DecreasesOrMaintainsScore(double mitLow, double mitHigh)
{
var inputLowMit = CreateInput(0.5, 0.5, 0.5, 0.5, 0.5, mitLow);
var inputHighMit = CreateInput(0.5, 0.5, 0.5, 0.5, 0.5, mitHigh);
var resultLowMit = Calculator.Calculate(inputLowMit, Policy);
var resultHighMit = Calculator.Calculate(inputHighMit, Policy);
resultHighMit.Score.Should().BeLessThanOrEqualTo(resultLowMit.Score,
"higher mitigation should result in lower or equal score");
}
[Theory]
[MemberData(nameof(GetBoundaryTestCases))]
public void BucketMatchesScore(double rch, double rts, double bkp, double xpl, double src, double mit)
{
var input = CreateInput(rch, rts, bkp, xpl, src, mit);
var result = Calculator.Calculate(input, Policy);
var expectedBucket = result.Score switch
{
>= 90 => ScoreBucket.ActNow,
>= 70 => ScoreBucket.ScheduleNext,
>= 40 => ScoreBucket.Investigate,
_ => ScoreBucket.Watchlist
};
result.Bucket.Should().Be(expectedBucket);
}
[Theory]
[MemberData(nameof(GetDeterminismTestCases))]
public void BreakdownHasCorrectDimensions(double rch, double rts, double bkp, double xpl, double src, double mit)
{
var input = CreateInput(rch, rts, bkp, xpl, src, mit);
var result = Calculator.Calculate(input, Policy);
result.Breakdown.Should().HaveCount(6);
result.Breakdown.Should().Contain(d => d.Symbol == "RCH");
result.Breakdown.Should().Contain(d => d.Symbol == "RTS");
result.Breakdown.Should().Contain(d => d.Symbol == "BKP");
result.Breakdown.Should().Contain(d => d.Symbol == "XPL");
result.Breakdown.Should().Contain(d => d.Symbol == "SRC");
result.Breakdown.Should().Contain(d => d.Symbol == "MIT" && d.IsSubtractive);
}
[Theory]
[MemberData(nameof(GetDeterminismTestCases))]
public void BreakdownContributionsSumApproximately(double rch, double rts, double bkp, double xpl, double src, double mit)
{
var input = CreateInput(rch, rts, bkp, xpl, src, mit);
var result = Calculator.Calculate(input, Policy);
var positiveSum = result.Breakdown
.Where(d => !d.IsSubtractive)
.Sum(d => d.Contribution);
var negativeSum = result.Breakdown
.Where(d => d.IsSubtractive)
.Sum(d => d.Contribution);
var netSum = positiveSum - negativeSum;
// Each contribution should be in valid range
foreach (var contrib in result.Breakdown)
{
contrib.Contribution.Should().BeGreaterThanOrEqualTo(0);
contrib.Contribution.Should().BeLessThanOrEqualTo(contrib.Weight * 1.01); // Allow small float tolerance
}
// Net should be non-negative and produce the score (approximately)
netSum.Should().BeGreaterThanOrEqualTo(0);
// The score should be approximately 100 * netSum (before guardrails)
var expectedRawScore = (int)Math.Round(netSum * 100);
result.Caps.OriginalScore.Should().BeCloseTo(expectedRawScore, 2);
}
[Fact]
public void AllZeroInputs_ProducesZeroScore()
{
var input = CreateInput(0, 0, 0, 0, 0, 0);
var result = Calculator.Calculate(input, Policy);
result.Score.Should().Be(0);
result.Bucket.Should().Be(ScoreBucket.Watchlist);
}
[Fact]
public void AllMaxInputs_WithZeroMitigation_ProducesHighScore()
{
var input = CreateInput(1.0, 1.0, 1.0, 1.0, 1.0, 0.0);
var result = Calculator.Calculate(input, Policy);
result.Score.Should().BeGreaterThan(80, "max positive inputs with no mitigation should produce high score");
}
[Fact]
public void MaxMitigation_SignificantlyReducesScore()
{
var inputNoMit = CreateInput(0.8, 0.8, 0.8, 0.8, 0.8, 0.0);
var inputMaxMit = CreateInput(0.8, 0.8, 0.8, 0.8, 0.8, 1.0);
var resultNoMit = Calculator.Calculate(inputNoMit, Policy);
var resultMaxMit = Calculator.Calculate(inputMaxMit, Policy);
var reduction = resultNoMit.Score - resultMaxMit.Score;
reduction.Should().BeGreaterThan(5, "max mitigation should significantly reduce score");
}
[Fact]
public void PolicyDigest_IsConsistentAcrossCalculations()
{
var input = CreateInput(0.5, 0.5, 0.5, 0.5, 0.5, 0.5);
var result1 = Calculator.Calculate(input, Policy);
var result2 = Calculator.Calculate(input, Policy);
result1.PolicyDigest.Should().Be(result2.PolicyDigest);
result1.PolicyDigest.Should().Be(Policy.ComputeDigest());
}
[Fact]
public void DifferentPolicies_ProduceDifferentDigests()
{
var policy2 = new EvidenceWeightPolicy
{
Profile = "different-policy",
Version = "ews.v2",
Weights = new EvidenceWeights
{
Rch = 0.40, // Different from default 0.30
Rts = 0.25,
Bkp = 0.15,
Xpl = 0.10, // Different from default 0.15
Src = 0.05, // Different from default 0.10
Mit = 0.05 // Different from default 0.10
}
};
Policy.ComputeDigest().Should().NotBe(policy2.ComputeDigest());
}
private static EvidenceWeightedScoreInput CreateInput(
double rch, double rts, double bkp, double xpl, double src, double mit)
{
return new EvidenceWeightedScoreInput
{
FindingId = "property-test",
Rch = rch,
Rts = rts,
Bkp = bkp,
Xpl = xpl,
Src = src,
Mit = mit
};
}
}

View File

@@ -15,6 +15,11 @@
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<!-- FsCheck for property-based testing (EvidenceWeightedScore) -->
<PackageReference Include="FsCheck" Version="3.0.0-rc3" />
<PackageReference Include="FsCheck.Xunit" Version="3.0.0-rc3" />
<!-- Verify for snapshot testing (EvidenceWeightedScore) -->
<PackageReference Include="Verify.Xunit" Version="28.7.2" />
</ItemGroup>
<ItemGroup>