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:
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user