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