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,141 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Core.Entropy;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Worker.Utilities;
namespace StellaOps.Scanner.Worker.Processing.Entropy;
/// <summary>
/// Computes entropy reports for executable/blobs and stores them in the analysis store
/// for downstream evidence emission.
/// </summary>
public sealed class EntropyStageExecutor : IScanStageExecutor
{
private readonly ILogger<EntropyStageExecutor> _logger;
private readonly EntropyReportBuilder _reportBuilder;
public EntropyStageExecutor(ILogger<EntropyStageExecutor> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_reportBuilder = new EntropyReportBuilder();
}
public string StageName => ScanStageNames.EmitReports;
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
// Expect analyzer stage to have populated filesystem snapshots.
if (!context.Analysis.TryGet<IReadOnlyList<ScanFileEntry>>(ScanAnalysisKeys.FileEntries, out var files) || files is null)
{
_logger.LogDebug("No file entries available; skipping entropy analysis.");
return;
}
var reports = new List<EntropyFileReport>();
foreach (var file in files)
{
if (!ShouldAnalyze(file))
{
continue;
}
cancellationToken.ThrowIfCancellationRequested();
try
{
var data = await ReadFileAsync(file.Path, cancellationToken).ConfigureAwait(false);
var flags = DeriveFlags(file);
var report = _reportBuilder.BuildFile(file.Path, data, flags);
reports.Add(report);
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
{
_logger.LogDebug(ex, "Skipping entropy for {Path}: {Reason}", file.Path, ex.Message);
}
}
if (reports.Count == 0)
{
_logger.LogDebug("Entropy analysis produced no reports.");
return;
}
var layerDigest = context.Lease.LayerDigest ?? string.Empty;
var layerSize = files.Sum(f => f.SizeBytes);
var imageOpaqueBytes = reports.Sum(r => r.OpaqueBytes);
var imageTotalBytes = files.Sum(f => f.SizeBytes);
var (summary, imageRatio) = _reportBuilder.BuildLayerSummary(
layerDigest,
reports,
layerSize,
imageOpaqueBytes,
imageTotalBytes);
var entropyReport = new EntropyReport(
ImageDigest: context.Lease.ImageDigest ?? string.Empty,
LayerDigest: layerDigest,
Files: reports,
ImageOpaqueRatio: imageRatio);
context.Analysis.Set(ScanAnalysisKeys.EntropyReport, entropyReport);
context.Analysis.Set(ScanAnalysisKeys.EntropyLayerSummary, summary);
_logger.LogInformation(
"Entropy report captured for layer {Layer}: opaqueBytes={OpaqueBytes} ratio={Ratio:F2}",
layerDigest,
summary.OpaqueBytes,
summary.OpaqueRatio);
}
private static bool ShouldAnalyze(ScanFileEntry file)
{
if (file is null || file.SizeBytes < 16 * 1024)
{
return false;
}
return file.Kind switch
{
"elf" => true,
"pe" => true,
"mach-o" => true,
"blob" => true,
_ => false
};
}
private static IEnumerable<string> DeriveFlags(ScanFileEntry file)
{
if (file?.Metadata is null)
{
yield break;
}
if (file.Metadata.TryGetValue("stripped", out var stripped) && stripped == "true")
{
yield return "stripped";
}
if (file.Metadata.TryGetValue("packer", out var packer) && !string.IsNullOrWhiteSpace(packer))
{
yield return $"packer:{packer}";
}
}
private static async Task<byte[]> ReadFileAsync(string path, CancellationToken cancellationToken)
{
await using var stream = File.OpenRead(path);
using var buffer = new MemoryStream();
await stream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
return buffer.ToArray();
}
}

View File

@@ -57,9 +57,9 @@ public sealed class ScanJobProcessor
foreach (var stage in ScanStageNames.Ordered)
{
cancellationToken.ThrowIfCancellationRequested();
if (!_executors.TryGetValue(stage, out var executor))
cancellationToken.ThrowIfCancellationRequested();
if (!_executors.TryGetValue(stage, out var executor))
{
continue;
}

View File

@@ -5,19 +5,21 @@ namespace StellaOps.Scanner.Worker.Processing;
public static class ScanStageNames
{
public const string ResolveImage = "resolve-image";
public const string PullLayers = "pull-layers";
public const string BuildFilesystem = "build-filesystem";
public const string ExecuteAnalyzers = "execute-analyzers";
public const string ComposeArtifacts = "compose-artifacts";
public const string EmitReports = "emit-reports";
public static readonly IReadOnlyList<string> Ordered = new[]
{
ResolveImage,
PullLayers,
BuildFilesystem,
ExecuteAnalyzers,
ComposeArtifacts,
EmitReports,
};
}
public const string PullLayers = "pull-layers";
public const string BuildFilesystem = "build-filesystem";
public const string ExecuteAnalyzers = "execute-analyzers";
public const string ComposeArtifacts = "compose-artifacts";
public const string EmitReports = "emit-reports";
public const string Entropy = "entropy";
public static readonly IReadOnlyList<string> Ordered = new[]
{
ResolveImage,
PullLayers,
BuildFilesystem,
ExecuteAnalyzers,
ComposeArtifacts,
Entropy,
EmitReports,
};
}

View File

@@ -85,6 +85,7 @@ builder.Services.AddSingleton<IScanStageExecutor, RegistrySecretStageExecutor>()
builder.Services.AddSingleton<IScanStageExecutor, AnalyzerStageExecutor>();
builder.Services.AddSingleton<IScanStageExecutor, Reachability.ReachabilityBuildStageExecutor>();
builder.Services.AddSingleton<IScanStageExecutor, Reachability.ReachabilityPublishStageExecutor>();
builder.Services.AddSingleton<IScanStageExecutor, Entropy.EntropyStageExecutor>();
builder.Services.AddSingleton<ScannerWorkerHostedService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<ScannerWorkerHostedService>());