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

@@ -6,3 +6,5 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| --- | --- | --- |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT-20260208-062-VEXREACH-001 | DONE | Added deterministic unit coverage for VEX+reachability filter matrix and controller endpoint (`6` tests passed on filtered run, 2026-02-08). |
| SPRINT-20260208-063-TRIAGE-001 | DONE | Add endpoint tests for triage cluster inbox stats and batch triage actions (2026-02-08). |

View File

@@ -0,0 +1,213 @@
using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Scanner.Triage.Models;
using StellaOps.Scanner.Triage.Services;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Endpoints.Triage;
using StellaOps.TestKit;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class TriageClusterEndpointsTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetClusterStats_ReturnsSeverityAndReachabilityDistributions()
{
var findings = BuildFindings();
await using var factory = ScannerApplicationFactory.CreateLightweight()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IFindingQueryService>();
services.AddSingleton<IFindingQueryService>(new StubFindingQueryService(findings));
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/triage/inbox/clusters/stats?artifactDigest=sha256:test");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var payload = await response.Content.ReadFromJsonAsync<TriageClusterStatsResponse>();
payload.Should().NotBeNull();
payload!.TotalClusters.Should().Be(2);
payload.TotalFindings.Should().Be(3);
payload.SeverityDistribution["critical"].Should().Be(1);
payload.ReachabilityDistribution["RuntimeConfirmed"].Should().Be(1);
payload.ReachabilityDistribution["Unreachable"].Should().Be(1);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PostClusterAction_AppliesActionToAllClusterFindings()
{
var findings = BuildFindings();
var triageStatus = new StubTriageStatusService();
await using var factory = ScannerApplicationFactory.CreateLightweight()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IFindingQueryService>();
services.RemoveAll<ITriageStatusService>();
services.AddSingleton<IFindingQueryService>(new StubFindingQueryService(findings));
services.AddSingleton<ITriageStatusService>(triageStatus);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var statsResponse = await client.GetAsync("/api/v1/triage/inbox/clusters/stats?artifactDigest=sha256:test");
var stats = await statsResponse.Content.ReadFromJsonAsync<TriageClusterStatsResponse>();
var cluster = stats!.Clusters.Single(c => c.FindingCount == 2);
var actionRequest = new BatchTriageClusterActionRequest
{
ArtifactDigest = "sha256:test",
DecisionKind = "MuteReach",
Reason = "batch triage test"
};
var actionResponse = await client.PostAsJsonAsync($"/api/v1/triage/inbox/clusters/{cluster.PathId}/actions", actionRequest);
actionResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var payload = await actionResponse.Content.ReadFromJsonAsync<BatchTriageClusterActionResponse>();
payload.Should().NotBeNull();
payload!.RequestedFindingCount.Should().Be(2);
payload.UpdatedFindingCount.Should().Be(2);
payload.Lane.Should().Be("MutedReach");
payload.DecisionKind.Should().Be("MuteReach");
payload.ActionRecord.ActionRecordId.Should().StartWith("triage-action:");
triageStatus.UpdatedFindingIds.Should().HaveCount(2);
}
private static IReadOnlyList<Finding> BuildFindings()
{
var timestamp = new DateTimeOffset(2026, 2, 8, 0, 0, 0, TimeSpan.Zero);
return
[
new Finding(
"finding-1",
"pkg:npm/acme/a@1.0.0",
"a",
"1.0.0",
["CVE-2026-0001"],
9.0m,
0.6m,
Severity.Critical,
"sha256:test",
timestamp,
["entry:http:post:/orders", "OrdersController.Post", "SqlSink.Write"],
"entry:http:post:/orders",
"SqlSink.Write",
ReachabilityStatus.RuntimeConfirmed,
0.95m),
new Finding(
"finding-2",
"pkg:npm/acme/a@1.0.0",
"a",
"1.0.0",
["CVE-2026-0002"],
7.5m,
0.4m,
Severity.High,
"sha256:test",
timestamp,
["entry:http:post:/orders", "OrdersController.Post", "KafkaSink.Publish"],
"entry:http:post:/orders",
"KafkaSink.Publish",
ReachabilityStatus.StaticallyReachable,
0.75m),
new Finding(
"finding-3",
"pkg:npm/acme/b@2.0.0",
"b",
"2.0.0",
["CVE-2026-0003"],
3.0m,
0.1m,
Severity.Low,
"sha256:test",
timestamp,
["entry:http:get:/health", "HealthController.Get", "LogSink.Write"],
"entry:http:get:/health",
"LogSink.Write",
ReachabilityStatus.Unreachable,
0.2m)
];
}
private sealed class StubFindingQueryService : IFindingQueryService
{
private readonly IReadOnlyList<Finding> _findings;
public StubFindingQueryService(IReadOnlyList<Finding> findings)
{
_findings = findings;
}
public Task<IReadOnlyList<Finding>> GetFindingsForArtifactAsync(string artifactDigest, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<Finding>>(
_findings.Where(f => string.Equals(f.ArtifactDigest, artifactDigest, StringComparison.Ordinal)).ToArray());
}
private sealed class StubTriageStatusService : ITriageStatusService
{
public List<string> UpdatedFindingIds { get; } = [];
public Task<FindingTriageStatusDto?> GetFindingStatusAsync(string findingId, CancellationToken ct = default)
=> Task.FromResult<FindingTriageStatusDto?>(null);
public Task<UpdateTriageStatusResponseDto?> UpdateStatusAsync(
string findingId,
UpdateTriageStatusRequestDto request,
string actor,
CancellationToken ct = default)
{
UpdatedFindingIds.Add(findingId);
return Task.FromResult<UpdateTriageStatusResponseDto?>(new UpdateTriageStatusResponseDto
{
FindingId = findingId,
PreviousLane = "Active",
NewLane = request.Lane ?? "Active",
PreviousVerdict = "Block",
NewVerdict = "Block",
SnapshotId = $"snap-{findingId}",
AppliedAt = new DateTimeOffset(2026, 2, 8, 0, 0, 0, TimeSpan.Zero)
});
}
public Task<SubmitVexStatementResponseDto?> SubmitVexStatementAsync(
string findingId,
SubmitVexStatementRequestDto request,
string actor,
CancellationToken ct = default)
=> Task.FromResult<SubmitVexStatementResponseDto?>(null);
public Task<BulkTriageQueryResponseDto> QueryFindingsAsync(
BulkTriageQueryRequestDto request,
int limit,
CancellationToken ct = default)
=> Task.FromResult(new BulkTriageQueryResponseDto
{
Findings = [],
TotalCount = 0,
NextCursor = null,
Summary = new TriageSummaryDto
{
ByLane = new Dictionary<string, int>(),
ByVerdict = new Dictionary<string, int>(),
CanShipCount = 0,
BlockingCount = 0
}
});
public Task<TriageSummaryDto> GetSummaryAsync(string artifactDigest, CancellationToken ct = default)
=> Task.FromResult(new TriageSummaryDto
{
ByLane = new Dictionary<string, int>(),
ByVerdict = new Dictionary<string, int>(),
CanShipCount = 0,
BlockingCount = 0
});
}
}

View File

@@ -0,0 +1,90 @@
// -----------------------------------------------------------------------------
// VexGateControllerFilterTests.cs
// Sprint: SPRINT_20260208_062_Scanner_vex_decision_filter_with_reachability
// Description: Unit tests for VEX reachability filtering endpoint logic.
// -----------------------------------------------------------------------------
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Scanner.Gate;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Controllers;
using StellaOps.Scanner.WebService.Services;
using StellaOps.TestKit;
namespace StellaOps.Scanner.WebService.Tests;
[Trait("Category", TestCategories.Unit)]
public sealed class VexGateControllerFilterTests
{
[Fact]
public void FilterByVexReachability_ValidRequest_ReturnsExpectedSummary()
{
var controller = CreateController();
var request = new VexReachabilityFilterRequest
{
Findings = new List<VexReachabilityFilterFindingDto>
{
new()
{
FindingId = "f-1",
Cve = "CVE-2026-1001",
VendorStatus = "not_affected",
ReachabilityTier = "unreachable",
ExistingDecision = "warn"
},
new()
{
FindingId = "f-2",
Cve = "CVE-2026-1002",
VendorStatus = "affected",
ReachabilityTier = "confirmed",
ExistingDecision = "warn"
}
}
};
var result = controller.FilterByVexReachability(request);
var ok = Assert.IsType<OkObjectResult>(result);
var payload = Assert.IsType<VexReachabilityFilterResponse>(ok.Value);
Assert.Equal(2, payload.Findings.Count);
Assert.Equal(1, payload.Summary.Suppressed);
Assert.Equal(1, payload.Summary.Elevated);
}
[Fact]
public void FilterByVexReachability_InvalidVendorStatus_ReturnsBadRequest()
{
var controller = CreateController();
var request = new VexReachabilityFilterRequest
{
Findings = new List<VexReachabilityFilterFindingDto>
{
new()
{
FindingId = "f-invalid",
Cve = "CVE-2026-1999",
VendorStatus = "broken_status",
ReachabilityTier = "confirmed",
ExistingDecision = "warn"
}
}
};
var result = controller.FilterByVexReachability(request);
Assert.IsType<BadRequestObjectResult>(result);
}
private static VexGateController CreateController()
{
var queryService = new Mock<IVexGateQueryService>(MockBehavior.Strict).Object;
var filter = new VexReachabilityDecisionFilter();
return new VexGateController(
queryService,
filter,
NullLogger<VexGateController>.Instance);
}
}

View File

@@ -214,6 +214,104 @@ public sealed class VexGateEndpointsTests
Assert.All(findings, f => Assert.Equal("Block", f.Decision));
}
[Fact]
public async Task FilterByVexReachability_WithMatrixCases_ReturnsAnnotatedActions()
{
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new VexReachabilityFilterRequest
{
Findings = new List<VexReachabilityFilterFindingDto>
{
new()
{
FindingId = "f-1",
Cve = "CVE-2026-0001",
Purl = "pkg:npm/a@1.0.0",
VendorStatus = "not_affected",
ReachabilityTier = "unreachable",
ExistingDecision = "warn"
},
new()
{
FindingId = "f-2",
Cve = "CVE-2026-0002",
Purl = "pkg:npm/b@1.0.0",
VendorStatus = "affected",
ReachabilityTier = "confirmed",
ExistingDecision = "warn"
},
new()
{
FindingId = "f-3",
Cve = "CVE-2026-0003",
Purl = "pkg:npm/c@1.0.0",
VendorStatus = "not_affected",
ReachabilityTier = "confirmed",
ExistingDecision = "pass"
}
}
};
var response = await client.PostAsJsonAsync($"{BasePath}/vex-reachability/filter", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<VexReachabilityFilterResponse>();
Assert.NotNull(payload);
Assert.Equal(3, payload!.Findings.Count);
Assert.Equal(1, payload.Summary.Suppressed);
Assert.Equal(1, payload.Summary.Elevated);
Assert.Equal(1, payload.Summary.FlagForReview);
var byId = payload.Findings.ToDictionary(f => f.FindingId, StringComparer.Ordinal);
Assert.Equal("suppress", byId["f-1"].Action);
Assert.Equal("pass", byId["f-1"].EffectiveDecision);
Assert.Equal("elevate", byId["f-2"].Action);
Assert.Equal("block", byId["f-2"].EffectiveDecision);
Assert.Equal("flag_for_review", byId["f-3"].Action);
Assert.Equal("warn", byId["f-3"].EffectiveDecision);
}
[Fact]
public async Task FilterByVexReachability_WithInvalidTier_ReturnsBadRequest()
{
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new VexReachabilityFilterRequest
{
Findings = new List<VexReachabilityFilterFindingDto>
{
new()
{
FindingId = "f-invalid",
Cve = "CVE-2026-0999",
Purl = "pkg:npm/invalid@1.0.0",
VendorStatus = "affected",
ReachabilityTier = "tier-9000",
ExistingDecision = "warn"
}
}
};
var response = await client.PostAsJsonAsync($"{BasePath}/vex-reachability/filter", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
private static VexGateResultsResponse CreateTestGateResults(
string scanId,
int blockedCount = 1,

View File

@@ -0,0 +1,106 @@
// -----------------------------------------------------------------------------
// VexReachabilityDecisionFilterTests.cs
// Sprint: SPRINT_20260208_062_Scanner_vex_decision_filter_with_reachability
// Description: Unit tests for VEX + reachability decision matrix filtering.
// -----------------------------------------------------------------------------
using StellaOps.Scanner.Gate;
using StellaOps.TestKit;
namespace StellaOps.Scanner.WebService.Tests;
[Trait("Category", TestCategories.Unit)]
public sealed class VexReachabilityDecisionFilterTests
{
private readonly VexReachabilityDecisionFilter _filter = new();
[Fact]
public void Evaluate_NotAffectedAndUnreachable_SuppressesToPass()
{
var input = CreateInput(
findingId: "f-1",
cve: "CVE-2026-0001",
vendorStatus: VexStatus.NotAffected,
tier: VexReachabilityTier.Unreachable,
existingDecision: VexGateDecision.Warn);
var result = _filter.Evaluate(input);
Assert.Equal(VexReachabilityFilterAction.Suppress, result.Action);
Assert.Equal(VexGateDecision.Pass, result.EffectiveDecision);
Assert.Equal("not_affected+unreachable", result.MatrixRule);
}
[Fact]
public void Evaluate_AffectedAndConfirmed_ElevatesToBlock()
{
var input = CreateInput(
findingId: "f-2",
cve: "CVE-2026-0002",
vendorStatus: VexStatus.Affected,
tier: VexReachabilityTier.Confirmed,
existingDecision: VexGateDecision.Warn);
var result = _filter.Evaluate(input);
Assert.Equal(VexReachabilityFilterAction.Elevate, result.Action);
Assert.Equal(VexGateDecision.Block, result.EffectiveDecision);
Assert.Equal("affected+reachable", result.MatrixRule);
}
[Fact]
public void Evaluate_NotAffectedAndConfirmed_FlagsForReview()
{
var input = CreateInput(
findingId: "f-3",
cve: "CVE-2026-0003",
vendorStatus: VexStatus.NotAffected,
tier: VexReachabilityTier.Confirmed,
existingDecision: VexGateDecision.Pass);
var result = _filter.Evaluate(input);
Assert.Equal(VexReachabilityFilterAction.FlagForReview, result.Action);
Assert.Equal(VexGateDecision.Warn, result.EffectiveDecision);
Assert.Equal("not_affected+reachable", result.MatrixRule);
}
[Fact]
public void EvaluateBatch_PreservesInputOrderDeterministically()
{
var inputs = new[]
{
CreateInput("f-a", "CVE-A", VexStatus.NotAffected, VexReachabilityTier.Unreachable, VexGateDecision.Warn),
CreateInput("f-b", "CVE-B", VexStatus.Affected, VexReachabilityTier.Likely, VexGateDecision.Warn),
CreateInput("f-c", "CVE-C", null, VexReachabilityTier.Present, VexGateDecision.Pass)
};
var results = _filter.EvaluateBatch(inputs);
Assert.Equal(3, results.Length);
Assert.Equal("f-a", results[0].FindingId);
Assert.Equal("f-b", results[1].FindingId);
Assert.Equal("f-c", results[2].FindingId);
Assert.Equal(VexReachabilityFilterAction.PassThrough, results[2].Action);
Assert.Equal(VexGateDecision.Pass, results[2].EffectiveDecision);
}
private static VexReachabilityDecisionInput CreateInput(
string findingId,
string cve,
VexStatus? vendorStatus,
VexReachabilityTier tier,
VexGateDecision existingDecision)
{
return new VexReachabilityDecisionInput
{
FindingId = findingId,
VulnerabilityId = cve,
Purl = "pkg:npm/test@1.0.0",
VendorStatus = vendorStatus,
ReachabilityTier = tier,
ExistingDecision = existingDecision
};
}
}