save progress
This commit is contained in:
@@ -90,7 +90,7 @@ public class NativeFormatDetectorTests
|
||||
Assert.Equal(NativeFormat.Elf, id.Format);
|
||||
Assert.Equal("x86_64", id.CpuArchitecture);
|
||||
Assert.Equal("/lib64/ld-linux-x86-64.so.2", id.InterpreterPath);
|
||||
Assert.Equal("0102030405060708090a0b0c0d0e0f10", id.BuildId);
|
||||
Assert.Equal("gnu-build-id:0102030405060708090a0b0c0d0e0f10", id.BuildId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -150,7 +150,7 @@ public class NativeFormatDetectorTests
|
||||
var cmdOffset = 32;
|
||||
BitConverter.GetBytes((uint)0x1B).CopyTo(buffer, cmdOffset); // LC_UUID
|
||||
BitConverter.GetBytes((uint)32).CopyTo(buffer, cmdOffset + 4); // cmdsize
|
||||
var uuid = Guid.NewGuid();
|
||||
var uuid = Guid.Parse("f81e1e08-4373-4df0-8a9e-19c23e2addc5");
|
||||
uuid.ToByteArray().CopyTo(buffer, cmdOffset + 8);
|
||||
|
||||
using var stream = new MemoryStream(buffer);
|
||||
@@ -158,7 +158,7 @@ public class NativeFormatDetectorTests
|
||||
|
||||
Assert.True(detected);
|
||||
Assert.Equal(NativeFormat.MachO, id.Format);
|
||||
Assert.Equal(uuid.ToString(), id.Uuid);
|
||||
Assert.Equal($"macho-uuid:{Convert.ToHexString(uuid.ToByteArray()).ToLowerInvariant()}", id.Uuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -19,7 +19,7 @@ public class PeImportParserTests : NativeTestBase
|
||||
var info = ParsePe(pe);
|
||||
|
||||
info.Is64Bit.Should().BeFalse();
|
||||
info.Machine.Should().Be("x86_64");
|
||||
info.Machine.Should().Be("x86");
|
||||
info.Subsystem.Should().Be(PeSubsystem.WindowsConsole);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using StellaOps.Scanner.Storage.Epss;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Tests;
|
||||
|
||||
public sealed class EpssChangeDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeFlags_MatchesExpectedBitmask()
|
||||
{
|
||||
var thresholds = EpssChangeDetector.DefaultThresholds;
|
||||
|
||||
var crossedHigh = EpssChangeDetector.ComputeFlags(
|
||||
oldScore: 0.40,
|
||||
newScore: 0.55,
|
||||
oldPercentile: 0.90,
|
||||
newPercentile: 0.95,
|
||||
thresholds);
|
||||
Assert.Equal(
|
||||
EpssChangeFlags.CrossedHigh | EpssChangeFlags.BigJumpUp | EpssChangeFlags.TopPercentile,
|
||||
crossedHigh);
|
||||
|
||||
var crossedLow = EpssChangeDetector.ComputeFlags(
|
||||
oldScore: 0.60,
|
||||
newScore: 0.45,
|
||||
oldPercentile: 0.96,
|
||||
newPercentile: 0.94,
|
||||
thresholds);
|
||||
Assert.Equal(
|
||||
EpssChangeFlags.CrossedLow | EpssChangeFlags.BigJumpDown | EpssChangeFlags.LeftTopPercentile,
|
||||
crossedLow);
|
||||
|
||||
var newScored = EpssChangeDetector.ComputeFlags(
|
||||
oldScore: null,
|
||||
newScore: 0.70,
|
||||
oldPercentile: null,
|
||||
newPercentile: 0.97,
|
||||
thresholds);
|
||||
Assert.Equal(EpssChangeFlags.NewScored | EpssChangeFlags.TopPercentile, newScored);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Storage.Epss;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Tests;
|
||||
|
||||
public sealed class EpssCsvStreamParserTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ParseGzip_ParsesRowsAndComputesDecompressedHash()
|
||||
{
|
||||
var csv = string.Join('\n',
|
||||
[
|
||||
"# EPSS v2025.12.17 published 2025-12-17",
|
||||
"cve,epss,percentile",
|
||||
"CVE-2024-0001,0.1,0.5",
|
||||
"cve-2024-0002,1.0,1.0",
|
||||
""
|
||||
]);
|
||||
|
||||
var decompressedBytes = Encoding.UTF8.GetBytes(csv);
|
||||
var expectedHash = "sha256:" + Convert.ToHexString(SHA256.HashData(decompressedBytes)).ToLowerInvariant();
|
||||
|
||||
await using var gzipBytes = new MemoryStream();
|
||||
await using (var gzip = new GZipStream(gzipBytes, CompressionLevel.Optimal, leaveOpen: true))
|
||||
{
|
||||
await gzip.WriteAsync(decompressedBytes);
|
||||
}
|
||||
gzipBytes.Position = 0;
|
||||
|
||||
var parser = new EpssCsvStreamParser();
|
||||
var session = parser.ParseGzip(gzipBytes);
|
||||
|
||||
var rows = new List<EpssScoreRow>();
|
||||
await foreach (var row in session)
|
||||
{
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
Assert.Equal(2, session.RowCount);
|
||||
Assert.Equal("v2025.12.17", session.ModelVersionTag);
|
||||
Assert.Equal(new DateOnly(2025, 12, 17), session.PublishedDate);
|
||||
Assert.Equal(expectedHash, session.DecompressedSha256);
|
||||
|
||||
Assert.Equal("CVE-2024-0001", rows[0].CveId);
|
||||
Assert.Equal(0.1, rows[0].Score, precision: 6);
|
||||
Assert.Equal(0.5, rows[0].Percentile, precision: 6);
|
||||
Assert.Equal("CVE-2024-0002", rows[1].CveId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Storage.Epss;
|
||||
using StellaOps.Scanner.Storage.Postgres;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Tests;
|
||||
|
||||
[Collection("scanner-postgres")]
|
||||
public sealed class EpssRepositoryIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ScannerPostgresFixture _fixture;
|
||||
private ScannerDataSource _dataSource = null!;
|
||||
private PostgresEpssRepository _repository = null!;
|
||||
|
||||
public EpssRepositoryIntegrationTests(ScannerPostgresFixture 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(Options.Create(options), NullLoggerFactory.Instance.CreateLogger<ScannerDataSource>());
|
||||
_repository = new PostgresEpssRepository(_dataSource);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task WriteSnapshot_ComputesChangesAndUpdatesCurrent()
|
||||
{
|
||||
var thresholds = EpssChangeDetector.DefaultThresholds;
|
||||
|
||||
var day1 = new DateOnly(2027, 1, 15);
|
||||
var run1 = await _repository.BeginImportAsync(day1, "bundle://day1.csv.gz", DateTimeOffset.Parse("2027-01-15T00:05:00Z"), "sha256:day1");
|
||||
Assert.Equal("PENDING", run1.Status);
|
||||
|
||||
var day1Rows = new[]
|
||||
{
|
||||
new EpssScoreRow("CVE-2024-0001", 0.40, 0.90),
|
||||
new EpssScoreRow("CVE-2024-0002", 0.60, 0.96)
|
||||
};
|
||||
|
||||
var write1 = await _repository.WriteSnapshotAsync(run1.ImportRunId, day1, DateTimeOffset.Parse("2027-01-15T00:06:00Z"), ToAsync(day1Rows));
|
||||
Assert.Equal(day1Rows.Length, write1.RowCount);
|
||||
await _repository.MarkImportSucceededAsync(run1.ImportRunId, write1.RowCount, decompressedSha256: "sha256:decompressed1", modelVersionTag: "v2027.01.15", publishedDate: day1);
|
||||
|
||||
var day2 = new DateOnly(2027, 1, 16);
|
||||
var run2 = await _repository.BeginImportAsync(day2, "bundle://day2.csv.gz", DateTimeOffset.Parse("2027-01-16T00:05:00Z"), "sha256:day2");
|
||||
|
||||
var day2Rows = 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)
|
||||
};
|
||||
|
||||
var write2 = await _repository.WriteSnapshotAsync(run2.ImportRunId, day2, DateTimeOffset.Parse("2027-01-16T00:06:00Z"), ToAsync(day2Rows));
|
||||
Assert.Equal(day2Rows.Length, write2.RowCount);
|
||||
await _repository.MarkImportSucceededAsync(run2.ImportRunId, write2.RowCount, decompressedSha256: "sha256:decompressed2", modelVersionTag: "v2027.01.16", publishedDate: day2);
|
||||
|
||||
var current = await _repository.GetCurrentAsync(new[] { "CVE-2024-0001", "CVE-2024-0002", "CVE-2024-0003" });
|
||||
Assert.Equal(3, current.Count);
|
||||
Assert.Equal(day2, current["CVE-2024-0001"].ModelDate);
|
||||
|
||||
await using var connection = await _dataSource.OpenSystemConnectionAsync();
|
||||
var changes = (await connection.QueryAsync<ChangeRow>(
|
||||
"""
|
||||
SELECT cve_id, old_score, new_score, old_percentile, new_percentile, flags
|
||||
FROM epss_changes
|
||||
WHERE model_date = @ModelDate
|
||||
ORDER BY cve_id
|
||||
""",
|
||||
new { ModelDate = day2 })).ToList();
|
||||
|
||||
Assert.Equal(3, changes.Count);
|
||||
|
||||
var cve1 = changes.Single(c => c.cve_id == "CVE-2024-0001");
|
||||
Assert.Equal(
|
||||
(int)EpssChangeDetector.ComputeFlags(cve1.old_score, cve1.new_score, cve1.old_percentile, cve1.new_percentile, thresholds),
|
||||
cve1.flags);
|
||||
|
||||
var cve2 = changes.Single(c => c.cve_id == "CVE-2024-0002");
|
||||
Assert.Equal(
|
||||
(int)EpssChangeDetector.ComputeFlags(cve2.old_score, cve2.new_score, cve2.old_percentile, cve2.new_percentile, thresholds),
|
||||
cve2.flags);
|
||||
|
||||
var cve3 = changes.Single(c => c.cve_id == "CVE-2024-0003");
|
||||
Assert.Null(cve3.old_score);
|
||||
Assert.Equal(
|
||||
(int)EpssChangeDetector.ComputeFlags(cve3.old_score, cve3.new_score, cve3.old_percentile, cve3.new_percentile, thresholds),
|
||||
cve3.flags);
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<EpssScoreRow> ToAsync(IEnumerable<EpssScoreRow> rows)
|
||||
{
|
||||
foreach (var row in rows)
|
||||
{
|
||||
yield return row;
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ChangeRow
|
||||
{
|
||||
public string cve_id { get; set; } = "";
|
||||
public double? old_score { get; set; }
|
||||
public double new_score { get; set; }
|
||||
public double? old_percentile { get; set; }
|
||||
public double new_percentile { get; set; }
|
||||
public int flags { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ public sealed class AuthorizationTests
|
||||
[Fact]
|
||||
public async Task ApiRoutesRequireAuthenticationWhenAuthorityEnabled()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "true";
|
||||
configuration["scanner:authority:allowAnonymousFallback"] = "false";
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class CallGraphEndpointsTests
|
||||
public async Task SubmitCallGraphRequiresContentDigestHeader()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
@@ -30,7 +30,7 @@ public sealed class CallGraphEndpointsTests
|
||||
public async Task SubmitCallGraphReturnsAcceptedAndDetectsDuplicates()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
@@ -101,4 +101,3 @@ public sealed class CallGraphEndpointsTests
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace StellaOps.Scanner.WebService.Tests.Integration;
|
||||
/// End-to-end integration tests for the Triage workflow.
|
||||
/// Tests the complete flow from alert list to decision recording.
|
||||
/// </summary>
|
||||
public sealed class TriageWorkflowIntegrationTests : IClassFixture<ScannerApplicationFactory>
|
||||
public sealed class TriageWorkflowIntegrationTests : IClassFixture<ScannerApplicationFixture>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
@@ -23,9 +23,9 @@ public sealed class TriageWorkflowIntegrationTests : IClassFixture<ScannerApplic
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public TriageWorkflowIntegrationTests(ScannerApplicationFactory factory)
|
||||
public TriageWorkflowIntegrationTests(ScannerApplicationFixture fixture)
|
||||
{
|
||||
_client = factory.CreateClient();
|
||||
_client = fixture.Factory.CreateClient();
|
||||
}
|
||||
|
||||
#region Alert List Tests
|
||||
|
||||
@@ -4,6 +4,10 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
@@ -22,7 +26,7 @@ public sealed class OfflineKitEndpointsTests
|
||||
var (keyId, keyPem, dsseJson) = CreateSignedDsse(bundleBytes);
|
||||
File.WriteAllText(Path.Combine(trustRoots.Path, $"{keyId}.pem"), keyPem, Encoding.UTF8);
|
||||
|
||||
using var factory = new ScannerApplicationFactory(config =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "true";
|
||||
@@ -89,7 +93,7 @@ public sealed class OfflineKitEndpointsTests
|
||||
signatures = new[] { new { keyid = keyId, sig = Convert.ToBase64String(new byte[] { 1, 2, 3 }) } }
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var factory = new ScannerApplicationFactory(config =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "true";
|
||||
@@ -142,7 +146,7 @@ public sealed class OfflineKitEndpointsTests
|
||||
signatures = new[] { new { keyid = "unknown", sig = Convert.ToBase64String(new byte[] { 1, 2, 3 }) } }
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var factory = new ScannerApplicationFactory(config =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "false";
|
||||
@@ -172,6 +176,57 @@ public sealed class OfflineKitEndpointsTests
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OfflineKitImport_EmitsAuditEvent_WithTenantHeader()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
var bundleBytes = Encoding.UTF8.GetBytes("deterministic-offline-kit-bundle");
|
||||
var bundleSha = ComputeSha256Hex(bundleBytes);
|
||||
|
||||
var auditEmitter = new CapturingAuditEmitter();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "false";
|
||||
config["Scanner:OfflineKit:RekorOfflineMode"] = "false";
|
||||
}, configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IOfflineKitAuditEmitter>();
|
||||
services.AddSingleton<IOfflineKitAuditEmitter>(auditEmitter);
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var metadataJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
bundleId = "test-bundle",
|
||||
bundleSha256 = $"sha256:{bundleSha}",
|
||||
bundleSize = bundleBytes.Length
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var content = new MultipartFormDataContent();
|
||||
content.Add(new StringContent(metadataJson, Encoding.UTF8, "application/json"), "metadata");
|
||||
|
||||
var bundleContent = new ByteArrayContent(bundleBytes);
|
||||
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
content.Add(bundleContent, "bundle", "bundle.tgz");
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/offline-kit/import") { Content = content };
|
||||
request.Headers.Add("X-Stella-Tenant", "tenant-a");
|
||||
|
||||
using var response = await client.SendAsync(request).ConfigureAwait(false);
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
|
||||
var entity = auditEmitter.LastRecorded;
|
||||
Assert.NotNull(entity);
|
||||
Assert.Equal("tenant-a", entity!.TenantId);
|
||||
Assert.Equal("offlinekit.import", entity.EventType);
|
||||
Assert.Equal("accepted", entity.Result);
|
||||
}
|
||||
|
||||
private static string ComputeSha256Hex(byte[] bytes)
|
||||
=> Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
|
||||
@@ -247,4 +302,21 @@ public sealed class OfflineKitEndpointsTests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CapturingAuditEmitter : IOfflineKitAuditEmitter
|
||||
{
|
||||
private readonly object gate = new();
|
||||
|
||||
public OfflineKitAuditEntity? LastRecorded { get; private set; }
|
||||
|
||||
public Task RecordAsync(OfflineKitAuditEntity entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
LastRecorded = entity;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public sealed class PlatformEventPublisherRegistrationTests
|
||||
[Fact]
|
||||
public void NullPublisherRegisteredWhenEventsDisabled()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:events:enabled"] = "false";
|
||||
configuration["scanner:events:dsn"] = string.Empty;
|
||||
@@ -40,7 +40,7 @@ public sealed class PlatformEventPublisherRegistrationTests
|
||||
|
||||
try
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:events:enabled"] = "true";
|
||||
configuration["scanner:events:driver"] = "redis";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
@@ -17,7 +18,7 @@ public sealed class ReachabilityDriftEndpointsTests
|
||||
public async Task GetDriftReturnsNotFoundWhenNoResultAndNoBaseScanProvided()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
@@ -35,15 +36,15 @@ public sealed class ReachabilityDriftEndpointsTests
|
||||
public async Task GetDriftComputesResultAndListsDriftedSinks()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var baseScanId = await CreateScanAsync(client);
|
||||
var headScanId = await CreateScanAsync(client);
|
||||
var baseScanId = await CreateScanAsync(client, "base");
|
||||
var headScanId = await CreateScanAsync(client, "head");
|
||||
|
||||
await SeedCallGraphSnapshotsAsync(factory.Services, baseScanId, headScanId);
|
||||
|
||||
@@ -134,7 +135,7 @@ public sealed class ReachabilityDriftEndpointsTests
|
||||
return provisional with { GraphDigest = CallGraphDigests.ComputeGraphDigest(provisional) };
|
||||
}
|
||||
|
||||
private static async Task<string> CreateScanAsync(HttpClient client)
|
||||
private static async Task<string> CreateScanAsync(HttpClient client, string? clientRequestId = null)
|
||||
{
|
||||
var response = await client.PostAsJsonAsync("/api/v1/scans", new ScanSubmitRequest
|
||||
{
|
||||
@@ -142,6 +143,11 @@ public sealed class ReachabilityDriftEndpointsTests
|
||||
{
|
||||
Reference = "example.com/demo:1.0",
|
||||
Digest = "sha256:0123456789abcdef"
|
||||
},
|
||||
ClientRequestId = clientRequestId,
|
||||
Metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["test.request"] = clientRequestId ?? string.Empty
|
||||
}
|
||||
});
|
||||
|
||||
@@ -161,4 +167,3 @@ public sealed class ReachabilityDriftEndpointsTests
|
||||
int Count,
|
||||
DriftedSink[] Sinks);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ rules:
|
||||
|
||||
var hmacKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("scanner-report-hmac-key-2025!"));
|
||||
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:signing:enabled"] = "true";
|
||||
configuration["scanner:signing:keyId"] = "scanner-report-signing";
|
||||
@@ -148,7 +148,7 @@ rules:
|
||||
action: block
|
||||
""";
|
||||
|
||||
using var factory = new ScannerApplicationFactory(
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configuration =>
|
||||
{
|
||||
configuration["scanner:signing:enabled"] = "true";
|
||||
|
||||
@@ -241,7 +241,7 @@ public sealed class RubyPackagesEndpointsTests
|
||||
new EntryTraceNdjsonMetadata("scan-placeholder", digest, generatedAt));
|
||||
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory(configureServices: services =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<IEntryTraceResultStore, RecordingEntryTraceResultStore>();
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ public sealed class RuntimeEndpointsTests
|
||||
[Fact]
|
||||
public async Task RuntimeEventsEndpointEnforcesRateLimit()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:runtime:perNodeBurst"] = "1";
|
||||
configuration["scanner:runtime:perNodeEventsPerSecond"] = "1";
|
||||
@@ -105,7 +105,7 @@ public sealed class RuntimeEndpointsTests
|
||||
[Fact]
|
||||
public async Task RuntimePolicyEndpointReturnsDecisions()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:runtime:policyCacheTtlSeconds"] = "600";
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory(
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
@@ -98,7 +98,7 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory(
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
@@ -188,7 +188,7 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory(
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
@@ -273,7 +273,7 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory(
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
@@ -398,7 +398,7 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory(
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class SbomEndpointsTests
|
||||
public async Task SubmitSbomAcceptsCycloneDxJson()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
}, configureServices: services =>
|
||||
|
||||
@@ -35,22 +35,36 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
|
||||
["scanner:features:enableSignedReports"] = "false"
|
||||
};
|
||||
|
||||
private readonly Action<IDictionary<string, string?>>? configureConfiguration;
|
||||
private readonly Action<IServiceCollection>? configureServices;
|
||||
private Action<IDictionary<string, string?>>? configureConfiguration;
|
||||
private Action<IServiceCollection>? configureServices;
|
||||
|
||||
public ScannerApplicationFactory(
|
||||
Action<IDictionary<string, string?>>? configureConfiguration = null,
|
||||
Action<IServiceCollection>? configureServices = null)
|
||||
public ScannerApplicationFactory()
|
||||
{
|
||||
postgresFixture = new ScannerWebServicePostgresFixture();
|
||||
postgresFixture.InitializeAsync().GetAwaiter().GetResult();
|
||||
|
||||
configuration["scanner:storage:dsn"] = postgresFixture.ConnectionString;
|
||||
configuration["scanner:storage:database"] = postgresFixture.SchemaName;
|
||||
}
|
||||
|
||||
public ScannerApplicationFactory(
|
||||
Action<IDictionary<string, string?>>? configureConfiguration = null,
|
||||
Action<IServiceCollection>? configureServices = null)
|
||||
: this()
|
||||
{
|
||||
this.configureConfiguration = configureConfiguration;
|
||||
this.configureServices = configureServices;
|
||||
}
|
||||
|
||||
public ScannerApplicationFactory WithOverrides(
|
||||
Action<IDictionary<string, string?>>? configureConfiguration = null,
|
||||
Action<IServiceCollection>? configureServices = null)
|
||||
{
|
||||
this.configureConfiguration = configureConfiguration;
|
||||
this.configureServices = configureServices;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
configureConfiguration?.Invoke(configuration);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
public sealed class ScannerApplicationFixture : IDisposable
|
||||
{
|
||||
public ScannerApplicationFactory Factory { get; } = new();
|
||||
|
||||
public void Dispose() => Factory.Dispose();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ public sealed partial class ScansEndpointsTests
|
||||
public async Task EntropyEndpoint_AttachesSnapshot_AndSurfacesInStatus()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory(cfg =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
|
||||
{
|
||||
cfg["scanner:authority:enabled"] = "false";
|
||||
cfg["scanner:authority:allowAnonymousFallback"] = "true";
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed partial class ScansEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var store = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory(configureConfiguration: cfg =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configureConfiguration: cfg =>
|
||||
{
|
||||
cfg["scanner:artifactStore:bucket"] = "replay-bucket";
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed partial class ScansEndpointsTests
|
||||
public async Task RecordModeService_AttachesReplayAndSurfacedInStatus()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory(cfg =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
|
||||
{
|
||||
cfg["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed partial class ScansEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
RecordingCoordinator coordinator = null!;
|
||||
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
}, configureServices: services =>
|
||||
@@ -78,7 +78,7 @@ public sealed partial class ScansEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
RecordingCoordinator coordinator = null!;
|
||||
|
||||
using var factory = new ScannerApplicationFactory(configuration =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:determinism:feedSnapshotId"] = "feed-2025-11-26";
|
||||
configuration["scanner:determinism:policySnapshotId"] = "rev-42";
|
||||
@@ -149,7 +149,7 @@ public sealed partial class ScansEndpointsTests
|
||||
var ndjson = EntryTraceNdjsonWriter.Serialize(graph, new EntryTraceNdjsonMetadata(scanId, "sha256:test", generatedAt));
|
||||
var storedResult = new EntryTraceResult(scanId, "sha256:test", generatedAt, graph, ndjson);
|
||||
|
||||
using var factory = new ScannerApplicationFactory(configureServices: services =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(storedResult));
|
||||
});
|
||||
@@ -169,7 +169,7 @@ public sealed partial class ScansEndpointsTests
|
||||
public async Task GetEntryTraceReturnsNotFoundWhenMissing()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory(configureServices: services =>
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(null));
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class ScoreReplayEndpointsTests : IDisposable
|
||||
public ScoreReplayEndpointsTests()
|
||||
{
|
||||
_secrets = new TestSurfaceSecretsScope();
|
||||
_factory = new ScannerApplicationFactory(cfg =>
|
||||
_factory = new ScannerApplicationFactory().WithOverrides(cfg =>
|
||||
{
|
||||
cfg["scanner:authority:enabled"] = "false";
|
||||
cfg["scanner:scoreReplay:enabled"] = "true";
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace StellaOps.Scanner.WebService.Tests;
|
||||
/// <summary>
|
||||
/// Integration tests for the Unknowns API endpoints.
|
||||
/// </summary>
|
||||
public sealed class UnknownsEndpointsTests : IClassFixture<ScannerApplicationFactory>
|
||||
public sealed class UnknownsEndpointsTests : IClassFixture<ScannerApplicationFixture>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
@@ -22,9 +22,9 @@ public sealed class UnknownsEndpointsTests : IClassFixture<ScannerApplicationFac
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public UnknownsEndpointsTests(ScannerApplicationFactory factory)
|
||||
public UnknownsEndpointsTests(ScannerApplicationFixture fixture)
|
||||
{
|
||||
_client = factory.CreateClient();
|
||||
_client = fixture.Factory.CreateClient();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user