feat: add security sink detection patterns for JavaScript/TypeScript

- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
This commit is contained in:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View File

@@ -0,0 +1,143 @@
// -----------------------------------------------------------------------------
// ActionablesEndpointsTests.cs
// Sprint: SPRINT_4200_0002_0006_delta_compare_api
// Description: Integration tests for actionables engine endpoints.
// -----------------------------------------------------------------------------
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
/// <summary>
/// Integration tests for actionables engine endpoints.
/// </summary>
public sealed class ActionablesEndpointsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
[Fact]
public async Task GetDeltaActionables_ValidDeltaId_ReturnsActionables()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ActionablesResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Equal("delta-12345678", result!.DeltaId);
Assert.NotNull(result.Actionables);
}
[Fact]
public async Task GetDeltaActionables_SortedByPriority()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678");
var result = await response.Content.ReadFromJsonAsync<ActionablesResponseDto>(SerializerOptions);
Assert.NotNull(result);
if (result!.Actionables.Count > 1)
{
var priorities = result.Actionables.Select(GetPriorityOrder).ToList();
Assert.True(priorities.SequenceEqual(priorities.Order()));
}
}
[Fact]
public async Task GetActionablesByPriority_Critical_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-priority/critical");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ActionablesResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.All(result!.Actionables, a => Assert.Equal("critical", a.Priority, StringComparer.OrdinalIgnoreCase));
}
[Fact]
public async Task GetActionablesByPriority_InvalidPriority_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-priority/invalid");
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task GetActionablesByType_Upgrade_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/upgrade");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ActionablesResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.All(result!.Actionables, a => Assert.Equal("upgrade", a.Type, StringComparer.OrdinalIgnoreCase));
}
[Fact]
public async Task GetActionablesByType_Vex_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/vex");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ActionablesResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.All(result!.Actionables, a => Assert.Equal("vex", a.Type, StringComparer.OrdinalIgnoreCase));
}
[Fact]
public async Task GetActionablesByType_InvalidType_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/invalid");
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task GetDeltaActionables_IncludesEstimatedEffort()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678");
var result = await response.Content.ReadFromJsonAsync<ActionablesResponseDto>(SerializerOptions);
Assert.NotNull(result);
foreach (var actionable in result!.Actionables)
{
Assert.NotNull(actionable.EstimatedEffort);
Assert.Contains(actionable.EstimatedEffort, new[] { "trivial", "low", "medium", "high" });
}
}
private static int GetPriorityOrder(ActionableDto actionable)
{
return actionable.Priority.ToLowerInvariant() switch
{
"critical" => 0,
"high" => 1,
"medium" => 2,
"low" => 3,
_ => 4
};
}
}

View File

@@ -0,0 +1,114 @@
// -----------------------------------------------------------------------------
// BaselineEndpointsTests.cs
// Sprint: SPRINT_4200_0002_0006_delta_compare_api
// Description: Integration tests for baseline selection endpoints.
// -----------------------------------------------------------------------------
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
/// <summary>
/// Integration tests for baseline selection endpoints.
/// </summary>
public sealed class BaselineEndpointsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
[Fact]
public async Task GetRecommendations_ValidDigest_ReturnsRecommendations()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<BaselineRecommendationsResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Equal("sha256:artifact123", result!.ArtifactDigest);
Assert.NotEmpty(result.Recommendations);
Assert.Contains(result.Recommendations, r => r.IsDefault);
}
[Fact]
public async Task GetRecommendations_WithEnvironment_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123?environment=production");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<BaselineRecommendationsResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.NotEmpty(result!.Recommendations);
}
[Fact]
public async Task GetRecommendations_IncludesRationale()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123");
var result = await response.Content.ReadFromJsonAsync<BaselineRecommendationsResponseDto>(SerializerOptions);
Assert.NotNull(result);
foreach (var rec in result!.Recommendations)
{
Assert.NotEmpty(rec.Rationale);
Assert.NotEmpty(rec.Type);
Assert.NotEmpty(rec.Label);
}
}
[Fact]
public async Task GetRationale_ValidDigests_ReturnsDetailedRationale()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/rationale/sha256:base123/sha256:head456");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<BaselineRationaleResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Equal("sha256:base123", result!.BaseDigest);
Assert.Equal("sha256:head456", result.HeadDigest);
Assert.NotEmpty(result.SelectionType);
Assert.NotEmpty(result.Rationale);
Assert.NotEmpty(result.DetailedExplanation);
}
[Fact]
public async Task GetRationale_IncludesSelectionCriteria()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/rationale/sha256:baseline-base123/sha256:head456");
var result = await response.Content.ReadFromJsonAsync<BaselineRationaleResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.NotNull(result!.SelectionCriteria);
Assert.NotEmpty(result.SelectionCriteria);
}
[Fact]
public async Task GetRecommendations_DefaultIsFirst()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123");
var result = await response.Content.ReadFromJsonAsync<BaselineRecommendationsResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.NotEmpty(result!.Recommendations);
Assert.True(result.Recommendations[0].IsDefault);
}
}

View File

@@ -0,0 +1,218 @@
// -----------------------------------------------------------------------------
// CounterfactualEndpointsTests.cs
// Sprint: SPRINT_4200_0002_0005_counterfactuals
// Description: Integration tests for counterfactual analysis endpoints.
// -----------------------------------------------------------------------------
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using StellaOps.Scanner.WebService.Endpoints;
namespace StellaOps.Scanner.WebService.Tests;
/// <summary>
/// Integration tests for counterfactual analysis endpoints.
/// </summary>
public sealed class CounterfactualEndpointsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
[Fact]
public async Task PostCompute_ValidRequest_ReturnsCounterfactuals()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
{
FindingId = "finding-123",
VulnId = "CVE-2021-44228",
Purl = "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
CurrentVerdict = "Block"
};
var response = await client.PostAsJsonAsync("/api/v1/counterfactuals/compute", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<CounterfactualResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Equal("finding-123", result!.FindingId);
Assert.Equal("Block", result.CurrentVerdict);
Assert.True(result.HasPaths);
Assert.NotEmpty(result.Paths);
Assert.NotEmpty(result.WouldPassIf);
}
[Fact]
public async Task PostCompute_MissingFindingId_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
{
FindingId = "",
VulnId = "CVE-2021-44228"
};
var response = await client.PostAsJsonAsync("/api/v1/counterfactuals/compute", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PostCompute_IncludesVexPath()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
{
FindingId = "finding-123",
VulnId = "CVE-2021-44228",
CurrentVerdict = "Block"
};
var response = await client.PostAsJsonAsync("/api/v1/counterfactuals/compute", request);
var result = await response.Content.ReadFromJsonAsync<CounterfactualResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Contains(result!.Paths, p => p.Type == "Vex");
}
[Fact]
public async Task PostCompute_IncludesReachabilityPath()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
{
FindingId = "finding-123",
VulnId = "CVE-2021-44228",
CurrentVerdict = "Block"
};
var response = await client.PostAsJsonAsync("/api/v1/counterfactuals/compute", request);
var result = await response.Content.ReadFromJsonAsync<CounterfactualResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Contains(result!.Paths, p => p.Type == "Reachability");
}
[Fact]
public async Task PostCompute_IncludesExceptionPath()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
{
FindingId = "finding-123",
VulnId = "CVE-2021-44228",
CurrentVerdict = "Block"
};
var response = await client.PostAsJsonAsync("/api/v1/counterfactuals/compute", request);
var result = await response.Content.ReadFromJsonAsync<CounterfactualResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Contains(result!.Paths, p => p.Type == "Exception");
}
[Fact]
public async Task PostCompute_WithMaxPaths_LimitsResults()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
{
FindingId = "finding-123",
VulnId = "CVE-2021-44228",
CurrentVerdict = "Block",
MaxPaths = 2
};
var response = await client.PostAsJsonAsync("/api/v1/counterfactuals/compute", request);
var result = await response.Content.ReadFromJsonAsync<CounterfactualResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.True(result!.Paths.Count <= 2);
}
[Fact]
public async Task GetForFinding_ValidId_ReturnsCounterfactuals()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/counterfactuals/finding/finding-123");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<CounterfactualResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Equal("finding-123", result!.FindingId);
}
[Fact]
public async Task GetScanSummary_ValidId_ReturnsSummary()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/counterfactuals/scan/scan-123/summary");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<CounterfactualScanSummaryDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Equal("scan-123", result!.ScanId);
Assert.NotNull(result.Findings);
}
[Fact]
public async Task GetScanSummary_IncludesPathCounts()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/counterfactuals/scan/scan-123/summary");
var result = await response.Content.ReadFromJsonAsync<CounterfactualScanSummaryDto>(SerializerOptions);
Assert.NotNull(result);
Assert.True(result!.TotalBlocked >= 0);
Assert.True(result.WithVexPath >= 0);
Assert.True(result.WithReachabilityPath >= 0);
Assert.True(result.WithUpgradePath >= 0);
Assert.True(result.WithExceptionPath >= 0);
}
[Fact]
public async Task PostCompute_PathsHaveConditions()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
{
FindingId = "finding-123",
VulnId = "CVE-2021-44228",
CurrentVerdict = "Block"
};
var response = await client.PostAsJsonAsync("/api/v1/counterfactuals/compute", request);
var result = await response.Content.ReadFromJsonAsync<CounterfactualResponseDto>(SerializerOptions);
Assert.NotNull(result);
foreach (var path in result!.Paths)
{
Assert.NotEmpty(path.Description);
Assert.NotEmpty(path.Conditions);
foreach (var condition in path.Conditions)
{
Assert.NotEmpty(condition.Field);
Assert.NotEmpty(condition.RequiredValue);
}
}
}
}

View File

@@ -0,0 +1,140 @@
// -----------------------------------------------------------------------------
// DeltaCompareEndpointsTests.cs
// Sprint: SPRINT_4200_0002_0006_delta_compare_api
// Description: Integration tests for delta compare endpoints.
// -----------------------------------------------------------------------------
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
/// <summary>
/// Integration tests for delta compare endpoints.
/// </summary>
public sealed class DeltaCompareEndpointsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
[Fact]
public async Task PostCompare_ValidRequest_ReturnsComparisonResult()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new DeltaCompareRequestDto
{
BaseDigest = "sha256:base123",
TargetDigest = "sha256:target456",
IncludeVulnerabilities = true,
IncludeComponents = true,
IncludePolicyDiff = true
};
var response = await client.PostAsJsonAsync("/api/v1/delta/compare", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<DeltaCompareResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.NotNull(result!.Base);
Assert.NotNull(result.Target);
Assert.NotNull(result.Summary);
Assert.NotEmpty(result.ComparisonId);
Assert.Equal("sha256:base123", result.Base.Digest);
Assert.Equal("sha256:target456", result.Target.Digest);
}
[Fact]
public async Task PostCompare_MissingBaseDigest_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new DeltaCompareRequestDto
{
BaseDigest = "",
TargetDigest = "sha256:target456"
};
var response = await client.PostAsJsonAsync("/api/v1/delta/compare", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task PostCompare_MissingTargetDigest_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new DeltaCompareRequestDto
{
BaseDigest = "sha256:base123",
TargetDigest = ""
};
var response = await client.PostAsJsonAsync("/api/v1/delta/compare", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task GetQuickDiff_ValidDigests_ReturnsQuickSummary()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/delta/quick?baseDigest=sha256:base123&targetDigest=sha256:target456");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<QuickDiffSummaryDto>(SerializerOptions);
Assert.NotNull(result);
Assert.Equal("sha256:base123", result!.BaseDigest);
Assert.Equal("sha256:target456", result.TargetDigest);
Assert.NotEmpty(result.RiskDirection);
Assert.NotEmpty(result.Summary);
}
[Fact]
public async Task GetQuickDiff_MissingDigest_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/delta/quick?baseDigest=sha256:base123");
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task GetComparison_NotFound_ReturnsNotFound()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/delta/nonexistent-id");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PostCompare_DeterministicComparisonId_SameInputsSameId()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new DeltaCompareRequestDto
{
BaseDigest = "sha256:base123",
TargetDigest = "sha256:target456"
};
var response1 = await client.PostAsJsonAsync("/api/v1/delta/compare", request);
var result1 = await response1.Content.ReadFromJsonAsync<DeltaCompareResponseDto>(SerializerOptions);
var response2 = await client.PostAsJsonAsync("/api/v1/delta/compare", request);
var result2 = await response2.Content.ReadFromJsonAsync<DeltaCompareResponseDto>(SerializerOptions);
Assert.NotNull(result1);
Assert.NotNull(result2);
Assert.Equal(result1!.ComparisonId, result2!.ComparisonId);
}
}

View File

@@ -0,0 +1,193 @@
// -----------------------------------------------------------------------------
// TriageStatusEndpointsTests.cs
// Sprint: SPRINT_4200_0001_0001_triage_rest_api
// Description: Integration tests for triage status endpoints.
// -----------------------------------------------------------------------------
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
/// <summary>
/// Integration tests for triage status endpoints.
/// </summary>
public sealed class TriageStatusEndpointsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
[Fact]
public async Task GetFindingStatus_NotFound_ReturnsNotFound()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/triage/findings/nonexistent-finding");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PostUpdateStatus_ValidRequest_ReturnsUpdatedStatus()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new UpdateTriageStatusRequestDto
{
Lane = "MutedVex",
DecisionKind = "VexNotAffected",
Reason = "Vendor confirms not affected"
};
var response = await client.PostAsJsonAsync("/api/v1/triage/findings/finding-123/status", request);
// Note: Will return 404 since finding doesn't exist in test context
Assert.True(response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.NotFound);
}
[Fact]
public async Task PostVexStatement_ValidRequest_ReturnsResponse()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new SubmitVexStatementRequestDto
{
Status = "NotAffected",
Justification = "vulnerable_code_not_in_execute_path",
ImpactStatement = "Code path analysis shows vulnerability is not reachable"
};
var response = await client.PostAsJsonAsync("/api/v1/triage/findings/finding-123/vex", request);
// Note: Will return 404 since finding doesn't exist in test context
Assert.True(response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.NotFound);
}
[Fact]
public async Task PostQuery_EmptyFilters_ReturnsResults()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new BulkTriageQueryRequestDto
{
Limit = 10
};
var response = await client.PostAsJsonAsync("/api/v1/triage/query", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<BulkTriageQueryResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.NotNull(result!.Findings);
Assert.NotNull(result.Summary);
}
[Fact]
public async Task PostQuery_WithLaneFilter_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new BulkTriageQueryRequestDto
{
Lanes = ["Active", "Blocked"],
Limit = 10
};
var response = await client.PostAsJsonAsync("/api/v1/triage/query", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<BulkTriageQueryResponseDto>(SerializerOptions);
Assert.NotNull(result);
}
[Fact]
public async Task PostQuery_WithVerdictFilter_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new BulkTriageQueryRequestDto
{
Verdicts = ["Block"],
Limit = 10
};
var response = await client.PostAsJsonAsync("/api/v1/triage/query", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<BulkTriageQueryResponseDto>(SerializerOptions);
Assert.NotNull(result);
}
[Fact]
public async Task GetSummary_ValidDigest_ReturnsSummary()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<TriageSummaryDto>(SerializerOptions);
Assert.NotNull(result);
Assert.NotNull(result!.ByLane);
Assert.NotNull(result.ByVerdict);
}
[Fact]
public async Task GetSummary_IncludesAllLanes()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123");
var result = await response.Content.ReadFromJsonAsync<TriageSummaryDto>(SerializerOptions);
Assert.NotNull(result);
var expectedLanes = new[] { "Active", "Blocked", "NeedsException", "MutedReach", "MutedVex", "Compensated" };
foreach (var lane in expectedLanes)
{
Assert.True(result!.ByLane.ContainsKey(lane), $"Expected lane '{lane}' to be present");
}
}
[Fact]
public async Task GetSummary_IncludesAllVerdicts()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123");
var result = await response.Content.ReadFromJsonAsync<TriageSummaryDto>(SerializerOptions);
Assert.NotNull(result);
var expectedVerdicts = new[] { "Ship", "Block", "Exception" };
foreach (var verdict in expectedVerdicts)
{
Assert.True(result!.ByVerdict.ContainsKey(verdict), $"Expected verdict '{verdict}' to be present");
}
}
[Fact]
public async Task PostQuery_ResponseIncludesSummary()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new BulkTriageQueryRequestDto
{
Limit = 10
};
var response = await client.PostAsJsonAsync("/api/v1/triage/query", request);
var result = await response.Content.ReadFromJsonAsync<BulkTriageQueryResponseDto>(SerializerOptions);
Assert.NotNull(result);
Assert.NotNull(result!.Summary);
Assert.True(result.Summary.CanShipCount >= 0);
Assert.True(result.Summary.BlockingCount >= 0);
}
}