partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

@@ -0,0 +1,375 @@
// -----------------------------------------------------------------------------
// LatticeTriageServiceTests.cs
// Sprint: SPRINT_20260208_052_ReachGraph_8_state_reachability_lattice
// Task: T1 - Unit tests for lattice triage service
// Description: Deterministic tests for triage service operations including
// evidence application, manual overrides, audit trail,
// queries, and reset functionality.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Time.Testing;
namespace StellaOps.Reachability.Core.Tests;
public sealed class LatticeTriageServiceTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly LatticeTriageService _service;
public LatticeTriageServiceTests()
{
_timeProvider = new FakeTimeProvider(
new DateTimeOffset(2026, 2, 9, 12, 0, 0, TimeSpan.Zero));
var services = new ServiceCollection();
services.AddSingleton<TimeProvider>(_timeProvider);
services.AddLogging();
services.AddSingleton<IMeterFactory>(new TestMeterFactory());
var provider = services.BuildServiceProvider();
_service = new LatticeTriageService(
_timeProvider,
provider.GetRequiredService<ILogger<LatticeTriageService>>(),
provider.GetRequiredService<IMeterFactory>());
}
/// <summary>Simple no-op meter factory for tests.</summary>
private sealed class TestMeterFactory : IMeterFactory
{
private readonly List<Meter> _meters = [];
public Meter Create(MeterOptions options)
{
var meter = new Meter(options);
_meters.Add(meter);
return meter;
}
public void Dispose()
{
foreach (var m in _meters) m.Dispose();
_meters.Clear();
}
}
// ── GetOrCreate ──────────────────────────────────────────────────────
[Fact]
public async Task GetOrCreate_NewEntry_ReturnsUnknownState()
{
var entry = await _service.GetOrCreateEntryAsync("pkg:npm/lodash@4.17.20", "CVE-2026-0001");
entry.CurrentState.Should().Be(LatticeState.Unknown);
entry.Confidence.Should().Be(0.0);
entry.VexStatus.Should().Be("under_investigation");
entry.RequiresReview.Should().BeFalse();
entry.Transitions.Should().BeEmpty();
}
[Fact]
public async Task GetOrCreate_SameKey_ReturnsSameEntry()
{
var entry1 = await _service.GetOrCreateEntryAsync("pkg:npm/lodash@4.17.20", "CVE-2026-0001");
var entry2 = await _service.GetOrCreateEntryAsync("pkg:npm/lodash@4.17.20", "CVE-2026-0001");
entry1.EntryId.Should().Be(entry2.EntryId);
}
[Fact]
public async Task GetOrCreate_DifferentKeys_ReturnsDifferentEntries()
{
var entry1 = await _service.GetOrCreateEntryAsync("pkg:npm/lodash@4.17.20", "CVE-2026-0001");
var entry2 = await _service.GetOrCreateEntryAsync("pkg:npm/lodash@4.17.21", "CVE-2026-0001");
entry1.EntryId.Should().NotBe(entry2.EntryId);
}
// ── ApplyEvidence ────────────────────────────────────────────────────
[Fact]
public async Task ApplyEvidence_StaticReachable_TransitionsFromUnknown()
{
var entry = await _service.ApplyEvidenceAsync(
"pkg:npm/lodash@4.17.20", "CVE-2026-0001",
EvidenceType.StaticReachable, "Static analysis found path");
entry.CurrentState.Should().Be(LatticeState.StaticReachable);
entry.Confidence.Should().BeGreaterThan(0.0);
entry.VexStatus.Should().Be("under_investigation");
entry.Transitions.Should().HaveCount(1);
entry.Transitions[0].FromState.Should().Be(LatticeState.Unknown);
entry.Transitions[0].ToState.Should().Be(LatticeState.StaticReachable);
entry.Transitions[0].Trigger.Should().Be(LatticeTransitionTrigger.StaticAnalysis);
}
[Fact]
public async Task ApplyEvidence_StaticThenRuntime_ReachesConfirmed()
{
await _service.ApplyEvidenceAsync(
"pkg:npm/lodash@4.17.20", "CVE-2026-0001",
EvidenceType.StaticReachable);
var entry = await _service.ApplyEvidenceAsync(
"pkg:npm/lodash@4.17.20", "CVE-2026-0001",
EvidenceType.RuntimeObserved);
entry.CurrentState.Should().Be(LatticeState.ConfirmedReachable);
entry.VexStatus.Should().Be("affected");
entry.Transitions.Should().HaveCount(2);
}
[Fact]
public async Task ApplyEvidence_ConflictingEvidence_EntersContested()
{
await _service.ApplyEvidenceAsync(
"pkg:npm/lodash@4.17.20", "CVE-2026-0001",
EvidenceType.StaticUnreachable);
var entry = await _service.ApplyEvidenceAsync(
"pkg:npm/lodash@4.17.20", "CVE-2026-0001",
EvidenceType.RuntimeObserved);
entry.CurrentState.Should().Be(LatticeState.Contested);
entry.RequiresReview.Should().BeTrue();
entry.VexStatus.Should().Be("under_investigation");
}
[Fact]
public async Task ApplyEvidence_WithDigests_RecordsInTransition()
{
var digests = new[] { "sha256:abc", "sha256:def" };
var entry = await _service.ApplyEvidenceAsync(
"pkg:npm/lodash@4.17.20", "CVE-2026-0001",
EvidenceType.StaticReachable,
evidenceDigests: digests);
entry.Transitions[0].EvidenceDigests.Should().HaveCount(2);
}
// ── Override ─────────────────────────────────────────────────────────
[Fact]
public async Task Override_SetsTargetState()
{
await _service.ApplyEvidenceAsync(
"pkg:npm/lodash@4.17.20", "CVE-2026-0001",
EvidenceType.StaticUnreachable);
var result = await _service.OverrideStateAsync(new LatticeOverrideRequest
{
ComponentPurl = "pkg:npm/lodash@4.17.20",
Cve = "CVE-2026-0001",
TargetState = LatticeState.ConfirmedReachable,
Reason = "Vendor confirmed reachability",
Actor = "security-team"
});
result.Applied.Should().BeTrue();
result.Entry.CurrentState.Should().Be(LatticeState.ConfirmedReachable);
result.Transition.IsManualOverride.Should().BeTrue();
result.Transition.Actor.Should().Be("security-team");
}
[Fact]
public async Task Override_FromConfirmed_ReturnsWarning()
{
// Reach ConfirmedReachable
await _service.ApplyEvidenceAsync(
"pkg:npm/lodash@4.17.20", "CVE-2026-0001",
EvidenceType.StaticReachable);
await _service.ApplyEvidenceAsync(
"pkg:npm/lodash@4.17.20", "CVE-2026-0001",
EvidenceType.RuntimeObserved);
var result = await _service.OverrideStateAsync(new LatticeOverrideRequest
{
ComponentPurl = "pkg:npm/lodash@4.17.20",
Cve = "CVE-2026-0001",
TargetState = LatticeState.ConfirmedUnreachable,
Reason = "Re-analysis confirmed false positive",
Actor = "admin"
});
result.Warning.Should().NotBeNullOrEmpty();
result.Warning.Should().Contain("Overriding from confirmed state");
}
[Fact]
public async Task Override_HasOverride_IsTrue()
{
var result = await _service.OverrideStateAsync(new LatticeOverrideRequest
{
ComponentPurl = "pkg:npm/lodash@4.17.20",
Cve = "CVE-2026-0001",
TargetState = LatticeState.ConfirmedUnreachable,
Reason = "Manual verification",
Actor = "tester"
});
result.Entry.HasOverride.Should().BeTrue();
}
// ── List and Query ───────────────────────────────────────────────────
[Fact]
public async Task List_FilterByState_ReturnsMatchingEntries()
{
await _service.ApplyEvidenceAsync("pkg:npm/a@1.0", "CVE-001", EvidenceType.StaticReachable);
await _service.ApplyEvidenceAsync("pkg:npm/b@1.0", "CVE-002", EvidenceType.RuntimeObserved);
await _service.ApplyEvidenceAsync("pkg:npm/c@1.0", "CVE-003", EvidenceType.StaticUnreachable);
var results = await _service.ListAsync(new LatticeTriageQuery
{
State = LatticeState.StaticReachable
});
results.Should().HaveCount(1);
results[0].ComponentPurl.Should().Be("pkg:npm/a@1.0");
}
[Fact]
public async Task List_FilterRequiresReview_ReturnsContestedOnly()
{
await _service.ApplyEvidenceAsync("pkg:npm/a@1.0", "CVE-001", EvidenceType.StaticUnreachable);
await _service.ApplyEvidenceAsync("pkg:npm/a@1.0", "CVE-001", EvidenceType.RuntimeObserved); // Contested
await _service.ApplyEvidenceAsync("pkg:npm/b@1.0", "CVE-002", EvidenceType.StaticReachable);
var results = await _service.ListAsync(new LatticeTriageQuery
{
RequiresReview = true
});
results.Should().HaveCount(1);
results[0].RequiresReview.Should().BeTrue();
}
[Fact]
public async Task List_FilterByPurlPrefix_ReturnsMatching()
{
await _service.ApplyEvidenceAsync("pkg:npm/lodash@4.17.20", "CVE-001", EvidenceType.StaticReachable);
await _service.ApplyEvidenceAsync("pkg:maven/log4j@2.14.0", "CVE-002", EvidenceType.StaticReachable);
var results = await _service.ListAsync(new LatticeTriageQuery
{
ComponentPurlPrefix = "pkg:npm/"
});
results.Should().HaveCount(1);
results[0].ComponentPurl.Should().StartWith("pkg:npm/");
}
// ── History ──────────────────────────────────────────────────────────
[Fact]
public async Task GetHistory_ReturnsFullTransitionLog()
{
await _service.ApplyEvidenceAsync("pkg:npm/a@1.0", "CVE-001", EvidenceType.StaticReachable);
await _service.ApplyEvidenceAsync("pkg:npm/a@1.0", "CVE-001", EvidenceType.RuntimeObserved);
var history = await _service.GetHistoryAsync("pkg:npm/a@1.0", "CVE-001");
history.Should().HaveCount(2);
history[0].FromState.Should().Be(LatticeState.Unknown);
history[0].ToState.Should().Be(LatticeState.StaticReachable);
history[1].FromState.Should().Be(LatticeState.StaticReachable);
history[1].ToState.Should().Be(LatticeState.ConfirmedReachable);
}
[Fact]
public async Task GetHistory_NonexistentEntry_ReturnsEmpty()
{
var history = await _service.GetHistoryAsync("pkg:npm/nonexistent@1.0", "CVE-999");
history.Should().BeEmpty();
}
// ── Reset ────────────────────────────────────────────────────────────
[Fact]
public async Task Reset_ReturnsToUnknown()
{
await _service.ApplyEvidenceAsync("pkg:npm/a@1.0", "CVE-001", EvidenceType.StaticReachable);
await _service.ApplyEvidenceAsync("pkg:npm/a@1.0", "CVE-001", EvidenceType.RuntimeObserved);
var entry = await _service.ResetAsync(
"pkg:npm/a@1.0", "CVE-001",
"admin", "Re-scan required");
entry.CurrentState.Should().Be(LatticeState.Unknown);
entry.Confidence.Should().Be(0.0);
}
[Fact]
public async Task Reset_RecordsTransition()
{
await _service.ApplyEvidenceAsync("pkg:npm/a@1.0", "CVE-001", EvidenceType.StaticReachable);
await _service.ResetAsync(
"pkg:npm/a@1.0", "CVE-001",
"admin", "Re-scan");
var history = await _service.GetHistoryAsync("pkg:npm/a@1.0", "CVE-001");
var last = history[^1];
last.Trigger.Should().Be(LatticeTransitionTrigger.SystemReset);
last.Actor.Should().Be("admin");
}
[Fact]
public async Task Reset_NonexistentEntry_Throws()
{
var act = () => _service.ResetAsync(
"pkg:npm/nonexistent@1.0", "CVE-999",
"admin", "test");
await act.Should().ThrowAsync<InvalidOperationException>();
}
// ── Edge cases ───────────────────────────────────────────────────────
[Fact]
public async Task GetOrCreate_ThrowsOnNullPurl()
{
var act = () => _service.GetOrCreateEntryAsync(null!, "CVE-001");
await act.Should().ThrowAsync<ArgumentException>();
}
[Fact]
public async Task Override_ThrowsOnEmptyReason()
{
var act = () => _service.OverrideStateAsync(new LatticeOverrideRequest
{
ComponentPurl = "pkg:npm/a@1.0",
Cve = "CVE-001",
TargetState = LatticeState.ConfirmedReachable,
Reason = "",
Actor = "admin"
});
await act.Should().ThrowAsync<ArgumentException>();
}
// ── VEX status mapping ───────────────────────────────────────────────
[Theory]
[InlineData(LatticeState.ConfirmedUnreachable, "not_affected")]
[InlineData(LatticeState.RuntimeObserved, "affected")]
[InlineData(LatticeState.StaticUnreachable, "not_affected")]
public async Task VexStatus_MapsCorrectly(LatticeState targetState, string expectedVex)
{
var result = await _service.OverrideStateAsync(new LatticeOverrideRequest
{
ComponentPurl = $"pkg:npm/test-{targetState}@1.0",
Cve = $"CVE-{(int)targetState:D3}",
TargetState = targetState,
Reason = "Test",
Actor = "test"
});
result.Entry.VexStatus.Should().Be(expectedVex);
}
}

View File

@@ -12,6 +12,8 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.Abstractions" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
</ItemGroup>
<ItemGroup>