doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements
This commit is contained in:
@@ -137,6 +137,54 @@ public class EvidenceWeightPolicyTests
|
||||
json.Should().Contain("\"weights\"");
|
||||
json.Should().Contain("\"guardrails\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCanonicalJson_IsDeterministic_100Iterations()
|
||||
{
|
||||
// TASK-028-002: Add determinism test - serialize 100x → all identical
|
||||
var policy = EvidenceWeightPolicy.DefaultProduction;
|
||||
|
||||
var results = Enumerable.Range(0, 100)
|
||||
.Select(_ => policy.GetCanonicalJson())
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
results.Should().ContainSingle("All 100 serializations should produce identical output");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_IsDeterministic_100Iterations()
|
||||
{
|
||||
// TASK-028-002: Add determinism test - hash 100x → all identical
|
||||
var policy = EvidenceWeightPolicy.DefaultProduction;
|
||||
|
||||
var digests = Enumerable.Range(0, 100)
|
||||
.Select(_ =>
|
||||
{
|
||||
// Create fresh policy instance each time to avoid caching
|
||||
var freshPolicy = new EvidenceWeightPolicy
|
||||
{
|
||||
Version = "ews.v1",
|
||||
Profile = "production",
|
||||
Weights = EvidenceWeights.Default
|
||||
};
|
||||
return freshPolicy.ComputeDigest();
|
||||
})
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
digests.Should().ContainSingle("All 100 digest computations should produce identical output");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_ProducesHexString()
|
||||
{
|
||||
var policy = EvidenceWeightPolicy.DefaultProduction;
|
||||
|
||||
var digest = policy.ComputeDigest();
|
||||
|
||||
digest.Should().MatchRegex("^[0-9a-f]{64}$", "Digest should be 64-character lowercase hex string");
|
||||
}
|
||||
}
|
||||
|
||||
public class EvidenceWeightsTests
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) 2026 StellaOps
|
||||
// Sprint: SPRINT_20260118_029_LIB_scoring_dimensions_expansion
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests.EvidenceWeightedScore;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the advisory formula scoring (SPRINT-029).
|
||||
/// Formula: raw = 0.25*cvss + 0.30*epss + 0.20*reachability + 0.10*exploit_maturity - 0.15*patch_proof
|
||||
/// </summary>
|
||||
public class EvidenceWeightedScoreAdvisoryFormulaTests
|
||||
{
|
||||
private readonly EvidenceWeightedScoreCalculator _calculator = new();
|
||||
private readonly EvidenceWeightPolicy _advisoryPolicy = EvidenceWeightPolicy.AdvisoryProduction;
|
||||
|
||||
#region TASK-029-001: CVSS Base Score Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.0, 0.0)] // Min CVSS
|
||||
[InlineData(5.0, 0.5)] // Mid CVSS
|
||||
[InlineData(10.0, 1.0)] // Max CVSS
|
||||
[InlineData(7.5, 0.75)] // High CVSS
|
||||
public void CvssBase_NormalizesToZeroToOne(double cvssBase, double expectedNormalized)
|
||||
{
|
||||
var input = CreateAdvisoryInput(cvssBase: cvssBase);
|
||||
|
||||
var normalized = input.GetNormalizedCvss();
|
||||
|
||||
normalized.Should().BeApproximately(expectedNormalized, 0.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvssBase_ClampsToValidRange()
|
||||
{
|
||||
var inputOver = CreateAdvisoryInput(cvssBase: 15.0);
|
||||
var inputUnder = CreateAdvisoryInput(cvssBase: -5.0);
|
||||
|
||||
var clampedOver = inputOver.Clamp();
|
||||
var clampedUnder = inputUnder.Clamp();
|
||||
|
||||
clampedOver.CvssBase.Should().Be(10.0);
|
||||
clampedUnder.CvssBase.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CvssBase_ContributesToAdvisoryFormula()
|
||||
{
|
||||
var lowCvss = CreateAdvisoryInput(cvssBase: 3.0, epss: 0.5, reachability: 0.5);
|
||||
var highCvss = CreateAdvisoryInput(cvssBase: 9.5, epss: 0.5, reachability: 0.5);
|
||||
|
||||
var resultLow = _calculator.Calculate(lowCvss, _advisoryPolicy);
|
||||
var resultHigh = _calculator.Calculate(highCvss, _advisoryPolicy);
|
||||
|
||||
resultHigh.Score.Should().BeGreaterThan(resultLow.Score);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HighCvss_GeneratesFlag()
|
||||
{
|
||||
var input = CreateAdvisoryInput(cvssBase: 8.5);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
result.Flags.Should().Contain("high-cvss");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TASK-029-002: Exploit Maturity Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(ExploitMaturityLevel.Unknown, 0.0)]
|
||||
[InlineData(ExploitMaturityLevel.None, 0.0)]
|
||||
[InlineData(ExploitMaturityLevel.ProofOfConcept, 0.6)]
|
||||
[InlineData(ExploitMaturityLevel.Functional, 0.8)]
|
||||
[InlineData(ExploitMaturityLevel.High, 1.0)]
|
||||
public void ExploitMaturity_NormalizesCorrectly(ExploitMaturityLevel level, double expectedNormalized)
|
||||
{
|
||||
var normalized = EvidenceWeightedScoreInput.NormalizeExploitMaturity(level);
|
||||
|
||||
normalized.Should().BeApproximately(expectedNormalized, 0.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExploitMaturity_ContributesToAdvisoryFormula()
|
||||
{
|
||||
var noExploit = CreateAdvisoryInput(cvssBase: 7.0, exploitMaturity: ExploitMaturityLevel.None);
|
||||
var highExploit = CreateAdvisoryInput(cvssBase: 7.0, exploitMaturity: ExploitMaturityLevel.High);
|
||||
|
||||
var resultNone = _calculator.Calculate(noExploit, _advisoryPolicy);
|
||||
var resultHigh = _calculator.Calculate(highExploit, _advisoryPolicy);
|
||||
|
||||
resultHigh.Score.Should().BeGreaterThan(resultNone.Score);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveExploitation_GeneratesFlag()
|
||||
{
|
||||
var input = CreateAdvisoryInput(cvssBase: 7.0, exploitMaturity: ExploitMaturityLevel.High);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
result.Flags.Should().Contain("active-exploitation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownExploit_GeneratesFlag()
|
||||
{
|
||||
var input = CreateAdvisoryInput(cvssBase: 7.0, exploitMaturity: ExploitMaturityLevel.ProofOfConcept);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
result.Flags.Should().Contain("known-exploit");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TASK-029-003: Patch Proof Confidence Tests
|
||||
|
||||
[Fact]
|
||||
public void PatchProofConfidence_IsSubtractive()
|
||||
{
|
||||
var noPatch = CreateAdvisoryInput(cvssBase: 8.0, epss: 0.6, patchProofConfidence: 0.0);
|
||||
var withPatch = CreateAdvisoryInput(cvssBase: 8.0, epss: 0.6, patchProofConfidence: 1.0);
|
||||
|
||||
var resultNoPatch = _calculator.Calculate(noPatch, _advisoryPolicy);
|
||||
var resultWithPatch = _calculator.Calculate(withPatch, _advisoryPolicy);
|
||||
|
||||
resultWithPatch.Score.Should().BeLessThan(resultNoPatch.Score);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PatchProofConfidence_ClampsToValidRange()
|
||||
{
|
||||
var inputOver = CreateAdvisoryInput(patchProofConfidence: 1.5);
|
||||
var inputUnder = CreateAdvisoryInput(patchProofConfidence: -0.5);
|
||||
|
||||
var clampedOver = inputOver.Clamp();
|
||||
var clampedUnder = inputUnder.Clamp();
|
||||
|
||||
clampedOver.PatchProofConfidence.Should().Be(1.0);
|
||||
clampedUnder.PatchProofConfidence.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PatchVerified_GeneratesFlag()
|
||||
{
|
||||
var input = CreateAdvisoryInput(cvssBase: 7.0, patchProofConfidence: 0.9);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
result.Flags.Should().Contain("patch-verified");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PatchProofDetails_ComputesConfidence()
|
||||
{
|
||||
var bothEvidence = new PatchProofDetails
|
||||
{
|
||||
VendorFixedClaim = true,
|
||||
DeltaSigConfidence = 0.95
|
||||
};
|
||||
|
||||
var vendorOnly = new PatchProofDetails
|
||||
{
|
||||
VendorFixedClaim = true,
|
||||
DeltaSigConfidence = 0.3
|
||||
};
|
||||
|
||||
var deltaOnly = new PatchProofDetails
|
||||
{
|
||||
VendorFixedClaim = false,
|
||||
DeltaSigConfidence = 0.8
|
||||
};
|
||||
|
||||
bothEvidence.ComputeConfidence().Should().Be(1.0);
|
||||
vendorOnly.ComputeConfidence().Should().Be(0.7);
|
||||
deltaOnly.ComputeConfidence().Should().Be(0.8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TASK-029-004 & 005: Advisory Formula Tests
|
||||
|
||||
[Fact]
|
||||
public void AdvisoryFormula_UsesCorrectWeights()
|
||||
{
|
||||
var policy = EvidenceWeightPolicy.AdvisoryProduction;
|
||||
|
||||
policy.Weights.Cvss.Should().Be(0.25);
|
||||
policy.Weights.Epss.Should().Be(0.30);
|
||||
policy.Weights.Reachability.Should().Be(0.20);
|
||||
policy.Weights.ExploitMaturity.Should().Be(0.10);
|
||||
policy.Weights.PatchProof.Should().Be(0.15);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvisoryFormula_CalculatesCorrectly()
|
||||
{
|
||||
// Known inputs:
|
||||
// CVSS: 8.0 -> normalized: 0.8
|
||||
// EPSS: 0.5
|
||||
// Reachability: 0.7
|
||||
// Exploit Maturity: Functional -> 0.8
|
||||
// Patch Proof: 0.0
|
||||
// Expected: 0.25*0.8 + 0.30*0.5 + 0.20*0.7 + 0.10*0.8 - 0.15*0.0
|
||||
// = 0.20 + 0.15 + 0.14 + 0.08 - 0.0 = 0.57 -> 57
|
||||
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "test",
|
||||
CvssBase = 8.0,
|
||||
EpssScore = 0.5,
|
||||
ExploitMaturity = ExploitMaturityLevel.Functional,
|
||||
PatchProofConfidence = 0.0,
|
||||
// Legacy fields (required)
|
||||
Rch = 0.7, // Used as reachability
|
||||
Rts = 0.0,
|
||||
Bkp = 0.0,
|
||||
Xpl = 0.0,
|
||||
Src = 0.0,
|
||||
Mit = 0.0
|
||||
};
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
result.Score.Should().BeCloseTo(57, 2); // Allow small rounding difference
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvisoryFormula_ReturnsAdvisoryBreakdown()
|
||||
{
|
||||
var input = CreateAdvisoryInput(cvssBase: 7.0, epss: 0.4, reachability: 0.6);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
result.Breakdown.Should().HaveCount(5); // CVS, EPS, RCH, XPL, PPF
|
||||
result.Breakdown.Should().Contain(d => d.Symbol == "CVS");
|
||||
result.Breakdown.Should().Contain(d => d.Symbol == "EPS");
|
||||
result.Breakdown.Should().Contain(d => d.Symbol == "RCH");
|
||||
result.Breakdown.Should().Contain(d => d.Symbol == "XPL");
|
||||
result.Breakdown.Should().Contain(d => d.Symbol == "PPF" && d.IsSubtractive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyFormula_UsesLegacyBreakdown()
|
||||
{
|
||||
var input = CreateAdvisoryInput(cvssBase: 7.0, epss: 0.4, reachability: 0.6);
|
||||
var legacyPolicy = EvidenceWeightPolicy.DefaultProduction;
|
||||
|
||||
var result = _calculator.Calculate(input, legacyPolicy);
|
||||
|
||||
result.Breakdown.Should().HaveCount(6); // RCH, RTS, BKP, XPL, SRC, MIT
|
||||
result.Breakdown.Should().Contain(d => d.Symbol == "RCH");
|
||||
result.Breakdown.Should().Contain(d => d.Symbol == "MIT" && d.IsSubtractive);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TASK-029-006: VEX Override Tests
|
||||
|
||||
[Fact]
|
||||
public void VexOverride_AppliesWhenAuthoritativeNotAffected()
|
||||
{
|
||||
var input = CreateAdvisoryInput(
|
||||
cvssBase: 9.0,
|
||||
epss: 0.8,
|
||||
reachability: 0.9,
|
||||
vexStatus: "not_affected",
|
||||
vexSource: ".vex/findings.json"
|
||||
);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
result.Score.Should().Be(0);
|
||||
result.Flags.Should().Contain("vex-override");
|
||||
result.Flags.Should().Contain("vendor-na");
|
||||
result.Explanations.Should().Contain(e => e.Contains("Authoritative VEX"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VexOverride_AppliesWhenAuthoritativeFixed()
|
||||
{
|
||||
var input = CreateAdvisoryInput(
|
||||
cvssBase: 8.5,
|
||||
epss: 0.7,
|
||||
vexStatus: "fixed",
|
||||
vexSource: "in-project:vex"
|
||||
);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
result.Score.Should().Be(0);
|
||||
result.Flags.Should().Contain("vex-override");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VexOverride_AppliesWithTrustedKey()
|
||||
{
|
||||
var policyWithTrustedKeys = new EvidenceWeightPolicy
|
||||
{
|
||||
Version = "ews.v2",
|
||||
Profile = "test",
|
||||
Weights = EvidenceWeights.Advisory,
|
||||
FormulaMode = FormulaMode.Advisory,
|
||||
TrustedVexKeys = ["vendor:acme", "vendor:bigcorp"]
|
||||
};
|
||||
|
||||
var input = CreateAdvisoryInput(
|
||||
cvssBase: 9.0,
|
||||
vexStatus: "not_affected",
|
||||
vexSource: "vendor:acme"
|
||||
);
|
||||
|
||||
var result = _calculator.Calculate(input, policyWithTrustedKeys);
|
||||
|
||||
result.Score.Should().Be(0);
|
||||
result.Flags.Should().Contain("vex-override");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VexOverride_AppliesWithWildcardKey()
|
||||
{
|
||||
var policyWithWildcard = new EvidenceWeightPolicy
|
||||
{
|
||||
Version = "ews.v2",
|
||||
Profile = "test",
|
||||
Weights = EvidenceWeights.Advisory,
|
||||
FormulaMode = FormulaMode.Advisory,
|
||||
TrustedVexKeys = ["vendor:*"]
|
||||
};
|
||||
|
||||
var input = CreateAdvisoryInput(
|
||||
cvssBase: 8.0,
|
||||
vexStatus: "fixed",
|
||||
vexSource: "vendor:some-company"
|
||||
);
|
||||
|
||||
var result = _calculator.Calculate(input, policyWithWildcard);
|
||||
|
||||
result.Score.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VexOverride_DoesNotApplyWithoutAuthoritativeSource()
|
||||
{
|
||||
var policyWithTrustedKeys = new EvidenceWeightPolicy
|
||||
{
|
||||
Version = "ews.v2",
|
||||
Profile = "test",
|
||||
Weights = EvidenceWeights.Advisory,
|
||||
FormulaMode = FormulaMode.Advisory,
|
||||
TrustedVexKeys = ["vendor:acme"]
|
||||
};
|
||||
|
||||
var input = CreateAdvisoryInput(
|
||||
cvssBase: 8.0,
|
||||
vexStatus: "not_affected",
|
||||
vexSource: "untrusted:random"
|
||||
);
|
||||
|
||||
var result = _calculator.Calculate(input, policyWithTrustedKeys);
|
||||
|
||||
result.Score.Should().BeGreaterThan(0);
|
||||
result.Flags.Should().NotContain("vex-override");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VexOverride_DoesNotApplyForAffectedStatus()
|
||||
{
|
||||
var input = CreateAdvisoryInput(
|
||||
cvssBase: 8.0,
|
||||
vexStatus: "affected",
|
||||
vexSource: ".vex/findings.json"
|
||||
);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
result.Score.Should().BeGreaterThan(0);
|
||||
result.Flags.Should().NotContain("vex-override");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TASK-029-007: Breakdown Enhancement Tests
|
||||
|
||||
[Fact]
|
||||
public void AdvisoryBreakdown_ContainsAllDimensions()
|
||||
{
|
||||
var input = CreateAdvisoryInput(
|
||||
cvssBase: 7.0,
|
||||
epss: 0.5,
|
||||
reachability: 0.6,
|
||||
exploitMaturity: ExploitMaturityLevel.Functional,
|
||||
patchProofConfidence: 0.3
|
||||
);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
var breakdown = result.Breakdown;
|
||||
breakdown.Should().HaveCount(5);
|
||||
|
||||
var cvs = breakdown.First(d => d.Symbol == "CVS");
|
||||
cvs.Dimension.Should().Be("CVSS Base");
|
||||
cvs.InputValue.Should().BeApproximately(0.7, 0.001); // 7.0/10
|
||||
cvs.Weight.Should().Be(0.25);
|
||||
cvs.Contribution.Should().BeApproximately(0.175, 0.01); // 0.7 * 0.25
|
||||
cvs.IsSubtractive.Should().BeFalse();
|
||||
|
||||
var ppf = breakdown.First(d => d.Symbol == "PPF");
|
||||
ppf.Dimension.Should().Be("Patch Proof");
|
||||
ppf.IsSubtractive.Should().BeTrue();
|
||||
ppf.Contribution.Should().BeLessThan(0); // Negative contribution
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvisoryBreakdown_SumsToRawScore()
|
||||
{
|
||||
var input = CreateAdvisoryInput(
|
||||
cvssBase: 7.0,
|
||||
epss: 0.5,
|
||||
reachability: 0.6,
|
||||
exploitMaturity: ExploitMaturityLevel.ProofOfConcept,
|
||||
patchProofConfidence: 0.2
|
||||
);
|
||||
|
||||
var result = _calculator.Calculate(input, _advisoryPolicy);
|
||||
|
||||
var breakdownSum = result.Breakdown.Sum(d => d.Contribution);
|
||||
var expectedRaw = result.Score / 100.0; // Approximate
|
||||
|
||||
breakdownSum.Should().BeApproximately(expectedRaw, 0.1); // Allow for rounding
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
public void AdvisoryFormula_IsDeterministic()
|
||||
{
|
||||
var input = CreateAdvisoryInput(
|
||||
cvssBase: 8.0,
|
||||
epss: 0.6,
|
||||
reachability: 0.7,
|
||||
exploitMaturity: ExploitMaturityLevel.High,
|
||||
patchProofConfidence: 0.3
|
||||
);
|
||||
|
||||
var results = Enumerable.Range(0, 100)
|
||||
.Select(_ => _calculator.Calculate(input, _advisoryPolicy))
|
||||
.ToList();
|
||||
|
||||
var firstScore = results.First().Score;
|
||||
results.Should().OnlyContain(r => r.Score == firstScore);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static EvidenceWeightedScoreInput CreateAdvisoryInput(
|
||||
double cvssBase = 5.0,
|
||||
double epss = 0.3,
|
||||
double reachability = 0.5,
|
||||
ExploitMaturityLevel exploitMaturity = ExploitMaturityLevel.Unknown,
|
||||
double patchProofConfidence = 0.0,
|
||||
string? vexStatus = null,
|
||||
string? vexSource = null,
|
||||
string findingId = "test")
|
||||
{
|
||||
return new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = findingId,
|
||||
// Advisory dimensions
|
||||
CvssBase = cvssBase,
|
||||
EpssScore = epss,
|
||||
ExploitMaturity = exploitMaturity,
|
||||
PatchProofConfidence = patchProofConfidence,
|
||||
VexStatus = vexStatus,
|
||||
VexSource = vexSource,
|
||||
// Legacy dimensions (required fields, use reachability for Rch)
|
||||
Rch = reachability,
|
||||
Rts = 0.0,
|
||||
Bkp = 0.0,
|
||||
Xpl = epss, // Map EPSS to XPL for legacy
|
||||
Src = 0.0,
|
||||
Mit = 0.0
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -415,6 +415,193 @@ public class EvidenceWeightedScoreDeterminismTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task TASK-028-003: Canonical Digest Tests
|
||||
|
||||
[Fact]
|
||||
public void Result_HasCanonicalDigest_AfterCalculation()
|
||||
{
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-12345",
|
||||
Rch = 0.8, Rts = 0.7, Bkp = 0.5, Xpl = 0.6, Src = 0.5, Mit = 0.1
|
||||
};
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
result.CanonicalDigest.Should().NotBeNullOrEmpty();
|
||||
result.CanonicalDigest.Should().MatchRegex("^[0-9a-f]{64}$",
|
||||
"Digest should be 64-character lowercase hex string");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_GetCanonicalJson_ProducesValidJson()
|
||||
{
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-12345",
|
||||
Rch = 0.8, Rts = 0.7, Bkp = 0.5, Xpl = 0.6, Src = 0.5, Mit = 0.1
|
||||
};
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
var canonicalJson = result.GetCanonicalJson();
|
||||
|
||||
canonicalJson.Should().NotBeNullOrEmpty();
|
||||
canonicalJson.Should().Contain("\"finding_id\"");
|
||||
canonicalJson.Should().Contain("\"score\"");
|
||||
canonicalJson.Should().Contain("\"bucket\"");
|
||||
canonicalJson.Should().Contain("\"inputs\"");
|
||||
canonicalJson.Should().Contain("\"weights\"");
|
||||
canonicalJson.Should().Contain("\"breakdown\"");
|
||||
canonicalJson.Should().Contain("\"flags\"");
|
||||
canonicalJson.Should().Contain("\"caps\"");
|
||||
canonicalJson.Should().Contain("\"policy_digest\"");
|
||||
canonicalJson.Should().Contain("\"calculated_at\"");
|
||||
// Should NOT contain the canonical_digest itself (avoids circular reference)
|
||||
canonicalJson.Should().NotContain("\"canonical_digest\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_ComputeDigest_ProducesHexString()
|
||||
{
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-12345",
|
||||
Rch = 0.8, Rts = 0.7, Bkp = 0.5, Xpl = 0.6, Src = 0.5, Mit = 0.1
|
||||
};
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
var digest = result.ComputeDigest();
|
||||
|
||||
digest.Should().MatchRegex("^[0-9a-f]{64}$",
|
||||
"Digest should be 64-character lowercase hex string");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_ComputeDigest_IsDeterministic_100Iterations()
|
||||
{
|
||||
// TASK-028-003: Add determinism test - hash 100x → all identical
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-12345",
|
||||
Rch = 0.8, Rts = 0.7, Bkp = 0.5, Xpl = 0.6, Src = 0.5, Mit = 0.1
|
||||
};
|
||||
|
||||
// Create fresh results each time to avoid caching effects
|
||||
var digests = Enumerable.Range(0, 100)
|
||||
.Select(_ =>
|
||||
{
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
return result.ComputeDigest();
|
||||
})
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
digests.Should().ContainSingle("All 100 digest computations should produce identical output");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_GetCanonicalJson_IsDeterministic_100Iterations()
|
||||
{
|
||||
// TASK-028-003: Add determinism test - serialize 100x → all identical
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-12345",
|
||||
Rch = 0.8, Rts = 0.7, Bkp = 0.5, Xpl = 0.6, Src = 0.5, Mit = 0.1
|
||||
};
|
||||
|
||||
var results = Enumerable.Range(0, 100)
|
||||
.Select(_ =>
|
||||
{
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
return result.GetCanonicalJson();
|
||||
})
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
results.Should().ContainSingle("All 100 serializations should produce identical output");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_DifferentInputs_ProduceDifferentDigests()
|
||||
{
|
||||
var input1 = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-00001",
|
||||
Rch = 0.8, Rts = 0.7, Bkp = 0.5, Xpl = 0.6, Src = 0.5, Mit = 0.1
|
||||
};
|
||||
|
||||
var input2 = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-00002",
|
||||
Rch = 0.3, Rts = 0.4, Bkp = 0.2, Xpl = 0.1, Src = 0.2, Mit = 0.05
|
||||
};
|
||||
|
||||
var result1 = _calculator.Calculate(input1, _defaultPolicy);
|
||||
var result2 = _calculator.Calculate(input2, _defaultPolicy);
|
||||
|
||||
result1.CanonicalDigest.Should().NotBe(result2.CanonicalDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_SameInputsDifferentPolicies_ProduceDifferentDigests()
|
||||
{
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-12345",
|
||||
Rch = 0.5, Rts = 0.5, Bkp = 0.5, Xpl = 0.5, Src = 0.5, Mit = 0.1
|
||||
};
|
||||
|
||||
var policy2 = new EvidenceWeightPolicy
|
||||
{
|
||||
Profile = "custom",
|
||||
Version = "v2",
|
||||
Weights = new EvidenceWeights
|
||||
{
|
||||
Rch = 0.25, Rts = 0.25, Bkp = 0.20, Xpl = 0.15, Src = 0.10, Mit = 0.05
|
||||
}
|
||||
};
|
||||
|
||||
var result1 = _calculator.Calculate(input, _defaultPolicy);
|
||||
var result2 = _calculator.Calculate(input, policy2);
|
||||
|
||||
// Results should differ because policy digests differ
|
||||
result1.PolicyDigest.Should().NotBe(result2.PolicyDigest);
|
||||
result1.CanonicalDigest.Should().NotBe(result2.CanonicalDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_WithComputedDigest_MatchesComputeDigest()
|
||||
{
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-12345",
|
||||
Rch = 0.8, Rts = 0.7, Bkp = 0.5, Xpl = 0.6, Src = 0.5, Mit = 0.1
|
||||
};
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
|
||||
// The CanonicalDigest property should match ComputeDigest()
|
||||
result.CanonicalDigest.Should().Be(result.ComputeDigest());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Result_CanonicalJson_ExcludesCanonicalDigest()
|
||||
{
|
||||
var input = new EvidenceWeightedScoreInput
|
||||
{
|
||||
FindingId = "CVE-2024-12345",
|
||||
Rch = 0.8, Rts = 0.7, Bkp = 0.5, Xpl = 0.6, Src = 0.5, Mit = 0.1
|
||||
};
|
||||
|
||||
var result = _calculator.Calculate(input, _defaultPolicy);
|
||||
var json = result.GetCanonicalJson();
|
||||
|
||||
// Should not include canonical_digest to avoid circular dependency
|
||||
json.Should().NotContain("canonical_digest");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task 54: Benchmark Tests
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user