up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (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
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (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
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,913 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Reflection;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Callgraph;
|
||||
|
||||
/// <summary>
|
||||
/// Builds Java reachability graphs from class path analysis.
|
||||
/// Extracts methods, call edges, synthetic roots, and emits unknowns.
|
||||
/// </summary>
|
||||
internal sealed class JavaCallgraphBuilder
|
||||
{
|
||||
private readonly Dictionary<string, JavaMethodNode> _methods = new();
|
||||
private readonly List<JavaCallEdge> _edges = new();
|
||||
private readonly List<JavaSyntheticRoot> _roots = new();
|
||||
private readonly List<JavaUnknown> _unknowns = new();
|
||||
private readonly Dictionary<string, string> _classToJarPath = new();
|
||||
private readonly string _contextDigest;
|
||||
private int _jarCount;
|
||||
private int _classCount;
|
||||
|
||||
public JavaCallgraphBuilder(string contextDigest)
|
||||
{
|
||||
_contextDigest = contextDigest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a class path analysis to the graph.
|
||||
/// </summary>
|
||||
public void AddClassPath(JavaClassPathAnalysis classPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var segment in classPath.Segments)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_jarCount++;
|
||||
|
||||
// Derive PURL from segment identifier (simplified - would use proper mapping in production)
|
||||
var purl = DerivePurlFromSegment(segment);
|
||||
|
||||
foreach (var kvp in segment.ClassLocations)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var className = kvp.Key;
|
||||
var location = kvp.Value;
|
||||
|
||||
_classCount++;
|
||||
_classToJarPath[className] = segment.Identifier;
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = location.OpenClassStream(cancellationToken);
|
||||
AddClassFile(stream, className, segment.Identifier, purl, cancellationToken);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Record as unknown if class file cannot be parsed
|
||||
var unknownId = JavaGraphIdentifiers.ComputeUnknownId(
|
||||
segment.Identifier,
|
||||
JavaUnknownType.UnresolvedClass,
|
||||
className,
|
||||
null);
|
||||
_unknowns.Add(new JavaUnknown(
|
||||
UnknownId: unknownId,
|
||||
UnknownType: JavaUnknownType.UnresolvedClass,
|
||||
SourceId: segment.Identifier,
|
||||
ClassName: className,
|
||||
MethodName: null,
|
||||
Reason: "Class file could not be parsed",
|
||||
JarPath: segment.Identifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? DerivePurlFromSegment(JavaClassPathSegment segment)
|
||||
{
|
||||
// Simplified PURL derivation from JAR path
|
||||
var fileName = Path.GetFileNameWithoutExtension(segment.Identifier);
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $"pkg:maven/{fileName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds reflection analysis edges.
|
||||
/// </summary>
|
||||
public void AddReflectionAnalysis(JavaReflectionAnalysis reflectionAnalysis)
|
||||
{
|
||||
foreach (var edge in reflectionAnalysis.Edges)
|
||||
{
|
||||
// Use actual property names from JavaReflectionEdge record
|
||||
var callerId = JavaGraphIdentifiers.ComputeMethodId(
|
||||
JavaGraphIdentifiers.NormalizeClassName(edge.SourceClass),
|
||||
edge.MethodName,
|
||||
edge.MethodDescriptor);
|
||||
|
||||
var targetClassName = edge.TargetType ?? "unknown";
|
||||
var isResolved = edge.TargetType is not null;
|
||||
|
||||
// For reflection, the callee is a class load, not a method call
|
||||
var calleeId = isResolved
|
||||
? JavaGraphIdentifiers.ComputeMethodId(JavaGraphIdentifiers.NormalizeClassName(targetClassName), "<clinit>", "()V")
|
||||
: $"reflection:{targetClassName}";
|
||||
|
||||
var edgeId = JavaGraphIdentifiers.ComputeEdgeId(callerId, calleeId, edge.InstructionOffset);
|
||||
var confidence = edge.Confidence == JavaReflectionConfidence.High ? 0.9 : 0.5;
|
||||
|
||||
var edgeType = edge.Reason switch
|
||||
{
|
||||
JavaReflectionReason.ClassForName => JavaEdgeType.Reflection,
|
||||
JavaReflectionReason.ClassLoaderLoadClass => JavaEdgeType.Reflection,
|
||||
JavaReflectionReason.ServiceLoaderLoad => JavaEdgeType.ServiceLoader,
|
||||
_ => JavaEdgeType.Reflection,
|
||||
};
|
||||
|
||||
_edges.Add(new JavaCallEdge(
|
||||
EdgeId: edgeId,
|
||||
CallerId: callerId,
|
||||
CalleeId: calleeId,
|
||||
CalleePurl: null, // Reflection targets often unknown
|
||||
CalleeMethodDigest: null,
|
||||
EdgeType: edgeType,
|
||||
BytecodeOffset: edge.InstructionOffset,
|
||||
IsResolved: isResolved,
|
||||
Confidence: confidence));
|
||||
|
||||
if (!isResolved)
|
||||
{
|
||||
var unknownId = JavaGraphIdentifiers.ComputeUnknownId(
|
||||
edgeId,
|
||||
JavaUnknownType.ReflectionTarget,
|
||||
null,
|
||||
null);
|
||||
_unknowns.Add(new JavaUnknown(
|
||||
UnknownId: unknownId,
|
||||
UnknownType: JavaUnknownType.ReflectionTarget,
|
||||
SourceId: edgeId,
|
||||
ClassName: null,
|
||||
MethodName: null,
|
||||
Reason: "Reflection target class could not be determined",
|
||||
JarPath: edge.SegmentIdentifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the final reachability graph.
|
||||
/// </summary>
|
||||
public JavaReachabilityGraph Build()
|
||||
{
|
||||
var methods = _methods.Values
|
||||
.OrderBy(m => m.ClassName)
|
||||
.ThenBy(m => m.MethodName)
|
||||
.ThenBy(m => m.Descriptor)
|
||||
.ToImmutableArray();
|
||||
|
||||
var edges = _edges
|
||||
.OrderBy(e => e.CallerId)
|
||||
.ThenBy(e => e.BytecodeOffset)
|
||||
.ToImmutableArray();
|
||||
|
||||
var roots = _roots
|
||||
.OrderBy(r => (int)r.Phase)
|
||||
.ThenBy(r => r.Order)
|
||||
.ThenBy(r => r.TargetId, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var unknowns = _unknowns
|
||||
.OrderBy(u => u.JarPath)
|
||||
.ThenBy(u => u.SourceId)
|
||||
.ToImmutableArray();
|
||||
|
||||
var contentHash = JavaGraphIdentifiers.ComputeGraphHash(methods, edges, roots);
|
||||
|
||||
var metadata = new JavaGraphMetadata(
|
||||
GeneratedAt: DateTimeOffset.UtcNow,
|
||||
GeneratorVersion: JavaGraphIdentifiers.GetGeneratorVersion(),
|
||||
ContextDigest: _contextDigest,
|
||||
JarCount: _jarCount,
|
||||
ClassCount: _classCount,
|
||||
MethodCount: methods.Length,
|
||||
EdgeCount: edges.Length,
|
||||
UnknownCount: unknowns.Length,
|
||||
SyntheticRootCount: roots.Length);
|
||||
|
||||
return new JavaReachabilityGraph(
|
||||
_contextDigest,
|
||||
methods,
|
||||
edges,
|
||||
roots,
|
||||
unknowns,
|
||||
metadata,
|
||||
contentHash);
|
||||
}
|
||||
|
||||
private void AddClassFile(Stream stream, string className, string jarPath, string? purl, CancellationToken cancellationToken)
|
||||
{
|
||||
var classFile = JavaClassFileParser.Parse(stream, cancellationToken);
|
||||
var normalizedClassName = JavaGraphIdentifiers.NormalizeClassName(className);
|
||||
|
||||
// Add methods
|
||||
foreach (var method in classFile.Methods)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
AddMethod(normalizedClassName, method, jarPath, purl);
|
||||
}
|
||||
|
||||
// Find synthetic roots
|
||||
FindSyntheticRoots(normalizedClassName, classFile, jarPath);
|
||||
|
||||
// Extract call edges from bytecode
|
||||
foreach (var method in classFile.Methods)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ExtractCallEdges(normalizedClassName, method, jarPath, classFile.ConstantPool);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMethod(string className, JavaClassFileParser.MethodInfo method, string jarPath, string? purl)
|
||||
{
|
||||
var methodId = JavaGraphIdentifiers.ComputeMethodId(className, method.Name, method.Descriptor);
|
||||
var methodDigest = JavaGraphIdentifiers.ComputeMethodDigest(className, method.Name, method.Descriptor, method.AccessFlags);
|
||||
|
||||
var isStatic = (method.AccessFlags & 0x0008) != 0;
|
||||
var isPublic = (method.AccessFlags & 0x0001) != 0;
|
||||
var isSynthetic = (method.AccessFlags & 0x1000) != 0;
|
||||
var isBridge = (method.AccessFlags & 0x0040) != 0;
|
||||
|
||||
var node = new JavaMethodNode(
|
||||
MethodId: methodId,
|
||||
ClassName: className,
|
||||
MethodName: method.Name,
|
||||
Descriptor: method.Descriptor,
|
||||
Purl: purl,
|
||||
JarPath: jarPath,
|
||||
AccessFlags: method.AccessFlags,
|
||||
MethodDigest: methodDigest,
|
||||
IsStatic: isStatic,
|
||||
IsPublic: isPublic,
|
||||
IsSynthetic: isSynthetic,
|
||||
IsBridge: isBridge);
|
||||
|
||||
_methods.TryAdd(methodId, node);
|
||||
}
|
||||
|
||||
private void FindSyntheticRoots(string className, JavaClassFileParser.ClassFile classFile, string jarPath)
|
||||
{
|
||||
var rootOrder = 0;
|
||||
|
||||
foreach (var method in classFile.Methods)
|
||||
{
|
||||
var methodId = JavaGraphIdentifiers.ComputeMethodId(className, method.Name, method.Descriptor);
|
||||
|
||||
// main method
|
||||
if (method.Name == "main" && method.Descriptor == "([Ljava/lang/String;)V" &&
|
||||
(method.AccessFlags & 0x0009) == 0x0009) // public static
|
||||
{
|
||||
var rootId = JavaGraphIdentifiers.ComputeRootId(JavaRootPhase.Main, rootOrder++, methodId);
|
||||
_roots.Add(new JavaSyntheticRoot(
|
||||
RootId: rootId,
|
||||
TargetId: methodId,
|
||||
RootType: JavaRootType.Main,
|
||||
Source: "main",
|
||||
JarPath: jarPath,
|
||||
Phase: JavaRootPhase.Main,
|
||||
Order: rootOrder - 1));
|
||||
}
|
||||
|
||||
// Static initializer
|
||||
if (method.Name == "<clinit>")
|
||||
{
|
||||
var rootId = JavaGraphIdentifiers.ComputeRootId(JavaRootPhase.ClassLoad, rootOrder++, methodId);
|
||||
_roots.Add(new JavaSyntheticRoot(
|
||||
RootId: rootId,
|
||||
TargetId: methodId,
|
||||
RootType: JavaRootType.StaticInitializer,
|
||||
Source: "static_init",
|
||||
JarPath: jarPath,
|
||||
Phase: JavaRootPhase.ClassLoad,
|
||||
Order: rootOrder - 1));
|
||||
}
|
||||
|
||||
// Servlet lifecycle methods
|
||||
if (classFile.SuperClassName?.Contains("Servlet") == true ||
|
||||
classFile.Interfaces.Any(i => i.Contains("Servlet")))
|
||||
{
|
||||
if (method.Name == "init" && method.Descriptor.StartsWith("(Ljavax/servlet/"))
|
||||
{
|
||||
var rootId = JavaGraphIdentifiers.ComputeRootId(JavaRootPhase.AppInit, rootOrder++, methodId);
|
||||
_roots.Add(new JavaSyntheticRoot(
|
||||
RootId: rootId,
|
||||
TargetId: methodId,
|
||||
RootType: JavaRootType.ServletInit,
|
||||
Source: "servlet_init",
|
||||
JarPath: jarPath,
|
||||
Phase: JavaRootPhase.AppInit,
|
||||
Order: rootOrder - 1));
|
||||
}
|
||||
else if (method.Name is "service" or "doGet" or "doPost" or "doPut" or "doDelete")
|
||||
{
|
||||
var rootId = JavaGraphIdentifiers.ComputeRootId(JavaRootPhase.Main, rootOrder++, methodId);
|
||||
_roots.Add(new JavaSyntheticRoot(
|
||||
RootId: rootId,
|
||||
TargetId: methodId,
|
||||
RootType: JavaRootType.ServletHandler,
|
||||
Source: "servlet_handler",
|
||||
JarPath: jarPath,
|
||||
Phase: JavaRootPhase.Main,
|
||||
Order: rootOrder - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// JUnit test methods (check for @Test annotation in attributes)
|
||||
if ((method.AccessFlags & 0x0001) != 0 && // public
|
||||
method.Descriptor == "()V" &&
|
||||
!method.Name.StartsWith("<") &&
|
||||
method.HasTestAnnotation)
|
||||
{
|
||||
var rootId = JavaGraphIdentifiers.ComputeRootId(JavaRootPhase.Main, rootOrder++, methodId);
|
||||
_roots.Add(new JavaSyntheticRoot(
|
||||
RootId: rootId,
|
||||
TargetId: methodId,
|
||||
RootType: JavaRootType.TestMethod,
|
||||
Source: "junit_test",
|
||||
JarPath: jarPath,
|
||||
Phase: JavaRootPhase.Main,
|
||||
Order: rootOrder - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractCallEdges(
|
||||
string className,
|
||||
JavaClassFileParser.MethodInfo method,
|
||||
string jarPath,
|
||||
JavaClassFileParser.ConstantPool pool)
|
||||
{
|
||||
var callerId = JavaGraphIdentifiers.ComputeMethodId(className, method.Name, method.Descriptor);
|
||||
|
||||
if (method.Code is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var code = method.Code;
|
||||
var offset = 0;
|
||||
|
||||
while (offset < code.Length)
|
||||
{
|
||||
var instructionOffset = offset;
|
||||
var opcode = code[offset++];
|
||||
|
||||
switch (opcode)
|
||||
{
|
||||
case 0xB8: // invokestatic
|
||||
case 0xB6: // invokevirtual
|
||||
case 0xB7: // invokespecial
|
||||
case 0xB9: // invokeinterface
|
||||
{
|
||||
if (offset + 2 > code.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var methodIndex = (code[offset++] << 8) | code[offset++];
|
||||
if (opcode == 0xB9)
|
||||
{
|
||||
offset += 2; // count and zero
|
||||
}
|
||||
|
||||
var methodRef = pool.GetMethodReference(methodIndex);
|
||||
if (methodRef.HasValue)
|
||||
{
|
||||
var targetClass = JavaGraphIdentifiers.NormalizeClassName(methodRef.Value.OwnerInternalName);
|
||||
var targetMethodId = JavaGraphIdentifiers.ComputeMethodId(
|
||||
targetClass,
|
||||
methodRef.Value.Name,
|
||||
methodRef.Value.Descriptor);
|
||||
|
||||
var edgeType = opcode switch
|
||||
{
|
||||
0xB8 => JavaEdgeType.InvokeStatic,
|
||||
0xB6 => JavaEdgeType.InvokeVirtual,
|
||||
0xB7 => methodRef.Value.Name == "<init>" ? JavaEdgeType.Constructor : JavaEdgeType.InvokeSpecial,
|
||||
0xB9 => JavaEdgeType.InvokeInterface,
|
||||
_ => JavaEdgeType.InvokeVirtual,
|
||||
};
|
||||
|
||||
// Check if target is resolved (known in our method set)
|
||||
var isResolved = _methods.ContainsKey(targetMethodId) ||
|
||||
_classToJarPath.ContainsKey(targetClass.Replace('.', '/'));
|
||||
var calleePurl = isResolved ? GetPurlForClass(targetClass) : null;
|
||||
|
||||
var edgeId = JavaGraphIdentifiers.ComputeEdgeId(callerId, targetMethodId, instructionOffset);
|
||||
|
||||
_edges.Add(new JavaCallEdge(
|
||||
EdgeId: edgeId,
|
||||
CallerId: callerId,
|
||||
CalleeId: targetMethodId,
|
||||
CalleePurl: calleePurl,
|
||||
CalleeMethodDigest: null, // Would compute if method is in our set
|
||||
EdgeType: edgeType,
|
||||
BytecodeOffset: instructionOffset,
|
||||
IsResolved: isResolved,
|
||||
Confidence: isResolved ? 1.0 : 0.7));
|
||||
|
||||
if (!isResolved)
|
||||
{
|
||||
var unknownId = JavaGraphIdentifiers.ComputeUnknownId(
|
||||
edgeId,
|
||||
JavaUnknownType.UnresolvedMethod,
|
||||
targetClass,
|
||||
methodRef.Value.Name);
|
||||
_unknowns.Add(new JavaUnknown(
|
||||
UnknownId: unknownId,
|
||||
UnknownType: JavaUnknownType.UnresolvedMethod,
|
||||
SourceId: edgeId,
|
||||
ClassName: targetClass,
|
||||
MethodName: methodRef.Value.Name,
|
||||
Reason: "Method not found in analyzed classpath",
|
||||
JarPath: jarPath));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 0xBA: // invokedynamic
|
||||
{
|
||||
if (offset + 4 > code.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var dynamicIndex = (code[offset++] << 8) | code[offset++];
|
||||
offset += 2; // skip zeros
|
||||
|
||||
// invokedynamic targets are typically lambdas/method refs - emit as unknown
|
||||
var targetId = $"dynamic:{dynamicIndex}";
|
||||
var edgeId = JavaGraphIdentifiers.ComputeEdgeId(callerId, targetId, instructionOffset);
|
||||
|
||||
_edges.Add(new JavaCallEdge(
|
||||
EdgeId: edgeId,
|
||||
CallerId: callerId,
|
||||
CalleeId: targetId,
|
||||
CalleePurl: null,
|
||||
CalleeMethodDigest: null,
|
||||
EdgeType: JavaEdgeType.InvokeDynamic,
|
||||
BytecodeOffset: instructionOffset,
|
||||
IsResolved: false,
|
||||
Confidence: 0.3));
|
||||
|
||||
var unknownId = JavaGraphIdentifiers.ComputeUnknownId(
|
||||
edgeId,
|
||||
JavaUnknownType.DynamicTarget,
|
||||
null,
|
||||
null);
|
||||
_unknowns.Add(new JavaUnknown(
|
||||
UnknownId: unknownId,
|
||||
UnknownType: JavaUnknownType.DynamicTarget,
|
||||
SourceId: edgeId,
|
||||
ClassName: null,
|
||||
MethodName: null,
|
||||
Reason: "invokedynamic target requires bootstrap method resolution",
|
||||
JarPath: jarPath));
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Skip other instructions - advance based on opcode
|
||||
offset += GetInstructionSize(opcode) - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetPurlForClass(string className)
|
||||
{
|
||||
var internalName = className.Replace('.', '/');
|
||||
if (_classToJarPath.TryGetValue(internalName, out var jarPath))
|
||||
{
|
||||
// In production, would map JAR to Maven coordinates
|
||||
return $"pkg:maven/{Path.GetFileNameWithoutExtension(jarPath)}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int GetInstructionSize(byte opcode)
|
||||
{
|
||||
// Simplified instruction size lookup - production would have full table
|
||||
return opcode switch
|
||||
{
|
||||
// Zero operand instructions
|
||||
>= 0x00 and <= 0x0F => 1, // nop, aconst_null, iconst_*, lconst_*, fconst_*, dconst_*
|
||||
>= 0x1A and <= 0x35 => 1, // iload_*, lload_*, fload_*, dload_*, aload_*, *aload
|
||||
>= 0x3B and <= 0x56 => 1, // istore_*, lstore_*, fstore_*, dstore_*, astore_*, *astore
|
||||
>= 0x57 and <= 0x83 => 1, // pop, dup, swap, arithmetic, conversions
|
||||
>= 0x94 and <= 0x98 => 1, // lcmp, fcmp*, dcmp*
|
||||
>= 0xAC and <= 0xB1 => 1, // *return, return
|
||||
0xBE => 1, // arraylength
|
||||
0xBF => 1, // athrow
|
||||
0xC2 => 1, // monitorenter
|
||||
0xC3 => 1, // monitorexit
|
||||
|
||||
// Single byte operand
|
||||
0x10 => 2, // bipush
|
||||
>= 0x15 and <= 0x19 => 2, // iload, lload, fload, dload, aload
|
||||
>= 0x36 and <= 0x3A => 2, // istore, lstore, fstore, dstore, astore
|
||||
0xA9 => 2, // ret
|
||||
0xBC => 2, // newarray
|
||||
|
||||
// Two byte operand
|
||||
0x11 => 3, // sipush
|
||||
0x12 => 2, // ldc
|
||||
0x13 => 3, // ldc_w
|
||||
0x14 => 3, // ldc2_w
|
||||
0x84 => 3, // iinc
|
||||
>= 0x99 and <= 0xA8 => 3, // if*, goto, jsr
|
||||
>= 0xB2 and <= 0xB5 => 3, // get/put static/field
|
||||
>= 0xB6 and <= 0xB8 => 3, // invoke virtual/special/static
|
||||
0xB9 => 5, // invokeinterface
|
||||
0xBA => 5, // invokedynamic
|
||||
0xBB => 3, // new
|
||||
0xBD => 3, // anewarray
|
||||
0xC0 => 3, // checkcast
|
||||
0xC1 => 3, // instanceof
|
||||
0xC5 => 4, // multianewarray
|
||||
0xC6 => 3, // ifnull
|
||||
0xC7 => 3, // ifnonnull
|
||||
0xC8 => 5, // goto_w
|
||||
0xC9 => 5, // jsr_w
|
||||
|
||||
// Variable length (tableswitch, lookupswitch) - simplified
|
||||
0xAA => 16, // tableswitch (minimum)
|
||||
0xAB => 8, // lookupswitch (minimum)
|
||||
|
||||
// wide prefix
|
||||
0xC4 => 4, // wide (varies, using minimum)
|
||||
|
||||
_ => 1, // default
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Java class file parser for callgraph extraction.
|
||||
/// </summary>
|
||||
internal static class JavaClassFileParser
|
||||
{
|
||||
public static ClassFile Parse(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
var magic = ReadUInt32BE(reader);
|
||||
if (magic != 0xCAFEBABE)
|
||||
{
|
||||
throw new InvalidDataException("Invalid Java class file magic.");
|
||||
}
|
||||
|
||||
_ = ReadUInt16BE(reader); // minor version
|
||||
_ = ReadUInt16BE(reader); // major version
|
||||
|
||||
var constantPoolCount = ReadUInt16BE(reader);
|
||||
var pool = new ConstantPool(constantPoolCount);
|
||||
|
||||
for (var i = 1; i < constantPoolCount; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tag = reader.ReadByte();
|
||||
var entry = ReadConstantPoolEntry(reader, tag);
|
||||
pool.Set(i, entry);
|
||||
|
||||
// Long and Double take two slots
|
||||
if (tag == 5 || tag == 6)
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
_ = ReadUInt16BE(reader); // access flags
|
||||
var thisClassIndex = ReadUInt16BE(reader);
|
||||
var superClassIndex = ReadUInt16BE(reader);
|
||||
|
||||
var interfaceCount = ReadUInt16BE(reader);
|
||||
var interfaces = new string[interfaceCount];
|
||||
for (var i = 0; i < interfaceCount; i++)
|
||||
{
|
||||
var idx = ReadUInt16BE(reader);
|
||||
interfaces[i] = pool.GetClassName(idx) ?? "";
|
||||
}
|
||||
|
||||
var fieldCount = ReadUInt16BE(reader);
|
||||
for (var i = 0; i < fieldCount; i++)
|
||||
{
|
||||
SkipMember(reader);
|
||||
}
|
||||
|
||||
var methodCount = ReadUInt16BE(reader);
|
||||
var methods = new List<MethodInfo>(methodCount);
|
||||
for (var i = 0; i < methodCount; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var method = ReadMethod(reader, pool);
|
||||
methods.Add(method);
|
||||
}
|
||||
|
||||
// Skip class attributes
|
||||
var attrCount = ReadUInt16BE(reader);
|
||||
for (var i = 0; i < attrCount; i++)
|
||||
{
|
||||
SkipAttribute(reader);
|
||||
}
|
||||
|
||||
var thisClassName = pool.GetClassName(thisClassIndex);
|
||||
var superClassName = superClassIndex > 0 ? pool.GetClassName(superClassIndex) : null;
|
||||
|
||||
return new ClassFile(thisClassName ?? "", superClassName, interfaces.ToImmutableArray(), methods.ToImmutableArray(), pool);
|
||||
}
|
||||
|
||||
private static MethodInfo ReadMethod(BinaryReader reader, ConstantPool pool)
|
||||
{
|
||||
var accessFlags = ReadUInt16BE(reader);
|
||||
var nameIndex = ReadUInt16BE(reader);
|
||||
var descriptorIndex = ReadUInt16BE(reader);
|
||||
|
||||
var name = pool.GetUtf8(nameIndex) ?? "";
|
||||
var descriptor = pool.GetUtf8(descriptorIndex) ?? "";
|
||||
|
||||
byte[]? code = null;
|
||||
var hasTestAnnotation = false;
|
||||
|
||||
var attrCount = ReadUInt16BE(reader);
|
||||
for (var i = 0; i < attrCount; i++)
|
||||
{
|
||||
var attrNameIndex = ReadUInt16BE(reader);
|
||||
var attrLength = ReadUInt32BE(reader);
|
||||
var attrName = pool.GetUtf8(attrNameIndex) ?? "";
|
||||
|
||||
if (attrName == "Code")
|
||||
{
|
||||
_ = ReadUInt16BE(reader); // max_stack
|
||||
_ = ReadUInt16BE(reader); // max_locals
|
||||
var codeLength = ReadUInt32BE(reader);
|
||||
code = reader.ReadBytes((int)codeLength);
|
||||
|
||||
var exceptionTableLength = ReadUInt16BE(reader);
|
||||
for (var e = 0; e < exceptionTableLength; e++)
|
||||
{
|
||||
reader.ReadBytes(8);
|
||||
}
|
||||
|
||||
var codeAttrCount = ReadUInt16BE(reader);
|
||||
for (var c = 0; c < codeAttrCount; c++)
|
||||
{
|
||||
SkipAttribute(reader);
|
||||
}
|
||||
}
|
||||
else if (attrName == "RuntimeVisibleAnnotations" || attrName == "RuntimeInvisibleAnnotations")
|
||||
{
|
||||
var startPos = reader.BaseStream.Position;
|
||||
var numAnnotations = ReadUInt16BE(reader);
|
||||
for (var a = 0; a < numAnnotations; a++)
|
||||
{
|
||||
var typeIndex = ReadUInt16BE(reader);
|
||||
var annotationType = pool.GetUtf8(typeIndex) ?? "";
|
||||
if (annotationType.Contains("Test") || annotationType.Contains("org/junit"))
|
||||
{
|
||||
hasTestAnnotation = true;
|
||||
}
|
||||
|
||||
var numPairs = ReadUInt16BE(reader);
|
||||
for (var p = 0; p < numPairs; p++)
|
||||
{
|
||||
_ = ReadUInt16BE(reader); // element_name_index
|
||||
SkipAnnotationValue(reader);
|
||||
}
|
||||
}
|
||||
|
||||
// Seek to end of attribute if we didn't read it all
|
||||
reader.BaseStream.Position = startPos + attrLength - 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.ReadBytes((int)attrLength);
|
||||
}
|
||||
}
|
||||
|
||||
return new MethodInfo(name, descriptor, accessFlags, code, hasTestAnnotation);
|
||||
}
|
||||
|
||||
private static void SkipMember(BinaryReader reader)
|
||||
{
|
||||
reader.ReadBytes(6); // access_flags, name_index, descriptor_index
|
||||
var attrCount = ReadUInt16BE(reader);
|
||||
for (var i = 0; i < attrCount; i++)
|
||||
{
|
||||
SkipAttribute(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SkipAttribute(BinaryReader reader)
|
||||
{
|
||||
_ = ReadUInt16BE(reader); // name_index
|
||||
var length = ReadUInt32BE(reader);
|
||||
reader.ReadBytes((int)length);
|
||||
}
|
||||
|
||||
private static void SkipAnnotationValue(BinaryReader reader)
|
||||
{
|
||||
var tag = (char)reader.ReadByte();
|
||||
switch (tag)
|
||||
{
|
||||
case 'B':
|
||||
case 'C':
|
||||
case 'D':
|
||||
case 'F':
|
||||
case 'I':
|
||||
case 'J':
|
||||
case 'S':
|
||||
case 'Z':
|
||||
case 's':
|
||||
case 'c':
|
||||
ReadUInt16BE(reader);
|
||||
break;
|
||||
case 'e':
|
||||
ReadUInt16BE(reader);
|
||||
ReadUInt16BE(reader);
|
||||
break;
|
||||
case '@':
|
||||
ReadUInt16BE(reader);
|
||||
var numPairs = ReadUInt16BE(reader);
|
||||
for (var i = 0; i < numPairs; i++)
|
||||
{
|
||||
ReadUInt16BE(reader);
|
||||
SkipAnnotationValue(reader);
|
||||
}
|
||||
|
||||
break;
|
||||
case '[':
|
||||
var numValues = ReadUInt16BE(reader);
|
||||
for (var i = 0; i < numValues; i++)
|
||||
{
|
||||
SkipAnnotationValue(reader);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static ConstantPoolEntry ReadConstantPoolEntry(BinaryReader reader, byte tag)
|
||||
{
|
||||
return tag switch
|
||||
{
|
||||
1 => new ConstantPoolEntry.Utf8Entry(ReadUtf8(reader)),
|
||||
3 => new ConstantPoolEntry.IntegerEntry(ReadUInt32BE(reader)),
|
||||
4 => new ConstantPoolEntry.FloatEntry(reader.ReadBytes(4)),
|
||||
5 => new ConstantPoolEntry.LongEntry(reader.ReadBytes(8)),
|
||||
6 => new ConstantPoolEntry.DoubleEntry(reader.ReadBytes(8)),
|
||||
7 => new ConstantPoolEntry.ClassEntry(ReadUInt16BE(reader)),
|
||||
8 => new ConstantPoolEntry.StringEntry(ReadUInt16BE(reader)),
|
||||
9 => new ConstantPoolEntry.FieldrefEntry(ReadUInt16BE(reader), ReadUInt16BE(reader)),
|
||||
10 => new ConstantPoolEntry.MethodrefEntry(ReadUInt16BE(reader), ReadUInt16BE(reader)),
|
||||
11 => new ConstantPoolEntry.InterfaceMethodrefEntry(ReadUInt16BE(reader), ReadUInt16BE(reader)),
|
||||
12 => new ConstantPoolEntry.NameAndTypeEntry(ReadUInt16BE(reader), ReadUInt16BE(reader)),
|
||||
15 => new ConstantPoolEntry.MethodHandleEntry(reader.ReadByte(), ReadUInt16BE(reader)),
|
||||
16 => new ConstantPoolEntry.MethodTypeEntry(ReadUInt16BE(reader)),
|
||||
17 => new ConstantPoolEntry.DynamicEntry(ReadUInt16BE(reader), ReadUInt16BE(reader)),
|
||||
18 => new ConstantPoolEntry.InvokeDynamicEntry(ReadUInt16BE(reader), ReadUInt16BE(reader)),
|
||||
19 => new ConstantPoolEntry.ModuleEntry(ReadUInt16BE(reader)),
|
||||
20 => new ConstantPoolEntry.PackageEntry(ReadUInt16BE(reader)),
|
||||
_ => throw new InvalidDataException($"Unknown constant pool tag: {tag}"),
|
||||
};
|
||||
}
|
||||
|
||||
private static ushort ReadUInt16BE(BinaryReader reader)
|
||||
{
|
||||
var b1 = reader.ReadByte();
|
||||
var b2 = reader.ReadByte();
|
||||
return (ushort)((b1 << 8) | b2);
|
||||
}
|
||||
|
||||
private static uint ReadUInt32BE(BinaryReader reader)
|
||||
{
|
||||
var b1 = reader.ReadByte();
|
||||
var b2 = reader.ReadByte();
|
||||
var b3 = reader.ReadByte();
|
||||
var b4 = reader.ReadByte();
|
||||
return (uint)((b1 << 24) | (b2 << 16) | (b3 << 8) | b4);
|
||||
}
|
||||
|
||||
private static string ReadUtf8(BinaryReader reader)
|
||||
{
|
||||
var length = ReadUInt16BE(reader);
|
||||
var bytes = reader.ReadBytes(length);
|
||||
return System.Text.Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
public sealed record ClassFile(
|
||||
string ThisClassName,
|
||||
string? SuperClassName,
|
||||
ImmutableArray<string> Interfaces,
|
||||
ImmutableArray<MethodInfo> Methods,
|
||||
ConstantPool ConstantPool);
|
||||
|
||||
public sealed record MethodInfo(
|
||||
string Name,
|
||||
string Descriptor,
|
||||
int AccessFlags,
|
||||
byte[]? Code,
|
||||
bool HasTestAnnotation);
|
||||
|
||||
public sealed class ConstantPool
|
||||
{
|
||||
private readonly ConstantPoolEntry?[] _entries;
|
||||
|
||||
public ConstantPool(int count)
|
||||
{
|
||||
_entries = new ConstantPoolEntry?[count];
|
||||
}
|
||||
|
||||
public void Set(int index, ConstantPoolEntry entry)
|
||||
{
|
||||
_entries[index] = entry;
|
||||
}
|
||||
|
||||
public string? GetUtf8(int index)
|
||||
{
|
||||
if (index <= 0 || index >= _entries.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _entries[index] is ConstantPoolEntry.Utf8Entry utf8 ? utf8.Value : null;
|
||||
}
|
||||
|
||||
public string? GetClassName(int index)
|
||||
{
|
||||
if (_entries[index] is ConstantPoolEntry.ClassEntry classEntry)
|
||||
{
|
||||
return GetUtf8(classEntry.NameIndex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public MethodReference? GetMethodReference(int index)
|
||||
{
|
||||
if (_entries[index] is not ConstantPoolEntry.MethodrefEntry and not ConstantPoolEntry.InterfaceMethodrefEntry)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var (classIndex, nameAndTypeIndex) = _entries[index] switch
|
||||
{
|
||||
ConstantPoolEntry.MethodrefEntry m => (m.ClassIndex, m.NameAndTypeIndex),
|
||||
ConstantPoolEntry.InterfaceMethodrefEntry m => (m.ClassIndex, m.NameAndTypeIndex),
|
||||
_ => (0, 0),
|
||||
};
|
||||
|
||||
var owner = GetClassName(classIndex);
|
||||
if (owner is null || _entries[nameAndTypeIndex] is not ConstantPoolEntry.NameAndTypeEntry nat)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = GetUtf8(nat.NameIndex) ?? "";
|
||||
var descriptor = GetUtf8(nat.DescriptorIndex) ?? "";
|
||||
return new MethodReference(owner, name, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct MethodReference(string OwnerInternalName, string Name, string Descriptor);
|
||||
|
||||
public abstract record ConstantPoolEntry
|
||||
{
|
||||
public sealed record Utf8Entry(string Value) : ConstantPoolEntry;
|
||||
|
||||
public sealed record IntegerEntry(uint Value) : ConstantPoolEntry;
|
||||
|
||||
public sealed record FloatEntry(byte[] Bytes) : ConstantPoolEntry;
|
||||
|
||||
public sealed record LongEntry(byte[] Bytes) : ConstantPoolEntry;
|
||||
|
||||
public sealed record DoubleEntry(byte[] Bytes) : ConstantPoolEntry;
|
||||
|
||||
public sealed record ClassEntry(int NameIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record StringEntry(int StringIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record FieldrefEntry(int ClassIndex, int NameAndTypeIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record MethodrefEntry(int ClassIndex, int NameAndTypeIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record InterfaceMethodrefEntry(int ClassIndex, int NameAndTypeIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record NameAndTypeEntry(int NameIndex, int DescriptorIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record MethodHandleEntry(byte ReferenceKind, int ReferenceIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record MethodTypeEntry(int DescriptorIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record DynamicEntry(int BootstrapMethodAttrIndex, int NameAndTypeIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record InvokeDynamicEntry(int BootstrapMethodAttrIndex, int NameAndTypeIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record ModuleEntry(int NameIndex) : ConstantPoolEntry;
|
||||
|
||||
public sealed record PackageEntry(int NameIndex) : ConstantPoolEntry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Callgraph;
|
||||
|
||||
/// <summary>
|
||||
/// Java reachability graph containing methods, call edges, and metadata.
|
||||
/// </summary>
|
||||
public sealed record JavaReachabilityGraph(
|
||||
string ContextDigest,
|
||||
ImmutableArray<JavaMethodNode> Methods,
|
||||
ImmutableArray<JavaCallEdge> Edges,
|
||||
ImmutableArray<JavaSyntheticRoot> SyntheticRoots,
|
||||
ImmutableArray<JavaUnknown> Unknowns,
|
||||
JavaGraphMetadata Metadata,
|
||||
string ContentHash);
|
||||
|
||||
/// <summary>
|
||||
/// A method node in the Java call graph.
|
||||
/// </summary>
|
||||
/// <param name="MethodId">Deterministic method identifier (sha256 of class+name+descriptor).</param>
|
||||
/// <param name="ClassName">Fully qualified class name (e.g., java.lang.String).</param>
|
||||
/// <param name="MethodName">Method name.</param>
|
||||
/// <param name="Descriptor">JVM method descriptor (e.g., (Ljava/lang/String;)V).</param>
|
||||
/// <param name="Purl">Package URL if resolvable (e.g., pkg:maven/org.example/lib).</param>
|
||||
/// <param name="JarPath">Path to the containing JAR file.</param>
|
||||
/// <param name="AccessFlags">Method access flags (public, static, etc.).</param>
|
||||
/// <param name="MethodDigest">SHA-256 of (class + name + descriptor + accessFlags).</param>
|
||||
/// <param name="IsStatic">Whether the method is static.</param>
|
||||
/// <param name="IsPublic">Whether the method is public.</param>
|
||||
/// <param name="IsSynthetic">Whether the method is synthetic (compiler-generated).</param>
|
||||
/// <param name="IsBridge">Whether the method is a bridge method.</param>
|
||||
public sealed record JavaMethodNode(
|
||||
string MethodId,
|
||||
string ClassName,
|
||||
string MethodName,
|
||||
string Descriptor,
|
||||
string? Purl,
|
||||
string JarPath,
|
||||
int AccessFlags,
|
||||
string MethodDigest,
|
||||
bool IsStatic,
|
||||
bool IsPublic,
|
||||
bool IsSynthetic,
|
||||
bool IsBridge);
|
||||
|
||||
/// <summary>
|
||||
/// A call edge in the Java call graph.
|
||||
/// </summary>
|
||||
/// <param name="EdgeId">Deterministic edge identifier.</param>
|
||||
/// <param name="CallerId">MethodId of the calling method.</param>
|
||||
/// <param name="CalleeId">MethodId of the called method (or Unknown placeholder).</param>
|
||||
/// <param name="CalleePurl">PURL of the callee if resolvable.</param>
|
||||
/// <param name="CalleeMethodDigest">Method digest of the callee.</param>
|
||||
/// <param name="EdgeType">Type of edge (invoke type).</param>
|
||||
/// <param name="BytecodeOffset">Bytecode offset 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 JavaCallEdge(
|
||||
string EdgeId,
|
||||
string CallerId,
|
||||
string CalleeId,
|
||||
string? CalleePurl,
|
||||
string? CalleeMethodDigest,
|
||||
JavaEdgeType EdgeType,
|
||||
int BytecodeOffset,
|
||||
bool IsResolved,
|
||||
double Confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Type of Java call edge.
|
||||
/// </summary>
|
||||
public enum JavaEdgeType
|
||||
{
|
||||
/// <summary>invokestatic - static method call.</summary>
|
||||
InvokeStatic,
|
||||
|
||||
/// <summary>invokevirtual - virtual method call.</summary>
|
||||
InvokeVirtual,
|
||||
|
||||
/// <summary>invokeinterface - interface method call.</summary>
|
||||
InvokeInterface,
|
||||
|
||||
/// <summary>invokespecial - constructor, super, private method call.</summary>
|
||||
InvokeSpecial,
|
||||
|
||||
/// <summary>invokedynamic - lambda/method reference.</summary>
|
||||
InvokeDynamic,
|
||||
|
||||
/// <summary>Class.forName reflection call.</summary>
|
||||
Reflection,
|
||||
|
||||
/// <summary>ServiceLoader.load call.</summary>
|
||||
ServiceLoader,
|
||||
|
||||
/// <summary>Constructor invocation.</summary>
|
||||
Constructor,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A synthetic root in the Java call graph.
|
||||
/// </summary>
|
||||
/// <param name="RootId">Deterministic root identifier.</param>
|
||||
/// <param name="TargetId">MethodId of the target method.</param>
|
||||
/// <param name="RootType">Type of synthetic root.</param>
|
||||
/// <param name="Source">Source of the root (e.g., main, static_init, servlet).</param>
|
||||
/// <param name="JarPath">Path to the containing JAR.</param>
|
||||
/// <param name="Phase">Execution phase.</param>
|
||||
/// <param name="Order">Order within the phase.</param>
|
||||
/// <param name="IsResolved">Whether the target was successfully resolved.</param>
|
||||
public sealed record JavaSyntheticRoot(
|
||||
string RootId,
|
||||
string TargetId,
|
||||
JavaRootType RootType,
|
||||
string Source,
|
||||
string JarPath,
|
||||
JavaRootPhase Phase,
|
||||
int Order,
|
||||
bool IsResolved = true);
|
||||
|
||||
/// <summary>
|
||||
/// Execution phase for Java synthetic roots.
|
||||
/// </summary>
|
||||
public enum JavaRootPhase
|
||||
{
|
||||
/// <summary>Class loading phase - static initializers.</summary>
|
||||
ClassLoad = 0,
|
||||
|
||||
/// <summary>Application initialization - servlet init, Spring context.</summary>
|
||||
AppInit = 1,
|
||||
|
||||
/// <summary>Main execution - main method, request handlers.</summary>
|
||||
Main = 2,
|
||||
|
||||
/// <summary>Shutdown - destroy methods, shutdown hooks.</summary>
|
||||
Shutdown = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of Java synthetic root.
|
||||
/// </summary>
|
||||
public enum JavaRootType
|
||||
{
|
||||
/// <summary>main(String[] args) method.</summary>
|
||||
Main,
|
||||
|
||||
/// <summary>Static initializer block (<clinit>).</summary>
|
||||
StaticInitializer,
|
||||
|
||||
/// <summary>Instance initializer (<init>).</summary>
|
||||
Constructor,
|
||||
|
||||
/// <summary>Servlet init method.</summary>
|
||||
ServletInit,
|
||||
|
||||
/// <summary>Servlet service/doGet/doPost methods.</summary>
|
||||
ServletHandler,
|
||||
|
||||
/// <summary>Spring @PostConstruct.</summary>
|
||||
PostConstruct,
|
||||
|
||||
/// <summary>Spring @PreDestroy.</summary>
|
||||
PreDestroy,
|
||||
|
||||
/// <summary>JUnit @Test method.</summary>
|
||||
TestMethod,
|
||||
|
||||
/// <summary>Runtime shutdown hook.</summary>
|
||||
ShutdownHook,
|
||||
|
||||
/// <summary>Thread run method.</summary>
|
||||
ThreadRun,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An unknown/unresolved reference in the Java call graph.
|
||||
/// </summary>
|
||||
public sealed record JavaUnknown(
|
||||
string UnknownId,
|
||||
JavaUnknownType UnknownType,
|
||||
string SourceId,
|
||||
string? ClassName,
|
||||
string? MethodName,
|
||||
string Reason,
|
||||
string JarPath);
|
||||
|
||||
/// <summary>
|
||||
/// Type of unknown reference in Java.
|
||||
/// </summary>
|
||||
public enum JavaUnknownType
|
||||
{
|
||||
/// <summary>Class could not be resolved.</summary>
|
||||
UnresolvedClass,
|
||||
|
||||
/// <summary>Method could not be resolved.</summary>
|
||||
UnresolvedMethod,
|
||||
|
||||
/// <summary>Dynamic invoke target is unknown.</summary>
|
||||
DynamicTarget,
|
||||
|
||||
/// <summary>Reflection target is unknown.</summary>
|
||||
ReflectionTarget,
|
||||
|
||||
/// <summary>Service provider class is unknown.</summary>
|
||||
ServiceProvider,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata for the Java reachability graph.
|
||||
/// </summary>
|
||||
public sealed record JavaGraphMetadata(
|
||||
DateTimeOffset GeneratedAt,
|
||||
string GeneratorVersion,
|
||||
string ContextDigest,
|
||||
int JarCount,
|
||||
int ClassCount,
|
||||
int MethodCount,
|
||||
int EdgeCount,
|
||||
int UnknownCount,
|
||||
int SyntheticRootCount);
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for creating deterministic Java graph identifiers.
|
||||
/// </summary>
|
||||
internal static class JavaGraphIdentifiers
|
||||
{
|
||||
private const string GeneratorVersion = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic method ID from class, name, and descriptor.
|
||||
/// </summary>
|
||||
public static string ComputeMethodId(string className, string methodName, string descriptor)
|
||||
{
|
||||
var input = $"{className}:{methodName}:{descriptor}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"jmethod:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic method digest.
|
||||
/// </summary>
|
||||
public static string ComputeMethodDigest(string className, string methodName, string descriptor, int accessFlags)
|
||||
{
|
||||
var input = $"{className}:{methodName}:{descriptor}:{accessFlags}";
|
||||
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, int bytecodeOffset)
|
||||
{
|
||||
var input = $"{callerId}:{calleeId}:{bytecodeOffset}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"jedge:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic root ID.
|
||||
/// </summary>
|
||||
public static string ComputeRootId(JavaRootPhase phase, int order, string targetId)
|
||||
{
|
||||
var phaseName = phase.ToString().ToLowerInvariant();
|
||||
return $"jroot:{phaseName}:{order}:{targetId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic unknown ID.
|
||||
/// </summary>
|
||||
public static string ComputeUnknownId(string sourceId, JavaUnknownType unknownType, string? className, string? methodName)
|
||||
{
|
||||
var input = $"{sourceId}:{unknownType}:{className ?? ""}:{methodName ?? ""}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"junk:{Convert.ToHexString(hash[..8]).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes content hash for the entire graph.
|
||||
/// </summary>
|
||||
public static string ComputeGraphHash(
|
||||
ImmutableArray<JavaMethodNode> methods,
|
||||
ImmutableArray<JavaCallEdge> edges,
|
||||
ImmutableArray<JavaSyntheticRoot> roots)
|
||||
{
|
||||
using var sha = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
|
||||
foreach (var m in methods.OrderBy(m => m.MethodId))
|
||||
{
|
||||
sha.AppendData(Encoding.UTF8.GetBytes(m.MethodId));
|
||||
sha.AppendData(Encoding.UTF8.GetBytes(m.MethodDigest));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a JVM internal class name to fully qualified format.
|
||||
/// </summary>
|
||||
public static string NormalizeClassName(string internalName)
|
||||
{
|
||||
return internalName.Replace('/', '.');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a method descriptor to extract readable signature.
|
||||
/// </summary>
|
||||
public static string ParseDescriptor(string descriptor)
|
||||
{
|
||||
// Simplified parsing - full implementation would properly decode JVM descriptors
|
||||
return descriptor;
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,48 @@ using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Maven;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Collects and parses Java lock files (Gradle lockfiles, Maven POMs) to produce dependency entries.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><strong>Lock Precedence Rules (Sprint 0403 / Interlock 2):</strong></para>
|
||||
/// <list type="number">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <strong>Gradle lockfiles</strong> are highest priority (most reliable, resolved coordinates).
|
||||
/// When multiple lockfiles exist across a multi-module project, they are processed in
|
||||
/// lexicographic order by relative path (ensures deterministic iteration).
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <strong>De-duplication rule:</strong> For the same GAV (group:artifact:version),
|
||||
/// the <em>first</em> lockfile encountered (by lexicographic path order) wins.
|
||||
/// This means root-level lockfiles (e.g., <c>gradle.lockfile</c>) are processed before
|
||||
/// submodule lockfiles (e.g., <c>app/gradle.lockfile</c>) and thus take precedence.
|
||||
/// <para>Rationale: Root lockfiles typically represent the resolved dependency graph for
|
||||
/// the entire project; module-level lockfiles may contain duplicates or overrides.</para>
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <strong>Gradle build files</strong> (when no lockfiles exist) are parsed with version
|
||||
/// catalog resolution. Same lexicographic + first-wins rule applies.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <strong>Maven POMs</strong> are lowest priority and are additive (TryAdd semantics).
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Each entry carries <c>lockLocator</c> (relative path to the source file) and
|
||||
/// <c>lockModulePath</c> (module directory context, e.g., <c>.</c> for root, <c>app</c> for submodule).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal static class JavaLockFileCollector
|
||||
{
|
||||
private static readonly string[] GradleLockPatterns = ["gradle.lockfile"];
|
||||
|
||||
public static async Task<JavaLockData> LoadAsync(LanguageAnalyzerContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
@@ -20,25 +58,17 @@ internal static class JavaLockFileCollector
|
||||
var entries = new Dictionary<string, JavaLockEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
var root = context.RootPath;
|
||||
|
||||
// Discover all build files
|
||||
// Discover all build files (returns paths sorted by RelativePath for determinism)
|
||||
var buildFiles = JavaBuildFileDiscovery.Discover(root);
|
||||
|
||||
// Priority 1: Gradle lockfiles (most reliable)
|
||||
foreach (var pattern in GradleLockPatterns)
|
||||
// Priority 1: Gradle lockfiles from discovery (most reliable)
|
||||
// Processed in lexicographic order by relative path; first-wins for duplicate GAVs.
|
||||
if (buildFiles.HasGradleLockFiles)
|
||||
{
|
||||
var lockPath = Path.Combine(root, pattern);
|
||||
if (File.Exists(lockPath))
|
||||
foreach (var lockFile in buildFiles.GradleLockFiles)
|
||||
{
|
||||
await ParseGradleLockFileAsync(context, lockPath, entries, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var dependencyLocksDir = Path.Combine(root, "gradle", "dependency-locks");
|
||||
if (Directory.Exists(dependencyLocksDir))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(dependencyLocksDir, "*.lockfile", SearchOption.AllDirectories))
|
||||
{
|
||||
await ParseGradleLockFileAsync(context, file, entries, cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await ParseGradleLockFileAsync(context, lockFile.AbsolutePath, lockFile.ProjectDirectory, entries, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,12 +99,16 @@ internal static class JavaLockFileCollector
|
||||
private static async Task ParseGradleLockFileAsync(
|
||||
LanguageAnalyzerContext context,
|
||||
string path,
|
||||
string modulePath,
|
||||
IDictionary<string, JavaLockEntry> entries,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var locator = NormalizeLocator(context, path);
|
||||
var normalizedModulePath = NormalizeModulePath(modulePath);
|
||||
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null)
|
||||
{
|
||||
@@ -113,7 +147,8 @@ internal static class JavaLockFileCollector
|
||||
artifactId.Trim(),
|
||||
version.Trim(),
|
||||
Path.GetFileName(path),
|
||||
NormalizeLocator(context, path),
|
||||
locator,
|
||||
normalizedModulePath,
|
||||
configuration,
|
||||
null,
|
||||
null,
|
||||
@@ -124,10 +159,22 @@ internal static class JavaLockFileCollector
|
||||
null,
|
||||
null);
|
||||
|
||||
entries[entry.Key] = entry;
|
||||
// First-wins for duplicate GAVs (entries are processed in lexicographic order)
|
||||
entries.TryAdd(entry.Key, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeModulePath(string? modulePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modulePath))
|
||||
{
|
||||
return ".";
|
||||
}
|
||||
|
||||
var normalized = modulePath.Replace('\\', '/').Trim('/');
|
||||
return string.IsNullOrEmpty(normalized) ? "." : normalized;
|
||||
}
|
||||
|
||||
private static async Task ParseGradleBuildFilesAsync(
|
||||
LanguageAnalyzerContext context,
|
||||
JavaBuildFiles buildFiles,
|
||||
@@ -190,6 +237,9 @@ internal static class JavaLockFileCollector
|
||||
GradleVersionCatalog? versionCatalog,
|
||||
IDictionary<string, JavaLockEntry> entries)
|
||||
{
|
||||
var locator = NormalizeLocator(context, buildFile.SourcePath);
|
||||
var modulePath = NormalizeModulePath(Path.GetDirectoryName(context.GetRelativePath(buildFile.SourcePath)));
|
||||
|
||||
foreach (var dep in buildFile.Dependencies)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dep.GroupId) || string.IsNullOrWhiteSpace(dep.ArtifactId))
|
||||
@@ -224,7 +274,8 @@ internal static class JavaLockFileCollector
|
||||
dep.ArtifactId,
|
||||
version,
|
||||
Path.GetFileName(buildFile.SourcePath),
|
||||
NormalizeLocator(context, buildFile.SourcePath),
|
||||
locator,
|
||||
modulePath,
|
||||
scope,
|
||||
null,
|
||||
null,
|
||||
@@ -257,6 +308,9 @@ internal static class JavaLockFileCollector
|
||||
var effectivePomBuilder = new MavenEffectivePomBuilder(context.RootPath);
|
||||
var effectivePom = await effectivePomBuilder.BuildAsync(pom, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var locator = NormalizeLocator(context, path);
|
||||
var modulePath = NormalizeModulePath(Path.GetDirectoryName(context.GetRelativePath(path)));
|
||||
|
||||
foreach (var dep in effectivePom.ResolvedDependencies)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -281,7 +335,8 @@ internal static class JavaLockFileCollector
|
||||
dep.ArtifactId,
|
||||
dep.Version,
|
||||
"pom.xml",
|
||||
NormalizeLocator(context, path),
|
||||
locator,
|
||||
modulePath,
|
||||
scope,
|
||||
null,
|
||||
null,
|
||||
@@ -311,6 +366,9 @@ internal static class JavaLockFileCollector
|
||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var document = await XDocument.LoadAsync(stream, LoadOptions.None, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var locator = NormalizeLocator(context, path);
|
||||
var modulePath = NormalizeModulePath(Path.GetDirectoryName(context.GetRelativePath(path)));
|
||||
|
||||
var dependencies = document
|
||||
.Descendants()
|
||||
.Where(static element => element.Name.LocalName.Equals("dependency", StringComparison.OrdinalIgnoreCase));
|
||||
@@ -343,7 +401,8 @@ internal static class JavaLockFileCollector
|
||||
artifactId,
|
||||
version,
|
||||
"pom.xml",
|
||||
NormalizeLocator(context, path),
|
||||
locator,
|
||||
modulePath,
|
||||
scope,
|
||||
repository,
|
||||
null,
|
||||
@@ -400,6 +459,7 @@ internal sealed record JavaLockEntry(
|
||||
string Version,
|
||||
string Source,
|
||||
string Locator,
|
||||
string? LockModulePath,
|
||||
string? Configuration,
|
||||
string? Repository,
|
||||
string? ResolvedUrl,
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
@@ -61,6 +62,9 @@ public sealed class JavaLanguageAnalyzer : ILanguageAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
// Task 403-004: Emit runtime image components (explicit-key, no PURL to avoid false vuln matches)
|
||||
EmitRuntimeImageComponents(workspace, Id, writer, context, cancellationToken);
|
||||
|
||||
// E5: Detect version conflicts
|
||||
var conflictAnalysis = BuildConflictAnalysis(lockData);
|
||||
|
||||
@@ -849,6 +853,7 @@ public sealed class JavaLanguageAnalyzer : ILanguageAnalyzer
|
||||
AddMetadata(metadata, "lockConfiguration", entry.Configuration);
|
||||
AddMetadata(metadata, "lockRepository", entry.Repository);
|
||||
AddMetadata(metadata, "lockResolved", entry.ResolvedUrl);
|
||||
AddMetadata(metadata, "lockModulePath", entry.LockModulePath);
|
||||
|
||||
// E4: Add scope and risk level metadata
|
||||
AddMetadata(metadata, "declaredScope", entry.Scope);
|
||||
@@ -1678,6 +1683,121 @@ internal sealed record JniHintSummary(
|
||||
EvidenceSha256: sha256);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits runtime image components discovered by JavaWorkspaceNormalizer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><strong>Runtime Component Identity Decision (Sprint 0403 / Action 2):</strong></para>
|
||||
/// <para>
|
||||
/// Java runtime images are emitted using <em>explicit-key</em> (not PURL) to avoid false
|
||||
/// vulnerability matches. There is no standardized PURL scheme for JDK/JRE installations
|
||||
/// that reliably maps to CVE advisories. Using explicit-key ensures runtime context is
|
||||
/// captured without introducing misleading vuln alerts.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The component key is formed from: analyzerId + "java-runtime" + version + vendor + relativePath.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Deduplication: identical runtime images (same version+vendor+relativePath) are emitted only once.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static void EmitRuntimeImageComponents(
|
||||
JavaWorkspace workspace,
|
||||
string analyzerId,
|
||||
LanguageComponentWriter writer,
|
||||
LanguageAnalyzerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(workspace);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(analyzerId);
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
if (workspace.RuntimeImages.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate by (version, vendor, relativePath) - deterministic ordering
|
||||
var seenRuntimes = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var runtime in workspace.RuntimeImages.OrderBy(r => r.RelativePath, StringComparer.Ordinal))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Create dedup key from identifying properties
|
||||
var dedupKey = $"{runtime.JavaVersion}|{runtime.Vendor}|{runtime.RelativePath}";
|
||||
if (!seenRuntimes.Add(dedupKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedPath = runtime.RelativePath.Replace('\\', '/');
|
||||
var releaseLocator = string.IsNullOrEmpty(normalizedPath) || normalizedPath == "."
|
||||
? "release"
|
||||
: $"{normalizedPath}/release";
|
||||
|
||||
// Compute evidence SHA256 from release file
|
||||
string? releaseSha256 = null;
|
||||
var releaseFilePath = Path.Combine(runtime.AbsolutePath, "release");
|
||||
if (File.Exists(releaseFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var releaseBytes = File.ReadAllBytes(releaseFilePath);
|
||||
releaseSha256 = Convert.ToHexString(SHA256.HashData(releaseBytes)).ToLowerInvariant();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Cannot read release file; proceed without SHA256
|
||||
}
|
||||
}
|
||||
|
||||
// Build component metadata
|
||||
var metadata = new List<KeyValuePair<string, string?>>(8);
|
||||
AddMetadata(metadata, "java.version", runtime.JavaVersion);
|
||||
AddMetadata(metadata, "java.vendor", runtime.Vendor);
|
||||
AddMetadata(metadata, "runtimeImagePath", normalizedPath, allowEmpty: true);
|
||||
AddMetadata(metadata, "componentType", "java-runtime");
|
||||
|
||||
// Build evidence referencing the release file
|
||||
var evidence = new[]
|
||||
{
|
||||
new LanguageComponentEvidence(
|
||||
LanguageEvidenceKind.File,
|
||||
"release",
|
||||
releaseLocator,
|
||||
null,
|
||||
releaseSha256),
|
||||
};
|
||||
|
||||
// Build explicit component key (no PURL to avoid false vuln matches)
|
||||
var componentKeyData = $"{runtime.JavaVersion}:{runtime.Vendor}:{normalizedPath}";
|
||||
var componentKey = LanguageExplicitKey.Create(
|
||||
analyzerId,
|
||||
"java-runtime",
|
||||
componentKeyData,
|
||||
releaseSha256 ?? string.Empty,
|
||||
releaseLocator);
|
||||
|
||||
// Emit component name: e.g., "java-runtime-21.0.1" or "java-runtime-21.0.1 (Eclipse Adoptium)"
|
||||
var componentName = string.IsNullOrWhiteSpace(runtime.Vendor)
|
||||
? $"java-runtime-{runtime.JavaVersion}"
|
||||
: $"java-runtime-{runtime.JavaVersion} ({runtime.Vendor})";
|
||||
|
||||
writer.AddFromExplicitKey(
|
||||
analyzerId: analyzerId,
|
||||
componentKey: componentKey,
|
||||
purl: null,
|
||||
name: componentName,
|
||||
version: runtime.JavaVersion,
|
||||
type: "java-runtime",
|
||||
metadata: SortMetadata(metadata),
|
||||
evidence: evidence,
|
||||
usedByEntrypoint: false);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<KeyValuePair<string, string?>> CreateDeclaredMetadata(
|
||||
JavaLockEntry entry,
|
||||
VersionConflictAnalysis conflictAnalysis)
|
||||
|
||||
Reference in New Issue
Block a user