- 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.
310 lines
11 KiB
C#
310 lines
11 KiB
C#
// -----------------------------------------------------------------------------
|
|
// ActionablesEndpoints.cs
|
|
// Sprint: SPRINT_4200_0002_0006_delta_compare_api
|
|
// Description: HTTP endpoints for actionable remediation recommendations.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.Scanner.WebService.Contracts;
|
|
using StellaOps.Scanner.WebService.Security;
|
|
|
|
namespace StellaOps.Scanner.WebService.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Endpoints for actionable remediation recommendations.
|
|
/// Per SPRINT_4200_0002_0006 T3.
|
|
/// </summary>
|
|
internal static class ActionablesEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Maps actionables endpoints.
|
|
/// </summary>
|
|
public static void MapActionablesEndpoints(this RouteGroupBuilder apiGroup, string prefix = "/actionables")
|
|
{
|
|
ArgumentNullException.ThrowIfNull(apiGroup);
|
|
|
|
var group = apiGroup.MapGroup(prefix)
|
|
.WithTags("Actionables");
|
|
|
|
// GET /v1/actionables/delta/{deltaId} - Get actionables for a delta
|
|
group.MapGet("/delta/{deltaId}", HandleGetDeltaActionablesAsync)
|
|
.WithName("scanner.actionables.delta")
|
|
.WithDescription("Get actionable recommendations for a delta comparison.")
|
|
.Produces<ActionablesResponseDto>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.RequireAuthorization(ScannerPolicies.ScansRead);
|
|
|
|
// GET /v1/actionables/delta/{deltaId}/by-priority/{priority} - Filter by priority
|
|
group.MapGet("/delta/{deltaId}/by-priority/{priority}", HandleGetActionablesByPriorityAsync)
|
|
.WithName("scanner.actionables.by-priority")
|
|
.WithDescription("Get actionables filtered by priority level.")
|
|
.Produces<ActionablesResponseDto>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.RequireAuthorization(ScannerPolicies.ScansRead);
|
|
|
|
// GET /v1/actionables/delta/{deltaId}/by-type/{type} - Filter by type
|
|
group.MapGet("/delta/{deltaId}/by-type/{type}", HandleGetActionablesByTypeAsync)
|
|
.WithName("scanner.actionables.by-type")
|
|
.WithDescription("Get actionables filtered by action type.")
|
|
.Produces<ActionablesResponseDto>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.RequireAuthorization(ScannerPolicies.ScansRead);
|
|
}
|
|
|
|
private static async Task<IResult> HandleGetDeltaActionablesAsync(
|
|
string deltaId,
|
|
IActionablesService actionablesService,
|
|
HttpContext context,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(actionablesService);
|
|
|
|
if (string.IsNullOrWhiteSpace(deltaId))
|
|
{
|
|
return Results.BadRequest(new
|
|
{
|
|
type = "validation-error",
|
|
title = "Invalid delta ID",
|
|
detail = "Delta ID is required."
|
|
});
|
|
}
|
|
|
|
var actionables = await actionablesService.GenerateForDeltaAsync(deltaId, cancellationToken);
|
|
|
|
if (actionables is null)
|
|
{
|
|
return Results.NotFound(new
|
|
{
|
|
type = "not-found",
|
|
title = "Delta not found",
|
|
detail = $"Delta with ID '{deltaId}' was not found."
|
|
});
|
|
}
|
|
|
|
return Results.Ok(actionables);
|
|
}
|
|
|
|
private static async Task<IResult> HandleGetActionablesByPriorityAsync(
|
|
string deltaId,
|
|
string priority,
|
|
IActionablesService actionablesService,
|
|
HttpContext context,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(actionablesService);
|
|
|
|
var validPriorities = new[] { "critical", "high", "medium", "low" };
|
|
if (!validPriorities.Contains(priority, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
return Results.BadRequest(new
|
|
{
|
|
type = "validation-error",
|
|
title = "Invalid priority",
|
|
detail = $"Priority must be one of: {string.Join(", ", validPriorities)}"
|
|
});
|
|
}
|
|
|
|
var allActionables = await actionablesService.GenerateForDeltaAsync(deltaId, cancellationToken);
|
|
|
|
if (allActionables is null)
|
|
{
|
|
return Results.NotFound(new
|
|
{
|
|
type = "not-found",
|
|
title = "Delta not found",
|
|
detail = $"Delta with ID '{deltaId}' was not found."
|
|
});
|
|
}
|
|
|
|
var filtered = allActionables.Actionables
|
|
.Where(a => a.Priority.Equals(priority, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
return Results.Ok(new ActionablesResponseDto
|
|
{
|
|
DeltaId = deltaId,
|
|
Actionables = filtered,
|
|
GeneratedAt = allActionables.GeneratedAt
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> HandleGetActionablesByTypeAsync(
|
|
string deltaId,
|
|
string type,
|
|
IActionablesService actionablesService,
|
|
HttpContext context,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(actionablesService);
|
|
|
|
var validTypes = new[] { "upgrade", "patch", "vex", "config", "investigate" };
|
|
if (!validTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
return Results.BadRequest(new
|
|
{
|
|
type = "validation-error",
|
|
title = "Invalid type",
|
|
detail = $"Type must be one of: {string.Join(", ", validTypes)}"
|
|
});
|
|
}
|
|
|
|
var allActionables = await actionablesService.GenerateForDeltaAsync(deltaId, cancellationToken);
|
|
|
|
if (allActionables is null)
|
|
{
|
|
return Results.NotFound(new
|
|
{
|
|
type = "not-found",
|
|
title = "Delta not found",
|
|
detail = $"Delta with ID '{deltaId}' was not found."
|
|
});
|
|
}
|
|
|
|
var filtered = allActionables.Actionables
|
|
.Where(a => a.Type.Equals(type, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
return Results.Ok(new ActionablesResponseDto
|
|
{
|
|
DeltaId = deltaId,
|
|
Actionables = filtered,
|
|
GeneratedAt = allActionables.GeneratedAt
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Service interface for actionables generation.
|
|
/// Per SPRINT_4200_0002_0006 T3.
|
|
/// </summary>
|
|
public interface IActionablesService
|
|
{
|
|
/// <summary>
|
|
/// Generates actionable recommendations for a delta.
|
|
/// </summary>
|
|
Task<ActionablesResponseDto?> GenerateForDeltaAsync(string deltaId, CancellationToken ct = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default implementation of actionables service.
|
|
/// </summary>
|
|
public sealed class ActionablesService : IActionablesService
|
|
{
|
|
private readonly TimeProvider _timeProvider;
|
|
private readonly IDeltaCompareService _deltaService;
|
|
|
|
public ActionablesService(TimeProvider timeProvider, IDeltaCompareService deltaService)
|
|
{
|
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
|
_deltaService = deltaService ?? throw new ArgumentNullException(nameof(deltaService));
|
|
}
|
|
|
|
public async Task<ActionablesResponseDto?> GenerateForDeltaAsync(string deltaId, CancellationToken ct = default)
|
|
{
|
|
// In a full implementation, this would retrieve the delta and generate
|
|
// actionables based on the findings. For now, return sample actionables.
|
|
|
|
var delta = await _deltaService.GetComparisonAsync(deltaId, ct);
|
|
|
|
// Even if delta is null, we can still generate sample actionables for demo
|
|
var actionables = new List<ActionableDto>();
|
|
|
|
// Sample upgrade actionable
|
|
actionables.Add(new ActionableDto
|
|
{
|
|
Id = $"action-upgrade-{deltaId[..8]}",
|
|
Type = "upgrade",
|
|
Priority = "critical",
|
|
Title = "Upgrade log4j to fix CVE-2021-44228",
|
|
Description = "Upgrade log4j from 2.14.1 to 2.17.1 to remediate the Log4Shell vulnerability. " +
|
|
"This is a critical remote code execution vulnerability.",
|
|
Component = "pkg:maven/org.apache.logging.log4j/log4j-core",
|
|
CurrentVersion = "2.14.1",
|
|
TargetVersion = "2.17.1",
|
|
CveIds = ["CVE-2021-44228", "CVE-2021-45046"],
|
|
EstimatedEffort = "low",
|
|
Evidence = new ActionableEvidenceDto
|
|
{
|
|
PolicyRuleId = "rule-critical-cve"
|
|
}
|
|
});
|
|
|
|
// Sample VEX actionable
|
|
actionables.Add(new ActionableDto
|
|
{
|
|
Id = $"action-vex-{deltaId[..8]}",
|
|
Type = "vex",
|
|
Priority = "high",
|
|
Title = "Submit VEX statement for CVE-2023-12345",
|
|
Description = "Reachability analysis shows the vulnerable function is not called. " +
|
|
"Consider submitting a VEX statement with status 'not_affected' and justification " +
|
|
"'vulnerable_code_not_in_execute_path'.",
|
|
Component = "pkg:npm/example-lib",
|
|
CveIds = ["CVE-2023-12345"],
|
|
EstimatedEffort = "trivial",
|
|
Evidence = new ActionableEvidenceDto
|
|
{
|
|
WitnessId = "witness-12345"
|
|
}
|
|
});
|
|
|
|
// Sample investigate actionable
|
|
actionables.Add(new ActionableDto
|
|
{
|
|
Id = $"action-investigate-{deltaId[..8]}",
|
|
Type = "investigate",
|
|
Priority = "medium",
|
|
Title = "Review reachability change for CVE-2023-67890",
|
|
Description = "Code path reachability changed from 'No' to 'Yes'. Review if the vulnerable " +
|
|
"function is now actually reachable from an entrypoint.",
|
|
Component = "pkg:pypi/requests",
|
|
CveIds = ["CVE-2023-67890"],
|
|
EstimatedEffort = "medium",
|
|
Evidence = new ActionableEvidenceDto
|
|
{
|
|
WitnessId = "witness-67890"
|
|
}
|
|
});
|
|
|
|
// Sample config actionable
|
|
actionables.Add(new ActionableDto
|
|
{
|
|
Id = $"action-config-{deltaId[..8]}",
|
|
Type = "config",
|
|
Priority = "low",
|
|
Title = "New component detected: review security requirements",
|
|
Description = "New dependency 'pkg:npm/axios@1.6.0' was added. Verify it meets security " +
|
|
"requirements and is from a trusted source.",
|
|
Component = "pkg:npm/axios",
|
|
CurrentVersion = "1.6.0",
|
|
EstimatedEffort = "trivial"
|
|
});
|
|
|
|
// Sort by priority
|
|
var sortedActionables = actionables
|
|
.OrderBy(a => GetPriorityOrder(a.Priority))
|
|
.ThenBy(a => a.Title, StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
return new ActionablesResponseDto
|
|
{
|
|
DeltaId = deltaId,
|
|
Actionables = sortedActionables,
|
|
GeneratedAt = _timeProvider.GetUtcNow()
|
|
};
|
|
}
|
|
|
|
private static int GetPriorityOrder(string priority)
|
|
{
|
|
return priority.ToLowerInvariant() switch
|
|
{
|
|
"critical" => 0,
|
|
"high" => 1,
|
|
"medium" => 2,
|
|
"low" => 3,
|
|
_ => 4
|
|
};
|
|
}
|
|
}
|