up
Some checks failed
api-governance / spectral-lint (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 20:23:28 +02:00
parent 4831c7fcb0
commit d63af51f84
139 changed files with 8010 additions and 2795 deletions

View File

@@ -0,0 +1,54 @@
using System;
using System.Net;
using System.Net.Http.Json;
using System.Threading.Tasks;
using StellaOps.Scanner.WebService.Contracts;
using Xunit;
namespace StellaOps.Scanner.WebService.Tests;
public sealed partial class ScansEndpointsTests
{
[Fact]
public async Task EntropyEndpoint_AttachesSnapshot_AndSurfacesInStatus()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory(cfg =>
{
cfg["scanner:authority:enabled"] = "false";
cfg["scanner:authority:allowAnonymousFallback"] = "true";
});
using var client = factory.CreateClient();
var submitResponse = await client.PostAsJsonAsync("/api/v1/scans", new
{
image = new { digest = "sha256:image-demo" }
});
submitResponse.EnsureSuccessStatusCode();
var submit = await submitResponse.Content.ReadFromJsonAsync<ScanSubmitResponse>();
Assert.NotNull(submit);
var entropyPayload = new EntropyIngestRequest(
ImageOpaqueRatio: 0.42,
Layers: new[]
{
new EntropyLayerRequest("sha256:layer-demo", 0.35, 3500, 10_000)
});
var attachResponse = await client.PostAsJsonAsync($"/api/v1/scans/{submit!.ScanId}/entropy", entropyPayload);
Assert.Equal(HttpStatusCode.Accepted, attachResponse.StatusCode);
var status = await client.GetFromJsonAsync<ScanStatusResponse>($"/api/v1/scans/{submit.ScanId}");
Assert.NotNull(status);
Assert.NotNull(status!.Entropy);
Assert.Equal(0.42, status.Entropy!.ImageOpaqueRatio, 3);
Assert.Single(status.Entropy!.Layers);
var layer = status.Entropy!.Layers[0];
Assert.Equal("sha256:layer-demo", layer.LayerDigest);
Assert.Equal(0.35, layer.OpaqueRatio, 3);
Assert.Equal(3500, layer.OpaqueBytes);
Assert.Equal(10_000, layer.TotalBytes);
}
}

View File

@@ -167,6 +167,12 @@ public sealed partial class ScansEndpointsTests
public ValueTask<ScanSnapshot?> TryFindByTargetAsync(string? reference, string? digest, CancellationToken cancellationToken)
=> _inner.TryFindByTargetAsync(reference, digest, cancellationToken);
public ValueTask<bool> AttachReplayAsync(ScanId scanId, ReplayArtifacts replay, CancellationToken cancellationToken)
=> _inner.AttachReplayAsync(scanId, replay, cancellationToken);
public ValueTask<bool> AttachEntropyAsync(ScanId scanId, EntropySnapshot entropy, CancellationToken cancellationToken)
=> _inner.AttachEntropyAsync(scanId, entropy, cancellationToken);
}
private sealed class StubEntryTraceResultStore : IEntryTraceResultStore

View File

@@ -0,0 +1,30 @@
using System;
using StellaOps.Scanner.Worker.Determinism;
using Xunit;
namespace StellaOps.Scanner.Worker.Tests.Determinism;
public class DeterministicTimeProviderTests
{
[Fact]
public void GetUtcNow_ReturnsFixedInstant()
{
var fixedInstant = new DateTimeOffset(2024, 01, 01, 12, 0, 0, TimeSpan.Zero);
var provider = new DeterministicTimeProvider(fixedInstant);
Assert.Equal(fixedInstant, provider.GetUtcNow());
}
[Fact]
public void DeterministicRandomProvider_ReturnsStableSequence_WhenSeeded()
{
var provider = new DeterministicRandomProvider(1234);
var rng1 = provider.Create();
var rng2 = provider.Create();
var seq1 = new[] { rng1.Next(), rng1.Next(), rng1.Next() };
var seq2 = new[] { rng2.Next(), rng2.Next(), rng2.Next() };
Assert.Equal(seq1, seq2);
}
}

View File

@@ -26,7 +26,7 @@ public class EntropyStageExecutorTests
var fileEntries = new List<ScanFileEntry>
{
new ScanFileEntry(tmp, sizeBytes: bytes.LongLength, kind: "blob", metadata: new Dictionary<string, string>())
new ScanFileEntry(tmp, SizeBytes: bytes.LongLength, Kind: "blob", Metadata: new Dictionary<string, string>())
};
var lease = new StubLease("job-1", "scan-1", imageDigest: "sha256:test", layerDigest: "sha256:layer");
@@ -55,13 +55,25 @@ public class EntropyStageExecutorTests
{
JobId = jobId;
ScanId = scanId;
ImageDigest = imageDigest;
LayerDigest = layerDigest;
Metadata = new Dictionary<string, string>
{
["image.digest"] = imageDigest,
["layerDigest"] = layerDigest
};
}
public string JobId { get; }
public string ScanId { get; }
public string? ImageDigest { get; }
public string? LayerDigest { get; }
public int Attempt => 1;
public DateTimeOffset EnqueuedAtUtc => DateTimeOffset.UtcNow;
public DateTimeOffset LeasedAtUtc => DateTimeOffset.UtcNow;
public TimeSpan LeaseDuration => TimeSpan.FromMinutes(5);
public IReadOnlyDictionary<string, string> Metadata { get; }
public ValueTask RenewAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask CompleteAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask AbandonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask PoisonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}

View File

@@ -15,6 +15,7 @@ using Microsoft.Extensions.Options;
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Ruby;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.Entropy;
using StellaOps.Scanner.EntryTrace;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.FS;
@@ -120,6 +121,62 @@ public sealed class SurfaceManifestStageExecutorTests
Assert.Contains(payloadMetrics, m => Equals("layer.fragments", m["surface.kind"]));
}
[Fact]
public async Task ExecuteAsync_IncludesEntropyPayloads_WhenPresent()
{
var metrics = new ScannerWorkerMetrics();
var publisher = new TestSurfaceManifestPublisher("tenant-a");
var cache = new RecordingSurfaceCache();
var environment = new TestSurfaceEnvironment("tenant-a");
var hash = CreateCryptoHash();
var executor = new SurfaceManifestStageExecutor(
publisher,
cache,
environment,
metrics,
NullLogger<SurfaceManifestStageExecutor>.Instance,
hash,
new NullRubyPackageInventoryStore());
var context = CreateContext();
var entropyReport = new EntropyReport(
ImageDigest: "sha256:image",
LayerDigest: "sha256:layer",
Files: new[]
{
new EntropyFileReport(
Path: "/bin/app",
Size: 1024 * 32,
OpaqueBytes: 1024 * 8,
OpaqueRatio: 0.25,
Flags: Array.Empty<string>(),
Windows: Array.Empty<EntropyFileWindow>())
},
ImageOpaqueRatio: 0.2);
var entropySummary = new EntropyLayerSummary(
LayerDigest: "sha256:layer",
OpaqueBytes: 1024 * 8,
TotalBytes: 1024 * 32,
OpaqueRatio: 0.25,
Indicators: Array.Empty<string>());
context.Analysis.Set(ScanAnalysisKeys.EntropyReport, entropyReport);
context.Analysis.Set(ScanAnalysisKeys.EntropyLayerSummary, entropySummary);
await executor.ExecuteAsync(context, CancellationToken.None);
Assert.Equal(1, publisher.PublishCalls);
Assert.NotNull(publisher.LastRequest);
Assert.Contains(publisher.LastRequest!.Payloads, p => p.Kind == "entropy.report");
Assert.Contains(publisher.LastRequest!.Payloads, p => p.Kind == "entropy.layer-summary");
// Two payloads + manifest persisted to cache.
Assert.Equal(3, cache.Entries.Count);
}
private static ScanJobContext CreateContext()
{
var lease = new FakeJobLease();