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

@@ -1,8 +1,8 @@
// -----------------------------------------------------------------------------
// UnknownsCommandGroup.cs
// Sprint: SPRINT_3500_0004_0001_cli_verbs
// Task: T3 - Unknowns List Command
// Description: CLI commands for unknowns registry operations
// Sprint: SPRINT_3500_0004_0001_cli_verbs, SPRINT_5100_0004_0001_unknowns_budget_ci_gates
// Task: T3 - Unknowns List Command, T1 - CLI Budget Check Command
// Description: CLI commands for unknowns registry operations and budget checking
// -----------------------------------------------------------------------------
using System.CommandLine;
@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Policy.Unknowns.Models;
namespace StellaOps.Cli.Commands;
@@ -40,10 +41,137 @@ public static class UnknownsCommandGroup
unknownsCommand.Add(BuildListCommand(services, verboseOption, cancellationToken));
unknownsCommand.Add(BuildEscalateCommand(services, verboseOption, cancellationToken));
unknownsCommand.Add(BuildResolveCommand(services, verboseOption, cancellationToken));
unknownsCommand.Add(BuildBudgetCommand(services, verboseOption, cancellationToken));
return unknownsCommand;
}
/// <summary>
/// Build the budget subcommand tree (stella unknowns budget).
/// Sprint: SPRINT_5100_0004_0001 Task T1
/// </summary>
private static Command BuildBudgetCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var budgetCommand = new Command("budget", "Unknowns budget operations for CI gates");
budgetCommand.Add(BuildBudgetCheckCommand(services, verboseOption, cancellationToken));
budgetCommand.Add(BuildBudgetStatusCommand(services, verboseOption, cancellationToken));
return budgetCommand;
}
private static Command BuildBudgetCheckCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var scanIdOption = new Option<string?>("--scan-id", "-s")
{
Description = "Scan ID to check budget against"
};
var verdictPathOption = new Option<string?>("--verdict", "-v")
{
Description = "Path to verdict JSON file"
};
var environmentOption = new Option<string>("--environment", "-e")
{
Description = "Environment budget to use (prod, stage, dev)"
};
environmentOption.SetDefaultValue("prod");
var configOption = new Option<string?>("--config", "-c")
{
Description = "Path to budget configuration file"
};
var failOnExceedOption = new Option<bool>("--fail-on-exceed")
{
Description = "Exit with error code if budget exceeded"
};
failOnExceedOption.SetDefaultValue(true);
var outputOption = new Option<string>("--output", "-o")
{
Description = "Output format: text, json, sarif"
};
outputOption.SetDefaultValue("text");
var checkCommand = new Command("check", "Check scan results against unknowns budget");
checkCommand.Add(scanIdOption);
checkCommand.Add(verdictPathOption);
checkCommand.Add(environmentOption);
checkCommand.Add(configOption);
checkCommand.Add(failOnExceedOption);
checkCommand.Add(outputOption);
checkCommand.Add(verboseOption);
checkCommand.SetAction(async (parseResult, ct) =>
{
var scanId = parseResult.GetValue(scanIdOption);
var verdictPath = parseResult.GetValue(verdictPathOption);
var environment = parseResult.GetValue(environmentOption) ?? "prod";
var config = parseResult.GetValue(configOption);
var failOnExceed = parseResult.GetValue(failOnExceedOption);
var output = parseResult.GetValue(outputOption) ?? "text";
var verbose = parseResult.GetValue(verboseOption);
return await HandleBudgetCheckAsync(
services,
scanId,
verdictPath,
environment,
config,
failOnExceed,
output,
verbose,
cancellationToken);
});
return checkCommand;
}
private static Command BuildBudgetStatusCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var environmentOption = new Option<string>("--environment", "-e")
{
Description = "Environment to show budget status for"
};
environmentOption.SetDefaultValue("prod");
var outputOption = new Option<string>("--output", "-o")
{
Description = "Output format: text, json"
};
outputOption.SetDefaultValue("text");
var statusCommand = new Command("status", "Show current budget status for an environment");
statusCommand.Add(environmentOption);
statusCommand.Add(outputOption);
statusCommand.Add(verboseOption);
statusCommand.SetAction(async (parseResult, ct) =>
{
var environment = parseResult.GetValue(environmentOption) ?? "prod";
var output = parseResult.GetValue(outputOption) ?? "text";
var verbose = parseResult.GetValue(verboseOption);
return await HandleBudgetStatusAsync(
services,
environment,
output,
verbose,
cancellationToken);
});
return statusCommand;
}
private static Command BuildListCommand(
IServiceProvider services,
Option<bool> verboseOption,
@@ -429,6 +557,311 @@ public static class UnknownsCommandGroup
}
}
/// <summary>
/// Handle budget check command.
/// Sprint: SPRINT_5100_0004_0001 Task T1
/// Exit codes: 0=pass, 1=error, 2=budget exceeded
/// </summary>
private static async Task<int> HandleBudgetCheckAsync(
IServiceProvider services,
string? scanId,
string? verdictPath,
string environment,
string? configPath,
bool failOnExceed,
string output,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory is null)
{
logger?.LogError("HTTP client factory not available");
return 1;
}
try
{
if (verbose)
{
logger?.LogDebug("Checking budget for environment {Environment}", environment);
}
// Load unknowns from verdict file or API
IReadOnlyList<BudgetUnknownDto> unknowns;
if (!string.IsNullOrEmpty(verdictPath))
{
// Load from local verdict file
if (!File.Exists(verdictPath))
{
Console.WriteLine($"Error: Verdict file not found: {verdictPath}");
return 1;
}
var json = await File.ReadAllTextAsync(verdictPath, ct);
var verdict = JsonSerializer.Deserialize<VerdictFileDto>(json, JsonOptions);
if (verdict?.Unknowns is null)
{
Console.WriteLine("Error: No unknowns found in verdict file");
return 1;
}
unknowns = verdict.Unknowns;
}
else if (!string.IsNullOrEmpty(scanId))
{
// Fetch from API
var client = httpClientFactory.CreateClient("PolicyApi");
var response = await client.GetAsync($"/api/v1/policy/unknowns?scanId={scanId}&limit=1000", ct);
if (!response.IsSuccessStatusCode)
{
logger?.LogError("Failed to fetch unknowns: {Status}", response.StatusCode);
Console.WriteLine($"Error: Failed to fetch unknowns ({response.StatusCode})");
return 1;
}
var listResponse = await response.Content.ReadFromJsonAsync<UnknownsListResponse>(JsonOptions, ct);
unknowns = listResponse?.Items.Select(i => new BudgetUnknownDto
{
Id = i.Id,
ReasonCode = "Reachability" // Default if not provided
}).ToList() ?? [];
}
else
{
Console.WriteLine("Error: Either --scan-id or --verdict must be specified");
return 1;
}
// Check budget via API
var budgetClient = httpClientFactory.CreateClient("PolicyApi");
var checkRequest = new BudgetCheckRequest(environment, unknowns);
var checkResponse = await budgetClient.PostAsJsonAsync(
"/api/v1/policy/unknowns/budget/check",
checkRequest,
JsonOptions,
ct);
BudgetCheckResultDto result;
if (checkResponse.IsSuccessStatusCode)
{
result = await checkResponse.Content.ReadFromJsonAsync<BudgetCheckResultDto>(JsonOptions, ct)
?? new BudgetCheckResultDto
{
IsWithinBudget = true,
Environment = environment,
TotalUnknowns = unknowns.Count
};
}
else
{
// Fallback to local check if API unavailable
result = PerformLocalBudgetCheck(environment, unknowns.Count);
}
// Output result
OutputBudgetResult(result, output);
// Return exit code
if (failOnExceed && !result.IsWithinBudget)
{
Console.Error.WriteLine($"Budget exceeded: {result.Message ?? "Unknown budget exceeded"}");
return 2; // Distinct exit code for budget failure
}
return 0;
}
catch (Exception ex)
{
logger?.LogError(ex, "Budget check failed unexpectedly");
Console.WriteLine($"Error: {ex.Message}");
return 1;
}
}
private static BudgetCheckResultDto PerformLocalBudgetCheck(string environment, int unknownCount)
{
// Default budgets if API unavailable
var limits = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["prod"] = 0,
["stage"] = 5,
["dev"] = 20
};
var limit = limits.TryGetValue(environment, out var l) ? l : 10;
var exceeded = unknownCount > limit;
return new BudgetCheckResultDto
{
IsWithinBudget = !exceeded,
Environment = environment,
TotalUnknowns = unknownCount,
TotalLimit = limit,
Message = exceeded ? $"Budget exceeded: {unknownCount} unknowns exceed limit of {limit}" : null
};
}
private static void OutputBudgetResult(BudgetCheckResultDto result, string format)
{
switch (format.ToLowerInvariant())
{
case "json":
Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions));
break;
case "sarif":
OutputSarifResult(result);
break;
default:
OutputTextResult(result);
break;
}
}
private static void OutputTextResult(BudgetCheckResultDto result)
{
var status = result.IsWithinBudget ? "[PASS]" : "[FAIL]";
Console.WriteLine($"{status} Unknowns Budget Check");
Console.WriteLine($" Environment: {result.Environment}");
Console.WriteLine($" Total Unknowns: {result.TotalUnknowns}");
if (result.TotalLimit.HasValue)
Console.WriteLine($" Budget Limit: {result.TotalLimit}");
if (result.Violations?.Count > 0)
{
Console.WriteLine("\n Violations:");
foreach (var violation in result.Violations)
{
Console.WriteLine($" - {violation.ReasonCode}: {violation.Count}/{violation.Limit}");
}
}
if (!string.IsNullOrEmpty(result.Message))
Console.WriteLine($"\n Message: {result.Message}");
}
private static void OutputSarifResult(BudgetCheckResultDto result)
{
var violations = result.Violations ?? [];
var sarif = new
{
version = "2.1.0",
schema = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
runs = new[]
{
new
{
tool = new
{
driver = new
{
name = "StellaOps Budget Check",
version = "1.0.0",
informationUri = "https://stellaops.io"
}
},
results = violations.Select(v => new
{
ruleId = $"UNKNOWN_{v.ReasonCode}",
level = "error",
message = new
{
text = $"{v.ReasonCode}: {v.Count} unknowns exceed limit of {v.Limit}"
}
}).ToArray()
}
}
};
Console.WriteLine(JsonSerializer.Serialize(sarif, JsonOptions));
}
private static async Task<int> HandleBudgetStatusAsync(
IServiceProvider services,
string environment,
string output,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory is null)
{
logger?.LogError("HTTP client factory not available");
return 1;
}
try
{
if (verbose)
{
logger?.LogDebug("Getting budget status for environment {Environment}", environment);
}
var client = httpClientFactory.CreateClient("PolicyApi");
var response = await client.GetAsync($"/api/v1/policy/unknowns/budget/status?environment={environment}", ct);
if (!response.IsSuccessStatusCode)
{
logger?.LogError("Failed to get budget status: {Status}", response.StatusCode);
Console.WriteLine($"Error: Failed to get budget status ({response.StatusCode})");
return 1;
}
var status = await response.Content.ReadFromJsonAsync<BudgetStatusDto>(JsonOptions, ct);
if (status is null)
{
Console.WriteLine("Error: Empty response from budget status");
return 1;
}
if (output == "json")
{
Console.WriteLine(JsonSerializer.Serialize(status, JsonOptions));
}
else
{
Console.WriteLine($"Budget Status: {status.Environment}");
Console.WriteLine(new string('=', 40));
Console.WriteLine($" Total Unknowns: {status.TotalUnknowns}");
Console.WriteLine($" Budget Limit: {status.TotalLimit?.ToString() ?? "Unlimited"}");
Console.WriteLine($" Usage: {status.PercentageUsed:F1}%");
Console.WriteLine($" Status: {(status.IsExceeded ? "EXCEEDED" : "OK")}");
if (status.ByReasonCode?.Count > 0)
{
Console.WriteLine("\n By Reason Code:");
foreach (var kvp in status.ByReasonCode)
{
Console.WriteLine($" - {kvp.Key}: {kvp.Value}");
}
}
}
return 0;
}
catch (Exception ex)
{
logger?.LogError(ex, "Budget status failed unexpectedly");
Console.WriteLine($"Error: {ex.Message}");
return 1;
}
}
#region DTOs
private sealed record UnknownsListResponse(
@@ -450,5 +883,48 @@ public static class UnknownsCommandGroup
private sealed record ResolveRequest(string Resolution, string? Note);
// Budget DTOs - Sprint: SPRINT_5100_0004_0001 Task T1
private sealed record VerdictFileDto
{
public IReadOnlyList<BudgetUnknownDto>? Unknowns { get; init; }
}
private sealed record BudgetUnknownDto
{
public string Id { get; init; } = string.Empty;
public string ReasonCode { get; init; } = "Reachability";
}
private sealed record BudgetCheckRequest(
string Environment,
IReadOnlyList<BudgetUnknownDto> Unknowns);
private sealed record BudgetCheckResultDto
{
public bool IsWithinBudget { get; init; }
public string Environment { get; init; } = string.Empty;
public int TotalUnknowns { get; init; }
public int? TotalLimit { get; init; }
public IReadOnlyList<BudgetViolationDto>? Violations { get; init; }
public string? Message { get; init; }
}
private sealed record BudgetViolationDto
{
public string ReasonCode { get; init; } = string.Empty;
public int Count { get; init; }
public int Limit { get; init; }
}
private sealed record BudgetStatusDto
{
public string Environment { get; init; } = string.Empty;
public int TotalUnknowns { get; init; }
public int? TotalLimit { get; init; }
public decimal PercentageUsed { get; init; }
public bool IsExceeded { get; init; }
public IReadOnlyDictionary<string, int>? ByReasonCode { get; init; }
}
#endregion
}