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

@@ -0,0 +1,134 @@
// -----------------------------------------------------------------------------
// DefaultBudgets.cs
// Sprint: SPRINT_4300_0002_0001 (Unknowns Budget Policy Integration)
// Task: BUDGET-015 - Implement default budgets
// Description: Default unknown budget configurations by environment.
// -----------------------------------------------------------------------------
using StellaOps.Policy.Unknowns.Models;
namespace StellaOps.Policy.Unknowns.Configuration;
/// <summary>
/// Provides default unknown budget configurations for common environments.
/// Advisory guidance: "Production should be strict (T2 max), staging should warn on T1."
/// </summary>
public static class DefaultBudgets
{
/// <summary>
/// Default budget for production environments.
/// Strict: T2 max tier, low count limits, block on exceed.
/// </summary>
public static UnknownBudget Production { get; } = new()
{
Environment = "production",
TotalLimit = 5,
ReasonLimits = new Dictionary<UnknownReasonCode, int>
{
[UnknownReasonCode.Reachability] = 0, // No reachability unknowns allowed
[UnknownReasonCode.Identity] = 2, // Max 2 identity unknowns
[UnknownReasonCode.Provenance] = 2, // Max 2 provenance unknowns
[UnknownReasonCode.VexConflict] = 0, // No VEX conflicts allowed
[UnknownReasonCode.FeedGap] = 5, // Some feed gaps tolerated
[UnknownReasonCode.ConfigUnknown] = 3, // Some config unknowns allowed
[UnknownReasonCode.AnalyzerLimit] = 5 // Analyzer limits are less critical
},
Action = BudgetAction.Block,
ExceededMessage = "Production deployment blocked: unknown budget exceeded. Review unknowns before proceeding."
};
/// <summary>
/// Default budget for staging environments.
/// Moderate: T1 warn, higher count limits, warn on exceed.
/// </summary>
public static UnknownBudget Staging { get; } = new()
{
Environment = "staging",
TotalLimit = 20,
ReasonLimits = new Dictionary<UnknownReasonCode, int>
{
[UnknownReasonCode.Reachability] = 5, // Some reachability unknowns allowed
[UnknownReasonCode.Identity] = 10, // More identity unknowns allowed
[UnknownReasonCode.Provenance] = 10, // More provenance unknowns allowed
[UnknownReasonCode.VexConflict] = 5, // Some VEX conflicts tolerated
[UnknownReasonCode.FeedGap] = 15, // More feed gaps tolerated
[UnknownReasonCode.ConfigUnknown] = 10, // More config unknowns allowed
[UnknownReasonCode.AnalyzerLimit] = 15 // Analyzer limits are informational
},
Action = BudgetAction.Warn,
ExceededMessage = "Staging deployment warning: unknown budget exceeded. Consider addressing before production."
};
/// <summary>
/// Default budget for development environments.
/// Permissive: High limits, warn only.
/// </summary>
public static UnknownBudget Development { get; } = new()
{
Environment = "development",
TotalLimit = 100,
ReasonLimits = new Dictionary<UnknownReasonCode, int>
{
[UnknownReasonCode.Reachability] = 25,
[UnknownReasonCode.Identity] = 50,
[UnknownReasonCode.Provenance] = 50,
[UnknownReasonCode.VexConflict] = 25,
[UnknownReasonCode.FeedGap] = 50,
[UnknownReasonCode.ConfigUnknown] = 50,
[UnknownReasonCode.AnalyzerLimit] = 50
},
Action = BudgetAction.Warn,
ExceededMessage = "Development environment unknown budget exceeded."
};
/// <summary>
/// Default budget when no environment-specific budget is configured.
/// Moderate: Similar to staging.
/// </summary>
public static UnknownBudget Default { get; } = new()
{
Environment = "default",
TotalLimit = 50,
ReasonLimits = new Dictionary<UnknownReasonCode, int>
{
[UnknownReasonCode.Reachability] = 10,
[UnknownReasonCode.Identity] = 20,
[UnknownReasonCode.Provenance] = 20,
[UnknownReasonCode.VexConflict] = 10,
[UnknownReasonCode.FeedGap] = 30,
[UnknownReasonCode.ConfigUnknown] = 20,
[UnknownReasonCode.AnalyzerLimit] = 25
},
Action = BudgetAction.Warn,
ExceededMessage = "Unknown budget exceeded."
};
/// <summary>
/// Gets the default budget for a given environment name.
/// </summary>
public static UnknownBudget GetDefaultForEnvironment(string? environment)
{
var normalized = environment?.Trim().ToLowerInvariant();
return normalized switch
{
"prod" or "production" => Production,
"stage" or "staging" => Staging,
"dev" or "development" => Development,
_ => Default
};
}
/// <summary>
/// Applies default budgets to an UnknownBudgetOptions instance.
/// </summary>
public static void ApplyDefaults(UnknownBudgetOptions options)
{
ArgumentNullException.ThrowIfNull(options);
options.Budgets.TryAdd("production", Production);
options.Budgets.TryAdd("staging", Staging);
options.Budgets.TryAdd("development", Development);
options.Budgets.TryAdd("default", Default);
}
}

View File

@@ -0,0 +1,211 @@
// -----------------------------------------------------------------------------
// BudgetExceededEventFactory.cs
// Sprint: SPRINT_4300_0002_0001 (Unknowns Budget Policy Integration)
// Task: BUDGET-018 - Create `UnknownBudgetExceeded` notification event
// Description: Factory for creating budget exceeded notification events.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using StellaOps.Policy.Unknowns.Models;
namespace StellaOps.Policy.Unknowns.Events;
/// <summary>
/// Factory for creating budget exceeded notification events.
/// </summary>
public static class BudgetExceededEventFactory
{
/// <summary>
/// Event kind for budget exceeded (blocking).
/// </summary>
public const string BudgetExceededKind = "policy.budget.exceeded";
/// <summary>
/// Event kind for budget warning (non-blocking).
/// </summary>
public const string BudgetWarningKind = "policy.budget.warning";
/// <summary>
/// Creates a budget exceeded notification event payload.
/// </summary>
public static BudgetEventPayload CreatePayload(
string environment,
BudgetCheckResult result,
string? imageDigest = null,
string? policyRevisionId = null)
{
ArgumentNullException.ThrowIfNull(result);
var violations = result.Violations
.Select(kvp => new BudgetViolationInfo(
kvp.Key.ToString(),
GetShortCode(kvp.Key),
kvp.Value.Count,
kvp.Value.Limit))
.ToImmutableList();
return new BudgetEventPayload
{
Environment = environment,
IsWithinBudget = result.IsWithinBudget,
Action = result.RecommendedAction.ToString().ToLowerInvariant(),
TotalUnknowns = result.TotalUnknowns,
TotalLimit = result.TotalLimit,
ViolationCount = result.Violations.Count,
Violations = violations,
Message = result.Message,
ImageDigest = imageDigest,
PolicyRevisionId = policyRevisionId,
Timestamp = DateTimeOffset.UtcNow
};
}
/// <summary>
/// Converts the payload to a JsonNode for the notification event.
/// </summary>
public static JsonNode ToJsonNode(BudgetEventPayload payload)
{
ArgumentNullException.ThrowIfNull(payload);
var obj = new JsonObject
{
["environment"] = payload.Environment,
["isWithinBudget"] = payload.IsWithinBudget,
["action"] = payload.Action,
["totalUnknowns"] = payload.TotalUnknowns,
["violationCount"] = payload.ViolationCount,
["timestamp"] = payload.Timestamp.ToString("O")
};
if (payload.TotalLimit.HasValue)
{
obj["totalLimit"] = payload.TotalLimit.Value;
}
if (payload.Message is not null)
{
obj["message"] = payload.Message;
}
if (payload.ImageDigest is not null)
{
obj["imageDigest"] = payload.ImageDigest;
}
if (payload.PolicyRevisionId is not null)
{
obj["policyRevisionId"] = payload.PolicyRevisionId;
}
if (payload.Violations.Count > 0)
{
var violations = new JsonArray();
foreach (var v in payload.Violations)
{
violations.Add(new JsonObject
{
["reasonCode"] = v.ReasonCode,
["shortCode"] = v.ShortCode,
["count"] = v.Count,
["limit"] = v.Limit
});
}
obj["violations"] = violations;
}
return obj;
}
/// <summary>
/// Gets the event kind based on the budget action.
/// </summary>
public static string GetEventKind(BudgetAction action)
{
return action == BudgetAction.Block
? BudgetExceededKind
: BudgetWarningKind;
}
private static string GetShortCode(UnknownReasonCode code) => code switch
{
UnknownReasonCode.Reachability => "U-RCH",
UnknownReasonCode.Identity => "U-ID",
UnknownReasonCode.Provenance => "U-PROV",
UnknownReasonCode.VexConflict => "U-VEX",
UnknownReasonCode.FeedGap => "U-FEED",
UnknownReasonCode.ConfigUnknown => "U-CONFIG",
UnknownReasonCode.AnalyzerLimit => "U-ANALYZER",
_ => "U-UNK"
};
}
/// <summary>
/// Payload for budget exceeded/warning notification events.
/// </summary>
public sealed record BudgetEventPayload
{
/// <summary>
/// Environment where budget was exceeded.
/// </summary>
public required string Environment { get; init; }
/// <summary>
/// Whether the result is within budget.
/// </summary>
public required bool IsWithinBudget { get; init; }
/// <summary>
/// Recommended action: "warn" or "block".
/// </summary>
public required string Action { get; init; }
/// <summary>
/// Total unknown count.
/// </summary>
public required int TotalUnknowns { get; init; }
/// <summary>
/// Configured total limit.
/// </summary>
public int? TotalLimit { get; init; }
/// <summary>
/// Number of violations.
/// </summary>
public required int ViolationCount { get; init; }
/// <summary>
/// Violation details.
/// </summary>
public ImmutableList<BudgetViolationInfo> Violations { get; init; } = ImmutableList<BudgetViolationInfo>.Empty;
/// <summary>
/// Budget exceeded message.
/// </summary>
public string? Message { get; init; }
/// <summary>
/// Image digest if applicable.
/// </summary>
public string? ImageDigest { get; init; }
/// <summary>
/// Policy revision ID if applicable.
/// </summary>
public string? PolicyRevisionId { get; init; }
/// <summary>
/// Event timestamp.
/// </summary>
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
}
/// <summary>
/// Information about a specific budget violation.
/// </summary>
public sealed record BudgetViolationInfo(
string ReasonCode,
string ShortCode,
int Count,
int Limit);

View File

@@ -30,6 +30,21 @@ public interface IUnknownsRepository
string packageVersion,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets all unknowns for a tenant.
/// Sprint: SPRINT_4300_0002_0001 (BUDGET-014)
/// </summary>
/// <param name="tenantId">Tenant identifier for RLS.</param>
/// <param name="limit">Maximum number of results.</param>
/// <param name="offset">Number of results to skip.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Ordered list of unknowns (by score descending).</returns>
Task<IReadOnlyList<Unknown>> GetAllAsync(
Guid tenantId,
int limit = 1000,
int offset = 0,
CancellationToken ct = default);
/// <summary>
/// Gets all unknowns for a tenant in a specific band.
/// </summary>

View File

@@ -76,6 +76,37 @@ public sealed class UnknownsRepository : IUnknownsRepository
return row?.ToModel();
}
/// <inheritdoc />
public async Task<IReadOnlyList<Unknown>> GetAllAsync(
Guid tenantId,
int limit = 1000,
int offset = 0,
CancellationToken ct = default)
{
const string sql = """
SELECT set_config('app.current_tenant', @TenantId::text, true);
SELECT id, tenant_id, package_id, package_version, band, score,
uncertainty_factor, exploit_pressure,
reason_code, remediation_hint,
evidence_refs::text as evidence_refs,
assumptions::text as assumptions,
blast_radius_dependents, blast_radius_net_facing, blast_radius_privilege,
containment_seccomp, containment_fs_mode, containment_network_policy,
first_seen_at, last_evaluated_at, resolution_reason, resolved_at,
created_at, updated_at
FROM policy.unknowns
ORDER BY score DESC, package_id ASC
LIMIT @Limit OFFSET @Offset;
""";
var param = new { TenantId = tenantId, Limit = limit, Offset = offset };
using var reader = await _connection.QueryMultipleAsync(sql, param);
await reader.ReadAsync();
var rows = await reader.ReadAsync<UnknownRow>();
return rows.Select(r => r.ToModel()).ToList().AsReadOnly();
}
/// <inheritdoc />
public async Task<IReadOnlyList<Unknown>> GetByBandAsync(
Guid tenantId,