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,309 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user