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

@@ -23,7 +23,11 @@ public sealed class PostgresUnknownRepository : IUnknownRepository
kind::text, severity::text, context, source_scan_id, source_graph_id, source_sbom_digest,
valid_from, valid_to, sys_from, sys_to,
resolved_at, resolution_type::text, resolution_ref, resolution_notes,
created_at, created_by, updated_at
created_at, created_by, updated_at,
popularity_score, deployment_count, exploit_potential_score, uncertainty_score, uncertainty_flags,
centrality_score, degree_centrality, betweenness_centrality, staleness_score, days_since_analysis,
composite_score, triage_band::text, scoring_trace, rescan_attempts, last_rescan_result,
next_scheduled_rescan, last_analyzed_at, evidence_set_hash, graph_slice_hash
""";
public PostgresUnknownRepository(
@@ -501,6 +505,277 @@ public sealed class PostgresUnknownRepository : IUnknownRepository
return result is long count ? count : 0;
}
public async Task<IReadOnlyList<Unknown>> GetByTriageBandAsync(
string tenantId,
TriageBand band,
int? limit = null,
int? offset = null,
CancellationToken cancellationToken = default)
{
var sql = $"""
SELECT {SelectColumns}
FROM unknowns.unknown
WHERE tenant_id = @tenant_id
AND triage_band = @triage_band::unknowns.triage_band
AND valid_to IS NULL
AND sys_to IS NULL
ORDER BY composite_score DESC
{(limit.HasValue ? "LIMIT @limit" : "")}
{(offset.HasValue ? "OFFSET @offset" : "")}
""";
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
await SetTenantContextAsync(connection, tenantId, cancellationToken);
await using var command = new NpgsqlCommand(sql, connection);
command.CommandTimeout = _commandTimeoutSeconds;
command.Parameters.AddWithValue("tenant_id", tenantId);
command.Parameters.AddWithValue("triage_band", MapTriageBand(band));
if (limit.HasValue)
command.Parameters.AddWithValue("limit", limit.Value);
if (offset.HasValue)
command.Parameters.AddWithValue("offset", offset.Value);
return await ReadUnknownsAsync(command, cancellationToken);
}
public async Task<IReadOnlyList<Unknown>> GetHotQueueAsync(
string tenantId,
int? limit = null,
CancellationToken cancellationToken = default)
{
return await GetByTriageBandAsync(tenantId, TriageBand.Hot, limit, null, cancellationToken);
}
public async Task<IReadOnlyList<Unknown>> GetDueForRescanAsync(
string tenantId,
int? limit = null,
CancellationToken cancellationToken = default)
{
var sql = $"""
SELECT {SelectColumns}
FROM unknowns.unknown
WHERE tenant_id = @tenant_id
AND next_scheduled_rescan <= @now
AND valid_to IS NULL
AND sys_to IS NULL
ORDER BY next_scheduled_rescan ASC
{(limit.HasValue ? "LIMIT @limit" : "")}
""";
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
await SetTenantContextAsync(connection, tenantId, cancellationToken);
await using var command = new NpgsqlCommand(sql, connection);
command.CommandTimeout = _commandTimeoutSeconds;
command.Parameters.AddWithValue("tenant_id", tenantId);
command.Parameters.AddWithValue("now", DateTimeOffset.UtcNow);
if (limit.HasValue)
command.Parameters.AddWithValue("limit", limit.Value);
return await ReadUnknownsAsync(command, cancellationToken);
}
public async Task<Unknown> UpdateScoresAsync(
string tenantId,
Guid id,
double popularityScore,
int deploymentCount,
double exploitPotentialScore,
double uncertaintyScore,
string? uncertaintyFlags,
double centralityScore,
int degreeCentrality,
double betweennessCentrality,
double stalenessScore,
int daysSinceAnalysis,
double compositeScore,
TriageBand triageBand,
string? scoringTrace,
DateTimeOffset? nextScheduledRescan,
CancellationToken cancellationToken)
{
var now = DateTimeOffset.UtcNow;
const string sql = """
UPDATE unknowns.unknown
SET popularity_score = @popularity_score,
deployment_count = @deployment_count,
exploit_potential_score = @exploit_potential_score,
uncertainty_score = @uncertainty_score,
uncertainty_flags = @uncertainty_flags::jsonb,
centrality_score = @centrality_score,
degree_centrality = @degree_centrality,
betweenness_centrality = @betweenness_centrality,
staleness_score = @staleness_score,
days_since_analysis = @days_since_analysis,
composite_score = @composite_score,
triage_band = @triage_band::unknowns.triage_band,
scoring_trace = @scoring_trace::jsonb,
next_scheduled_rescan = @next_scheduled_rescan,
last_analyzed_at = @last_analyzed_at,
updated_at = @updated_at
WHERE tenant_id = @tenant_id
AND id = @id
AND sys_to IS NULL
""";
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
await SetTenantContextAsync(connection, tenantId, cancellationToken);
await using var command = new NpgsqlCommand(sql, connection);
command.CommandTimeout = _commandTimeoutSeconds;
command.Parameters.AddWithValue("tenant_id", tenantId);
command.Parameters.AddWithValue("id", id);
command.Parameters.AddWithValue("popularity_score", popularityScore);
command.Parameters.AddWithValue("deployment_count", deploymentCount);
command.Parameters.AddWithValue("exploit_potential_score", exploitPotentialScore);
command.Parameters.AddWithValue("uncertainty_score", uncertaintyScore);
command.Parameters.Add(new NpgsqlParameter("uncertainty_flags", NpgsqlDbType.Jsonb)
{
Value = uncertaintyFlags ?? "{}"
});
command.Parameters.AddWithValue("centrality_score", centralityScore);
command.Parameters.AddWithValue("degree_centrality", degreeCentrality);
command.Parameters.AddWithValue("betweenness_centrality", betweennessCentrality);
command.Parameters.AddWithValue("staleness_score", stalenessScore);
command.Parameters.AddWithValue("days_since_analysis", daysSinceAnalysis);
command.Parameters.AddWithValue("composite_score", compositeScore);
command.Parameters.AddWithValue("triage_band", MapTriageBand(triageBand));
command.Parameters.Add(new NpgsqlParameter("scoring_trace", NpgsqlDbType.Jsonb)
{
Value = scoringTrace ?? "{}"
});
command.Parameters.AddWithValue("next_scheduled_rescan", nextScheduledRescan.HasValue ? nextScheduledRescan.Value : DBNull.Value);
command.Parameters.AddWithValue("last_analyzed_at", now);
command.Parameters.AddWithValue("updated_at", now);
var affected = await command.ExecuteNonQueryAsync(cancellationToken);
if (affected == 0)
{
throw new InvalidOperationException($"Unknown {id} not found or already superseded.");
}
_logger.LogDebug("Updated scores for unknown {Id}, band={Band}, score={Score}", id, triageBand, compositeScore);
var updated = await GetByIdAsync(tenantId, id, cancellationToken);
return updated ?? throw new InvalidOperationException($"Failed to retrieve updated unknown {id}.");
}
public async Task<Unknown> RecordRescanAttemptAsync(
string tenantId,
Guid id,
string result,
DateTimeOffset? nextRescan,
CancellationToken cancellationToken)
{
var now = DateTimeOffset.UtcNow;
const string sql = """
UPDATE unknowns.unknown
SET rescan_attempts = rescan_attempts + 1,
last_rescan_result = @last_rescan_result,
next_scheduled_rescan = @next_scheduled_rescan,
updated_at = @updated_at
WHERE tenant_id = @tenant_id
AND id = @id
AND sys_to IS NULL
""";
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
await SetTenantContextAsync(connection, tenantId, cancellationToken);
await using var command = new NpgsqlCommand(sql, connection);
command.CommandTimeout = _commandTimeoutSeconds;
command.Parameters.AddWithValue("tenant_id", tenantId);
command.Parameters.AddWithValue("id", id);
command.Parameters.AddWithValue("last_rescan_result", result);
command.Parameters.AddWithValue("next_scheduled_rescan", nextRescan.HasValue ? nextRescan.Value : DBNull.Value);
command.Parameters.AddWithValue("updated_at", now);
var affected = await command.ExecuteNonQueryAsync(cancellationToken);
if (affected == 0)
{
throw new InvalidOperationException($"Unknown {id} not found or already superseded.");
}
_logger.LogDebug("Recorded rescan attempt for unknown {Id}, result={Result}", id, result);
var updated = await GetByIdAsync(tenantId, id, cancellationToken);
return updated ?? throw new InvalidOperationException($"Failed to retrieve updated unknown {id}.");
}
public async Task<IReadOnlyDictionary<TriageBand, long>> CountByTriageBandAsync(
string tenantId,
CancellationToken cancellationToken)
{
const string sql = """
SELECT triage_band::text, count(*)
FROM unknowns.unknown
WHERE tenant_id = @tenant_id
AND valid_to IS NULL
AND sys_to IS NULL
GROUP BY triage_band
""";
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
await SetTenantContextAsync(connection, tenantId, cancellationToken);
await using var command = new NpgsqlCommand(sql, connection);
command.CommandTimeout = _commandTimeoutSeconds;
command.Parameters.AddWithValue("tenant_id", tenantId);
var result = new Dictionary<TriageBand, long>();
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
var bandStr = reader.IsDBNull(0) ? "cold" : reader.GetFieldValue<string>(0);
var count = reader.GetInt64(1);
result[ParseTriageBand(bandStr)] = count;
}
return result;
}
public async Task<IReadOnlyList<TriageSummary>> GetTriageSummaryAsync(
string tenantId,
CancellationToken cancellationToken)
{
const string sql = """
SELECT triage_band::text, kind::text, count(*), avg(composite_score), max(composite_score), min(composite_score)
FROM unknowns.unknown
WHERE tenant_id = @tenant_id
AND valid_to IS NULL
AND sys_to IS NULL
GROUP BY triage_band, kind
ORDER BY triage_band, kind
""";
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
await SetTenantContextAsync(connection, tenantId, cancellationToken);
await using var command = new NpgsqlCommand(sql, connection);
command.CommandTimeout = _commandTimeoutSeconds;
command.Parameters.AddWithValue("tenant_id", tenantId);
var results = new List<TriageSummary>();
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
results.Add(new TriageSummary
{
Band = ParseTriageBand(reader.IsDBNull(0) ? "cold" : reader.GetFieldValue<string>(0)),
Kind = ParseUnknownKind(reader.GetFieldValue<string>(1)),
Count = reader.GetInt64(2),
AvgScore = reader.IsDBNull(3) ? 0.0 : reader.GetDouble(3),
MaxScore = reader.IsDBNull(4) ? 0.0 : reader.GetDouble(4),
MinScore = reader.IsDBNull(5) ? 0.0 : reader.GetDouble(5)
});
}
return results;
}
private static async Task SetTenantContextAsync(
NpgsqlConnection connection,
string tenantId,
@@ -529,6 +804,8 @@ public sealed class PostgresUnknownRepository : IUnknownRepository
private static Unknown MapUnknown(NpgsqlDataReader reader)
{
var contextJson = reader.IsDBNull(7) ? null : reader.GetFieldValue<string>(7);
var uncertaintyFlagsJson = reader.IsDBNull(25) ? null : reader.GetFieldValue<string>(25);
var scoringTraceJson = reader.IsDBNull(33) ? null : reader.GetFieldValue<string>(33);
return new Unknown
{
@@ -553,7 +830,27 @@ public sealed class PostgresUnknownRepository : IUnknownRepository
ResolutionNotes = reader.IsDBNull(18) ? null : reader.GetString(18),
CreatedAt = reader.GetFieldValue<DateTimeOffset>(19),
CreatedBy = reader.GetString(20),
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(21)
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(21),
// Scoring fields
PopularityScore = reader.IsDBNull(22) ? 0.0 : reader.GetDouble(22),
DeploymentCount = reader.IsDBNull(23) ? 0 : reader.GetInt32(23),
ExploitPotentialScore = reader.IsDBNull(24) ? 0.0 : reader.GetDouble(24),
UncertaintyScore = reader.IsDBNull(25) ? 0.0 : reader.GetDouble(25),
UncertaintyFlags = uncertaintyFlagsJson is not null ? JsonDocument.Parse(uncertaintyFlagsJson) : null,
CentralityScore = reader.IsDBNull(27) ? 0.0 : reader.GetDouble(27),
DegreeCentrality = reader.IsDBNull(28) ? 0 : reader.GetInt32(28),
BetweennessCentrality = reader.IsDBNull(29) ? 0.0 : reader.GetDouble(29),
StalenessScore = reader.IsDBNull(30) ? 0.0 : reader.GetDouble(30),
DaysSinceAnalysis = reader.IsDBNull(31) ? 0 : reader.GetInt32(31),
CompositeScore = reader.IsDBNull(32) ? 0.0 : reader.GetDouble(32),
TriageBand = reader.IsDBNull(33) ? TriageBand.Cold : ParseTriageBand(reader.GetFieldValue<string>(33)),
ScoringTrace = scoringTraceJson is not null ? JsonDocument.Parse(scoringTraceJson) : null,
RescanAttempts = reader.IsDBNull(35) ? 0 : reader.GetInt32(35),
LastRescanResult = reader.IsDBNull(36) ? null : reader.GetString(36),
NextScheduledRescan = reader.IsDBNull(37) ? null : reader.GetFieldValue<DateTimeOffset>(37),
LastAnalyzedAt = reader.IsDBNull(38) ? null : reader.GetFieldValue<DateTimeOffset>(38),
EvidenceSetHash = reader.IsDBNull(39) ? null : reader.GetFieldValue<byte[]>(39),
GraphSliceHash = reader.IsDBNull(40) ? null : reader.GetFieldValue<byte[]>(40)
};
}
@@ -657,4 +954,20 @@ public sealed class PostgresUnknownRepository : IUnknownRepository
"wont_fix" => ResolutionType.WontFix,
_ => throw new ArgumentOutOfRangeException(nameof(value))
};
private static string MapTriageBand(TriageBand band) => band switch
{
TriageBand.Hot => "hot",
TriageBand.Warm => "warm",
TriageBand.Cold => "cold",
_ => throw new ArgumentOutOfRangeException(nameof(band))
};
private static TriageBand ParseTriageBand(string value) => value switch
{
"hot" => TriageBand.Hot,
"warm" => TriageBand.Warm,
"cold" => TriageBand.Cold,
_ => throw new ArgumentOutOfRangeException(nameof(value))
};
}

View File

@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-preview.1.25080.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Npgsql" Version="9.0.3" />
</ItemGroup>