notify doctors work, audit work, new product advisory sprints
This commit is contained in:
@@ -10,7 +10,9 @@ public sealed class DenoRuntimeTraceRecorderTests
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var recorder = new DenoRuntimeTraceRecorder(root);
|
||||
var recorder = new DenoRuntimeTraceRecorder(
|
||||
root,
|
||||
new FixedTimeProvider(DateTimeOffset.Parse("2025-11-17T12:00:00Z")));
|
||||
|
||||
recorder.AddPermissionUse(
|
||||
absoluteModulePath: Path.Combine(root, "c.ts"),
|
||||
@@ -58,4 +60,16 @@ public sealed class DenoRuntimeTraceRecorderTests
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime)
|
||||
{
|
||||
_fixedTime = fixedTime;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +61,65 @@ public sealed class DenoRuntimeTraceRunnerTests
|
||||
await File.WriteAllTextAsync(entrypoint, "console.log('hi')", TestContext.Current.CancellationToken);
|
||||
|
||||
using var entryEnv = new EnvironmentVariableScope("STELLA_DENO_ENTRYPOINT", entrypoint);
|
||||
using var binaryEnv = new EnvironmentVariableScope("STELLA_DENO_BINARY", Guid.NewGuid().ToString("N"));
|
||||
using var binaryEnv = new EnvironmentVariableScope("STELLA_DENO_BINARY", Path.Combine("tools", "deno"));
|
||||
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var result = await DenoRuntimeTraceRunner.TryExecuteAsync(context, logger: null, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.True(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName)));
|
||||
Assert.False(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsFalse_WhenEntrypointOutsideRoot()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
var externalRoot = TestPaths.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var externalEntrypoint = Path.Combine(externalRoot, "main.ts");
|
||||
await File.WriteAllTextAsync(externalEntrypoint, "console.log('hi')", TestContext.Current.CancellationToken);
|
||||
|
||||
using var entryEnv = new EnvironmentVariableScope("STELLA_DENO_ENTRYPOINT", externalEntrypoint);
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
|
||||
var result = await DenoRuntimeTraceRunner.TryExecuteAsync(context, logger: null, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.False(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName)));
|
||||
Assert.False(File.Exists(Path.Combine(root, "deno-runtime.ndjson")));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(externalRoot);
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsFalse_WhenDenoBinaryDisallowed()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var entrypoint = Path.Combine(root, "main.ts");
|
||||
await File.WriteAllTextAsync(entrypoint, "console.log('hi')", TestContext.Current.CancellationToken);
|
||||
|
||||
using var entryEnv = new EnvironmentVariableScope("STELLA_DENO_ENTRYPOINT", entrypoint);
|
||||
using var binaryEnv = new EnvironmentVariableScope("STELLA_DENO_BINARY", "not-deno");
|
||||
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var result = await DenoRuntimeTraceRunner.TryExecuteAsync(context, logger: null, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.False(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -137,14 +189,7 @@ public sealed class DenoRuntimeTraceRunnerTests
|
||||
EOF
|
||||
""";
|
||||
File.WriteAllText(path, script);
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start("chmod", $"+x {path}")?.WaitForExit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best effort; on Windows this branch won't execute
|
||||
}
|
||||
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
|
||||
}
|
||||
|
||||
return path;
|
||||
|
||||
@@ -92,11 +92,11 @@ public sealed class DenoWorkspaceNormalizerTests
|
||||
if (vendorCacheEdges.Length == 0)
|
||||
{
|
||||
var sample = string.Join(
|
||||
Environment.NewLine,
|
||||
"\n",
|
||||
graph.Edges
|
||||
.Select(edge => $"{edge.ImportKind}:{edge.Specifier}:{edge.Provenance}")
|
||||
.Take(10));
|
||||
Assert.Fail($"Expected vendor cache edges but none were found. Sample edges:{Environment.NewLine}{sample}");
|
||||
Assert.Fail($"Expected vendor cache edges but none were found. Sample edges:\n{sample}");
|
||||
}
|
||||
|
||||
var vendorEdge = vendorCacheEdges.FirstOrDefault(
|
||||
@@ -104,9 +104,9 @@ public sealed class DenoWorkspaceNormalizerTests
|
||||
if (vendorEdge is null)
|
||||
{
|
||||
var details = string.Join(
|
||||
Environment.NewLine,
|
||||
"\n",
|
||||
vendorCacheEdges.Select(edge => $"{edge.Specifier} [{edge.Provenance}] -> {edge.Resolution}"));
|
||||
Assert.Fail($"Unable to locate vendor cache edge for std server.ts. Observed edges:{Environment.NewLine}{details}");
|
||||
Assert.Fail($"Unable to locate vendor cache edge for std server.ts. Observed edges:\n{details}");
|
||||
}
|
||||
|
||||
var npmBridgeEdges = graph.Edges
|
||||
@@ -115,11 +115,11 @@ public sealed class DenoWorkspaceNormalizerTests
|
||||
if (npmBridgeEdges.Length == 0)
|
||||
{
|
||||
var bridgeSample = string.Join(
|
||||
Environment.NewLine,
|
||||
"\n",
|
||||
graph.Edges
|
||||
.Select(edge => $"{edge.ImportKind}:{edge.Specifier}:{edge.Resolution}")
|
||||
.Take(10));
|
||||
Assert.Fail($"No npm bridge edges discovered. Sample:{Environment.NewLine}{bridgeSample}");
|
||||
Assert.Fail($"No npm bridge edges discovered. Sample:\n{bridgeSample}");
|
||||
}
|
||||
|
||||
Assert.Contains(
|
||||
|
||||
@@ -3,6 +3,7 @@ using Xunit;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Unicode;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
|
||||
@@ -270,6 +271,6 @@ public sealed class DenoAnalyzerGoldenTests
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptionsProvider = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestUtilities;
|
||||
|
||||
internal static class TestPaths
|
||||
{
|
||||
private static long _tempCounter;
|
||||
|
||||
public static string ResolveFixture(params string[] segments)
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
@@ -17,7 +22,8 @@ internal static class TestPaths
|
||||
|
||||
public static string CreateTemporaryDirectory()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "tmp", Guid.NewGuid().ToString("N"));
|
||||
var suffix = Interlocked.Increment(ref _tempCounter).ToString("D4", CultureInfo.InvariantCulture);
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "tmp", suffix);
|
||||
Directory.CreateDirectory(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Bundling;
|
||||
|
||||
public sealed class BundlingSignalTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToMetadata_UsesInvariantCultureForNumericValues()
|
||||
{
|
||||
var originalCulture = CultureInfo.CurrentCulture;
|
||||
var originalUiCulture = CultureInfo.CurrentUICulture;
|
||||
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = new CultureInfo("ar-SA");
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("ar-SA");
|
||||
|
||||
var signal = new BundlingSignal(
|
||||
FilePath: "app.exe",
|
||||
Kind: BundlingKind.ILMerge,
|
||||
IsSkipped: false,
|
||||
SkipReason: null,
|
||||
Indicators: ImmutableArray<string>.Empty,
|
||||
SizeBytes: 123456,
|
||||
EstimatedBundledAssemblies: 42);
|
||||
|
||||
var metadata = signal.ToMetadata()
|
||||
.ToDictionary(item => item.Key, item => item.Value, StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal("123456", metadata["bundle.sizeBytes"]);
|
||||
Assert.Equal("42", metadata["bundle.estimatedAssemblies"]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
CultureInfo.CurrentUICulture = originalUiCulture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Callgraph;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Callgraph;
|
||||
|
||||
public sealed class DotNetCallgraphBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_UsesProvidedTimeProvider()
|
||||
{
|
||||
var fixedTime = new DateTimeOffset(2024, 1, 2, 3, 4, 5, TimeSpan.Zero);
|
||||
var builder = new DotNetCallgraphBuilder("context-digest", new FixedTimeProvider(fixedTime));
|
||||
|
||||
var graph = builder.Build();
|
||||
|
||||
Assert.Equal(fixedTime, graph.Metadata.GeneratedAt);
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _utcNow;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset utcNow)
|
||||
{
|
||||
_utcNow = utcNow;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _utcNow;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<!-- Disable Concelier test infrastructure - not needed for scanner tests -->
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Bundling;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities;
|
||||
@@ -8,6 +10,7 @@ namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities;
|
||||
/// </summary>
|
||||
internal static class DotNetFixtureBuilder
|
||||
{
|
||||
private static int _tempCounter;
|
||||
/// <summary>
|
||||
/// Creates a minimal SDK-style project file.
|
||||
/// </summary>
|
||||
@@ -373,7 +376,9 @@ internal static class DotNetFixtureBuilder
|
||||
/// </summary>
|
||||
public static string CreateTemporaryDirectory()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), "stellaops-tests", Guid.NewGuid().ToString("N"));
|
||||
var counter = Interlocked.Increment(ref _tempCounter)
|
||||
.ToString(CultureInfo.InvariantCulture);
|
||||
var path = Path.Combine(Path.GetTempPath(), "stellaops-tests", $"run-{counter}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ using FluentAssertions;
|
||||
using StellaOps.Scanner.Analyzers.Native.RuntimeCapture.Timeline;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests.RuntimeCapture.Timeline;
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Library.Tests.RuntimeCapture.Timeline;
|
||||
|
||||
public class TimelineBuilderTests
|
||||
{
|
||||
private readonly TimelineBuilder _builder = new();
|
||||
private static readonly DateTimeOffset BaseTime = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void Build_WithNoObservations_ReturnsUnknownPosture()
|
||||
@@ -78,15 +79,15 @@ public class TimelineBuilderTests
|
||||
{
|
||||
return new RuntimeEvidence
|
||||
{
|
||||
FirstObservation = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
LastObservation = DateTimeOffset.UtcNow,
|
||||
FirstObservation = BaseTime.AddHours(-1),
|
||||
LastObservation = BaseTime,
|
||||
Observations = Array.Empty<RuntimeObservation>(),
|
||||
Sessions = new[]
|
||||
{
|
||||
new RuntimeSession
|
||||
{
|
||||
StartTime = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
EndTime = DateTimeOffset.UtcNow,
|
||||
StartTime = BaseTime.AddHours(-1),
|
||||
EndTime = BaseTime,
|
||||
Platform = "linux-x64"
|
||||
}
|
||||
},
|
||||
@@ -96,7 +97,7 @@ public class TimelineBuilderTests
|
||||
|
||||
private static RuntimeEvidence CreateEvidenceWithoutComponent()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = BaseTime;
|
||||
return new RuntimeEvidence
|
||||
{
|
||||
FirstObservation = now.AddHours(-1),
|
||||
@@ -127,7 +128,7 @@ public class TimelineBuilderTests
|
||||
|
||||
private static RuntimeEvidence CreateEvidenceWithNetworkExposure()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = BaseTime;
|
||||
return new RuntimeEvidence
|
||||
{
|
||||
FirstObservation = now.AddHours(-1),
|
||||
@@ -166,7 +167,7 @@ public class TimelineBuilderTests
|
||||
|
||||
private static RuntimeEvidence CreateEvidenceOver24Hours()
|
||||
{
|
||||
var start = DateTimeOffset.UtcNow.AddHours(-24);
|
||||
var start = BaseTime.AddHours(-24);
|
||||
var observations = new List<RuntimeObservation>();
|
||||
|
||||
for (int i = 0; i < 24; i++)
|
||||
@@ -200,7 +201,7 @@ public class TimelineBuilderTests
|
||||
|
||||
private static RuntimeEvidence CreateEvidenceWithComponentLoad()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = BaseTime;
|
||||
return new RuntimeEvidence
|
||||
{
|
||||
FirstObservation = now.AddHours(-1),
|
||||
@@ -231,7 +232,7 @@ public class TimelineBuilderTests
|
||||
|
||||
private static RuntimeEvidence CreateEvidenceWith10Observations()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = BaseTime;
|
||||
var observations = new List<RuntimeObservation>();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Analyzers.Native\StellaOps.Scanner.Analyzers.Native.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
internal sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime)
|
||||
{
|
||||
_fixedTime = fixedTime;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
using System.Buffers.Binary;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Analyzers.Native.Hardening;
|
||||
using StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening;
|
||||
@@ -18,7 +19,10 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening;
|
||||
/// </summary>
|
||||
public class ElfHardeningExtractorTests
|
||||
{
|
||||
private readonly ElfHardeningExtractor _extractor = new();
|
||||
private static readonly TimeProvider FixedTimeProvider =
|
||||
new FixedTimeProvider(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
|
||||
private readonly ElfHardeningExtractor _extractor = new(FixedTimeProvider);
|
||||
|
||||
#region Magic Detection Tests
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening;
|
||||
/// </summary>
|
||||
public class HardeningScoreCalculatorTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTime = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
#region Score Range Tests
|
||||
|
||||
[Fact]
|
||||
@@ -38,7 +40,7 @@ public class HardeningScoreCalculatorTests
|
||||
Flags: flags,
|
||||
HardeningScore: CalculateScore(flags, BinaryFormat.Elf),
|
||||
MissingFlags: [],
|
||||
ExtractedAt: DateTimeOffset.UtcNow);
|
||||
ExtractedAt: FixedTime);
|
||||
|
||||
// Assert
|
||||
result.HardeningScore.Should().BeGreaterThanOrEqualTo(0.8);
|
||||
@@ -63,7 +65,7 @@ public class HardeningScoreCalculatorTests
|
||||
Flags: flags,
|
||||
HardeningScore: CalculateScore(flags, BinaryFormat.Elf),
|
||||
MissingFlags: ["PIE", "RELRO", "NX", "STACK_CANARY", "FORTIFY"],
|
||||
ExtractedAt: DateTimeOffset.UtcNow);
|
||||
ExtractedAt: FixedTime);
|
||||
|
||||
// Assert
|
||||
result.HardeningScore.Should().Be(0);
|
||||
@@ -82,7 +84,7 @@ public class HardeningScoreCalculatorTests
|
||||
Flags: flags,
|
||||
HardeningScore: CalculateScore(flags, BinaryFormat.Elf),
|
||||
MissingFlags: [],
|
||||
ExtractedAt: DateTimeOffset.UtcNow);
|
||||
ExtractedAt: FixedTime);
|
||||
|
||||
// Assert
|
||||
result.HardeningScore.Should().Be(0);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
using System.Buffers.Binary;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Analyzers.Native.Hardening;
|
||||
using StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening;
|
||||
@@ -18,7 +19,10 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening;
|
||||
/// </summary>
|
||||
public class PeHardeningExtractorTests
|
||||
{
|
||||
private readonly PeHardeningExtractor _extractor = new();
|
||||
private static readonly TimeProvider FixedTimeProvider =
|
||||
new FixedTimeProvider(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
|
||||
private readonly PeHardeningExtractor _extractor = new(FixedTimeProvider);
|
||||
|
||||
#region Magic Detection Tests
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
using StellaOps.Scanner.Analyzers.Native.Index;
|
||||
using StellaOps.Scanner.ProofSpine;
|
||||
using StellaOps.Scanner.ProofSpine.Options;
|
||||
@@ -10,33 +11,21 @@ using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Index.Tests;
|
||||
|
||||
public sealed class OfflineBuildIdIndexSignatureTests : IDisposable
|
||||
public sealed class OfflineBuildIdIndexSignatureTests
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public OfflineBuildIdIndexSignatureTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"buildid-sig-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
private static readonly TimeProvider FixedTimeProvider =
|
||||
new FixedTimeProvider(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_RequiresTrustedDsseSignature_WhenEnabled()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31","distro":"debian","confidence":"exact","indexed_at":"2025-01-15T10:00:00Z"}
|
||||
""");
|
||||
|
||||
var signaturePath = Path.Combine(_tempDir, "index.ndjson.dsse.json");
|
||||
var signaturePath = Path.Combine(temp.Path, "index.ndjson.dsse.json");
|
||||
await File.WriteAllTextAsync(signaturePath, CreateDsseSignature(indexPath, expectedSha256: ComputeSha256Hex(indexPath)));
|
||||
|
||||
var dsseService = CreateTrustedDsseService(keyId: "buildid-index-test-key", secretBase64: Convert.ToBase64String("supersecret-supersecret-supersecret"u8.ToArray()));
|
||||
@@ -48,7 +37,7 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable
|
||||
RequireSignature = true,
|
||||
});
|
||||
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, dsseService);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider, dsseService);
|
||||
await index.LoadAsync();
|
||||
|
||||
Assert.True(index.IsLoaded);
|
||||
@@ -62,12 +51,13 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LoadAsync_RefusesToLoadIndex_WhenDigestDoesNotMatchSignaturePayload()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
|
||||
""");
|
||||
|
||||
var signaturePath = Path.Combine(_tempDir, "index.ndjson.dsse.json");
|
||||
var signaturePath = Path.Combine(temp.Path, "index.ndjson.dsse.json");
|
||||
await File.WriteAllTextAsync(signaturePath, CreateDsseSignature(indexPath, expectedSha256: "deadbeef"));
|
||||
|
||||
var dsseService = CreateTrustedDsseService(keyId: "buildid-index-test-key", secretBase64: Convert.ToBase64String("supersecret-supersecret-supersecret"u8.ToArray()));
|
||||
@@ -79,7 +69,7 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable
|
||||
RequireSignature = true,
|
||||
});
|
||||
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, dsseService);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider, dsseService);
|
||||
await index.LoadAsync();
|
||||
|
||||
Assert.True(index.IsLoaded);
|
||||
@@ -89,12 +79,13 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LoadAsync_RefusesToLoadIndex_WhenSignatureFileMissing()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
|
||||
""");
|
||||
|
||||
var signaturePath = Path.Combine(_tempDir, "missing.dsse.json");
|
||||
var signaturePath = Path.Combine(temp.Path, "missing.dsse.json");
|
||||
var dsseService = CreateTrustedDsseService(keyId: "buildid-index-test-key", secretBase64: Convert.ToBase64String("supersecret-supersecret-supersecret"u8.ToArray()));
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions
|
||||
@@ -104,7 +95,7 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable
|
||||
RequireSignature = true,
|
||||
});
|
||||
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, dsseService);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider, dsseService);
|
||||
await index.LoadAsync();
|
||||
|
||||
Assert.True(index.IsLoaded);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Index.Tests;
|
||||
@@ -8,23 +9,10 @@ namespace StellaOps.Scanner.Analyzers.Native.Index.Tests;
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="OfflineBuildIdIndex"/>.
|
||||
/// </summary>
|
||||
public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
public sealed class OfflineBuildIdIndexTests
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public OfflineBuildIdIndexTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"buildid-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
private static readonly TimeProvider FixedTimeProvider =
|
||||
new FixedTimeProvider(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
|
||||
#region Loading Tests
|
||||
|
||||
@@ -32,7 +20,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
public async Task LoadAsync_EmptyIndex_WhenNoPathConfigured()
|
||||
{
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = null });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
|
||||
await index.LoadAsync();
|
||||
|
||||
@@ -44,7 +32,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
public async Task LoadAsync_EmptyIndex_WhenFileNotFound()
|
||||
{
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = "/nonexistent/file.ndjson" });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
|
||||
await index.LoadAsync();
|
||||
|
||||
@@ -55,7 +43,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LoadAsync_ParsesNdjsonEntries()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31","distro":"debian","confidence":"exact","indexed_at":"2025-01-15T10:00:00Z"}
|
||||
{"build_id":"pe-cv:12345678-1234-1234-1234-123456789012-1","purl":"pkg:nuget/System.Text.Json@8.0.0","confidence":"inferred"}
|
||||
@@ -63,7 +52,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
|
||||
await index.LoadAsync();
|
||||
|
||||
@@ -74,7 +63,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LoadAsync_SkipsEmptyLines()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index-empty-lines.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index-empty-lines.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
|
||||
|
||||
@@ -83,7 +73,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
|
||||
await index.LoadAsync();
|
||||
|
||||
@@ -93,7 +83,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LoadAsync_SkipsCommentLines()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index-comments.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index-comments.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
# This is a comment
|
||||
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
|
||||
@@ -102,7 +93,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
|
||||
await index.LoadAsync();
|
||||
|
||||
@@ -112,7 +103,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LoadAsync_SkipsInvalidJsonLines()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index-invalid.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index-invalid.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
|
||||
not valid json at all
|
||||
@@ -120,7 +112,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
|
||||
await index.LoadAsync();
|
||||
|
||||
@@ -134,13 +126,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LookupAsync_ReturnsNull_WhenNotFound()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
await index.LoadAsync();
|
||||
|
||||
var result = await index.LookupAsync("gnu-build-id:notfound");
|
||||
@@ -151,13 +144,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LookupAsync_ReturnsNull_ForNullOrEmpty()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
await index.LoadAsync();
|
||||
|
||||
Assert.Null(await index.LookupAsync(null!));
|
||||
@@ -168,13 +162,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LookupAsync_FindsExactMatch()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:abc123def456","purl":"pkg:deb/debian/libc6@2.31","version":"2.31","distro":"debian","confidence":"exact","indexed_at":"2025-01-15T10:00:00Z"}
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
await index.LoadAsync();
|
||||
|
||||
var result = await index.LookupAsync("gnu-build-id:abc123def456");
|
||||
@@ -190,13 +185,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task LookupAsync_CaseInsensitive()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:ABC123DEF456","purl":"pkg:deb/debian/libc6@2.31"}
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
await index.LoadAsync();
|
||||
|
||||
// Query with lowercase
|
||||
@@ -213,7 +209,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task BatchLookupAsync_ReturnsFoundEntries()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:aaa","purl":"pkg:deb/debian/liba@1.0"}
|
||||
{"build_id":"gnu-build-id:bbb","purl":"pkg:deb/debian/libb@1.0"}
|
||||
@@ -221,7 +218,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
await index.LoadAsync();
|
||||
|
||||
var results = await index.BatchLookupAsync(["gnu-build-id:aaa", "gnu-build-id:notfound", "gnu-build-id:ccc"]);
|
||||
@@ -234,13 +231,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[Fact]
|
||||
public async Task BatchLookupAsync_SkipsNullAndEmpty()
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
await File.WriteAllTextAsync(indexPath, """
|
||||
{"build_id":"gnu-build-id:aaa","purl":"pkg:deb/debian/liba@1.0"}
|
||||
""");
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
await index.LoadAsync();
|
||||
|
||||
var results = await index.BatchLookupAsync([null!, "", " ", "gnu-build-id:aaa"]);
|
||||
@@ -263,12 +261,13 @@ public sealed class OfflineBuildIdIndexTests : IDisposable
|
||||
[InlineData("", BuildIdConfidence.Heuristic)]
|
||||
public async Task LoadAsync_ParsesConfidenceLevels(string confidenceValue, BuildIdConfidence expected)
|
||||
{
|
||||
var indexPath = Path.Combine(_tempDir, "index.ndjson");
|
||||
using var temp = TempDirectory.Create();
|
||||
var indexPath = Path.Combine(temp.Path, "index.ndjson");
|
||||
var entry = new { build_id = "gnu-build-id:test", purl = "pkg:test/test@1.0", confidence = confidenceValue };
|
||||
await File.WriteAllTextAsync(indexPath, JsonSerializer.Serialize(entry));
|
||||
|
||||
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
|
||||
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance, FixedTimeProvider);
|
||||
await index.LoadAsync();
|
||||
|
||||
var result = await index.LookupAsync("gnu-build-id:test");
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Index.Tests;
|
||||
|
||||
internal sealed class TempDirectory : IDisposable
|
||||
{
|
||||
public string Path { get; }
|
||||
|
||||
private TempDirectory(string path)
|
||||
{
|
||||
Path = path;
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
public static TempDirectory Create([CallerMemberName] string? testName = null)
|
||||
{
|
||||
var safeName = Sanitize(testName ?? "unknown");
|
||||
var path = System.IO.Path.Combine(
|
||||
System.IO.Path.GetTempPath(),
|
||||
"stellaops-tests",
|
||||
"buildid-index",
|
||||
safeName);
|
||||
return new TempDirectory(path);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Sanitize(string value)
|
||||
{
|
||||
var buffer = new char[value.Length];
|
||||
for (var i = 0; i < value.Length; i++)
|
||||
{
|
||||
var ch = value[i];
|
||||
buffer[i] = char.IsLetterOrDigit(ch) ? ch : '_';
|
||||
}
|
||||
return new string(buffer);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests.Reachability;
|
||||
/// </summary>
|
||||
public class RichgraphV1AlignmentTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTime = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// §8.2: SymbolID Construction uses sym: prefix with 16 hex chars.
|
||||
/// </summary>
|
||||
@@ -400,7 +402,7 @@ public class RichgraphV1AlignmentTests
|
||||
{
|
||||
// Arrange & Act
|
||||
var metadata = new TestGraphMetadata(
|
||||
GeneratedAt: DateTimeOffset.UtcNow,
|
||||
GeneratedAt: FixedTime,
|
||||
GeneratorVersion: "1.0.0",
|
||||
LayerDigest: "sha256:layer123",
|
||||
BinaryCount: 5,
|
||||
@@ -410,7 +412,7 @@ public class RichgraphV1AlignmentTests
|
||||
SyntheticRootCount: 8);
|
||||
|
||||
// Assert
|
||||
metadata.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromMinutes(1));
|
||||
metadata.GeneratedAt.Should().Be(FixedTime);
|
||||
metadata.GeneratorVersion.Should().Be("1.0.0");
|
||||
metadata.LayerDigest.Should().StartWith("sha256:");
|
||||
metadata.BinaryCount.Should().BeGreaterThan(0);
|
||||
|
||||
@@ -1,10 +1,38 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
internal static class RuntimeCaptureTestClock
|
||||
{
|
||||
internal static readonly DateTime BaseTime = new(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
internal static readonly DateTimeOffset BaseTimeOffset = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
internal static class RuntimeCaptureTestFactory
|
||||
{
|
||||
internal static TimeProvider CreateTimeProvider() => new FixedTimeProvider(RuntimeCaptureTestClock.BaseTimeOffset);
|
||||
|
||||
internal static IGuidProvider CreateGuidProvider() => new SequentialGuidProvider();
|
||||
|
||||
internal static IRuntimeCaptureAdapter? CreatePlatformAdapter(TimeProvider timeProvider, IGuidProvider guidProvider)
|
||||
{
|
||||
if (OperatingSystem.IsLinux())
|
||||
return new LinuxEbpfCaptureAdapter(timeProvider, guidProvider);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
return new WindowsEtwCaptureAdapter(timeProvider, guidProvider);
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
return new MacOsDyldCaptureAdapter(timeProvider, guidProvider);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class RuntimeCaptureOptionsTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -261,7 +289,7 @@ public class RuntimeEvidenceAggregatorTests
|
||||
var events = new[]
|
||||
{
|
||||
new RuntimeLoadEvent(
|
||||
DateTime.UtcNow.AddMinutes(-5),
|
||||
RuntimeCaptureTestClock.BaseTime.AddMinutes(-5),
|
||||
ProcessId: 1234,
|
||||
ThreadId: 1,
|
||||
LoadType: RuntimeLoadType.Dlopen,
|
||||
@@ -273,7 +301,7 @@ public class RuntimeEvidenceAggregatorTests
|
||||
CallerModule: "myapp",
|
||||
CallerAddress: 0x400000),
|
||||
new RuntimeLoadEvent(
|
||||
DateTime.UtcNow.AddMinutes(-4),
|
||||
RuntimeCaptureTestClock.BaseTime.AddMinutes(-4),
|
||||
ProcessId: 1234,
|
||||
ThreadId: 1,
|
||||
LoadType: RuntimeLoadType.Dlopen,
|
||||
@@ -288,8 +316,8 @@ public class RuntimeEvidenceAggregatorTests
|
||||
|
||||
var session = new RuntimeCaptureSession(
|
||||
SessionId: "test-session",
|
||||
StartTime: DateTime.UtcNow.AddMinutes(-10),
|
||||
EndTime: DateTime.UtcNow,
|
||||
StartTime: RuntimeCaptureTestClock.BaseTime.AddMinutes(-10),
|
||||
EndTime: RuntimeCaptureTestClock.BaseTime,
|
||||
Platform: "linux",
|
||||
CaptureMethod: "ebpf",
|
||||
TargetProcessId: 1234,
|
||||
@@ -315,7 +343,7 @@ public class RuntimeEvidenceAggregatorTests
|
||||
public void Aggregate_DuplicateLoads_AggregatesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var baseTime = DateTime.UtcNow.AddMinutes(-10);
|
||||
var baseTime = RuntimeCaptureTestClock.BaseTime.AddMinutes(-10);
|
||||
var events = new[]
|
||||
{
|
||||
new RuntimeLoadEvent(baseTime, 1, 1, RuntimeLoadType.Dlopen, "libc.so.6", "/lib/libc.so.6", null, true, null, null, null),
|
||||
@@ -323,7 +351,7 @@ public class RuntimeEvidenceAggregatorTests
|
||||
new RuntimeLoadEvent(baseTime.AddMinutes(2), 1, 1, RuntimeLoadType.Dlopen, "libc.so.6", "/lib/libc.so.6", null, true, null, null, null),
|
||||
};
|
||||
|
||||
var session = new RuntimeCaptureSession("test", baseTime, DateTime.UtcNow, "linux", "ebpf", 1, events, 0, 0);
|
||||
var session = new RuntimeCaptureSession("test", baseTime, RuntimeCaptureTestClock.BaseTime, "linux", "ebpf", 1, events, 0, 0);
|
||||
|
||||
// Act
|
||||
var evidence = RuntimeEvidenceAggregator.Aggregate([session]);
|
||||
@@ -341,10 +369,19 @@ public class RuntimeEvidenceAggregatorTests
|
||||
// Arrange
|
||||
var events = new[]
|
||||
{
|
||||
new RuntimeLoadEvent(DateTime.UtcNow, 1, 1, RuntimeLoadType.Dlopen, "missing.so", null, null, false, -1, null, null),
|
||||
new RuntimeLoadEvent(RuntimeCaptureTestClock.BaseTime, 1, 1, RuntimeLoadType.Dlopen, "missing.so", null, null, false, -1, null, null),
|
||||
};
|
||||
|
||||
var session = new RuntimeCaptureSession("test", DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow, "linux", "ebpf", 1, events, 0, 0);
|
||||
var session = new RuntimeCaptureSession(
|
||||
"test",
|
||||
RuntimeCaptureTestClock.BaseTime.AddMinutes(-1),
|
||||
RuntimeCaptureTestClock.BaseTime,
|
||||
"linux",
|
||||
"ebpf",
|
||||
1,
|
||||
events,
|
||||
0,
|
||||
0);
|
||||
|
||||
// Act
|
||||
var evidence = RuntimeEvidenceAggregator.Aggregate([session]);
|
||||
@@ -359,8 +396,8 @@ public class RuntimeEvidenceAggregatorTests
|
||||
public void Aggregate_MultipleSessions_MergesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var time1 = DateTime.UtcNow.AddHours(-2);
|
||||
var time2 = DateTime.UtcNow.AddHours(-1);
|
||||
var time1 = RuntimeCaptureTestClock.BaseTime.AddHours(-2);
|
||||
var time2 = RuntimeCaptureTestClock.BaseTime.AddHours(-1);
|
||||
|
||||
var session1 = new RuntimeCaptureSession(
|
||||
"s1", time1, time1.AddMinutes(30), "linux", "ebpf", 1,
|
||||
@@ -387,8 +424,11 @@ public class RuntimeCaptureAdapterFactoryTests
|
||||
[Fact]
|
||||
public void CreateForCurrentPlatform_ReturnsAdapter()
|
||||
{
|
||||
var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider();
|
||||
var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider();
|
||||
|
||||
// Act
|
||||
var adapter = RuntimeCaptureAdapterFactory.CreateForCurrentPlatform();
|
||||
var adapter = RuntimeCaptureAdapterFactory.CreateForCurrentPlatform(timeProvider, guidProvider);
|
||||
|
||||
// Assert
|
||||
// Should return an adapter on Linux/Windows/macOS, null on other platforms
|
||||
@@ -406,8 +446,11 @@ public class RuntimeCaptureAdapterFactoryTests
|
||||
[Fact]
|
||||
public void GetAvailableAdapters_ReturnsAdaptersForCurrentPlatform()
|
||||
{
|
||||
var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider();
|
||||
var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider();
|
||||
|
||||
// Act
|
||||
var adapters = RuntimeCaptureAdapterFactory.GetAvailableAdapters();
|
||||
var adapters = RuntimeCaptureAdapterFactory.GetAvailableAdapters(timeProvider, guidProvider);
|
||||
|
||||
// Assert
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsWindows() || OperatingSystem.IsMacOS())
|
||||
@@ -431,8 +474,8 @@ public class SandboxCaptureTests
|
||||
// Arrange
|
||||
var mockEvents = new[]
|
||||
{
|
||||
new RuntimeLoadEvent(DateTime.UtcNow, 1, 1, RuntimeLoadType.Dlopen, "libtest.so", "/lib/libtest.so", null, true, null, null, null),
|
||||
new RuntimeLoadEvent(DateTime.UtcNow, 1, 1, RuntimeLoadType.Dlopen, "libother.so", "/lib/libother.so", null, true, null, null, null),
|
||||
new RuntimeLoadEvent(RuntimeCaptureTestClock.BaseTime, 1, 1, RuntimeLoadType.Dlopen, "libtest.so", "/lib/libtest.so", null, true, null, null, null),
|
||||
new RuntimeLoadEvent(RuntimeCaptureTestClock.BaseTime, 1, 1, RuntimeLoadType.Dlopen, "libother.so", "/lib/libother.so", null, true, null, null, null),
|
||||
};
|
||||
|
||||
var options = new RuntimeCaptureOptions
|
||||
@@ -446,13 +489,9 @@ public class SandboxCaptureTests
|
||||
}
|
||||
};
|
||||
|
||||
IRuntimeCaptureAdapter? adapter = null;
|
||||
if (OperatingSystem.IsLinux())
|
||||
adapter = new LinuxEbpfCaptureAdapter();
|
||||
else if (OperatingSystem.IsWindows())
|
||||
adapter = new WindowsEtwCaptureAdapter();
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
adapter = new MacOsDyldCaptureAdapter();
|
||||
var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider();
|
||||
var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider();
|
||||
var adapter = RuntimeCaptureTestFactory.CreatePlatformAdapter(timeProvider, guidProvider);
|
||||
|
||||
if (adapter == null)
|
||||
return; // Skip on unsupported platforms
|
||||
@@ -486,13 +525,9 @@ public class SandboxCaptureTests
|
||||
}
|
||||
};
|
||||
|
||||
IRuntimeCaptureAdapter? adapter = null;
|
||||
if (OperatingSystem.IsLinux())
|
||||
adapter = new LinuxEbpfCaptureAdapter();
|
||||
else if (OperatingSystem.IsWindows())
|
||||
adapter = new WindowsEtwCaptureAdapter();
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
adapter = new MacOsDyldCaptureAdapter();
|
||||
var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider();
|
||||
var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider();
|
||||
var adapter = RuntimeCaptureTestFactory.CreatePlatformAdapter(timeProvider, guidProvider);
|
||||
|
||||
if (adapter == null)
|
||||
return; // Skip on unsupported platforms
|
||||
@@ -527,13 +562,9 @@ public class SandboxCaptureTests
|
||||
}
|
||||
};
|
||||
|
||||
IRuntimeCaptureAdapter? adapter = null;
|
||||
if (OperatingSystem.IsLinux())
|
||||
adapter = new LinuxEbpfCaptureAdapter();
|
||||
else if (OperatingSystem.IsWindows())
|
||||
adapter = new WindowsEtwCaptureAdapter();
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
adapter = new MacOsDyldCaptureAdapter();
|
||||
var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider();
|
||||
var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider();
|
||||
var adapter = RuntimeCaptureTestFactory.CreatePlatformAdapter(timeProvider, guidProvider);
|
||||
|
||||
if (adapter == null)
|
||||
return; // Skip on unsupported platforms
|
||||
@@ -558,7 +589,7 @@ public class RuntimeEvidenceModelTests
|
||||
public void RuntimeLoadEvent_RecordEquality_Works()
|
||||
{
|
||||
// Arrange
|
||||
var time = DateTime.UtcNow;
|
||||
var time = RuntimeCaptureTestClock.BaseTime;
|
||||
var event1 = new RuntimeLoadEvent(time, 1, 1, RuntimeLoadType.Dlopen, "lib.so", "/lib.so", null, true, null, null, null);
|
||||
var event2 = new RuntimeLoadEvent(time, 1, 1, RuntimeLoadType.Dlopen, "lib.so", "/lib.so", null, true, null, null, null);
|
||||
var event3 = new RuntimeLoadEvent(time, 2, 1, RuntimeLoadType.Dlopen, "lib.so", "/lib.so", null, true, null, null, null);
|
||||
@@ -580,10 +611,54 @@ public class RuntimeEvidenceModelTests
|
||||
{
|
||||
// Verify each type can be used to create an event
|
||||
var evt = new RuntimeLoadEvent(
|
||||
DateTime.UtcNow, 1, 1, loadType,
|
||||
RuntimeCaptureTestClock.BaseTime, 1, 1, loadType,
|
||||
"test.so", "/test.so", null, true, null, null, null);
|
||||
|
||||
evt.LoadType.Should().Be(loadType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CaptureDurationTimerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RunAsync_CallsStopWithProvidedToken()
|
||||
{
|
||||
using var captureCts = new CancellationTokenSource();
|
||||
using var stopCts = new CancellationTokenSource();
|
||||
var tcs = new TaskCompletionSource<CancellationToken>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Task StopAsync(CancellationToken token)
|
||||
{
|
||||
tcs.TrySetResult(token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var timerTask = CaptureDurationTimer.RunAsync(TimeSpan.Zero, StopAsync, captureCts.Token, stopCts.Token);
|
||||
var observedToken = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(1));
|
||||
|
||||
observedToken.Should().Be(stopCts.Token);
|
||||
await timerTask;
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RunAsync_CanceledCapture_DoesNotInvokeStop()
|
||||
{
|
||||
using var captureCts = new CancellationTokenSource();
|
||||
using var stopCts = new CancellationTokenSource();
|
||||
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Task StopAsync(CancellationToken token)
|
||||
{
|
||||
tcs.TrySetResult();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
captureCts.Cancel();
|
||||
await CaptureDurationTimer.RunAsync(TimeSpan.FromMinutes(1), StopAsync, captureCts.Token, stopCts.Token);
|
||||
|
||||
tcs.Task.IsCompleted.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Exclude TimelineBuilderTests.cs as the Timeline namespace is in a different project -->
|
||||
<ItemGroup>
|
||||
<Compile Remove="RuntimeCapture\Timeline\TimelineBuilderTests.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
@@ -32,4 +27,4 @@
|
||||
<PropertyGroup>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -426,6 +426,20 @@ public class CallGraphDigestsTests
|
||||
Assert.Equal("native:SSL_read", stableId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeResultDigest_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateMinimalResult();
|
||||
|
||||
// Act
|
||||
var digest1 = CallGraphDigests.ComputeResultDigest(result);
|
||||
var digest2 = CallGraphDigests.ComputeResultDigest(result);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(digest1, digest2);
|
||||
}
|
||||
|
||||
private static CallGraphSnapshot CreateMinimalSnapshot()
|
||||
{
|
||||
return new CallGraphSnapshot(
|
||||
@@ -453,6 +467,24 @@ public class CallGraphDigestsTests
|
||||
);
|
||||
}
|
||||
|
||||
private static ReachabilityAnalysisResult CreateMinimalResult()
|
||||
{
|
||||
return new ReachabilityAnalysisResult(
|
||||
ScanId: "test-scan-001",
|
||||
GraphDigest: "sha256:graph",
|
||||
Language: "native",
|
||||
ComputedAt: DateTimeOffset.UtcNow,
|
||||
ReachableNodeIds: ImmutableArray.Create("entry", "node-a"),
|
||||
ReachableSinkIds: ImmutableArray.Create("sink-a"),
|
||||
Paths: ImmutableArray.Create(
|
||||
new ReachabilityPath(
|
||||
EntrypointId: "entry",
|
||||
SinkId: "sink-a",
|
||||
NodeIds: ImmutableArray.Create("entry", "node-a", "sink-a"))),
|
||||
ResultDigest: string.Empty
|
||||
);
|
||||
}
|
||||
|
||||
private static bool IsValidHex(string hex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hex))
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
|
||||
using System.Globalization;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Reachability.Stack;
|
||||
|
||||
@@ -10,7 +12,7 @@ namespace StellaOps.Scanner.Reachability.Stack.Tests;
|
||||
|
||||
public class ReachabilityStackEvaluatorTests
|
||||
{
|
||||
private readonly ReachabilityStackEvaluator _evaluator = new();
|
||||
private readonly ReachabilityStackEvaluator _evaluator = new(new SequentialGuidProvider());
|
||||
|
||||
private static VulnerableSymbol CreateTestSymbol() => new(
|
||||
Name: "EVP_DecryptUpdate",
|
||||
@@ -172,17 +174,19 @@ public class ReachabilityStackEvaluatorTests
|
||||
var layer1 = CreateLayer1(isReachable: true, ConfidenceLevel.High);
|
||||
var layer2 = CreateLayer2(isResolved: true, ConfidenceLevel.High);
|
||||
var layer3 = CreateLayer3(isGated: false, GatingOutcome.NotGated, ConfidenceLevel.High);
|
||||
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 12, 9, 0, 0, TimeSpan.Zero));
|
||||
var expectedId = new SequentialGuidProvider().NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
var stack = _evaluator.Evaluate("finding-123", symbol, layer1, layer2, layer3);
|
||||
var stack = _evaluator.Evaluate("finding-123", symbol, layer1, layer2, layer3, timeProvider);
|
||||
|
||||
stack.Id.Should().NotBeNullOrEmpty();
|
||||
stack.Id.Should().Be(expectedId);
|
||||
stack.FindingId.Should().Be("finding-123");
|
||||
stack.Symbol.Should().Be(symbol);
|
||||
stack.StaticCallGraph.Should().Be(layer1);
|
||||
stack.BinaryResolution.Should().Be(layer2);
|
||||
stack.RuntimeGating.Should().Be(layer3);
|
||||
stack.Verdict.Should().Be(ReachabilityVerdict.Exploitable);
|
||||
stack.AnalyzedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
stack.AnalyzedAt.Should().Be(timeProvider.GetUtcNow());
|
||||
stack.Explanation.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
@@ -422,4 +426,16 @@ public class ReachabilityStackEvaluatorTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _fixedTime;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset fixedTime)
|
||||
{
|
||||
_fixedTime = fixedTime;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Canonical.Json/StellaOps.Canonical.Json.csproj" />
|
||||
<ProjectReference Include="../../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Decompiler/StellaOps.BinaryIndex.Decompiler.csproj" />
|
||||
<ProjectReference Include="../../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Ghidra/StellaOps.BinaryIndex.Ghidra.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Security;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Scanner.Reachability.Witnesses;
|
||||
using Xunit;
|
||||
|
||||
@@ -137,6 +141,49 @@ public class WitnessDsseSignerTests
|
||||
Assert.Equal(result1.PayloadBytes, result2.PayloadBytes);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SignWitness_UsesCanonicalPayloadAndDssePae()
|
||||
{
|
||||
// Arrange
|
||||
var witness = CreateTestWitness();
|
||||
var (privateKey, publicKey) = CreateTestKeyPair();
|
||||
var signingKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey);
|
||||
var signer = new WitnessDsseSigner(new EnvelopeSignatureService());
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.Default
|
||||
};
|
||||
|
||||
// Act
|
||||
var signResult = signer.SignWitness(witness, signingKey, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.True(signResult.IsSuccess, signResult.Error);
|
||||
Assert.NotNull(signResult.Envelope);
|
||||
Assert.NotNull(signResult.PayloadBytes);
|
||||
|
||||
var payloadBytes = signResult.PayloadBytes!;
|
||||
var expectedPayload = CanonJson.Canonicalize(witness, options);
|
||||
Assert.Equal(expectedPayload, payloadBytes);
|
||||
|
||||
var verifyKey = EnvelopeKey.CreateEd25519Verifier(publicKey);
|
||||
var signatureBytes = Convert.FromBase64String(signResult.Envelope!.Signatures[0].Signature);
|
||||
var envelopeSignature = new EnvelopeSignature(signingKey.KeyId, signingKey.AlgorithmId, signatureBytes);
|
||||
var verifyResult = new EnvelopeSignatureService().VerifyDsse(
|
||||
WitnessSchema.DssePayloadType,
|
||||
payloadBytes,
|
||||
envelopeSignature,
|
||||
verifyKey,
|
||||
TestCancellationToken);
|
||||
|
||||
Assert.True(verifyResult.IsSuccess);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyWitness_WithInvalidPayloadType_ReturnsFails()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Security;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Scanner.Reachability.Witnesses;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
@@ -286,6 +290,49 @@ public sealed class SuppressionDsseSignerTests
|
||||
verifyResult.Witness.Evidence.Unreachability?.UnreachableSymbol);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SignWitness_UsesCanonicalPayloadAndDssePae()
|
||||
{
|
||||
// Arrange
|
||||
var witness = CreateTestWitness();
|
||||
var (privateKey, publicKey) = CreateTestKeyPair();
|
||||
var signingKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey);
|
||||
var signer = new SuppressionDsseSigner(new EnvelopeSignatureService());
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.Default
|
||||
};
|
||||
|
||||
// Act
|
||||
var signResult = signer.SignWitness(witness, signingKey, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.True(signResult.IsSuccess, signResult.Error);
|
||||
Assert.NotNull(signResult.Envelope);
|
||||
Assert.NotNull(signResult.PayloadBytes);
|
||||
|
||||
var payloadBytes = signResult.PayloadBytes!;
|
||||
var expectedPayload = CanonJson.Canonicalize(witness, options);
|
||||
Assert.Equal(expectedPayload, payloadBytes);
|
||||
|
||||
var verifyKey = EnvelopeKey.CreateEd25519Verifier(publicKey);
|
||||
var signatureBytes = Convert.FromBase64String(signResult.Envelope!.Signatures[0].Signature);
|
||||
var envelopeSignature = new EnvelopeSignature(signingKey.KeyId, signingKey.AlgorithmId, signatureBytes);
|
||||
var verifyResult = new EnvelopeSignatureService().VerifyDsse(
|
||||
SuppressionWitnessSchema.DssePayloadType,
|
||||
payloadBytes,
|
||||
envelopeSignature,
|
||||
verifyKey,
|
||||
TestCancellationToken);
|
||||
|
||||
Assert.True(verifyResult.IsSuccess);
|
||||
}
|
||||
|
||||
private sealed class FixedRandomGenerator : Org.BouncyCastle.Crypto.Prng.IRandomGenerator
|
||||
{
|
||||
private byte _value;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Attestation;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
@@ -17,7 +18,9 @@ public sealed class AttestorClientTests
|
||||
public async Task SendPlaceholderAsync_PostsJsonPayload()
|
||||
{
|
||||
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Accepted));
|
||||
using var httpClient = new HttpClient(handler);
|
||||
using var provider = BuildHttpClientProvider(handler);
|
||||
var factory = provider.GetRequiredService<IHttpClientFactory>();
|
||||
using var httpClient = factory.CreateClient("attestor");
|
||||
var client = new AttestorClient(httpClient);
|
||||
|
||||
var document = BuildDescriptorDocument();
|
||||
@@ -43,7 +46,9 @@ public sealed class AttestorClientTests
|
||||
{
|
||||
Content = new StringContent("invalid")
|
||||
});
|
||||
using var httpClient = new HttpClient(handler);
|
||||
using var provider = BuildHttpClientProvider(handler);
|
||||
var factory = provider.GetRequiredService<IHttpClientFactory>();
|
||||
using var httpClient = factory.CreateClient("attestor");
|
||||
var client = new AttestorClient(httpClient);
|
||||
|
||||
var document = BuildDescriptorDocument();
|
||||
@@ -54,12 +59,21 @@ public sealed class AttestorClientTests
|
||||
|
||||
private static DescriptorDocument BuildDescriptorDocument()
|
||||
{
|
||||
var createdAt = DateTimeOffset.Parse("2026-01-13T00:00:00Z");
|
||||
var subject = new DescriptorSubject("application/vnd.oci.image.manifest.v1+json", "sha256:img");
|
||||
var artifact = new DescriptorArtifact("application/vnd.cyclonedx+json; version=1.7", "sha256:sbom", 42, new System.Collections.Generic.Dictionary<string, string>());
|
||||
var provenance = new DescriptorProvenance("pending", "sha256:dsse", "nonce", "https://attestor.example.com/api/v1/provenance", "https://slsa.dev/provenance/v1");
|
||||
var generatorMetadata = new DescriptorGeneratorMetadata("generator", "1.0.0");
|
||||
var metadata = new System.Collections.Generic.Dictionary<string, string>();
|
||||
return new DescriptorDocument("schema", DateTimeOffset.UtcNow, generatorMetadata, subject, artifact, provenance, metadata);
|
||||
return new DescriptorDocument("schema", createdAt, generatorMetadata, subject, artifact, provenance, metadata);
|
||||
}
|
||||
|
||||
private static ServiceProvider BuildHttpClientProvider(HttpMessageHandler handler)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddHttpClient("attestor")
|
||||
.ConfigurePrimaryHttpMessageHandler(() => handler);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private sealed class RecordingHandler : HttpMessageHandler
|
||||
|
||||
@@ -74,6 +74,15 @@ public sealed class DescriptorCommandSurfaceTests
|
||||
throw new FileNotFoundException($"BuildX plug-in assembly not found at '{pluginAssembly}'.");
|
||||
}
|
||||
|
||||
EnsurePathWithinRoot(actualRepoRoot, pluginAssembly, "Plug-in assembly");
|
||||
EnsurePathWithinRoot(actualRepoRoot, manifestDirectory, "Manifest directory");
|
||||
EnsurePathWithinRoot(temp.Path, casRoot, "CAS root");
|
||||
EnsurePathWithinRoot(temp.Path, sbomPath, "SBOM path");
|
||||
EnsurePathWithinRoot(temp.Path, layerFragmentsPath, "Layer fragments path");
|
||||
EnsurePathWithinRoot(temp.Path, entryTraceGraphPath, "Entry trace graph path");
|
||||
EnsurePathWithinRoot(temp.Path, entryTraceNdjsonPath, "Entry trace NDJSON path");
|
||||
EnsurePathWithinRoot(temp.Path, manifestOutputPath, "Manifest output path");
|
||||
|
||||
var psi = new ProcessStartInfo("dotnet")
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
@@ -107,10 +116,11 @@ public sealed class DescriptorCommandSurfaceTests
|
||||
psi.ArgumentList.Add("--surface-manifest-output");
|
||||
psi.ArgumentList.Add(manifestOutputPath);
|
||||
|
||||
ValidateProcessStartInfo(psi);
|
||||
var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start BuildX plug-in process.");
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync();
|
||||
var stderr = await process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
await process.WaitForExitAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(process.ExitCode == 0, $"Descriptor command failed.\nSTDOUT: {stdout}\nSTDERR: {stderr}");
|
||||
|
||||
@@ -163,4 +173,33 @@ public sealed class DescriptorCommandSurfaceTests
|
||||
var digest = hash.ComputeHash(bytes, HashAlgorithms.Sha256);
|
||||
return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static void ValidateProcessStartInfo(ProcessStartInfo psi)
|
||||
{
|
||||
if (!string.Equals(System.IO.Path.GetFileName(psi.FileName), "dotnet", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Only dotnet execution is permitted for plug-in tests.");
|
||||
}
|
||||
|
||||
foreach (var argument in psi.ArgumentList)
|
||||
{
|
||||
if (argument.Contains('\n') || argument.Contains('\r'))
|
||||
{
|
||||
throw new InvalidOperationException("Process arguments must not contain newline characters.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsurePathWithinRoot(string root, string candidatePath, string label)
|
||||
{
|
||||
var normalizedRoot = System.IO.Path.GetFullPath(root)
|
||||
.TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar)
|
||||
+ System.IO.Path.DirectorySeparatorChar;
|
||||
var normalizedPath = System.IO.Path.GetFullPath(candidatePath);
|
||||
|
||||
if (!normalizedPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"{label} must stay under '{normalizedRoot}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Surface;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
@@ -43,7 +47,8 @@ public sealed class SurfaceManifestWriterTests
|
||||
EntryTraceNdjsonPath: ndjsonPath,
|
||||
ManifestOutputPath: manifestOutputPath);
|
||||
|
||||
var writer = new SurfaceManifestWriter(TimeProvider.System, CryptoHashFactory.CreateDefault());
|
||||
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero));
|
||||
var writer = new SurfaceManifestWriter(timeProvider, CryptoHashFactory.CreateDefault());
|
||||
var result = await writer.WriteAsync(options, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(result);
|
||||
@@ -67,6 +72,10 @@ public sealed class SurfaceManifestWriterTests
|
||||
Assert.False(string.IsNullOrWhiteSpace(artifact.ManifestArtifact.Uri));
|
||||
Assert.StartsWith("cas://scanner-artifacts/", artifact.ManifestArtifact.Uri, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
var canonicalBytes = CanonJson.Canonicalize(result.Document, CreateManifestJsonOptions());
|
||||
var manifestBytes = await File.ReadAllBytesAsync(result.ManifestPath, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(canonicalBytes, manifestBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -89,8 +98,60 @@ public sealed class SurfaceManifestWriterTests
|
||||
EntryTraceNdjsonPath: null,
|
||||
ManifestOutputPath: null);
|
||||
|
||||
var writer = new SurfaceManifestWriter(TimeProvider.System, CryptoHashFactory.CreateDefault());
|
||||
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero));
|
||||
var writer = new SurfaceManifestWriter(timeProvider, CryptoHashFactory.CreateDefault());
|
||||
var result = await writer.WriteAsync(options, TestContext.Current.CancellationToken);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_DefaultsWorkerInstanceToComponent()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var fragmentsPath = Path.Combine(temp.Path, "layer-fragments.json");
|
||||
await File.WriteAllTextAsync(fragmentsPath, "[]");
|
||||
|
||||
var options = new SurfaceOptions(
|
||||
CacheRoot: temp.Path,
|
||||
CacheBucket: "scanner-artifacts",
|
||||
RootPrefix: "scanner",
|
||||
Tenant: "tenant-a",
|
||||
Component: "scanner.buildx",
|
||||
ComponentVersion: "1.2.3",
|
||||
WorkerInstance: "",
|
||||
Attempt: 1,
|
||||
ImageDigest: "sha256:feedface",
|
||||
ScanId: "scan-123",
|
||||
LayerFragmentsPath: fragmentsPath,
|
||||
EntryTraceGraphPath: null,
|
||||
EntryTraceNdjsonPath: null,
|
||||
ManifestOutputPath: null);
|
||||
|
||||
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero));
|
||||
var writer = new SurfaceManifestWriter(timeProvider, CryptoHashFactory.CreateDefault());
|
||||
var result = await writer.WriteAsync(options, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("scanner.buildx", result!.Document.Source?.WorkerInstance);
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreateManifestJsonOptions()
|
||||
=> new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false,
|
||||
Encoder = JavaScriptEncoder.Default
|
||||
};
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _utcNow;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset utcNow)
|
||||
{
|
||||
_utcNow = utcNow;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _utcNow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
|
||||
internal sealed class TempDirectory : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private static int _sequence;
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public TempDirectory()
|
||||
{
|
||||
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-buildx-{Guid.NewGuid():N}");
|
||||
var suffix = Interlocked.Increment(ref _sequence);
|
||||
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-buildx-{suffix:D4}");
|
||||
Directory.CreateDirectory(Path);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ public sealed class ActionablesEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetDeltaActionables_ValidDeltaId_ReturnsActionables()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678");
|
||||
@@ -40,7 +41,8 @@ public sealed class ActionablesEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetDeltaActionables_SortedByPriority()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678");
|
||||
@@ -58,7 +60,8 @@ public sealed class ActionablesEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetActionablesByPriority_Critical_FiltersCorrectly()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-priority/critical");
|
||||
@@ -73,7 +76,8 @@ public sealed class ActionablesEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetActionablesByPriority_InvalidPriority_ReturnsBadRequest()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-priority/invalid");
|
||||
@@ -84,7 +88,8 @@ public sealed class ActionablesEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetActionablesByType_Upgrade_FiltersCorrectly()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/upgrade");
|
||||
@@ -99,7 +104,8 @@ public sealed class ActionablesEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetActionablesByType_Vex_FiltersCorrectly()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/vex");
|
||||
@@ -114,7 +120,8 @@ public sealed class ActionablesEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetActionablesByType_InvalidType_ReturnsBadRequest()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/invalid");
|
||||
@@ -125,7 +132,8 @@ public sealed class ActionablesEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetDeltaActionables_IncludesEstimatedEffort()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678");
|
||||
|
||||
@@ -18,27 +18,27 @@ namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "3801.0001")]
|
||||
public sealed class ApprovalEndpointsTests : IDisposable
|
||||
public sealed class ApprovalEndpointsTests : IAsyncLifetime
|
||||
{
|
||||
private readonly TestSurfaceSecretsScope _secrets;
|
||||
private readonly ScannerApplicationFactory _factory;
|
||||
private readonly HttpClient _client;
|
||||
private TestSurfaceSecretsScope _secrets = null!;
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public ApprovalEndpointsTests()
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_secrets = new TestSurfaceSecretsScope();
|
||||
|
||||
// Use default factory without auth overrides - same pattern as ManifestEndpointsTests
|
||||
// The factory defaults to anonymous auth which allows all policy assertions
|
||||
_factory = new ScannerApplicationFactory();
|
||||
|
||||
await _factory.InitializeAsync();
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
_factory.Dispose();
|
||||
await _factory.DisposeAsync();
|
||||
_secrets.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ public sealed class AuthorizationTests
|
||||
[Fact]
|
||||
public async Task ApiRoutesRequireAuthenticationWhenAuthorityEnabled()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "true";
|
||||
configuration["scanner:authority:allowAnonymousFallback"] = "false";
|
||||
@@ -19,6 +19,7 @@ public sealed class AuthorizationTests
|
||||
configuration["scanner:authority:clientId"] = "scanner-web";
|
||||
configuration["scanner:authority:clientSecret"] = "secret";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync("/api/v1/__auth-probe");
|
||||
|
||||
@@ -25,7 +25,8 @@ public sealed class BaselineEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetRecommendations_ValidDigest_ReturnsRecommendations()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123", TestContext.Current.CancellationToken);
|
||||
@@ -42,7 +43,8 @@ public sealed class BaselineEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetRecommendations_WithEnvironment_FiltersCorrectly()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123?environment=production", TestContext.Current.CancellationToken);
|
||||
@@ -57,7 +59,8 @@ public sealed class BaselineEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetRecommendations_IncludesRationale()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123", TestContext.Current.CancellationToken);
|
||||
@@ -76,7 +79,8 @@ public sealed class BaselineEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetRationale_ValidDigests_ReturnsDetailedRationale()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/baselines/rationale/sha256:base123/sha256:head456");
|
||||
@@ -95,7 +99,8 @@ public sealed class BaselineEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetRationale_IncludesSelectionCriteria()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/baselines/rationale/sha256:baseline-base123/sha256:head456");
|
||||
@@ -110,7 +115,8 @@ public sealed class BaselineEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetRecommendations_DefaultIsFirst()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123", TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// Task: TRI-MASTER-0007 - Performance benchmark suite (TTFS)
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
@@ -18,10 +20,10 @@ namespace StellaOps.Scanner.WebService.Tests.Benchmarks;
|
||||
/// TTFS (Time-To-First-Signal) performance benchmarks for triage workflows.
|
||||
/// Measures the latency from request initiation to first meaningful evidence display.
|
||||
///
|
||||
/// Target KPIs (from Triage Advisory §3):
|
||||
/// Target KPIs (from Triage Advisory section 3):
|
||||
/// - TTFS p95 < 1.5s (with 100ms RTT, 1% loss)
|
||||
/// - Clicks-to-Closure median < 6 clicks
|
||||
/// - Evidence Completeness ≥ 90%
|
||||
/// - Evidence Completeness >= 90%
|
||||
/// </summary>
|
||||
[Config(typeof(TtfsBenchmarkConfig))]
|
||||
[MemoryDiagnoser]
|
||||
@@ -149,7 +151,7 @@ public sealed class TtfsPerformanceTests
|
||||
{
|
||||
// Arrange
|
||||
var cache = new MockEvidenceCache();
|
||||
var alertId = Guid.NewGuid().ToString();
|
||||
const string alertId = "alert-test-0001";
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
@@ -189,7 +191,7 @@ public sealed class TtfsPerformanceTests
|
||||
{
|
||||
// Arrange
|
||||
var cache = new MockEvidenceCache();
|
||||
var alertId = Guid.NewGuid().ToString();
|
||||
const string alertId = "alert-test-0002";
|
||||
var evidence = cache.GetEvidence(alertId);
|
||||
|
||||
// Act
|
||||
@@ -235,7 +237,7 @@ public sealed class TtfsPerformanceTests
|
||||
{
|
||||
// Arrange
|
||||
var cache = new MockEvidenceCache();
|
||||
var alertId = Guid.NewGuid().ToString();
|
||||
const string alertId = "alert-test-0003";
|
||||
|
||||
// Act
|
||||
var evidence = cache.GetEvidence(alertId);
|
||||
@@ -290,11 +292,11 @@ public sealed class MockAlertDataStore
|
||||
_alerts = Enumerable.Range(0, alertCount)
|
||||
.Select(i => new Alert
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Id = $"alert-{i:D6}",
|
||||
CveId = $"CVE-2024-{10000 + i}",
|
||||
Severity = _random.Next(0, 4) switch { 0 => "LOW", 1 => "MEDIUM", 2 => "HIGH", _ => "CRITICAL" },
|
||||
Status = "open",
|
||||
CreatedAt = DateTime.UtcNow.AddDays(-_random.Next(1, 30))
|
||||
CreatedAt = TtfsTestClock.FixedUtc.AddDays(-_random.Next(1, 30))
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
@@ -303,9 +305,6 @@ public sealed class MockAlertDataStore
|
||||
|
||||
public AlertListResult GetAlerts(int page, int pageSize)
|
||||
{
|
||||
// Simulate DB query latency
|
||||
Thread.Sleep(5);
|
||||
|
||||
var skip = (page - 1) * pageSize;
|
||||
return new AlertListResult
|
||||
{
|
||||
@@ -318,14 +317,12 @@ public sealed class MockAlertDataStore
|
||||
|
||||
public Alert GetAlert(string id)
|
||||
{
|
||||
Thread.Sleep(2);
|
||||
return _alerts.First(a => a.Id == id);
|
||||
}
|
||||
|
||||
public DecisionResult RecordDecision(string alertId, DecisionRequest request)
|
||||
{
|
||||
Thread.Sleep(3);
|
||||
return new DecisionResult { Success = true, DecisionId = Guid.NewGuid().ToString() };
|
||||
return new DecisionResult { Success = true, DecisionId = $"decision-{alertId}" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,9 +330,6 @@ public sealed class MockEvidenceCache
|
||||
{
|
||||
public EvidenceBundle GetEvidence(string alertId)
|
||||
{
|
||||
// Simulate evidence retrieval latency
|
||||
Thread.Sleep(10);
|
||||
|
||||
return new EvidenceBundle
|
||||
{
|
||||
AlertId = alertId,
|
||||
@@ -357,7 +351,7 @@ public sealed class MockEvidenceCache
|
||||
VexStatus = new VexStatusEvidence
|
||||
{
|
||||
Status = "under_investigation",
|
||||
LastUpdated = DateTime.UtcNow.AddDays(-2)
|
||||
LastUpdated = TtfsTestClock.FixedUtc.AddDays(-2)
|
||||
},
|
||||
GraphRevision = new GraphRevisionEvidence
|
||||
{
|
||||
@@ -374,16 +368,23 @@ public static class ReplayTokenGenerator
|
||||
public static ReplayToken Generate(string alertId, EvidenceBundle evidence)
|
||||
{
|
||||
// Simulate token generation
|
||||
var hash = $"{alertId}:{evidence.Reachability?.Tier}:{evidence.VexStatus?.Status}".GetHashCode();
|
||||
var input = $"{alertId}:{evidence.Reachability?.Tier}:{evidence.VexStatus?.Status}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
var hex = Convert.ToHexString(hash).ToLowerInvariant();
|
||||
return new ReplayToken
|
||||
{
|
||||
Token = $"replay_{Math.Abs(hash):x8}",
|
||||
Token = $"replay_{hex[..8]}",
|
||||
AlertId = alertId,
|
||||
GeneratedAt = DateTime.UtcNow
|
||||
GeneratedAt = TtfsTestClock.FixedUtc
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal static class TtfsTestClock
|
||||
{
|
||||
public static readonly DateTime FixedUtc = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Models
|
||||
|
||||
@@ -14,10 +14,11 @@ public sealed class CallGraphEndpointsTests
|
||||
public async Task SubmitCallGraphRequiresContentDigestHeader()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -34,10 +35,11 @@ public sealed class CallGraphEndpointsTests
|
||||
public async Task SubmitCallGraphReturnsAcceptedAndDetectsDuplicates()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.TestKit.Fixtures;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests.Contract;
|
||||
|
||||
@@ -22,10 +23,12 @@ public sealed class ScannerOpenApiContractTests : IClassFixture<ScannerApplicati
|
||||
{
|
||||
private readonly ScannerApplicationFactory _factory;
|
||||
private readonly string _snapshotPath;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public ScannerOpenApiContractTests(ScannerApplicationFactory factory)
|
||||
public ScannerOpenApiContractTests(ScannerApplicationFactory factory, ITestOutputHelper output)
|
||||
{
|
||||
_factory = factory;
|
||||
_output = output;
|
||||
_snapshotPath = Path.Combine(AppContext.BaseDirectory, "Contract", "Expected", "scanner-openapi.json");
|
||||
}
|
||||
|
||||
@@ -79,10 +82,10 @@ public sealed class ScannerOpenApiContractTests : IClassFixture<ScannerApplicati
|
||||
// Log non-breaking changes for awareness
|
||||
if (changes.NonBreakingChanges.Count > 0)
|
||||
{
|
||||
Console.WriteLine("Non-breaking API changes detected:");
|
||||
_output.WriteLine("Non-breaking API changes detected:");
|
||||
foreach (var change in changes.NonBreakingChanges)
|
||||
{
|
||||
Console.WriteLine($" + {change}");
|
||||
_output.WriteLine($" + {change}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompute_ValidRequest_ReturnsCounterfactuals()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new CounterfactualRequestDto
|
||||
@@ -52,7 +53,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompute_MissingFindingId_ReturnsBadRequest()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new CounterfactualRequestDto
|
||||
@@ -69,7 +71,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompute_IncludesVexPath()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new CounterfactualRequestDto
|
||||
@@ -90,7 +93,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompute_IncludesReachabilityPath()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new CounterfactualRequestDto
|
||||
@@ -111,7 +115,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompute_IncludesExceptionPath()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new CounterfactualRequestDto
|
||||
@@ -132,7 +137,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompute_WithMaxPaths_LimitsResults()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new CounterfactualRequestDto
|
||||
@@ -154,7 +160,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetForFinding_ValidId_ReturnsCounterfactuals()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/counterfactuals/finding/finding-123");
|
||||
@@ -169,7 +176,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetScanSummary_ValidId_ReturnsSummary()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/counterfactuals/scan/scan-123/summary");
|
||||
@@ -185,7 +193,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetScanSummary_IncludesPathCounts()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/counterfactuals/scan/scan-123/summary");
|
||||
@@ -203,7 +212,8 @@ public sealed class CounterfactualEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompute_PathsHaveConditions()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new CounterfactualRequestDto
|
||||
|
||||
@@ -25,7 +25,8 @@ public sealed class DeltaCompareEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompare_ValidRequest_ReturnsComparisonResult()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new DeltaCompareRequestDto
|
||||
@@ -54,7 +55,8 @@ public sealed class DeltaCompareEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompare_MissingBaseDigest_ReturnsBadRequest()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new DeltaCompareRequestDto
|
||||
@@ -71,7 +73,8 @@ public sealed class DeltaCompareEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompare_MissingTargetDigest_ReturnsBadRequest()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new DeltaCompareRequestDto
|
||||
@@ -88,7 +91,8 @@ public sealed class DeltaCompareEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetQuickDiff_ValidDigests_ReturnsQuickSummary()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/delta/quick?baseDigest=sha256:base123&targetDigest=sha256:target456");
|
||||
@@ -106,7 +110,8 @@ public sealed class DeltaCompareEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetQuickDiff_MissingDigest_ReturnsBadRequest()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/delta/quick?baseDigest=sha256:base123");
|
||||
@@ -117,7 +122,8 @@ public sealed class DeltaCompareEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetComparison_NotFound_ReturnsNotFound()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/delta/nonexistent-id");
|
||||
@@ -128,7 +134,8 @@ public sealed class DeltaCompareEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostCompare_DeterministicComparisonId_SameInputsSameId()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new DeltaCompareRequestDto
|
||||
|
||||
@@ -17,14 +17,14 @@ namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "3410.0002")]
|
||||
public sealed class EpssEndpointsTests : IDisposable
|
||||
public sealed class EpssEndpointsTests : IAsyncLifetime
|
||||
{
|
||||
private readonly TestSurfaceSecretsScope _secrets;
|
||||
private readonly InMemoryEpssProvider _epssProvider;
|
||||
private readonly ScannerApplicationFactory _factory;
|
||||
private readonly HttpClient _client;
|
||||
private TestSurfaceSecretsScope _secrets = null!;
|
||||
private InMemoryEpssProvider _epssProvider = null!;
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public EpssEndpointsTests()
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_secrets = new TestSurfaceSecretsScope();
|
||||
_epssProvider = new InMemoryEpssProvider();
|
||||
@@ -37,13 +37,14 @@ public sealed class EpssEndpointsTests : IDisposable
|
||||
services.AddSingleton<IEpssProvider>(_epssProvider);
|
||||
});
|
||||
|
||||
await _factory.InitializeAsync();
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
_factory.Dispose();
|
||||
await _factory.DisposeAsync();
|
||||
_secrets.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,11 @@ public sealed class EvidenceEndpointsTests
|
||||
public async Task GetEvidence_ReturnsBadRequest_WhenScanIdInvalid()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Empty scan ID - route doesn't match
|
||||
@@ -42,10 +43,11 @@ public sealed class EvidenceEndpointsTests
|
||||
public async Task GetEvidence_ReturnsNotFound_WhenScanDoesNotExist()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync(
|
||||
@@ -60,10 +62,11 @@ public sealed class EvidenceEndpointsTests
|
||||
{
|
||||
// When no finding ID is provided, the route matches the list endpoint
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Create a scan first
|
||||
@@ -81,10 +84,11 @@ public sealed class EvidenceEndpointsTests
|
||||
public async Task ListEvidence_ReturnsEmptyList_WhenNoFindings()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await CreateScanAsync(client);
|
||||
@@ -106,10 +110,11 @@ public sealed class EvidenceEndpointsTests
|
||||
// The current implementation returns empty list for non-existent scans
|
||||
// because the reachability service returns empty findings for unknown scans
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/scans/nonexistent-scan/evidence");
|
||||
|
||||
@@ -21,10 +21,11 @@ public sealed class FindingsEvidenceControllerTests
|
||||
public async Task GetEvidence_ReturnsNotFound_WhenFindingMissing()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
await EnsureTriageSchemaAsync(factory);
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -38,10 +39,11 @@ public sealed class FindingsEvidenceControllerTests
|
||||
public async Task GetEvidence_ReturnsForbidden_WhenRawScopeMissing()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
await EnsureTriageSchemaAsync(factory);
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -55,10 +57,11 @@ public sealed class FindingsEvidenceControllerTests
|
||||
public async Task GetEvidence_ReturnsEvidence_WhenFindingExists()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
await EnsureTriageSchemaAsync(factory);
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -79,10 +82,11 @@ public sealed class FindingsEvidenceControllerTests
|
||||
public async Task BatchEvidence_ReturnsBadRequest_WhenTooMany()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
await EnsureTriageSchemaAsync(factory);
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -101,10 +105,11 @@ public sealed class FindingsEvidenceControllerTests
|
||||
public async Task BatchEvidence_ReturnsResults_ForExistingFindings()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
await EnsureTriageSchemaAsync(factory);
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ public sealed class HealthEndpointsTests
|
||||
[Fact]
|
||||
public async Task HealthAndReadyEndpointsRespond()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var healthResponse = await client.GetAsync("/healthz");
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
@@ -28,6 +29,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly HumanApprovalAttestationService _service;
|
||||
private readonly IGuidProvider _guidProvider = new SequentialGuidProvider();
|
||||
|
||||
public HumanApprovalAttestationServiceTests()
|
||||
{
|
||||
@@ -35,6 +37,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
_service = new HumanApprovalAttestationService(
|
||||
NullLogger<HumanApprovalAttestationService>.Instance,
|
||||
MsOptions.Options.Create(new HumanApprovalAttestationOptions { DefaultApprovalTtlDays = 30 }),
|
||||
_guidProvider,
|
||||
_timeProvider);
|
||||
}
|
||||
|
||||
@@ -319,7 +322,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
public async Task GetAttestationAsync_NonExistentAttestation_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = await _service.GetAttestationAsync(ScanId.New(), "nonexistent");
|
||||
var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), "nonexistent");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
@@ -338,6 +341,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
var service = new HumanApprovalAttestationService(
|
||||
NullLogger<HumanApprovalAttestationService>.Instance,
|
||||
MsOptions.Options.Create(new HumanApprovalAttestationOptions()),
|
||||
_guidProvider,
|
||||
expiredProvider);
|
||||
|
||||
// Need to create in this service instance for the store to be shared
|
||||
@@ -354,7 +358,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
await _service.CreateAttestationAsync(input);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetAttestationAsync(ScanId.New(), input.FindingId);
|
||||
var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), input.FindingId);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
@@ -384,7 +388,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
public async Task GetApprovalsByScanAsync_MultipleApprovals_ReturnsAll()
|
||||
{
|
||||
// Arrange
|
||||
var scanId = ScanId.New();
|
||||
var scanId = ScanId.New(_guidProvider);
|
||||
var input1 = CreateValidInput() with { ScanId = scanId, FindingId = "CVE-2024-0001" };
|
||||
var input2 = CreateValidInput() with { ScanId = scanId, FindingId = "CVE-2024-0002" };
|
||||
|
||||
@@ -403,7 +407,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
public async Task GetApprovalsByScanAsync_NoApprovals_ReturnsEmptyList()
|
||||
{
|
||||
// Act
|
||||
var results = await _service.GetApprovalsByScanAsync(ScanId.New());
|
||||
var results = await _service.GetApprovalsByScanAsync(ScanId.New(_guidProvider));
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
@@ -414,7 +418,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
public async Task GetApprovalsByScanAsync_ExcludesRevokedApprovals()
|
||||
{
|
||||
// Arrange
|
||||
var scanId = ScanId.New();
|
||||
var scanId = ScanId.New(_guidProvider);
|
||||
var input = CreateValidInput() with { ScanId = scanId };
|
||||
await _service.CreateAttestationAsync(input);
|
||||
await _service.RevokeApprovalAsync(scanId, input.FindingId, "admin", "Testing");
|
||||
@@ -455,7 +459,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
{
|
||||
// Act
|
||||
var result = await _service.RevokeApprovalAsync(
|
||||
ScanId.New(),
|
||||
ScanId.New(_guidProvider),
|
||||
"nonexistent",
|
||||
"admin@example.com",
|
||||
"Testing");
|
||||
@@ -541,7 +545,7 @@ public sealed class HumanApprovalAttestationServiceTests
|
||||
{
|
||||
return new HumanApprovalAttestationInput
|
||||
{
|
||||
ScanId = ScanId.New(),
|
||||
ScanId = ScanId.New(_guidProvider),
|
||||
FindingId = "CVE-2024-12345",
|
||||
Decision = ApprovalDecision.AcceptRisk,
|
||||
ApproverUserId = "security-lead@example.com",
|
||||
|
||||
@@ -24,20 +24,25 @@ public sealed class IdempotencyMiddlewareTests
|
||||
private const string IdempotencyKeyHeader = "X-Idempotency-Key";
|
||||
private const string IdempotencyCachedHeader = "X-Idempotency-Cached";
|
||||
|
||||
private static ScannerApplicationFactory CreateFactory() =>
|
||||
new ScannerApplicationFactory().WithOverrides(
|
||||
private static async Task<ScannerApplicationFactory> CreateFactoryAsync()
|
||||
{
|
||||
var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureConfiguration: config =>
|
||||
{
|
||||
config["Scanner:Idempotency:Enabled"] = "true";
|
||||
config["Scanner:Idempotency:Window"] = "24:00:00";
|
||||
});
|
||||
|
||||
await factory.InitializeAsync();
|
||||
return factory;
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PostRequest_WithContentDigest_ReturnsIdempotencyKey()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var content = new StringContent("""{"test":"data"}""", Encoding.UTF8, "application/json");
|
||||
@@ -58,7 +63,7 @@ public sealed class IdempotencyMiddlewareTests
|
||||
public async Task DuplicateRequest_WithSameContentDigest_ReturnsCachedResponse()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var requestBody = """{"artifactDigest":"sha256:test123"}""";
|
||||
@@ -84,7 +89,7 @@ public sealed class IdempotencyMiddlewareTests
|
||||
public async Task DifferentRequests_WithDifferentDigests_AreProcessedSeparately()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var requestBody1 = """{"artifactDigest":"sha256:unique1"}""";
|
||||
@@ -110,7 +115,7 @@ public sealed class IdempotencyMiddlewareTests
|
||||
public async Task GetRequest_BypassesIdempotencyMiddleware()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Act
|
||||
@@ -125,7 +130,7 @@ public sealed class IdempotencyMiddlewareTests
|
||||
public async Task PostRequest_WithoutContentDigest_ComputesDigest()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var content = new StringContent("""{"test":"nodigest"}""", Encoding.UTF8, "application/json");
|
||||
|
||||
@@ -27,7 +27,7 @@ public sealed class EvidenceIntegrationTests : IAsyncLifetime
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public ValueTask InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configuration =>
|
||||
@@ -41,8 +41,8 @@ public sealed class EvidenceIntegrationTests : IAsyncLifetime
|
||||
services.AddSingleton<IArtifactObjectStore>(new InMemoryArtifactObjectStore());
|
||||
});
|
||||
|
||||
await _factory.InitializeAsync();
|
||||
_client = _factory.CreateClient();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public ValueTask InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configuration =>
|
||||
@@ -48,8 +48,8 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime
|
||||
services.AddSingleton<IPedigreeDataProvider>(new MockPedigreeDataProvider());
|
||||
});
|
||||
|
||||
await _factory.InitializeAsync();
|
||||
_client = _factory.CreateClient();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
@@ -412,7 +412,7 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime
|
||||
return Task.FromResult<PedigreeData?>(null);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyDictionary<string, PedigreeData>> GetPedigreesBatchAsync(
|
||||
public async Task<IReadOnlyDictionary<string, PedigreeData>> GetPedigreesBatchAsync(
|
||||
IEnumerable<string> purls,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -420,14 +420,14 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime
|
||||
|
||||
foreach (var purl in purls)
|
||||
{
|
||||
var data = GetPedigreeAsync(purl, cancellationToken).Result;
|
||||
var data = await GetPedigreeAsync(purl, cancellationToken);
|
||||
if (data != null)
|
||||
{
|
||||
results[purl] = data;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyDictionary<string, PedigreeData>>(results);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// ProofReplayWorkflowTests.cs
|
||||
// Sprint: SPRINT_3500_0002_0003_proof_replay_api
|
||||
// Task: T7 - Integration Tests for Proof Replay Workflow
|
||||
// Description: End-to-end tests for scan → manifest → proofs workflow
|
||||
// Description: End-to-end tests for scan -> manifest -> proofs workflow
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net;
|
||||
@@ -20,7 +20,7 @@ namespace StellaOps.Scanner.WebService.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the complete proof replay workflow:
|
||||
/// Submit scan → Get manifest → Replay score → Get proofs.
|
||||
/// Submit scan -> Get manifest -> Replay score -> Get proofs.
|
||||
/// </summary>
|
||||
public sealed class ProofReplayWorkflowTests
|
||||
{
|
||||
@@ -31,27 +31,28 @@ public sealed class ProofReplayWorkflowTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
|
||||
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(1);
|
||||
|
||||
// Seed test data for the scan
|
||||
var manifestRow = new ScanManifestRow
|
||||
{
|
||||
ManifestId = Guid.NewGuid(),
|
||||
ManifestId = CreateGuid(2),
|
||||
ScanId = scanId,
|
||||
ManifestHash = "sha256:workflow-manifest",
|
||||
SbomHash = "sha256:workflow-sbom",
|
||||
RulesHash = "sha256:workflow-rules",
|
||||
FeedHash = "sha256:workflow-feed",
|
||||
PolicyHash = "sha256:workflow-policy",
|
||||
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-10),
|
||||
ScanCompletedAt = DateTimeOffset.UtcNow,
|
||||
ScanStartedAt = FixedNow.AddMinutes(-10),
|
||||
ScanCompletedAt = FixedNow,
|
||||
ManifestContent = """{"version":"1.0","test":"workflow"}""",
|
||||
ScannerVersion = "1.0.0-integration",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = FixedNow
|
||||
};
|
||||
|
||||
await manifestRepository.SaveAsync(manifestRow);
|
||||
@@ -62,7 +63,7 @@ public sealed class ProofReplayWorkflowTests
|
||||
RootHash = "sha256:workflow-root",
|
||||
BundleType = "standard",
|
||||
BundleHash = "sha256:workflow-bundle",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = FixedNow
|
||||
};
|
||||
|
||||
await bundleRepository.SaveAsync(proofBundle);
|
||||
@@ -103,11 +104,12 @@ public sealed class ProofReplayWorkflowTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
|
||||
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(3);
|
||||
|
||||
// Create two proof bundles with the same content should produce same hash
|
||||
var manifestContent = """{"version":"1.0","inputs":{"deterministic":true,"seed":"test-seed-123"}}""";
|
||||
@@ -115,18 +117,18 @@ public sealed class ProofReplayWorkflowTests
|
||||
|
||||
var manifestRow = new ScanManifestRow
|
||||
{
|
||||
ManifestId = Guid.NewGuid(),
|
||||
ManifestId = CreateGuid(4),
|
||||
ScanId = scanId,
|
||||
ManifestHash = $"sha256:{expectedHash}",
|
||||
SbomHash = "sha256:deterministic-sbom",
|
||||
RulesHash = "sha256:deterministic-rules",
|
||||
FeedHash = "sha256:deterministic-feed",
|
||||
PolicyHash = "sha256:deterministic-policy",
|
||||
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
ScanCompletedAt = DateTimeOffset.UtcNow,
|
||||
ScanStartedAt = FixedNow.AddMinutes(-5),
|
||||
ScanCompletedAt = FixedNow,
|
||||
ManifestContent = manifestContent,
|
||||
ScannerVersion = "1.0.0-deterministic",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = FixedNow
|
||||
};
|
||||
|
||||
await manifestRepository.SaveAsync(manifestRow);
|
||||
@@ -161,6 +163,7 @@ public sealed class ProofReplayWorkflowTests
|
||||
{
|
||||
config["Scanner:Idempotency:Enabled"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var requestBody = """{"artifactDigest":"sha256:idempotent-test-123"}""";
|
||||
@@ -195,8 +198,9 @@ public sealed class ProofReplayWorkflowTests
|
||||
config["scanner:rateLimiting:manifestPermitLimit"] = "2";
|
||||
config["scanner:rateLimiting:manifestWindow"] = "00:00:30";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(5);
|
||||
|
||||
// Act - Send requests exceeding the limit
|
||||
var responses = new List<HttpResponseMessage>();
|
||||
@@ -226,8 +230,9 @@ public sealed class ProofReplayWorkflowTests
|
||||
config["scanner:rateLimiting:manifestPermitLimit"] = "1";
|
||||
config["scanner:rateLimiting:manifestWindow"] = "01:00:00";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(6);
|
||||
|
||||
// First request
|
||||
await client.GetAsync($"/api/v1/scans/{scanId}/manifest");
|
||||
@@ -246,6 +251,11 @@ public sealed class ProofReplayWorkflowTests
|
||||
|
||||
#endregion
|
||||
|
||||
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static Guid CreateGuid(int seed)
|
||||
=> new($"00000000-0000-0000-0000-{seed:D12}");
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static string ComputeSha256(string content)
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public ValueTask InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configuration =>
|
||||
@@ -51,8 +51,8 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
|
||||
services.AddSingleton<ISbomValidator>(new MockSbomValidator());
|
||||
});
|
||||
|
||||
await _factory.InitializeAsync();
|
||||
_client = _factory.CreateClient();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
@@ -159,7 +159,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
|
||||
sbomBytes,
|
||||
SbomFormat.CycloneDxJson,
|
||||
validationOptions,
|
||||
CancellationToken.None);
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
@@ -189,7 +189,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
|
||||
spdxBytes,
|
||||
SbomFormat.Spdx3JsonLd,
|
||||
validationOptions,
|
||||
CancellationToken.None);
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
@@ -207,7 +207,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
|
||||
Assert.True(mockValidator.SupportsFormat(SbomFormat.Spdx3JsonLd));
|
||||
Assert.True(mockValidator.SupportsFormat(SbomFormat.Unknown));
|
||||
|
||||
var info = await mockValidator.GetInfoAsync(CancellationToken.None);
|
||||
var info = await mockValidator.GetInfoAsync(TestContext.Current.CancellationToken);
|
||||
Assert.True(info.IsAvailable);
|
||||
Assert.Contains(SbomFormat.CycloneDxJson, info.SupportedFormats);
|
||||
}
|
||||
@@ -229,7 +229,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
|
||||
sbomBytes,
|
||||
SbomFormat.CycloneDxJson,
|
||||
validationOptions,
|
||||
CancellationToken.None);
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
@@ -254,7 +254,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
|
||||
sbomBytes,
|
||||
SbomFormat.CycloneDxJson,
|
||||
validationOptions,
|
||||
CancellationToken.None);
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
@@ -310,11 +310,11 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
|
||||
Reference = "example.com/validation-test:1.0",
|
||||
Digest = "sha256:validation123"
|
||||
}
|
||||
});
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<ScanSubmitResponse>();
|
||||
var payload = await response.Content.ReadFromJsonAsync<ScanSubmitResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(payload);
|
||||
return payload!.ScanId;
|
||||
}
|
||||
|
||||
@@ -34,11 +34,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Submit scan via HTTP POST to get scan ID
|
||||
@@ -62,11 +63,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
public async Task ListLayers_WhenScanNotFound_Returns404()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/scan-not-found/layers");
|
||||
@@ -81,11 +83,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await SubmitScanAsync(client, imageDigest);
|
||||
@@ -121,11 +124,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await SubmitScanAsync(client, imageDigest);
|
||||
@@ -149,11 +153,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await SubmitScanAsync(client, imageDigest);
|
||||
@@ -177,11 +182,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await SubmitScanAsync(client, imageDigest);
|
||||
@@ -201,11 +207,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
public async Task GetLayerSbom_WhenScanNotFound_Returns404()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/scan-not-found/layers/sha256:layer123/sbom");
|
||||
@@ -220,11 +227,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await SubmitScanAsync(client, imageDigest);
|
||||
@@ -246,11 +254,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await SubmitScanAsync(client, imageDigest);
|
||||
@@ -273,11 +282,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
public async Task GetCompositionRecipe_WhenScanNotFound_Returns404()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/scan-not-found/composition-recipe");
|
||||
@@ -292,11 +302,12 @@ public sealed class LayerSbomEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await SubmitScanAsync(client, imageDigest);
|
||||
@@ -326,12 +337,13 @@ public sealed class LayerSbomEndpointsTests
|
||||
Errors = ImmutableArray<string>.Empty,
|
||||
});
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
|
||||
@@ -359,12 +371,13 @@ public sealed class LayerSbomEndpointsTests
|
||||
Errors = ImmutableArray.Create("Merkle root mismatch: expected sha256:abc, got sha256:def"),
|
||||
});
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
|
||||
@@ -382,12 +395,13 @@ public sealed class LayerSbomEndpointsTests
|
||||
[Fact]
|
||||
public async Task VerifyCompositionRecipe_WhenScanNotFound_Returns404()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsync($"{BasePath}/scan-not-found/composition-recipe/verify", null);
|
||||
|
||||
@@ -35,26 +35,27 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(1);
|
||||
|
||||
var manifestRow = new ScanManifestRow
|
||||
{
|
||||
ManifestId = Guid.NewGuid(),
|
||||
ManifestId = CreateGuid(2),
|
||||
ScanId = scanId,
|
||||
ManifestHash = "sha256:manifest123",
|
||||
SbomHash = "sha256:sbom123",
|
||||
RulesHash = "sha256:rules123",
|
||||
FeedHash = "sha256:feed123",
|
||||
PolicyHash = "sha256:policy123",
|
||||
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
ScanCompletedAt = DateTimeOffset.UtcNow,
|
||||
ScanStartedAt = FixedNow.AddMinutes(-5),
|
||||
ScanCompletedAt = FixedNow,
|
||||
ManifestContent = """{"version":"1.0","inputs":{"sbomHash":"sha256:sbom123"}}""",
|
||||
ScannerVersion = "1.0.0-test",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = FixedNow
|
||||
};
|
||||
|
||||
await manifestRepository.SaveAsync(manifestRow, TestContext.Current.CancellationToken);
|
||||
@@ -82,8 +83,9 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(3);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/v1/scans/{scanId}/manifest", TestContext.Current.CancellationToken);
|
||||
@@ -98,6 +100,7 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Act
|
||||
@@ -113,11 +116,12 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(4);
|
||||
|
||||
var manifestContent = JsonSerializer.Serialize(new
|
||||
{
|
||||
@@ -133,18 +137,18 @@ public sealed class ManifestEndpointsTests
|
||||
|
||||
var manifestRow = new ScanManifestRow
|
||||
{
|
||||
ManifestId = Guid.NewGuid(),
|
||||
ManifestId = CreateGuid(5),
|
||||
ScanId = scanId,
|
||||
ManifestHash = "sha256:manifest456",
|
||||
SbomHash = "sha256:sbom123",
|
||||
RulesHash = "sha256:rules123",
|
||||
FeedHash = "sha256:feed123",
|
||||
PolicyHash = "sha256:policy123",
|
||||
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
ScanCompletedAt = DateTimeOffset.UtcNow,
|
||||
ScanStartedAt = FixedNow.AddMinutes(-5),
|
||||
ScanCompletedAt = FixedNow,
|
||||
ManifestContent = manifestContent,
|
||||
ScannerVersion = "1.0.0-test",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = FixedNow
|
||||
};
|
||||
|
||||
await manifestRepository.SaveAsync(manifestRow, TestContext.Current.CancellationToken);
|
||||
@@ -173,26 +177,27 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(6);
|
||||
|
||||
var manifestRow = new ScanManifestRow
|
||||
{
|
||||
ManifestId = Guid.NewGuid(),
|
||||
ManifestId = CreateGuid(7),
|
||||
ScanId = scanId,
|
||||
ManifestHash = "sha256:content-digest-test",
|
||||
SbomHash = "sha256:sbom789",
|
||||
RulesHash = "sha256:rules789",
|
||||
FeedHash = "sha256:feed789",
|
||||
PolicyHash = "sha256:policy789",
|
||||
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-2),
|
||||
ScanCompletedAt = DateTimeOffset.UtcNow,
|
||||
ScanStartedAt = FixedNow.AddMinutes(-2),
|
||||
ScanCompletedAt = FixedNow,
|
||||
ManifestContent = """{"test":"content-digest"}""",
|
||||
ScannerVersion = "1.0.0-test",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = FixedNow
|
||||
};
|
||||
|
||||
await manifestRepository.SaveAsync(manifestRow, TestContext.Current.CancellationToken);
|
||||
@@ -219,8 +224,9 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(8);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/v1/scans/{scanId}/proofs");
|
||||
@@ -240,11 +246,12 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(9);
|
||||
|
||||
var bundle1 = new ProofBundleRow
|
||||
{
|
||||
@@ -252,7 +259,7 @@ public sealed class ManifestEndpointsTests
|
||||
RootHash = "sha256:root1",
|
||||
BundleType = "standard",
|
||||
BundleHash = "sha256:bundle1",
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5)
|
||||
CreatedAt = FixedNow.AddMinutes(-5)
|
||||
};
|
||||
|
||||
var bundle2 = new ProofBundleRow
|
||||
@@ -261,7 +268,7 @@ public sealed class ManifestEndpointsTests
|
||||
RootHash = "sha256:root2",
|
||||
BundleType = "extended",
|
||||
BundleHash = "sha256:bundle2",
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-2)
|
||||
CreatedAt = FixedNow.AddMinutes(-2)
|
||||
};
|
||||
|
||||
await bundleRepository.SaveAsync(bundle1);
|
||||
@@ -286,6 +293,7 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Act
|
||||
@@ -305,11 +313,12 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(10);
|
||||
var rootHash = "sha256:detailroot1";
|
||||
|
||||
var bundle = new ProofBundleRow
|
||||
@@ -324,8 +333,8 @@ public sealed class ManifestEndpointsTests
|
||||
VexHash = "sha256:vex1",
|
||||
SignatureKeyId = "key-001",
|
||||
SignatureAlgorithm = "ed25519",
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-3),
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
|
||||
CreatedAt = FixedNow.AddMinutes(-3),
|
||||
ExpiresAt = FixedNow.AddDays(30)
|
||||
};
|
||||
|
||||
await bundleRepository.SaveAsync(bundle);
|
||||
@@ -356,8 +365,9 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(11);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync($"/api/v1/scans/{scanId}/proofs/sha256:nonexistent");
|
||||
@@ -372,12 +382,13 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
|
||||
var scanId1 = Guid.NewGuid();
|
||||
var scanId2 = Guid.NewGuid();
|
||||
var scanId1 = CreateGuid(12);
|
||||
var scanId2 = CreateGuid(13);
|
||||
var rootHash = "sha256:crossscanroot";
|
||||
|
||||
var bundle = new ProofBundleRow
|
||||
@@ -386,7 +397,7 @@ public sealed class ManifestEndpointsTests
|
||||
RootHash = rootHash,
|
||||
BundleType = "standard",
|
||||
BundleHash = "sha256:crossscanbundle",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = FixedNow
|
||||
};
|
||||
|
||||
await bundleRepository.SaveAsync(bundle);
|
||||
@@ -404,6 +415,7 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Act
|
||||
@@ -419,8 +431,9 @@ public sealed class ManifestEndpointsTests
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
var scanId = CreateGuid(14);
|
||||
|
||||
// Act - Trailing slash with empty root hash
|
||||
var response = await client.GetAsync($"/api/v1/scans/{scanId}/proofs/");
|
||||
@@ -431,4 +444,9 @@ public sealed class ManifestEndpointsTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static Guid CreateGuid(int seed)
|
||||
=> new($"00000000-0000-0000-0000-{seed:D12}");
|
||||
}
|
||||
|
||||
@@ -29,7 +29,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().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "true";
|
||||
@@ -39,6 +39,7 @@ public sealed class OfflineKitEndpointsTests
|
||||
config["Scanner:OfflineKit:TrustAnchors:0:PurlPattern"] = "*";
|
||||
config["Scanner:OfflineKit:TrustAnchors:0:AllowedKeyIds:0"] = keyId;
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -97,7 +98,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().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "true";
|
||||
@@ -107,6 +108,7 @@ public sealed class OfflineKitEndpointsTests
|
||||
config["Scanner:OfflineKit:TrustAnchors:0:PurlPattern"] = "*";
|
||||
config["Scanner:OfflineKit:TrustAnchors:0:AllowedKeyIds:0"] = keyId;
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -151,12 +153,13 @@ 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().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "false";
|
||||
config["Scanner:OfflineKit:RekorOfflineMode"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -192,7 +195,7 @@ public sealed class OfflineKitEndpointsTests
|
||||
|
||||
var auditEmitter = new CapturingAuditEmitter();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "false";
|
||||
@@ -202,6 +205,7 @@ public sealed class OfflineKitEndpointsTests
|
||||
services.RemoveAll<IOfflineKitAuditEmitter>();
|
||||
services.AddSingleton<IOfflineKitAuditEmitter>(auditEmitter);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -241,10 +245,11 @@ public sealed class OfflineKitEndpointsTests
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -262,11 +267,12 @@ public sealed class OfflineKitEndpointsTests
|
||||
var bundleBytes = Encoding.UTF8.GetBytes("deterministic-offline-kit-bundle");
|
||||
var bundleSha = ComputeSha256Hex(bundleBytes);
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -304,10 +310,11 @@ public sealed class OfflineKitEndpointsTests
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -346,10 +353,11 @@ public sealed class OfflineKitEndpointsTests
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -382,10 +390,11 @@ public sealed class OfflineKitEndpointsTests
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -425,10 +434,11 @@ public sealed class OfflineKitEndpointsTests
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -469,10 +479,11 @@ public sealed class OfflineKitEndpointsTests
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -498,10 +509,11 @@ public sealed class OfflineKitEndpointsTests
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -517,10 +529,11 @@ public sealed class OfflineKitEndpointsTests
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
@@ -536,10 +549,11 @@ public sealed class OfflineKitEndpointsTests
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
@@ -13,11 +13,12 @@ public sealed class PlatformEventPublisherRegistrationTests
|
||||
[Fact]
|
||||
public void NullPublisherRegisteredWhenEventsDisabled()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:events:enabled"] = "false";
|
||||
configuration["scanner:events:dsn"] = string.Empty;
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var publisher = scope.ServiceProvider.GetRequiredService<IPlatformEventPublisher>();
|
||||
@@ -44,7 +45,7 @@ public sealed class PlatformEventPublisherRegistrationTests
|
||||
|
||||
try
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:events:enabled"] = "true";
|
||||
configuration["scanner:events:driver"] = "redis";
|
||||
@@ -53,6 +54,7 @@ public sealed class PlatformEventPublisherRegistrationTests
|
||||
configuration["scanner:events:publishTimeoutSeconds"] = "1";
|
||||
configuration["scanner:events:maxStreamLength"] = "100";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var options = scope.ServiceProvider.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
@@ -28,6 +29,7 @@ public sealed class PolicyDecisionAttestationServiceTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly PolicyDecisionAttestationService _service;
|
||||
private readonly IGuidProvider _guidProvider = new SequentialGuidProvider();
|
||||
|
||||
public PolicyDecisionAttestationServiceTests()
|
||||
{
|
||||
@@ -284,7 +286,7 @@ public sealed class PolicyDecisionAttestationServiceTests
|
||||
{
|
||||
// Act
|
||||
var result = await _service.GetAttestationAsync(
|
||||
ScanId.New(),
|
||||
ScanId.New(_guidProvider),
|
||||
"CVE-2024-00000@pkg:npm/nonexistent@1.0.0");
|
||||
|
||||
// Assert
|
||||
@@ -301,7 +303,7 @@ public sealed class PolicyDecisionAttestationServiceTests
|
||||
|
||||
// Act
|
||||
var result = await _service.GetAttestationAsync(
|
||||
ScanId.New(), // Different scan ID
|
||||
ScanId.New(_guidProvider), // Different scan ID
|
||||
input.FindingId);
|
||||
|
||||
// Assert
|
||||
@@ -393,7 +395,7 @@ public sealed class PolicyDecisionAttestationServiceTests
|
||||
{
|
||||
return new PolicyDecisionInput
|
||||
{
|
||||
ScanId = ScanId.New(),
|
||||
ScanId = ScanId.New(_guidProvider),
|
||||
FindingId = "CVE-2024-12345@pkg:npm/stripe@6.1.2",
|
||||
Cve = "CVE-2024-12345",
|
||||
ComponentPurl = "pkg:npm/stripe@6.1.2",
|
||||
|
||||
@@ -18,7 +18,8 @@ public sealed class PolicyEndpointsTests
|
||||
[Fact]
|
||||
public async Task PolicySchemaReturnsEmbeddedSchema()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/policy/schema", TestContext.Current.CancellationToken);
|
||||
@@ -34,7 +35,8 @@ public sealed class PolicyEndpointsTests
|
||||
[Fact]
|
||||
public async Task PolicyDiagnosticsReturnsRecommendations()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new PolicyDiagnosticsRequestDto
|
||||
@@ -62,7 +64,8 @@ public sealed class PolicyEndpointsTests
|
||||
[Fact]
|
||||
public async Task PolicyPreviewUsesProposedPolicy()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
const string policyYaml = """
|
||||
|
||||
@@ -22,6 +22,7 @@ public sealed class ProofSpineEndpointsTests
|
||||
public async Task GetSpine_ReturnsSpine_WithVerification()
|
||||
{
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
|
||||
@@ -62,6 +63,7 @@ public sealed class ProofSpineEndpointsTests
|
||||
public async Task GetSpine_ReturnsCbor_WhenAcceptHeaderRequestsCbor()
|
||||
{
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
|
||||
@@ -99,6 +101,7 @@ public sealed class ProofSpineEndpointsTests
|
||||
public async Task ListSpinesByScan_ReturnsSummaries_WithSegmentCount()
|
||||
{
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
|
||||
@@ -138,6 +141,7 @@ public sealed class ProofSpineEndpointsTests
|
||||
public async Task ListSpinesByScan_ReturnsCbor_WhenAcceptHeaderRequestsCbor()
|
||||
{
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
|
||||
@@ -177,6 +181,7 @@ public sealed class ProofSpineEndpointsTests
|
||||
public async Task GetSpine_ReturnsInvalidStatus_WhenSegmentTampered()
|
||||
{
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
|
||||
|
||||
@@ -22,8 +22,11 @@ public sealed class RateLimitingTests
|
||||
private const string RateLimitRemainingHeader = "X-RateLimit-Remaining";
|
||||
private const string RetryAfterHeader = "Retry-After";
|
||||
|
||||
private static ScannerApplicationFactory CreateFactory(int permitLimit = 100, int windowSeconds = 3600) =>
|
||||
new ScannerApplicationFactory().WithOverrides(
|
||||
private static async Task<ScannerApplicationFactory> CreateFactoryAsync(
|
||||
int permitLimit = 100,
|
||||
int windowSeconds = 3600)
|
||||
{
|
||||
var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureConfiguration: config =>
|
||||
{
|
||||
config["scanner:rateLimiting:scoreReplayPermitLimit"] = permitLimit.ToString();
|
||||
@@ -34,12 +37,16 @@ public sealed class RateLimitingTests
|
||||
config["scanner:rateLimiting:proofBundleWindow"] = TimeSpan.FromSeconds(windowSeconds).ToString();
|
||||
});
|
||||
|
||||
await factory.InitializeAsync();
|
||||
return factory;
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ManifestEndpoint_IncludesRateLimitHeaders()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
|
||||
@@ -58,7 +65,7 @@ public sealed class RateLimitingTests
|
||||
public async Task ProofBundleEndpoint_IncludesRateLimitHeaders()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
|
||||
@@ -76,7 +83,7 @@ public sealed class RateLimitingTests
|
||||
public async Task ExcessiveRequests_Returns429()
|
||||
{
|
||||
// Arrange - Create factory with very low rate limit for testing
|
||||
await using var factory = CreateFactory(permitLimit: 2, windowSeconds: 60);
|
||||
await using var factory = await CreateFactoryAsync(permitLimit: 2, windowSeconds: 60);
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
|
||||
@@ -103,7 +110,7 @@ public sealed class RateLimitingTests
|
||||
public async Task RateLimited_Returns429WithRetryAfter()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory(permitLimit: 1, windowSeconds: 3600);
|
||||
await using var factory = await CreateFactoryAsync(permitLimit: 1, windowSeconds: 3600);
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
|
||||
@@ -126,7 +133,7 @@ public sealed class RateLimitingTests
|
||||
public async Task HealthEndpoint_NotRateLimited()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory(permitLimit: 1);
|
||||
await using var factory = await CreateFactoryAsync(permitLimit: 1);
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Act - Send multiple health requests
|
||||
@@ -146,7 +153,7 @@ public sealed class RateLimitingTests
|
||||
public async Task RateLimitedResponse_HasProblemDetails()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = CreateFactory(permitLimit: 1, windowSeconds: 3600);
|
||||
await using var factory = await CreateFactoryAsync(permitLimit: 1, windowSeconds: 3600);
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
|
||||
@@ -173,7 +180,7 @@ public sealed class RateLimitingTests
|
||||
// In practice, this requires setting up different auth contexts
|
||||
|
||||
// Arrange
|
||||
await using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = Guid.NewGuid();
|
||||
|
||||
|
||||
@@ -22,10 +22,11 @@ public sealed class ReachabilityDriftEndpointsTests
|
||||
public async Task GetDriftReturnsNotFoundWhenNoResultAndNoBaseScanProvided()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -41,10 +42,11 @@ public sealed class ReachabilityDriftEndpointsTests
|
||||
public async Task GetDriftComputesResultAndListsDriftedSinks()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Scanner.Storage.Models;
|
||||
using StellaOps.Scanner.Storage.Services;
|
||||
@@ -36,7 +37,13 @@ public sealed class ReportEventDispatcherTests
|
||||
{
|
||||
var publisher = new RecordingEventPublisher();
|
||||
var tracker = new RecordingClassificationChangeTracker();
|
||||
var dispatcher = new ReportEventDispatcher(publisher, tracker, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
|
||||
var dispatcher = new ReportEventDispatcher(
|
||||
publisher,
|
||||
tracker,
|
||||
Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()),
|
||||
new SequentialGuidProvider(),
|
||||
TimeProvider.System,
|
||||
NullLogger<ReportEventDispatcher>.Instance);
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var request = new ReportRequestDto
|
||||
@@ -177,7 +184,13 @@ public sealed class ReportEventDispatcherTests
|
||||
{
|
||||
var publisher = new RecordingEventPublisher();
|
||||
var tracker = new RecordingClassificationChangeTracker();
|
||||
var dispatcher = new ReportEventDispatcher(publisher, tracker, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
|
||||
var dispatcher = new ReportEventDispatcher(
|
||||
publisher,
|
||||
tracker,
|
||||
Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()),
|
||||
new SequentialGuidProvider(),
|
||||
TimeProvider.System,
|
||||
NullLogger<ReportEventDispatcher>.Instance);
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var request = new ReportRequestDto
|
||||
@@ -262,7 +275,13 @@ public sealed class ReportEventDispatcherTests
|
||||
{
|
||||
ThrowOnTrack = true
|
||||
};
|
||||
var dispatcher = new ReportEventDispatcher(publisher, tracker, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
|
||||
var dispatcher = new ReportEventDispatcher(
|
||||
publisher,
|
||||
tracker,
|
||||
Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()),
|
||||
new SequentialGuidProvider(),
|
||||
TimeProvider.System,
|
||||
NullLogger<ReportEventDispatcher>.Instance);
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var request = new ReportRequestDto
|
||||
@@ -333,7 +352,13 @@ public sealed class ReportEventDispatcherTests
|
||||
|
||||
var publisher = new RecordingEventPublisher();
|
||||
var tracker = new RecordingClassificationChangeTracker();
|
||||
var dispatcher = new ReportEventDispatcher(publisher, tracker, options, TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
|
||||
var dispatcher = new ReportEventDispatcher(
|
||||
publisher,
|
||||
tracker,
|
||||
options,
|
||||
new SequentialGuidProvider(),
|
||||
TimeProvider.System,
|
||||
NullLogger<ReportEventDispatcher>.Instance);
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var request = new ReportRequestDto
|
||||
|
||||
@@ -39,7 +39,7 @@ rules:
|
||||
|
||||
var hmacKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("scanner-report-hmac-key-2025!"));
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:signing:enabled"] = "true";
|
||||
configuration["scanner:signing:keyId"] = "scanner-report-signing";
|
||||
@@ -47,6 +47,7 @@ rules:
|
||||
configuration["scanner:signing:keyPem"] = hmacKey;
|
||||
configuration["scanner:features:enableSignedReports"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
var store = factory.Services.GetRequiredService<PolicySnapshotStore>();
|
||||
await store.SaveAsync(
|
||||
@@ -110,7 +111,8 @@ rules:
|
||||
[Fact]
|
||||
public async Task ReportsEndpointValidatesDigest()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new ReportRequestDto
|
||||
@@ -127,7 +129,8 @@ rules:
|
||||
[Fact]
|
||||
public async Task ReportsEndpointReturnsServiceUnavailableWhenPolicyMissing()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new ReportRequestDto
|
||||
@@ -155,7 +158,7 @@ rules:
|
||||
action: block
|
||||
""";
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configuration =>
|
||||
{
|
||||
configuration["scanner:signing:enabled"] = "true";
|
||||
@@ -176,6 +179,7 @@ rules:
|
||||
services.AddSingleton<RecordingPlatformEventPublisher>();
|
||||
services.AddSingleton<IPlatformEventPublisher>(sp => sp.GetRequiredService<RecordingPlatformEventPublisher>());
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
var store = factory.Services.GetRequiredService<PolicySnapshotStore>();
|
||||
var saveResult = await store.SaveAsync(
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
@@ -28,6 +29,7 @@ public sealed class RichGraphAttestationServiceTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly RichGraphAttestationService _service;
|
||||
private readonly IGuidProvider _guidProvider = new SequentialGuidProvider();
|
||||
|
||||
public RichGraphAttestationServiceTests()
|
||||
{
|
||||
@@ -302,7 +304,7 @@ public sealed class RichGraphAttestationServiceTests
|
||||
public async Task GetAttestationAsync_NonExistentAttestation_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = await _service.GetAttestationAsync(ScanId.New(), "nonexistent-graph");
|
||||
var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), "nonexistent-graph");
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
@@ -317,7 +319,7 @@ public sealed class RichGraphAttestationServiceTests
|
||||
await _service.CreateAttestationAsync(input);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetAttestationAsync(ScanId.New(), input.GraphId);
|
||||
var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), input.GraphId);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
@@ -396,7 +398,7 @@ public sealed class RichGraphAttestationServiceTests
|
||||
{
|
||||
return new RichGraphAttestationInput
|
||||
{
|
||||
ScanId = ScanId.New(),
|
||||
ScanId = ScanId.New(_guidProvider),
|
||||
GraphId = $"richgraph-{Guid.NewGuid():N}",
|
||||
GraphDigest = "sha256:abc123def456789",
|
||||
NodeCount = 1234,
|
||||
|
||||
@@ -29,7 +29,8 @@ public sealed class RubyPackagesEndpointsTests
|
||||
public async Task GetRubyPackagesReturnsNotFoundWhenInventoryMissing()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/scans/scan-ruby-missing/ruby-packages");
|
||||
@@ -46,7 +47,8 @@ public sealed class RubyPackagesEndpointsTests
|
||||
var generatedAt = DateTime.UtcNow.AddMinutes(-10);
|
||||
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using (var serviceScope = factory.Services.CreateScope())
|
||||
{
|
||||
@@ -97,7 +99,8 @@ public sealed class RubyPackagesEndpointsTests
|
||||
var generatedAt = DateTime.UtcNow.AddMinutes(-5);
|
||||
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
|
||||
string? scanId = null;
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
@@ -155,7 +158,8 @@ public sealed class RubyPackagesEndpointsTests
|
||||
const string reference = "ghcr.io/demo/ruby-service:latest";
|
||||
const string digest = "sha512:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd";
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
|
||||
string? scanId = null;
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
@@ -248,10 +252,11 @@ public sealed class RubyPackagesEndpointsTests
|
||||
new EntryTraceNdjsonMetadata("scan-placeholder", digest, generatedAt));
|
||||
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<IEntryTraceResultStore, RecordingEntryTraceResultStore>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
string? canonicalScanId = null;
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
|
||||
@@ -23,7 +23,8 @@ public sealed class RuntimeEndpointsTests
|
||||
[Fact]
|
||||
public async Task RuntimeEventsEndpointPersistsEvents()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new RuntimeEventsIngestRequestDto
|
||||
@@ -62,7 +63,8 @@ public sealed class RuntimeEndpointsTests
|
||||
[Fact]
|
||||
public async Task RuntimeEventsEndpointRejectsUnsupportedSchema()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var envelope = CreateEnvelope("evt-100", schemaVersion: "zastava.runtime.event@v2.0");
|
||||
@@ -80,13 +82,14 @@ public sealed class RuntimeEndpointsTests
|
||||
[Fact]
|
||||
public async Task RuntimeEventsEndpointEnforcesRateLimit()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:runtime:perNodeBurst"] = "1";
|
||||
configuration["scanner:runtime:perNodeEventsPerSecond"] = "1";
|
||||
configuration["scanner:runtime:perTenantBurst"] = "1";
|
||||
configuration["scanner:runtime:perTenantEventsPerSecond"] = "1";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new RuntimeEventsIngestRequestDto
|
||||
@@ -112,10 +115,11 @@ public sealed class RuntimeEndpointsTests
|
||||
[Fact]
|
||||
public async Task RuntimePolicyEndpointReturnsDecisions()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:runtime:policyCacheTtlSeconds"] = "600";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
const string imageDigest = "sha256:deadbeef";
|
||||
|
||||
@@ -170,20 +174,20 @@ rules:
|
||||
|
||||
await links.UpsertAsync(new LinkDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Id = "link-0001",
|
||||
FromType = LinkSourceType.Image,
|
||||
FromDigest = imageDigest,
|
||||
ArtifactId = sbomArtifactId,
|
||||
CreatedAtUtc = DateTime.UtcNow
|
||||
CreatedAtUtc = FixedUtc
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
await links.UpsertAsync(new LinkDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Id = "link-0002",
|
||||
FromType = LinkSourceType.Image,
|
||||
FromDigest = imageDigest,
|
||||
ArtifactId = attestationArtifactId,
|
||||
CreatedAtUtc = DateTime.UtcNow
|
||||
CreatedAtUtc = FixedUtc
|
||||
}, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
@@ -195,7 +199,10 @@ rules:
|
||||
CreateEnvelope("evt-211", imageDigest: imageDigest, buildId: "1122AABBCCDDEEFF00112233445566778899AABB")
|
||||
}
|
||||
};
|
||||
var ingestResponse = await client.PostAsJsonAsync("/api/v1/runtime/events", ingestRequest);
|
||||
var ingestResponse = await client.PostAsJsonAsync(
|
||||
"/api/v1/runtime/events",
|
||||
ingestRequest,
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Accepted, ingestResponse.StatusCode);
|
||||
|
||||
var request = new RuntimePolicyRequestDto
|
||||
@@ -205,7 +212,10 @@ rules:
|
||||
Labels = new Dictionary<string, string> { ["app"] = "api" }
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request);
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/api/v1/policy/runtime",
|
||||
request,
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
@@ -214,7 +224,7 @@ rules:
|
||||
Assert.True(payload is not null, $"Runtime policy response: {raw}");
|
||||
Assert.Equal(600, payload!.TtlSeconds);
|
||||
Assert.NotNull(payload.PolicyRevision);
|
||||
Assert.True(payload.ExpiresAtUtc > DateTimeOffset.UtcNow);
|
||||
Assert.True(payload.ExpiresAtUtc > FixedNow);
|
||||
|
||||
var decision = payload.Results[imageDigest];
|
||||
Assert.Equal("pass", decision.PolicyVerdict);
|
||||
@@ -232,7 +242,6 @@ rules:
|
||||
Assert.NotNull(decision.BuildIds);
|
||||
Assert.Contains("1122aabbccddeeff00112233445566778899aabb", decision.BuildIds!);
|
||||
var metadataString = decision.Metadata;
|
||||
Console.WriteLine($"Runtime policy metadata: {metadataString ?? "<null>"}");
|
||||
Assert.False(string.IsNullOrWhiteSpace(metadataString));
|
||||
using var metadataDocument = JsonDocument.Parse(decision.Metadata!);
|
||||
Assert.True(metadataDocument.RootElement.TryGetProperty("heuristics", out _));
|
||||
@@ -242,7 +251,8 @@ rules:
|
||||
[Fact]
|
||||
public async Task RuntimePolicyEndpointFlagsUnsignedAndMissingSbom()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
const string imageDigest = "sha256:feedface";
|
||||
@@ -268,10 +278,10 @@ rules: []
|
||||
{
|
||||
Namespace = "payments",
|
||||
Images = new[] { imageDigest }
|
||||
});
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var payload = await response.Content.ReadFromJsonAsync<RuntimePolicyResponseDto>();
|
||||
var payload = await response.Content.ReadFromJsonAsync<RuntimePolicyResponseDto>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(payload);
|
||||
var decision = payload!.Results[imageDigest];
|
||||
|
||||
@@ -299,7 +309,8 @@ rules: []
|
||||
[Fact]
|
||||
public async Task RuntimePolicyEndpointValidatesRequest()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new RuntimePolicyRequestDto
|
||||
@@ -307,7 +318,7 @@ rules: []
|
||||
Images = Array.Empty<string>()
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request);
|
||||
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -321,7 +332,7 @@ rules: []
|
||||
var runtimeEvent = new RuntimeEvent
|
||||
{
|
||||
EventId = eventId,
|
||||
When = DateTimeOffset.UtcNow,
|
||||
When = FixedNow,
|
||||
Kind = RuntimeEventKind.ContainerStart,
|
||||
Tenant = "tenant-alpha",
|
||||
Node = "node-a",
|
||||
@@ -363,4 +374,7 @@ rules: []
|
||||
Event = runtimeEvent
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
private static readonly DateTime FixedUtc = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ public sealed class RuntimeReconciliationTests
|
||||
[Fact]
|
||||
public async Task ReconcileEndpoint_WithNoRuntimeEvents_ReturnsNotFound()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new RuntimeReconcileRequestDto
|
||||
@@ -54,12 +55,13 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Ingest runtime event with loaded libraries
|
||||
@@ -104,12 +106,13 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Setup: Create SBOM artifact with components
|
||||
@@ -195,12 +198,13 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
const string imageDigest = "sha256:pathtest123";
|
||||
@@ -281,12 +285,13 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
const string imageDigest = "sha256:eventidtest";
|
||||
@@ -368,7 +373,8 @@ public sealed class RuntimeReconciliationTests
|
||||
[Fact]
|
||||
public async Task ReconcileEndpoint_WithNonExistentEventId_ReturnsNotFound()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new RuntimeReconcileRequestDto
|
||||
@@ -390,7 +396,8 @@ public sealed class RuntimeReconciliationTests
|
||||
[Fact]
|
||||
public async Task ReconcileEndpoint_WithMissingImageDigest_ReturnsBadRequest()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new RuntimeReconcileRequestDto
|
||||
@@ -409,12 +416,13 @@ public sealed class RuntimeReconciliationTests
|
||||
{
|
||||
var mockObjectStore = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
const string imageDigest = "sha256:mixedtest";
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed class SbomEndpointsTests
|
||||
public async Task SubmitSbomAcceptsCycloneDxJson()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
}, configureServices: services =>
|
||||
@@ -27,6 +27,7 @@ public sealed class SbomEndpointsTests
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
services.AddSingleton<IArtifactObjectStore>(new InMemoryArtifactObjectStore());
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var scanId = await CreateScanAsync(client);
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class SbomUploadEndpointsTests
|
||||
public async Task Upload_accepts_cyclonedx_fixture_and_returns_record()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new SbomUploadRequestDto
|
||||
@@ -64,7 +64,7 @@ public sealed class SbomUploadEndpointsTests
|
||||
public async Task Upload_accepts_spdx_fixture_and_reports_quality_score()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new SbomUploadRequestDto
|
||||
@@ -90,7 +90,7 @@ public sealed class SbomUploadEndpointsTests
|
||||
public async Task Upload_rejects_unknown_format()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = CreateFactory();
|
||||
await using var factory = await CreateFactoryAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var invalid = new SbomUploadRequestDto
|
||||
@@ -103,9 +103,9 @@ public sealed class SbomUploadEndpointsTests
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
private static ScannerApplicationFactory CreateFactory()
|
||||
private static async Task<ScannerApplicationFactory> CreateFactoryAsync()
|
||||
{
|
||||
return new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
}, configureServices: services =>
|
||||
@@ -113,6 +113,9 @@ public sealed class SbomUploadEndpointsTests
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
services.AddSingleton<IArtifactObjectStore>(new InMemoryArtifactObjectStore());
|
||||
});
|
||||
|
||||
await factory.InitializeAsync();
|
||||
return factory;
|
||||
}
|
||||
|
||||
private static string LoadFixtureBase64(string fileName)
|
||||
|
||||
@@ -21,12 +21,12 @@ using StellaOps.Scanner.Triage;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.WebService.Diagnostics;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceStatus>
|
||||
public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceStatus>, IAsyncLifetime, IAsyncDisposable
|
||||
{
|
||||
private readonly ScannerWebServicePostgresFixture? postgresFixture;
|
||||
private readonly bool skipPostgres;
|
||||
private readonly Dictionary<string, string?> configuration = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -53,6 +53,10 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
|
||||
private Action<IDictionary<string, string?>>? configureConfiguration;
|
||||
private Action<IServiceCollection>? configureServices;
|
||||
private bool useTestAuthentication;
|
||||
private ScannerWebServicePostgresFixture? postgresFixture;
|
||||
private Task? initializationTask;
|
||||
private bool initialized;
|
||||
private bool disposed;
|
||||
|
||||
public ScannerApplicationFactory() : this(skipPostgres: false)
|
||||
{
|
||||
@@ -61,25 +65,16 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
|
||||
private ScannerApplicationFactory(bool skipPostgres)
|
||||
{
|
||||
this.skipPostgres = skipPostgres;
|
||||
initialized = skipPostgres;
|
||||
|
||||
if (!skipPostgres)
|
||||
{
|
||||
postgresFixture = new ScannerWebServicePostgresFixture();
|
||||
postgresFixture.InitializeAsync().GetAwaiter().GetResult();
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionBuilder = new NpgsqlConnectionStringBuilder(postgresFixture.ConnectionString)
|
||||
{
|
||||
SearchPath = $"{postgresFixture.SchemaName},public"
|
||||
};
|
||||
configuration["scanner:storage:dsn"] = connectionBuilder.ToString();
|
||||
configuration["scanner:storage:database"] = postgresFixture.SchemaName;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Lightweight mode: use stub connection string
|
||||
configuration["scanner:storage:dsn"] = "Host=localhost;Database=test;";
|
||||
configuration["scanner:storage:database"] = "test";
|
||||
}
|
||||
// Lightweight mode: use stub connection string
|
||||
configuration["scanner:storage:dsn"] = "Host=localhost;Database=test;";
|
||||
configuration["scanner:storage:database"] = "test";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -109,8 +104,62 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
|
||||
return this;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
initializationTask ??= InitializeCoreAsync();
|
||||
return initializationTask;
|
||||
}
|
||||
|
||||
private async Task InitializeCoreAsync()
|
||||
{
|
||||
if (initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipPostgres)
|
||||
{
|
||||
initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
postgresFixture = new ScannerWebServicePostgresFixture();
|
||||
await postgresFixture.InitializeAsync();
|
||||
|
||||
var connectionBuilder = new NpgsqlConnectionStringBuilder(postgresFixture.ConnectionString)
|
||||
{
|
||||
SearchPath = $"{postgresFixture.SchemaName},public"
|
||||
};
|
||||
configuration["scanner:storage:dsn"] = connectionBuilder.ToString();
|
||||
configuration["scanner:storage:database"] = postgresFixture.SchemaName;
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
Task IAsyncLifetime.DisposeAsync() => DisposeAsync().AsTask();
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
base.Dispose();
|
||||
|
||||
if (postgresFixture is not null)
|
||||
{
|
||||
await postgresFixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
if (!initialized)
|
||||
{
|
||||
throw new InvalidOperationException("ScannerApplicationFactory must be initialized via InitializeAsync before use.");
|
||||
}
|
||||
|
||||
configureConfiguration?.Invoke(configuration);
|
||||
|
||||
builder.UseEnvironment("Testing");
|
||||
@@ -200,16 +249,6 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing && postgresFixture is not null)
|
||||
{
|
||||
postgresFixture.DisposeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSurfaceValidatorRunner : ISurfaceValidatorRunner
|
||||
{
|
||||
public ValueTask<SurfaceValidationResult> RunAllAsync(
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
public sealed class ScannerApplicationFixture : IDisposable
|
||||
public sealed class ScannerApplicationFixture : IAsyncLifetime
|
||||
{
|
||||
private ScannerApplicationFactory? _authenticatedFactory;
|
||||
|
||||
@@ -22,10 +23,12 @@ public sealed class ScannerApplicationFixture : IDisposable
|
||||
return client;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public Task InitializeAsync() => Factory.InitializeAsync();
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_authenticatedFactory?.Dispose();
|
||||
Factory.Dispose();
|
||||
_authenticatedFactory = null;
|
||||
await Factory.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,16 @@ public sealed class ScannerSurfaceSecretConfiguratorTests
|
||||
_handles = handles ?? throw new ArgumentNullException(nameof(handles));
|
||||
}
|
||||
|
||||
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
|
||||
{
|
||||
if (_handles.TryGetValue(request.SecretType, out var handle))
|
||||
{
|
||||
return handle;
|
||||
}
|
||||
|
||||
throw new SurfaceSecretNotFoundException(request);
|
||||
}
|
||||
|
||||
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_handles.TryGetValue(request.SecretType, out var handle))
|
||||
|
||||
@@ -16,11 +16,12 @@ public sealed partial class ScansEndpointsTests
|
||||
public async Task EntropyEndpoint_AttachesSnapshot_AndSurfacesInStatus()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
|
||||
{
|
||||
cfg["scanner:authority:enabled"] = "false";
|
||||
cfg["scanner:authority:allowAnonymousFallback"] = "true";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public sealed partial class ScansEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var store = new InMemoryArtifactObjectStore();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configureConfiguration: cfg =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configureConfiguration: cfg =>
|
||||
{
|
||||
cfg["scanner:artifactStore:bucket"] = "replay-bucket";
|
||||
},
|
||||
@@ -36,6 +36,7 @@ public sealed partial class ScansEndpointsTests
|
||||
services.RemoveAll<IArtifactObjectStore>();
|
||||
services.AddSingleton<IArtifactObjectStore>(store);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var submit = await client.PostAsJsonAsync("/api/v1/scans", new { image = new { digest = "sha256:demo" } }, TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -21,10 +21,11 @@ public sealed partial class ScansEndpointsTests
|
||||
public async Task RecordModeService_AttachesReplayAndSurfacedInStatus()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
|
||||
{
|
||||
cfg["scanner:authority:enabled"] = "false";
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var submitResponse = await client.PostAsJsonAsync("/api/v1/scans", new
|
||||
|
||||
@@ -25,7 +25,8 @@ public sealed partial class ScansEndpointsTests
|
||||
public async Task SubmitScanValidatesImageDescriptor()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/scans", new
|
||||
@@ -43,7 +44,7 @@ public sealed partial class ScansEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
RecordingCoordinator coordinator = null!;
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:authority:enabled"] = "false";
|
||||
}, configureServices: services =>
|
||||
@@ -57,6 +58,7 @@ public sealed partial class ScansEndpointsTests
|
||||
return coordinator;
|
||||
});
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
@@ -83,7 +85,7 @@ public sealed partial class ScansEndpointsTests
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
RecordingCoordinator coordinator = null!;
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
|
||||
{
|
||||
configuration["scanner:determinism:feedSnapshotId"] = "feed-2025-11-26";
|
||||
configuration["scanner:determinism:policySnapshotId"] = "rev-42";
|
||||
@@ -98,6 +100,7 @@ public sealed partial class ScansEndpointsTests
|
||||
return coordinator;
|
||||
});
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var request = new ScanSubmitRequest
|
||||
@@ -155,10 +158,11 @@ 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().WithOverrides(configureServices: services =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(storedResult));
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
|
||||
@@ -176,10 +180,11 @@ public sealed partial class ScansEndpointsTests
|
||||
public async Task GetEntryTraceReturnsNotFoundWhenMissing()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(null));
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync("/api/v1/scans/scan-missing/entrytrace");
|
||||
|
||||
@@ -19,13 +19,13 @@ namespace StellaOps.Scanner.WebService.Tests;
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "3401.0002")]
|
||||
public sealed class ScoreReplayEndpointsTests : IDisposable
|
||||
public sealed class ScoreReplayEndpointsTests : IAsyncLifetime
|
||||
{
|
||||
private readonly TestSurfaceSecretsScope _secrets;
|
||||
private readonly ScannerApplicationFactory _factory;
|
||||
private readonly HttpClient _client;
|
||||
private TestSurfaceSecretsScope _secrets = null!;
|
||||
private ScannerApplicationFactory _factory = null!;
|
||||
private HttpClient _client = null!;
|
||||
|
||||
public ScoreReplayEndpointsTests()
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_secrets = new TestSurfaceSecretsScope();
|
||||
_factory = new ScannerApplicationFactory().WithOverrides(cfg =>
|
||||
@@ -33,13 +33,14 @@ public sealed class ScoreReplayEndpointsTests : IDisposable
|
||||
cfg["scanner:authority:enabled"] = "false";
|
||||
cfg["scanner:scoreReplay:enabled"] = "true";
|
||||
});
|
||||
await _factory.InitializeAsync();
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
_factory.Dispose();
|
||||
await _factory.DisposeAsync();
|
||||
_secrets.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,9 @@ public sealed class ScannerAuthorizationTests
|
||||
[InlineData("/api/v1/sbom/upload")]
|
||||
public async Task ProtectedPostEndpoints_RequireAuthentication(string endpoint)
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
useTestAuthentication: true);
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -59,7 +60,8 @@ public sealed class ScannerAuthorizationTests
|
||||
[InlineData("/readyz")]
|
||||
public async Task HealthEndpoints_ArePubliclyAccessible(string endpoint)
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync(endpoint, TestContext.Current.CancellationToken);
|
||||
@@ -81,8 +83,9 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task ExpiredToken_IsRejected()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
useTestAuthentication: true);
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -111,8 +114,9 @@ public sealed class ScannerAuthorizationTests
|
||||
[InlineData("Bearer only-one-part")]
|
||||
public async Task MalformedToken_IsRejected(string token)
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
useTestAuthentication: true);
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
@@ -134,8 +138,9 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task TokenWithWrongIssuer_IsRejected()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
useTestAuthentication: true);
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -160,8 +165,9 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task TokenWithWrongAudience_IsRejected()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
useTestAuthentication: true);
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -190,7 +196,8 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task AnonymousFallback_AllowsAccess_WhenNoAuthConfigured()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
var response = await client.GetAsync("/healthz", TestContext.Current.CancellationToken);
|
||||
@@ -208,8 +215,9 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task AnonymousFallback_DeniesAccess_WhenAuthRequired()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
useTestAuthentication: true);
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -234,8 +242,9 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task WriteOperations_RequireAuthentication()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
useTestAuthentication: true);
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -256,8 +265,9 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task DeleteOperations_RequireAuthentication()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
useTestAuthentication: true);
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -281,7 +291,8 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task RequestWithoutTenant_IsHandledAppropriately()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
// Request without tenant header - use health endpoint
|
||||
@@ -305,7 +316,8 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task Responses_ContainSecurityHeaders()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/healthz", TestContext.Current.CancellationToken);
|
||||
@@ -321,7 +333,8 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task Cors_IsProperlyConfigured()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Options, "/healthz");
|
||||
@@ -349,8 +362,9 @@ public sealed class ScannerAuthorizationTests
|
||||
[Fact]
|
||||
public async Task ValidToken_IsAccepted()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
await using var factory = new ScannerApplicationFactory().WithOverrides(
|
||||
useTestAuthentication: true);
|
||||
await factory.InitializeAsync();
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests
|
||||
[Fact]
|
||||
public void Configure_UsesSurfaceEnvironmentAndCacheRoot()
|
||||
{
|
||||
var cacheRoot = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
|
||||
var cacheRoot = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "stellaops-tests", "surface-manifest"));
|
||||
var settings = new SurfaceEnvironmentSettings(
|
||||
new Uri("https://surface.example"),
|
||||
"surface-bucket",
|
||||
@@ -30,7 +30,7 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests
|
||||
"tenant-a",
|
||||
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()))
|
||||
{
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
CreatedAtUtc = FixedNow
|
||||
};
|
||||
|
||||
var environment = new StubSurfaceEnvironment(settings);
|
||||
@@ -45,6 +45,8 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests
|
||||
Assert.Equal(Path.Combine(cacheRoot.FullName, "manifests"), options.RootDirectory);
|
||||
}
|
||||
|
||||
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private sealed class StubSurfaceEnvironment : ISurfaceEnvironment
|
||||
{
|
||||
public StubSurfaceEnvironment(SurfaceEnvironmentSettings settings)
|
||||
|
||||
@@ -24,7 +24,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetFindingStatus_NotFound_ReturnsNotFound()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/triage/findings/nonexistent-finding");
|
||||
@@ -35,7 +36,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostUpdateStatus_ValidRequest_ReturnsUpdatedStatus()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new UpdateTriageStatusRequestDto
|
||||
@@ -54,7 +56,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostVexStatement_ValidRequest_ReturnsResponse()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new SubmitVexStatementRequestDto
|
||||
@@ -73,7 +76,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostQuery_EmptyFilters_ReturnsResults()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new BulkTriageQueryRequestDto
|
||||
@@ -94,7 +98,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostQuery_WithLaneFilter_FiltersCorrectly()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new BulkTriageQueryRequestDto
|
||||
@@ -114,7 +119,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostQuery_WithVerdictFilter_FiltersCorrectly()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new BulkTriageQueryRequestDto
|
||||
@@ -134,7 +140,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetSummary_ValidDigest_ReturnsSummary()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123");
|
||||
@@ -150,7 +157,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetSummary_IncludesAllLanes()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123");
|
||||
@@ -168,7 +176,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetSummary_IncludesAllVerdicts()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123");
|
||||
@@ -186,7 +195,8 @@ public sealed class TriageStatusEndpointsTests
|
||||
[Fact]
|
||||
public async Task PostQuery_ResponseIncludesSummary()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
await using var factory = new ScannerApplicationFactory();
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var request = new BulkTriageQueryRequestDto
|
||||
|
||||
@@ -185,7 +185,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
DeltaId = "delta-101",
|
||||
PreviousScanId = "scan-099",
|
||||
CurrentScanId = "scan-100",
|
||||
ComparedAt = DateTimeOffset.UtcNow,
|
||||
ComparedAt = FixedNow,
|
||||
Summary = new DeltaSummaryDto
|
||||
{
|
||||
AddedCount = 5,
|
||||
@@ -306,7 +306,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
ComponentPurl = "pkg:npm/test@1.0.0",
|
||||
Manifests = CreateMinimalManifests(),
|
||||
Verification = CreateMinimalVerification(),
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
GeneratedAt = FixedNow,
|
||||
// All tabs null
|
||||
Sbom = null,
|
||||
Reachability = null,
|
||||
@@ -346,7 +346,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
Status = "affected",
|
||||
TrustScore = 1.0,
|
||||
MeetsPolicyThreshold = true,
|
||||
IssuedAt = DateTimeOffset.UtcNow
|
||||
IssuedAt = FixedNow
|
||||
},
|
||||
new VexClaimDto
|
||||
{
|
||||
@@ -355,7 +355,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
Status = "not_affected",
|
||||
TrustScore = 0.95,
|
||||
MeetsPolicyThreshold = true,
|
||||
IssuedAt = DateTimeOffset.UtcNow.AddDays(-1)
|
||||
IssuedAt = FixedNow.AddDays(-1)
|
||||
},
|
||||
new VexClaimDto
|
||||
{
|
||||
@@ -364,12 +364,12 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
Status = "under_investigation",
|
||||
TrustScore = 0.6,
|
||||
MeetsPolicyThreshold = false,
|
||||
IssuedAt = DateTimeOffset.UtcNow.AddDays(-7)
|
||||
IssuedAt = FixedNow.AddDays(-7)
|
||||
}
|
||||
},
|
||||
Manifests = CreateMinimalManifests(),
|
||||
Verification = CreateMinimalVerification(),
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
GeneratedAt = FixedNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
@@ -429,7 +429,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
EvidenceBundleUrl = "https://api.stellaops.local/bundles/bundle-123",
|
||||
Manifests = CreateMinimalManifests(),
|
||||
Verification = CreateMinimalVerification(),
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
GeneratedAt = FixedNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
@@ -455,7 +455,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
AttestationsVerified = true,
|
||||
EvidenceComplete = true,
|
||||
Issues = null,
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = FixedNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
@@ -478,7 +478,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
AttestationsVerified = false,
|
||||
EvidenceComplete = true,
|
||||
Issues = new[] { "Attestation signature verification failed" },
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = FixedNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
@@ -505,7 +505,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
"Attestation not found",
|
||||
"VEX evidence missing"
|
||||
},
|
||||
VerifiedAt = DateTimeOffset.UtcNow
|
||||
VerifiedAt = FixedNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
@@ -747,7 +747,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
EvidenceBundleUrl = "https://api.stellaops.local/bundles/scan-001-finding-001",
|
||||
Manifests = CreateMinimalManifests(),
|
||||
Verification = CreateMinimalVerification(),
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
GeneratedAt = FixedNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
@@ -809,7 +809,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
Status = "not_affected",
|
||||
TrustScore = 0.95,
|
||||
MeetsPolicyThreshold = true,
|
||||
IssuedAt = DateTimeOffset.UtcNow
|
||||
IssuedAt = FixedNow
|
||||
}
|
||||
},
|
||||
Attestations = new[]
|
||||
@@ -827,7 +827,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
DeltaId = "delta-001",
|
||||
PreviousScanId = "scan-099",
|
||||
CurrentScanId = "scan-100",
|
||||
ComparedAt = DateTimeOffset.UtcNow
|
||||
ComparedAt = FixedNow
|
||||
},
|
||||
Policy = new PolicyEvidenceDto
|
||||
{
|
||||
@@ -838,7 +838,7 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
Manifests = CreateMinimalManifests(),
|
||||
Verification = CreateMinimalVerification(),
|
||||
ReplayCommand = "stellaops replay --target pkg:npm/lodash@4.17.21 --verify",
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
GeneratedAt = FixedNow
|
||||
};
|
||||
|
||||
private static string DetermineVerificationStatus(
|
||||
@@ -852,6 +852,8 @@ public sealed class UnifiedEvidenceServiceTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,12 +24,13 @@ public sealed class VexGateEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetGatePolicy_ReturnsPolicy()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexGateQueryService>();
|
||||
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/gate-policy");
|
||||
@@ -44,12 +45,13 @@ public sealed class VexGateEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetGatePolicy_WithTenantId_ReturnsPolicy()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexGateQueryService>();
|
||||
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/gate-policy?tenantId=tenant-a");
|
||||
@@ -62,12 +64,13 @@ public sealed class VexGateEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetGateResults_WhenScanNotFound_Returns404()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexGateQueryService>();
|
||||
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/scan-not-exists/gate-results");
|
||||
@@ -78,16 +81,17 @@ public sealed class VexGateEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetGateResults_WhenScanExists_ReturnsResults()
|
||||
{
|
||||
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
||||
var scanId = "scan-0001";
|
||||
var mockService = new InMemoryVexGateQueryService();
|
||||
mockService.AddScanResult(scanId, CreateTestGateResults(scanId));
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexGateQueryService>();
|
||||
services.AddSingleton<IVexGateQueryService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/{scanId}/gate-results");
|
||||
@@ -103,16 +107,17 @@ public sealed class VexGateEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetGateResults_WithDecisionFilter_ReturnsFilteredResults()
|
||||
{
|
||||
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
||||
var scanId = "scan-0002";
|
||||
var mockService = new InMemoryVexGateQueryService();
|
||||
mockService.AddScanResult(scanId, CreateTestGateResults(scanId, blockedCount: 3, warnCount: 5, passCount: 10));
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexGateQueryService>();
|
||||
services.AddSingleton<IVexGateQueryService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/{scanId}/gate-results?decision=Block");
|
||||
@@ -126,12 +131,13 @@ public sealed class VexGateEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetGateSummary_WhenScanNotFound_Returns404()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexGateQueryService>();
|
||||
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/scan-not-exists/gate-summary");
|
||||
@@ -142,16 +148,17 @@ public sealed class VexGateEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetGateSummary_WhenScanExists_ReturnsSummary()
|
||||
{
|
||||
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
||||
var scanId = "scan-0003";
|
||||
var mockService = new InMemoryVexGateQueryService();
|
||||
mockService.AddScanResult(scanId, CreateTestGateResults(scanId, blockedCount: 2, warnCount: 8, passCount: 40));
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexGateQueryService>();
|
||||
services.AddSingleton<IVexGateQueryService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/{scanId}/gate-summary");
|
||||
@@ -168,12 +175,13 @@ public sealed class VexGateEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetBlockedFindings_WhenScanNotFound_Returns404()
|
||||
{
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexGateQueryService>();
|
||||
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/scan-not-exists/gate-blocked");
|
||||
@@ -184,16 +192,17 @@ public sealed class VexGateEndpointsTests
|
||||
[Fact]
|
||||
public async Task GetBlockedFindings_WhenScanExists_ReturnsOnlyBlocked()
|
||||
{
|
||||
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
||||
var scanId = "scan-0004";
|
||||
var mockService = new InMemoryVexGateQueryService();
|
||||
mockService.AddScanResult(scanId, CreateTestGateResults(scanId, blockedCount: 5, warnCount: 10, passCount: 20));
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
await using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<IVexGateQueryService>();
|
||||
services.AddSingleton<IVexGateQueryService>(mockService);
|
||||
});
|
||||
await factory.InitializeAsync();
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync($"{BasePath}/{scanId}/gate-blocked");
|
||||
@@ -238,7 +247,7 @@ public sealed class VexGateEndpointsTests
|
||||
Passed = passCount,
|
||||
Warned = warnCount,
|
||||
Blocked = blockedCount,
|
||||
EvaluatedAt = DateTimeOffset.UtcNow,
|
||||
EvaluatedAt = FixedNow,
|
||||
},
|
||||
GatedFindings = findings,
|
||||
};
|
||||
@@ -248,7 +257,7 @@ public sealed class VexGateEndpointsTests
|
||||
{
|
||||
return new GatedFindingDto
|
||||
{
|
||||
FindingId = $"finding-{Guid.NewGuid():N}",
|
||||
FindingId = $"finding-{cve.ToLowerInvariant()}",
|
||||
Cve = cve,
|
||||
Purl = purl,
|
||||
Decision = decision,
|
||||
@@ -269,6 +278,8 @@ public sealed class VexGateEndpointsTests
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -486,6 +486,22 @@ public sealed class EntryTraceExecutionServiceTests : IDisposable
|
||||
_throwOnMissing = throwOnMissing;
|
||||
}
|
||||
|
||||
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
|
||||
{
|
||||
var key = (request.SecretType, request.Name ?? string.Empty);
|
||||
if (_secrets.TryGetValue(key, out var payload))
|
||||
{
|
||||
return SurfaceSecretHandle.FromBytes(payload);
|
||||
}
|
||||
|
||||
if (_throwOnMissing)
|
||||
{
|
||||
throw new SurfaceSecretNotFoundException(request);
|
||||
}
|
||||
|
||||
return SurfaceSecretHandle.Empty;
|
||||
}
|
||||
|
||||
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = (request.SecretType, request.Name ?? string.Empty);
|
||||
|
||||
@@ -145,6 +145,12 @@ public sealed class RegistrySecretStageExecutorTests
|
||||
_json = json;
|
||||
}
|
||||
|
||||
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(_json);
|
||||
return SurfaceSecretHandle.FromBytes(bytes);
|
||||
}
|
||||
|
||||
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(_json);
|
||||
@@ -154,6 +160,9 @@ public sealed class RegistrySecretStageExecutorTests
|
||||
|
||||
private sealed class MissingSecretProvider : ISurfaceSecretProvider
|
||||
{
|
||||
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
|
||||
=> throw new SurfaceSecretNotFoundException(request);
|
||||
|
||||
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
|
||||
=> throw new SurfaceSecretNotFoundException(request);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ public sealed class ScannerStorageSurfaceSecretConfiguratorTests
|
||||
_handle = handle;
|
||||
}
|
||||
|
||||
public SurfaceSecretHandle Get(SurfaceSecretRequest request) => _handle;
|
||||
|
||||
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
|
||||
=> ValueTask.FromResult(_handle);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user