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