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:
master
2026-01-19 09:02:59 +02:00
parent 8c4bf54aed
commit 17419ba7c4
809 changed files with 170738 additions and 12244 deletions

View File

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

View File

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

View File

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