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