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:
252
src/__Libraries/StellaOps.Metrics/Kpi/KpiCollector.cs
Normal file
252
src/__Libraries/StellaOps.Metrics/Kpi/KpiCollector.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
100
src/__Libraries/StellaOps.Metrics/Kpi/KpiTrendService.cs
Normal file
100
src/__Libraries/StellaOps.Metrics/Kpi/KpiTrendService.cs
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
12
src/__Libraries/StellaOps.Metrics/StellaOps.Metrics.csproj
Normal file
12
src/__Libraries/StellaOps.Metrics/StellaOps.Metrics.csproj
Normal 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>
|
||||
Reference in New Issue
Block a user