Files
git.stella-ops.org/src/Scanner/StellaOps.Scanner.WebService/Endpoints/ActionablesEndpoints.cs
StellaOps Bot 5146204f1b 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.
2025-12-22 23:21:21 +02:00

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
};
}
}