up
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>());
|
||||
|
||||
Reference in New Issue
Block a user