up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user