344 lines
11 KiB
C#
344 lines
11 KiB
C#
// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors.
|
|
// Sprint: SPRINT_20260110_012_007_RISK
|
|
// Task: FCR-009 - Integration Tests
|
|
|
|
using System.Collections.Immutable;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.RiskEngine.Core.Contracts;
|
|
using StellaOps.RiskEngine.Core.Providers.FixChain;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.RiskEngine.Tests;
|
|
|
|
[Trait("Category", "Integration")]
|
|
public sealed class FixChainRiskIntegrationTests
|
|
{
|
|
private readonly FixChainRiskOptions _options;
|
|
private readonly InMemoryFixChainAttestationClient _attestationClient;
|
|
private readonly FixChainRiskProvider _provider;
|
|
|
|
public FixChainRiskIntegrationTests()
|
|
{
|
|
_options = new FixChainRiskOptions
|
|
{
|
|
Enabled = true,
|
|
FixedReduction = 0.90,
|
|
PartialReduction = 0.50,
|
|
MinConfidenceThreshold = 0.60m
|
|
};
|
|
|
|
_attestationClient = new InMemoryFixChainAttestationClient();
|
|
_provider = new FixChainRiskProvider(
|
|
_options,
|
|
_attestationClient,
|
|
NullLogger<FixChainRiskProvider>.Instance);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullWorkflow_FixedVerdict_ReducesRisk()
|
|
{
|
|
// Arrange
|
|
var cveId = "CVE-2024-12345";
|
|
var binarySha256 = new string('a', 64);
|
|
var attestation = new FixChainAttestationData
|
|
{
|
|
ContentDigest = "sha256:abc123",
|
|
CveId = cveId,
|
|
ComponentPurl = "pkg:deb/debian/openssl@3.0.11",
|
|
BinarySha256 = binarySha256,
|
|
Verdict = new FixChainVerdictData
|
|
{
|
|
Status = "fixed",
|
|
Confidence = 0.97m,
|
|
Rationale = ["3 vulnerable functions removed", "All paths eliminated"]
|
|
},
|
|
GoldenSetId = "gs-openssl-0727",
|
|
VerifiedAt = DateTimeOffset.UtcNow
|
|
};
|
|
_attestationClient.AddAttestation(cveId, binarySha256, attestation);
|
|
|
|
// Act
|
|
var status = await _provider.GetFixStatusAsync(cveId, binarySha256);
|
|
|
|
// Assert
|
|
status.Should().NotBeNull();
|
|
status!.Verdict.Should().Be("fixed");
|
|
status.Confidence.Should().Be(0.97m);
|
|
status.Rationale.Should().HaveCount(2);
|
|
status.GoldenSetId.Should().Be("gs-openssl-0727");
|
|
|
|
// Verify risk adjustment
|
|
var adjustment = _provider.ComputeRiskAdjustment(status);
|
|
adjustment.Should().BeLessThan(0.3); // Significant reduction
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullWorkflow_CreateRiskFactor_ProducesValidFactor()
|
|
{
|
|
// Arrange
|
|
var cveId = "CVE-2024-67890";
|
|
var binarySha256 = new string('b', 64);
|
|
var attestation = new FixChainAttestationData
|
|
{
|
|
ContentDigest = "sha256:def456",
|
|
CveId = cveId,
|
|
ComponentPurl = "pkg:npm/lodash@4.17.21",
|
|
BinarySha256 = binarySha256,
|
|
Verdict = new FixChainVerdictData
|
|
{
|
|
Status = "partial",
|
|
Confidence = 0.75m,
|
|
Rationale = ["2 paths eliminated", "1 path remaining"]
|
|
},
|
|
VerifiedAt = DateTimeOffset.UtcNow
|
|
};
|
|
_attestationClient.AddAttestation(cveId, binarySha256, attestation);
|
|
|
|
// Act
|
|
var status = await _provider.GetFixStatusAsync(cveId, binarySha256);
|
|
var factor = _provider.CreateRiskFactor(status!);
|
|
|
|
// Assert
|
|
factor.Verdict.Should().Be(FixChainVerdictStatus.Partial);
|
|
factor.Confidence.Should().Be(0.75m);
|
|
factor.RiskModifier.Should().BeLessThan(0);
|
|
factor.AttestationRef.Should().StartWith("fixchain://");
|
|
factor.Rationale.Should().HaveCount(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullWorkflow_DisplayModel_HasCorrectValues()
|
|
{
|
|
// Arrange
|
|
var cveId = "CVE-2024-99999";
|
|
var binarySha256 = new string('c', 64);
|
|
var attestation = new FixChainAttestationData
|
|
{
|
|
ContentDigest = "sha256:ghi789",
|
|
CveId = cveId,
|
|
ComponentPurl = "pkg:maven/org.example/lib@1.0.0",
|
|
BinarySha256 = binarySha256,
|
|
Verdict = new FixChainVerdictData
|
|
{
|
|
Status = "fixed",
|
|
Confidence = 0.95m,
|
|
Rationale = ["Fix verified"]
|
|
},
|
|
GoldenSetId = "gs-example",
|
|
VerifiedAt = DateTimeOffset.UtcNow
|
|
};
|
|
_attestationClient.AddAttestation(cveId, binarySha256, attestation);
|
|
|
|
// Act
|
|
var status = await _provider.GetFixStatusAsync(cveId, binarySha256);
|
|
var factor = _provider.CreateRiskFactor(status!);
|
|
var display = factor.ToDisplay();
|
|
|
|
// Assert
|
|
display.Label.Should().Be("Fix Verification");
|
|
display.Value.Should().Contain("Fixed");
|
|
display.Value.Should().Contain("95");
|
|
display.ImpactDirection.Should().Be("decrease");
|
|
display.EvidenceRef.Should().Contain("fixchain://");
|
|
display.Details.Should().ContainKey("golden_set_id");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullWorkflow_Badge_HasCorrectStyle()
|
|
{
|
|
// Arrange
|
|
var cveId = "CVE-2024-11111";
|
|
var binarySha256 = new string('d', 64);
|
|
var attestation = new FixChainAttestationData
|
|
{
|
|
ContentDigest = "sha256:jkl012",
|
|
CveId = cveId,
|
|
ComponentPurl = "pkg:pypi/requests@2.28.0",
|
|
BinarySha256 = binarySha256,
|
|
Verdict = new FixChainVerdictData
|
|
{
|
|
Status = "inconclusive",
|
|
Confidence = 0.45m,
|
|
Rationale = ["Could not determine"]
|
|
},
|
|
VerifiedAt = DateTimeOffset.UtcNow
|
|
};
|
|
_attestationClient.AddAttestation(cveId, binarySha256, attestation);
|
|
|
|
// Act
|
|
var status = await _provider.GetFixStatusAsync(cveId, binarySha256);
|
|
var factor = _provider.CreateRiskFactor(status!);
|
|
var badge = factor.ToBadge();
|
|
|
|
// Assert
|
|
badge.Status.Should().Be("Inconclusive");
|
|
badge.Color.Should().Be("gray");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullWorkflow_MultipleAttestations_SameComponent()
|
|
{
|
|
// Arrange - add multiple CVE attestations for same component
|
|
var binarySha256 = new string('e', 64);
|
|
var cveIds = new[] { "CVE-2024-001", "CVE-2024-002", "CVE-2024-003" };
|
|
|
|
foreach (var cveId in cveIds)
|
|
{
|
|
_attestationClient.AddAttestation(cveId, binarySha256, new FixChainAttestationData
|
|
{
|
|
ContentDigest = $"sha256:{cveId}",
|
|
CveId = cveId,
|
|
ComponentPurl = "pkg:deb/debian/openssl@3.0.11",
|
|
BinarySha256 = binarySha256,
|
|
Verdict = new FixChainVerdictData
|
|
{
|
|
Status = "fixed",
|
|
Confidence = 0.95m,
|
|
Rationale = [$"Fix for {cveId}"]
|
|
},
|
|
VerifiedAt = DateTimeOffset.UtcNow
|
|
});
|
|
}
|
|
|
|
// Act & Assert - each CVE can be queried individually
|
|
foreach (var cveId in cveIds)
|
|
{
|
|
var status = await _provider.GetFixStatusAsync(cveId, binarySha256);
|
|
status.Should().NotBeNull();
|
|
status!.Verdict.Should().Be("fixed");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullWorkflow_ScoreRequest_AppliesAdjustment()
|
|
{
|
|
// Arrange
|
|
var signals = new Dictionary<string, double>
|
|
{
|
|
[FixChainRiskProvider.SignalFixConfidence] = 0.90,
|
|
[FixChainRiskProvider.SignalFixStatus] = FixChainRiskProvider.EncodeStatus("fixed")
|
|
};
|
|
var request = new ScoreRequest("fixchain", "test-subject", signals);
|
|
|
|
// Act
|
|
var score = await _provider.ScoreAsync(request, CancellationToken.None);
|
|
|
|
// Assert
|
|
score.Should().BeLessThan(0.5); // Significant reduction applied
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullWorkflow_DisabledProvider_NoAdjustment()
|
|
{
|
|
// Arrange
|
|
var disabledOptions = new FixChainRiskOptions { Enabled = false };
|
|
var disabledProvider = new FixChainRiskProvider(disabledOptions);
|
|
|
|
var signals = new Dictionary<string, double>
|
|
{
|
|
[FixChainRiskProvider.SignalFixConfidence] = 1.0,
|
|
[FixChainRiskProvider.SignalFixStatus] = FixChainRiskProvider.EncodeStatus("fixed")
|
|
};
|
|
var request = new ScoreRequest("fixchain", "test-subject", signals);
|
|
|
|
// Act
|
|
var score = await disabledProvider.ScoreAsync(request, CancellationToken.None);
|
|
|
|
// Assert
|
|
score.Should().Be(1.0); // No adjustment when disabled
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullWorkflow_NoAttestation_ReturnsNull()
|
|
{
|
|
// Act
|
|
var status = await _provider.GetFixStatusAsync(
|
|
"CVE-NONEXISTENT",
|
|
new string('x', 64));
|
|
|
|
// Assert
|
|
status.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FullWorkflow_GetForComponent_ReturnsMultiple()
|
|
{
|
|
// Arrange
|
|
var componentPurl = "pkg:deb/debian/test@1.0.0";
|
|
var cves = new[] { "CVE-2024-A", "CVE-2024-B" };
|
|
|
|
foreach (var cveId in cves)
|
|
{
|
|
_attestationClient.AddAttestation(cveId, new string('f', 64), new FixChainAttestationData
|
|
{
|
|
ContentDigest = $"sha256:{cveId}",
|
|
CveId = cveId,
|
|
ComponentPurl = componentPurl,
|
|
BinarySha256 = new string('f', 64),
|
|
Verdict = new FixChainVerdictData
|
|
{
|
|
Status = "fixed",
|
|
Confidence = 0.90m,
|
|
Rationale = []
|
|
},
|
|
VerifiedAt = DateTimeOffset.UtcNow
|
|
});
|
|
}
|
|
|
|
// Act
|
|
var attestations = await _attestationClient.GetForComponentAsync(componentPurl);
|
|
|
|
// Assert
|
|
attestations.Should().HaveCount(2);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// In-memory attestation client for testing.
|
|
/// </summary>
|
|
internal sealed class InMemoryFixChainAttestationClient : IFixChainAttestationClient
|
|
{
|
|
private readonly Dictionary<string, FixChainAttestationData> _store = new();
|
|
private readonly Dictionary<string, List<FixChainAttestationData>> _byComponent = new();
|
|
|
|
public void AddAttestation(string cveId, string binarySha256, FixChainAttestationData attestation)
|
|
{
|
|
var key = $"{cveId}:{binarySha256}";
|
|
_store[key] = attestation;
|
|
|
|
if (!string.IsNullOrEmpty(attestation.ComponentPurl))
|
|
{
|
|
if (!_byComponent.TryGetValue(attestation.ComponentPurl, out var list))
|
|
{
|
|
list = [];
|
|
_byComponent[attestation.ComponentPurl] = list;
|
|
}
|
|
list.Add(attestation);
|
|
}
|
|
}
|
|
|
|
public Task<FixChainAttestationData?> GetFixChainAsync(
|
|
string cveId,
|
|
string binarySha256,
|
|
string? componentPurl = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var key = $"{cveId}:{binarySha256}";
|
|
return Task.FromResult(_store.GetValueOrDefault(key));
|
|
}
|
|
|
|
public Task<ImmutableArray<FixChainAttestationData>> GetForComponentAsync(
|
|
string componentPurl,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (_byComponent.TryGetValue(componentPurl, out var list))
|
|
{
|
|
return Task.FromResult(list.ToImmutableArray());
|
|
}
|
|
return Task.FromResult(ImmutableArray<FixChainAttestationData>.Empty);
|
|
}
|
|
}
|