save work
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// FidelityMetricsIntegrationTests.cs
|
||||
// Sprint: SPRINT_3403_0001_0001_fidelity_metrics
|
||||
// Task: FID-3403-013
|
||||
// Description: Integration tests for fidelity metrics in determinism harness
|
||||
// Description: Integration tests for fidelity metrics in determinism reports
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Worker.Determinism;
|
||||
@@ -16,13 +16,12 @@ public sealed class FidelityMetricsIntegrationTests
|
||||
[Fact]
|
||||
public void DeterminismReport_WithFidelityMetrics_IncludesAllThreeTiers()
|
||||
{
|
||||
// Arrange & Act
|
||||
var fidelity = CreateTestFidelityMetrics(
|
||||
bitwiseFidelity: 0.98,
|
||||
semanticFidelity: 0.99,
|
||||
policyFidelity: 1.0);
|
||||
|
||||
var report = new DeterminismReport(
|
||||
var report = new global::StellaOps.Scanner.Worker.Determinism.DeterminismReport(
|
||||
Version: "1.0.0",
|
||||
Release: "test-release",
|
||||
Platform: "linux-amd64",
|
||||
@@ -35,9 +34,8 @@ public sealed class FidelityMetricsIntegrationTests
|
||||
Images: [],
|
||||
Fidelity: fidelity);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(report.Fidelity);
|
||||
Assert.Equal(0.98, report.Fidelity.BitwiseFidelity);
|
||||
Assert.Equal(0.98, report.Fidelity!.BitwiseFidelity);
|
||||
Assert.Equal(0.99, report.Fidelity.SemanticFidelity);
|
||||
Assert.Equal(1.0, report.Fidelity.PolicyFidelity);
|
||||
}
|
||||
@@ -45,13 +43,12 @@ public sealed class FidelityMetricsIntegrationTests
|
||||
[Fact]
|
||||
public void DeterminismImageReport_WithFidelityMetrics_TracksPerImage()
|
||||
{
|
||||
// Arrange
|
||||
var imageFidelity = CreateTestFidelityMetrics(
|
||||
bitwiseFidelity: 0.95,
|
||||
semanticFidelity: 0.98,
|
||||
policyFidelity: 1.0);
|
||||
|
||||
var imageReport = new DeterminismImageReport(
|
||||
var imageReport = new global::StellaOps.Scanner.Worker.Determinism.DeterminismImageReport(
|
||||
Image: "sha256:image123",
|
||||
Runs: 5,
|
||||
Identical: 4,
|
||||
@@ -60,120 +57,40 @@ public sealed class FidelityMetricsIntegrationTests
|
||||
RunsDetail: [],
|
||||
Fidelity: imageFidelity);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(imageReport.Fidelity);
|
||||
Assert.Equal(0.95, imageReport.Fidelity.BitwiseFidelity);
|
||||
Assert.Equal(0.95, imageReport.Fidelity!.BitwiseFidelity);
|
||||
Assert.Equal(5, imageReport.Fidelity.TotalReplays);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FidelityMetricsService_ComputesAllThreeTiers()
|
||||
public void FidelityMetricsService_Calculate_ComputesAllThreeTiers()
|
||||
{
|
||||
// Arrange
|
||||
var service = new FidelityMetricsService(
|
||||
new BitwiseFidelityCalculator(),
|
||||
new SemanticFidelityCalculator(),
|
||||
new PolicyFidelityCalculator());
|
||||
var service = new FidelityMetricsService();
|
||||
|
||||
var baseline = CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "pass");
|
||||
var replay = CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "pass");
|
||||
|
||||
// Act
|
||||
var metrics = service.Compute(baseline, new[] { replay });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, metrics.TotalReplays);
|
||||
Assert.True(metrics.BitwiseFidelity >= 0.0 && metrics.BitwiseFidelity <= 1.0);
|
||||
Assert.True(metrics.SemanticFidelity >= 0.0 && metrics.SemanticFidelity <= 1.0);
|
||||
Assert.True(metrics.PolicyFidelity >= 0.0 && metrics.PolicyFidelity <= 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FidelityMetrics_SemanticEquivalent_ButBitwiseDifferent()
|
||||
{
|
||||
// Arrange - same semantic content, different formatting/ordering
|
||||
var service = new FidelityMetricsService(
|
||||
new BitwiseFidelityCalculator(),
|
||||
new SemanticFidelityCalculator(),
|
||||
new PolicyFidelityCalculator());
|
||||
|
||||
var baseline = CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "HIGH", "pass");
|
||||
var replay = CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "pass"); // case difference
|
||||
|
||||
// Act
|
||||
var metrics = service.Compute(baseline, new[] { replay });
|
||||
|
||||
// Assert
|
||||
// Bitwise should be < 1.0 (different bytes)
|
||||
// Semantic should be 1.0 (same meaning)
|
||||
// Policy should be 1.0 (same decision)
|
||||
Assert.True(metrics.SemanticFidelity >= metrics.BitwiseFidelity);
|
||||
Assert.Equal(1.0, metrics.PolicyFidelity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FidelityMetrics_PolicyDifference_ReflectedInPF()
|
||||
{
|
||||
// Arrange
|
||||
var service = new FidelityMetricsService(
|
||||
new BitwiseFidelityCalculator(),
|
||||
new SemanticFidelityCalculator(),
|
||||
new PolicyFidelityCalculator());
|
||||
|
||||
var baseline = CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "pass");
|
||||
var replay = CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "fail"); // policy differs
|
||||
|
||||
// Act
|
||||
var metrics = service.Compute(baseline, new[] { replay });
|
||||
|
||||
// Assert
|
||||
Assert.True(metrics.PolicyFidelity < 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FidelityMetrics_MultipleReplays_AveragesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var service = new FidelityMetricsService(
|
||||
new BitwiseFidelityCalculator(),
|
||||
new SemanticFidelityCalculator(),
|
||||
new PolicyFidelityCalculator());
|
||||
|
||||
var baseline = CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "pass");
|
||||
var replays = new[]
|
||||
var baselineHashes = new Dictionary<string, string>
|
||||
{
|
||||
CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "pass"), // identical
|
||||
CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "pass"), // identical
|
||||
CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "fail"), // policy diff
|
||||
["sbom.json"] = "sha256:baseline",
|
||||
};
|
||||
var replayHashes = new List<IReadOnlyDictionary<string, string>>
|
||||
{
|
||||
new Dictionary<string, string> { ["sbom.json"] = "sha256:baseline" }
|
||||
};
|
||||
|
||||
// Act
|
||||
var metrics = service.Compute(baseline, replays);
|
||||
var baselineFindings = CreateNormalizedFindings();
|
||||
var replayFindings = new List<NormalizedFindings> { CreateNormalizedFindings() };
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, metrics.TotalReplays);
|
||||
// 2 out of 3 have matching policy
|
||||
Assert.True(metrics.PolicyFidelity >= 0.6 && metrics.PolicyFidelity <= 0.7);
|
||||
}
|
||||
var baselineDecision = CreatePolicyDecision();
|
||||
var replayDecisions = new List<PolicyDecision> { CreatePolicyDecision() };
|
||||
|
||||
[Fact]
|
||||
public void FidelityMetrics_IncludesMismatchDiagnostics()
|
||||
{
|
||||
// Arrange
|
||||
var service = new FidelityMetricsService(
|
||||
new BitwiseFidelityCalculator(),
|
||||
new SemanticFidelityCalculator(),
|
||||
new PolicyFidelityCalculator());
|
||||
var metrics = service.Calculate(
|
||||
baselineHashes, replayHashes,
|
||||
baselineFindings, replayFindings,
|
||||
baselineDecision, replayDecisions);
|
||||
|
||||
var baseline = CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "high", "pass");
|
||||
var replay = CreateTestScanResult("pkg:npm/lodash@4.17.21", "CVE-2021-23337", "critical", "fail"); // semantic + policy diff
|
||||
|
||||
// Act
|
||||
var metrics = service.Compute(baseline, new[] { replay });
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(metrics.Mismatches);
|
||||
Assert.NotEmpty(metrics.Mismatches);
|
||||
Assert.Equal(1, metrics.TotalReplays);
|
||||
Assert.Equal(1.0, metrics.BitwiseFidelity);
|
||||
Assert.Equal(1.0, metrics.SemanticFidelity);
|
||||
Assert.Equal(1.0, metrics.PolicyFidelity);
|
||||
}
|
||||
|
||||
private static FidelityMetrics CreateTestFidelityMetrics(
|
||||
@@ -195,38 +112,22 @@ public sealed class FidelityMetricsIntegrationTests
|
||||
};
|
||||
}
|
||||
|
||||
private static TestScanResult CreateTestScanResult(
|
||||
string purl,
|
||||
string cve,
|
||||
string severity,
|
||||
string policyDecision)
|
||||
private static NormalizedFindings CreateNormalizedFindings() => new()
|
||||
{
|
||||
return new TestScanResult
|
||||
Packages = new List<NormalizedPackage>
|
||||
{
|
||||
Packages = new[] { new TestPackage { Purl = purl } },
|
||||
Findings = new[] { new TestFinding { Cve = cve, Severity = severity } },
|
||||
PolicyDecision = policyDecision,
|
||||
PolicyReasonCodes = policyDecision == "pass" ? Array.Empty<string>() : new[] { "severity_exceeded" }
|
||||
};
|
||||
}
|
||||
new("pkg:npm/test@1.0.0", "1.0.0")
|
||||
},
|
||||
Cves = new HashSet<string> { "CVE-2024-0001" },
|
||||
SeverityCounts = new Dictionary<string, int> { ["MEDIUM"] = 1 },
|
||||
Verdicts = new Dictionary<string, string> { ["overall"] = "pass" }
|
||||
};
|
||||
|
||||
// Test support types
|
||||
private sealed record TestScanResult
|
||||
private static PolicyDecision CreatePolicyDecision() => new()
|
||||
{
|
||||
public required IReadOnlyList<TestPackage> Packages { get; init; }
|
||||
public required IReadOnlyList<TestFinding> Findings { get; init; }
|
||||
public required string PolicyDecision { get; init; }
|
||||
public required IReadOnlyList<string> PolicyReasonCodes { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestPackage
|
||||
{
|
||||
public required string Purl { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestFinding
|
||||
{
|
||||
public required string Cve { get; init; }
|
||||
public required string Severity { get; init; }
|
||||
}
|
||||
Passed = true,
|
||||
ReasonCodes = new List<string> { "CLEAN" },
|
||||
ViolationCount = 0,
|
||||
BlockLevel = "none"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Scanner.Core.Epss;
|
||||
using StellaOps.Scanner.Storage.Epss;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests.Epss;
|
||||
|
||||
public sealed class EpssEnrichmentJobTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EnrichAsync_EmitsPriorityChangedSignalWhenBandChanges()
|
||||
{
|
||||
var modelDate = new DateOnly(2027, 1, 16);
|
||||
|
||||
var changes = new List<EpssChangeRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
CveId = "CVE-2024-0001",
|
||||
Flags = EpssChangeFlags.BigJumpUp,
|
||||
PreviousScore = 0.20,
|
||||
NewScore = 0.70,
|
||||
NewPercentile = 0.995,
|
||||
PreviousBand = EpssPriorityBand.Medium,
|
||||
ModelDate = modelDate
|
||||
}
|
||||
};
|
||||
|
||||
var epssRepository = new Mock<IEpssRepository>(MockBehavior.Strict);
|
||||
epssRepository
|
||||
.Setup(r => r.GetChangesAsync(modelDate, null, 100000, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(changes);
|
||||
|
||||
var epssProvider = new Mock<IEpssProvider>(MockBehavior.Strict);
|
||||
epssProvider
|
||||
.Setup(p => p.GetLatestModelDateAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(modelDate);
|
||||
epssProvider
|
||||
.Setup(p => p.GetCurrentBatchAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EpssBatchResult
|
||||
{
|
||||
ModelDate = modelDate,
|
||||
Found = new[]
|
||||
{
|
||||
EpssEvidence.CreateWithTimestamp(
|
||||
"CVE-2024-0001",
|
||||
score: 0.70,
|
||||
percentile: 0.995,
|
||||
modelDate: modelDate,
|
||||
capturedAt: DateTimeOffset.Parse("2027-01-16T00:07:00Z"),
|
||||
source: "test",
|
||||
fromCache: false)
|
||||
},
|
||||
NotFound = Array.Empty<string>(),
|
||||
PartiallyFromCache = false,
|
||||
LookupTimeMs = 1
|
||||
});
|
||||
|
||||
var published = new List<(string cve, string oldBand, string newBand)>();
|
||||
var publisher = new Mock<IEpssSignalPublisher>(MockBehavior.Strict);
|
||||
publisher
|
||||
.Setup(p => p.PublishPriorityChangedAsync(
|
||||
It.IsAny<Guid>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<double>(),
|
||||
It.IsAny<DateOnly>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<Guid, string, string, string, double, DateOnly, CancellationToken>((_, cve, oldBand, newBand, _, _, _) =>
|
||||
published.Add((cve, oldBand, newBand)))
|
||||
.ReturnsAsync(new EpssSignalPublishResult { Success = true, MessageId = "ok" });
|
||||
|
||||
var job = new EpssEnrichmentJob(
|
||||
epssRepository.Object,
|
||||
epssProvider.Object,
|
||||
publisher.Object,
|
||||
Microsoft.Extensions.Options.Options.Create(new EpssEnrichmentOptions
|
||||
{
|
||||
Enabled = true,
|
||||
BatchSize = 100,
|
||||
FlagsToProcess = EpssChangeFlags.None,
|
||||
HighPercentile = 0.99,
|
||||
CriticalPercentile = 0.995,
|
||||
MediumPercentile = 0.90,
|
||||
}),
|
||||
TimeProvider.System,
|
||||
NullLogger<EpssEnrichmentJob>.Instance);
|
||||
|
||||
await job.EnrichAsync();
|
||||
|
||||
Assert.Single(published);
|
||||
Assert.Equal("CVE-2024-0001", published[0].cve);
|
||||
Assert.Equal(EpssPriorityBand.Medium.ToString(), published[0].oldBand);
|
||||
Assert.Equal(EpssPriorityBand.Critical.ToString(), published[0].newBand);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using StellaOps.Scanner.Core.Epss;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Storage.Epss;
|
||||
using StellaOps.Scanner.Storage.Postgres;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests.Epss;
|
||||
|
||||
[Collection("scanner-worker-postgres")]
|
||||
public sealed class EpssSignalFlowIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ScannerWorkerPostgresFixture _fixture;
|
||||
private ScannerDataSource _dataSource = null!;
|
||||
|
||||
public EpssSignalFlowIntegrationTests(ScannerWorkerPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
var options = new ScannerStorageOptions
|
||||
{
|
||||
Postgres = new StellaOps.Infrastructure.Postgres.Options.PostgresOptions
|
||||
{
|
||||
ConnectionString = _fixture.ConnectionString,
|
||||
SchemaName = _fixture.SchemaName
|
||||
}
|
||||
};
|
||||
|
||||
_dataSource = new ScannerDataSource(Microsoft.Extensions.Options.Options.Create(options), NullLogger<ScannerDataSource>.Instance);
|
||||
|
||||
await using var connection = new NpgsqlConnection(_fixture.ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = $"""
|
||||
CREATE TABLE IF NOT EXISTS {_fixture.SchemaName}.vuln_instance_triage (
|
||||
instance_id UUID PRIMARY KEY,
|
||||
tenant_id UUID NOT NULL,
|
||||
cve_id TEXT NOT NULL
|
||||
);
|
||||
""";
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateSignalsAsync_WritesSignalsPerObservedTenant()
|
||||
{
|
||||
var epssRepository = new PostgresEpssRepository(_dataSource);
|
||||
var signalRepository = new PostgresEpssSignalRepository(_dataSource);
|
||||
var observedCveRepository = new PostgresObservedCveRepository(_dataSource);
|
||||
|
||||
var day1 = new DateOnly(2027, 1, 15);
|
||||
var run1 = await epssRepository.BeginImportAsync(day1, "bundle://day1.csv.gz", DateTimeOffset.Parse("2027-01-15T00:05:00Z"), "sha256:day1");
|
||||
var write1 = await epssRepository.WriteSnapshotAsync(
|
||||
run1.ImportRunId,
|
||||
day1,
|
||||
DateTimeOffset.Parse("2027-01-15T00:06:00Z"),
|
||||
ToAsync(new[]
|
||||
{
|
||||
new EpssScoreRow("CVE-2024-0001", 0.40, 0.90),
|
||||
new EpssScoreRow("CVE-2024-0002", 0.60, 0.96)
|
||||
}));
|
||||
await epssRepository.MarkImportSucceededAsync(run1.ImportRunId, write1.RowCount, "sha256:decompressed1", "v2027.01.15", day1);
|
||||
|
||||
var day2 = new DateOnly(2027, 1, 16);
|
||||
var run2 = await epssRepository.BeginImportAsync(day2, "bundle://day2.csv.gz", DateTimeOffset.Parse("2027-01-16T00:05:00Z"), "sha256:day2");
|
||||
var write2 = await epssRepository.WriteSnapshotAsync(
|
||||
run2.ImportRunId,
|
||||
day2,
|
||||
DateTimeOffset.Parse("2027-01-16T00:06:00Z"),
|
||||
ToAsync(new[]
|
||||
{
|
||||
new EpssScoreRow("CVE-2024-0001", 0.55, 0.95),
|
||||
new EpssScoreRow("CVE-2024-0002", 0.45, 0.94),
|
||||
new EpssScoreRow("CVE-2024-0003", 0.70, 0.97)
|
||||
}));
|
||||
await epssRepository.MarkImportSucceededAsync(run2.ImportRunId, write2.RowCount, "sha256:decompressed2", "v2027.01.16", day2);
|
||||
|
||||
var tenantA = Guid.Parse("aaaaaaaa-1111-1111-1111-111111111111");
|
||||
var tenantB = Guid.Parse("bbbbbbbb-2222-2222-2222-222222222222");
|
||||
|
||||
await InsertTriageRowAsync(tenantA, Guid.Parse("00000000-0000-0000-0000-000000000001"), "CVE-2024-0001");
|
||||
await InsertTriageRowAsync(tenantA, Guid.Parse("00000000-0000-0000-0000-000000000002"), "CVE-2024-0003");
|
||||
await InsertTriageRowAsync(tenantB, Guid.Parse("00000000-0000-0000-0000-000000000003"), "CVE-2024-0002");
|
||||
|
||||
var provider = new FixedEpssProvider(day2);
|
||||
var publisher = new RecordingEpssSignalPublisher();
|
||||
|
||||
var job = new EpssSignalJob(
|
||||
epssRepository,
|
||||
signalRepository,
|
||||
observedCveRepository,
|
||||
publisher,
|
||||
provider,
|
||||
Microsoft.Extensions.Options.Options.Create(new EpssSignalOptions
|
||||
{
|
||||
Enabled = true,
|
||||
BatchSize = 500
|
||||
}),
|
||||
TimeProvider.System,
|
||||
NullLogger<EpssSignalJob>.Instance);
|
||||
|
||||
await job.GenerateSignalsAsync();
|
||||
|
||||
var tenantASignals = await signalRepository.GetByTenantAsync(tenantA, day2, day2);
|
||||
Assert.Equal(2, tenantASignals.Count);
|
||||
Assert.Contains(tenantASignals, s => s.CveId == "CVE-2024-0001");
|
||||
Assert.Contains(tenantASignals, s => s.CveId == "CVE-2024-0003");
|
||||
|
||||
var tenantBSignals = await signalRepository.GetByTenantAsync(tenantB, day2, day2);
|
||||
Assert.Single(tenantBSignals);
|
||||
Assert.Equal("CVE-2024-0002", tenantBSignals[0].CveId);
|
||||
|
||||
Assert.Equal(3, publisher.Published.Count);
|
||||
Assert.All(publisher.Published, s => Assert.Equal(day2, s.ModelDate));
|
||||
}
|
||||
|
||||
private async Task InsertTriageRowAsync(Guid tenantId, Guid instanceId, string cveId)
|
||||
{
|
||||
await using var connection = new NpgsqlConnection(_fixture.ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = $"""
|
||||
INSERT INTO {_fixture.SchemaName}.vuln_instance_triage (instance_id, tenant_id, cve_id)
|
||||
VALUES (@InstanceId, @TenantId, @CveId)
|
||||
ON CONFLICT (instance_id) DO NOTHING;
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("InstanceId", instanceId);
|
||||
cmd.Parameters.AddWithValue("TenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("CveId", cveId);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<EpssScoreRow> ToAsync(IEnumerable<EpssScoreRow> rows)
|
||||
{
|
||||
foreach (var row in rows)
|
||||
{
|
||||
yield return row;
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedEpssProvider : IEpssProvider
|
||||
{
|
||||
private readonly DateOnly? _latestModelDate;
|
||||
|
||||
public FixedEpssProvider(DateOnly? latestModelDate)
|
||||
{
|
||||
_latestModelDate = latestModelDate;
|
||||
}
|
||||
|
||||
public Task<EpssEvidence?> GetCurrentAsync(string cveId, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public Task<EpssBatchResult> GetCurrentBatchAsync(IEnumerable<string> cveIds, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public Task<EpssEvidence?> GetAsOfDateAsync(string cveId, DateOnly asOfDate, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public Task<IReadOnlyList<EpssEvidence>> GetHistoryAsync(string cveId, DateOnly startDate, DateOnly endDate, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public Task<DateOnly?> GetLatestModelDateAsync(CancellationToken cancellationToken = default) => Task.FromResult(_latestModelDate);
|
||||
public Task<bool> IsAvailableAsync(CancellationToken cancellationToken = default) => Task.FromResult(true);
|
||||
}
|
||||
|
||||
private sealed class RecordingEpssSignalPublisher : IEpssSignalPublisher
|
||||
{
|
||||
public List<EpssSignal> Published { get; } = new();
|
||||
|
||||
public Task<EpssSignalPublishResult> PublishAsync(EpssSignal signal, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Published.Add(signal);
|
||||
return Task.FromResult(new EpssSignalPublishResult { Success = true, MessageId = "recorded" });
|
||||
}
|
||||
|
||||
public Task<int> PublishBatchAsync(IEnumerable<EpssSignal> signals, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Published.AddRange(signals);
|
||||
return Task.FromResult(signals.Count());
|
||||
}
|
||||
|
||||
public Task<EpssSignalPublishResult> PublishPriorityChangedAsync(Guid tenantId, string cveId, string oldBand, string newBand, double epssScore, DateOnly modelDate, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new EpssSignalPublishResult { Success = true, MessageId = "recorded" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Scanner.Core.Epss;
|
||||
using StellaOps.Scanner.Storage.Epss;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests.Epss;
|
||||
|
||||
public sealed class EpssSignalJobTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GenerateSignalsAsync_CreatesSignalsAndPublishesBatch()
|
||||
{
|
||||
var modelDate = new DateOnly(2027, 1, 16);
|
||||
var tenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
var provider = new FixedEpssProvider(modelDate);
|
||||
|
||||
var epssRepository = new Mock<IEpssRepository>(MockBehavior.Strict);
|
||||
epssRepository
|
||||
.Setup(r => r.GetImportRunAsync(modelDate, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EpssImportRun(
|
||||
ImportRunId: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||
ModelDate: modelDate,
|
||||
SourceUri: "bundle://test.csv.gz",
|
||||
RetrievedAtUtc: DateTimeOffset.Parse("2027-01-16T00:05:00Z"),
|
||||
FileSha256: "sha256:test",
|
||||
DecompressedSha256: "sha256:decompressed",
|
||||
RowCount: 3,
|
||||
ModelVersionTag: "v2027.01.16",
|
||||
PublishedDate: modelDate,
|
||||
Status: "SUCCEEDED",
|
||||
Error: null,
|
||||
CreatedAtUtc: DateTimeOffset.Parse("2027-01-16T00:06:00Z")));
|
||||
|
||||
var changes = new List<EpssChangeRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
CveId = "CVE-2024-0001",
|
||||
Flags = EpssChangeFlags.BigJumpUp,
|
||||
PreviousScore = 0.10,
|
||||
NewScore = 0.30,
|
||||
NewPercentile = 0.995,
|
||||
PreviousBand = EpssPriorityBand.Medium,
|
||||
ModelDate = modelDate
|
||||
},
|
||||
new()
|
||||
{
|
||||
CveId = "CVE-2024-0002",
|
||||
Flags = EpssChangeFlags.NewScored,
|
||||
PreviousScore = null,
|
||||
NewScore = 0.60,
|
||||
NewPercentile = 0.97,
|
||||
PreviousBand = EpssPriorityBand.Unknown,
|
||||
ModelDate = modelDate
|
||||
}
|
||||
};
|
||||
|
||||
epssRepository
|
||||
.Setup(r => r.GetChangesAsync(modelDate, null, 200000, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(changes);
|
||||
|
||||
var observedCveRepository = new Mock<IObservedCveRepository>(MockBehavior.Strict);
|
||||
observedCveRepository
|
||||
.Setup(r => r.GetActiveTenantsAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new[] { tenantId });
|
||||
observedCveRepository
|
||||
.Setup(r => r.FilterObservedAsync(tenantId, It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Guid _, IEnumerable<string> cves, CancellationToken __) =>
|
||||
new HashSet<string>(cves, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
var createdSignals = new List<EpssSignal>();
|
||||
var signalRepository = new Mock<IEpssSignalRepository>(MockBehavior.Strict);
|
||||
signalRepository
|
||||
.Setup(r => r.CreateBulkAsync(It.IsAny<IEnumerable<EpssSignal>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<EpssSignal>, CancellationToken>((signals, _) => createdSignals.AddRange(signals))
|
||||
.ReturnsAsync((IEnumerable<EpssSignal> signals, CancellationToken _) => signals.Count());
|
||||
signalRepository
|
||||
.Setup(r => r.CreateAsync(It.IsAny<EpssSignal>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((EpssSignal signal, CancellationToken _) => signal);
|
||||
signalRepository
|
||||
.Setup(r => r.PruneAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(0);
|
||||
signalRepository
|
||||
.Setup(r => r.GetByTenantAsync(It.IsAny<Guid>(), It.IsAny<DateOnly>(), It.IsAny<DateOnly>(), It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<EpssSignal>());
|
||||
signalRepository
|
||||
.Setup(r => r.GetByCveAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<EpssSignal>());
|
||||
signalRepository
|
||||
.Setup(r => r.GetHighPriorityAsync(It.IsAny<Guid>(), It.IsAny<DateOnly>(), It.IsAny<DateOnly>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<EpssSignal>());
|
||||
signalRepository
|
||||
.Setup(r => r.GetConfigAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((EpssSignalConfig?)null);
|
||||
signalRepository
|
||||
.Setup(r => r.UpsertConfigAsync(It.IsAny<EpssSignalConfig>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((EpssSignalConfig cfg, CancellationToken _) => cfg);
|
||||
|
||||
var publisher = new Mock<IEpssSignalPublisher>(MockBehavior.Strict);
|
||||
publisher
|
||||
.Setup(p => p.PublishBatchAsync(It.IsAny<IEnumerable<EpssSignal>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IEnumerable<EpssSignal> signals, CancellationToken _) => signals.Count());
|
||||
publisher
|
||||
.Setup(p => p.PublishAsync(It.IsAny<EpssSignal>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EpssSignalPublishResult { Success = true, MessageId = "ok" });
|
||||
publisher
|
||||
.Setup(p => p.PublishPriorityChangedAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>(), It.IsAny<DateOnly>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EpssSignalPublishResult { Success = true, MessageId = "ok" });
|
||||
|
||||
var job = new EpssSignalJob(
|
||||
epssRepository.Object,
|
||||
signalRepository.Object,
|
||||
observedCveRepository.Object,
|
||||
publisher.Object,
|
||||
provider,
|
||||
Microsoft.Extensions.Options.Options.Create(new EpssSignalOptions
|
||||
{
|
||||
Enabled = true,
|
||||
BatchSize = 500
|
||||
}),
|
||||
TimeProvider.System,
|
||||
NullLogger<EpssSignalJob>.Instance);
|
||||
|
||||
await job.GenerateSignalsAsync();
|
||||
|
||||
Assert.Equal(2, createdSignals.Count);
|
||||
Assert.All(createdSignals, s =>
|
||||
{
|
||||
Assert.Equal(tenantId, s.TenantId);
|
||||
Assert.Equal(modelDate, s.ModelDate);
|
||||
Assert.Equal("v2027.01.16", s.ModelVersion);
|
||||
Assert.False(s.IsModelChange);
|
||||
Assert.False(string.IsNullOrWhiteSpace(s.DedupeKey));
|
||||
Assert.NotNull(s.ExplainHash);
|
||||
Assert.NotEmpty(s.ExplainHash);
|
||||
});
|
||||
|
||||
Assert.Contains(createdSignals, s => s.EventType == EpssSignalEventTypes.NewHigh && s.CveId == "CVE-2024-0002");
|
||||
Assert.Contains(createdSignals, s => s.EventType == EpssSignalEventTypes.RiskSpike && s.CveId == "CVE-2024-0001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateSignalsAsync_EmitsModelUpdatedSummarySignal()
|
||||
{
|
||||
var modelDate = new DateOnly(2027, 1, 16);
|
||||
var tenantId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
var provider = new FixedEpssProvider(modelDate);
|
||||
|
||||
var epssRepository = new Mock<IEpssRepository>(MockBehavior.Strict);
|
||||
epssRepository
|
||||
.SetupSequence(r => r.GetImportRunAsync(modelDate, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EpssImportRun(
|
||||
ImportRunId: Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||
ModelDate: modelDate,
|
||||
SourceUri: "bundle://test.csv.gz",
|
||||
RetrievedAtUtc: DateTimeOffset.Parse("2027-01-16T00:05:00Z"),
|
||||
FileSha256: "sha256:test",
|
||||
DecompressedSha256: "sha256:decompressed",
|
||||
RowCount: 1,
|
||||
ModelVersionTag: "v2027.01.16",
|
||||
PublishedDate: modelDate,
|
||||
Status: "SUCCEEDED",
|
||||
Error: null,
|
||||
CreatedAtUtc: DateTimeOffset.Parse("2027-01-16T00:06:00Z")))
|
||||
.ReturnsAsync(new EpssImportRun(
|
||||
ImportRunId: Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
|
||||
ModelDate: modelDate,
|
||||
SourceUri: "bundle://test.csv.gz",
|
||||
RetrievedAtUtc: DateTimeOffset.Parse("2027-01-16T00:05:00Z"),
|
||||
FileSha256: "sha256:test",
|
||||
DecompressedSha256: "sha256:decompressed",
|
||||
RowCount: 1,
|
||||
ModelVersionTag: "v2027.01.16b",
|
||||
PublishedDate: modelDate,
|
||||
Status: "SUCCEEDED",
|
||||
Error: null,
|
||||
CreatedAtUtc: DateTimeOffset.Parse("2027-01-16T00:06:00Z")));
|
||||
|
||||
var changes = new List<EpssChangeRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
CveId = "CVE-2024-0001",
|
||||
Flags = EpssChangeFlags.NewScored,
|
||||
PreviousScore = null,
|
||||
NewScore = 0.10,
|
||||
NewPercentile = 0.91,
|
||||
PreviousBand = EpssPriorityBand.Unknown,
|
||||
ModelDate = modelDate
|
||||
}
|
||||
};
|
||||
|
||||
epssRepository
|
||||
.Setup(r => r.GetChangesAsync(modelDate, null, 200000, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(changes);
|
||||
|
||||
var observedCveRepository = new Mock<IObservedCveRepository>(MockBehavior.Strict);
|
||||
observedCveRepository
|
||||
.Setup(r => r.GetActiveTenantsAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new[] { tenantId });
|
||||
observedCveRepository
|
||||
.Setup(r => r.FilterObservedAsync(tenantId, It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Guid _, IEnumerable<string> cves, CancellationToken __) =>
|
||||
new HashSet<string>(cves, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
var createdSignals = new List<EpssSignal>();
|
||||
var createdSummaries = new List<EpssSignal>();
|
||||
|
||||
var signalRepository = new Mock<IEpssSignalRepository>(MockBehavior.Strict);
|
||||
signalRepository
|
||||
.Setup(r => r.CreateBulkAsync(It.IsAny<IEnumerable<EpssSignal>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<EpssSignal>, CancellationToken>((signals, _) => createdSignals.AddRange(signals))
|
||||
.ReturnsAsync((IEnumerable<EpssSignal> signals, CancellationToken _) => signals.Count());
|
||||
signalRepository
|
||||
.Setup(r => r.CreateAsync(It.IsAny<EpssSignal>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<EpssSignal, CancellationToken>((signal, _) => createdSummaries.Add(signal))
|
||||
.ReturnsAsync((EpssSignal signal, CancellationToken _) => signal);
|
||||
signalRepository
|
||||
.Setup(r => r.PruneAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(0);
|
||||
signalRepository
|
||||
.Setup(r => r.GetByTenantAsync(It.IsAny<Guid>(), It.IsAny<DateOnly>(), It.IsAny<DateOnly>(), It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<EpssSignal>());
|
||||
signalRepository
|
||||
.Setup(r => r.GetByCveAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<EpssSignal>());
|
||||
signalRepository
|
||||
.Setup(r => r.GetHighPriorityAsync(It.IsAny<Guid>(), It.IsAny<DateOnly>(), It.IsAny<DateOnly>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<EpssSignal>());
|
||||
signalRepository
|
||||
.Setup(r => r.GetConfigAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((EpssSignalConfig?)null);
|
||||
signalRepository
|
||||
.Setup(r => r.UpsertConfigAsync(It.IsAny<EpssSignalConfig>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((EpssSignalConfig cfg, CancellationToken _) => cfg);
|
||||
|
||||
var publisher = new Mock<IEpssSignalPublisher>(MockBehavior.Strict);
|
||||
publisher
|
||||
.Setup(p => p.PublishBatchAsync(It.IsAny<IEnumerable<EpssSignal>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IEnumerable<EpssSignal> signals, CancellationToken _) => signals.Count());
|
||||
publisher
|
||||
.Setup(p => p.PublishAsync(It.IsAny<EpssSignal>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EpssSignalPublishResult { Success = true, MessageId = "ok" });
|
||||
publisher
|
||||
.Setup(p => p.PublishPriorityChangedAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>(), It.IsAny<DateOnly>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EpssSignalPublishResult { Success = true, MessageId = "ok" });
|
||||
|
||||
var job = new EpssSignalJob(
|
||||
epssRepository.Object,
|
||||
signalRepository.Object,
|
||||
observedCveRepository.Object,
|
||||
publisher.Object,
|
||||
provider,
|
||||
Microsoft.Extensions.Options.Options.Create(new EpssSignalOptions
|
||||
{
|
||||
Enabled = true,
|
||||
BatchSize = 500
|
||||
}),
|
||||
TimeProvider.System,
|
||||
NullLogger<EpssSignalJob>.Instance);
|
||||
|
||||
await job.GenerateSignalsAsync(); // establishes _lastModelVersion
|
||||
await job.GenerateSignalsAsync(); // model version changes -> emits summary
|
||||
|
||||
Assert.Single(createdSummaries);
|
||||
Assert.Equal(EpssSignalEventTypes.ModelUpdated, createdSummaries[0].EventType);
|
||||
Assert.Equal("MODEL_UPDATE", createdSummaries[0].CveId);
|
||||
Assert.True(createdSummaries[0].IsModelChange);
|
||||
Assert.Contains("v2027.01.16->v2027.01.16b", createdSummaries[0].DedupeKey, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private sealed class FixedEpssProvider : IEpssProvider
|
||||
{
|
||||
private readonly DateOnly? _latestModelDate;
|
||||
|
||||
public FixedEpssProvider(DateOnly? latestModelDate)
|
||||
{
|
||||
_latestModelDate = latestModelDate;
|
||||
}
|
||||
|
||||
public Task<EpssEvidence?> GetCurrentAsync(string cveId, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public Task<EpssBatchResult> GetCurrentBatchAsync(IEnumerable<string> cveIds, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public Task<EpssEvidence?> GetAsOfDateAsync(string cveId, DateOnly asOfDate, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public Task<IReadOnlyList<EpssEvidence>> GetHistoryAsync(string cveId, DateOnly startDate, DateOnly endDate, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public Task<DateOnly?> GetLatestModelDateAsync(CancellationToken cancellationToken = default) => Task.FromResult(_latestModelDate);
|
||||
public Task<bool> IsAvailableAsync(CancellationToken cancellationToken = default) => Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Reflection;
|
||||
using StellaOps.Infrastructure.Postgres.Testing;
|
||||
using StellaOps.Scanner.Storage;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests.Epss;
|
||||
|
||||
public sealed class ScannerWorkerPostgresFixture : PostgresIntegrationFixture, ICollectionFixture<ScannerWorkerPostgresFixture>
|
||||
{
|
||||
protected override Assembly? GetMigrationAssembly() => typeof(ScannerStorageOptions).Assembly;
|
||||
|
||||
protected override string GetModuleName() => "Scanner.Storage";
|
||||
}
|
||||
|
||||
[CollectionDefinition("scanner-worker-postgres")]
|
||||
public sealed class ScannerWorkerPostgresCollection : ICollectionFixture<ScannerWorkerPostgresFixture>
|
||||
{
|
||||
}
|
||||
@@ -29,8 +29,8 @@ public sealed class ScanCompletionMetricsIntegrationTests
|
||||
.Callback<ScanMetrics, CancellationToken>((m, _) => savedMetrics.Add(m))
|
||||
.Returns(Task.CompletedTask);
|
||||
mockRepository
|
||||
.Setup(r => r.SavePhasesAsync(It.IsAny<IEnumerable<ExecutionPhase>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ExecutionPhase>, CancellationToken>((p, _) => savedPhases.AddRange(p))
|
||||
.Setup(r => r.SavePhasesAsync(It.IsAny<IReadOnlyList<ExecutionPhase>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IReadOnlyList<ExecutionPhase>, CancellationToken>((p, _) => savedPhases.AddRange(p))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var factory = new TestScanMetricsCollectorFactory(mockRepository.Object);
|
||||
@@ -120,7 +120,7 @@ public sealed class ScanCompletionMetricsIntegrationTests
|
||||
.Callback<ScanMetrics, CancellationToken>((m, _) => savedMetrics.Add(m))
|
||||
.Returns(Task.CompletedTask);
|
||||
mockRepository
|
||||
.Setup(r => r.SavePhasesAsync(It.IsAny<IEnumerable<ExecutionPhase>>(), It.IsAny<CancellationToken>()))
|
||||
.Setup(r => r.SavePhasesAsync(It.IsAny<IReadOnlyList<ExecutionPhase>>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var factory = new TestScanMetricsCollectorFactory(mockRepository.Object);
|
||||
@@ -162,7 +162,7 @@ public sealed class ScanCompletionMetricsIntegrationTests
|
||||
.Callback<ScanMetrics, CancellationToken>((m, _) => savedMetrics.Add(m))
|
||||
.Returns(Task.CompletedTask);
|
||||
mockRepository
|
||||
.Setup(r => r.SavePhasesAsync(It.IsAny<IEnumerable<ExecutionPhase>>(), It.IsAny<CancellationToken>()))
|
||||
.Setup(r => r.SavePhasesAsync(It.IsAny<IReadOnlyList<ExecutionPhase>>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var factory = new TestScanMetricsCollectorFactory(mockRepository.Object);
|
||||
|
||||
@@ -11,5 +11,9 @@
|
||||
<ProjectReference Include="../../StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Queue/StellaOps.Scanner.Queue.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Ruby/StellaOps.Scanner.Analyzers.Lang.Ruby.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user