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
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user