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:
@@ -46,7 +46,7 @@ internal static class DenoRuntimeTraceProbe
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(line);
|
||||
using var document = JsonDocument.Parse(line.ToArray());
|
||||
if (!document.RootElement.TryGetProperty("type", out var typeProp))
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves runtime edges from parsed Java runtime events.
|
||||
/// Produces append-only edges for runtime-class, runtime-spi, runtime-load patterns.
|
||||
/// </summary>
|
||||
internal static class JavaRuntimeEdgeResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves runtime edges and entrypoints from parsed events.
|
||||
/// </summary>
|
||||
public static JavaRuntimeIngestion ResolveFromEvents(
|
||||
ImmutableArray<JavaRuntimeEvent> events,
|
||||
ImmutableArray<JavaRuntimeIngestionWarning> parseWarnings,
|
||||
string contentHash,
|
||||
JavaRuntimeIngestionConfig config,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var edges = ImmutableArray.CreateBuilder<JavaRuntimeEdge>();
|
||||
var entrypoints = ImmutableArray.CreateBuilder<JavaRuntimeEntrypoint>();
|
||||
var warnings = ImmutableArray.CreateBuilder<JavaRuntimeIngestionWarning>();
|
||||
warnings.AddRange(parseWarnings);
|
||||
|
||||
// Track seen edges for deduplication
|
||||
var seenEdges = new HashSet<string>();
|
||||
|
||||
// Track entrypoint invocation counts
|
||||
var entrypointCounts = new Dictionary<string, (JavaRuntimeEntrypoint Entry, int Count)>();
|
||||
|
||||
// Summary counters
|
||||
int classLoadCount = 0, serviceLoaderCount = 0, nativeLoadCount = 0;
|
||||
int reflectionCount = 0, resourceAccessCount = 0, moduleResolveCount = 0;
|
||||
DateTimeOffset? startTime = null, endTime = null;
|
||||
|
||||
foreach (var evt in events)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Track time bounds
|
||||
if (startTime is null || evt.Timestamp < startTime)
|
||||
{
|
||||
startTime = evt.Timestamp;
|
||||
}
|
||||
if (endTime is null || evt.Timestamp > endTime)
|
||||
{
|
||||
endTime = evt.Timestamp;
|
||||
}
|
||||
|
||||
switch (evt)
|
||||
{
|
||||
case JavaClassLoadEvent classLoad:
|
||||
classLoadCount++;
|
||||
ResolveClassLoadEdges(classLoad, edges, seenEdges, config);
|
||||
break;
|
||||
|
||||
case JavaServiceLoaderEvent serviceLoader:
|
||||
serviceLoaderCount++;
|
||||
ResolveSpiEdges(serviceLoader, edges, entrypoints, entrypointCounts, seenEdges, config);
|
||||
break;
|
||||
|
||||
case JavaNativeLoadEvent nativeLoad:
|
||||
nativeLoadCount++;
|
||||
ResolveNativeLoadEdges(nativeLoad, edges, seenEdges, config);
|
||||
break;
|
||||
|
||||
case JavaReflectionEvent reflection:
|
||||
reflectionCount++;
|
||||
ResolveReflectionEdges(reflection, edges, entrypoints, entrypointCounts, seenEdges, config);
|
||||
break;
|
||||
|
||||
case JavaResourceAccessEvent resourceAccess:
|
||||
resourceAccessCount++;
|
||||
ResolveResourceEdges(resourceAccess, edges, seenEdges, config);
|
||||
break;
|
||||
|
||||
case JavaModuleResolveEvent moduleResolve:
|
||||
moduleResolveCount++;
|
||||
ResolveModuleEdges(moduleResolve, edges, seenEdges, config);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build final entrypoints from tracked counts
|
||||
var finalEntrypoints = entrypointCounts.Values
|
||||
.Select(v => v.Entry with { InvocationCount = v.Count })
|
||||
.ToImmutableArray();
|
||||
|
||||
var summary = new JavaRuntimeTraceSummary(
|
||||
StartTime: startTime ?? DateTimeOffset.MinValue,
|
||||
EndTime: endTime ?? DateTimeOffset.MinValue,
|
||||
JavaVersion: null, // Would come from trace metadata if available
|
||||
JavaVendor: null,
|
||||
JvmName: null,
|
||||
JvmArgs: null,
|
||||
ClassLoadCount: classLoadCount,
|
||||
ServiceLoaderCount: serviceLoaderCount,
|
||||
NativeLoadCount: nativeLoadCount,
|
||||
ReflectionCount: reflectionCount,
|
||||
ResourceAccessCount: resourceAccessCount,
|
||||
ModuleResolveCount: moduleResolveCount);
|
||||
|
||||
return new JavaRuntimeIngestion(
|
||||
events,
|
||||
edges.ToImmutable(),
|
||||
finalEntrypoints,
|
||||
summary,
|
||||
warnings.ToImmutable(),
|
||||
contentHash);
|
||||
}
|
||||
|
||||
private static void ResolveClassLoadEdges(
|
||||
JavaClassLoadEvent evt,
|
||||
ImmutableArray<JavaRuntimeEdge>.Builder edges,
|
||||
HashSet<string> seenEdges,
|
||||
JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
var edgeKey = $"runtime-class:{evt.InitiatingClass ?? "bootstrap"}:{evt.ClassName}";
|
||||
if (config.DeduplicateEdges && !seenEdges.Add(edgeKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var reason = evt.ClassLoader switch
|
||||
{
|
||||
"bootstrap" => JavaRuntimeEdgeReason.ClassLoadBootstrap,
|
||||
"platform" or "ext" => JavaRuntimeEdgeReason.ClassLoadPlatform,
|
||||
"app" or "system" => JavaRuntimeEdgeReason.ClassLoadApplication,
|
||||
_ => JavaRuntimeEdgeReason.ClassLoadCustom,
|
||||
};
|
||||
|
||||
edges.Add(new JavaRuntimeEdge(
|
||||
EdgeId: ComputeEdgeId(edgeKey),
|
||||
SourceClass: evt.InitiatingClass,
|
||||
TargetClass: evt.ClassName,
|
||||
EdgeType: JavaRuntimeEdgeType.RuntimeClass,
|
||||
Reason: reason,
|
||||
Timestamp: evt.Timestamp,
|
||||
Source: evt.Source,
|
||||
SourceHash: evt.SourceHash,
|
||||
Confidence: 1.0,
|
||||
Details: $"classloader={evt.ClassLoader}"));
|
||||
}
|
||||
|
||||
private static void ResolveSpiEdges(
|
||||
JavaServiceLoaderEvent evt,
|
||||
ImmutableArray<JavaRuntimeEdge>.Builder edges,
|
||||
ImmutableArray<JavaRuntimeEntrypoint>.Builder entrypoints,
|
||||
Dictionary<string, (JavaRuntimeEntrypoint Entry, int Count)> entrypointCounts,
|
||||
HashSet<string> seenEdges,
|
||||
JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
foreach (var provider in evt.Providers)
|
||||
{
|
||||
var edgeKey = $"runtime-spi:{evt.ServiceInterface}:{provider.ProviderClass}";
|
||||
if (!config.DeduplicateEdges || seenEdges.Add(edgeKey))
|
||||
{
|
||||
edges.Add(new JavaRuntimeEdge(
|
||||
EdgeId: ComputeEdgeId(edgeKey),
|
||||
SourceClass: evt.ServiceInterface,
|
||||
TargetClass: provider.ProviderClass,
|
||||
EdgeType: JavaRuntimeEdgeType.RuntimeSpi,
|
||||
Reason: JavaRuntimeEdgeReason.ServiceLoaderExplicit,
|
||||
Timestamp: evt.Timestamp,
|
||||
Source: provider.Source,
|
||||
SourceHash: provider.SourceHash,
|
||||
Confidence: 1.0,
|
||||
Details: $"service={evt.ServiceInterface}"));
|
||||
}
|
||||
|
||||
// Track provider as entrypoint
|
||||
var entrypointKey = $"spi:{provider.ProviderClass}";
|
||||
if (entrypointCounts.TryGetValue(entrypointKey, out var existing))
|
||||
{
|
||||
entrypointCounts[entrypointKey] = (existing.Entry, existing.Count + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
var entrypoint = new JavaRuntimeEntrypoint(
|
||||
EntrypointId: ComputeEdgeId(entrypointKey),
|
||||
ClassName: provider.ProviderClass,
|
||||
MethodName: null,
|
||||
EntrypointType: JavaRuntimeEntrypointType.ServiceProvider,
|
||||
FirstSeen: evt.Timestamp,
|
||||
InvocationCount: 1,
|
||||
Source: provider.Source,
|
||||
SourceHash: provider.SourceHash,
|
||||
Confidence: 1.0);
|
||||
entrypointCounts[entrypointKey] = (entrypoint, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResolveNativeLoadEdges(
|
||||
JavaNativeLoadEvent evt,
|
||||
ImmutableArray<JavaRuntimeEdge>.Builder edges,
|
||||
HashSet<string> seenEdges,
|
||||
JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
var edgeKey = $"runtime-native:{evt.InitiatingClass ?? "unknown"}:{evt.LibraryName}";
|
||||
if (config.DeduplicateEdges && !seenEdges.Add(edgeKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var reason = evt.LoadMethod switch
|
||||
{
|
||||
"System.load" => JavaRuntimeEdgeReason.SystemLoad,
|
||||
"System.loadLibrary" => JavaRuntimeEdgeReason.SystemLoadLibrary,
|
||||
"Runtime.load" => JavaRuntimeEdgeReason.RuntimeLoad,
|
||||
"Runtime.loadLibrary" => JavaRuntimeEdgeReason.RuntimeLoadLibrary,
|
||||
_ => JavaRuntimeEdgeReason.SystemLoadLibrary,
|
||||
};
|
||||
|
||||
if (!evt.Success)
|
||||
{
|
||||
reason = JavaRuntimeEdgeReason.NativeLoadFailure;
|
||||
}
|
||||
|
||||
edges.Add(new JavaRuntimeEdge(
|
||||
EdgeId: ComputeEdgeId(edgeKey),
|
||||
SourceClass: evt.InitiatingClass,
|
||||
TargetClass: evt.LibraryName,
|
||||
EdgeType: JavaRuntimeEdgeType.RuntimeNativeLoad,
|
||||
Reason: reason,
|
||||
Timestamp: evt.Timestamp,
|
||||
Source: evt.ResolvedPath,
|
||||
SourceHash: evt.PathHash,
|
||||
Confidence: evt.Success ? 1.0 : 0.5,
|
||||
Details: evt.Success ? $"resolved={evt.ResolvedPath}" : "load_failed"));
|
||||
}
|
||||
|
||||
private static void ResolveReflectionEdges(
|
||||
JavaReflectionEvent evt,
|
||||
ImmutableArray<JavaRuntimeEdge>.Builder edges,
|
||||
ImmutableArray<JavaRuntimeEntrypoint>.Builder entrypoints,
|
||||
Dictionary<string, (JavaRuntimeEntrypoint Entry, int Count)> entrypointCounts,
|
||||
HashSet<string> seenEdges,
|
||||
JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
var edgeKey = $"runtime-reflect:{evt.InitiatingClass ?? "unknown"}:{evt.TargetClass}:{evt.ReflectionMethod}";
|
||||
if (!config.DeduplicateEdges || seenEdges.Add(edgeKey))
|
||||
{
|
||||
var reason = evt.ReflectionMethod switch
|
||||
{
|
||||
"Class.forName" => JavaRuntimeEdgeReason.ClassForName,
|
||||
"Class.newInstance" => JavaRuntimeEdgeReason.ClassNewInstance,
|
||||
"Constructor.newInstance" => JavaRuntimeEdgeReason.ConstructorNewInstance,
|
||||
"Method.invoke" => JavaRuntimeEdgeReason.MethodInvoke,
|
||||
_ => JavaRuntimeEdgeReason.ClassForName,
|
||||
};
|
||||
|
||||
edges.Add(new JavaRuntimeEdge(
|
||||
EdgeId: ComputeEdgeId(edgeKey),
|
||||
SourceClass: evt.InitiatingClass,
|
||||
TargetClass: evt.TargetClass,
|
||||
EdgeType: JavaRuntimeEdgeType.RuntimeReflection,
|
||||
Reason: reason,
|
||||
Timestamp: evt.Timestamp,
|
||||
Source: null,
|
||||
SourceHash: null,
|
||||
Confidence: 0.9, // Reflection edges have slightly lower confidence
|
||||
Details: $"method={evt.ReflectionMethod}"));
|
||||
}
|
||||
|
||||
// Track reflection target as entrypoint
|
||||
var entrypointKey = $"reflect:{evt.TargetClass}";
|
||||
if (entrypointCounts.TryGetValue(entrypointKey, out var existing))
|
||||
{
|
||||
entrypointCounts[entrypointKey] = (existing.Entry, existing.Count + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
var entrypoint = new JavaRuntimeEntrypoint(
|
||||
EntrypointId: ComputeEdgeId(entrypointKey),
|
||||
ClassName: evt.TargetClass,
|
||||
MethodName: null,
|
||||
EntrypointType: JavaRuntimeEntrypointType.ReflectionTarget,
|
||||
FirstSeen: evt.Timestamp,
|
||||
InvocationCount: 1,
|
||||
Source: null,
|
||||
SourceHash: null,
|
||||
Confidence: 0.9);
|
||||
entrypointCounts[entrypointKey] = (entrypoint, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResolveResourceEdges(
|
||||
JavaResourceAccessEvent evt,
|
||||
ImmutableArray<JavaRuntimeEdge>.Builder edges,
|
||||
HashSet<string> seenEdges,
|
||||
JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
if (!evt.Found)
|
||||
{
|
||||
return; // Only track successful resource lookups
|
||||
}
|
||||
|
||||
var edgeKey = $"runtime-resource:{evt.InitiatingClass ?? "unknown"}:{evt.ResourceName}";
|
||||
if (config.DeduplicateEdges && !seenEdges.Add(edgeKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
edges.Add(new JavaRuntimeEdge(
|
||||
EdgeId: ComputeEdgeId(edgeKey),
|
||||
SourceClass: evt.InitiatingClass,
|
||||
TargetClass: evt.ResourceName,
|
||||
EdgeType: JavaRuntimeEdgeType.RuntimeResource,
|
||||
Reason: JavaRuntimeEdgeReason.GetResource,
|
||||
Timestamp: evt.Timestamp,
|
||||
Source: evt.Source,
|
||||
SourceHash: evt.SourceHash,
|
||||
Confidence: 1.0,
|
||||
Details: null));
|
||||
}
|
||||
|
||||
private static void ResolveModuleEdges(
|
||||
JavaModuleResolveEvent evt,
|
||||
ImmutableArray<JavaRuntimeEdge>.Builder edges,
|
||||
HashSet<string> seenEdges,
|
||||
JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
if (string.IsNullOrEmpty(evt.RequiredBy))
|
||||
{
|
||||
return; // Skip root modules without a requiring module
|
||||
}
|
||||
|
||||
var edgeKey = $"runtime-module:{evt.RequiredBy}:{evt.ModuleName}";
|
||||
if (config.DeduplicateEdges && !seenEdges.Add(edgeKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
edges.Add(new JavaRuntimeEdge(
|
||||
EdgeId: ComputeEdgeId(edgeKey),
|
||||
SourceClass: evt.RequiredBy,
|
||||
TargetClass: evt.ModuleName,
|
||||
EdgeType: JavaRuntimeEdgeType.RuntimeModule,
|
||||
Reason: JavaRuntimeEdgeReason.ModuleRequires,
|
||||
Timestamp: evt.Timestamp,
|
||||
Source: evt.ModuleLocation,
|
||||
SourceHash: evt.LocationHash,
|
||||
Confidence: 1.0,
|
||||
Details: evt.IsOpen ? "open_module" : null));
|
||||
}
|
||||
|
||||
private static string ComputeEdgeId(string input)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return $"runtime:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Parses NDJSON runtime trace files produced by Java agent or JFR export.
|
||||
/// Supports both agent-produced traces and JFR .ndjson exports.
|
||||
/// </summary>
|
||||
internal static class JavaRuntimeEventParser
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Parses a runtime trace file and returns all events.
|
||||
/// </summary>
|
||||
/// <param name="stream">Stream containing NDJSON trace data.</param>
|
||||
/// <param name="config">Ingestion configuration.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Parsed events and warnings.</returns>
|
||||
public static async Task<(ImmutableArray<JavaRuntimeEvent> Events, ImmutableArray<JavaRuntimeIngestionWarning> Warnings, string ContentHash)>
|
||||
ParseAsync(Stream stream, JavaRuntimeIngestionConfig config, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var events = ImmutableArray.CreateBuilder<JavaRuntimeEvent>();
|
||||
var warnings = ImmutableArray.CreateBuilder<JavaRuntimeIngestionWarning>();
|
||||
|
||||
using var hashAlgorithm = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
|
||||
|
||||
var lineNumber = 0;
|
||||
var eventCount = 0;
|
||||
|
||||
while (await reader.ReadLineAsync(cancellationToken) is { } line)
|
||||
{
|
||||
lineNumber++;
|
||||
|
||||
// Update content hash
|
||||
hashAlgorithm.AppendData(Encoding.UTF8.GetBytes(line));
|
||||
hashAlgorithm.AppendData("\n"u8);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check max events limit
|
||||
if (config.MaxEvents > 0 && eventCount >= config.MaxEvents)
|
||||
{
|
||||
warnings.Add(new JavaRuntimeIngestionWarning(
|
||||
"MAX_EVENTS_REACHED",
|
||||
$"Maximum event limit ({config.MaxEvents}) reached, stopping parse",
|
||||
lineNumber,
|
||||
null));
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var evt = ParseLine(line, config);
|
||||
if (evt is not null)
|
||||
{
|
||||
events.Add(evt);
|
||||
eventCount++;
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
warnings.Add(new JavaRuntimeIngestionWarning(
|
||||
"PARSE_ERROR",
|
||||
$"Failed to parse JSON: {ex.Message}",
|
||||
lineNumber,
|
||||
line.Length > 200 ? line[..200] + "..." : line));
|
||||
}
|
||||
}
|
||||
|
||||
var hash = Convert.ToHexString(hashAlgorithm.GetCurrentHash()).ToLowerInvariant();
|
||||
return (events.ToImmutable(), warnings.ToImmutable(), hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a single NDJSON line into a runtime event.
|
||||
/// </summary>
|
||||
private static JavaRuntimeEvent? ParseLine(string line, JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(line);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("type", out var typeElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var type = typeElement.GetString();
|
||||
return type switch
|
||||
{
|
||||
"java.class.load" => ParseClassLoadEvent(root, config),
|
||||
"java.service.load" => ParseServiceLoaderEvent(root, config),
|
||||
"java.native.load" => ParseNativeLoadEvent(root, config),
|
||||
"java.reflection.access" => ParseReflectionEvent(root, config),
|
||||
"java.resource.access" => ParseResourceAccessEvent(root, config),
|
||||
"java.module.resolve" => ParseModuleResolveEvent(root, config),
|
||||
"java.class.statistics" => config.IncludeStatistics ? ParseClassStatisticsEvent(root) : null,
|
||||
_ => null, // Unknown event types are silently ignored
|
||||
};
|
||||
}
|
||||
|
||||
private static JavaClassLoadEvent? ParseClassLoadEvent(JsonElement root, JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
var ts = GetTimestamp(root);
|
||||
var className = root.GetProperty("class_name").GetString() ?? string.Empty;
|
||||
var classLoader = root.TryGetProperty("class_loader", out var cl) ? cl.GetString() ?? "app" : "app";
|
||||
var source = root.TryGetProperty("source", out var s) ? s.GetString() : null;
|
||||
var sourceHash = root.TryGetProperty("source_hash", out var sh) ? sh.GetString() : null;
|
||||
var initiatingClass = root.TryGetProperty("initiating_class", out var ic) ? ic.GetString() : null;
|
||||
var threadName = root.TryGetProperty("thread_name", out var tn) ? tn.GetString() : null;
|
||||
|
||||
// Filter JDK classes if configured
|
||||
if (!config.IncludeJdkClasses && IsJdkClass(className))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Compute source hash if not provided and scrubbing is enabled
|
||||
if (config.ScrubPaths && source is not null && sourceHash is null)
|
||||
{
|
||||
sourceHash = ComputePathHash(source);
|
||||
}
|
||||
|
||||
return new JavaClassLoadEvent(ts, className, classLoader, source, sourceHash, initiatingClass, threadName);
|
||||
}
|
||||
|
||||
private static JavaServiceLoaderEvent ParseServiceLoaderEvent(JsonElement root, JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
var ts = GetTimestamp(root);
|
||||
var serviceInterface = root.GetProperty("service_interface").GetString() ?? string.Empty;
|
||||
var initiatingClass = root.TryGetProperty("initiating_class", out var ic) ? ic.GetString() : null;
|
||||
var threadName = root.TryGetProperty("thread_name", out var tn) ? tn.GetString() : null;
|
||||
|
||||
var providers = ImmutableArray.CreateBuilder<JavaServiceProviderInfo>();
|
||||
if (root.TryGetProperty("providers", out var providersElement) && providersElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var p in providersElement.EnumerateArray())
|
||||
{
|
||||
var providerClass = p.TryGetProperty("provider_class", out var pc) ? pc.GetString() ?? string.Empty : string.Empty;
|
||||
var source = p.TryGetProperty("source", out var s) ? s.GetString() : null;
|
||||
var sourceHash = p.TryGetProperty("source_hash", out var sh) ? sh.GetString() : null;
|
||||
|
||||
if (config.ScrubPaths && source is not null && sourceHash is null)
|
||||
{
|
||||
sourceHash = ComputePathHash(source);
|
||||
}
|
||||
|
||||
providers.Add(new JavaServiceProviderInfo(providerClass, source, sourceHash));
|
||||
}
|
||||
}
|
||||
|
||||
return new JavaServiceLoaderEvent(ts, serviceInterface, providers.ToImmutable(), initiatingClass, threadName);
|
||||
}
|
||||
|
||||
private static JavaNativeLoadEvent ParseNativeLoadEvent(JsonElement root, JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
var ts = GetTimestamp(root);
|
||||
var libraryName = root.GetProperty("library_name").GetString() ?? string.Empty;
|
||||
var resolvedPath = root.TryGetProperty("resolved_path", out var rp) ? rp.GetString() : null;
|
||||
var pathHash = root.TryGetProperty("path_hash", out var ph) ? ph.GetString() : null;
|
||||
var loadMethod = root.TryGetProperty("load_method", out var lm) ? lm.GetString() ?? "System.loadLibrary" : "System.loadLibrary";
|
||||
var initiatingClass = root.TryGetProperty("initiating_class", out var ic) ? ic.GetString() : null;
|
||||
var threadName = root.TryGetProperty("thread_name", out var tn) ? tn.GetString() : null;
|
||||
var success = root.TryGetProperty("success", out var sc) && sc.GetBoolean();
|
||||
|
||||
if (config.ScrubPaths && resolvedPath is not null && pathHash is null)
|
||||
{
|
||||
pathHash = ComputePathHash(resolvedPath);
|
||||
}
|
||||
|
||||
return new JavaNativeLoadEvent(ts, libraryName, resolvedPath, pathHash, loadMethod, initiatingClass, threadName, success);
|
||||
}
|
||||
|
||||
private static JavaReflectionEvent ParseReflectionEvent(JsonElement root, JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
var ts = GetTimestamp(root);
|
||||
var targetClass = root.GetProperty("target_class").GetString() ?? string.Empty;
|
||||
var reflectionMethod = root.TryGetProperty("reflection_method", out var rm) ? rm.GetString() ?? "Class.forName" : "Class.forName";
|
||||
var initiatingClass = root.TryGetProperty("initiating_class", out var ic) ? ic.GetString() : null;
|
||||
var sourceLine = root.TryGetProperty("source_line", out var sl) ? sl.GetString() : null;
|
||||
var threadName = root.TryGetProperty("thread_name", out var tn) ? tn.GetString() : null;
|
||||
|
||||
// Filter JDK classes if configured
|
||||
if (!config.IncludeJdkClasses && IsJdkClass(targetClass))
|
||||
{
|
||||
return new JavaReflectionEvent(ts, targetClass, reflectionMethod, initiatingClass, sourceLine, threadName);
|
||||
}
|
||||
|
||||
return new JavaReflectionEvent(ts, targetClass, reflectionMethod, initiatingClass, sourceLine, threadName);
|
||||
}
|
||||
|
||||
private static JavaResourceAccessEvent ParseResourceAccessEvent(JsonElement root, JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
var ts = GetTimestamp(root);
|
||||
var resourceName = root.GetProperty("resource_name").GetString() ?? string.Empty;
|
||||
var source = root.TryGetProperty("source", out var s) ? s.GetString() : null;
|
||||
var sourceHash = root.TryGetProperty("source_hash", out var sh) ? sh.GetString() : null;
|
||||
var initiatingClass = root.TryGetProperty("initiating_class", out var ic) ? ic.GetString() : null;
|
||||
var found = root.TryGetProperty("found", out var f) && f.GetBoolean();
|
||||
|
||||
if (config.ScrubPaths && source is not null && sourceHash is null)
|
||||
{
|
||||
sourceHash = ComputePathHash(source);
|
||||
}
|
||||
|
||||
return new JavaResourceAccessEvent(ts, resourceName, source, sourceHash, initiatingClass, found);
|
||||
}
|
||||
|
||||
private static JavaModuleResolveEvent ParseModuleResolveEvent(JsonElement root, JavaRuntimeIngestionConfig config)
|
||||
{
|
||||
var ts = GetTimestamp(root);
|
||||
var moduleName = root.GetProperty("module_name").GetString() ?? string.Empty;
|
||||
var moduleLocation = root.TryGetProperty("module_location", out var ml) ? ml.GetString() : null;
|
||||
var locationHash = root.TryGetProperty("location_hash", out var lh) ? lh.GetString() : null;
|
||||
var requiredBy = root.TryGetProperty("required_by", out var rb) ? rb.GetString() : null;
|
||||
var isOpen = root.TryGetProperty("is_open", out var io) && io.GetBoolean();
|
||||
|
||||
if (config.ScrubPaths && moduleLocation is not null && locationHash is null)
|
||||
{
|
||||
locationHash = ComputePathHash(moduleLocation);
|
||||
}
|
||||
|
||||
return new JavaModuleResolveEvent(ts, moduleName, moduleLocation, locationHash, requiredBy, isOpen);
|
||||
}
|
||||
|
||||
private static JavaClassLoadingStatisticsEvent ParseClassStatisticsEvent(JsonElement root)
|
||||
{
|
||||
var ts = GetTimestamp(root);
|
||||
var loadedClassCount = root.TryGetProperty("loaded_class_count", out var lcc) ? lcc.GetInt64() : 0;
|
||||
var unloadedClassCount = root.TryGetProperty("unloaded_class_count", out var ucc) ? ucc.GetInt64() : 0;
|
||||
var classLoaders = root.TryGetProperty("class_loaders", out var cl) ? cl.GetInt32() : 0;
|
||||
var hiddenClasses = root.TryGetProperty("hidden_classes", out var hc) ? hc.GetInt32() : 0;
|
||||
|
||||
return new JavaClassLoadingStatisticsEvent(ts, loadedClassCount, unloadedClassCount, classLoaders, hiddenClasses);
|
||||
}
|
||||
|
||||
private static DateTimeOffset GetTimestamp(JsonElement root)
|
||||
{
|
||||
if (root.TryGetProperty("ts", out var ts))
|
||||
{
|
||||
if (ts.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return DateTimeOffset.Parse(ts.GetString()!, null, System.Globalization.DateTimeStyles.RoundtripKind);
|
||||
}
|
||||
if (ts.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeMilliseconds(ts.GetInt64());
|
||||
}
|
||||
}
|
||||
return DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
private static bool IsJdkClass(string className)
|
||||
{
|
||||
// Check for JDK internal packages
|
||||
return className.StartsWith("java/", StringComparison.Ordinal)
|
||||
|| className.StartsWith("javax/", StringComparison.Ordinal)
|
||||
|| className.StartsWith("jdk/", StringComparison.Ordinal)
|
||||
|| className.StartsWith("sun/", StringComparison.Ordinal)
|
||||
|| className.StartsWith("com/sun/", StringComparison.Ordinal)
|
||||
|| className.StartsWith("oracle/", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a SHA-256 hash of a path for deterministic path-safe evidence.
|
||||
/// </summary>
|
||||
internal static string ComputePathHash(string path)
|
||||
{
|
||||
// Normalize path separators to forward slash
|
||||
var normalized = path.Replace('\\', '/');
|
||||
var bytes = Encoding.UTF8.GetBytes(normalized);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base type for Java runtime events captured via Java agent or JFR.
|
||||
/// Events are serialized as NDJSON with deterministic key ordering.
|
||||
/// </summary>
|
||||
internal abstract record JavaRuntimeEvent(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("ts")] DateTimeOffset Timestamp);
|
||||
|
||||
/// <summary>
|
||||
/// Class load event captured when a class is loaded by the JVM.
|
||||
/// </summary>
|
||||
/// <param name="Ts">Event timestamp (UTC).</param>
|
||||
/// <param name="ClassName">Fully qualified class name (e.g., "java/lang/String").</param>
|
||||
/// <param name="ClassLoader">Class loader name (e.g., "app", "platform", "bootstrap").</param>
|
||||
/// <param name="Source">JAR/location where the class was loaded from.</param>
|
||||
/// <param name="SourceHash">SHA-256 hash of normalized source path for path-safe evidence.</param>
|
||||
/// <param name="InitiatingClass">Class that initiated the load (if available).</param>
|
||||
/// <param name="ThreadName">Name of the thread where load occurred.</param>
|
||||
internal sealed record JavaClassLoadEvent(
|
||||
DateTimeOffset Ts,
|
||||
string ClassName,
|
||||
string ClassLoader,
|
||||
string? Source,
|
||||
string? SourceHash,
|
||||
string? InitiatingClass,
|
||||
string? ThreadName) : JavaRuntimeEvent("java.class.load", Ts);
|
||||
|
||||
/// <summary>
|
||||
/// ServiceLoader lookup event captured when ServiceLoader.load() is called.
|
||||
/// </summary>
|
||||
/// <param name="Ts">Event timestamp (UTC).</param>
|
||||
/// <param name="ServiceInterface">Service interface being loaded.</param>
|
||||
/// <param name="Providers">List of provider classes discovered.</param>
|
||||
/// <param name="InitiatingClass">Class that called ServiceLoader.load().</param>
|
||||
/// <param name="ThreadName">Name of the thread where lookup occurred.</param>
|
||||
internal sealed record JavaServiceLoaderEvent(
|
||||
DateTimeOffset Ts,
|
||||
string ServiceInterface,
|
||||
IReadOnlyList<JavaServiceProviderInfo> Providers,
|
||||
string? InitiatingClass,
|
||||
string? ThreadName) : JavaRuntimeEvent("java.service.load", Ts);
|
||||
|
||||
/// <summary>
|
||||
/// Information about a service provider discovered by ServiceLoader.
|
||||
/// </summary>
|
||||
/// <param name="ProviderClass">Provider implementation class name.</param>
|
||||
/// <param name="Source">JAR/module where provider was found.</param>
|
||||
/// <param name="SourceHash">SHA-256 hash of normalized source path.</param>
|
||||
internal sealed record JavaServiceProviderInfo(
|
||||
[property: JsonPropertyName("provider_class")] string ProviderClass,
|
||||
[property: JsonPropertyName("source")] string? Source,
|
||||
[property: JsonPropertyName("source_hash")] string? SourceHash);
|
||||
|
||||
/// <summary>
|
||||
/// Native library load event captured when System.load/loadLibrary is called.
|
||||
/// </summary>
|
||||
/// <param name="Ts">Event timestamp (UTC).</param>
|
||||
/// <param name="LibraryName">Library name (from loadLibrary) or path (from load).</param>
|
||||
/// <param name="ResolvedPath">Actual resolved path to the native library.</param>
|
||||
/// <param name="PathHash">SHA-256 hash of normalized path for path-safe evidence.</param>
|
||||
/// <param name="LoadMethod">How the library was loaded: "System.load", "System.loadLibrary", "Runtime.load", "Runtime.loadLibrary".</param>
|
||||
/// <param name="InitiatingClass">Class that initiated the load.</param>
|
||||
/// <param name="ThreadName">Name of the thread where load occurred.</param>
|
||||
/// <param name="Success">Whether the load succeeded.</param>
|
||||
internal sealed record JavaNativeLoadEvent(
|
||||
DateTimeOffset Ts,
|
||||
string LibraryName,
|
||||
string? ResolvedPath,
|
||||
string? PathHash,
|
||||
string LoadMethod,
|
||||
string? InitiatingClass,
|
||||
string? ThreadName,
|
||||
bool Success) : JavaRuntimeEvent("java.native.load", Ts);
|
||||
|
||||
/// <summary>
|
||||
/// Reflection class instantiation event captured via instrumentation.
|
||||
/// </summary>
|
||||
/// <param name="Ts">Event timestamp (UTC).</param>
|
||||
/// <param name="TargetClass">Class being instantiated/accessed via reflection.</param>
|
||||
/// <param name="ReflectionMethod">Method used: "Class.forName", "Class.newInstance", "Constructor.newInstance", "Method.invoke".</param>
|
||||
/// <param name="InitiatingClass">Class that performed the reflection call.</param>
|
||||
/// <param name="SourceLine">Source line information if available (class:line format).</param>
|
||||
/// <param name="ThreadName">Name of the thread where reflection occurred.</param>
|
||||
internal sealed record JavaReflectionEvent(
|
||||
DateTimeOffset Ts,
|
||||
string TargetClass,
|
||||
string ReflectionMethod,
|
||||
string? InitiatingClass,
|
||||
string? SourceLine,
|
||||
string? ThreadName) : JavaRuntimeEvent("java.reflection.access", Ts);
|
||||
|
||||
/// <summary>
|
||||
/// Resource access event captured when ClassLoader.getResource* is called.
|
||||
/// </summary>
|
||||
/// <param name="Ts">Event timestamp (UTC).</param>
|
||||
/// <param name="ResourceName">Name of the resource being accessed.</param>
|
||||
/// <param name="Source">JAR/location where resource was found.</param>
|
||||
/// <param name="SourceHash">SHA-256 hash of normalized source path.</param>
|
||||
/// <param name="InitiatingClass">Class that requested the resource.</param>
|
||||
/// <param name="Found">Whether the resource was found.</param>
|
||||
internal sealed record JavaResourceAccessEvent(
|
||||
DateTimeOffset Ts,
|
||||
string ResourceName,
|
||||
string? Source,
|
||||
string? SourceHash,
|
||||
string? InitiatingClass,
|
||||
bool Found) : JavaRuntimeEvent("java.resource.access", Ts);
|
||||
|
||||
/// <summary>
|
||||
/// Module resolution event captured when JPMS resolves module dependencies.
|
||||
/// </summary>
|
||||
/// <param name="Ts">Event timestamp (UTC).</param>
|
||||
/// <param name="ModuleName">Name of the module being resolved.</param>
|
||||
/// <param name="ModuleLocation">Location URI of the module.</param>
|
||||
/// <param name="LocationHash">SHA-256 hash of normalized location.</param>
|
||||
/// <param name="RequiredBy">Module that required this module.</param>
|
||||
/// <param name="IsOpen">Whether this is an open module.</param>
|
||||
internal sealed record JavaModuleResolveEvent(
|
||||
DateTimeOffset Ts,
|
||||
string ModuleName,
|
||||
string? ModuleLocation,
|
||||
string? LocationHash,
|
||||
string? RequiredBy,
|
||||
bool IsOpen) : JavaRuntimeEvent("java.module.resolve", Ts);
|
||||
|
||||
/// <summary>
|
||||
/// JFR event containing aggregated class loading statistics.
|
||||
/// </summary>
|
||||
/// <param name="Ts">Event timestamp (UTC).</param>
|
||||
/// <param name="LoadedClassCount">Total number of loaded classes.</param>
|
||||
/// <param name="UnloadedClassCount">Total number of unloaded classes.</param>
|
||||
/// <param name="ClassLoaders">Number of live class loaders.</param>
|
||||
/// <param name="HiddenClasses">Number of hidden/anonymous classes.</param>
|
||||
internal sealed record JavaClassLoadingStatisticsEvent(
|
||||
DateTimeOffset Ts,
|
||||
long LoadedClassCount,
|
||||
long UnloadedClassCount,
|
||||
int ClassLoaders,
|
||||
int HiddenClasses) : JavaRuntimeEvent("java.class.statistics", Ts);
|
||||
|
||||
/// <summary>
|
||||
/// Summary metadata for a runtime trace session.
|
||||
/// </summary>
|
||||
/// <param name="StartTime">Trace session start time.</param>
|
||||
/// <param name="EndTime">Trace session end time.</param>
|
||||
/// <param name="JavaVersion">Java version string.</param>
|
||||
/// <param name="JavaVendor">Java vendor string.</param>
|
||||
/// <param name="JvmName">JVM name (e.g., "OpenJDK 64-Bit Server VM").</param>
|
||||
/// <param name="JvmArgs">Sanitized JVM arguments (secrets redacted).</param>
|
||||
/// <param name="ClassLoadCount">Total class load events.</param>
|
||||
/// <param name="ServiceLoaderCount">Total ServiceLoader events.</param>
|
||||
/// <param name="NativeLoadCount">Total native library load events.</param>
|
||||
/// <param name="ReflectionCount">Total reflection events.</param>
|
||||
/// <param name="ResourceAccessCount">Total resource access events.</param>
|
||||
/// <param name="ModuleResolveCount">Total module resolution events.</param>
|
||||
internal sealed record JavaRuntimeTraceSummary(
|
||||
DateTimeOffset StartTime,
|
||||
DateTimeOffset EndTime,
|
||||
string? JavaVersion,
|
||||
string? JavaVendor,
|
||||
string? JvmName,
|
||||
IReadOnlyList<string>? JvmArgs,
|
||||
int ClassLoadCount,
|
||||
int ServiceLoaderCount,
|
||||
int NativeLoadCount,
|
||||
int ReflectionCount,
|
||||
int ResourceAccessCount,
|
||||
int ModuleResolveCount);
|
||||
@@ -0,0 +1,211 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Result of Java runtime trace ingestion per task 21-010.
|
||||
/// Contains parsed events and derived runtime edges for entrypoint resolution.
|
||||
/// </summary>
|
||||
/// <param name="Events">All parsed runtime events.</param>
|
||||
/// <param name="RuntimeEdges">Edges derived from runtime observation.</param>
|
||||
/// <param name="RuntimeEntrypoints">Entrypoints discovered through runtime execution.</param>
|
||||
/// <param name="Summary">Summary metadata for the trace session.</param>
|
||||
/// <param name="Warnings">Warnings encountered during parsing/ingestion.</param>
|
||||
/// <param name="ContentHash">SHA-256 hash of the trace content for deterministic identification.</param>
|
||||
internal sealed record JavaRuntimeIngestion(
|
||||
ImmutableArray<JavaRuntimeEvent> Events,
|
||||
ImmutableArray<JavaRuntimeEdge> RuntimeEdges,
|
||||
ImmutableArray<JavaRuntimeEntrypoint> RuntimeEntrypoints,
|
||||
JavaRuntimeTraceSummary Summary,
|
||||
ImmutableArray<JavaRuntimeIngestionWarning> Warnings,
|
||||
string ContentHash)
|
||||
{
|
||||
public static readonly JavaRuntimeIngestion Empty = new(
|
||||
ImmutableArray<JavaRuntimeEvent>.Empty,
|
||||
ImmutableArray<JavaRuntimeEdge>.Empty,
|
||||
ImmutableArray<JavaRuntimeEntrypoint>.Empty,
|
||||
new JavaRuntimeTraceSummary(
|
||||
StartTime: DateTimeOffset.MinValue,
|
||||
EndTime: DateTimeOffset.MinValue,
|
||||
JavaVersion: null,
|
||||
JavaVendor: null,
|
||||
JvmName: null,
|
||||
JvmArgs: null,
|
||||
ClassLoadCount: 0,
|
||||
ServiceLoaderCount: 0,
|
||||
NativeLoadCount: 0,
|
||||
ReflectionCount: 0,
|
||||
ResourceAccessCount: 0,
|
||||
ModuleResolveCount: 0),
|
||||
ImmutableArray<JavaRuntimeIngestionWarning>.Empty,
|
||||
string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A runtime edge observed during Java execution.
|
||||
/// These are append-only edges that augment static analysis with runtime evidence.
|
||||
/// </summary>
|
||||
/// <param name="EdgeId">Deterministic edge identifier.</param>
|
||||
/// <param name="SourceClass">Class that initiated the load/lookup.</param>
|
||||
/// <param name="TargetClass">Class/resource/library that was loaded.</param>
|
||||
/// <param name="EdgeType">Type of runtime edge.</param>
|
||||
/// <param name="Reason">Detailed reason code for the edge.</param>
|
||||
/// <param name="Timestamp">When the edge was observed.</param>
|
||||
/// <param name="Source">JAR/module where target was loaded from.</param>
|
||||
/// <param name="SourceHash">SHA-256 hash of source path.</param>
|
||||
/// <param name="Confidence">Confidence level (runtime edges are typically 1.0).</param>
|
||||
/// <param name="Details">Additional details about the edge.</param>
|
||||
internal sealed record JavaRuntimeEdge(
|
||||
string EdgeId,
|
||||
string? SourceClass,
|
||||
string TargetClass,
|
||||
JavaRuntimeEdgeType EdgeType,
|
||||
JavaRuntimeEdgeReason Reason,
|
||||
DateTimeOffset Timestamp,
|
||||
string? Source,
|
||||
string? SourceHash,
|
||||
double Confidence,
|
||||
string? Details);
|
||||
|
||||
/// <summary>
|
||||
/// An entrypoint discovered through runtime execution.
|
||||
/// These are classes/methods that were actually invoked during execution.
|
||||
/// </summary>
|
||||
/// <param name="EntrypointId">Deterministic identifier.</param>
|
||||
/// <param name="ClassName">Fully qualified class name.</param>
|
||||
/// <param name="MethodName">Method name if applicable.</param>
|
||||
/// <param name="EntrypointType">Type of runtime entrypoint.</param>
|
||||
/// <param name="FirstSeen">First observation timestamp.</param>
|
||||
/// <param name="InvocationCount">Number of times this entrypoint was observed.</param>
|
||||
/// <param name="Source">JAR/module containing the entrypoint.</param>
|
||||
/// <param name="SourceHash">SHA-256 hash of source path.</param>
|
||||
/// <param name="Confidence">Confidence level.</param>
|
||||
internal sealed record JavaRuntimeEntrypoint(
|
||||
string EntrypointId,
|
||||
string ClassName,
|
||||
string? MethodName,
|
||||
JavaRuntimeEntrypointType EntrypointType,
|
||||
DateTimeOffset FirstSeen,
|
||||
int InvocationCount,
|
||||
string? Source,
|
||||
string? SourceHash,
|
||||
double Confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Warning encountered during runtime ingestion.
|
||||
/// </summary>
|
||||
/// <param name="WarningCode">Machine-readable warning code.</param>
|
||||
/// <param name="Message">Human-readable message.</param>
|
||||
/// <param name="Line">Line number in trace file if applicable.</param>
|
||||
/// <param name="Details">Additional context.</param>
|
||||
internal sealed record JavaRuntimeIngestionWarning(
|
||||
string WarningCode,
|
||||
string Message,
|
||||
int? Line,
|
||||
string? Details);
|
||||
|
||||
/// <summary>
|
||||
/// Types of runtime edges (observed during execution).
|
||||
/// </summary>
|
||||
internal enum JavaRuntimeEdgeType
|
||||
{
|
||||
/// <summary>Class was loaded during runtime.</summary>
|
||||
RuntimeClass,
|
||||
|
||||
/// <summary>ServiceLoader discovered provider at runtime.</summary>
|
||||
RuntimeSpi,
|
||||
|
||||
/// <summary>Native library was loaded at runtime.</summary>
|
||||
RuntimeNativeLoad,
|
||||
|
||||
/// <summary>Reflection-based class access at runtime.</summary>
|
||||
RuntimeReflection,
|
||||
|
||||
/// <summary>Resource was accessed at runtime.</summary>
|
||||
RuntimeResource,
|
||||
|
||||
/// <summary>Module was resolved at runtime.</summary>
|
||||
RuntimeModule,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reason codes for runtime edges (more specific than edge type).
|
||||
/// </summary>
|
||||
internal enum JavaRuntimeEdgeReason
|
||||
{
|
||||
// Class loading reasons
|
||||
ClassLoadBootstrap,
|
||||
ClassLoadPlatform,
|
||||
ClassLoadApplication,
|
||||
ClassLoadCustom,
|
||||
|
||||
// ServiceLoader reasons
|
||||
ServiceLoaderExplicit,
|
||||
ServiceLoaderModuleInfo,
|
||||
ServiceLoaderMetaInf,
|
||||
|
||||
// Native load reasons
|
||||
SystemLoad,
|
||||
SystemLoadLibrary,
|
||||
RuntimeLoad,
|
||||
RuntimeLoadLibrary,
|
||||
NativeLoadFailure,
|
||||
|
||||
// Reflection reasons
|
||||
ClassForName,
|
||||
ClassNewInstance,
|
||||
ConstructorNewInstance,
|
||||
MethodInvoke,
|
||||
|
||||
// Resource reasons
|
||||
GetResource,
|
||||
GetResourceAsStream,
|
||||
GetResources,
|
||||
|
||||
// Module reasons
|
||||
ModuleRequires,
|
||||
ModuleOpens,
|
||||
ModuleExports,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types of runtime entrypoints (discovered during execution).
|
||||
/// </summary>
|
||||
internal enum JavaRuntimeEntrypointType
|
||||
{
|
||||
/// <summary>Main method was executed.</summary>
|
||||
MainMethod,
|
||||
|
||||
/// <summary>ServiceLoader provider was instantiated.</summary>
|
||||
ServiceProvider,
|
||||
|
||||
/// <summary>Reflection target was accessed.</summary>
|
||||
ReflectionTarget,
|
||||
|
||||
/// <summary>Native method was called (JNI callback).</summary>
|
||||
NativeCallback,
|
||||
|
||||
/// <summary>CDI/Spring bean was instantiated.</summary>
|
||||
ManagedBean,
|
||||
|
||||
/// <summary>Servlet/filter was initialized.</summary>
|
||||
WebComponent,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for runtime ingestion behavior.
|
||||
/// </summary>
|
||||
/// <param name="ScrubPaths">Whether to hash/scrub file paths for privacy.</param>
|
||||
/// <param name="IncludeJdkClasses">Whether to include JDK internal class loads.</param>
|
||||
/// <param name="IncludeStatistics">Whether to process statistics events.</param>
|
||||
/// <param name="MaxEvents">Maximum number of events to process (0 = unlimited).</param>
|
||||
/// <param name="DeduplicateEdges">Whether to deduplicate identical edges.</param>
|
||||
internal sealed record JavaRuntimeIngestionConfig(
|
||||
bool ScrubPaths = true,
|
||||
bool IncludeJdkClasses = false,
|
||||
bool IncludeStatistics = true,
|
||||
int MaxEvents = 0,
|
||||
bool DeduplicateEdges = true)
|
||||
{
|
||||
public static readonly JavaRuntimeIngestionConfig Default = new();
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Resolver;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Main entry point for Java runtime trace ingestion (task 21-010).
|
||||
/// Ingests NDJSON trace files from Java agent or JFR and produces runtime edges.
|
||||
/// </summary>
|
||||
internal static class JavaRuntimeIngestor
|
||||
{
|
||||
/// <summary>
|
||||
/// Ingests a runtime trace file and returns runtime edges and entrypoints.
|
||||
/// </summary>
|
||||
/// <param name="stream">Stream containing NDJSON trace data.</param>
|
||||
/// <param name="config">Ingestion configuration.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Ingestion result with runtime edges and entrypoints.</returns>
|
||||
public static async Task<JavaRuntimeIngestion> IngestAsync(
|
||||
Stream stream,
|
||||
JavaRuntimeIngestionConfig? config = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
config ??= JavaRuntimeIngestionConfig.Default;
|
||||
|
||||
// Parse events from NDJSON
|
||||
var (events, warnings, contentHash) = await JavaRuntimeEventParser.ParseAsync(
|
||||
stream,
|
||||
config,
|
||||
cancellationToken);
|
||||
|
||||
// Resolve edges from events
|
||||
return JavaRuntimeEdgeResolver.ResolveFromEvents(
|
||||
events,
|
||||
warnings,
|
||||
contentHash,
|
||||
config,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ingests a runtime trace file from a file path.
|
||||
/// </summary>
|
||||
public static async Task<JavaRuntimeIngestion> IngestFromFileAsync(
|
||||
string filePath,
|
||||
JavaRuntimeIngestionConfig? config = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
return await IngestAsync(stream, config, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges runtime edges into an existing entrypoint resolution.
|
||||
/// Creates a new resolution with combined static and runtime evidence.
|
||||
/// </summary>
|
||||
/// <param name="staticResolution">Resolution from static analysis (21-005/006/007/008).</param>
|
||||
/// <param name="runtimeIngestion">Ingestion result from runtime trace.</param>
|
||||
/// <returns>Combined resolution with runtime edges appended.</returns>
|
||||
public static JavaEntrypointResolution MergeRuntimeEdges(
|
||||
JavaEntrypointResolution staticResolution,
|
||||
JavaRuntimeIngestion runtimeIngestion)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(staticResolution);
|
||||
ArgumentNullException.ThrowIfNull(runtimeIngestion);
|
||||
|
||||
// Convert runtime edges to resolved edges
|
||||
var convertedEdges = runtimeIngestion.RuntimeEdges
|
||||
.Select(ConvertRuntimeEdge)
|
||||
.ToImmutableArray();
|
||||
|
||||
// Convert runtime entrypoints to resolved entrypoints
|
||||
var convertedEntrypoints = runtimeIngestion.RuntimeEntrypoints
|
||||
.Select(ConvertRuntimeEntrypoint)
|
||||
.ToImmutableArray();
|
||||
|
||||
// Merge edges (runtime edges appended after static)
|
||||
var allEdges = staticResolution.Edges.AddRange(convertedEdges);
|
||||
|
||||
// Merge entrypoints (avoid duplicates by class name)
|
||||
var existingClasses = staticResolution.Entrypoints
|
||||
.Select(e => e.ClassFqcn)
|
||||
.ToHashSet();
|
||||
|
||||
var newEntrypoints = convertedEntrypoints
|
||||
.Where(e => !existingClasses.Contains(e.ClassFqcn))
|
||||
.ToImmutableArray();
|
||||
|
||||
var allEntrypoints = staticResolution.Entrypoints.AddRange(newEntrypoints);
|
||||
|
||||
// Add runtime warnings as resolution warnings
|
||||
var runtimeWarnings = runtimeIngestion.Warnings
|
||||
.Select(w => new JavaResolutionWarning(
|
||||
w.WarningCode,
|
||||
w.Message,
|
||||
null,
|
||||
w.Details))
|
||||
.ToImmutableArray();
|
||||
|
||||
var allWarnings = staticResolution.Warnings.AddRange(runtimeWarnings);
|
||||
|
||||
// Recalculate statistics
|
||||
var statistics = RecalculateStatistics(
|
||||
allEntrypoints,
|
||||
staticResolution.Components,
|
||||
allEdges,
|
||||
staticResolution.Statistics.ResolutionDuration);
|
||||
|
||||
return new JavaEntrypointResolution(
|
||||
allEntrypoints,
|
||||
staticResolution.Components,
|
||||
allEdges,
|
||||
statistics,
|
||||
allWarnings);
|
||||
}
|
||||
|
||||
private static JavaResolvedEdge ConvertRuntimeEdge(JavaRuntimeEdge edge)
|
||||
{
|
||||
var edgeType = edge.EdgeType switch
|
||||
{
|
||||
JavaRuntimeEdgeType.RuntimeClass => JavaEdgeType.ReflectionLoad,
|
||||
JavaRuntimeEdgeType.RuntimeSpi => JavaEdgeType.ServiceProvider,
|
||||
JavaRuntimeEdgeType.RuntimeNativeLoad => JavaEdgeType.JniNativeLib,
|
||||
JavaRuntimeEdgeType.RuntimeReflection => JavaEdgeType.ReflectionLoad,
|
||||
JavaRuntimeEdgeType.RuntimeResource => JavaEdgeType.ResourceBundle,
|
||||
JavaRuntimeEdgeType.RuntimeModule => JavaEdgeType.JpmsRequires,
|
||||
_ => JavaEdgeType.ReflectionLoad,
|
||||
};
|
||||
|
||||
var reason = edge.Reason switch
|
||||
{
|
||||
JavaRuntimeEdgeReason.ClassLoadBootstrap => JavaEdgeReason.ClassLoaderLoadClass,
|
||||
JavaRuntimeEdgeReason.ClassLoadPlatform => JavaEdgeReason.ClassLoaderLoadClass,
|
||||
JavaRuntimeEdgeReason.ClassLoadApplication => JavaEdgeReason.ClassLoaderLoadClass,
|
||||
JavaRuntimeEdgeReason.ClassLoadCustom => JavaEdgeReason.ClassLoaderLoadClass,
|
||||
JavaRuntimeEdgeReason.ServiceLoaderExplicit => JavaEdgeReason.MetaInfServices,
|
||||
JavaRuntimeEdgeReason.ServiceLoaderModuleInfo => JavaEdgeReason.ModuleInfoProvides,
|
||||
JavaRuntimeEdgeReason.ServiceLoaderMetaInf => JavaEdgeReason.MetaInfServices,
|
||||
JavaRuntimeEdgeReason.SystemLoad => JavaEdgeReason.SystemLoad,
|
||||
JavaRuntimeEdgeReason.SystemLoadLibrary => JavaEdgeReason.SystemLoadLibrary,
|
||||
JavaRuntimeEdgeReason.RuntimeLoad => JavaEdgeReason.RuntimeLoadLibrary,
|
||||
JavaRuntimeEdgeReason.RuntimeLoadLibrary => JavaEdgeReason.RuntimeLoadLibrary,
|
||||
JavaRuntimeEdgeReason.ClassForName => JavaEdgeReason.ClassForName,
|
||||
JavaRuntimeEdgeReason.ClassNewInstance => JavaEdgeReason.ConstructorNewInstance,
|
||||
JavaRuntimeEdgeReason.ConstructorNewInstance => JavaEdgeReason.ConstructorNewInstance,
|
||||
JavaRuntimeEdgeReason.MethodInvoke => JavaEdgeReason.MethodInvoke,
|
||||
JavaRuntimeEdgeReason.GetResource => JavaEdgeReason.ResourceReference,
|
||||
JavaRuntimeEdgeReason.GetResourceAsStream => JavaEdgeReason.ResourceReference,
|
||||
JavaRuntimeEdgeReason.GetResources => JavaEdgeReason.ResourceReference,
|
||||
JavaRuntimeEdgeReason.ModuleRequires => JavaEdgeReason.JpmsRequiresTransitive,
|
||||
_ => JavaEdgeReason.ClassForName,
|
||||
};
|
||||
|
||||
return new JavaResolvedEdge(
|
||||
EdgeId: edge.EdgeId,
|
||||
SourceId: edge.SourceClass ?? "runtime",
|
||||
TargetId: edge.TargetClass,
|
||||
EdgeType: edgeType,
|
||||
Reason: reason,
|
||||
Confidence: edge.Confidence,
|
||||
SegmentIdentifier: edge.Source ?? "runtime",
|
||||
Details: $"[runtime] {edge.Details}");
|
||||
}
|
||||
|
||||
private static JavaResolvedEntrypoint ConvertRuntimeEntrypoint(JavaRuntimeEntrypoint entry)
|
||||
{
|
||||
var entrypointType = entry.EntrypointType switch
|
||||
{
|
||||
JavaRuntimeEntrypointType.MainMethod => JavaEntrypointType.MainClass,
|
||||
JavaRuntimeEntrypointType.ServiceProvider => JavaEntrypointType.ServiceProvider,
|
||||
JavaRuntimeEntrypointType.ReflectionTarget => JavaEntrypointType.ServiceProvider, // Mapped to ServiceProvider for simplicity
|
||||
JavaRuntimeEntrypointType.NativeCallback => JavaEntrypointType.NativeMethod,
|
||||
JavaRuntimeEntrypointType.ManagedBean => JavaEntrypointType.CdiObserver,
|
||||
JavaRuntimeEntrypointType.WebComponent => JavaEntrypointType.Servlet,
|
||||
_ => JavaEntrypointType.ServiceProvider,
|
||||
};
|
||||
|
||||
return new JavaResolvedEntrypoint(
|
||||
EntrypointId: entry.EntrypointId,
|
||||
ClassFqcn: entry.ClassName,
|
||||
MethodName: entry.MethodName,
|
||||
MethodDescriptor: null,
|
||||
EntrypointType: entrypointType,
|
||||
SegmentIdentifier: entry.Source ?? "runtime",
|
||||
Framework: null,
|
||||
Confidence: entry.Confidence,
|
||||
ResolutionPath: ImmutableArray.Create("runtime-trace"),
|
||||
Metadata: ImmutableDictionary<string, string>.Empty
|
||||
.Add("runtime.invocation_count", entry.InvocationCount.ToString())
|
||||
.Add("runtime.first_seen", entry.FirstSeen.ToString("O")));
|
||||
}
|
||||
|
||||
private static JavaResolutionStatistics RecalculateStatistics(
|
||||
ImmutableArray<JavaResolvedEntrypoint> entrypoints,
|
||||
ImmutableArray<JavaResolvedComponent> components,
|
||||
ImmutableArray<JavaResolvedEdge> edges,
|
||||
TimeSpan originalDuration)
|
||||
{
|
||||
var entrypointsByType = entrypoints
|
||||
.GroupBy(e => e.EntrypointType)
|
||||
.ToImmutableDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
var edgesByType = edges
|
||||
.GroupBy(e => e.EdgeType)
|
||||
.ToImmutableDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
var entrypointsByFramework = entrypoints
|
||||
.Where(e => e.Framework is not null)
|
||||
.GroupBy(e => e.Framework!)
|
||||
.ToImmutableDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
var highConfidence = entrypoints.Count(e => e.Confidence >= 0.8);
|
||||
var mediumConfidence = entrypoints.Count(e => e.Confidence >= 0.5 && e.Confidence < 0.8);
|
||||
var lowConfidence = entrypoints.Count(e => e.Confidence < 0.5);
|
||||
|
||||
var signedComponents = components.Count(c => c.IsSigned);
|
||||
var modularComponents = components.Count(c => c.ModuleInfo is not null);
|
||||
|
||||
return new JavaResolutionStatistics(
|
||||
TotalEntrypoints: entrypoints.Length,
|
||||
TotalComponents: components.Length,
|
||||
TotalEdges: edges.Length,
|
||||
EntrypointsByType: entrypointsByType,
|
||||
EdgesByType: edgesByType,
|
||||
EntrypointsByFramework: entrypointsByFramework,
|
||||
HighConfidenceCount: highConfidence,
|
||||
MediumConfidenceCount: mediumConfidence,
|
||||
LowConfidenceCount: lowConfidence,
|
||||
SignedComponents: signedComponents,
|
||||
ModularComponents: modularComponents,
|
||||
ResolutionDuration: originalDuration);
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ internal enum PhpCapabilityKind
|
||||
/// <summary>
|
||||
/// Risk levels for capability usage.
|
||||
/// </summary>
|
||||
internal enum PhpCapabilityRisk
|
||||
public enum PhpCapabilityRisk
|
||||
{
|
||||
/// <summary>Low risk, common usage patterns.</summary>
|
||||
Low,
|
||||
|
||||
@@ -17,21 +17,26 @@ internal static partial class PhpVersionConflictDetector
|
||||
{
|
||||
var conflicts = new List<PhpVersionConflict>();
|
||||
|
||||
if (manifest is null || lockData is null || lockData.IsEmpty)
|
||||
if (manifest is null)
|
||||
{
|
||||
return PhpConflictAnalysis.Empty;
|
||||
}
|
||||
|
||||
// Combine all locked packages
|
||||
var lockedPackages = lockData.Packages
|
||||
.Concat(lockData.DevPackages)
|
||||
// Combine all locked packages (may be empty if lockData is null/empty)
|
||||
var lockedPackages = (lockData?.Packages ?? [])
|
||||
.Concat(lockData?.DevPackages ?? [])
|
||||
.ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Check for missing platform requirements (php version, extensions)
|
||||
conflicts.AddRange(AnalyzePlatformRequirements(manifest));
|
||||
|
||||
// Check for packages in manifest.require that might have constraint issues
|
||||
conflicts.AddRange(AnalyzeRequireConstraints(manifest, lockedPackages));
|
||||
// Only check require constraints if we have a valid lock file to compare against
|
||||
// (LockPath being set indicates a lock file exists, even if empty)
|
||||
if (lockData is not null && !string.IsNullOrEmpty(lockData.LockPath))
|
||||
{
|
||||
// Check for packages in manifest.require that might have constraint issues
|
||||
conflicts.AddRange(AnalyzeRequireConstraints(manifest, lockedPackages));
|
||||
}
|
||||
|
||||
// Check for packages with unstable versions
|
||||
conflicts.AddRange(AnalyzeUnstableVersions(lockedPackages.Values));
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
global using System.Collections.Immutable;
|
||||
global using System.Security.Cryptography;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
global using System.Text.Json.Serialization;
|
||||
@@ -0,0 +1,337 @@
|
||||
using StellaOps.Scanner.Analyzers.Native.Internal.Elf;
|
||||
using StellaOps.Scanner.Analyzers.Native.Internal.Graph;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Internal.Callgraph;
|
||||
|
||||
/// <summary>
|
||||
/// Builds native reachability graphs from ELF files.
|
||||
/// Extracts functions, call edges, synthetic roots, and emits unknowns.
|
||||
/// </summary>
|
||||
internal sealed class NativeCallgraphBuilder
|
||||
{
|
||||
private readonly Dictionary<string, NativeFunctionNode> _functions = new();
|
||||
private readonly List<NativeCallEdge> _edges = new();
|
||||
private readonly List<NativeSyntheticRoot> _roots = new();
|
||||
private readonly List<NativeUnknown> _unknowns = new();
|
||||
private readonly Dictionary<ulong, string> _addressToSymbolId = new();
|
||||
private readonly string _layerDigest;
|
||||
private int _binaryCount;
|
||||
|
||||
public NativeCallgraphBuilder(string layerDigest)
|
||||
{
|
||||
_layerDigest = layerDigest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an ELF file to the graph.
|
||||
/// </summary>
|
||||
public void AddElfFile(ElfFile elf)
|
||||
{
|
||||
_binaryCount++;
|
||||
|
||||
// Add function symbols
|
||||
foreach (var sym in elf.Symbols.Concat(elf.DynamicSymbols))
|
||||
{
|
||||
if (sym.Type != ElfSymbolType.Func || string.IsNullOrEmpty(sym.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddFunction(sym, elf);
|
||||
}
|
||||
|
||||
// Add synthetic roots for _start, _init, main
|
||||
AddSyntheticRoots(elf);
|
||||
|
||||
// Add edges from relocations
|
||||
AddRelocationEdges(elf);
|
||||
|
||||
// Add edges from init arrays
|
||||
AddInitArrayEdges(elf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the final reachability graph.
|
||||
/// </summary>
|
||||
public NativeReachabilityGraph Build()
|
||||
{
|
||||
var functions = _functions.Values
|
||||
.OrderBy(f => f.BinaryPath)
|
||||
.ThenBy(f => f.Address)
|
||||
.ToImmutableArray();
|
||||
|
||||
var edges = _edges
|
||||
.OrderBy(e => e.CallerId)
|
||||
.ThenBy(e => e.CallSiteOffset)
|
||||
.ToImmutableArray();
|
||||
|
||||
var roots = _roots
|
||||
.OrderBy(r => r.BinaryPath)
|
||||
.ThenBy(r => r.Phase)
|
||||
.ThenBy(r => r.Order)
|
||||
.ToImmutableArray();
|
||||
|
||||
var unknowns = _unknowns
|
||||
.OrderBy(u => u.BinaryPath)
|
||||
.ThenBy(u => u.SourceId)
|
||||
.ToImmutableArray();
|
||||
|
||||
var contentHash = NativeGraphIdentifiers.ComputeGraphHash(functions, edges, roots);
|
||||
|
||||
var metadata = new NativeGraphMetadata(
|
||||
GeneratedAt: DateTimeOffset.UtcNow,
|
||||
GeneratorVersion: NativeGraphIdentifiers.GetGeneratorVersion(),
|
||||
LayerDigest: _layerDigest,
|
||||
BinaryCount: _binaryCount,
|
||||
FunctionCount: functions.Length,
|
||||
EdgeCount: edges.Length,
|
||||
UnknownCount: unknowns.Length,
|
||||
SyntheticRootCount: roots.Length);
|
||||
|
||||
return new NativeReachabilityGraph(
|
||||
_layerDigest,
|
||||
functions,
|
||||
edges,
|
||||
roots,
|
||||
unknowns,
|
||||
metadata,
|
||||
contentHash);
|
||||
}
|
||||
|
||||
private void AddFunction(ElfSymbol sym, ElfFile elf)
|
||||
{
|
||||
var binding = sym.Binding.ToString().ToLowerInvariant();
|
||||
var visibility = sym.Visibility.ToString().ToLowerInvariant();
|
||||
|
||||
var symbolId = NativeGraphIdentifiers.ComputeSymbolId(sym.Name, sym.Value, sym.Size, binding);
|
||||
var symbolDigest = NativeGraphIdentifiers.ComputeSymbolDigest(sym.Name, sym.Value, sym.Size, binding);
|
||||
|
||||
// Generate PURL based on binary path (simplified - would use proper package mapping in production)
|
||||
var purl = GeneratePurl(elf.Path, sym.Name);
|
||||
|
||||
var isExported = sym.Binding == ElfSymbolBinding.Global && sym.Visibility == ElfSymbolVisibility.Default;
|
||||
|
||||
var func = new NativeFunctionNode(
|
||||
SymbolId: symbolId,
|
||||
Name: sym.Name,
|
||||
Purl: purl,
|
||||
BinaryPath: elf.Path,
|
||||
BuildId: elf.BuildId,
|
||||
Address: sym.Value,
|
||||
Size: sym.Size,
|
||||
SymbolDigest: symbolDigest,
|
||||
Binding: binding,
|
||||
Visibility: visibility,
|
||||
IsExported: isExported);
|
||||
|
||||
_functions.TryAdd(symbolId, func);
|
||||
_addressToSymbolId.TryAdd(sym.Value, symbolId);
|
||||
}
|
||||
|
||||
private void AddSyntheticRoots(ElfFile elf)
|
||||
{
|
||||
// Find and add _start
|
||||
AddRootIfExists(elf, "_start", NativeRootType.Start, "load", 0);
|
||||
|
||||
// Find and add _init
|
||||
AddRootIfExists(elf, "_init", NativeRootType.Init, "init", 0);
|
||||
|
||||
// Find and add _fini
|
||||
AddRootIfExists(elf, "_fini", NativeRootType.Fini, "fini", 0);
|
||||
|
||||
// Find and add main
|
||||
AddRootIfExists(elf, "main", NativeRootType.Main, "main", 0);
|
||||
|
||||
// Add preinit_array entries
|
||||
for (var i = 0; i < elf.PreInitArraySymbols.Length; i++)
|
||||
{
|
||||
var symName = elf.PreInitArraySymbols[i];
|
||||
AddRootByName(elf, symName, NativeRootType.PreInitArray, "preinit", i);
|
||||
}
|
||||
|
||||
// Add init_array entries
|
||||
for (var i = 0; i < elf.InitArraySymbols.Length; i++)
|
||||
{
|
||||
var symName = elf.InitArraySymbols[i];
|
||||
AddRootByName(elf, symName, NativeRootType.InitArray, "init", i);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRootIfExists(ElfFile elf, string symbolName, NativeRootType rootType, string phase, int order)
|
||||
{
|
||||
var sym = elf.Symbols.Concat(elf.DynamicSymbols)
|
||||
.FirstOrDefault(s => s.Name == symbolName && s.Type == ElfSymbolType.Func);
|
||||
|
||||
if (sym is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var binding = sym.Binding.ToString().ToLowerInvariant();
|
||||
var symbolId = NativeGraphIdentifiers.ComputeSymbolId(sym.Name, sym.Value, sym.Size, binding);
|
||||
|
||||
var rootId = NativeGraphIdentifiers.ComputeRootId(symbolId, rootType, order);
|
||||
|
||||
_roots.Add(new NativeSyntheticRoot(
|
||||
RootId: rootId,
|
||||
TargetId: symbolId,
|
||||
RootType: rootType,
|
||||
BinaryPath: elf.Path,
|
||||
Phase: phase,
|
||||
Order: order));
|
||||
}
|
||||
|
||||
private void AddRootByName(ElfFile elf, string symbolName, NativeRootType rootType, string phase, int order)
|
||||
{
|
||||
// Check if it's a hex address placeholder
|
||||
if (symbolName.StartsWith("func_0x", StringComparison.Ordinal))
|
||||
{
|
||||
// Create an unknown for unresolved init array entry
|
||||
var unknownId = NativeGraphIdentifiers.ComputeUnknownId(symbolName, NativeUnknownType.UnresolvedTarget, symbolName);
|
||||
_unknowns.Add(new NativeUnknown(
|
||||
UnknownId: unknownId,
|
||||
UnknownType: NativeUnknownType.UnresolvedTarget,
|
||||
SourceId: $"{elf.Path}:{phase}:{order}",
|
||||
Name: symbolName,
|
||||
Reason: "Init array entry could not be resolved to a symbol",
|
||||
BinaryPath: elf.Path));
|
||||
return;
|
||||
}
|
||||
|
||||
AddRootIfExists(elf, symbolName, rootType, phase, order);
|
||||
}
|
||||
|
||||
private void AddRelocationEdges(ElfFile elf)
|
||||
{
|
||||
var allSymbols = elf.Symbols.Concat(elf.DynamicSymbols).ToList();
|
||||
|
||||
foreach (var reloc in elf.Relocations)
|
||||
{
|
||||
if (reloc.SymbolIndex == 0 || reloc.SymbolIndex >= allSymbols.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetSym = allSymbols[(int)reloc.SymbolIndex];
|
||||
if (targetSym.Type != ElfSymbolType.Func || string.IsNullOrEmpty(targetSym.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the function containing this relocation
|
||||
var callerSym = FindFunctionContainingAddress(allSymbols, reloc.Offset);
|
||||
if (callerSym is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var callerBinding = callerSym.Binding.ToString().ToLowerInvariant();
|
||||
var targetBinding = targetSym.Binding.ToString().ToLowerInvariant();
|
||||
|
||||
var callerId = NativeGraphIdentifiers.ComputeSymbolId(callerSym.Name, callerSym.Value, callerSym.Size, callerBinding);
|
||||
var calleeId = NativeGraphIdentifiers.ComputeSymbolId(targetSym.Name, targetSym.Value, targetSym.Size, targetBinding);
|
||||
var calleeDigest = NativeGraphIdentifiers.ComputeSymbolDigest(targetSym.Name, targetSym.Value, targetSym.Size, targetBinding);
|
||||
|
||||
var edgeId = NativeGraphIdentifiers.ComputeEdgeId(callerId, calleeId, reloc.Offset);
|
||||
|
||||
// Determine if target is resolved (has a defined address)
|
||||
var isResolved = targetSym.Value != 0 || targetSym.SectionIndex != 0;
|
||||
var calleePurl = isResolved ? GeneratePurl(elf.Path, targetSym.Name) : null;
|
||||
|
||||
_edges.Add(new NativeCallEdge(
|
||||
EdgeId: edgeId,
|
||||
CallerId: callerId,
|
||||
CalleeId: calleeId,
|
||||
CalleePurl: calleePurl,
|
||||
CalleeSymbolDigest: calleeDigest,
|
||||
EdgeType: NativeEdgeType.Relocation,
|
||||
CallSiteOffset: reloc.Offset,
|
||||
IsResolved: isResolved,
|
||||
Confidence: isResolved ? 1.0 : 0.5));
|
||||
|
||||
if (!isResolved)
|
||||
{
|
||||
// Emit unknown for unresolved external symbol
|
||||
var unknownId = NativeGraphIdentifiers.ComputeUnknownId(edgeId, NativeUnknownType.UnresolvedTarget, targetSym.Name);
|
||||
_unknowns.Add(new NativeUnknown(
|
||||
UnknownId: unknownId,
|
||||
UnknownType: NativeUnknownType.UnresolvedTarget,
|
||||
SourceId: edgeId,
|
||||
Name: targetSym.Name,
|
||||
Reason: "External symbol not resolved within this layer",
|
||||
BinaryPath: elf.Path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddInitArrayEdges(ElfFile elf)
|
||||
{
|
||||
var allSymbols = elf.Symbols.Concat(elf.DynamicSymbols).ToList();
|
||||
|
||||
// Add edges from synthetic _init root to init_array entries
|
||||
var initSym = allSymbols.FirstOrDefault(s => s.Name == "_init" && s.Type == ElfSymbolType.Func);
|
||||
if (initSym is not null)
|
||||
{
|
||||
var initBinding = initSym.Binding.ToString().ToLowerInvariant();
|
||||
var initId = NativeGraphIdentifiers.ComputeSymbolId(initSym.Name, initSym.Value, initSym.Size, initBinding);
|
||||
|
||||
foreach (var (symName, idx) in elf.InitArraySymbols.Select((s, i) => (s, i)))
|
||||
{
|
||||
if (symName.StartsWith("func_0x", StringComparison.Ordinal))
|
||||
{
|
||||
continue; // Already handled as unknown
|
||||
}
|
||||
|
||||
var targetSym = allSymbols.FirstOrDefault(s => s.Name == symName && s.Type == ElfSymbolType.Func);
|
||||
if (targetSym is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetBinding = targetSym.Binding.ToString().ToLowerInvariant();
|
||||
var targetId = NativeGraphIdentifiers.ComputeSymbolId(targetSym.Name, targetSym.Value, targetSym.Size, targetBinding);
|
||||
var targetDigest = NativeGraphIdentifiers.ComputeSymbolDigest(targetSym.Name, targetSym.Value, targetSym.Size, targetBinding);
|
||||
|
||||
var edgeId = NativeGraphIdentifiers.ComputeEdgeId(initId, targetId, (ulong)idx);
|
||||
|
||||
_edges.Add(new NativeCallEdge(
|
||||
EdgeId: edgeId,
|
||||
CallerId: initId,
|
||||
CalleeId: targetId,
|
||||
CalleePurl: GeneratePurl(elf.Path, targetSym.Name),
|
||||
CalleeSymbolDigest: targetDigest,
|
||||
EdgeType: NativeEdgeType.InitArray,
|
||||
CallSiteOffset: (ulong)idx,
|
||||
IsResolved: true,
|
||||
Confidence: 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ElfSymbol? FindFunctionContainingAddress(IList<ElfSymbol> symbols, ulong address)
|
||||
{
|
||||
return symbols
|
||||
.Where(s => s.Type == ElfSymbolType.Func && s.Size > 0)
|
||||
.FirstOrDefault(s => address >= s.Value && address < s.Value + s.Size);
|
||||
}
|
||||
|
||||
private static string? GeneratePurl(string binaryPath, string symbolName)
|
||||
{
|
||||
// Extract library name from path (simplified)
|
||||
var fileName = Path.GetFileName(binaryPath);
|
||||
|
||||
// Handle common patterns like libfoo.so.1.2.3
|
||||
if (fileName.StartsWith("lib", StringComparison.Ordinal))
|
||||
{
|
||||
var soIndex = fileName.IndexOf(".so", StringComparison.Ordinal);
|
||||
if (soIndex > 3)
|
||||
{
|
||||
var libName = fileName[3..soIndex];
|
||||
return $"pkg:elf/{libName}#{symbolName}";
|
||||
}
|
||||
}
|
||||
|
||||
// For executables or other binaries
|
||||
return $"pkg:elf/{fileName}#{symbolName}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Internal.Elf;
|
||||
|
||||
/// <summary>
|
||||
/// Reads and parses ELF (Executable and Linkable Format) files.
|
||||
/// Extracts build-id, symbols, relocations, and init arrays for reachability analysis.
|
||||
/// </summary>
|
||||
internal static class ElfReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a file starts with ELF magic bytes.
|
||||
/// </summary>
|
||||
public static bool IsElf(ReadOnlySpan<byte> data) =>
|
||||
data.Length >= ElfMagic.IdentSize && data[..4].SequenceEqual(ElfMagic.Bytes);
|
||||
|
||||
/// <summary>
|
||||
/// Parses an ELF file from a stream.
|
||||
/// </summary>
|
||||
public static ElfFile? Parse(Stream stream, string path, string layerDigest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
Span<byte> ident = stackalloc byte[ElfMagic.IdentSize];
|
||||
if (stream.Read(ident) < ElfMagic.IdentSize || !IsElf(ident))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var elfClass = (ElfClass)ident[4];
|
||||
var elfData = (ElfData)ident[5];
|
||||
|
||||
if (elfClass is not (ElfClass.Elf32 or ElfClass.Elf64))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var isLittleEndian = elfData == ElfData.Lsb;
|
||||
var is64Bit = elfClass == ElfClass.Elf64;
|
||||
|
||||
stream.Position = 0;
|
||||
var fileData = new byte[stream.Length];
|
||||
stream.ReadExactly(fileData);
|
||||
|
||||
return Parse(fileData, path, layerDigest, is64Bit, isLittleEndian);
|
||||
}
|
||||
|
||||
private static ElfFile Parse(byte[] data, string path, string layerDigest, bool is64Bit, bool isLittleEndian)
|
||||
{
|
||||
var reader = new ElfDataReader(data, isLittleEndian);
|
||||
|
||||
// Parse header
|
||||
var header = ParseHeader(reader, is64Bit);
|
||||
|
||||
// Parse section headers
|
||||
var sections = ParseSectionHeaders(reader, header, is64Bit);
|
||||
|
||||
// Get string table for section names
|
||||
var shStrTab = GetStringTable(data, sections, header.SectionNameStringTableIndex);
|
||||
|
||||
// Update section names
|
||||
sections = sections.Select(s => s with { Name = GetString(shStrTab, s.NameIndex) }).ToImmutableArray();
|
||||
|
||||
// Parse symbol tables
|
||||
var (symbols, symStrTab) = ParseSymbolTable(data, sections, ".symtab", is64Bit, isLittleEndian);
|
||||
var (dynSymbols, dynStrTab) = ParseSymbolTable(data, sections, ".dynsym", is64Bit, isLittleEndian);
|
||||
|
||||
// Update symbol names
|
||||
symbols = symbols.Select(s => s with { Name = GetString(symStrTab, s.NameIndex) }).ToImmutableArray();
|
||||
dynSymbols = dynSymbols.Select(s => s with { Name = GetString(dynStrTab, s.NameIndex) }).ToImmutableArray();
|
||||
|
||||
// Parse notes (for build-id)
|
||||
var notes = ParseNotes(data, sections, isLittleEndian);
|
||||
|
||||
// Extract build-id from GNU notes
|
||||
var buildId = ExtractBuildId(notes);
|
||||
var codeId = buildId is not null ? FormatCodeId(buildId) : null;
|
||||
|
||||
// Compute .text section hash as fallback identifier
|
||||
var textSectionHash = ComputeTextSectionHash(data, sections);
|
||||
|
||||
// Parse relocations
|
||||
var relocations = ParseRelocations(data, sections, is64Bit, isLittleEndian);
|
||||
|
||||
// Extract init array symbols
|
||||
var initArraySymbols = ExtractInitArraySymbols(data, sections, symbols, dynSymbols, is64Bit, isLittleEndian);
|
||||
var preInitArraySymbols = ExtractPreInitArraySymbols(data, sections, symbols, dynSymbols, is64Bit, isLittleEndian);
|
||||
|
||||
// Extract needed libraries from .dynamic section
|
||||
var neededLibraries = ExtractNeededLibraries(data, sections, is64Bit, isLittleEndian);
|
||||
|
||||
return new ElfFile(
|
||||
path,
|
||||
layerDigest,
|
||||
header,
|
||||
sections,
|
||||
symbols,
|
||||
dynSymbols,
|
||||
notes,
|
||||
relocations,
|
||||
buildId,
|
||||
codeId,
|
||||
textSectionHash,
|
||||
initArraySymbols,
|
||||
preInitArraySymbols,
|
||||
neededLibraries);
|
||||
}
|
||||
|
||||
private static ElfHeader ParseHeader(ElfDataReader reader, bool is64Bit)
|
||||
{
|
||||
reader.Position = 0;
|
||||
|
||||
// Skip e_ident (already validated)
|
||||
reader.Position = ElfMagic.IdentSize;
|
||||
|
||||
var type = (ElfType)reader.ReadUInt16();
|
||||
var machine = (ElfMachine)reader.ReadUInt16();
|
||||
var version = reader.ReadUInt32();
|
||||
|
||||
ulong entry, phOff, shOff;
|
||||
if (is64Bit)
|
||||
{
|
||||
entry = reader.ReadUInt64();
|
||||
phOff = reader.ReadUInt64();
|
||||
shOff = reader.ReadUInt64();
|
||||
}
|
||||
else
|
||||
{
|
||||
entry = reader.ReadUInt32();
|
||||
phOff = reader.ReadUInt32();
|
||||
shOff = reader.ReadUInt32();
|
||||
}
|
||||
|
||||
var flags = reader.ReadUInt32();
|
||||
var ehSize = reader.ReadUInt16();
|
||||
var phEntSize = reader.ReadUInt16();
|
||||
var phNum = reader.ReadUInt16();
|
||||
var shEntSize = reader.ReadUInt16();
|
||||
var shNum = reader.ReadUInt16();
|
||||
var shStrNdx = reader.ReadUInt16();
|
||||
|
||||
return new ElfHeader(
|
||||
Class: is64Bit ? ElfClass.Elf64 : ElfClass.Elf32,
|
||||
Data: reader.IsLittleEndian ? ElfData.Lsb : ElfData.Msb,
|
||||
OsAbi: (ElfOsAbi)reader.Data[7],
|
||||
Type: type,
|
||||
Machine: machine,
|
||||
EntryPoint: entry,
|
||||
ProgramHeaderOffset: phOff,
|
||||
SectionHeaderOffset: shOff,
|
||||
ProgramHeaderEntrySize: phEntSize,
|
||||
ProgramHeaderCount: phNum,
|
||||
SectionHeaderEntrySize: shEntSize,
|
||||
SectionHeaderCount: shNum,
|
||||
SectionNameStringTableIndex: shStrNdx);
|
||||
}
|
||||
|
||||
private static ImmutableArray<ElfSectionHeader> ParseSectionHeaders(ElfDataReader reader, ElfHeader header, bool is64Bit)
|
||||
{
|
||||
var sections = ImmutableArray.CreateBuilder<ElfSectionHeader>(header.SectionHeaderCount);
|
||||
var entrySize = is64Bit ? 64 : 40;
|
||||
|
||||
for (var i = 0; i < header.SectionHeaderCount; i++)
|
||||
{
|
||||
reader.Position = (int)header.SectionHeaderOffset + i * entrySize;
|
||||
|
||||
var nameIndex = reader.ReadUInt32();
|
||||
var type = (ElfSectionType)reader.ReadUInt32();
|
||||
|
||||
ulong flags, addr, offset, size;
|
||||
uint link, info;
|
||||
ulong addralign, entsize;
|
||||
|
||||
if (is64Bit)
|
||||
{
|
||||
flags = reader.ReadUInt64();
|
||||
addr = reader.ReadUInt64();
|
||||
offset = reader.ReadUInt64();
|
||||
size = reader.ReadUInt64();
|
||||
link = reader.ReadUInt32();
|
||||
info = reader.ReadUInt32();
|
||||
addralign = reader.ReadUInt64();
|
||||
entsize = reader.ReadUInt64();
|
||||
}
|
||||
else
|
||||
{
|
||||
flags = reader.ReadUInt32();
|
||||
addr = reader.ReadUInt32();
|
||||
offset = reader.ReadUInt32();
|
||||
size = reader.ReadUInt32();
|
||||
link = reader.ReadUInt32();
|
||||
info = reader.ReadUInt32();
|
||||
addralign = reader.ReadUInt32();
|
||||
entsize = reader.ReadUInt32();
|
||||
}
|
||||
|
||||
sections.Add(new ElfSectionHeader(
|
||||
nameIndex, string.Empty, type, flags, addr, offset, size, link, info, addralign, entsize));
|
||||
}
|
||||
|
||||
return sections.ToImmutable();
|
||||
}
|
||||
|
||||
private static (ImmutableArray<ElfSymbol> Symbols, byte[] StringTable) ParseSymbolTable(
|
||||
byte[] data, ImmutableArray<ElfSectionHeader> sections, string tableName, bool is64Bit, bool isLittleEndian)
|
||||
{
|
||||
var symTab = sections.FirstOrDefault(s => s.Name == tableName);
|
||||
if (symTab is null || symTab.Type is not (ElfSectionType.SymTab or ElfSectionType.DynSym))
|
||||
{
|
||||
return (ImmutableArray<ElfSymbol>.Empty, Array.Empty<byte>());
|
||||
}
|
||||
|
||||
// Get associated string table
|
||||
var strTab = sections.ElementAtOrDefault((int)symTab.Link);
|
||||
var strTabData = strTab is not null
|
||||
? data.AsSpan((int)strTab.Offset, (int)strTab.Size).ToArray()
|
||||
: Array.Empty<byte>();
|
||||
|
||||
var entrySize = is64Bit ? 24 : 16;
|
||||
var symbolCount = (int)(symTab.Size / (ulong)entrySize);
|
||||
var symbols = ImmutableArray.CreateBuilder<ElfSymbol>(symbolCount);
|
||||
var reader = new ElfDataReader(data, isLittleEndian) { Position = (int)symTab.Offset };
|
||||
|
||||
for (var i = 0; i < symbolCount; i++)
|
||||
{
|
||||
uint nameIdx;
|
||||
ulong value, size;
|
||||
byte info, other;
|
||||
ushort shndx;
|
||||
|
||||
if (is64Bit)
|
||||
{
|
||||
nameIdx = reader.ReadUInt32();
|
||||
info = reader.ReadByte();
|
||||
other = reader.ReadByte();
|
||||
shndx = reader.ReadUInt16();
|
||||
value = reader.ReadUInt64();
|
||||
size = reader.ReadUInt64();
|
||||
}
|
||||
else
|
||||
{
|
||||
nameIdx = reader.ReadUInt32();
|
||||
value = reader.ReadUInt32();
|
||||
size = reader.ReadUInt32();
|
||||
info = reader.ReadByte();
|
||||
other = reader.ReadByte();
|
||||
shndx = reader.ReadUInt16();
|
||||
}
|
||||
|
||||
var binding = (ElfSymbolBinding)(info >> 4);
|
||||
var type = (ElfSymbolType)(info & 0xF);
|
||||
var visibility = (ElfSymbolVisibility)(other & 0x3);
|
||||
|
||||
symbols.Add(new ElfSymbol(nameIdx, string.Empty, value, size, binding, type, visibility, shndx));
|
||||
}
|
||||
|
||||
return (symbols.ToImmutable(), strTabData);
|
||||
}
|
||||
|
||||
private static ImmutableArray<ElfNote> ParseNotes(byte[] data, ImmutableArray<ElfSectionHeader> sections, bool isLittleEndian)
|
||||
{
|
||||
var notes = ImmutableArray.CreateBuilder<ElfNote>();
|
||||
|
||||
foreach (var section in sections.Where(s => s.Type == ElfSectionType.Note))
|
||||
{
|
||||
var reader = new ElfDataReader(data, isLittleEndian) { Position = (int)section.Offset };
|
||||
var end = (int)(section.Offset + section.Size);
|
||||
|
||||
while (reader.Position < end)
|
||||
{
|
||||
var namesz = reader.ReadUInt32();
|
||||
var descsz = reader.ReadUInt32();
|
||||
var type = (ElfGnuNoteType)reader.ReadUInt32();
|
||||
|
||||
var name = Encoding.ASCII.GetString(data, reader.Position, (int)namesz - 1);
|
||||
reader.Position += Align4((int)namesz);
|
||||
|
||||
var desc = data.AsMemory(reader.Position, (int)descsz);
|
||||
reader.Position += Align4((int)descsz);
|
||||
|
||||
notes.Add(new ElfNote(name, type, desc));
|
||||
}
|
||||
}
|
||||
|
||||
return notes.ToImmutable();
|
||||
}
|
||||
|
||||
private static ImmutableArray<ElfRelocation> ParseRelocations(
|
||||
byte[] data, ImmutableArray<ElfSectionHeader> sections, bool is64Bit, bool isLittleEndian)
|
||||
{
|
||||
var relocations = ImmutableArray.CreateBuilder<ElfRelocation>();
|
||||
|
||||
foreach (var section in sections.Where(s => s.Type is ElfSectionType.Rela or ElfSectionType.Rel))
|
||||
{
|
||||
var hasAddend = section.Type == ElfSectionType.Rela;
|
||||
var entrySize = is64Bit ? (hasAddend ? 24 : 16) : (hasAddend ? 12 : 8);
|
||||
var count = (int)(section.Size / (ulong)entrySize);
|
||||
var reader = new ElfDataReader(data, isLittleEndian) { Position = (int)section.Offset };
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
ulong offset;
|
||||
uint type, symIdx;
|
||||
long addend = 0;
|
||||
|
||||
if (is64Bit)
|
||||
{
|
||||
offset = reader.ReadUInt64();
|
||||
var info = reader.ReadUInt64();
|
||||
type = (uint)(info & 0xFFFFFFFF);
|
||||
symIdx = (uint)(info >> 32);
|
||||
if (hasAddend) addend = reader.ReadInt64();
|
||||
}
|
||||
else
|
||||
{
|
||||
offset = reader.ReadUInt32();
|
||||
var info = reader.ReadUInt32();
|
||||
type = info & 0xFF;
|
||||
symIdx = info >> 8;
|
||||
if (hasAddend) addend = reader.ReadInt32();
|
||||
}
|
||||
|
||||
relocations.Add(new ElfRelocation(offset, type, symIdx, addend));
|
||||
}
|
||||
}
|
||||
|
||||
return relocations.ToImmutable();
|
||||
}
|
||||
|
||||
private static string? ExtractBuildId(ImmutableArray<ElfNote> notes)
|
||||
{
|
||||
var gnuBuildId = notes.FirstOrDefault(n => n.Name == "GNU" && n.Type == ElfGnuNoteType.BuildId);
|
||||
if (gnuBuildId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Convert.ToHexString(gnuBuildId.Descriptor.Span).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string FormatCodeId(string buildId)
|
||||
{
|
||||
// Format as ELF code-id (same as build-id for ELF)
|
||||
return buildId;
|
||||
}
|
||||
|
||||
private static string ComputeTextSectionHash(byte[] data, ImmutableArray<ElfSectionHeader> sections)
|
||||
{
|
||||
var textSection = sections.FirstOrDefault(s => s.Name == ".text");
|
||||
if (textSection is null || textSection.Size == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var textData = data.AsSpan((int)textSection.Offset, (int)textSection.Size);
|
||||
var hash = SHA256.HashData(textData);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ExtractInitArraySymbols(
|
||||
byte[] data, ImmutableArray<ElfSectionHeader> sections,
|
||||
ImmutableArray<ElfSymbol> symbols, ImmutableArray<ElfSymbol> dynSymbols,
|
||||
bool is64Bit, bool isLittleEndian)
|
||||
{
|
||||
return ExtractArraySymbols(data, sections, symbols, dynSymbols, ".init_array", is64Bit, isLittleEndian);
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ExtractPreInitArraySymbols(
|
||||
byte[] data, ImmutableArray<ElfSectionHeader> sections,
|
||||
ImmutableArray<ElfSymbol> symbols, ImmutableArray<ElfSymbol> dynSymbols,
|
||||
bool is64Bit, bool isLittleEndian)
|
||||
{
|
||||
return ExtractArraySymbols(data, sections, symbols, dynSymbols, ".preinit_array", is64Bit, isLittleEndian);
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ExtractArraySymbols(
|
||||
byte[] data, ImmutableArray<ElfSectionHeader> sections,
|
||||
ImmutableArray<ElfSymbol> symbols, ImmutableArray<ElfSymbol> dynSymbols,
|
||||
string sectionName, bool is64Bit, bool isLittleEndian)
|
||||
{
|
||||
var section = sections.FirstOrDefault(s => s.Name == sectionName);
|
||||
if (section is null || section.Size == 0)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var allSymbols = symbols.Concat(dynSymbols).ToList();
|
||||
var ptrSize = is64Bit ? 8 : 4;
|
||||
var count = (int)(section.Size / (ulong)ptrSize);
|
||||
var result = ImmutableArray.CreateBuilder<string>(count);
|
||||
var reader = new ElfDataReader(data, isLittleEndian) { Position = (int)section.Offset };
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var addr = is64Bit ? reader.ReadUInt64() : reader.ReadUInt32();
|
||||
var sym = allSymbols.FirstOrDefault(s => s.Value == addr && s.Type == ElfSymbolType.Func);
|
||||
result.Add(sym?.Name ?? $"func_0x{addr:x}");
|
||||
}
|
||||
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ExtractNeededLibraries(
|
||||
byte[] data, ImmutableArray<ElfSectionHeader> sections, bool is64Bit, bool isLittleEndian)
|
||||
{
|
||||
var dynSection = sections.FirstOrDefault(s => s.Name == ".dynamic");
|
||||
if (dynSection is null)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var dynStrSection = sections.FirstOrDefault(s => s.Name == ".dynstr");
|
||||
if (dynStrSection is null)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var strTab = data.AsSpan((int)dynStrSection.Offset, (int)dynStrSection.Size).ToArray();
|
||||
var entrySize = is64Bit ? 16 : 8;
|
||||
var count = (int)(dynSection.Size / (ulong)entrySize);
|
||||
var result = ImmutableArray.CreateBuilder<string>();
|
||||
var reader = new ElfDataReader(data, isLittleEndian) { Position = (int)dynSection.Offset };
|
||||
|
||||
const ulong DT_NEEDED = 1;
|
||||
const ulong DT_NULL = 0;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var tag = is64Bit ? reader.ReadUInt64() : reader.ReadUInt32();
|
||||
var val = is64Bit ? reader.ReadUInt64() : reader.ReadUInt32();
|
||||
|
||||
if (tag == DT_NULL) break;
|
||||
if (tag == DT_NEEDED)
|
||||
{
|
||||
result.Add(GetString(strTab, (uint)val));
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToImmutable();
|
||||
}
|
||||
|
||||
private static byte[] GetStringTable(byte[] data, ImmutableArray<ElfSectionHeader> sections, ushort index)
|
||||
{
|
||||
if (index >= sections.Length) return Array.Empty<byte>();
|
||||
var section = sections[index];
|
||||
return data.AsSpan((int)section.Offset, (int)section.Size).ToArray();
|
||||
}
|
||||
|
||||
private static string GetString(byte[] strTab, uint offset)
|
||||
{
|
||||
if (offset >= strTab.Length) return string.Empty;
|
||||
var end = Array.IndexOf(strTab, (byte)0, (int)offset);
|
||||
if (end < 0) end = strTab.Length;
|
||||
return Encoding.UTF8.GetString(strTab, (int)offset, end - (int)offset);
|
||||
}
|
||||
|
||||
private static int Align4(int value) => (value + 3) & ~3;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for reading binary data with endianness support.
|
||||
/// </summary>
|
||||
private sealed class ElfDataReader(byte[] data, bool isLittleEndian)
|
||||
{
|
||||
public byte[] Data { get; } = data;
|
||||
public bool IsLittleEndian { get; } = isLittleEndian;
|
||||
public int Position { get; set; }
|
||||
|
||||
public byte ReadByte() => Data[Position++];
|
||||
|
||||
public ushort ReadUInt16()
|
||||
{
|
||||
var value = IsLittleEndian
|
||||
? BinaryPrimitives.ReadUInt16LittleEndian(Data.AsSpan(Position))
|
||||
: BinaryPrimitives.ReadUInt16BigEndian(Data.AsSpan(Position));
|
||||
Position += 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
public uint ReadUInt32()
|
||||
{
|
||||
var value = IsLittleEndian
|
||||
? BinaryPrimitives.ReadUInt32LittleEndian(Data.AsSpan(Position))
|
||||
: BinaryPrimitives.ReadUInt32BigEndian(Data.AsSpan(Position));
|
||||
Position += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
public ulong ReadUInt64()
|
||||
{
|
||||
var value = IsLittleEndian
|
||||
? BinaryPrimitives.ReadUInt64LittleEndian(Data.AsSpan(Position))
|
||||
: BinaryPrimitives.ReadUInt64BigEndian(Data.AsSpan(Position));
|
||||
Position += 8;
|
||||
return value;
|
||||
}
|
||||
|
||||
public int ReadInt32()
|
||||
{
|
||||
var value = IsLittleEndian
|
||||
? BinaryPrimitives.ReadInt32LittleEndian(Data.AsSpan(Position))
|
||||
: BinaryPrimitives.ReadInt32BigEndian(Data.AsSpan(Position));
|
||||
Position += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
public long ReadInt64()
|
||||
{
|
||||
var value = IsLittleEndian
|
||||
? BinaryPrimitives.ReadInt64LittleEndian(Data.AsSpan(Position))
|
||||
: BinaryPrimitives.ReadInt64BigEndian(Data.AsSpan(Position));
|
||||
Position += 8;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Internal.Elf;
|
||||
|
||||
/// <summary>
|
||||
/// ELF file class (32-bit or 64-bit).
|
||||
/// </summary>
|
||||
internal enum ElfClass : byte
|
||||
{
|
||||
None = 0,
|
||||
Elf32 = 1,
|
||||
Elf64 = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF data encoding (endianness).
|
||||
/// </summary>
|
||||
internal enum ElfData : byte
|
||||
{
|
||||
None = 0,
|
||||
Lsb = 1, // Little-endian
|
||||
Msb = 2, // Big-endian
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF OS/ABI.
|
||||
/// </summary>
|
||||
internal enum ElfOsAbi : byte
|
||||
{
|
||||
None = 0,
|
||||
Linux = 3,
|
||||
FreeBsd = 9,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF file type.
|
||||
/// </summary>
|
||||
internal enum ElfType : ushort
|
||||
{
|
||||
None = 0,
|
||||
Rel = 1, // Relocatable
|
||||
Exec = 2, // Executable
|
||||
Dyn = 3, // Shared object
|
||||
Core = 4, // Core dump
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF machine architecture.
|
||||
/// </summary>
|
||||
internal enum ElfMachine : ushort
|
||||
{
|
||||
None = 0,
|
||||
I386 = 3,
|
||||
X86_64 = 62,
|
||||
Arm = 40,
|
||||
Aarch64 = 183,
|
||||
RiscV = 243,
|
||||
LoongArch = 258,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF section type.
|
||||
/// </summary>
|
||||
internal enum ElfSectionType : uint
|
||||
{
|
||||
Null = 0,
|
||||
ProgBits = 1,
|
||||
SymTab = 2,
|
||||
StrTab = 3,
|
||||
Rela = 4,
|
||||
Hash = 5,
|
||||
Dynamic = 6,
|
||||
Note = 7,
|
||||
NoBits = 8,
|
||||
Rel = 9,
|
||||
ShLib = 10,
|
||||
DynSym = 11,
|
||||
InitArray = 14,
|
||||
FiniArray = 15,
|
||||
PreInitArray = 16,
|
||||
Group = 17,
|
||||
SymTabShndx = 18,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF symbol binding.
|
||||
/// </summary>
|
||||
internal enum ElfSymbolBinding : byte
|
||||
{
|
||||
Local = 0,
|
||||
Global = 1,
|
||||
Weak = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF symbol type.
|
||||
/// </summary>
|
||||
internal enum ElfSymbolType : byte
|
||||
{
|
||||
NoType = 0,
|
||||
Object = 1,
|
||||
Func = 2,
|
||||
Section = 3,
|
||||
File = 4,
|
||||
Common = 5,
|
||||
Tls = 6,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF symbol visibility.
|
||||
/// </summary>
|
||||
internal enum ElfSymbolVisibility : byte
|
||||
{
|
||||
Default = 0,
|
||||
Internal = 1,
|
||||
Hidden = 2,
|
||||
Protected = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ELF note type for GNU notes.
|
||||
/// </summary>
|
||||
internal enum ElfGnuNoteType : uint
|
||||
{
|
||||
AbiTag = 1,
|
||||
Hwcap = 2,
|
||||
BuildId = 3,
|
||||
GoldVersion = 4,
|
||||
Property = 5,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsed ELF header information.
|
||||
/// </summary>
|
||||
internal sealed record ElfHeader(
|
||||
ElfClass Class,
|
||||
ElfData Data,
|
||||
ElfOsAbi OsAbi,
|
||||
ElfType Type,
|
||||
ElfMachine Machine,
|
||||
ulong EntryPoint,
|
||||
ulong ProgramHeaderOffset,
|
||||
ulong SectionHeaderOffset,
|
||||
ushort ProgramHeaderEntrySize,
|
||||
ushort ProgramHeaderCount,
|
||||
ushort SectionHeaderEntrySize,
|
||||
ushort SectionHeaderCount,
|
||||
ushort SectionNameStringTableIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Parsed ELF section header.
|
||||
/// </summary>
|
||||
internal sealed record ElfSectionHeader(
|
||||
uint NameIndex,
|
||||
string Name,
|
||||
ElfSectionType Type,
|
||||
ulong Flags,
|
||||
ulong Address,
|
||||
ulong Offset,
|
||||
ulong Size,
|
||||
uint Link,
|
||||
uint Info,
|
||||
ulong AddressAlign,
|
||||
ulong EntrySize);
|
||||
|
||||
/// <summary>
|
||||
/// Parsed ELF symbol.
|
||||
/// </summary>
|
||||
internal sealed record ElfSymbol(
|
||||
uint NameIndex,
|
||||
string Name,
|
||||
ulong Value,
|
||||
ulong Size,
|
||||
ElfSymbolBinding Binding,
|
||||
ElfSymbolType Type,
|
||||
ElfSymbolVisibility Visibility,
|
||||
ushort SectionIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Parsed ELF note.
|
||||
/// </summary>
|
||||
internal sealed record ElfNote(
|
||||
string Name,
|
||||
ElfGnuNoteType Type,
|
||||
ReadOnlyMemory<byte> Descriptor);
|
||||
|
||||
/// <summary>
|
||||
/// ELF relocation entry.
|
||||
/// </summary>
|
||||
internal sealed record ElfRelocation(
|
||||
ulong Offset,
|
||||
uint Type,
|
||||
uint SymbolIndex,
|
||||
long Addend);
|
||||
|
||||
/// <summary>
|
||||
/// Parsed ELF file summary.
|
||||
/// </summary>
|
||||
internal sealed record ElfFile(
|
||||
string Path,
|
||||
string LayerDigest,
|
||||
ElfHeader Header,
|
||||
ImmutableArray<ElfSectionHeader> Sections,
|
||||
ImmutableArray<ElfSymbol> Symbols,
|
||||
ImmutableArray<ElfSymbol> DynamicSymbols,
|
||||
ImmutableArray<ElfNote> Notes,
|
||||
ImmutableArray<ElfRelocation> Relocations,
|
||||
string? BuildId,
|
||||
string? CodeId,
|
||||
string TextSectionHash,
|
||||
ImmutableArray<string> InitArraySymbols,
|
||||
ImmutableArray<string> PreInitArraySymbols,
|
||||
ImmutableArray<string> NeededLibraries);
|
||||
|
||||
/// <summary>
|
||||
/// Magic bytes for ELF identification.
|
||||
/// </summary>
|
||||
internal static class ElfMagic
|
||||
{
|
||||
public static ReadOnlySpan<byte> Bytes => "\x7FELF"u8;
|
||||
public const int IdentSize = 16;
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Internal.Graph;
|
||||
|
||||
/// <summary>
|
||||
/// Writes native reachability graphs as DSSE bundles (NDJSON format).
|
||||
/// Per reachability spec: deterministic ordering, UTC timestamps, stable hashes.
|
||||
/// </summary>
|
||||
internal static class NativeGraphDsseWriter
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Writes the graph as NDJSON to a stream.
|
||||
/// </summary>
|
||||
public static async Task WriteNdjsonAsync(NativeReachabilityGraph graph, Stream stream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
// Write metadata header
|
||||
var header = new NdjsonGraphHeader(
|
||||
Type: "native.reachability.graph",
|
||||
Version: "1.0.0",
|
||||
LayerDigest: graph.LayerDigest,
|
||||
ContentHash: graph.ContentHash,
|
||||
GeneratedAt: graph.Metadata.GeneratedAt.ToString("O"),
|
||||
GeneratorVersion: graph.Metadata.GeneratorVersion,
|
||||
BinaryCount: graph.Metadata.BinaryCount,
|
||||
FunctionCount: graph.Metadata.FunctionCount,
|
||||
EdgeCount: graph.Metadata.EdgeCount,
|
||||
UnknownCount: graph.Metadata.UnknownCount,
|
||||
SyntheticRootCount: graph.Metadata.SyntheticRootCount);
|
||||
|
||||
await WriteLineAsync(writer, header, cancellationToken);
|
||||
|
||||
// Write functions (sorted by symbol_id for determinism)
|
||||
foreach (var func in graph.Functions.OrderBy(f => f.SymbolId))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var record = new NdjsonFunctionRecord(
|
||||
RecordType: "function",
|
||||
SymbolId: func.SymbolId,
|
||||
Name: func.Name,
|
||||
Purl: func.Purl,
|
||||
BinaryPath: func.BinaryPath,
|
||||
BuildId: func.BuildId,
|
||||
Address: $"0x{func.Address:x}",
|
||||
Size: func.Size,
|
||||
SymbolDigest: func.SymbolDigest,
|
||||
Binding: func.Binding,
|
||||
Visibility: func.Visibility,
|
||||
IsExported: func.IsExported);
|
||||
|
||||
await WriteLineAsync(writer, record, cancellationToken);
|
||||
}
|
||||
|
||||
// Write edges (sorted by edge_id for determinism)
|
||||
foreach (var edge in graph.Edges.OrderBy(e => e.EdgeId))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var record = new NdjsonEdgeRecord(
|
||||
RecordType: "edge",
|
||||
EdgeId: edge.EdgeId,
|
||||
CallerId: edge.CallerId,
|
||||
CalleeId: edge.CalleeId,
|
||||
CalleePurl: edge.CalleePurl,
|
||||
CalleeSymbolDigest: edge.CalleeSymbolDigest,
|
||||
EdgeType: edge.EdgeType.ToString().ToLowerInvariant(),
|
||||
CallSiteOffset: $"0x{edge.CallSiteOffset:x}",
|
||||
IsResolved: edge.IsResolved,
|
||||
Confidence: edge.Confidence);
|
||||
|
||||
await WriteLineAsync(writer, record, cancellationToken);
|
||||
}
|
||||
|
||||
// Write synthetic roots (sorted by root_id for determinism)
|
||||
foreach (var root in graph.SyntheticRoots.OrderBy(r => r.RootId))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var record = new NdjsonRootRecord(
|
||||
RecordType: "synthetic_root",
|
||||
RootId: root.RootId,
|
||||
TargetId: root.TargetId,
|
||||
RootType: root.RootType.ToString().ToLowerInvariant(),
|
||||
BinaryPath: root.BinaryPath,
|
||||
Phase: root.Phase,
|
||||
Order: root.Order);
|
||||
|
||||
await WriteLineAsync(writer, record, cancellationToken);
|
||||
}
|
||||
|
||||
// Write unknowns (sorted by unknown_id for determinism)
|
||||
foreach (var unknown in graph.Unknowns.OrderBy(u => u.UnknownId))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var record = new NdjsonUnknownRecord(
|
||||
RecordType: "unknown",
|
||||
UnknownId: unknown.UnknownId,
|
||||
UnknownType: unknown.UnknownType.ToString().ToLowerInvariant(),
|
||||
SourceId: unknown.SourceId,
|
||||
Name: unknown.Name,
|
||||
Reason: unknown.Reason,
|
||||
BinaryPath: unknown.BinaryPath);
|
||||
|
||||
await WriteLineAsync(writer, record, cancellationToken);
|
||||
}
|
||||
|
||||
await writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the graph as a JSON object (for DSSE payload).
|
||||
/// </summary>
|
||||
public static string WriteJson(NativeReachabilityGraph graph)
|
||||
{
|
||||
var payload = new NdjsonGraphPayload(
|
||||
Type: "native.reachability.graph",
|
||||
Version: "1.0.0",
|
||||
LayerDigest: graph.LayerDigest,
|
||||
ContentHash: graph.ContentHash,
|
||||
Metadata: new NdjsonMetadataPayload(
|
||||
GeneratedAt: graph.Metadata.GeneratedAt.ToString("O"),
|
||||
GeneratorVersion: graph.Metadata.GeneratorVersion,
|
||||
BinaryCount: graph.Metadata.BinaryCount,
|
||||
FunctionCount: graph.Metadata.FunctionCount,
|
||||
EdgeCount: graph.Metadata.EdgeCount,
|
||||
UnknownCount: graph.Metadata.UnknownCount,
|
||||
SyntheticRootCount: graph.Metadata.SyntheticRootCount),
|
||||
Functions: graph.Functions.OrderBy(f => f.SymbolId).Select(f => new NdjsonFunctionPayload(
|
||||
SymbolId: f.SymbolId,
|
||||
Name: f.Name,
|
||||
Purl: f.Purl,
|
||||
BinaryPath: f.BinaryPath,
|
||||
BuildId: f.BuildId,
|
||||
Address: $"0x{f.Address:x}",
|
||||
Size: f.Size,
|
||||
SymbolDigest: f.SymbolDigest,
|
||||
Binding: f.Binding,
|
||||
Visibility: f.Visibility,
|
||||
IsExported: f.IsExported)).ToArray(),
|
||||
Edges: graph.Edges.OrderBy(e => e.EdgeId).Select(e => new NdjsonEdgePayload(
|
||||
EdgeId: e.EdgeId,
|
||||
CallerId: e.CallerId,
|
||||
CalleeId: e.CalleeId,
|
||||
CalleePurl: e.CalleePurl,
|
||||
CalleeSymbolDigest: e.CalleeSymbolDigest,
|
||||
EdgeType: e.EdgeType.ToString().ToLowerInvariant(),
|
||||
CallSiteOffset: $"0x{e.CallSiteOffset:x}",
|
||||
IsResolved: e.IsResolved,
|
||||
Confidence: e.Confidence)).ToArray(),
|
||||
SyntheticRoots: graph.SyntheticRoots.OrderBy(r => r.RootId).Select(r => new NdjsonRootPayload(
|
||||
RootId: r.RootId,
|
||||
TargetId: r.TargetId,
|
||||
RootType: r.RootType.ToString().ToLowerInvariant(),
|
||||
BinaryPath: r.BinaryPath,
|
||||
Phase: r.Phase,
|
||||
Order: r.Order)).ToArray(),
|
||||
Unknowns: graph.Unknowns.OrderBy(u => u.UnknownId).Select(u => new NdjsonUnknownPayload(
|
||||
UnknownId: u.UnknownId,
|
||||
UnknownType: u.UnknownType.ToString().ToLowerInvariant(),
|
||||
SourceId: u.SourceId,
|
||||
Name: u.Name,
|
||||
Reason: u.Reason,
|
||||
BinaryPath: u.BinaryPath)).ToArray());
|
||||
|
||||
return JsonSerializer.Serialize(payload, JsonOptions);
|
||||
}
|
||||
|
||||
private static async Task WriteLineAsync<T>(StreamWriter writer, T record, CancellationToken ct)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(record, JsonOptions);
|
||||
await writer.WriteLineAsync(json.AsMemory(), ct);
|
||||
}
|
||||
|
||||
// NDJSON record types
|
||||
private sealed record NdjsonGraphHeader(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("version")] string Version,
|
||||
[property: JsonPropertyName("layer_digest")] string LayerDigest,
|
||||
[property: JsonPropertyName("content_hash")] string ContentHash,
|
||||
[property: JsonPropertyName("generated_at")] string GeneratedAt,
|
||||
[property: JsonPropertyName("generator_version")] string GeneratorVersion,
|
||||
[property: JsonPropertyName("binary_count")] int BinaryCount,
|
||||
[property: JsonPropertyName("function_count")] int FunctionCount,
|
||||
[property: JsonPropertyName("edge_count")] int EdgeCount,
|
||||
[property: JsonPropertyName("unknown_count")] int UnknownCount,
|
||||
[property: JsonPropertyName("synthetic_root_count")] int SyntheticRootCount);
|
||||
|
||||
private sealed record NdjsonFunctionRecord(
|
||||
[property: JsonPropertyName("record_type")] string RecordType,
|
||||
[property: JsonPropertyName("symbol_id")] string SymbolId,
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("purl")] string? Purl,
|
||||
[property: JsonPropertyName("binary_path")] string BinaryPath,
|
||||
[property: JsonPropertyName("build_id")] string? BuildId,
|
||||
[property: JsonPropertyName("address")] string Address,
|
||||
[property: JsonPropertyName("size")] ulong Size,
|
||||
[property: JsonPropertyName("symbol_digest")] string SymbolDigest,
|
||||
[property: JsonPropertyName("binding")] string Binding,
|
||||
[property: JsonPropertyName("visibility")] string Visibility,
|
||||
[property: JsonPropertyName("is_exported")] bool IsExported);
|
||||
|
||||
private sealed record NdjsonEdgeRecord(
|
||||
[property: JsonPropertyName("record_type")] string RecordType,
|
||||
[property: JsonPropertyName("edge_id")] string EdgeId,
|
||||
[property: JsonPropertyName("caller_id")] string CallerId,
|
||||
[property: JsonPropertyName("callee_id")] string CalleeId,
|
||||
[property: JsonPropertyName("callee_purl")] string? CalleePurl,
|
||||
[property: JsonPropertyName("callee_symbol_digest")] string? CalleeSymbolDigest,
|
||||
[property: JsonPropertyName("edge_type")] string EdgeType,
|
||||
[property: JsonPropertyName("call_site_offset")] string CallSiteOffset,
|
||||
[property: JsonPropertyName("is_resolved")] bool IsResolved,
|
||||
[property: JsonPropertyName("confidence")] double Confidence);
|
||||
|
||||
private sealed record NdjsonRootRecord(
|
||||
[property: JsonPropertyName("record_type")] string RecordType,
|
||||
[property: JsonPropertyName("root_id")] string RootId,
|
||||
[property: JsonPropertyName("target_id")] string TargetId,
|
||||
[property: JsonPropertyName("root_type")] string RootType,
|
||||
[property: JsonPropertyName("binary_path")] string BinaryPath,
|
||||
[property: JsonPropertyName("phase")] string Phase,
|
||||
[property: JsonPropertyName("order")] int Order);
|
||||
|
||||
private sealed record NdjsonUnknownRecord(
|
||||
[property: JsonPropertyName("record_type")] string RecordType,
|
||||
[property: JsonPropertyName("unknown_id")] string UnknownId,
|
||||
[property: JsonPropertyName("unknown_type")] string UnknownType,
|
||||
[property: JsonPropertyName("source_id")] string SourceId,
|
||||
[property: JsonPropertyName("name")] string? Name,
|
||||
[property: JsonPropertyName("reason")] string Reason,
|
||||
[property: JsonPropertyName("binary_path")] string BinaryPath);
|
||||
|
||||
// JSON payload types (for DSSE envelope)
|
||||
private sealed record NdjsonGraphPayload(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("version")] string Version,
|
||||
[property: JsonPropertyName("layer_digest")] string LayerDigest,
|
||||
[property: JsonPropertyName("content_hash")] string ContentHash,
|
||||
[property: JsonPropertyName("metadata")] NdjsonMetadataPayload Metadata,
|
||||
[property: JsonPropertyName("functions")] NdjsonFunctionPayload[] Functions,
|
||||
[property: JsonPropertyName("edges")] NdjsonEdgePayload[] Edges,
|
||||
[property: JsonPropertyName("synthetic_roots")] NdjsonRootPayload[] SyntheticRoots,
|
||||
[property: JsonPropertyName("unknowns")] NdjsonUnknownPayload[] Unknowns);
|
||||
|
||||
private sealed record NdjsonMetadataPayload(
|
||||
[property: JsonPropertyName("generated_at")] string GeneratedAt,
|
||||
[property: JsonPropertyName("generator_version")] string GeneratorVersion,
|
||||
[property: JsonPropertyName("binary_count")] int BinaryCount,
|
||||
[property: JsonPropertyName("function_count")] int FunctionCount,
|
||||
[property: JsonPropertyName("edge_count")] int EdgeCount,
|
||||
[property: JsonPropertyName("unknown_count")] int UnknownCount,
|
||||
[property: JsonPropertyName("synthetic_root_count")] int SyntheticRootCount);
|
||||
|
||||
private sealed record NdjsonFunctionPayload(
|
||||
[property: JsonPropertyName("symbol_id")] string SymbolId,
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("purl")] string? Purl,
|
||||
[property: JsonPropertyName("binary_path")] string BinaryPath,
|
||||
[property: JsonPropertyName("build_id")] string? BuildId,
|
||||
[property: JsonPropertyName("address")] string Address,
|
||||
[property: JsonPropertyName("size")] ulong Size,
|
||||
[property: JsonPropertyName("symbol_digest")] string SymbolDigest,
|
||||
[property: JsonPropertyName("binding")] string Binding,
|
||||
[property: JsonPropertyName("visibility")] string Visibility,
|
||||
[property: JsonPropertyName("is_exported")] bool IsExported);
|
||||
|
||||
private sealed record NdjsonEdgePayload(
|
||||
[property: JsonPropertyName("edge_id")] string EdgeId,
|
||||
[property: JsonPropertyName("caller_id")] string CallerId,
|
||||
[property: JsonPropertyName("callee_id")] string CalleeId,
|
||||
[property: JsonPropertyName("callee_purl")] string? CalleePurl,
|
||||
[property: JsonPropertyName("callee_symbol_digest")] string? CalleeSymbolDigest,
|
||||
[property: JsonPropertyName("edge_type")] string EdgeType,
|
||||
[property: JsonPropertyName("call_site_offset")] string CallSiteOffset,
|
||||
[property: JsonPropertyName("is_resolved")] bool IsResolved,
|
||||
[property: JsonPropertyName("confidence")] double Confidence);
|
||||
|
||||
private sealed record NdjsonRootPayload(
|
||||
[property: JsonPropertyName("root_id")] string RootId,
|
||||
[property: JsonPropertyName("target_id")] string TargetId,
|
||||
[property: JsonPropertyName("root_type")] string RootType,
|
||||
[property: JsonPropertyName("binary_path")] string BinaryPath,
|
||||
[property: JsonPropertyName("phase")] string Phase,
|
||||
[property: JsonPropertyName("order")] int Order);
|
||||
|
||||
private sealed record NdjsonUnknownPayload(
|
||||
[property: JsonPropertyName("unknown_id")] string UnknownId,
|
||||
[property: JsonPropertyName("unknown_type")] string UnknownType,
|
||||
[property: JsonPropertyName("source_id")] string SourceId,
|
||||
[property: JsonPropertyName("name")] string? Name,
|
||||
[property: JsonPropertyName("reason")] string Reason,
|
||||
[property: JsonPropertyName("binary_path")] string BinaryPath);
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Internal.Graph;
|
||||
|
||||
/// <summary>
|
||||
/// Native reachability graph containing functions, call edges, and metadata.
|
||||
/// Per SCAN-NATIVE-REACH-0146-13 requirements.
|
||||
/// </summary>
|
||||
public sealed record NativeReachabilityGraph(
|
||||
string LayerDigest,
|
||||
ImmutableArray<NativeFunctionNode> Functions,
|
||||
ImmutableArray<NativeCallEdge> Edges,
|
||||
ImmutableArray<NativeSyntheticRoot> SyntheticRoots,
|
||||
ImmutableArray<NativeUnknown> Unknowns,
|
||||
NativeGraphMetadata Metadata,
|
||||
string ContentHash);
|
||||
|
||||
/// <summary>
|
||||
/// A function node in the native call graph.
|
||||
/// </summary>
|
||||
/// <param name="SymbolId">Deterministic symbol identifier (sha256 of purl+name+binding).</param>
|
||||
/// <param name="Name">Demangled or raw symbol name.</param>
|
||||
/// <param name="Purl">Package URL if resolvable (e.g., pkg:elf/libc.so.6).</param>
|
||||
/// <param name="BinaryPath">Path to the containing binary.</param>
|
||||
/// <param name="BuildId">ELF build-id if available.</param>
|
||||
/// <param name="Address">Virtual address of the function.</param>
|
||||
/// <param name="Size">Size of the function in bytes.</param>
|
||||
/// <param name="SymbolDigest">SHA-256 of (name + addr + size + binding).</param>
|
||||
/// <param name="Binding">Symbol binding (local/global/weak).</param>
|
||||
/// <param name="Visibility">Symbol visibility.</param>
|
||||
/// <param name="IsExported">Whether the symbol is exported (visible externally).</param>
|
||||
public sealed record NativeFunctionNode(
|
||||
string SymbolId,
|
||||
string Name,
|
||||
string? Purl,
|
||||
string BinaryPath,
|
||||
string? BuildId,
|
||||
ulong Address,
|
||||
ulong Size,
|
||||
string SymbolDigest,
|
||||
string Binding,
|
||||
string Visibility,
|
||||
bool IsExported);
|
||||
|
||||
/// <summary>
|
||||
/// A call edge in the native call graph.
|
||||
/// </summary>
|
||||
/// <param name="EdgeId">Deterministic edge identifier.</param>
|
||||
/// <param name="CallerId">SymbolId of the calling function.</param>
|
||||
/// <param name="CalleeId">SymbolId of the called function (or Unknown placeholder).</param>
|
||||
/// <param name="CalleePurl">PURL of the callee if resolvable.</param>
|
||||
/// <param name="CalleeSymbolDigest">Symbol digest of the callee.</param>
|
||||
/// <param name="EdgeType">Type of edge (direct, plt, got, reloc).</param>
|
||||
/// <param name="CallSiteOffset">Offset within caller where call occurs.</param>
|
||||
/// <param name="IsResolved">Whether the callee was successfully resolved.</param>
|
||||
/// <param name="Confidence">Confidence level (1.0 for resolved, lower for heuristic).</param>
|
||||
public sealed record NativeCallEdge(
|
||||
string EdgeId,
|
||||
string CallerId,
|
||||
string CalleeId,
|
||||
string? CalleePurl,
|
||||
string? CalleeSymbolDigest,
|
||||
NativeEdgeType EdgeType,
|
||||
ulong CallSiteOffset,
|
||||
bool IsResolved,
|
||||
double Confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Type of call edge.
|
||||
/// </summary>
|
||||
public enum NativeEdgeType
|
||||
{
|
||||
/// <summary>Direct function call.</summary>
|
||||
Direct,
|
||||
|
||||
/// <summary>Call through PLT (Procedure Linkage Table).</summary>
|
||||
Plt,
|
||||
|
||||
/// <summary>Call through GOT (Global Offset Table).</summary>
|
||||
Got,
|
||||
|
||||
/// <summary>Relocation-based call.</summary>
|
||||
Relocation,
|
||||
|
||||
/// <summary>Indirect call (target unknown).</summary>
|
||||
Indirect,
|
||||
|
||||
/// <summary>Init/preinit array entry.</summary>
|
||||
InitArray,
|
||||
|
||||
/// <summary>Fini array entry.</summary>
|
||||
FiniArray,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A synthetic root in the call graph (entry points that don't have callers).
|
||||
/// </summary>
|
||||
/// <param name="RootId">Deterministic root identifier.</param>
|
||||
/// <param name="TargetId">SymbolId of the target function.</param>
|
||||
/// <param name="RootType">Type of synthetic root.</param>
|
||||
/// <param name="BinaryPath">Path to the containing binary.</param>
|
||||
/// <param name="Phase">Execution phase (load, init, main, fini).</param>
|
||||
/// <param name="Order">Order within the phase (for init arrays).</param>
|
||||
public sealed record NativeSyntheticRoot(
|
||||
string RootId,
|
||||
string TargetId,
|
||||
NativeRootType RootType,
|
||||
string BinaryPath,
|
||||
string Phase,
|
||||
int Order);
|
||||
|
||||
/// <summary>
|
||||
/// Type of synthetic root.
|
||||
/// </summary>
|
||||
public enum NativeRootType
|
||||
{
|
||||
/// <summary>_start entry point.</summary>
|
||||
Start,
|
||||
|
||||
/// <summary>_init function.</summary>
|
||||
Init,
|
||||
|
||||
/// <summary>.preinit_array entry.</summary>
|
||||
PreInitArray,
|
||||
|
||||
/// <summary>.init_array entry.</summary>
|
||||
InitArray,
|
||||
|
||||
/// <summary>.fini_array entry.</summary>
|
||||
FiniArray,
|
||||
|
||||
/// <summary>_fini function.</summary>
|
||||
Fini,
|
||||
|
||||
/// <summary>main function.</summary>
|
||||
Main,
|
||||
|
||||
/// <summary>Constructor (C++).</summary>
|
||||
Constructor,
|
||||
|
||||
/// <summary>Destructor (C++).</summary>
|
||||
Destructor,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An unknown/unresolved reference in the call graph.
|
||||
/// Per docs/signals/unknowns-registry.md specification.
|
||||
/// </summary>
|
||||
/// <param name="UnknownId">Deterministic identifier.</param>
|
||||
/// <param name="UnknownType">Type of unknown reference.</param>
|
||||
/// <param name="SourceId">SymbolId or EdgeId that references this unknown.</param>
|
||||
/// <param name="Name">Symbol name if available.</param>
|
||||
/// <param name="Reason">Why resolution failed.</param>
|
||||
/// <param name="BinaryPath">Binary where the reference occurs.</param>
|
||||
public sealed record NativeUnknown(
|
||||
string UnknownId,
|
||||
NativeUnknownType UnknownType,
|
||||
string SourceId,
|
||||
string? Name,
|
||||
string Reason,
|
||||
string BinaryPath);
|
||||
|
||||
/// <summary>
|
||||
/// Type of unknown reference.
|
||||
/// </summary>
|
||||
public enum NativeUnknownType
|
||||
{
|
||||
/// <summary>Symbol could not be resolved to a PURL.</summary>
|
||||
UnresolvedPurl,
|
||||
|
||||
/// <summary>Call target could not be determined.</summary>
|
||||
UnresolvedTarget,
|
||||
|
||||
/// <summary>Symbol hash could not be computed.</summary>
|
||||
UnresolvedHash,
|
||||
|
||||
/// <summary>Binary could not be identified.</summary>
|
||||
UnresolvedBinary,
|
||||
|
||||
/// <summary>Indirect call target is ambiguous.</summary>
|
||||
AmbiguousTarget,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata for the native reachability graph.
|
||||
/// </summary>
|
||||
/// <param name="GeneratedAt">UTC timestamp of generation.</param>
|
||||
/// <param name="GeneratorVersion">Version of the generator.</param>
|
||||
/// <param name="LayerDigest">Digest of the layer.</param>
|
||||
/// <param name="BinaryCount">Number of binaries analyzed.</param>
|
||||
/// <param name="FunctionCount">Number of functions discovered.</param>
|
||||
/// <param name="EdgeCount">Number of edges discovered.</param>
|
||||
/// <param name="UnknownCount">Number of unknown references.</param>
|
||||
/// <param name="SyntheticRootCount">Number of synthetic roots.</param>
|
||||
public sealed record NativeGraphMetadata(
|
||||
DateTimeOffset GeneratedAt,
|
||||
string GeneratorVersion,
|
||||
string LayerDigest,
|
||||
int BinaryCount,
|
||||
int FunctionCount,
|
||||
int EdgeCount,
|
||||
int UnknownCount,
|
||||
int SyntheticRootCount);
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for creating deterministic identifiers.
|
||||
/// </summary>
|
||||
internal static class NativeGraphIdentifiers
|
||||
{
|
||||
private const string GeneratorVersion = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic symbol ID from name, address, size, and binding.
|
||||
/// </summary>
|
||||
public static string ComputeSymbolId(string name, ulong address, ulong size, string binding)
|
||||
{
|
||||
var input = $"{name}:{address:x}:{size}:{binding}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"sym:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic symbol digest.
|
||||
/// </summary>
|
||||
public static string ComputeSymbolDigest(string name, ulong address, ulong size, string binding)
|
||||
{
|
||||
var input = $"{name}:{address:x}:{size}:{binding}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic edge ID.
|
||||
/// </summary>
|
||||
public static string ComputeEdgeId(string callerId, string calleeId, ulong callSiteOffset)
|
||||
{
|
||||
var input = $"{callerId}:{calleeId}:{callSiteOffset:x}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"edge:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic root ID.
|
||||
/// </summary>
|
||||
public static string ComputeRootId(string targetId, NativeRootType rootType, int order)
|
||||
{
|
||||
var input = $"{targetId}:{rootType}:{order}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"root:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic unknown ID.
|
||||
/// </summary>
|
||||
public static string ComputeUnknownId(string sourceId, NativeUnknownType unknownType, string? name)
|
||||
{
|
||||
var input = $"{sourceId}:{unknownType}:{name ?? ""}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"unk:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes content hash for the entire graph.
|
||||
/// </summary>
|
||||
public static string ComputeGraphHash(
|
||||
ImmutableArray<NativeFunctionNode> functions,
|
||||
ImmutableArray<NativeCallEdge> edges,
|
||||
ImmutableArray<NativeSyntheticRoot> roots)
|
||||
{
|
||||
using var sha = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
|
||||
foreach (var f in functions.OrderBy(f => f.SymbolId))
|
||||
{
|
||||
sha.AppendData(Encoding.UTF8.GetBytes(f.SymbolId));
|
||||
sha.AppendData(Encoding.UTF8.GetBytes(f.SymbolDigest));
|
||||
}
|
||||
|
||||
foreach (var e in edges.OrderBy(e => e.EdgeId))
|
||||
{
|
||||
sha.AppendData(Encoding.UTF8.GetBytes(e.EdgeId));
|
||||
}
|
||||
|
||||
foreach (var r in roots.OrderBy(r => r.RootId))
|
||||
{
|
||||
sha.AppendData(Encoding.UTF8.GetBytes(r.RootId));
|
||||
}
|
||||
|
||||
return Convert.ToHexString(sha.GetCurrentHash()).ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current generator version.
|
||||
/// </summary>
|
||||
public static string GetGeneratorVersion() => GeneratorVersion;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<EnableDefaultItems>false</EnableDefaultItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="**\*.cs" Exclude="obj\**;bin\**" />
|
||||
<EmbeddedResource Include="**\*.json" Exclude="obj\**;bin\**" />
|
||||
<None Include="**\*" Exclude="**\*.cs;**\*.json;bin\**;obj\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user