up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 07:47:08 +02:00
parent 56e2f64d07
commit 1c782897f7
184 changed files with 8991 additions and 649 deletions

View File

@@ -0,0 +1,40 @@
using System;
using System.Linq;
using StellaOps.Scanner.Core.Entropy;
using Xunit;
namespace StellaOps.Scanner.Core.Tests.Entropy;
public class EntropyCalculatorTests
{
[Fact]
public void Compute_ReturnsEmpty_WhenBufferTooSmall()
{
var result = EntropyCalculator.Compute(new byte[10], windowSize: 32, stride: 8);
Assert.Empty(result);
}
[Fact]
public void Compute_ProducesZeroEntropy_ForConstantData()
{
var data = Enumerable.Repeat((byte)0xAA, 4096 * 2).ToArray();
var windows = EntropyCalculator.Compute(data, windowSize: 4096, stride: 1024);
Assert.NotEmpty(windows);
Assert.All(windows, w => Assert.InRange(w.Entropy, 0, 0.0001));
}
[Fact]
public void Compute_DetectsHighEntropy_ForRandomBytes()
{
var rng = new Random(1234);
var data = new byte[8192];
rng.NextBytes(data);
var windows = EntropyCalculator.Compute(data, windowSize: 4096, stride: 1024);
Assert.NotEmpty(windows);
Assert.All(windows, w => Assert.InRange(w.Entropy, 7.0, 8.1));
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Linq;
using StellaOps.Scanner.Core.Entropy;
using Xunit;
namespace StellaOps.Scanner.Core.Tests.Entropy;
public class EntropyReportBuilderTests
{
[Fact]
public void BuildFile_FlagsOpaqueHigh_WhenRatioExceedsThreshold()
{
var builder = new EntropyReportBuilder(windowSize: 4, stride: 4, opaqueThreshold: 1.0, opaqueFileRatioFlag: 0.25);
// Alternating bytes produce high entropy in every window.
var data = Enumerable.Range(0, 64).Select(i => (byte)(i % 2)).ToArray();
var report = builder.BuildFile("/bin/demo", data);
Assert.Contains("opaque-high", report.Flags);
Assert.True(report.OpaqueRatio > 0.25);
}
[Fact]
public void BuildFile_RespectsProvidedFlags()
{
var builder = new EntropyReportBuilder(windowSize: 8, stride: 8, opaqueThreshold: 7.0, opaqueFileRatioFlag: 0.90);
var data = new byte[64];
var report = builder.BuildFile("/bin/zero", data, new[] { "stripped", "", "debug-missing" });
Assert.Contains("stripped", report.Flags);
Assert.Contains("debug-missing", report.Flags);
}
[Fact]
public void BuildLayerSummary_ComputesRatios()
{
var builder = new EntropyReportBuilder(windowSize: 4, stride: 4, opaqueThreshold: 1.0, opaqueFileRatioFlag: 0.25);
var data = Enumerable.Range(0, 64).Select(i => (byte)(i % 2)).ToArray();
var file = builder.BuildFile("/bin/demo", data);
var (summary, imageRatio) = builder.BuildLayerSummary(
"sha256:layer",
new[] { file },
layerTotalBytes: 64,
imageOpaqueBytes: file.OpaqueBytes,
imageTotalBytes: 128);
Assert.Equal("sha256:layer", summary.LayerDigest);
Assert.InRange(summary.OpaqueRatio, 0.25, 1.0);
Assert.InRange(imageRatio, 0.0, 1.0);
}
}

View File

@@ -0,0 +1,56 @@
using System;
using FluentAssertions;
using StellaOps.Replay.Core;
using StellaOps.Scanner.Core.Replay;
using Xunit;
namespace StellaOps.Scanner.Core.Tests.Replay;
public sealed class RecordModeAssemblerTests
{
[Fact]
public void BuildRun_ComputesManifestHashAndOutputs()
{
var manifest = new ReplayManifest
{
Scan = new ReplayScanMetadata { Id = "scan-1", Time = DateTimeOffset.UnixEpoch }
};
var assembler = new RecordModeAssembler(new FixedTimeProvider(new DateTimeOffset(2025, 11, 25, 12, 0, 0, TimeSpan.Zero)));
var run = assembler.BuildRun("scan-1", manifest, "sha256:sbom", "findings-digest", vexDigest: "sha256:vex");
run.Id.Should().Be("scan-1");
run.ManifestHash.Should().StartWith("sha256:");
run.CreatedAt.Should().Be(new DateTime(2025, 11, 25, 12, 0, 0, DateTimeKind.Utc));
run.Outputs.Sbom.Should().Be("sha256:sbom");
run.Outputs.Findings.Should().Be("sha256:findings-digest");
run.Outputs.Vex.Should().Be("sha256:vex");
run.Status.Should().Be("pending");
}
[Fact]
public void BuildBundles_ProducesDeterministicRecords()
{
var assembler = new RecordModeAssembler(new FixedTimeProvider(DateTimeOffset.UnixEpoch));
var input = new ReplayBundleWriteResult("tar1", "z1", 10, 20, "cas://replay/zz/z1.tar.zst");
var output = new ReplayBundleWriteResult("tar2", "z2", 30, 40, "cas://replay/aa/z2.tar.zst");
var bundles = assembler.BuildBundles(input, output);
bundles.Should().HaveCount(2);
bundles[0].Id.Should().Be("z1");
bundles[0].Type.Should().Be("input");
bundles[1].Id.Should().Be("z2");
bundles[1].Location.Should().Be("cas://replay/aa/z2.tar.zst");
bundles[0].CreatedAt.Should().Be(DateTime.UnixEpoch);
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _utc;
public FixedTimeProvider(DateTimeOffset utc) => _utc = utc;
public override DateTimeOffset GetUtcNow() => _utc;
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Replay.Core;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Replay;
using StellaOps.Scanner.WebService.Services;
using Xunit;
namespace StellaOps.Scanner.WebService.Tests;
public sealed partial class ScansEndpointsTests
{
[Fact]
public async Task RecordModeService_AttachesReplayAndSurfacedInStatus()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory(cfg =>
{
cfg["scanner:authority:enabled"] = "false";
});
using var client = factory.CreateClient();
var submitResponse = await client.PostAsJsonAsync("/api/v1/scans", new
{
image = new { digest = "sha256:demo" }
});
submitResponse.EnsureSuccessStatusCode();
var submitPayload = await submitResponse.Content.ReadFromJsonAsync<ScanSubmitResponse>();
Assert.NotNull(submitPayload);
var scanId = submitPayload!.ScanId;
using var scope = factory.Services.CreateScope();
var coordinator = scope.ServiceProvider.GetRequiredService<IScanCoordinator>();
var recordMode = scope.ServiceProvider.GetRequiredService<IRecordModeService>();
var timeProvider = scope.ServiceProvider.GetRequiredService<TimeProvider>();
var manifest = new ReplayManifest
{
Scan = new ReplayScanMetadata
{
Id = scanId,
Time = timeProvider.GetUtcNow()
}
};
var replay = await recordMode.AttachAsync(
new ScanId(scanId),
manifest,
new ReplayBundleWriteResult("tar1", "z1", 128, 64, "cas://replay/z1.tar.zst"),
new ReplayBundleWriteResult("tar2", "z2", 256, 96, "cas://replay/z2.tar.zst"),
sbomDigest: "sha256:sbom",
findingsDigest: "findings-digest",
coordinator: coordinator,
additionalBundles: new[]
{
(new ReplayBundleWriteResult("tar3", "z3", 1, 2, "cas://replay/z3.tar.zst"), "reachability")
});
Assert.NotNull(replay);
var status = await client.GetFromJsonAsync<ScanStatusResponse>($"/api/v1/scans/{scanId}");
Assert.NotNull(status);
Assert.NotNull(status!.Replay);
Assert.Equal(replay!.ManifestHash, status.Replay!.ManifestHash);
Assert.Equal(3, status.Replay!.Bundles.Count);
Assert.Contains(status.Replay!.Bundles, b => b.Type == "reachability");
Assert.All(status.Replay!.Bundles, b => Assert.StartsWith("sha256:", b.Digest, StringComparison.Ordinal));
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.Entropy;
using StellaOps.Scanner.Worker.Processing;
using StellaOps.Scanner.Worker.Processing.Entropy;
using Xunit;
namespace StellaOps.Scanner.Worker.Tests;
public class EntropyStageExecutorTests
{
[Fact]
public async Task ExecuteAsync_WritesEntropyReportAndSummary()
{
// Arrange: create a temp file with random bytes to yield high entropy.
var tmp = Path.GetTempFileName();
var rng = new Random(1234);
var bytes = new byte[64 * 1024];
rng.NextBytes(bytes);
File.WriteAllBytes(tmp, bytes);
var fileEntries = new List<ScanFileEntry>
{
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");
var context = new ScanJobContext(lease, TimeProvider.System, DateTimeOffset.UtcNow, CancellationToken.None);
context.Analysis.Set(ScanAnalysisKeys.FileEntries, (IReadOnlyList<ScanFileEntry>)fileEntries);
var executor = new EntropyStageExecutor(NullLogger<EntropyStageExecutor>.Instance);
// Act
await executor.ExecuteAsync(context, CancellationToken.None);
// Assert
Assert.True(context.Analysis.TryGet<EntropyReport>(ScanAnalysisKeys.EntropyReport, out var report));
Assert.NotNull(report);
Assert.Equal("sha256:layer", report!.LayerDigest);
Assert.NotEmpty(report.Files);
Assert.True(context.Analysis.TryGet<EntropyLayerSummary>(ScanAnalysisKeys.EntropyLayerSummary, out var summary));
Assert.NotNull(summary);
Assert.Equal("sha256:layer", summary!.LayerDigest);
}
private sealed class StubLease : IScanJobLease
{
public StubLease(string jobId, string scanId, string imageDigest, string layerDigest)
{
JobId = jobId;
ScanId = scanId;
ImageDigest = imageDigest;
LayerDigest = layerDigest;
}
public string JobId { get; }
public string ScanId { get; }
public string? ImageDigest { get; }
public string? LayerDigest { get; }
}
}