old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) 2025 StellaOps
|
||||
// Sprint: SPRINT_20260112_004_BE_findings_scoring_attested_reduction (EWS-API-004)
|
||||
// Task: Unit tests for attested-reduction response fields
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MsOptions = Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
using StellaOps.Findings.Ledger.WebService.Services;
|
||||
using StellaOps.Signals.EvidenceWeightedScore;
|
||||
using StellaOps.Signals.EvidenceWeightedScore.Normalizers;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Services;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public class FindingScoringServiceTests
|
||||
{
|
||||
private readonly Mock<INormalizerAggregator> _normalizer = new();
|
||||
private readonly Mock<IEvidenceWeightedScoreCalculator> _calculator = new();
|
||||
private readonly Mock<IEvidenceWeightPolicyProvider> _policyProvider = new();
|
||||
private readonly Mock<IFindingEvidenceProvider> _evidenceProvider = new();
|
||||
private readonly Mock<IScoreHistoryStore> _historyStore = new();
|
||||
private readonly Mock<TimeProvider> _timeProvider = new();
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly FindingScoringService _service;
|
||||
private readonly DateTimeOffset _now = new(2026, 1, 14, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
public FindingScoringServiceTests()
|
||||
{
|
||||
_cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var options = MsOptions.Options.Create(new FindingScoringOptions
|
||||
{
|
||||
CacheTtlMinutes = 60,
|
||||
MaxBatchSize = 100,
|
||||
MaxConcurrency = 10
|
||||
});
|
||||
_timeProvider.Setup(tp => tp.GetUtcNow()).Returns(_now);
|
||||
|
||||
_service = new FindingScoringService(
|
||||
_normalizer.Object,
|
||||
_calculator.Object,
|
||||
_policyProvider.Object,
|
||||
_evidenceProvider.Object,
|
||||
_historyStore.Object,
|
||||
_cache,
|
||||
options,
|
||||
NullLogger<FindingScoringService>.Instance,
|
||||
_timeProvider.Object);
|
||||
}
|
||||
|
||||
#region Attested Reduction Response Fields Tests
|
||||
|
||||
[Fact]
|
||||
public async Task CalculateScoreAsync_AttestedReductionEnabled_PopulatesReductionProfile()
|
||||
{
|
||||
// Arrange
|
||||
var findingId = "CVE-2024-1234@pkg:npm/lodash@4.17.20";
|
||||
var evidence = CreateFindingEvidence(findingId);
|
||||
var policy = CreateAttestedReductionPolicy(enabled: true);
|
||||
var input = CreateEvidenceWeightedScoreInput(findingId);
|
||||
var result = CreateScoreResult(findingId, withAttestedReduction: true);
|
||||
|
||||
SetupMocks(evidence, policy, input, result);
|
||||
|
||||
// Act
|
||||
var response = await _service.CalculateScoreAsync(
|
||||
findingId,
|
||||
new CalculateScoreRequest { IncludeBreakdown = true },
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
response.Should().NotBeNull();
|
||||
response!.ReductionProfile.Should().NotBeNull();
|
||||
response.ReductionProfile!.Enabled.Should().BeTrue();
|
||||
response.ReductionProfile.Mode.Should().NotBeNullOrEmpty();
|
||||
response.ReductionProfile.MaxReductionPercent.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CalculateScoreAsync_HardFailTriggered_SetsHardFailTrue()
|
||||
{
|
||||
// Arrange
|
||||
var findingId = "CVE-2024-9999@pkg:npm/critical@1.0.0";
|
||||
var evidence = CreateFindingEvidence(findingId);
|
||||
var policy = CreateAttestedReductionPolicy(enabled: true);
|
||||
var input = CreateEvidenceWeightedScoreInput(findingId);
|
||||
var result = CreateScoreResult(findingId, withHardFail: true);
|
||||
|
||||
SetupMocks(evidence, policy, input, result);
|
||||
|
||||
// Act
|
||||
var response = await _service.CalculateScoreAsync(
|
||||
findingId,
|
||||
new CalculateScoreRequest { IncludeBreakdown = true },
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
response.Should().NotBeNull();
|
||||
response!.HardFail.Should().BeTrue();
|
||||
response.ShortCircuitReason.Should().Be("anchored_affected_runtime_confirmed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CalculateScoreAsync_AnchoredVexNotAffected_SetsShortCircuitReason()
|
||||
{
|
||||
// Arrange
|
||||
var findingId = "CVE-2024-5555@pkg:npm/not-affected@1.0.0";
|
||||
var evidence = CreateFindingEvidenceWithAnchor(findingId);
|
||||
var policy = CreateAttestedReductionPolicy(enabled: true);
|
||||
var input = CreateEvidenceWeightedScoreInput(findingId);
|
||||
var result = CreateScoreResult(findingId, withAnchoredVex: true, score: 0);
|
||||
|
||||
SetupMocks(evidence, policy, input, result);
|
||||
|
||||
// Act
|
||||
var response = await _service.CalculateScoreAsync(
|
||||
findingId,
|
||||
new CalculateScoreRequest { IncludeBreakdown = true },
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
response.Should().NotBeNull();
|
||||
response!.Score.Should().Be(0);
|
||||
response.ShortCircuitReason.Should().Be("anchored_vex_not_affected");
|
||||
response.HardFail.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CalculateScoreAsync_WithAnchor_PopulatesAnchorDto()
|
||||
{
|
||||
// Arrange
|
||||
var findingId = "CVE-2024-1111@pkg:npm/anchored@2.0.0";
|
||||
var evidence = CreateFindingEvidenceWithAnchor(findingId);
|
||||
var policy = CreateAttestedReductionPolicy(enabled: true);
|
||||
var input = CreateEvidenceWeightedScoreInput(findingId);
|
||||
var result = CreateScoreResult(findingId);
|
||||
|
||||
SetupMocks(evidence, policy, input, result);
|
||||
|
||||
// Act
|
||||
var response = await _service.CalculateScoreAsync(
|
||||
findingId,
|
||||
new CalculateScoreRequest { IncludeBreakdown = true },
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
response.Should().NotBeNull();
|
||||
response!.Anchor.Should().NotBeNull();
|
||||
response.Anchor!.Anchored.Should().BeTrue();
|
||||
response.Anchor.EnvelopeDigest.Should().Be("sha256:abc123");
|
||||
response.Anchor.RekorLogIndex.Should().Be(12345);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CalculateScoreAsync_NoReductionProfile_ReturnsNullReductionProfile()
|
||||
{
|
||||
// Arrange
|
||||
var findingId = "CVE-2024-2222@pkg:npm/standard@1.0.0";
|
||||
var evidence = CreateFindingEvidence(findingId);
|
||||
var policy = CreateStandardPolicy(); // No attested reduction
|
||||
var input = CreateEvidenceWeightedScoreInput(findingId);
|
||||
var result = CreateScoreResult(findingId);
|
||||
|
||||
SetupMocks(evidence, policy, input, result);
|
||||
|
||||
// Act
|
||||
var response = await _service.CalculateScoreAsync(
|
||||
findingId,
|
||||
new CalculateScoreRequest { IncludeBreakdown = true },
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
response.Should().NotBeNull();
|
||||
response!.ReductionProfile.Should().BeNull();
|
||||
response.HardFail.Should().BeFalse();
|
||||
response.ShortCircuitReason.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CalculateScoreAsync_NoEvidence_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var findingId = "CVE-2024-0000@pkg:npm/missing@1.0.0";
|
||||
|
||||
_evidenceProvider.Setup(p => p.GetEvidenceAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((FindingEvidence?)null);
|
||||
|
||||
// Act
|
||||
var response = await _service.CalculateScoreAsync(
|
||||
findingId,
|
||||
new CalculateScoreRequest(),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
response.Should().BeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cache Key Tests
|
||||
|
||||
[Fact]
|
||||
public async Task CalculateScoreAsync_DifferentPolicies_UseDifferentCacheKeys()
|
||||
{
|
||||
// Arrange
|
||||
var findingId = "CVE-2024-3333@pkg:npm/cached@1.0.0";
|
||||
var evidence = CreateFindingEvidence(findingId);
|
||||
var policy1 = CreateAttestedReductionPolicy(enabled: true);
|
||||
var policy2 = CreateAttestedReductionPolicy(enabled: false);
|
||||
var input = CreateEvidenceWeightedScoreInput(findingId);
|
||||
var result1 = CreateScoreResult(findingId, withAttestedReduction: true, score: 25);
|
||||
var result2 = CreateScoreResult(findingId, score: 75);
|
||||
|
||||
// First call with reduction enabled
|
||||
SetupMocks(evidence, policy1, input, result1);
|
||||
var response1 = await _service.CalculateScoreAsync(
|
||||
findingId,
|
||||
new CalculateScoreRequest { ForceRecalculate = true },
|
||||
CancellationToken.None);
|
||||
|
||||
// Change policy to disabled
|
||||
SetupMocks(evidence, policy2, input, result2);
|
||||
var response2 = await _service.CalculateScoreAsync(
|
||||
findingId,
|
||||
new CalculateScoreRequest { ForceRecalculate = true },
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert - different scores due to different cache keys
|
||||
response1!.Score.Should().NotBe(response2!.Score);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private void SetupMocks(
|
||||
FindingEvidence evidence,
|
||||
EvidenceWeightPolicy policy,
|
||||
EvidenceWeightedScoreInput input,
|
||||
EvidenceWeightedScoreResult result)
|
||||
{
|
||||
_evidenceProvider.Setup(p => p.GetEvidenceAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(evidence);
|
||||
_policyProvider.Setup(p => p.GetDefaultPolicyAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(policy);
|
||||
_normalizer.Setup(n => n.Aggregate(It.IsAny<FindingEvidence>()))
|
||||
.Returns(input);
|
||||
_calculator.Setup(c => c.Calculate(It.IsAny<EvidenceWeightedScoreInput>(), It.IsAny<EvidenceWeightPolicy>()))
|
||||
.Returns(result);
|
||||
}
|
||||
|
||||
private static FindingEvidence CreateFindingEvidence(string findingId) => new()
|
||||
{
|
||||
FindingId = findingId
|
||||
};
|
||||
|
||||
private static FindingEvidence CreateFindingEvidenceWithAnchor(string findingId) => new()
|
||||
{
|
||||
FindingId = findingId,
|
||||
Anchor = new EvidenceAnchor
|
||||
{
|
||||
Anchored = true,
|
||||
EnvelopeDigest = "sha256:abc123",
|
||||
PredicateType = "https://stellaops.io/attestation/vex/v1",
|
||||
RekorLogIndex = 12345,
|
||||
RekorEntryId = "entry-123",
|
||||
Scope = "finding",
|
||||
Verified = true,
|
||||
AttestedAt = DateTimeOffset.UtcNow.AddHours(-1)
|
||||
}
|
||||
};
|
||||
|
||||
private static EvidenceWeightedScoreInput CreateEvidenceWeightedScoreInput(string findingId) => new()
|
||||
{
|
||||
FindingId = findingId,
|
||||
Rch = 0.5,
|
||||
Rts = 0.3,
|
||||
Bkp = 0.0,
|
||||
Xpl = 0.4,
|
||||
Src = 0.6,
|
||||
Mit = 0.1
|
||||
};
|
||||
|
||||
private static EvidenceWeightPolicy CreateAttestedReductionPolicy(bool enabled) => new()
|
||||
{
|
||||
Version = "1.0.0",
|
||||
Profile = "test",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Weights = EvidenceWeights.Default,
|
||||
Guardrails = GuardrailConfig.Default,
|
||||
Buckets = BucketThresholds.Default,
|
||||
AttestedReduction = enabled
|
||||
? AttestedReductionConfig.EnabledDefault
|
||||
: AttestedReductionConfig.Default
|
||||
};
|
||||
|
||||
private static EvidenceWeightPolicy CreateStandardPolicy() => new()
|
||||
{
|
||||
Version = "1.0.0",
|
||||
Profile = "standard",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Weights = EvidenceWeights.Default,
|
||||
Guardrails = GuardrailConfig.Default,
|
||||
Buckets = BucketThresholds.Default,
|
||||
AttestedReduction = AttestedReductionConfig.Default
|
||||
};
|
||||
|
||||
private EvidenceWeightedScoreResult CreateScoreResult(
|
||||
string findingId,
|
||||
bool withAttestedReduction = false,
|
||||
bool withHardFail = false,
|
||||
bool withAnchoredVex = false,
|
||||
int score = 50)
|
||||
{
|
||||
var flags = new List<string>();
|
||||
if (withAttestedReduction) flags.Add("attested-reduction");
|
||||
if (withHardFail)
|
||||
{
|
||||
flags.Add("hard-fail");
|
||||
flags.Add("anchored-vex");
|
||||
flags.Add("anchored-runtime");
|
||||
}
|
||||
if (withAnchoredVex)
|
||||
{
|
||||
flags.Add("anchored-vex");
|
||||
flags.Add("vendor-na");
|
||||
}
|
||||
|
||||
return new EvidenceWeightedScoreResult
|
||||
{
|
||||
FindingId = findingId,
|
||||
Score = score,
|
||||
Bucket = score >= 90 ? ScoreBucket.ActNow :
|
||||
score >= 70 ? ScoreBucket.ScheduleNext :
|
||||
score >= 40 ? ScoreBucket.Investigate : ScoreBucket.Watchlist,
|
||||
Inputs = new EvidenceInputValues(0.5, 0.3, 0.0, 0.4, 0.6, 0.1),
|
||||
Weights = EvidenceWeights.Default,
|
||||
Breakdown = [],
|
||||
Flags = flags,
|
||||
Explanations = ["Test explanation"],
|
||||
Caps = AppliedGuardrails.None(score),
|
||||
PolicyDigest = "sha256:policy123",
|
||||
CalculatedAt = _now
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user