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