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,252 @@
using Microsoft.Extensions.Logging;
using StellaOps.Metrics.Kpi.Repositories;
namespace StellaOps.Metrics.Kpi;
/// <summary>
/// Interface for collecting quality KPIs.
/// </summary>
public interface IKpiCollector
{
/// <summary>
/// Collects all quality KPIs for a given period.
/// </summary>
Task<TriageQualityKpis> CollectAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId = null,
CancellationToken ct = default);
/// <summary>
/// Records a reachability result for real-time tracking.
/// </summary>
Task RecordReachabilityResultAsync(Guid findingId, string state, CancellationToken ct);
/// <summary>
/// Records a runtime observation for real-time tracking.
/// </summary>
Task RecordRuntimeObservationAsync(Guid findingId, string posture, CancellationToken ct);
/// <summary>
/// Records a verdict for real-time tracking.
/// </summary>
Task RecordVerdictAsync(Guid verdictId, bool hasReasonSteps, bool hasProofPointer, CancellationToken ct);
/// <summary>
/// Records a replay attempt for real-time tracking.
/// </summary>
Task RecordReplayAttemptAsync(Guid attestationId, bool success, string? failureReason, CancellationToken ct);
}
/// <summary>
/// Collects quality KPIs for explainable triage.
/// </summary>
public sealed class KpiCollector : IKpiCollector
{
private readonly IKpiRepository _repository;
private readonly IFindingRepository _findingRepo;
private readonly IVerdictRepository _verdictRepo;
private readonly IReplayRepository _replayRepo;
private readonly ILogger<KpiCollector> _logger;
public KpiCollector(
IKpiRepository repository,
IFindingRepository findingRepo,
IVerdictRepository verdictRepo,
IReplayRepository replayRepo,
ILogger<KpiCollector> logger)
{
_repository = repository;
_findingRepo = findingRepo;
_verdictRepo = verdictRepo;
_replayRepo = replayRepo;
_logger = logger;
}
/// <inheritdoc />
public async Task<TriageQualityKpis> CollectAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId = null,
CancellationToken ct = default)
{
_logger.LogDebug("Collecting KPIs for period {Start} to {End}, tenant {Tenant}",
start, end, tenantId ?? "global");
var reachability = await CollectReachabilityKpisAsync(start, end, tenantId, ct);
var runtime = await CollectRuntimeKpisAsync(start, end, tenantId, ct);
var explainability = await CollectExplainabilityKpisAsync(start, end, tenantId, ct);
var replay = await CollectReplayKpisAsync(start, end, tenantId, ct);
var unknowns = await CollectUnknownBudgetKpisAsync(start, end, tenantId, ct);
var operational = await CollectOperationalKpisAsync(start, end, tenantId, ct);
return new TriageQualityKpis
{
PeriodStart = start,
PeriodEnd = end,
TenantId = tenantId,
Reachability = reachability,
Runtime = runtime,
Explainability = explainability,
Replay = replay,
Unknowns = unknowns,
Operational = operational
};
}
private async Task<ReachabilityKpis> CollectReachabilityKpisAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct)
{
var findings = await _findingRepo.GetInPeriodAsync(start, end, tenantId, ct);
var byState = findings
.GroupBy(f => f.ReachabilityState ?? "Unknown")
.ToDictionary(g => g.Key, g => g.Count());
var withKnown = findings.Count(f =>
f.ReachabilityState is not null and not "Unknown");
return new ReachabilityKpis
{
TotalFindings = findings.Count,
WithKnownReachability = withKnown,
ByState = byState
};
}
private async Task<RuntimeKpis> CollectRuntimeKpisAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct)
{
var findings = await _findingRepo.GetWithSensorDeployedAsync(start, end, tenantId, ct);
var withRuntime = findings.Count(f => f.HasRuntimeEvidence);
var byPosture = findings
.Where(f => f.RuntimePosture is not null)
.GroupBy(f => f.RuntimePosture!)
.ToDictionary(g => g.Key, g => g.Count());
return new RuntimeKpis
{
TotalWithSensorDeployed = findings.Count,
WithRuntimeCorroboration = withRuntime,
ByPosture = byPosture
};
}
private async Task<ExplainabilityKpis> CollectExplainabilityKpisAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct)
{
var verdicts = await _verdictRepo.GetInPeriodAsync(start, end, tenantId, ct);
var withReasonSteps = verdicts.Count(v => v.ReasonSteps?.Count > 0);
var withProofPointer = verdicts.Count(v => v.ProofPointers?.Count > 0);
var fullyExplainable = verdicts.Count(v =>
v.ReasonSteps?.Count > 0 && v.ProofPointers?.Count > 0);
return new ExplainabilityKpis
{
TotalVerdicts = verdicts.Count,
WithReasonSteps = withReasonSteps,
WithProofPointer = withProofPointer,
FullyExplainable = fullyExplainable
};
}
private async Task<ReplayKpis> CollectReplayKpisAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct)
{
var replays = await _replayRepo.GetInPeriodAsync(start, end, tenantId, ct);
var successful = replays.Count(r => r.Success);
var failureReasons = replays
.Where(r => !r.Success && r.FailureReason is not null)
.GroupBy(r => r.FailureReason!)
.ToDictionary(g => g.Key, g => g.Count());
return new ReplayKpis
{
TotalAttempts = replays.Count,
Successful = successful,
FailureReasons = failureReasons
};
}
private async Task<UnknownBudgetKpis> CollectUnknownBudgetKpisAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct)
{
var breaches = await _repository.GetBudgetBreachesAsync(start, end, tenantId, ct);
var overrides = await _repository.GetOverridesAsync(start, end, tenantId, ct);
return new UnknownBudgetKpis
{
TotalEnvironments = breaches.Count,
BreachesByEnvironment = breaches,
OverridesGranted = overrides.Count,
AvgOverrideAgeDays = overrides.Any()
? (decimal)overrides.Average(o => (DateTimeOffset.UtcNow - o.GrantedAt).TotalDays)
: 0
};
}
private async Task<OperationalKpis> CollectOperationalKpisAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct)
{
var metrics = await _repository.GetOperationalMetricsAsync(start, end, tenantId, ct);
return new OperationalKpis
{
MedianTimeToVerdictSeconds = metrics.MedianVerdictTime.TotalSeconds,
CacheHitRate = metrics.CacheHitRate,
AvgEvidenceSizeBytes = metrics.AvgEvidenceSize,
P95VerdictTimeSeconds = metrics.P95VerdictTime.TotalSeconds
};
}
/// <inheritdoc />
public Task RecordReachabilityResultAsync(Guid findingId, string state, CancellationToken ct) =>
_repository.IncrementCounterAsync("reachability", state, ct);
/// <inheritdoc />
public Task RecordRuntimeObservationAsync(Guid findingId, string posture, CancellationToken ct) =>
_repository.IncrementCounterAsync("runtime", posture, ct);
/// <inheritdoc />
public Task RecordVerdictAsync(Guid verdictId, bool hasReasonSteps, bool hasProofPointer, CancellationToken ct)
{
var label = (hasReasonSteps, hasProofPointer) switch
{
(true, true) => "fully_explainable",
(true, false) => "reasons_only",
(false, true) => "proofs_only",
(false, false) => "unexplained"
};
return _repository.IncrementCounterAsync("explainability", label, ct);
}
/// <inheritdoc />
public Task RecordReplayAttemptAsync(Guid attestationId, bool success, string? failureReason, CancellationToken ct)
{
var label = success ? "success" : (failureReason ?? "unknown_failure");
return _repository.IncrementCounterAsync("replay", label, ct);
}
}

View File

@@ -0,0 +1,100 @@
namespace StellaOps.Metrics.Kpi;
/// <summary>
/// Interface for KPI trend analysis.
/// </summary>
public interface IKpiTrendService
{
/// <summary>
/// Gets KPI trend over a number of days.
/// </summary>
Task<KpiTrend> GetTrendAsync(int days, string? tenantId, CancellationToken ct);
}
/// <summary>
/// Provides KPI trend analysis.
/// </summary>
public sealed class KpiTrendService : IKpiTrendService
{
private readonly IKpiCollector _collector;
public KpiTrendService(IKpiCollector collector)
{
_collector = collector;
}
/// <inheritdoc />
public async Task<KpiTrend> GetTrendAsync(int days, string? tenantId, CancellationToken ct)
{
var snapshots = new List<KpiSnapshot>();
var end = DateTimeOffset.UtcNow;
var start = end.AddDays(-days);
// Collect daily snapshots
var currentStart = start;
while (currentStart < end)
{
var currentEnd = currentStart.AddDays(1);
if (currentEnd > end) currentEnd = end;
var kpis = await _collector.CollectAsync(currentStart, currentEnd, tenantId, ct);
snapshots.Add(new KpiSnapshot(
currentStart.Date,
kpis.Reachability.PercentKnown,
kpis.Runtime.CoveragePercent,
kpis.Explainability.CompletenessPercent,
kpis.Replay.SuccessRate,
kpis.Reachability.NoiseReductionPercent));
currentStart = currentEnd;
}
// Calculate changes
var firstValid = snapshots.FirstOrDefault(s => s.ReachabilityKnownPercent > 0);
var lastValid = snapshots.LastOrDefault(s => s.ReachabilityKnownPercent > 0);
var changes = new KpiChanges(
ReachabilityDelta: lastValid?.ReachabilityKnownPercent - firstValid?.ReachabilityKnownPercent ?? 0,
RuntimeDelta: lastValid?.RuntimeCoveragePercent - firstValid?.RuntimeCoveragePercent ?? 0,
ExplainabilityDelta: lastValid?.ExplainabilityPercent - firstValid?.ExplainabilityPercent ?? 0,
ReplayDelta: lastValid?.ReplaySuccessRate - firstValid?.ReplaySuccessRate ?? 0);
return new KpiTrend(
Days: days,
TenantId: tenantId,
Snapshots: snapshots,
Changes: changes,
GeneratedAt: DateTimeOffset.UtcNow);
}
}
/// <summary>
/// KPI trend over time.
/// </summary>
public sealed record KpiTrend(
int Days,
string? TenantId,
IReadOnlyList<KpiSnapshot> Snapshots,
KpiChanges Changes,
DateTimeOffset GeneratedAt);
/// <summary>
/// A single day's KPI snapshot.
/// </summary>
public sealed record KpiSnapshot(
DateTimeOffset Date,
decimal ReachabilityKnownPercent,
decimal RuntimeCoveragePercent,
decimal ExplainabilityPercent,
decimal ReplaySuccessRate,
decimal NoiseReductionPercent);
/// <summary>
/// Changes in KPIs over the trend period.
/// </summary>
public sealed record KpiChanges(
decimal ReachabilityDelta,
decimal RuntimeDelta,
decimal ExplainabilityDelta,
decimal ReplayDelta);

View File

@@ -0,0 +1,35 @@
namespace StellaOps.Metrics.Kpi.Repositories;
/// <summary>
/// Repository for querying findings for KPI calculations.
/// </summary>
public interface IFindingRepository
{
/// <summary>
/// Gets all findings in a period for KPI calculation.
/// </summary>
Task<IReadOnlyList<FindingKpiData>> GetInPeriodAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct);
/// <summary>
/// Gets findings where a runtime sensor was deployed.
/// </summary>
Task<IReadOnlyList<FindingKpiData>> GetWithSensorDeployedAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct);
}
/// <summary>
/// Finding data needed for KPI calculations.
/// </summary>
public sealed record FindingKpiData(
Guid Id,
string? ReachabilityState,
bool HasRuntimeEvidence,
string? RuntimePosture,
DateTimeOffset CreatedAt);

View File

@@ -0,0 +1,57 @@
namespace StellaOps.Metrics.Kpi.Repositories;
/// <summary>
/// Repository for KPI counter operations and operational metrics.
/// </summary>
public interface IKpiRepository
{
/// <summary>
/// Increments a counter for a specific category and label.
/// </summary>
Task IncrementCounterAsync(string category, string label, CancellationToken ct);
/// <summary>
/// Gets budget breaches by environment for a given period.
/// </summary>
Task<IReadOnlyDictionary<string, int>> GetBudgetBreachesAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct);
/// <summary>
/// Gets overrides granted in a given period.
/// </summary>
Task<IReadOnlyList<OverrideRecord>> GetOverridesAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct);
/// <summary>
/// Gets operational metrics for a given period.
/// </summary>
Task<OperationalMetrics> GetOperationalMetricsAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct);
}
/// <summary>
/// Represents an override record.
/// </summary>
public sealed record OverrideRecord(
Guid Id,
string EnvironmentId,
DateTimeOffset GrantedAt,
string Reason);
/// <summary>
/// Operational metrics from the repository.
/// </summary>
public sealed record OperationalMetrics(
TimeSpan MedianVerdictTime,
TimeSpan P95VerdictTime,
decimal CacheHitRate,
long AvgEvidenceSize);

View File

@@ -0,0 +1,25 @@
namespace StellaOps.Metrics.Kpi.Repositories;
/// <summary>
/// Repository for querying replay attempts for KPI calculations.
/// </summary>
public interface IReplayRepository
{
/// <summary>
/// Gets all replay attempts in a period for KPI calculation.
/// </summary>
Task<IReadOnlyList<ReplayKpiData>> GetInPeriodAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct);
}
/// <summary>
/// Replay attempt data needed for KPI calculations.
/// </summary>
public sealed record ReplayKpiData(
Guid AttestationId,
bool Success,
string? FailureReason,
DateTimeOffset AttemptedAt);

View File

@@ -0,0 +1,25 @@
namespace StellaOps.Metrics.Kpi.Repositories;
/// <summary>
/// Repository for querying verdicts for KPI calculations.
/// </summary>
public interface IVerdictRepository
{
/// <summary>
/// Gets all verdicts in a period for KPI calculation.
/// </summary>
Task<IReadOnlyList<VerdictKpiData>> GetInPeriodAsync(
DateTimeOffset start,
DateTimeOffset end,
string? tenantId,
CancellationToken ct);
}
/// <summary>
/// Verdict data needed for KPI calculations.
/// </summary>
public sealed record VerdictKpiData(
Guid Id,
IReadOnlyList<string>? ReasonSteps,
IReadOnlyList<string>? ProofPointers,
DateTimeOffset CreatedAt);

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
</ItemGroup>
</Project>