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,313 @@
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using StellaOps.Metrics.Kpi;
using StellaOps.Metrics.Kpi.Repositories;
using Xunit;
namespace StellaOps.Metrics.Tests.Kpi;
public class KpiCollectorTests
{
private readonly Mock<IKpiRepository> _kpiRepoMock;
private readonly Mock<IFindingRepository> _findingRepoMock;
private readonly Mock<IVerdictRepository> _verdictRepoMock;
private readonly Mock<IReplayRepository> _replayRepoMock;
private readonly Mock<ILogger<KpiCollector>> _loggerMock;
private readonly KpiCollector _collector;
public KpiCollectorTests()
{
_kpiRepoMock = new Mock<IKpiRepository>();
_findingRepoMock = new Mock<IFindingRepository>();
_verdictRepoMock = new Mock<IVerdictRepository>();
_replayRepoMock = new Mock<IReplayRepository>();
_loggerMock = new Mock<ILogger<KpiCollector>>();
_collector = new KpiCollector(
_kpiRepoMock.Object,
_findingRepoMock.Object,
_verdictRepoMock.Object,
_replayRepoMock.Object,
_loggerMock.Object);
}
[Fact]
public async Task CollectAsync_ReturnsAllCategories()
{
// Arrange
SetupDefaultMocks();
// Act
var result = await _collector.CollectAsync(
DateTimeOffset.UtcNow.AddDays(-7),
DateTimeOffset.UtcNow,
ct: CancellationToken.None);
// Assert
result.Reachability.Should().NotBeNull();
result.Runtime.Should().NotBeNull();
result.Explainability.Should().NotBeNull();
result.Replay.Should().NotBeNull();
result.Unknowns.Should().NotBeNull();
result.Operational.Should().NotBeNull();
}
[Fact]
public async Task CollectAsync_CalculatesReachabilityPercentagesCorrectly()
{
// Arrange
var findings = new List<FindingKpiData>
{
new(Guid.NewGuid(), "Reachable", false, null, DateTimeOffset.UtcNow),
new(Guid.NewGuid(), "Reachable", false, null, DateTimeOffset.UtcNow),
new(Guid.NewGuid(), "ConfirmedUnreachable", false, null, DateTimeOffset.UtcNow),
new(Guid.NewGuid(), "Unknown", false, null, DateTimeOffset.UtcNow)
};
_findingRepoMock
.Setup(x => x.GetInPeriodAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(findings);
SetupOtherMocks();
// Act
var result = await _collector.CollectAsync(
DateTimeOffset.UtcNow.AddDays(-7),
DateTimeOffset.UtcNow,
ct: CancellationToken.None);
// Assert
result.Reachability.TotalFindings.Should().Be(4);
result.Reachability.WithKnownReachability.Should().Be(3); // 2 Reachable + 1 ConfirmedUnreachable
result.Reachability.PercentKnown.Should().Be(75m);
}
[Fact]
public async Task CollectAsync_CalculatesExplainabilityCorrectly()
{
// Arrange
var verdicts = new List<VerdictKpiData>
{
new(Guid.NewGuid(), new[] { "step1" }, new[] { "proof1" }, DateTimeOffset.UtcNow),
new(Guid.NewGuid(), new[] { "step1", "step2" }, null, DateTimeOffset.UtcNow),
new(Guid.NewGuid(), null, new[] { "proof1" }, DateTimeOffset.UtcNow),
new(Guid.NewGuid(), null, null, DateTimeOffset.UtcNow)
};
_verdictRepoMock
.Setup(x => x.GetInPeriodAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(verdicts);
SetupOtherMocksExceptVerdicts();
// Act
var result = await _collector.CollectAsync(
DateTimeOffset.UtcNow.AddDays(-7),
DateTimeOffset.UtcNow,
ct: CancellationToken.None);
// Assert
result.Explainability.TotalVerdicts.Should().Be(4);
result.Explainability.WithReasonSteps.Should().Be(2);
result.Explainability.WithProofPointer.Should().Be(2);
result.Explainability.FullyExplainable.Should().Be(1);
result.Explainability.CompletenessPercent.Should().Be(25m);
}
[Fact]
public async Task RecordVerdictAsync_FullyExplainable_IncrementsCorrectCounter()
{
// Act
await _collector.RecordVerdictAsync(
Guid.NewGuid(),
hasReasonSteps: true,
hasProofPointer: true,
CancellationToken.None);
// Assert
_kpiRepoMock.Verify(
r => r.IncrementCounterAsync("explainability", "fully_explainable", It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RecordVerdictAsync_ReasonsOnly_IncrementsCorrectCounter()
{
// Act
await _collector.RecordVerdictAsync(
Guid.NewGuid(),
hasReasonSteps: true,
hasProofPointer: false,
CancellationToken.None);
// Assert
_kpiRepoMock.Verify(
r => r.IncrementCounterAsync("explainability", "reasons_only", It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RecordVerdictAsync_ProofsOnly_IncrementsCorrectCounter()
{
// Act
await _collector.RecordVerdictAsync(
Guid.NewGuid(),
hasReasonSteps: false,
hasProofPointer: true,
CancellationToken.None);
// Assert
_kpiRepoMock.Verify(
r => r.IncrementCounterAsync("explainability", "proofs_only", It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RecordVerdictAsync_Unexplained_IncrementsCorrectCounter()
{
// Act
await _collector.RecordVerdictAsync(
Guid.NewGuid(),
hasReasonSteps: false,
hasProofPointer: false,
CancellationToken.None);
// Assert
_kpiRepoMock.Verify(
r => r.IncrementCounterAsync("explainability", "unexplained", It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RecordReplayAttemptAsync_Success_IncrementsSuccessCounter()
{
// Act
await _collector.RecordReplayAttemptAsync(
Guid.NewGuid(),
success: true,
failureReason: null,
CancellationToken.None);
// Assert
_kpiRepoMock.Verify(
r => r.IncrementCounterAsync("replay", "success", It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RecordReplayAttemptAsync_Failure_IncrementsFailureReasonCounter()
{
// Act
await _collector.RecordReplayAttemptAsync(
Guid.NewGuid(),
success: false,
failureReason: "FeedDrift",
CancellationToken.None);
// Assert
_kpiRepoMock.Verify(
r => r.IncrementCounterAsync("replay", "FeedDrift", It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RecordReachabilityResultAsync_IncrementsCorrectCounter()
{
// Act
await _collector.RecordReachabilityResultAsync(
Guid.NewGuid(),
"ConfirmedUnreachable",
CancellationToken.None);
// Assert
_kpiRepoMock.Verify(
r => r.IncrementCounterAsync("reachability", "ConfirmedUnreachable", It.IsAny<CancellationToken>()),
Times.Once);
}
private void SetupDefaultMocks()
{
_findingRepoMock
.Setup(x => x.GetInPeriodAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<FindingKpiData>());
_findingRepoMock
.Setup(x => x.GetWithSensorDeployedAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<FindingKpiData>());
_verdictRepoMock
.Setup(x => x.GetInPeriodAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<VerdictKpiData>());
_replayRepoMock
.Setup(x => x.GetInPeriodAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<ReplayKpiData>());
_kpiRepoMock
.Setup(x => x.GetBudgetBreachesAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Dictionary<string, int>());
_kpiRepoMock
.Setup(x => x.GetOverridesAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<OverrideRecord>());
_kpiRepoMock
.Setup(x => x.GetOperationalMetricsAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OperationalMetrics(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), 0.85m, 1024));
}
private void SetupOtherMocks()
{
_findingRepoMock
.Setup(x => x.GetWithSensorDeployedAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<FindingKpiData>());
_verdictRepoMock
.Setup(x => x.GetInPeriodAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<VerdictKpiData>());
_replayRepoMock
.Setup(x => x.GetInPeriodAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<ReplayKpiData>());
_kpiRepoMock
.Setup(x => x.GetBudgetBreachesAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Dictionary<string, int>());
_kpiRepoMock
.Setup(x => x.GetOverridesAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<OverrideRecord>());
_kpiRepoMock
.Setup(x => x.GetOperationalMetricsAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OperationalMetrics(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), 0.85m, 1024));
}
private void SetupOtherMocksExceptVerdicts()
{
_findingRepoMock
.Setup(x => x.GetInPeriodAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<FindingKpiData>());
_findingRepoMock
.Setup(x => x.GetWithSensorDeployedAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<FindingKpiData>());
_replayRepoMock
.Setup(x => x.GetInPeriodAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<ReplayKpiData>());
_kpiRepoMock
.Setup(x => x.GetBudgetBreachesAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Dictionary<string, int>());
_kpiRepoMock
.Setup(x => x.GetOverridesAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<OverrideRecord>());
_kpiRepoMock
.Setup(x => x.GetOperationalMetricsAsync(It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OperationalMetrics(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), 0.85m, 1024));
}
}

View File

@@ -0,0 +1,180 @@
using FluentAssertions;
using StellaOps.Metrics.Kpi;
using Xunit;
namespace StellaOps.Metrics.Tests.Kpi;
public class KpiModelsTests
{
[Fact]
public void ReachabilityKpis_PercentKnown_CalculatesCorrectly()
{
// Arrange
var kpis = new ReachabilityKpis
{
TotalFindings = 100,
WithKnownReachability = 75,
ByState = new Dictionary<string, int>
{
["Reachable"] = 50,
["ConfirmedUnreachable"] = 25,
["Unknown"] = 25
}
};
// Assert
kpis.PercentKnown.Should().Be(75m);
}
[Fact]
public void ReachabilityKpis_NoiseReductionPercent_CalculatesCorrectly()
{
// Arrange
var kpis = new ReachabilityKpis
{
TotalFindings = 100,
WithKnownReachability = 75,
ByState = new Dictionary<string, int>
{
["Reachable"] = 50,
["ConfirmedUnreachable"] = 25,
["Unknown"] = 25
}
};
// Assert
kpis.NoiseReductionPercent.Should().Be(25m);
}
[Fact]
public void ReachabilityKpis_WithZeroTotal_ReturnsZeroPercent()
{
// Arrange
var kpis = new ReachabilityKpis
{
TotalFindings = 0,
WithKnownReachability = 0,
ByState = new Dictionary<string, int>()
};
// Assert
kpis.PercentKnown.Should().Be(0m);
kpis.NoiseReductionPercent.Should().Be(0m);
}
[Fact]
public void RuntimeKpis_CoveragePercent_CalculatesCorrectly()
{
// Arrange
var kpis = new RuntimeKpis
{
TotalWithSensorDeployed = 200,
WithRuntimeCorroboration = 100,
ByPosture = new Dictionary<string, int>
{
["Supports"] = 60,
["Contradicts"] = 30,
["Unknown"] = 10
}
};
// Assert
kpis.CoveragePercent.Should().Be(50m);
}
[Fact]
public void ExplainabilityKpis_CompletenessPercent_CalculatesCorrectly()
{
// Arrange
var kpis = new ExplainabilityKpis
{
TotalVerdicts = 100,
WithReasonSteps = 90,
WithProofPointer = 85,
FullyExplainable = 80
};
// Assert
kpis.CompletenessPercent.Should().Be(80m);
}
[Fact]
public void ReplayKpis_SuccessRate_CalculatesCorrectly()
{
// Arrange
var kpis = new ReplayKpis
{
TotalAttempts = 50,
Successful = 45,
FailureReasons = new Dictionary<string, int>
{
["FeedDrift"] = 3,
["PolicyChange"] = 2
}
};
// Assert
kpis.SuccessRate.Should().Be(90m);
}
[Fact]
public void TriageQualityKpis_ContainsAllCategories()
{
// Arrange
var kpis = CreateSampleKpis();
// Assert
kpis.Reachability.Should().NotBeNull();
kpis.Runtime.Should().NotBeNull();
kpis.Explainability.Should().NotBeNull();
kpis.Replay.Should().NotBeNull();
kpis.Unknowns.Should().NotBeNull();
kpis.Operational.Should().NotBeNull();
}
private static TriageQualityKpis CreateSampleKpis() => new()
{
PeriodStart = DateTimeOffset.UtcNow.AddDays(-7),
PeriodEnd = DateTimeOffset.UtcNow,
TenantId = null,
Reachability = new ReachabilityKpis
{
TotalFindings = 100,
WithKnownReachability = 80,
ByState = new Dictionary<string, int>()
},
Runtime = new RuntimeKpis
{
TotalWithSensorDeployed = 50,
WithRuntimeCorroboration = 30,
ByPosture = new Dictionary<string, int>()
},
Explainability = new ExplainabilityKpis
{
TotalVerdicts = 100,
WithReasonSteps = 95,
WithProofPointer = 90,
FullyExplainable = 88
},
Replay = new ReplayKpis
{
TotalAttempts = 20,
Successful = 19,
FailureReasons = new Dictionary<string, int>()
},
Unknowns = new UnknownBudgetKpis
{
TotalEnvironments = 5,
BreachesByEnvironment = new Dictionary<string, int>(),
OverridesGranted = 2,
AvgOverrideAgeDays = 3.5m
},
Operational = new OperationalKpis
{
MedianTimeToVerdictSeconds = 1.5,
CacheHitRate = 0.85m,
AvgEvidenceSizeBytes = 1024000,
P95VerdictTimeSeconds = 5.2
}
};
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Metrics\StellaOps.Metrics.csproj" />
</ItemGroup>
</Project>