up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-11 02:32:18 +02:00
parent 92bc4d3a07
commit 49922dff5a
474 changed files with 76071 additions and 12411 deletions

View File

@@ -0,0 +1,248 @@
using StellaOps.Scanner.Analyzers.Native.Internal.Callgraph;
using StellaOps.Scanner.Analyzers.Native.Internal.Elf;
using StellaOps.Scanner.Analyzers.Native.Internal.Graph;
namespace StellaOps.Scanner.Analyzers.Native;
/// <summary>
/// Analyzes native ELF binaries for reachability graphs.
/// Implements SCAN-NATIVE-REACH-0146-13 requirements:
/// - Call-graph extraction from ELF binaries
/// - Synthetic roots (_init, .init_array, .preinit_array, entry points)
/// - Build-id capture
/// - PURL/symbol digests
/// - Unknowns emission
/// - DSSE graph bundles
/// </summary>
public sealed class NativeReachabilityAnalyzer
{
/// <summary>
/// Analyzes a directory of ELF binaries and produces a reachability graph.
/// </summary>
/// <param name="layerPath">Path to the layer directory.</param>
/// <param name="layerDigest">Digest of the layer.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The native reachability graph.</returns>
public async Task<NativeReachabilityGraph> AnalyzeLayerAsync(
string layerPath,
string layerDigest,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(layerPath);
ArgumentException.ThrowIfNullOrEmpty(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest);
// Find all potential ELF files in the layer
await foreach (var filePath in FindElfFilesAsync(layerPath, cancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
await using var stream = File.OpenRead(filePath);
var relativePath = Path.GetRelativePath(layerPath, filePath).Replace('\\', '/');
var elf = ElfReader.Parse(stream, relativePath, layerDigest);
if (elf is not null)
{
builder.AddElfFile(elf);
}
}
catch (IOException)
{
// Skip files that can't be read
}
catch (UnauthorizedAccessException)
{
// Skip files without permission
}
}
return builder.Build();
}
/// <summary>
/// Analyzes a single ELF file and produces a reachability graph.
/// </summary>
public async Task<NativeReachabilityGraph> AnalyzeFileAsync(
string filePath,
string layerDigest,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(filePath);
ArgumentException.ThrowIfNullOrEmpty(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest);
await using var stream = File.OpenRead(filePath);
var elf = ElfReader.Parse(stream, filePath, layerDigest);
if (elf is not null)
{
builder.AddElfFile(elf);
}
return builder.Build();
}
/// <summary>
/// Analyzes an ELF file from a stream.
/// </summary>
public NativeReachabilityGraph AnalyzeStream(
Stream stream,
string filePath,
string layerDigest)
{
ArgumentNullException.ThrowIfNull(stream);
ArgumentException.ThrowIfNullOrEmpty(filePath);
ArgumentException.ThrowIfNullOrEmpty(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest);
var elf = ElfReader.Parse(stream, filePath, layerDigest);
if (elf is not null)
{
builder.AddElfFile(elf);
}
return builder.Build();
}
/// <summary>
/// Writes the graph as NDJSON to a stream.
/// </summary>
public static Task WriteNdjsonAsync(
NativeReachabilityGraph graph,
Stream stream,
CancellationToken cancellationToken = default)
{
return NativeGraphDsseWriter.WriteNdjsonAsync(graph, stream, cancellationToken);
}
/// <summary>
/// Writes the graph as JSON (for DSSE payload).
/// </summary>
public static string WriteJson(NativeReachabilityGraph graph)
{
return NativeGraphDsseWriter.WriteJson(graph);
}
private static async IAsyncEnumerable<string> FindElfFilesAsync(
string rootPath,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
var searchDirs = new Stack<string>();
searchDirs.Push(rootPath);
// Common directories containing ELF binaries
var binaryDirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"bin", "sbin", "lib", "lib64", "lib32", "libx32",
"usr/bin", "usr/sbin", "usr/lib", "usr/lib64", "usr/lib32",
"usr/local/bin", "usr/local/sbin", "usr/local/lib",
"opt"
};
while (searchDirs.Count > 0)
{
cancellationToken.ThrowIfCancellationRequested();
var currentDir = searchDirs.Pop();
IEnumerable<string> files;
try
{
files = Directory.EnumerateFiles(currentDir);
}
catch (Exception) when (IsIgnorableException(default!))
{
continue;
}
foreach (var file in files)
{
cancellationToken.ThrowIfCancellationRequested();
// Quick check: skip obvious non-ELF files
var ext = Path.GetExtension(file);
if (IsSkippableExtension(ext))
{
continue;
}
// Check if file starts with ELF magic
if (await IsElfFileAsync(file, cancellationToken))
{
yield return file;
}
}
// Recurse into subdirectories
IEnumerable<string> subdirs;
try
{
subdirs = Directory.EnumerateDirectories(currentDir);
}
catch (Exception) when (IsIgnorableException(default!))
{
continue;
}
foreach (var subdir in subdirs)
{
var dirName = Path.GetFileName(subdir);
// Skip common non-binary directories
if (IsSkippableDirectory(dirName))
{
continue;
}
searchDirs.Push(subdir);
}
}
}
private static async Task<bool> IsElfFileAsync(string filePath, CancellationToken ct)
{
try
{
var buffer = new byte[4];
await using var stream = File.OpenRead(filePath);
var bytesRead = await stream.ReadAsync(buffer, ct);
return bytesRead >= 4 && ElfReader.IsElf(buffer);
}
catch
{
return false;
}
}
private static bool IsSkippableExtension(string ext)
{
return ext is ".txt" or ".md" or ".json" or ".xml" or ".yaml" or ".yml"
or ".html" or ".css" or ".js" or ".ts" or ".py" or ".rb" or ".php"
or ".java" or ".class" or ".jar" or ".war" or ".ear"
or ".png" or ".jpg" or ".jpeg" or ".gif" or ".svg" or ".ico"
or ".zip" or ".tar" or ".gz" or ".bz2" or ".xz" or ".7z"
or ".deb" or ".rpm" or ".apk"
or ".pem" or ".crt" or ".key" or ".pub"
or ".log" or ".pid" or ".lock";
}
private static bool IsSkippableDirectory(string dirName)
{
return dirName is "." or ".."
or "proc" or "sys" or "dev" or "run" or "tmp" or "var"
or "home" or "root" or "etc" or "boot" or "media" or "mnt"
or "node_modules" or ".git" or ".svn" or ".hg"
or "__pycache__" or ".cache" or ".npm" or ".cargo"
or "share" or "doc" or "man" or "info" or "locale";
}
private static bool IsIgnorableException(Exception ex)
{
return ex is IOException or UnauthorizedAccessException or DirectoryNotFoundException;
}
}