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;
///
/// 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
///
public sealed class NativeReachabilityAnalyzer
{
private readonly TimeProvider _timeProvider;
public NativeReachabilityAnalyzer(TimeProvider timeProvider)
{
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
}
///
/// Analyzes a directory of ELF binaries and produces a reachability graph.
///
/// Path to the layer directory.
/// Digest of the layer.
/// Cancellation token.
/// The native reachability graph.
public async Task AnalyzeLayerAsync(
string layerPath,
string layerDigest,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(layerPath);
ArgumentException.ThrowIfNullOrEmpty(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest, _timeProvider);
// 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();
}
///
/// Analyzes a single ELF file and produces a reachability graph.
///
public async Task AnalyzeFileAsync(
string filePath,
string layerDigest,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(filePath);
ArgumentException.ThrowIfNullOrEmpty(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest, _timeProvider);
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();
}
///
/// Analyzes an ELF file from a stream.
///
public NativeReachabilityGraph AnalyzeStream(
Stream stream,
string filePath,
string layerDigest)
{
ArgumentNullException.ThrowIfNull(stream);
ArgumentException.ThrowIfNullOrEmpty(filePath);
ArgumentException.ThrowIfNullOrEmpty(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest, _timeProvider);
var elf = ElfReader.Parse(stream, filePath, layerDigest);
if (elf is not null)
{
builder.AddElfFile(elf);
}
return builder.Build();
}
///
/// Writes the graph as NDJSON to a stream.
///
public static Task WriteNdjsonAsync(
NativeReachabilityGraph graph,
Stream stream,
CancellationToken cancellationToken = default)
{
return NativeGraphDsseWriter.WriteNdjsonAsync(graph, stream, cancellationToken);
}
///
/// Writes the graph as JSON (for DSSE payload).
///
public static string WriteJson(NativeReachabilityGraph graph)
{
return NativeGraphDsseWriter.WriteJson(graph);
}
private static async IAsyncEnumerable FindElfFilesAsync(
string rootPath,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
var searchDirs = new Stack();
searchDirs.Push(rootPath);
// Common directories containing ELF binaries
var binaryDirs = new HashSet(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 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 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 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;
}
}