Refactor and update test projects, remove obsolete tests, and upgrade dependencies
- Deleted obsolete test files for SchedulerAuditService and SchedulerMongoSessionFactory. - Removed unused TestDataFactory class. - Updated project files for Mongo.Tests to remove references to deleted files. - Upgraded BouncyCastle.Cryptography package to version 2.6.2 across multiple projects. - Replaced Microsoft.Extensions.Http.Polly with Microsoft.Extensions.Http.Resilience in Zastava.Webhook project. - Updated NetEscapades.Configuration.Yaml package to version 3.1.0 in Configuration library. - Upgraded Pkcs11Interop package to version 5.1.2 in Cryptography libraries. - Refactored Argon2idPasswordHasher to use BouncyCastle for hashing instead of Konscious. - Updated JsonSchema.Net package to version 7.3.2 in Microservice project. - Updated global.json to use .NET SDK version 10.0.101.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Jni;
|
||||
|
||||
/// <summary>
|
||||
/// Results of JNI/native code analysis including edges with reason codes and confidence.
|
||||
/// </summary>
|
||||
internal sealed record JavaJniAnalysis(
|
||||
ImmutableArray<JavaJniEdge> Edges,
|
||||
ImmutableArray<JavaJniWarning> Warnings)
|
||||
{
|
||||
public static readonly JavaJniAnalysis Empty = new(
|
||||
ImmutableArray<JavaJniEdge>.Empty,
|
||||
ImmutableArray<JavaJniWarning>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JNI edge from a source class/method to a native target.
|
||||
/// </summary>
|
||||
/// <param name="SourceClass">Fully qualified class name containing the JNI reference.</param>
|
||||
/// <param name="SegmentIdentifier">Classpath segment (JAR, module) identifier.</param>
|
||||
/// <param name="TargetLibrary">Target native library name or path (null for native method declarations).</param>
|
||||
/// <param name="Reason">Reason code for the edge (native, load, loadLibrary, graalConfig).</param>
|
||||
/// <param name="Confidence">Confidence level for the edge detection.</param>
|
||||
/// <param name="MethodName">Method name where the JNI reference occurs.</param>
|
||||
/// <param name="MethodDescriptor">JVM method descriptor.</param>
|
||||
/// <param name="InstructionOffset">Bytecode offset where the call site occurs (-1 for native methods).</param>
|
||||
/// <param name="Details">Additional details about the JNI usage.</param>
|
||||
internal sealed record JavaJniEdge(
|
||||
string SourceClass,
|
||||
string SegmentIdentifier,
|
||||
string? TargetLibrary,
|
||||
JavaJniReason Reason,
|
||||
JavaJniConfidence Confidence,
|
||||
string MethodName,
|
||||
string MethodDescriptor,
|
||||
int InstructionOffset,
|
||||
string? Details);
|
||||
|
||||
/// <summary>
|
||||
/// Warning emitted during JNI analysis.
|
||||
/// </summary>
|
||||
internal sealed record JavaJniWarning(
|
||||
string SourceClass,
|
||||
string SegmentIdentifier,
|
||||
string WarningCode,
|
||||
string Message,
|
||||
string MethodName,
|
||||
string MethodDescriptor);
|
||||
|
||||
/// <summary>
|
||||
/// Reason codes for JNI edges per task 21-006 specification.
|
||||
/// </summary>
|
||||
internal enum JavaJniReason
|
||||
{
|
||||
/// <summary>Method declared with native keyword.</summary>
|
||||
NativeMethod,
|
||||
|
||||
/// <summary>System.load(String) call loading native library by path.</summary>
|
||||
SystemLoad,
|
||||
|
||||
/// <summary>System.loadLibrary(String) call loading native library by name.</summary>
|
||||
SystemLoadLibrary,
|
||||
|
||||
/// <summary>Runtime.load(String) call.</summary>
|
||||
RuntimeLoad,
|
||||
|
||||
/// <summary>Runtime.loadLibrary(String) call.</summary>
|
||||
RuntimeLoadLibrary,
|
||||
|
||||
/// <summary>GraalVM native-image JNI configuration.</summary>
|
||||
GraalJniConfig,
|
||||
|
||||
/// <summary>Bundled native library file detected.</summary>
|
||||
BundledNativeLib,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confidence levels for JNI edge detection.
|
||||
/// </summary>
|
||||
internal enum JavaJniConfidence
|
||||
{
|
||||
/// <summary>Low confidence (dynamic library name, indirect reference).</summary>
|
||||
Low = 1,
|
||||
|
||||
/// <summary>Medium confidence (config-based, pattern match).</summary>
|
||||
Medium = 2,
|
||||
|
||||
/// <summary>High confidence (direct bytecode evidence, native keyword).</summary>
|
||||
High = 3,
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Jni;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes Java bytecode for JNI/native code usage and emits edges with reason codes.
|
||||
/// Implements task SCANNER-ANALYZERS-JAVA-21-006.
|
||||
/// </summary>
|
||||
internal static class JavaJniAnalyzer
|
||||
{
|
||||
private const ushort AccNative = 0x0100;
|
||||
|
||||
// Method references for System.load/loadLibrary and Runtime.load/loadLibrary
|
||||
private static readonly (string ClassName, string MethodName, string Descriptor, JavaJniReason Reason)[] JniLoadMethods =
|
||||
[
|
||||
("java/lang/System", "load", "(Ljava/lang/String;)V", JavaJniReason.SystemLoad),
|
||||
("java/lang/System", "loadLibrary", "(Ljava/lang/String;)V", JavaJniReason.SystemLoadLibrary),
|
||||
("java/lang/Runtime", "load", "(Ljava/lang/String;)V", JavaJniReason.RuntimeLoad),
|
||||
("java/lang/Runtime", "loadLibrary", "(Ljava/lang/String;)V", JavaJniReason.RuntimeLoadLibrary),
|
||||
];
|
||||
|
||||
public static JavaJniAnalysis Analyze(JavaClassPathAnalysis classPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(classPath);
|
||||
|
||||
if (classPath.Segments.IsDefaultOrEmpty)
|
||||
{
|
||||
return JavaJniAnalysis.Empty;
|
||||
}
|
||||
|
||||
var edges = new List<JavaJniEdge>();
|
||||
var warnings = new List<JavaJniWarning>();
|
||||
|
||||
foreach (var segment in classPath.Segments)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var kvp in segment.ClassLocations)
|
||||
{
|
||||
var className = kvp.Key;
|
||||
var location = kvp.Value;
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = location.OpenClassStream(cancellationToken);
|
||||
var classFile = JniClassFile.Parse(stream, cancellationToken);
|
||||
|
||||
// Detect native method declarations
|
||||
foreach (var method in classFile.Methods)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (method.IsNative)
|
||||
{
|
||||
edges.Add(new JavaJniEdge(
|
||||
SourceClass: className,
|
||||
SegmentIdentifier: segment.Identifier,
|
||||
TargetLibrary: null, // native declaration doesn't specify library
|
||||
Reason: JavaJniReason.NativeMethod,
|
||||
Confidence: JavaJniConfidence.High,
|
||||
MethodName: method.Name,
|
||||
MethodDescriptor: method.Descriptor,
|
||||
InstructionOffset: -1,
|
||||
Details: "native method declaration"));
|
||||
}
|
||||
|
||||
// Analyze bytecode for System.load/loadLibrary calls
|
||||
if (method.Code is not null)
|
||||
{
|
||||
AnalyzeMethodCode(classFile, method, segment.Identifier, className, edges, warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
warnings.Add(new JavaJniWarning(
|
||||
SourceClass: className,
|
||||
SegmentIdentifier: segment.Identifier,
|
||||
WarningCode: "JNI_PARSE_ERROR",
|
||||
Message: $"Failed to parse class file: {ex.Message}",
|
||||
MethodName: string.Empty,
|
||||
MethodDescriptor: string.Empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (edges.Count == 0 && warnings.Count == 0)
|
||||
{
|
||||
return JavaJniAnalysis.Empty;
|
||||
}
|
||||
|
||||
return new JavaJniAnalysis(
|
||||
edges.ToImmutableArray(),
|
||||
warnings.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static void AnalyzeMethodCode(
|
||||
JniClassFile classFile,
|
||||
JniMethod method,
|
||||
string segmentIdentifier,
|
||||
string className,
|
||||
List<JavaJniEdge> edges,
|
||||
List<JavaJniWarning> warnings)
|
||||
{
|
||||
if (method.Code is null || method.Code.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var code = method.Code;
|
||||
var offset = 0;
|
||||
|
||||
while (offset < code.Length)
|
||||
{
|
||||
var opcode = code[offset];
|
||||
|
||||
switch (opcode)
|
||||
{
|
||||
// invokestatic (0xB8) or invokevirtual (0xB6)
|
||||
case 0xB6 or 0xB8:
|
||||
if (offset + 2 < code.Length)
|
||||
{
|
||||
var methodRefIndex = BinaryPrimitives.ReadUInt16BigEndian(code.AsSpan(offset + 1));
|
||||
TryEmitJniLoadEdge(classFile, method, methodRefIndex, offset, segmentIdentifier, className, edges);
|
||||
}
|
||||
offset += 3;
|
||||
break;
|
||||
|
||||
// Skip other instructions based on their sizes
|
||||
case 0x10: // bipush
|
||||
case 0x12: // ldc
|
||||
case 0x15: // iload
|
||||
case 0x16: // lload
|
||||
case 0x17: // fload
|
||||
case 0x18: // dload
|
||||
case 0x19: // aload
|
||||
case 0x36: // istore
|
||||
case 0x37: // lstore
|
||||
case 0x38: // fstore
|
||||
case 0x39: // dstore
|
||||
case 0x3A: // astore
|
||||
case 0xA9: // ret
|
||||
case 0xBC: // newarray
|
||||
offset += 2;
|
||||
break;
|
||||
|
||||
case 0x11: // sipush
|
||||
case 0x13: // ldc_w
|
||||
case 0x14: // ldc2_w
|
||||
case 0xB2: // getstatic
|
||||
case 0xB3: // putstatic
|
||||
case 0xB4: // getfield
|
||||
case 0xB5: // putfield
|
||||
case 0xB7: // invokespecial
|
||||
case 0xBB: // new
|
||||
case 0xBD: // anewarray
|
||||
case 0xC0: // checkcast
|
||||
case 0xC1: // instanceof
|
||||
case 0x99: // ifeq
|
||||
case 0x9A: // ifne
|
||||
case 0x9B: // iflt
|
||||
case 0x9C: // ifge
|
||||
case 0x9D: // ifgt
|
||||
case 0x9E: // ifle
|
||||
case 0x9F: // if_icmpeq
|
||||
case 0xA0: // if_icmpne
|
||||
case 0xA1: // if_icmplt
|
||||
case 0xA2: // if_icmpge
|
||||
case 0xA3: // if_icmpgt
|
||||
case 0xA4: // if_icmple
|
||||
case 0xA5: // if_acmpeq
|
||||
case 0xA6: // if_acmpne
|
||||
case 0xA7: // goto
|
||||
case 0xA8: // jsr
|
||||
case 0xC6: // ifnull
|
||||
case 0xC7: // ifnonnull
|
||||
case 0x84: // iinc
|
||||
offset += 3;
|
||||
break;
|
||||
|
||||
case 0xB9: // invokeinterface (5 bytes total: opcode + 2 index + count + 0)
|
||||
offset += 5;
|
||||
break;
|
||||
|
||||
case 0xBA: // invokedynamic
|
||||
offset += 5;
|
||||
break;
|
||||
|
||||
case 0xC4: // wide
|
||||
if (offset + 1 < code.Length)
|
||||
{
|
||||
var widened = code[offset + 1];
|
||||
offset += widened == 0x84 ? 6 : 4; // iinc vs other wide instructions
|
||||
}
|
||||
else
|
||||
{
|
||||
offset += 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xC5: // multianewarray
|
||||
offset += 4;
|
||||
break;
|
||||
|
||||
case 0xC8: // goto_w
|
||||
case 0xC9: // jsr_w
|
||||
offset += 5;
|
||||
break;
|
||||
|
||||
case 0xAA: // tableswitch
|
||||
offset = SkipTableSwitch(code, offset);
|
||||
break;
|
||||
|
||||
case 0xAB: // lookupswitch
|
||||
offset = SkipLookupSwitch(code, offset);
|
||||
break;
|
||||
|
||||
default:
|
||||
offset += 1; // single-byte instruction
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryEmitJniLoadEdge(
|
||||
JniClassFile classFile,
|
||||
JniMethod method,
|
||||
ushort methodRefIndex,
|
||||
int instructionOffset,
|
||||
string segmentIdentifier,
|
||||
string className,
|
||||
List<JavaJniEdge> edges)
|
||||
{
|
||||
var methodRef = classFile.ConstantPool.ResolveMethodRef(methodRefIndex);
|
||||
if (methodRef is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (targetClass, targetMethod, descriptor, reason) in JniLoadMethods)
|
||||
{
|
||||
if (methodRef.Value.ClassName == targetClass &&
|
||||
methodRef.Value.MethodName == targetMethod &&
|
||||
methodRef.Value.Descriptor == descriptor)
|
||||
{
|
||||
// Try to extract the library name from preceding LDC instruction
|
||||
var libraryName = TryExtractLibraryName(classFile, method.Code!, instructionOffset);
|
||||
|
||||
edges.Add(new JavaJniEdge(
|
||||
SourceClass: className,
|
||||
SegmentIdentifier: segmentIdentifier,
|
||||
TargetLibrary: libraryName,
|
||||
Reason: reason,
|
||||
Confidence: libraryName is not null ? JavaJniConfidence.High : JavaJniConfidence.Medium,
|
||||
MethodName: method.Name,
|
||||
MethodDescriptor: method.Descriptor,
|
||||
InstructionOffset: instructionOffset,
|
||||
Details: libraryName is not null
|
||||
? $"loads native library: {libraryName}"
|
||||
: "loads native library (name resolved dynamically)"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryExtractLibraryName(JniClassFile classFile, byte[] code, int callSiteOffset)
|
||||
{
|
||||
// Look backwards for LDC or LDC_W that loads a string constant
|
||||
// This is a simplified heuristic; library name might be constructed dynamically
|
||||
for (var i = callSiteOffset - 1; i >= 0 && i > callSiteOffset - 20; i--)
|
||||
{
|
||||
var opcode = code[i];
|
||||
if (opcode == 0x12 && i + 1 < callSiteOffset) // ldc
|
||||
{
|
||||
var index = code[i + 1];
|
||||
return classFile.ConstantPool.ResolveString(index);
|
||||
}
|
||||
if (opcode == 0x13 && i + 2 < callSiteOffset) // ldc_w
|
||||
{
|
||||
var index = BinaryPrimitives.ReadUInt16BigEndian(code.AsSpan(i + 1));
|
||||
return classFile.ConstantPool.ResolveString(index);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int SkipTableSwitch(byte[] code, int offset)
|
||||
{
|
||||
// Align to 4-byte boundary
|
||||
var baseOffset = offset;
|
||||
offset = (offset + 4) & ~3;
|
||||
|
||||
if (offset + 12 > code.Length) return code.Length;
|
||||
|
||||
var low = BinaryPrimitives.ReadInt32BigEndian(code.AsSpan(offset + 4));
|
||||
var high = BinaryPrimitives.ReadInt32BigEndian(code.AsSpan(offset + 8));
|
||||
var count = high - low + 1;
|
||||
|
||||
return offset + 12 + (count * 4);
|
||||
}
|
||||
|
||||
private static int SkipLookupSwitch(byte[] code, int offset)
|
||||
{
|
||||
// Align to 4-byte boundary
|
||||
offset = (offset + 4) & ~3;
|
||||
|
||||
if (offset + 8 > code.Length) return code.Length;
|
||||
|
||||
var npairs = BinaryPrimitives.ReadInt32BigEndian(code.AsSpan(offset + 4));
|
||||
|
||||
return offset + 8 + (npairs * 8);
|
||||
}
|
||||
|
||||
#region JNI-specific class file parser
|
||||
|
||||
private sealed class JniClassFile
|
||||
{
|
||||
public JniClassFile(string thisClassName, JniConstantPool constantPool, ImmutableArray<JniMethod> methods)
|
||||
{
|
||||
ThisClassName = thisClassName;
|
||||
ConstantPool = constantPool;
|
||||
Methods = methods;
|
||||
}
|
||||
|
||||
public string ThisClassName { get; }
|
||||
public JniConstantPool ConstantPool { get; }
|
||||
public ImmutableArray<JniMethod> Methods { get; }
|
||||
|
||||
public static JniClassFile Parse(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var reader = new BigEndianReader(stream, leaveOpen: true);
|
||||
if (reader.ReadUInt32() != 0xCAFEBABE)
|
||||
{
|
||||
throw new InvalidDataException("Invalid Java class file magic header.");
|
||||
}
|
||||
|
||||
_ = reader.ReadUInt16(); // minor
|
||||
_ = reader.ReadUInt16(); // major
|
||||
|
||||
var constantPoolCount = reader.ReadUInt16();
|
||||
var pool = new JniConstantPool(constantPoolCount);
|
||||
|
||||
var index = 1;
|
||||
while (index < constantPoolCount)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tag = reader.ReadByte();
|
||||
switch ((JniConstantTag)tag)
|
||||
{
|
||||
case JniConstantTag.Utf8:
|
||||
pool.Set(index, JniConstantPoolEntry.Utf8(reader.ReadUtf8()));
|
||||
index++;
|
||||
break;
|
||||
case JniConstantTag.Integer:
|
||||
case JniConstantTag.Float:
|
||||
reader.Skip(4);
|
||||
pool.Set(index, JniConstantPoolEntry.Other(tag));
|
||||
index++;
|
||||
break;
|
||||
case JniConstantTag.Long:
|
||||
case JniConstantTag.Double:
|
||||
reader.Skip(8);
|
||||
pool.Set(index, JniConstantPoolEntry.Other(tag));
|
||||
index += 2;
|
||||
break;
|
||||
case JniConstantTag.Class:
|
||||
case JniConstantTag.String:
|
||||
case JniConstantTag.MethodType:
|
||||
pool.Set(index, JniConstantPoolEntry.Indexed(tag, reader.ReadUInt16()));
|
||||
index++;
|
||||
break;
|
||||
case JniConstantTag.Fieldref:
|
||||
case JniConstantTag.Methodref:
|
||||
case JniConstantTag.InterfaceMethodref:
|
||||
case JniConstantTag.NameAndType:
|
||||
case JniConstantTag.InvokeDynamic:
|
||||
pool.Set(index, JniConstantPoolEntry.IndexedPair(tag, reader.ReadUInt16(), reader.ReadUInt16()));
|
||||
index++;
|
||||
break;
|
||||
case JniConstantTag.MethodHandle:
|
||||
reader.Skip(1);
|
||||
pool.Set(index, JniConstantPoolEntry.Indexed(tag, reader.ReadUInt16()));
|
||||
index++;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException($"Unsupported constant pool tag {tag}.");
|
||||
}
|
||||
}
|
||||
|
||||
_ = reader.ReadUInt16(); // access flags
|
||||
var thisClassIndex = reader.ReadUInt16();
|
||||
_ = reader.ReadUInt16(); // super
|
||||
|
||||
var interfacesCount = reader.ReadUInt16();
|
||||
reader.Skip(interfacesCount * 2);
|
||||
|
||||
var fieldsCount = reader.ReadUInt16();
|
||||
for (var i = 0; i < fieldsCount; i++)
|
||||
{
|
||||
SkipMember(reader);
|
||||
}
|
||||
|
||||
var methodsCount = reader.ReadUInt16();
|
||||
var methods = ImmutableArray.CreateBuilder<JniMethod>(methodsCount);
|
||||
for (var i = 0; i < methodsCount; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var accessFlags = reader.ReadUInt16();
|
||||
var nameIndex = reader.ReadUInt16();
|
||||
var descriptorIndex = reader.ReadUInt16();
|
||||
var attributesCount = reader.ReadUInt16();
|
||||
|
||||
byte[]? code = null;
|
||||
|
||||
for (var attr = 0; attr < attributesCount; attr++)
|
||||
{
|
||||
var attributeNameIndex = reader.ReadUInt16();
|
||||
var attributeLength = reader.ReadUInt32();
|
||||
var attributeName = pool.GetUtf8(attributeNameIndex) ?? string.Empty;
|
||||
|
||||
if (attributeName == "Code")
|
||||
{
|
||||
_ = reader.ReadUInt16(); // max_stack
|
||||
_ = reader.ReadUInt16(); // max_locals
|
||||
var codeLength = reader.ReadUInt32();
|
||||
code = reader.ReadBytes((int)codeLength);
|
||||
var exceptionTableLength = reader.ReadUInt16();
|
||||
reader.Skip(exceptionTableLength * 8);
|
||||
var codeAttributeCount = reader.ReadUInt16();
|
||||
for (var c = 0; c < codeAttributeCount; c++)
|
||||
{
|
||||
reader.Skip(2);
|
||||
var len = reader.ReadUInt32();
|
||||
reader.Skip((int)len);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Skip((int)attributeLength);
|
||||
}
|
||||
}
|
||||
|
||||
var name = pool.GetUtf8(nameIndex) ?? string.Empty;
|
||||
var descriptor = pool.GetUtf8(descriptorIndex) ?? string.Empty;
|
||||
var isNative = (accessFlags & AccNative) != 0;
|
||||
methods.Add(new JniMethod(name, descriptor, code, isNative));
|
||||
}
|
||||
|
||||
var thisClassName = pool.ResolveClassName(thisClassIndex) ?? string.Empty;
|
||||
|
||||
return new JniClassFile(thisClassName, pool, methods.ToImmutable());
|
||||
}
|
||||
|
||||
private static void SkipMember(BigEndianReader reader)
|
||||
{
|
||||
reader.Skip(2 + 2 + 2); // access_flags, name_index, descriptor_index
|
||||
var attributeCount = reader.ReadUInt16();
|
||||
for (var i = 0; i < attributeCount; i++)
|
||||
{
|
||||
reader.Skip(2);
|
||||
var len = reader.ReadUInt32();
|
||||
reader.Skip((int)len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record JniMethod(string Name, string Descriptor, byte[]? Code, bool IsNative);
|
||||
|
||||
private sealed class JniConstantPool
|
||||
{
|
||||
private readonly JniConstantPoolEntry[] _entries;
|
||||
|
||||
public JniConstantPool(int count)
|
||||
{
|
||||
_entries = new JniConstantPoolEntry[count];
|
||||
}
|
||||
|
||||
public void Set(int index, JniConstantPoolEntry entry)
|
||||
{
|
||||
if (index > 0 && index < _entries.Length)
|
||||
{
|
||||
_entries[index] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
public string? GetUtf8(int index)
|
||||
{
|
||||
if (index <= 0 || index >= _entries.Length) return null;
|
||||
var entry = _entries[index];
|
||||
return entry.Tag == (byte)JniConstantTag.Utf8 ? entry.Utf8Value : null;
|
||||
}
|
||||
|
||||
public string? ResolveClassName(int classIndex)
|
||||
{
|
||||
if (classIndex <= 0 || classIndex >= _entries.Length) return null;
|
||||
var entry = _entries[classIndex];
|
||||
if (entry.Tag != (byte)JniConstantTag.Class) return null;
|
||||
return GetUtf8(entry.Index1);
|
||||
}
|
||||
|
||||
public string? ResolveString(int index)
|
||||
{
|
||||
if (index <= 0 || index >= _entries.Length) return null;
|
||||
var entry = _entries[index];
|
||||
if (entry.Tag == (byte)JniConstantTag.String)
|
||||
{
|
||||
return GetUtf8(entry.Index1);
|
||||
}
|
||||
if (entry.Tag == (byte)JniConstantTag.Utf8)
|
||||
{
|
||||
return entry.Utf8Value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public (string ClassName, string MethodName, string Descriptor)? ResolveMethodRef(int index)
|
||||
{
|
||||
if (index <= 0 || index >= _entries.Length) return null;
|
||||
var entry = _entries[index];
|
||||
if (entry.Tag != (byte)JniConstantTag.Methodref) return null;
|
||||
|
||||
var className = ResolveClassName(entry.Index1);
|
||||
if (className is null) return null;
|
||||
|
||||
var nameAndTypeIndex = entry.Index2;
|
||||
if (nameAndTypeIndex <= 0 || nameAndTypeIndex >= _entries.Length) return null;
|
||||
var nameAndType = _entries[nameAndTypeIndex];
|
||||
if (nameAndType.Tag != (byte)JniConstantTag.NameAndType) return null;
|
||||
|
||||
var methodName = GetUtf8(nameAndType.Index1);
|
||||
var descriptor = GetUtf8(nameAndType.Index2);
|
||||
|
||||
if (methodName is null || descriptor is null) return null;
|
||||
|
||||
return (className, methodName, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct JniConstantPoolEntry
|
||||
{
|
||||
public byte Tag { get; init; }
|
||||
public string? Utf8Value { get; init; }
|
||||
public ushort Index1 { get; init; }
|
||||
public ushort Index2 { get; init; }
|
||||
|
||||
public static JniConstantPoolEntry Utf8(string value) => new() { Tag = (byte)JniConstantTag.Utf8, Utf8Value = value };
|
||||
public static JniConstantPoolEntry Indexed(byte tag, ushort index) => new() { Tag = tag, Index1 = index };
|
||||
public static JniConstantPoolEntry IndexedPair(byte tag, ushort index1, ushort index2) => new() { Tag = tag, Index1 = index1, Index2 = index2 };
|
||||
public static JniConstantPoolEntry Other(byte tag) => new() { Tag = tag };
|
||||
}
|
||||
|
||||
private enum JniConstantTag : byte
|
||||
{
|
||||
Utf8 = 1,
|
||||
Integer = 3,
|
||||
Float = 4,
|
||||
Long = 5,
|
||||
Double = 6,
|
||||
Class = 7,
|
||||
String = 8,
|
||||
Fieldref = 9,
|
||||
Methodref = 10,
|
||||
InterfaceMethodref = 11,
|
||||
NameAndType = 12,
|
||||
MethodHandle = 15,
|
||||
MethodType = 16,
|
||||
InvokeDynamic = 18,
|
||||
}
|
||||
|
||||
private sealed class BigEndianReader
|
||||
{
|
||||
private readonly BinaryReader _reader;
|
||||
|
||||
public BigEndianReader(Stream stream, bool leaveOpen)
|
||||
{
|
||||
_reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen);
|
||||
}
|
||||
|
||||
public byte ReadByte() => _reader.ReadByte();
|
||||
|
||||
public ushort ReadUInt16()
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[2];
|
||||
_reader.Read(buffer);
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(buffer);
|
||||
}
|
||||
|
||||
public uint ReadUInt32()
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
_reader.Read(buffer);
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(buffer);
|
||||
}
|
||||
|
||||
public byte[] ReadBytes(int count) => _reader.ReadBytes(count);
|
||||
|
||||
public string ReadUtf8()
|
||||
{
|
||||
var length = ReadUInt16();
|
||||
var bytes = _reader.ReadBytes(length);
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
public void Skip(int count)
|
||||
{
|
||||
if (_reader.BaseStream.CanSeek)
|
||||
{
|
||||
_reader.BaseStream.Seek(count, SeekOrigin.Current);
|
||||
}
|
||||
else
|
||||
{
|
||||
_reader.ReadBytes(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Resolver;
|
||||
|
||||
/// <summary>
|
||||
/// Writes Java entrypoint resolution results in Append-Only Contract (AOC) format.
|
||||
/// Produces deterministic, immutable NDJSON output suitable for linkset correlation.
|
||||
/// </summary>
|
||||
internal static class JavaEntrypointAocWriter
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower) },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Writes resolution results to NDJSON format for AOC storage.
|
||||
/// </summary>
|
||||
public static async Task WriteNdjsonAsync(
|
||||
JavaEntrypointResolution resolution,
|
||||
string tenantId,
|
||||
string scanId,
|
||||
Stream outputStream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(resolution);
|
||||
ArgumentNullException.ThrowIfNull(outputStream);
|
||||
|
||||
using var writer = new StreamWriter(outputStream, Encoding.UTF8, leaveOpen: true);
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
|
||||
// Write header record
|
||||
var header = new AocHeader
|
||||
{
|
||||
RecordType = "header",
|
||||
SchemaVersion = "1.0.0",
|
||||
TenantId = tenantId,
|
||||
ScanId = scanId,
|
||||
GeneratedAt = timestamp,
|
||||
ToolVersion = GetToolVersion(),
|
||||
Statistics = MapStatistics(resolution.Statistics),
|
||||
};
|
||||
await WriteRecordAsync(writer, header, cancellationToken);
|
||||
|
||||
// Write component records (sorted for determinism)
|
||||
foreach (var component in resolution.Components.OrderBy(c => c.ComponentId, StringComparer.Ordinal))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var record = MapComponent(component, tenantId, scanId, timestamp);
|
||||
await WriteRecordAsync(writer, record, cancellationToken);
|
||||
}
|
||||
|
||||
// Write entrypoint records (sorted for determinism)
|
||||
foreach (var entrypoint in resolution.Entrypoints.OrderBy(e => e.EntrypointId, StringComparer.Ordinal))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var record = MapEntrypoint(entrypoint, tenantId, scanId, timestamp);
|
||||
await WriteRecordAsync(writer, record, cancellationToken);
|
||||
}
|
||||
|
||||
// Write edge records (sorted for determinism)
|
||||
foreach (var edge in resolution.Edges.OrderBy(e => e.EdgeId, StringComparer.Ordinal))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var record = MapEdge(edge, tenantId, scanId, timestamp);
|
||||
await WriteRecordAsync(writer, record, cancellationToken);
|
||||
}
|
||||
|
||||
// Write warning records
|
||||
foreach (var warning in resolution.Warnings)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var record = MapWarning(warning, tenantId, scanId, timestamp);
|
||||
await WriteRecordAsync(writer, record, cancellationToken);
|
||||
}
|
||||
|
||||
// Write footer with content hash
|
||||
var contentHash = ComputeContentHash(resolution);
|
||||
var footer = new AocFooter
|
||||
{
|
||||
RecordType = "footer",
|
||||
TenantId = tenantId,
|
||||
ScanId = scanId,
|
||||
ContentHash = contentHash,
|
||||
TotalRecords = resolution.Components.Length + resolution.Entrypoints.Length + resolution.Edges.Length,
|
||||
GeneratedAt = timestamp,
|
||||
};
|
||||
await WriteRecordAsync(writer, footer, cancellationToken);
|
||||
|
||||
await writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic content hash for the resolution.
|
||||
/// </summary>
|
||||
public static string ComputeContentHash(JavaEntrypointResolution resolution)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
using var stream = new MemoryStream();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
// Hash components in sorted order
|
||||
foreach (var c in resolution.Components.OrderBy(x => x.ComponentId, StringComparer.Ordinal))
|
||||
{
|
||||
writer.Write(c.ComponentId);
|
||||
writer.Write(c.SegmentIdentifier);
|
||||
writer.Write(c.Name);
|
||||
}
|
||||
|
||||
// Hash entrypoints in sorted order
|
||||
foreach (var e in resolution.Entrypoints.OrderBy(x => x.EntrypointId, StringComparer.Ordinal))
|
||||
{
|
||||
writer.Write(e.EntrypointId);
|
||||
writer.Write(e.ClassFqcn);
|
||||
writer.Write(e.MethodName ?? string.Empty);
|
||||
writer.Write(e.Confidence.ToString("F4"));
|
||||
}
|
||||
|
||||
// Hash edges in sorted order
|
||||
foreach (var e in resolution.Edges.OrderBy(x => x.EdgeId, StringComparer.Ordinal))
|
||||
{
|
||||
writer.Write(e.EdgeId);
|
||||
writer.Write(e.SourceId);
|
||||
writer.Write(e.TargetId);
|
||||
writer.Write(e.Confidence.ToString("F4"));
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
stream.Position = 0;
|
||||
|
||||
var hash = sha256.ComputeHash(stream);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static async Task WriteRecordAsync<T>(StreamWriter writer, T record, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(record, JsonOptions);
|
||||
await writer.WriteLineAsync(json.AsMemory(), cancellationToken);
|
||||
}
|
||||
|
||||
private static string GetToolVersion()
|
||||
{
|
||||
var assembly = typeof(JavaEntrypointAocWriter).Assembly;
|
||||
var version = assembly.GetName().Version;
|
||||
return version?.ToString() ?? "0.0.0";
|
||||
}
|
||||
|
||||
private static AocStatistics MapStatistics(JavaResolutionStatistics stats)
|
||||
{
|
||||
return new AocStatistics
|
||||
{
|
||||
TotalEntrypoints = stats.TotalEntrypoints,
|
||||
TotalComponents = stats.TotalComponents,
|
||||
TotalEdges = stats.TotalEdges,
|
||||
HighConfidenceCount = stats.HighConfidenceCount,
|
||||
MediumConfidenceCount = stats.MediumConfidenceCount,
|
||||
LowConfidenceCount = stats.LowConfidenceCount,
|
||||
SignedComponents = stats.SignedComponents,
|
||||
ModularComponents = stats.ModularComponents,
|
||||
ResolutionDurationMs = (long)stats.ResolutionDuration.TotalMilliseconds,
|
||||
};
|
||||
}
|
||||
|
||||
private static AocComponentRecord MapComponent(
|
||||
JavaResolvedComponent component,
|
||||
string tenantId,
|
||||
string scanId,
|
||||
DateTimeOffset timestamp)
|
||||
{
|
||||
return new AocComponentRecord
|
||||
{
|
||||
RecordType = "component",
|
||||
TenantId = tenantId,
|
||||
ScanId = scanId,
|
||||
ComponentId = component.ComponentId,
|
||||
SegmentIdentifier = component.SegmentIdentifier,
|
||||
ComponentType = component.ComponentType.ToString().ToLowerInvariant(),
|
||||
Name = component.Name,
|
||||
Version = component.Version,
|
||||
IsSigned = component.IsSigned,
|
||||
SignerFingerprint = component.SignerFingerprint,
|
||||
MainClass = component.MainClass,
|
||||
ModuleInfo = component.ModuleInfo is not null ? MapModuleInfo(component.ModuleInfo) : null,
|
||||
GeneratedAt = timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
private static AocModuleInfo MapModuleInfo(JavaModuleInfo module)
|
||||
{
|
||||
return new AocModuleInfo
|
||||
{
|
||||
ModuleName = module.ModuleName,
|
||||
IsOpen = module.IsOpen,
|
||||
Requires = module.Requires.IsDefaultOrEmpty ? null : module.Requires.ToArray(),
|
||||
Exports = module.Exports.IsDefaultOrEmpty ? null : module.Exports.ToArray(),
|
||||
Opens = module.Opens.IsDefaultOrEmpty ? null : module.Opens.ToArray(),
|
||||
Uses = module.Uses.IsDefaultOrEmpty ? null : module.Uses.ToArray(),
|
||||
Provides = module.Provides.IsDefaultOrEmpty ? null : module.Provides.ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
private static AocEntrypointRecord MapEntrypoint(
|
||||
JavaResolvedEntrypoint entrypoint,
|
||||
string tenantId,
|
||||
string scanId,
|
||||
DateTimeOffset timestamp)
|
||||
{
|
||||
return new AocEntrypointRecord
|
||||
{
|
||||
RecordType = "entrypoint",
|
||||
TenantId = tenantId,
|
||||
ScanId = scanId,
|
||||
EntrypointId = entrypoint.EntrypointId,
|
||||
ClassFqcn = entrypoint.ClassFqcn,
|
||||
MethodName = entrypoint.MethodName,
|
||||
MethodDescriptor = entrypoint.MethodDescriptor,
|
||||
EntrypointType = entrypoint.EntrypointType.ToString(),
|
||||
SegmentIdentifier = entrypoint.SegmentIdentifier,
|
||||
Framework = entrypoint.Framework,
|
||||
Confidence = entrypoint.Confidence,
|
||||
ResolutionPath = entrypoint.ResolutionPath.IsDefaultOrEmpty ? null : entrypoint.ResolutionPath.ToArray(),
|
||||
Metadata = entrypoint.Metadata?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
GeneratedAt = timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
private static AocEdgeRecord MapEdge(
|
||||
JavaResolvedEdge edge,
|
||||
string tenantId,
|
||||
string scanId,
|
||||
DateTimeOffset timestamp)
|
||||
{
|
||||
return new AocEdgeRecord
|
||||
{
|
||||
RecordType = "edge",
|
||||
TenantId = tenantId,
|
||||
ScanId = scanId,
|
||||
EdgeId = edge.EdgeId,
|
||||
SourceId = edge.SourceId,
|
||||
TargetId = edge.TargetId,
|
||||
EdgeType = edge.EdgeType.ToString(),
|
||||
Reason = edge.Reason.ToString(),
|
||||
Confidence = edge.Confidence,
|
||||
SegmentIdentifier = edge.SegmentIdentifier,
|
||||
Details = edge.Details,
|
||||
GeneratedAt = timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
private static AocWarningRecord MapWarning(
|
||||
JavaResolutionWarning warning,
|
||||
string tenantId,
|
||||
string scanId,
|
||||
DateTimeOffset timestamp)
|
||||
{
|
||||
return new AocWarningRecord
|
||||
{
|
||||
RecordType = "warning",
|
||||
TenantId = tenantId,
|
||||
ScanId = scanId,
|
||||
WarningCode = warning.WarningCode,
|
||||
Message = warning.Message,
|
||||
SegmentIdentifier = warning.SegmentIdentifier,
|
||||
Details = warning.Details,
|
||||
GeneratedAt = timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
#region AOC Record Types
|
||||
|
||||
private sealed class AocHeader
|
||||
{
|
||||
public string RecordType { get; init; } = "header";
|
||||
public string SchemaVersion { get; init; } = "1.0.0";
|
||||
public string TenantId { get; init; } = string.Empty;
|
||||
public string ScanId { get; init; } = string.Empty;
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
public string ToolVersion { get; init; } = string.Empty;
|
||||
public AocStatistics? Statistics { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AocStatistics
|
||||
{
|
||||
public int TotalEntrypoints { get; init; }
|
||||
public int TotalComponents { get; init; }
|
||||
public int TotalEdges { get; init; }
|
||||
public int HighConfidenceCount { get; init; }
|
||||
public int MediumConfidenceCount { get; init; }
|
||||
public int LowConfidenceCount { get; init; }
|
||||
public int SignedComponents { get; init; }
|
||||
public int ModularComponents { get; init; }
|
||||
public long ResolutionDurationMs { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AocComponentRecord
|
||||
{
|
||||
public string RecordType { get; init; } = "component";
|
||||
public string TenantId { get; init; } = string.Empty;
|
||||
public string ScanId { get; init; } = string.Empty;
|
||||
public string ComponentId { get; init; } = string.Empty;
|
||||
public string SegmentIdentifier { get; init; } = string.Empty;
|
||||
public string ComponentType { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string? Version { get; init; }
|
||||
public bool IsSigned { get; init; }
|
||||
public string? SignerFingerprint { get; init; }
|
||||
public string? MainClass { get; init; }
|
||||
public AocModuleInfo? ModuleInfo { get; init; }
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AocModuleInfo
|
||||
{
|
||||
public string ModuleName { get; init; } = string.Empty;
|
||||
public bool IsOpen { get; init; }
|
||||
public string[]? Requires { get; init; }
|
||||
public string[]? Exports { get; init; }
|
||||
public string[]? Opens { get; init; }
|
||||
public string[]? Uses { get; init; }
|
||||
public string[]? Provides { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AocEntrypointRecord
|
||||
{
|
||||
public string RecordType { get; init; } = "entrypoint";
|
||||
public string TenantId { get; init; } = string.Empty;
|
||||
public string ScanId { get; init; } = string.Empty;
|
||||
public string EntrypointId { get; init; } = string.Empty;
|
||||
public string ClassFqcn { get; init; } = string.Empty;
|
||||
public string? MethodName { get; init; }
|
||||
public string? MethodDescriptor { get; init; }
|
||||
public string EntrypointType { get; init; } = string.Empty;
|
||||
public string SegmentIdentifier { get; init; } = string.Empty;
|
||||
public string? Framework { get; init; }
|
||||
public double Confidence { get; init; }
|
||||
public string[]? ResolutionPath { get; init; }
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AocEdgeRecord
|
||||
{
|
||||
public string RecordType { get; init; } = "edge";
|
||||
public string TenantId { get; init; } = string.Empty;
|
||||
public string ScanId { get; init; } = string.Empty;
|
||||
public string EdgeId { get; init; } = string.Empty;
|
||||
public string SourceId { get; init; } = string.Empty;
|
||||
public string TargetId { get; init; } = string.Empty;
|
||||
public string EdgeType { get; init; } = string.Empty;
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
public double Confidence { get; init; }
|
||||
public string SegmentIdentifier { get; init; } = string.Empty;
|
||||
public string? Details { get; init; }
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AocWarningRecord
|
||||
{
|
||||
public string RecordType { get; init; } = "warning";
|
||||
public string TenantId { get; init; } = string.Empty;
|
||||
public string ScanId { get; init; } = string.Empty;
|
||||
public string WarningCode { get; init; } = string.Empty;
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public string? SegmentIdentifier { get; init; }
|
||||
public string? Details { get; init; }
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AocFooter
|
||||
{
|
||||
public string RecordType { get; init; } = "footer";
|
||||
public string TenantId { get; init; } = string.Empty;
|
||||
public string ScanId { get; init; } = string.Empty;
|
||||
public string ContentHash { get; init; } = string.Empty;
|
||||
public int TotalRecords { get; init; }
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Resolver;
|
||||
|
||||
/// <summary>
|
||||
/// Result of Java entrypoint resolution per task 21-008.
|
||||
/// Combines outputs from 21-005 (framework configs), 21-006 (JNI), 21-007 (signature/manifest)
|
||||
/// into unified entrypoints, components, and edges.
|
||||
/// </summary>
|
||||
internal sealed record JavaEntrypointResolution(
|
||||
ImmutableArray<JavaResolvedEntrypoint> Entrypoints,
|
||||
ImmutableArray<JavaResolvedComponent> Components,
|
||||
ImmutableArray<JavaResolvedEdge> Edges,
|
||||
JavaResolutionStatistics Statistics,
|
||||
ImmutableArray<JavaResolutionWarning> Warnings)
|
||||
{
|
||||
public static readonly JavaEntrypointResolution Empty = new(
|
||||
ImmutableArray<JavaResolvedEntrypoint>.Empty,
|
||||
ImmutableArray<JavaResolvedComponent>.Empty,
|
||||
ImmutableArray<JavaResolvedEdge>.Empty,
|
||||
JavaResolutionStatistics.Empty,
|
||||
ImmutableArray<JavaResolutionWarning>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A resolved Java entrypoint (Main-Class, servlet, agent, REST endpoint, etc.).
|
||||
/// </summary>
|
||||
/// <param name="EntrypointId">Deterministic identifier (sha256 of class+method+descriptor).</param>
|
||||
/// <param name="ClassFqcn">Fully qualified class name.</param>
|
||||
/// <param name="MethodName">Method name (null for class-level entrypoints like Main-Class).</param>
|
||||
/// <param name="MethodDescriptor">JVM method descriptor.</param>
|
||||
/// <param name="EntrypointType">Type of entrypoint.</param>
|
||||
/// <param name="SegmentIdentifier">JAR/module segment containing this entrypoint.</param>
|
||||
/// <param name="Framework">Detected framework (Spring, Jakarta, etc.).</param>
|
||||
/// <param name="Confidence">Resolution confidence (0-1).</param>
|
||||
/// <param name="ResolutionPath">Chain of rules/analyzers that identified this entrypoint.</param>
|
||||
/// <param name="Metadata">Additional type-specific metadata.</param>
|
||||
internal sealed record JavaResolvedEntrypoint(
|
||||
string EntrypointId,
|
||||
string ClassFqcn,
|
||||
string? MethodName,
|
||||
string? MethodDescriptor,
|
||||
JavaEntrypointType EntrypointType,
|
||||
string SegmentIdentifier,
|
||||
string? Framework,
|
||||
double Confidence,
|
||||
ImmutableArray<string> ResolutionPath,
|
||||
ImmutableDictionary<string, string>? Metadata);
|
||||
|
||||
/// <summary>
|
||||
/// A resolved Java component (JAR, module, or bundle).
|
||||
/// </summary>
|
||||
/// <param name="ComponentId">Deterministic identifier.</param>
|
||||
/// <param name="SegmentIdentifier">Path/identifier of this component.</param>
|
||||
/// <param name="ComponentType">Type of component.</param>
|
||||
/// <param name="Name">Component name (module name, bundle symbolic name, or JAR name).</param>
|
||||
/// <param name="Version">Component version if available.</param>
|
||||
/// <param name="IsSigned">Whether the component is signed.</param>
|
||||
/// <param name="SignerFingerprint">Signer certificate fingerprint if signed.</param>
|
||||
/// <param name="MainClass">Main-Class if applicable.</param>
|
||||
/// <param name="ModuleInfo">JPMS module descriptor info if available.</param>
|
||||
internal sealed record JavaResolvedComponent(
|
||||
string ComponentId,
|
||||
string SegmentIdentifier,
|
||||
JavaComponentType ComponentType,
|
||||
string Name,
|
||||
string? Version,
|
||||
bool IsSigned,
|
||||
string? SignerFingerprint,
|
||||
string? MainClass,
|
||||
JavaModuleInfo? ModuleInfo);
|
||||
|
||||
/// <summary>
|
||||
/// JPMS module descriptor information.
|
||||
/// </summary>
|
||||
internal sealed record JavaModuleInfo(
|
||||
string ModuleName,
|
||||
bool IsOpen,
|
||||
ImmutableArray<string> Requires,
|
||||
ImmutableArray<string> Exports,
|
||||
ImmutableArray<string> Opens,
|
||||
ImmutableArray<string> Uses,
|
||||
ImmutableArray<string> Provides);
|
||||
|
||||
/// <summary>
|
||||
/// A resolved edge between components or classes.
|
||||
/// </summary>
|
||||
/// <param name="EdgeId">Deterministic edge identifier.</param>
|
||||
/// <param name="SourceId">Source component/class identifier.</param>
|
||||
/// <param name="TargetId">Target component/class identifier.</param>
|
||||
/// <param name="EdgeType">Type of edge (dependency relationship).</param>
|
||||
/// <param name="Reason">Reason code for this edge.</param>
|
||||
/// <param name="Confidence">Edge confidence (0-1).</param>
|
||||
/// <param name="SegmentIdentifier">Segment where this edge was detected.</param>
|
||||
/// <param name="Details">Additional details about the edge.</param>
|
||||
internal sealed record JavaResolvedEdge(
|
||||
string EdgeId,
|
||||
string SourceId,
|
||||
string TargetId,
|
||||
JavaEdgeType EdgeType,
|
||||
JavaEdgeReason Reason,
|
||||
double Confidence,
|
||||
string SegmentIdentifier,
|
||||
string? Details);
|
||||
|
||||
/// <summary>
|
||||
/// Resolution statistics for telemetry and validation.
|
||||
/// </summary>
|
||||
internal sealed record JavaResolutionStatistics(
|
||||
int TotalEntrypoints,
|
||||
int TotalComponents,
|
||||
int TotalEdges,
|
||||
ImmutableDictionary<JavaEntrypointType, int> EntrypointsByType,
|
||||
ImmutableDictionary<JavaEdgeType, int> EdgesByType,
|
||||
ImmutableDictionary<string, int> EntrypointsByFramework,
|
||||
int HighConfidenceCount,
|
||||
int MediumConfidenceCount,
|
||||
int LowConfidenceCount,
|
||||
int SignedComponents,
|
||||
int ModularComponents,
|
||||
TimeSpan ResolutionDuration)
|
||||
{
|
||||
public static readonly JavaResolutionStatistics Empty = new(
|
||||
TotalEntrypoints: 0,
|
||||
TotalComponents: 0,
|
||||
TotalEdges: 0,
|
||||
EntrypointsByType: ImmutableDictionary<JavaEntrypointType, int>.Empty,
|
||||
EdgesByType: ImmutableDictionary<JavaEdgeType, int>.Empty,
|
||||
EntrypointsByFramework: ImmutableDictionary<string, int>.Empty,
|
||||
HighConfidenceCount: 0,
|
||||
MediumConfidenceCount: 0,
|
||||
LowConfidenceCount: 0,
|
||||
SignedComponents: 0,
|
||||
ModularComponents: 0,
|
||||
ResolutionDuration: TimeSpan.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning emitted during resolution.
|
||||
/// </summary>
|
||||
internal sealed record JavaResolutionWarning(
|
||||
string WarningCode,
|
||||
string Message,
|
||||
string? SegmentIdentifier,
|
||||
string? Details);
|
||||
|
||||
/// <summary>
|
||||
/// Types of Java entrypoints.
|
||||
/// </summary>
|
||||
internal enum JavaEntrypointType
|
||||
{
|
||||
/// <summary>Main-Class manifest attribute entry.</summary>
|
||||
MainClass,
|
||||
|
||||
/// <summary>Start-Class for Spring Boot fat JARs.</summary>
|
||||
SpringBootStartClass,
|
||||
|
||||
/// <summary>Premain-Class for Java agents.</summary>
|
||||
JavaAgentPremain,
|
||||
|
||||
/// <summary>Agent-Class for Java agents (attach API).</summary>
|
||||
JavaAgentAttach,
|
||||
|
||||
/// <summary>Launcher-Agent-Class for native launcher agents.</summary>
|
||||
LauncherAgent,
|
||||
|
||||
/// <summary>Servlet or filter.</summary>
|
||||
Servlet,
|
||||
|
||||
/// <summary>JAX-RS resource method.</summary>
|
||||
JaxRsEndpoint,
|
||||
|
||||
/// <summary>Spring MVC/WebFlux controller method.</summary>
|
||||
SpringEndpoint,
|
||||
|
||||
/// <summary>EJB session bean method.</summary>
|
||||
EjbMethod,
|
||||
|
||||
/// <summary>Message-driven bean.</summary>
|
||||
MessageDriven,
|
||||
|
||||
/// <summary>Scheduled task.</summary>
|
||||
ScheduledTask,
|
||||
|
||||
/// <summary>CDI observer method.</summary>
|
||||
CdiObserver,
|
||||
|
||||
/// <summary>JUnit/TestNG test method.</summary>
|
||||
TestMethod,
|
||||
|
||||
/// <summary>CLI command handler.</summary>
|
||||
CliCommand,
|
||||
|
||||
/// <summary>gRPC service method.</summary>
|
||||
GrpcMethod,
|
||||
|
||||
/// <summary>GraphQL resolver.</summary>
|
||||
GraphQlResolver,
|
||||
|
||||
/// <summary>WebSocket endpoint.</summary>
|
||||
WebSocketEndpoint,
|
||||
|
||||
/// <summary>Native method (JNI).</summary>
|
||||
NativeMethod,
|
||||
|
||||
/// <summary>ServiceLoader provider.</summary>
|
||||
ServiceProvider,
|
||||
|
||||
/// <summary>Module main class.</summary>
|
||||
ModuleMain,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types of Java components.
|
||||
/// </summary>
|
||||
internal enum JavaComponentType
|
||||
{
|
||||
/// <summary>Standard JAR file.</summary>
|
||||
Jar,
|
||||
|
||||
/// <summary>WAR web application.</summary>
|
||||
War,
|
||||
|
||||
/// <summary>EAR enterprise application.</summary>
|
||||
Ear,
|
||||
|
||||
/// <summary>JPMS module (jmod or modular JAR).</summary>
|
||||
JpmsModule,
|
||||
|
||||
/// <summary>OSGi bundle.</summary>
|
||||
OsgiBundle,
|
||||
|
||||
/// <summary>Spring Boot fat JAR.</summary>
|
||||
SpringBootFatJar,
|
||||
|
||||
/// <summary>jlink runtime image.</summary>
|
||||
JlinkImage,
|
||||
|
||||
/// <summary>Native image (GraalVM).</summary>
|
||||
NativeImage,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types of edges between components/classes.
|
||||
/// </summary>
|
||||
internal enum JavaEdgeType
|
||||
{
|
||||
/// <summary>JPMS module requires directive.</summary>
|
||||
JpmsRequires,
|
||||
|
||||
/// <summary>JPMS module exports directive.</summary>
|
||||
JpmsExports,
|
||||
|
||||
/// <summary>JPMS module opens directive.</summary>
|
||||
JpmsOpens,
|
||||
|
||||
/// <summary>JPMS module uses directive.</summary>
|
||||
JpmsUses,
|
||||
|
||||
/// <summary>JPMS module provides directive.</summary>
|
||||
JpmsProvides,
|
||||
|
||||
/// <summary>Classpath dependency (compile/runtime).</summary>
|
||||
ClasspathDependency,
|
||||
|
||||
/// <summary>ServiceLoader provider registration.</summary>
|
||||
ServiceProvider,
|
||||
|
||||
/// <summary>Reflection-based class loading.</summary>
|
||||
ReflectionLoad,
|
||||
|
||||
/// <summary>JNI native library dependency.</summary>
|
||||
JniNativeLib,
|
||||
|
||||
/// <summary>Class inheritance/implementation.</summary>
|
||||
Inheritance,
|
||||
|
||||
/// <summary>Annotation processing.</summary>
|
||||
AnnotationProcessing,
|
||||
|
||||
/// <summary>Resource bundle dependency.</summary>
|
||||
ResourceBundle,
|
||||
|
||||
/// <summary>OSGi Import-Package.</summary>
|
||||
OsgiImport,
|
||||
|
||||
/// <summary>OSGi Require-Bundle.</summary>
|
||||
OsgiRequire,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reason codes for edges (more specific than edge type).
|
||||
/// </summary>
|
||||
internal enum JavaEdgeReason
|
||||
{
|
||||
// JPMS reasons
|
||||
JpmsRequiresTransitive,
|
||||
JpmsRequiresStatic,
|
||||
JpmsRequiresMandated,
|
||||
JpmsExportsQualified,
|
||||
JpmsOpensQualified,
|
||||
JpmsUsesService,
|
||||
JpmsProvidesService,
|
||||
|
||||
// Classpath reasons
|
||||
MavenCompileDependency,
|
||||
MavenRuntimeDependency,
|
||||
MavenTestDependency,
|
||||
MavenProvidedDependency,
|
||||
GradleImplementation,
|
||||
GradleApi,
|
||||
GradleCompileOnly,
|
||||
GradleRuntimeOnly,
|
||||
ManifestClassPath,
|
||||
|
||||
// SPI reasons
|
||||
MetaInfServices,
|
||||
ModuleInfoProvides,
|
||||
SpringFactories,
|
||||
|
||||
// Reflection reasons
|
||||
ClassForName,
|
||||
ClassLoaderLoadClass,
|
||||
MethodInvoke,
|
||||
ConstructorNewInstance,
|
||||
ProxyCreation,
|
||||
GraalReflectConfig,
|
||||
|
||||
// JNI reasons
|
||||
SystemLoadLibrary,
|
||||
SystemLoad,
|
||||
RuntimeLoadLibrary,
|
||||
NativeMethodDeclaration,
|
||||
GraalJniConfig,
|
||||
BundledNativeLib,
|
||||
|
||||
// Other
|
||||
Extends,
|
||||
Implements,
|
||||
Annotated,
|
||||
ResourceReference,
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Jni;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Reflection;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Signature;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Resolver;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves Java entrypoints by combining analysis results from:
|
||||
/// - 21-005: Framework configs (Spring, Jakarta, etc.)
|
||||
/// - 21-006: JNI/native hints
|
||||
/// - 21-007: Signature/manifest metadata
|
||||
/// - Reflection analysis
|
||||
/// - SPI catalog
|
||||
/// - JPMS module info
|
||||
/// </summary>
|
||||
internal static class JavaEntrypointResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves entrypoints, components, and edges from analysis inputs.
|
||||
/// </summary>
|
||||
public static JavaEntrypointResolution Resolve(
|
||||
JavaClassPathAnalysis classPath,
|
||||
JavaSignatureManifestAnalysis? signatureManifest,
|
||||
JavaJniAnalysis? jniAnalysis,
|
||||
JavaReflectionAnalysis? reflectionAnalysis,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(classPath);
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var entrypoints = ImmutableArray.CreateBuilder<JavaResolvedEntrypoint>();
|
||||
var components = ImmutableArray.CreateBuilder<JavaResolvedComponent>();
|
||||
var edges = ImmutableArray.CreateBuilder<JavaResolvedEdge>();
|
||||
var warnings = ImmutableArray.CreateBuilder<JavaResolutionWarning>();
|
||||
|
||||
// Process manifest entrypoints first (before segment loop) since signatureManifest
|
||||
// represents the specific archive being analyzed
|
||||
string? manifestSegmentId = null;
|
||||
string? manifestComponentId = null;
|
||||
if (signatureManifest is not null)
|
||||
{
|
||||
// Use a synthetic segment identifier for manifest-based entrypoints
|
||||
manifestSegmentId = "manifest-archive";
|
||||
manifestComponentId = ComputeId("component", manifestSegmentId);
|
||||
ResolveManifestEntrypoints(signatureManifest.LoaderAttributes, manifestSegmentId, entrypoints);
|
||||
|
||||
// Extract classpath edges from manifest Class-Path
|
||||
if (signatureManifest.LoaderAttributes.ClassPath is not null)
|
||||
{
|
||||
ResolveClassPathEdges(signatureManifest.LoaderAttributes, manifestComponentId, manifestSegmentId, edges);
|
||||
}
|
||||
}
|
||||
|
||||
// Process each segment in the classpath
|
||||
foreach (var segment in classPath.Segments)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Resolve component for this segment
|
||||
var component = ResolveComponent(segment, signatureManifest);
|
||||
components.Add(component);
|
||||
|
||||
// Extract JPMS module edges
|
||||
if (segment.Module is not null)
|
||||
{
|
||||
ResolveModuleEdges(segment, component.ComponentId, edges);
|
||||
}
|
||||
}
|
||||
|
||||
// Process JNI edges
|
||||
if (jniAnalysis is not null && !jniAnalysis.Edges.IsDefaultOrEmpty)
|
||||
{
|
||||
ResolveJniEdges(jniAnalysis, edges, entrypoints);
|
||||
}
|
||||
|
||||
// Process reflection edges
|
||||
if (reflectionAnalysis is not null && !reflectionAnalysis.Edges.IsDefaultOrEmpty)
|
||||
{
|
||||
ResolveReflectionEdges(reflectionAnalysis, edges);
|
||||
}
|
||||
|
||||
// Process SPI edges from classpath segments
|
||||
foreach (var segment in classPath.Segments)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ResolveSpiEdges(segment, edges, entrypoints);
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
// Calculate statistics
|
||||
var statistics = CalculateStatistics(
|
||||
entrypoints.ToImmutable(),
|
||||
components.ToImmutable(),
|
||||
edges.ToImmutable(),
|
||||
stopwatch.Elapsed);
|
||||
|
||||
return new JavaEntrypointResolution(
|
||||
entrypoints.ToImmutable(),
|
||||
components.ToImmutable(),
|
||||
edges.ToImmutable(),
|
||||
statistics,
|
||||
warnings.ToImmutable());
|
||||
}
|
||||
|
||||
private static JavaResolvedComponent ResolveComponent(
|
||||
JavaClassPathSegment segment,
|
||||
JavaSignatureManifestAnalysis? signatureManifest)
|
||||
{
|
||||
var componentId = ComputeId("component", segment.Identifier);
|
||||
var componentType = DetermineComponentType(segment, signatureManifest);
|
||||
var name = segment.Module?.Name ?? Path.GetFileNameWithoutExtension(segment.Identifier);
|
||||
var version = segment.Module?.Version;
|
||||
var isSigned = signatureManifest?.IsSigned ?? false;
|
||||
var signerFingerprint = signatureManifest?.Signatures.FirstOrDefault()?.SignerFingerprint;
|
||||
var mainClass = signatureManifest?.LoaderAttributes.MainClass;
|
||||
|
||||
JavaModuleInfo? moduleInfo = null;
|
||||
if (segment.Module is not null)
|
||||
{
|
||||
// ACC_OPEN flag = 0x0020 in module-info
|
||||
const ushort AccOpen = 0x0020;
|
||||
moduleInfo = new JavaModuleInfo(
|
||||
ModuleName: segment.Module.Name,
|
||||
IsOpen: (segment.Module.Flags & AccOpen) != 0,
|
||||
Requires: segment.Module.Requires.Select(r => r.Name).ToImmutableArray(),
|
||||
Exports: segment.Module.Exports.Select(e => e.Package).ToImmutableArray(),
|
||||
Opens: segment.Module.Opens.Select(o => o.Package).ToImmutableArray(),
|
||||
Uses: segment.Module.Uses,
|
||||
Provides: segment.Module.Provides.Select(p => p.Service).ToImmutableArray());
|
||||
}
|
||||
|
||||
return new JavaResolvedComponent(
|
||||
ComponentId: componentId,
|
||||
SegmentIdentifier: segment.Identifier,
|
||||
ComponentType: componentType,
|
||||
Name: name,
|
||||
Version: version,
|
||||
IsSigned: isSigned,
|
||||
SignerFingerprint: signerFingerprint,
|
||||
MainClass: mainClass,
|
||||
ModuleInfo: moduleInfo);
|
||||
}
|
||||
|
||||
private static JavaComponentType DetermineComponentType(
|
||||
JavaClassPathSegment segment,
|
||||
JavaSignatureManifestAnalysis? signatureManifest)
|
||||
{
|
||||
// Check for JPMS module
|
||||
if (segment.Module is not null)
|
||||
{
|
||||
return JavaComponentType.JpmsModule;
|
||||
}
|
||||
|
||||
// Check for Spring Boot fat JAR (has Start-Class)
|
||||
if (signatureManifest?.LoaderAttributes.StartClass is not null)
|
||||
{
|
||||
return JavaComponentType.SpringBootFatJar;
|
||||
}
|
||||
|
||||
// Check segment path for packaging hints
|
||||
var path = segment.Identifier.ToLowerInvariant();
|
||||
if (path.EndsWith(".war", StringComparison.Ordinal))
|
||||
{
|
||||
return JavaComponentType.War;
|
||||
}
|
||||
if (path.EndsWith(".ear", StringComparison.Ordinal))
|
||||
{
|
||||
return JavaComponentType.Ear;
|
||||
}
|
||||
if (path.EndsWith(".jmod", StringComparison.Ordinal))
|
||||
{
|
||||
return JavaComponentType.JpmsModule;
|
||||
}
|
||||
|
||||
return JavaComponentType.Jar;
|
||||
}
|
||||
|
||||
private static void ResolveManifestEntrypoints(
|
||||
ManifestLoaderAttributes attributes,
|
||||
string segmentIdentifier,
|
||||
ImmutableArray<JavaResolvedEntrypoint>.Builder entrypoints)
|
||||
{
|
||||
// Main-Class entrypoint
|
||||
if (!string.IsNullOrEmpty(attributes.MainClass))
|
||||
{
|
||||
entrypoints.Add(CreateEntrypoint(
|
||||
attributes.MainClass,
|
||||
"main",
|
||||
"([Ljava/lang/String;)V",
|
||||
JavaEntrypointType.MainClass,
|
||||
segmentIdentifier,
|
||||
framework: null,
|
||||
confidence: 0.95,
|
||||
"manifest:Main-Class"));
|
||||
}
|
||||
|
||||
// Start-Class (Spring Boot)
|
||||
if (!string.IsNullOrEmpty(attributes.StartClass))
|
||||
{
|
||||
entrypoints.Add(CreateEntrypoint(
|
||||
attributes.StartClass,
|
||||
"main",
|
||||
"([Ljava/lang/String;)V",
|
||||
JavaEntrypointType.SpringBootStartClass,
|
||||
segmentIdentifier,
|
||||
framework: "spring-boot",
|
||||
confidence: 0.98,
|
||||
"manifest:Start-Class"));
|
||||
}
|
||||
|
||||
// Premain-Class (Java agent)
|
||||
if (!string.IsNullOrEmpty(attributes.PremainClass))
|
||||
{
|
||||
entrypoints.Add(CreateEntrypoint(
|
||||
attributes.PremainClass,
|
||||
"premain",
|
||||
"(Ljava/lang/String;Ljava/lang/instrument/Instrumentation;)V",
|
||||
JavaEntrypointType.JavaAgentPremain,
|
||||
segmentIdentifier,
|
||||
framework: null,
|
||||
confidence: 0.95,
|
||||
"manifest:Premain-Class"));
|
||||
}
|
||||
|
||||
// Agent-Class (Java agent attach API)
|
||||
if (!string.IsNullOrEmpty(attributes.AgentClass))
|
||||
{
|
||||
entrypoints.Add(CreateEntrypoint(
|
||||
attributes.AgentClass,
|
||||
"agentmain",
|
||||
"(Ljava/lang/String;Ljava/lang/instrument/Instrumentation;)V",
|
||||
JavaEntrypointType.JavaAgentAttach,
|
||||
segmentIdentifier,
|
||||
framework: null,
|
||||
confidence: 0.95,
|
||||
"manifest:Agent-Class"));
|
||||
}
|
||||
|
||||
// Launcher-Agent-Class
|
||||
if (!string.IsNullOrEmpty(attributes.LauncherAgentClass))
|
||||
{
|
||||
entrypoints.Add(CreateEntrypoint(
|
||||
attributes.LauncherAgentClass,
|
||||
"agentmain",
|
||||
"(Ljava/lang/String;Ljava/lang/instrument/Instrumentation;)V",
|
||||
JavaEntrypointType.LauncherAgent,
|
||||
segmentIdentifier,
|
||||
framework: null,
|
||||
confidence: 0.90,
|
||||
"manifest:Launcher-Agent-Class"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResolveModuleEdges(
|
||||
JavaClassPathSegment segment,
|
||||
string componentId,
|
||||
ImmutableArray<JavaResolvedEdge>.Builder edges)
|
||||
{
|
||||
var module = segment.Module!;
|
||||
|
||||
// Process requires directives
|
||||
foreach (var requires in module.Requires)
|
||||
{
|
||||
var targetId = ComputeId("module", requires.Name);
|
||||
edges.Add(new JavaResolvedEdge(
|
||||
EdgeId: ComputeId("edge", $"{componentId}:{targetId}:requires"),
|
||||
SourceId: componentId,
|
||||
TargetId: targetId,
|
||||
EdgeType: JavaEdgeType.JpmsRequires,
|
||||
Reason: JavaEdgeReason.JpmsRequiresTransitive, // Simplified - could parse modifiers
|
||||
Confidence: 1.0,
|
||||
SegmentIdentifier: segment.Identifier,
|
||||
Details: $"requires {requires.Name}"));
|
||||
}
|
||||
|
||||
// Process uses directives
|
||||
foreach (var uses in module.Uses)
|
||||
{
|
||||
var targetId = ComputeId("service", uses);
|
||||
edges.Add(new JavaResolvedEdge(
|
||||
EdgeId: ComputeId("edge", $"{componentId}:{targetId}:uses"),
|
||||
SourceId: componentId,
|
||||
TargetId: targetId,
|
||||
EdgeType: JavaEdgeType.JpmsUses,
|
||||
Reason: JavaEdgeReason.JpmsUsesService,
|
||||
Confidence: 1.0,
|
||||
SegmentIdentifier: segment.Identifier,
|
||||
Details: $"uses {uses}"));
|
||||
}
|
||||
|
||||
// Process provides directives
|
||||
foreach (var provides in module.Provides)
|
||||
{
|
||||
var targetId = ComputeId("service", provides.Service);
|
||||
edges.Add(new JavaResolvedEdge(
|
||||
EdgeId: ComputeId("edge", $"{componentId}:{targetId}:provides"),
|
||||
SourceId: componentId,
|
||||
TargetId: targetId,
|
||||
EdgeType: JavaEdgeType.JpmsProvides,
|
||||
Reason: JavaEdgeReason.JpmsProvidesService,
|
||||
Confidence: 1.0,
|
||||
SegmentIdentifier: segment.Identifier,
|
||||
Details: $"provides {provides.Service}"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResolveClassPathEdges(
|
||||
ManifestLoaderAttributes attributes,
|
||||
string componentId,
|
||||
string segmentIdentifier,
|
||||
ImmutableArray<JavaResolvedEdge>.Builder edges)
|
||||
{
|
||||
foreach (var cpEntry in attributes.ParsedClassPath)
|
||||
{
|
||||
var targetId = ComputeId("classpath", cpEntry);
|
||||
edges.Add(new JavaResolvedEdge(
|
||||
EdgeId: ComputeId("edge", $"{componentId}:{targetId}:cp"),
|
||||
SourceId: componentId,
|
||||
TargetId: targetId,
|
||||
EdgeType: JavaEdgeType.ClasspathDependency,
|
||||
Reason: JavaEdgeReason.ManifestClassPath,
|
||||
Confidence: 0.95,
|
||||
SegmentIdentifier: segmentIdentifier,
|
||||
Details: $"Class-Path: {cpEntry}"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResolveJniEdges(
|
||||
JavaJniAnalysis jniAnalysis,
|
||||
ImmutableArray<JavaResolvedEdge>.Builder edges,
|
||||
ImmutableArray<JavaResolvedEntrypoint>.Builder entrypoints)
|
||||
{
|
||||
foreach (var jniEdge in jniAnalysis.Edges)
|
||||
{
|
||||
var sourceId = ComputeId("class", jniEdge.SourceClass);
|
||||
var targetId = jniEdge.TargetLibrary is not null
|
||||
? ComputeId("native", jniEdge.TargetLibrary)
|
||||
: ComputeId("native", "unknown");
|
||||
|
||||
var reason = jniEdge.Reason switch
|
||||
{
|
||||
JavaJniReason.SystemLoad => JavaEdgeReason.SystemLoad,
|
||||
JavaJniReason.SystemLoadLibrary => JavaEdgeReason.SystemLoadLibrary,
|
||||
JavaJniReason.RuntimeLoad => JavaEdgeReason.RuntimeLoadLibrary,
|
||||
JavaJniReason.RuntimeLoadLibrary => JavaEdgeReason.RuntimeLoadLibrary,
|
||||
JavaJniReason.NativeMethod => JavaEdgeReason.NativeMethodDeclaration,
|
||||
JavaJniReason.GraalJniConfig => JavaEdgeReason.GraalJniConfig,
|
||||
JavaJniReason.BundledNativeLib => JavaEdgeReason.BundledNativeLib,
|
||||
_ => JavaEdgeReason.NativeMethodDeclaration,
|
||||
};
|
||||
|
||||
var confidence = jniEdge.Confidence switch
|
||||
{
|
||||
JavaJniConfidence.High => 0.95,
|
||||
JavaJniConfidence.Medium => 0.75,
|
||||
JavaJniConfidence.Low => 0.50,
|
||||
_ => 0.50,
|
||||
};
|
||||
|
||||
edges.Add(new JavaResolvedEdge(
|
||||
EdgeId: ComputeId("edge", $"{sourceId}:{targetId}:jni:{jniEdge.InstructionOffset}"),
|
||||
SourceId: sourceId,
|
||||
TargetId: targetId,
|
||||
EdgeType: JavaEdgeType.JniNativeLib,
|
||||
Reason: reason,
|
||||
Confidence: confidence,
|
||||
SegmentIdentifier: jniEdge.SegmentIdentifier,
|
||||
Details: jniEdge.Details));
|
||||
|
||||
// Native methods are entrypoints
|
||||
if (jniEdge.Reason == JavaJniReason.NativeMethod)
|
||||
{
|
||||
entrypoints.Add(CreateEntrypoint(
|
||||
jniEdge.SourceClass,
|
||||
jniEdge.MethodName,
|
||||
jniEdge.MethodDescriptor,
|
||||
JavaEntrypointType.NativeMethod,
|
||||
jniEdge.SegmentIdentifier,
|
||||
framework: null,
|
||||
confidence: confidence,
|
||||
"jni:native-method"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResolveReflectionEdges(
|
||||
JavaReflectionAnalysis reflectionAnalysis,
|
||||
ImmutableArray<JavaResolvedEdge>.Builder edges)
|
||||
{
|
||||
foreach (var reflectEdge in reflectionAnalysis.Edges)
|
||||
{
|
||||
var sourceId = ComputeId("class", reflectEdge.SourceClass);
|
||||
var targetId = reflectEdge.TargetType is not null
|
||||
? ComputeId("class", reflectEdge.TargetType)
|
||||
: ComputeId("class", "dynamic");
|
||||
|
||||
var reason = reflectEdge.Reason switch
|
||||
{
|
||||
JavaReflectionReason.ClassForName => JavaEdgeReason.ClassForName,
|
||||
JavaReflectionReason.ClassLoaderLoadClass => JavaEdgeReason.ClassLoaderLoadClass,
|
||||
JavaReflectionReason.ServiceLoaderLoad => JavaEdgeReason.MetaInfServices,
|
||||
JavaReflectionReason.ResourceLookup => JavaEdgeReason.ResourceReference,
|
||||
_ => JavaEdgeReason.ClassForName,
|
||||
};
|
||||
|
||||
var confidence = reflectEdge.Confidence switch
|
||||
{
|
||||
JavaReflectionConfidence.High => 0.85,
|
||||
JavaReflectionConfidence.Medium => 0.65,
|
||||
JavaReflectionConfidence.Low => 0.45,
|
||||
_ => 0.45,
|
||||
};
|
||||
|
||||
edges.Add(new JavaResolvedEdge(
|
||||
EdgeId: ComputeId("edge", $"{sourceId}:{targetId}:reflect:{reflectEdge.InstructionOffset}"),
|
||||
SourceId: sourceId,
|
||||
TargetId: targetId,
|
||||
EdgeType: JavaEdgeType.ReflectionLoad,
|
||||
Reason: reason,
|
||||
Confidence: confidence,
|
||||
SegmentIdentifier: reflectEdge.SegmentIdentifier,
|
||||
Details: reflectEdge.Details));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResolveSpiEdges(
|
||||
JavaClassPathSegment segment,
|
||||
ImmutableArray<JavaResolvedEdge>.Builder edges,
|
||||
ImmutableArray<JavaResolvedEntrypoint>.Builder entrypoints)
|
||||
{
|
||||
// Check for META-INF/services entries in segment
|
||||
foreach (var location in segment.ClassLocations)
|
||||
{
|
||||
// This would need archive access to scan META-INF/services
|
||||
// For now, we process SPI from module-info provides directives (handled in ResolveModuleEdges)
|
||||
}
|
||||
|
||||
// Process module-info provides as SPI entrypoints
|
||||
if (segment.Module is not null)
|
||||
{
|
||||
foreach (var provides in segment.Module.Provides)
|
||||
{
|
||||
// Each implementation class is a service provider entrypoint
|
||||
foreach (var impl in provides.Implementations)
|
||||
{
|
||||
entrypoints.Add(CreateEntrypoint(
|
||||
impl, // Implementation class
|
||||
methodName: null,
|
||||
methodDescriptor: null,
|
||||
JavaEntrypointType.ServiceProvider,
|
||||
segment.Identifier,
|
||||
framework: null,
|
||||
confidence: 1.0,
|
||||
$"module-info:provides:{provides.Service}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JavaResolvedEntrypoint CreateEntrypoint(
|
||||
string classFqcn,
|
||||
string? methodName,
|
||||
string? methodDescriptor,
|
||||
JavaEntrypointType entrypointType,
|
||||
string segmentIdentifier,
|
||||
string? framework,
|
||||
double confidence,
|
||||
params string[] resolutionPath)
|
||||
{
|
||||
var id = ComputeId("entry", $"{classFqcn}:{methodName ?? "class"}:{methodDescriptor ?? ""}");
|
||||
|
||||
return new JavaResolvedEntrypoint(
|
||||
EntrypointId: id,
|
||||
ClassFqcn: classFqcn,
|
||||
MethodName: methodName,
|
||||
MethodDescriptor: methodDescriptor,
|
||||
EntrypointType: entrypointType,
|
||||
SegmentIdentifier: segmentIdentifier,
|
||||
Framework: framework,
|
||||
Confidence: confidence,
|
||||
ResolutionPath: resolutionPath.ToImmutableArray(),
|
||||
Metadata: null);
|
||||
}
|
||||
|
||||
private static string ComputeId(string prefix, string input)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
var shortHash = Convert.ToHexString(hash[..8]).ToLowerInvariant();
|
||||
return $"{prefix}:{shortHash}";
|
||||
}
|
||||
|
||||
private static JavaResolutionStatistics CalculateStatistics(
|
||||
ImmutableArray<JavaResolvedEntrypoint> entrypoints,
|
||||
ImmutableArray<JavaResolvedComponent> components,
|
||||
ImmutableArray<JavaResolvedEdge> edges,
|
||||
TimeSpan duration)
|
||||
{
|
||||
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: duration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Signature;
|
||||
|
||||
/// <summary>
|
||||
/// Results of JAR signature and manifest metadata analysis per task 21-007.
|
||||
/// Captures signature structure, signers, and loader attributes.
|
||||
/// </summary>
|
||||
internal sealed record JavaSignatureManifestAnalysis(
|
||||
ImmutableArray<JarSignature> Signatures,
|
||||
ManifestLoaderAttributes LoaderAttributes,
|
||||
ImmutableArray<SignatureWarning> Warnings)
|
||||
{
|
||||
public static readonly JavaSignatureManifestAnalysis Empty = new(
|
||||
ImmutableArray<JarSignature>.Empty,
|
||||
ManifestLoaderAttributes.Empty,
|
||||
ImmutableArray<SignatureWarning>.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// True if the JAR contains any valid signature files.
|
||||
/// </summary>
|
||||
public bool IsSigned => Signatures.Length > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JAR signature found in META-INF.
|
||||
/// </summary>
|
||||
/// <param name="SignerName">Base name of the signature (e.g., "MYAPP" from MYAPP.SF/MYAPP.RSA).</param>
|
||||
/// <param name="SignatureFileEntry">Path to the .SF signature file.</param>
|
||||
/// <param name="SignatureBlockEntry">Path to the signature block file (.RSA, .DSA, .EC).</param>
|
||||
/// <param name="Algorithm">Signature algorithm inferred from block file extension.</param>
|
||||
/// <param name="SignerSubject">X.509 subject DN of the signer certificate (if extractable).</param>
|
||||
/// <param name="SignerIssuer">X.509 issuer DN (if extractable).</param>
|
||||
/// <param name="SignerSerialNumber">Certificate serial number (if extractable).</param>
|
||||
/// <param name="SignerFingerprint">SHA-256 fingerprint of the signer certificate (if extractable).</param>
|
||||
/// <param name="DigestAlgorithms">Digest algorithms used in the signature file.</param>
|
||||
/// <param name="Confidence">Confidence level of the signature detection.</param>
|
||||
internal sealed record JarSignature(
|
||||
string SignerName,
|
||||
string SignatureFileEntry,
|
||||
string? SignatureBlockEntry,
|
||||
SignatureAlgorithm Algorithm,
|
||||
string? SignerSubject,
|
||||
string? SignerIssuer,
|
||||
string? SignerSerialNumber,
|
||||
string? SignerFingerprint,
|
||||
ImmutableArray<string> DigestAlgorithms,
|
||||
SignatureConfidence Confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Manifest loader attributes that define entrypoint and classpath behavior.
|
||||
/// </summary>
|
||||
/// <param name="MainClass">Main-Class attribute for executable JARs.</param>
|
||||
/// <param name="StartClass">Start-Class attribute for Spring Boot fat JARs.</param>
|
||||
/// <param name="AgentClass">Agent-Class attribute for Java agents (JVM attach API).</param>
|
||||
/// <param name="PremainClass">Premain-Class attribute for Java agents (startup instrumentation).</param>
|
||||
/// <param name="LauncherAgentClass">Launcher-Agent-Class for native launcher agents.</param>
|
||||
/// <param name="ClassPath">Class-Path manifest attribute (space-separated relative paths).</param>
|
||||
/// <param name="AutomaticModuleName">Automatic-Module-Name for JPMS.</param>
|
||||
/// <param name="MultiRelease">True if Multi-Release: true is present.</param>
|
||||
/// <param name="SealedPackages">List of sealed package names.</param>
|
||||
internal sealed record ManifestLoaderAttributes(
|
||||
string? MainClass,
|
||||
string? StartClass,
|
||||
string? AgentClass,
|
||||
string? PremainClass,
|
||||
string? LauncherAgentClass,
|
||||
string? ClassPath,
|
||||
string? AutomaticModuleName,
|
||||
bool MultiRelease,
|
||||
ImmutableArray<string> SealedPackages)
|
||||
{
|
||||
public static readonly ManifestLoaderAttributes Empty = new(
|
||||
MainClass: null,
|
||||
StartClass: null,
|
||||
AgentClass: null,
|
||||
PremainClass: null,
|
||||
LauncherAgentClass: null,
|
||||
ClassPath: null,
|
||||
AutomaticModuleName: null,
|
||||
MultiRelease: false,
|
||||
SealedPackages: ImmutableArray<string>.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// True if this JAR has any entrypoint attribute (Main-Class, Agent-Class, etc.).
|
||||
/// </summary>
|
||||
public bool HasEntrypoint =>
|
||||
!string.IsNullOrEmpty(MainClass) ||
|
||||
!string.IsNullOrEmpty(StartClass) ||
|
||||
!string.IsNullOrEmpty(AgentClass) ||
|
||||
!string.IsNullOrEmpty(PremainClass) ||
|
||||
!string.IsNullOrEmpty(LauncherAgentClass);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the primary entrypoint class (Main-Class, Start-Class, or agent class).
|
||||
/// </summary>
|
||||
public string? PrimaryEntrypoint =>
|
||||
MainClass ?? StartClass ?? PremainClass ?? AgentClass ?? LauncherAgentClass;
|
||||
|
||||
/// <summary>
|
||||
/// Returns parsed Class-Path entries as individual paths.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> ParsedClassPath =>
|
||||
string.IsNullOrWhiteSpace(ClassPath)
|
||||
? ImmutableArray<string>.Empty
|
||||
: ClassPath.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warning emitted during signature/manifest analysis.
|
||||
/// </summary>
|
||||
internal sealed record SignatureWarning(
|
||||
string SegmentIdentifier,
|
||||
string WarningCode,
|
||||
string Message,
|
||||
string? Details);
|
||||
|
||||
/// <summary>
|
||||
/// Signature algorithm inferred from signature block file extension.
|
||||
/// </summary>
|
||||
internal enum SignatureAlgorithm
|
||||
{
|
||||
/// <summary>Unknown or unsupported algorithm.</summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>RSA signature (.RSA file).</summary>
|
||||
RSA,
|
||||
|
||||
/// <summary>DSA signature (.DSA file).</summary>
|
||||
DSA,
|
||||
|
||||
/// <summary>ECDSA signature (.EC file).</summary>
|
||||
EC,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confidence level for signature detection.
|
||||
/// </summary>
|
||||
internal enum SignatureConfidence
|
||||
{
|
||||
/// <summary>Low confidence - signature file exists but block missing or invalid.</summary>
|
||||
Low = 1,
|
||||
|
||||
/// <summary>Medium confidence - signature structure present but certificate extraction failed.</summary>
|
||||
Medium = 2,
|
||||
|
||||
/// <summary>High confidence - complete signature with extractable certificate info.</summary>
|
||||
High = 3,
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Osgi;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Signature;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes JAR signature structure and manifest loader attributes per task 21-007.
|
||||
/// </summary>
|
||||
internal static partial class JavaSignatureManifestAnalyzer
|
||||
{
|
||||
private static readonly Regex DigestAlgorithmPattern = DigestAlgorithmRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a single JAR archive for signature and manifest metadata.
|
||||
/// </summary>
|
||||
public static JavaSignatureManifestAnalysis Analyze(JavaArchive archive, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(archive);
|
||||
|
||||
var warnings = ImmutableArray.CreateBuilder<SignatureWarning>();
|
||||
var segmentId = archive.RelativePath;
|
||||
|
||||
// Analyze signatures
|
||||
var signatures = AnalyzeSignatures(archive, segmentId, warnings);
|
||||
|
||||
// Extract loader attributes
|
||||
var loaderAttributes = ExtractLoaderAttributes(archive, cancellationToken);
|
||||
|
||||
return new JavaSignatureManifestAnalysis(
|
||||
signatures,
|
||||
loaderAttributes,
|
||||
warnings.ToImmutable());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a single archive for JAR signatures.
|
||||
/// </summary>
|
||||
public static ImmutableArray<JarSignature> AnalyzeSignatures(
|
||||
JavaArchive archive,
|
||||
string segmentId,
|
||||
ImmutableArray<SignatureWarning>.Builder warnings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(archive);
|
||||
|
||||
var signatureFiles = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var signatureBlocks = new Dictionary<string, (string Path, SignatureAlgorithm Algorithm)>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Collect signature files from META-INF
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
var path = entry.EffectivePath;
|
||||
if (!path.StartsWith("META-INF/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileName(path);
|
||||
var baseName = Path.GetFileNameWithoutExtension(fileName);
|
||||
var extension = Path.GetExtension(fileName).ToUpperInvariant();
|
||||
|
||||
switch (extension)
|
||||
{
|
||||
case ".SF":
|
||||
signatureFiles[baseName] = path;
|
||||
break;
|
||||
case ".RSA":
|
||||
signatureBlocks[baseName] = (path, SignatureAlgorithm.RSA);
|
||||
break;
|
||||
case ".DSA":
|
||||
signatureBlocks[baseName] = (path, SignatureAlgorithm.DSA);
|
||||
break;
|
||||
case ".EC":
|
||||
signatureBlocks[baseName] = (path, SignatureAlgorithm.EC);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (signatureFiles.Count == 0)
|
||||
{
|
||||
return ImmutableArray<JarSignature>.Empty;
|
||||
}
|
||||
|
||||
var signatures = ImmutableArray.CreateBuilder<JarSignature>();
|
||||
|
||||
foreach (var (signerName, sfPath) in signatureFiles)
|
||||
{
|
||||
var digestAlgorithms = ExtractDigestAlgorithms(archive, sfPath);
|
||||
|
||||
if (signatureBlocks.TryGetValue(signerName, out var blockInfo))
|
||||
{
|
||||
// Complete signature pair found
|
||||
var (blockPath, algorithm) = blockInfo;
|
||||
var certInfo = ExtractCertificateInfo(archive, blockPath);
|
||||
|
||||
var confidence = certInfo.Subject is not null
|
||||
? SignatureConfidence.High
|
||||
: SignatureConfidence.Medium;
|
||||
|
||||
signatures.Add(new JarSignature(
|
||||
SignerName: signerName,
|
||||
SignatureFileEntry: sfPath,
|
||||
SignatureBlockEntry: blockPath,
|
||||
Algorithm: algorithm,
|
||||
SignerSubject: certInfo.Subject,
|
||||
SignerIssuer: certInfo.Issuer,
|
||||
SignerSerialNumber: certInfo.SerialNumber,
|
||||
SignerFingerprint: certInfo.Fingerprint,
|
||||
DigestAlgorithms: digestAlgorithms,
|
||||
Confidence: confidence));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Signature file without corresponding block - incomplete signature
|
||||
warnings.Add(new SignatureWarning(
|
||||
segmentId,
|
||||
"INCOMPLETE_SIGNATURE",
|
||||
$"Signature file {sfPath} has no corresponding block file (.RSA/.DSA/.EC)",
|
||||
Details: null));
|
||||
|
||||
signatures.Add(new JarSignature(
|
||||
SignerName: signerName,
|
||||
SignatureFileEntry: sfPath,
|
||||
SignatureBlockEntry: null,
|
||||
Algorithm: SignatureAlgorithm.Unknown,
|
||||
SignerSubject: null,
|
||||
SignerIssuer: null,
|
||||
SignerSerialNumber: null,
|
||||
SignerFingerprint: null,
|
||||
DigestAlgorithms: digestAlgorithms,
|
||||
Confidence: SignatureConfidence.Low));
|
||||
}
|
||||
}
|
||||
|
||||
return signatures.ToImmutable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts loader attributes from the JAR manifest.
|
||||
/// </summary>
|
||||
public static ManifestLoaderAttributes ExtractLoaderAttributes(JavaArchive archive, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(archive);
|
||||
|
||||
if (!archive.TryGetEntry("META-INF/MANIFEST.MF", out var manifestEntry))
|
||||
{
|
||||
return ManifestLoaderAttributes.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var entryStream = archive.OpenEntry(manifestEntry);
|
||||
using var reader = new StreamReader(entryStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
var manifest = OsgiBundleParser.ParseManifest(content);
|
||||
|
||||
manifest.TryGetValue("Main-Class", out var mainClass);
|
||||
manifest.TryGetValue("Start-Class", out var startClass);
|
||||
manifest.TryGetValue("Agent-Class", out var agentClass);
|
||||
manifest.TryGetValue("Premain-Class", out var premainClass);
|
||||
manifest.TryGetValue("Launcher-Agent-Class", out var launcherAgentClass);
|
||||
manifest.TryGetValue("Class-Path", out var classPath);
|
||||
manifest.TryGetValue("Automatic-Module-Name", out var automaticModuleName);
|
||||
manifest.TryGetValue("Multi-Release", out var multiReleaseStr);
|
||||
|
||||
var multiRelease = string.Equals(multiReleaseStr, "true", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Extract sealed packages from per-entry attributes
|
||||
var sealedPackages = ExtractSealedPackages(manifest);
|
||||
|
||||
return new ManifestLoaderAttributes(
|
||||
MainClass: mainClass?.Trim(),
|
||||
StartClass: startClass?.Trim(),
|
||||
AgentClass: agentClass?.Trim(),
|
||||
PremainClass: premainClass?.Trim(),
|
||||
LauncherAgentClass: launcherAgentClass?.Trim(),
|
||||
ClassPath: classPath?.Trim(),
|
||||
AutomaticModuleName: automaticModuleName?.Trim(),
|
||||
MultiRelease: multiRelease,
|
||||
SealedPackages: sealedPackages);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ManifestLoaderAttributes.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ExtractDigestAlgorithms(JavaArchive archive, string sfPath)
|
||||
{
|
||||
if (!archive.TryGetEntry(sfPath, out var sfEntry))
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var entryStream = archive.OpenEntry(sfEntry);
|
||||
using var reader = new StreamReader(entryStream, Encoding.UTF8);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
var algorithms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var matches = DigestAlgorithmPattern.Matches(content);
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
algorithms.Add(match.Groups[1].Value.ToUpperInvariant());
|
||||
}
|
||||
|
||||
return algorithms.OrderBy(static a => a, StringComparer.Ordinal).ToImmutableArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static (string? Subject, string? Issuer, string? SerialNumber, string? Fingerprint) ExtractCertificateInfo(
|
||||
JavaArchive archive,
|
||||
string blockPath)
|
||||
{
|
||||
if (!archive.TryGetEntry(blockPath, out var blockEntry))
|
||||
{
|
||||
return (null, null, null, null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var entryStream = archive.OpenEntry(blockEntry);
|
||||
using var memoryStream = new MemoryStream();
|
||||
entryStream.CopyTo(memoryStream);
|
||||
var data = memoryStream.ToArray();
|
||||
|
||||
// Compute SHA-256 hash of the signature block for identification
|
||||
var fingerprint = Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant();
|
||||
|
||||
// Try basic ASN.1 parsing to extract certificate subject
|
||||
// The PKCS#7 SignedData structure contains certificates as nested ASN.1 sequences
|
||||
var certInfo = TryParseSignatureBlockCertificate(data);
|
||||
|
||||
return (
|
||||
Subject: certInfo.Subject,
|
||||
Issuer: certInfo.Issuer,
|
||||
SerialNumber: certInfo.SerialNumber,
|
||||
Fingerprint: fingerprint);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Certificate extraction failed - return nulls
|
||||
return (null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts basic parsing of PKCS#7 SignedData to extract certificate info.
|
||||
/// This is a simplified parser that extracts the signer certificate subject if possible.
|
||||
/// </summary>
|
||||
private static (string? Subject, string? Issuer, string? SerialNumber) TryParseSignatureBlockCertificate(byte[] data)
|
||||
{
|
||||
// PKCS#7 SignedData is an ASN.1 SEQUENCE containing:
|
||||
// - contentType (OID)
|
||||
// - content (EXPLICIT [0] SignedData)
|
||||
// - version
|
||||
// - digestAlgorithms
|
||||
// - contentInfo
|
||||
// - certificates [0] IMPLICIT (optional)
|
||||
// - crls [1] IMPLICIT (optional)
|
||||
// - signerInfos
|
||||
//
|
||||
// This simplified parser looks for patterns in the DER encoding
|
||||
// to extract basic certificate info without full ASN.1 parsing.
|
||||
|
||||
if (data.Length < 10)
|
||||
{
|
||||
return (null, null, null);
|
||||
}
|
||||
|
||||
// Look for X.509 certificate structure markers
|
||||
// The certificate contains issuer and subject as ASN.1 sequences
|
||||
// For now, return null - full certificate parsing would require
|
||||
// System.Security.Cryptography.Pkcs or custom ASN.1 parser
|
||||
|
||||
// Future: implement proper certificate extraction using BouncyCastle
|
||||
// or System.Security.Cryptography.Pkcs if package reference is added
|
||||
|
||||
return (null, null, null);
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ExtractSealedPackages(IReadOnlyDictionary<string, string> manifest)
|
||||
{
|
||||
// In standard JAR manifests, sealed packages are indicated by per-package sections
|
||||
// with "Sealed: true". The OsgiBundleParser doesn't parse per-entry sections,
|
||||
// so we just check for the top-level "Sealed" attribute as a fallback.
|
||||
// A complete implementation would parse per-entry sections from the manifest.
|
||||
|
||||
if (manifest.TryGetValue("Sealed", out var sealedValue) &&
|
||||
string.Equals(sealedValue, "true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Entire JAR is sealed - return empty since we can't enumerate packages here
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"([\w-]+)-Digest(?:-Manifest)?:", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
|
||||
private static partial Regex DigestAlgorithmRegex();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"description": "Java EE Enterprise Archive with EJBs and embedded modules",
|
||||
"components": [
|
||||
{
|
||||
"jarPath": "enterprise.ear",
|
||||
"packaging": "Ear",
|
||||
"moduleInfo": null,
|
||||
"applicationXml": {
|
||||
"displayName": "Enterprise Application",
|
||||
"modules": [
|
||||
{
|
||||
"type": "ejb",
|
||||
"path": "ejb-module.jar"
|
||||
},
|
||||
{
|
||||
"type": "web",
|
||||
"path": "web-module.war",
|
||||
"contextRoot": "/app"
|
||||
}
|
||||
]
|
||||
},
|
||||
"embeddedModules": [
|
||||
{
|
||||
"jarPath": "ejb-module.jar",
|
||||
"packaging": "Jar",
|
||||
"ejbJarXml": {
|
||||
"sessionBeans": [
|
||||
{
|
||||
"ejbName": "AccountService",
|
||||
"ejbClass": "com.example.ejb.AccountServiceBean",
|
||||
"sessionType": "Stateless"
|
||||
},
|
||||
{
|
||||
"ejbName": "OrderProcessor",
|
||||
"ejbClass": "com.example.ejb.OrderProcessorBean",
|
||||
"sessionType": "Stateful"
|
||||
}
|
||||
],
|
||||
"messageDrivenBeans": [
|
||||
{
|
||||
"ejbName": "OrderEventListener",
|
||||
"ejbClass": "com.example.mdb.OrderEventListenerBean",
|
||||
"destinationType": "javax.jms.Queue"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"jarPath": "web-module.war",
|
||||
"packaging": "War"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"expectedEntrypoints": [
|
||||
{
|
||||
"entrypointType": "EjbSessionBean",
|
||||
"classFqcn": "com.example.ejb.AccountServiceBean",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": "ejb"
|
||||
},
|
||||
{
|
||||
"entrypointType": "EjbSessionBean",
|
||||
"classFqcn": "com.example.ejb.OrderProcessorBean",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": "ejb"
|
||||
},
|
||||
{
|
||||
"entrypointType": "EjbMessageDrivenBean",
|
||||
"classFqcn": "com.example.mdb.OrderEventListenerBean",
|
||||
"methodName": "onMessage",
|
||||
"methodDescriptor": "(Ljavax/jms/Message;)V",
|
||||
"framework": "ejb"
|
||||
}
|
||||
],
|
||||
"expectedComponents": [
|
||||
{
|
||||
"componentType": "Ear",
|
||||
"name": "enterprise.ear"
|
||||
},
|
||||
{
|
||||
"componentType": "Jar",
|
||||
"name": "ejb-module.jar"
|
||||
},
|
||||
{
|
||||
"componentType": "War",
|
||||
"name": "web-module.war"
|
||||
}
|
||||
],
|
||||
"expectedEdges": [
|
||||
{
|
||||
"edgeType": "EarModule",
|
||||
"source": "enterprise.ear",
|
||||
"target": "ejb-module.jar"
|
||||
},
|
||||
{
|
||||
"edgeType": "EarModule",
|
||||
"source": "enterprise.ear",
|
||||
"target": "web-module.war"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"description": "JNI-heavy application with native methods, System.load calls, and bundled native libraries",
|
||||
"components": [
|
||||
{
|
||||
"jarPath": "native-app.jar",
|
||||
"packaging": "Jar",
|
||||
"moduleInfo": null,
|
||||
"manifest": {
|
||||
"Main-Class": "com.example.native.NativeApp",
|
||||
"Bundle-NativeCode": "native/linux-x64/libcrypto.so;osname=Linux;processor=x86-64,native/win-x64/crypto.dll;osname=Windows;processor=x86-64,native/darwin-arm64/libcrypto.dylib;osname=MacOS;processor=aarch64"
|
||||
},
|
||||
"nativeLibraries": [
|
||||
"native/linux-x64/libcrypto.so",
|
||||
"native/linux-x64/libssl.so",
|
||||
"native/win-x64/crypto.dll",
|
||||
"native/darwin-arm64/libcrypto.dylib"
|
||||
],
|
||||
"graalNativeConfig": {
|
||||
"jni-config.json": [
|
||||
{
|
||||
"name": "com.example.native.CryptoBinding",
|
||||
"methods": [
|
||||
{"name": "encrypt", "parameterTypes": ["byte[]", "byte[]"]},
|
||||
{"name": "decrypt", "parameterTypes": ["byte[]", "byte[]"]}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"nativeMethods": [
|
||||
{
|
||||
"className": "com.example.native.CryptoBinding",
|
||||
"methodName": "nativeEncrypt",
|
||||
"descriptor": "([B[B)[B"
|
||||
},
|
||||
{
|
||||
"className": "com.example.native.CryptoBinding",
|
||||
"methodName": "nativeDecrypt",
|
||||
"descriptor": "([B[B)[B"
|
||||
},
|
||||
{
|
||||
"className": "com.example.native.SystemInfo",
|
||||
"methodName": "getProcessorCount",
|
||||
"descriptor": "()I"
|
||||
}
|
||||
],
|
||||
"systemLoadCalls": [
|
||||
{
|
||||
"className": "com.example.native.CryptoBinding",
|
||||
"methodName": "<clinit>",
|
||||
"loadTarget": "crypto",
|
||||
"loadType": "SystemLoadLibrary"
|
||||
},
|
||||
{
|
||||
"className": "com.example.native.DirectLoader",
|
||||
"methodName": "loadNative",
|
||||
"loadTarget": "/opt/native/libcustom.so",
|
||||
"loadType": "SystemLoad"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"expectedEntrypoints": [
|
||||
{
|
||||
"entrypointType": "MainClass",
|
||||
"classFqcn": "com.example.native.NativeApp",
|
||||
"methodName": "main",
|
||||
"methodDescriptor": "([Ljava/lang/String;)V",
|
||||
"framework": null
|
||||
},
|
||||
{
|
||||
"entrypointType": "NativeMethod",
|
||||
"classFqcn": "com.example.native.CryptoBinding",
|
||||
"methodName": "nativeEncrypt",
|
||||
"methodDescriptor": "([B[B)[B",
|
||||
"framework": null
|
||||
},
|
||||
{
|
||||
"entrypointType": "NativeMethod",
|
||||
"classFqcn": "com.example.native.CryptoBinding",
|
||||
"methodName": "nativeDecrypt",
|
||||
"methodDescriptor": "([B[B)[B",
|
||||
"framework": null
|
||||
},
|
||||
{
|
||||
"entrypointType": "NativeMethod",
|
||||
"classFqcn": "com.example.native.SystemInfo",
|
||||
"methodName": "getProcessorCount",
|
||||
"methodDescriptor": "()I",
|
||||
"framework": null
|
||||
}
|
||||
],
|
||||
"expectedEdges": [
|
||||
{
|
||||
"edgeType": "JniLoad",
|
||||
"source": "com.example.native.CryptoBinding",
|
||||
"target": "crypto",
|
||||
"reason": "SystemLoadLibrary",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"edgeType": "JniLoad",
|
||||
"source": "com.example.native.DirectLoader",
|
||||
"target": "/opt/native/libcustom.so",
|
||||
"reason": "SystemLoad",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"edgeType": "JniBundledLib",
|
||||
"source": "native-app.jar",
|
||||
"target": "native/linux-x64/libcrypto.so",
|
||||
"reason": "BundledNativeLib",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"edgeType": "JniGraalConfig",
|
||||
"source": "native-app.jar",
|
||||
"target": "com.example.native.CryptoBinding",
|
||||
"reason": "GraalJniConfig",
|
||||
"confidence": "High"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"description": "MicroProfile application with JAX-RS endpoints, CDI beans, and config injection",
|
||||
"components": [
|
||||
{
|
||||
"jarPath": "microservice.jar",
|
||||
"packaging": "Jar",
|
||||
"moduleInfo": null,
|
||||
"manifest": {
|
||||
"Main-Class": "io.helidon.microprofile.cdi.Main"
|
||||
},
|
||||
"microprofileConfig": {
|
||||
"META-INF/microprofile-config.properties": {
|
||||
"mp.config.profile": "prod",
|
||||
"server.port": "8080",
|
||||
"datasource.url": "jdbc:postgresql://localhost/mydb"
|
||||
},
|
||||
"META-INF/beans.xml": {
|
||||
"beanDiscoveryMode": "annotated"
|
||||
}
|
||||
},
|
||||
"jaxRsEndpoints": [
|
||||
{
|
||||
"resourceClass": "com.example.api.UserResource",
|
||||
"path": "/users",
|
||||
"methods": [
|
||||
{"httpMethod": "GET", "path": "", "produces": "application/json"},
|
||||
{"httpMethod": "GET", "path": "/{id}", "produces": "application/json"},
|
||||
{"httpMethod": "POST", "path": "", "consumes": "application/json", "produces": "application/json"},
|
||||
{"httpMethod": "PUT", "path": "/{id}", "consumes": "application/json"},
|
||||
{"httpMethod": "DELETE", "path": "/{id}"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceClass": "com.example.api.OrderResource",
|
||||
"path": "/orders",
|
||||
"methods": [
|
||||
{"httpMethod": "GET", "path": "", "produces": "application/json"},
|
||||
{"httpMethod": "POST", "path": "", "consumes": "application/json", "produces": "application/json"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"cdiComponents": [
|
||||
{
|
||||
"beanClass": "com.example.service.UserService",
|
||||
"scope": "ApplicationScoped",
|
||||
"qualifiers": []
|
||||
},
|
||||
{
|
||||
"beanClass": "com.example.service.OrderService",
|
||||
"scope": "RequestScoped",
|
||||
"qualifiers": []
|
||||
},
|
||||
{
|
||||
"beanClass": "com.example.producer.DataSourceProducer",
|
||||
"scope": "ApplicationScoped",
|
||||
"produces": ["javax.sql.DataSource"]
|
||||
}
|
||||
],
|
||||
"mpRestClients": [
|
||||
{
|
||||
"interfaceClass": "com.example.client.PaymentServiceClient",
|
||||
"configKey": "payment-service",
|
||||
"baseUrl": "https://payment.example.com/api"
|
||||
}
|
||||
],
|
||||
"mpHealthChecks": [
|
||||
{
|
||||
"checkClass": "com.example.health.DatabaseHealthCheck",
|
||||
"type": "readiness"
|
||||
},
|
||||
{
|
||||
"checkClass": "com.example.health.DiskSpaceHealthCheck",
|
||||
"type": "liveness"
|
||||
}
|
||||
],
|
||||
"mpMetrics": [
|
||||
{
|
||||
"metricClass": "com.example.api.UserResource",
|
||||
"metricType": "Counted",
|
||||
"metricName": "user_requests_total"
|
||||
},
|
||||
{
|
||||
"metricClass": "com.example.service.OrderService",
|
||||
"metricType": "Timed",
|
||||
"metricName": "order_processing_time"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"expectedEntrypoints": [
|
||||
{
|
||||
"entrypointType": "MainClass",
|
||||
"classFqcn": "io.helidon.microprofile.cdi.Main",
|
||||
"methodName": "main",
|
||||
"methodDescriptor": "([Ljava/lang/String;)V",
|
||||
"framework": "helidon"
|
||||
},
|
||||
{
|
||||
"entrypointType": "JaxRsResource",
|
||||
"classFqcn": "com.example.api.UserResource",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": "jax-rs",
|
||||
"httpMetadata": {
|
||||
"path": "/users",
|
||||
"methods": ["GET", "POST", "PUT", "DELETE"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"entrypointType": "JaxRsResource",
|
||||
"classFqcn": "com.example.api.OrderResource",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": "jax-rs",
|
||||
"httpMetadata": {
|
||||
"path": "/orders",
|
||||
"methods": ["GET", "POST"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"entrypointType": "CdiBean",
|
||||
"classFqcn": "com.example.service.UserService",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": "cdi"
|
||||
},
|
||||
{
|
||||
"entrypointType": "CdiBean",
|
||||
"classFqcn": "com.example.service.OrderService",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": "cdi"
|
||||
},
|
||||
{
|
||||
"entrypointType": "MpHealthCheck",
|
||||
"classFqcn": "com.example.health.DatabaseHealthCheck",
|
||||
"methodName": "check",
|
||||
"methodDescriptor": "()Lorg/eclipse/microprofile/health/HealthCheckResponse;",
|
||||
"framework": "mp-health"
|
||||
},
|
||||
{
|
||||
"entrypointType": "MpHealthCheck",
|
||||
"classFqcn": "com.example.health.DiskSpaceHealthCheck",
|
||||
"methodName": "check",
|
||||
"methodDescriptor": "()Lorg/eclipse/microprofile/health/HealthCheckResponse;",
|
||||
"framework": "mp-health"
|
||||
},
|
||||
{
|
||||
"entrypointType": "MpRestClient",
|
||||
"classFqcn": "com.example.client.PaymentServiceClient",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": "mp-rest-client"
|
||||
}
|
||||
],
|
||||
"expectedEdges": [
|
||||
{
|
||||
"edgeType": "CdiInjection",
|
||||
"source": "com.example.api.UserResource",
|
||||
"target": "com.example.service.UserService",
|
||||
"reason": "Inject",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"edgeType": "CdiInjection",
|
||||
"source": "com.example.api.OrderResource",
|
||||
"target": "com.example.service.OrderService",
|
||||
"reason": "Inject",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"edgeType": "MpRestClientCall",
|
||||
"source": "com.example.service.OrderService",
|
||||
"target": "com.example.client.PaymentServiceClient",
|
||||
"reason": "RestClientInjection",
|
||||
"confidence": "High"
|
||||
}
|
||||
],
|
||||
"expectedMetadata": {
|
||||
"framework": "microprofile",
|
||||
"serverPort": 8080,
|
||||
"configProfile": "prod",
|
||||
"healthEndpoints": {
|
||||
"liveness": "/health/live",
|
||||
"readiness": "/health/ready"
|
||||
},
|
||||
"metricsEndpoint": "/metrics"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"description": "JPMS modular application with module-info.java",
|
||||
"components": [
|
||||
{
|
||||
"jarPath": "app.jar",
|
||||
"packaging": "JpmsModule",
|
||||
"moduleInfo": {
|
||||
"moduleName": "com.example.app",
|
||||
"isOpen": false,
|
||||
"requires": ["java.base", "java.logging", "com.example.lib"],
|
||||
"exports": ["com.example.app.api"],
|
||||
"opens": ["com.example.app.internal to com.example.lib"],
|
||||
"uses": ["com.example.spi.ServiceProvider"],
|
||||
"provides": []
|
||||
},
|
||||
"manifest": {
|
||||
"Main-Class": "com.example.app.Main",
|
||||
"Automatic-Module-Name": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"jarPath": "lib.jar",
|
||||
"packaging": "JpmsModule",
|
||||
"moduleInfo": {
|
||||
"moduleName": "com.example.lib",
|
||||
"isOpen": false,
|
||||
"requires": ["java.base"],
|
||||
"exports": ["com.example.lib.util"],
|
||||
"opens": [],
|
||||
"uses": [],
|
||||
"provides": ["com.example.spi.ServiceProvider with com.example.lib.impl.DefaultProvider"]
|
||||
},
|
||||
"manifest": {
|
||||
"Main-Class": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectedEntrypoints": [
|
||||
{
|
||||
"entrypointType": "MainClass",
|
||||
"classFqcn": "com.example.app.Main",
|
||||
"methodName": "main",
|
||||
"methodDescriptor": "([Ljava/lang/String;)V",
|
||||
"framework": null
|
||||
},
|
||||
{
|
||||
"entrypointType": "ServiceProvider",
|
||||
"classFqcn": "com.example.lib.impl.DefaultProvider",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": null
|
||||
}
|
||||
],
|
||||
"expectedEdges": [
|
||||
{
|
||||
"edgeType": "JpmsRequires",
|
||||
"sourceModule": "com.example.app",
|
||||
"targetModule": "com.example.lib"
|
||||
},
|
||||
{
|
||||
"edgeType": "JpmsExports",
|
||||
"sourceModule": "com.example.app",
|
||||
"targetPackage": "com.example.app.api"
|
||||
},
|
||||
{
|
||||
"edgeType": "JpmsOpens",
|
||||
"sourceModule": "com.example.app",
|
||||
"targetPackage": "com.example.app.internal",
|
||||
"toModule": "com.example.lib"
|
||||
},
|
||||
{
|
||||
"edgeType": "JpmsUses",
|
||||
"sourceModule": "com.example.app",
|
||||
"serviceInterface": "com.example.spi.ServiceProvider"
|
||||
},
|
||||
{
|
||||
"edgeType": "JpmsProvides",
|
||||
"sourceModule": "com.example.lib",
|
||||
"serviceInterface": "com.example.spi.ServiceProvider",
|
||||
"implementation": "com.example.lib.impl.DefaultProvider"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"description": "Multi-release JAR with version-specific classes for Java 11, 17, and 21",
|
||||
"components": [
|
||||
{
|
||||
"jarPath": "multi-release-lib.jar",
|
||||
"packaging": "Jar",
|
||||
"moduleInfo": null,
|
||||
"manifest": {
|
||||
"Multi-Release": "true",
|
||||
"Main-Class": "com.example.lib.Main",
|
||||
"Implementation-Title": "Multi-Release Library",
|
||||
"Implementation-Version": "2.0.0"
|
||||
},
|
||||
"multiReleaseVersions": [11, 17, 21],
|
||||
"baseClasses": [
|
||||
"com/example/lib/Main.class",
|
||||
"com/example/lib/StringUtils.class",
|
||||
"com/example/lib/HttpClient.class"
|
||||
],
|
||||
"versionedClasses": {
|
||||
"11": [
|
||||
"META-INF/versions/11/com/example/lib/StringUtils.class",
|
||||
"META-INF/versions/11/com/example/lib/HttpClient.class"
|
||||
],
|
||||
"17": [
|
||||
"META-INF/versions/17/com/example/lib/StringUtils.class",
|
||||
"META-INF/versions/17/com/example/lib/RecordSupport.class"
|
||||
],
|
||||
"21": [
|
||||
"META-INF/versions/21/com/example/lib/VirtualThreadSupport.class",
|
||||
"META-INF/versions/21/com/example/lib/PatternMatchingUtils.class"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectedEntrypoints": [
|
||||
{
|
||||
"entrypointType": "MainClass",
|
||||
"classFqcn": "com.example.lib.Main",
|
||||
"methodName": "main",
|
||||
"methodDescriptor": "([Ljava/lang/String;)V",
|
||||
"framework": null
|
||||
}
|
||||
],
|
||||
"expectedComponents": [
|
||||
{
|
||||
"componentType": "Jar",
|
||||
"name": "multi-release-lib.jar",
|
||||
"isMultiRelease": true,
|
||||
"supportedVersions": [11, 17, 21]
|
||||
}
|
||||
],
|
||||
"expectedMetadata": {
|
||||
"multiRelease": true,
|
||||
"baseJavaVersion": 8,
|
||||
"versionSpecificOverrides": {
|
||||
"11": ["StringUtils", "HttpClient"],
|
||||
"17": ["StringUtils", "RecordSupport"],
|
||||
"21": ["VirtualThreadSupport", "PatternMatchingUtils"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"description": "Reflection-heavy application with Class.forName, ServiceLoader, and proxy patterns",
|
||||
"components": [
|
||||
{
|
||||
"jarPath": "plugin-host.jar",
|
||||
"packaging": "Jar",
|
||||
"moduleInfo": null,
|
||||
"manifest": {
|
||||
"Main-Class": "com.example.plugin.PluginHost"
|
||||
},
|
||||
"reflectionCalls": [
|
||||
{
|
||||
"sourceClass": "com.example.plugin.PluginLoader",
|
||||
"sourceMethod": "loadPlugin",
|
||||
"reflectionType": "ClassForName",
|
||||
"targetClass": null,
|
||||
"confidence": "Low"
|
||||
},
|
||||
{
|
||||
"sourceClass": "com.example.plugin.PluginLoader",
|
||||
"sourceMethod": "loadPluginClass",
|
||||
"reflectionType": "ClassForName",
|
||||
"targetClass": "com.example.plugins.DefaultPlugin",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"sourceClass": "com.example.plugin.ServiceRegistry",
|
||||
"sourceMethod": "loadServices",
|
||||
"reflectionType": "ServiceLoaderLoad",
|
||||
"targetService": "com.example.spi.Plugin",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"sourceClass": "com.example.plugin.DynamicProxy",
|
||||
"sourceMethod": "createProxy",
|
||||
"reflectionType": "ProxyNewInstance",
|
||||
"targetInterfaces": ["com.example.api.Service", "com.example.api.Lifecycle"],
|
||||
"confidence": "Medium"
|
||||
},
|
||||
{
|
||||
"sourceClass": "com.example.plugin.ConfigLoader",
|
||||
"sourceMethod": "loadConfig",
|
||||
"reflectionType": "ResourceLookup",
|
||||
"targetResource": "plugin.properties",
|
||||
"confidence": "High"
|
||||
}
|
||||
],
|
||||
"graalReflectConfig": {
|
||||
"reflect-config.json": [
|
||||
{
|
||||
"name": "com.example.plugins.DefaultPlugin",
|
||||
"allDeclaredConstructors": true,
|
||||
"allPublicMethods": true
|
||||
},
|
||||
{
|
||||
"name": "com.example.plugins.AdvancedPlugin",
|
||||
"allDeclaredConstructors": true,
|
||||
"allPublicMethods": true,
|
||||
"fields": [{"name": "config", "allowWrite": true}]
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceProviders": [
|
||||
{
|
||||
"serviceInterface": "com.example.spi.Plugin",
|
||||
"implementations": [
|
||||
"com.example.plugins.DefaultPlugin",
|
||||
"com.example.plugins.AdvancedPlugin"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"expectedEntrypoints": [
|
||||
{
|
||||
"entrypointType": "MainClass",
|
||||
"classFqcn": "com.example.plugin.PluginHost",
|
||||
"methodName": "main",
|
||||
"methodDescriptor": "([Ljava/lang/String;)V",
|
||||
"framework": null
|
||||
},
|
||||
{
|
||||
"entrypointType": "ServiceProvider",
|
||||
"classFqcn": "com.example.plugins.DefaultPlugin",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": null
|
||||
},
|
||||
{
|
||||
"entrypointType": "ServiceProvider",
|
||||
"classFqcn": "com.example.plugins.AdvancedPlugin",
|
||||
"methodName": null,
|
||||
"methodDescriptor": null,
|
||||
"framework": null
|
||||
}
|
||||
],
|
||||
"expectedEdges": [
|
||||
{
|
||||
"edgeType": "Reflection",
|
||||
"source": "com.example.plugin.PluginLoader",
|
||||
"target": "com.example.plugins.DefaultPlugin",
|
||||
"reason": "ClassForName",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"edgeType": "Reflection",
|
||||
"source": "com.example.plugin.PluginLoader",
|
||||
"target": null,
|
||||
"reason": "ClassForName",
|
||||
"confidence": "Low"
|
||||
},
|
||||
{
|
||||
"edgeType": "Spi",
|
||||
"source": "com.example.plugin.ServiceRegistry",
|
||||
"target": "com.example.spi.Plugin",
|
||||
"reason": "ServiceLoaderLoad",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"edgeType": "Spi",
|
||||
"source": "com.example.spi.Plugin",
|
||||
"target": "com.example.plugins.DefaultPlugin",
|
||||
"reason": "ServiceProviderImplementation",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"edgeType": "Spi",
|
||||
"source": "com.example.spi.Plugin",
|
||||
"target": "com.example.plugins.AdvancedPlugin",
|
||||
"reason": "ServiceProviderImplementation",
|
||||
"confidence": "High"
|
||||
},
|
||||
{
|
||||
"edgeType": "Reflection",
|
||||
"source": "com.example.plugin.DynamicProxy",
|
||||
"target": "com.example.api.Service",
|
||||
"reason": "ProxyNewInstance",
|
||||
"confidence": "Medium"
|
||||
},
|
||||
{
|
||||
"edgeType": "Resource",
|
||||
"source": "com.example.plugin.ConfigLoader",
|
||||
"target": "plugin.properties",
|
||||
"reason": "ResourceLookup",
|
||||
"confidence": "High"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"description": "Signed JAR with multiple signers and certificate chain",
|
||||
"components": [
|
||||
{
|
||||
"jarPath": "signed-library.jar",
|
||||
"packaging": "Jar",
|
||||
"moduleInfo": null,
|
||||
"manifest": {
|
||||
"Main-Class": "com.example.secure.SecureMain",
|
||||
"Implementation-Title": "Secure Library",
|
||||
"Implementation-Version": "1.0.0",
|
||||
"Implementation-Vendor": "SecureCorp Inc.",
|
||||
"Sealed": "true"
|
||||
},
|
||||
"signatures": [
|
||||
{
|
||||
"signerName": "SECURECO",
|
||||
"signatureFile": "META-INF/SECURECO.SF",
|
||||
"signatureBlock": "META-INF/SECURECO.RSA",
|
||||
"algorithm": "RSA",
|
||||
"digestAlgorithms": ["SHA-256"],
|
||||
"certificate": {
|
||||
"subject": "CN=SecureCorp Code Signing, O=SecureCorp Inc., C=US",
|
||||
"issuer": "CN=SecureCorp CA, O=SecureCorp Inc., C=US",
|
||||
"serialNumber": "1234567890ABCDEF",
|
||||
"fingerprint": "a1b2c3d4e5f6789012345678901234567890abcd1234567890abcdef12345678",
|
||||
"validFrom": "2024-01-01T00:00:00Z",
|
||||
"validTo": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
"confidence": "Complete"
|
||||
},
|
||||
{
|
||||
"signerName": "TIMESTAM",
|
||||
"signatureFile": "META-INF/TIMESTAM.SF",
|
||||
"signatureBlock": "META-INF/TIMESTAM.RSA",
|
||||
"algorithm": "RSA",
|
||||
"digestAlgorithms": ["SHA-256"],
|
||||
"certificate": {
|
||||
"subject": "CN=Timestamp Authority, O=DigiCert Inc., C=US",
|
||||
"issuer": "CN=DigiCert SHA2 Timestamp CA, O=DigiCert Inc., C=US",
|
||||
"serialNumber": "0987654321FEDCBA",
|
||||
"fingerprint": "f1e2d3c4b5a6978012345678901234567890fedc1234567890abcdef09876543",
|
||||
"validFrom": "2023-01-01T00:00:00Z",
|
||||
"validTo": "2028-01-01T00:00:00Z"
|
||||
},
|
||||
"confidence": "Complete"
|
||||
}
|
||||
],
|
||||
"sealedPackages": [
|
||||
"com.example.secure.api",
|
||||
"com.example.secure.impl"
|
||||
]
|
||||
}
|
||||
],
|
||||
"expectedEntrypoints": [
|
||||
{
|
||||
"entrypointType": "MainClass",
|
||||
"classFqcn": "com.example.secure.SecureMain",
|
||||
"methodName": "main",
|
||||
"methodDescriptor": "([Ljava/lang/String;)V",
|
||||
"framework": null
|
||||
}
|
||||
],
|
||||
"expectedComponents": [
|
||||
{
|
||||
"componentType": "Jar",
|
||||
"name": "signed-library.jar",
|
||||
"isSigned": true,
|
||||
"signerCount": 2,
|
||||
"primarySigner": {
|
||||
"subject": "CN=SecureCorp Code Signing, O=SecureCorp Inc., C=US",
|
||||
"fingerprint": "a1b2c3d4e5f6789012345678901234567890abcd1234567890abcdef12345678"
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectedMetadata": {
|
||||
"sealed": true,
|
||||
"sealedPackages": ["com.example.secure.api", "com.example.secure.impl"],
|
||||
"signatureValidation": {
|
||||
"allEntriesSigned": true,
|
||||
"signatureCount": 2,
|
||||
"digestAlgorithm": "SHA-256"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"description": "Spring Boot fat JAR with embedded dependencies",
|
||||
"components": [
|
||||
{
|
||||
"jarPath": "myapp-0.0.1-SNAPSHOT.jar",
|
||||
"packaging": "SpringBootFatJar",
|
||||
"moduleInfo": null,
|
||||
"manifest": {
|
||||
"Main-Class": "org.springframework.boot.loader.JarLauncher",
|
||||
"Start-Class": "com.example.demo.DemoApplication",
|
||||
"Spring-Boot-Version": "3.2.0",
|
||||
"Spring-Boot-Classes": "BOOT-INF/classes/",
|
||||
"Spring-Boot-Lib": "BOOT-INF/lib/",
|
||||
"Spring-Boot-Classpath-Index": "BOOT-INF/classpath.idx"
|
||||
},
|
||||
"embeddedLibs": [
|
||||
"BOOT-INF/lib/spring-core-6.1.0.jar",
|
||||
"BOOT-INF/lib/spring-context-6.1.0.jar",
|
||||
"BOOT-INF/lib/spring-boot-autoconfigure-3.2.0.jar"
|
||||
]
|
||||
}
|
||||
],
|
||||
"expectedEntrypoints": [
|
||||
{
|
||||
"entrypointType": "SpringBootApplication",
|
||||
"classFqcn": "com.example.demo.DemoApplication",
|
||||
"methodName": "main",
|
||||
"methodDescriptor": "([Ljava/lang/String;)V",
|
||||
"framework": "spring-boot"
|
||||
},
|
||||
{
|
||||
"entrypointType": "SpringBootLauncher",
|
||||
"classFqcn": "org.springframework.boot.loader.JarLauncher",
|
||||
"methodName": "main",
|
||||
"methodDescriptor": "([Ljava/lang/String;)V",
|
||||
"framework": "spring-boot"
|
||||
}
|
||||
],
|
||||
"expectedEdges": [
|
||||
{
|
||||
"edgeType": "ClassPath",
|
||||
"source": "myapp-0.0.1-SNAPSHOT.jar",
|
||||
"target": "BOOT-INF/lib/spring-core-6.1.0.jar",
|
||||
"reason": "SpringBootLib"
|
||||
},
|
||||
{
|
||||
"edgeType": "ClassPath",
|
||||
"source": "myapp-0.0.1-SNAPSHOT.jar",
|
||||
"target": "BOOT-INF/lib/spring-context-6.1.0.jar",
|
||||
"reason": "SpringBootLib"
|
||||
}
|
||||
],
|
||||
"expectedComponents": [
|
||||
{
|
||||
"componentType": "SpringBootFatJar",
|
||||
"name": "myapp-0.0.1-SNAPSHOT.jar",
|
||||
"mainClass": "org.springframework.boot.loader.JarLauncher",
|
||||
"startClass": "com.example.demo.DemoApplication"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"description": "Java EE / Jakarta EE WAR with servlets and web.xml",
|
||||
"components": [
|
||||
{
|
||||
"jarPath": "webapp.war",
|
||||
"packaging": "War",
|
||||
"moduleInfo": null,
|
||||
"manifest": {},
|
||||
"webXml": {
|
||||
"servlets": [
|
||||
{
|
||||
"servletName": "DispatcherServlet",
|
||||
"servletClass": "org.springframework.web.servlet.DispatcherServlet",
|
||||
"urlPatterns": ["/*"]
|
||||
},
|
||||
{
|
||||
"servletName": "ApiServlet",
|
||||
"servletClass": "com.example.web.ApiServlet",
|
||||
"urlPatterns": ["/api/*"]
|
||||
}
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"filterName": "encodingFilter",
|
||||
"filterClass": "org.springframework.web.filter.CharacterEncodingFilter"
|
||||
}
|
||||
],
|
||||
"listeners": [
|
||||
"org.springframework.web.context.ContextLoaderListener"
|
||||
]
|
||||
},
|
||||
"embeddedLibs": [
|
||||
"WEB-INF/lib/spring-webmvc-6.1.0.jar",
|
||||
"WEB-INF/lib/jackson-databind-2.15.0.jar"
|
||||
]
|
||||
}
|
||||
],
|
||||
"expectedEntrypoints": [
|
||||
{
|
||||
"entrypointType": "ServletClass",
|
||||
"classFqcn": "org.springframework.web.servlet.DispatcherServlet",
|
||||
"methodName": "service",
|
||||
"methodDescriptor": "(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;)V",
|
||||
"framework": "servlet"
|
||||
},
|
||||
{
|
||||
"entrypointType": "ServletClass",
|
||||
"classFqcn": "com.example.web.ApiServlet",
|
||||
"methodName": "service",
|
||||
"methodDescriptor": "(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;)V",
|
||||
"framework": "servlet"
|
||||
},
|
||||
{
|
||||
"entrypointType": "ServletFilter",
|
||||
"classFqcn": "org.springframework.web.filter.CharacterEncodingFilter",
|
||||
"methodName": "doFilter",
|
||||
"methodDescriptor": "(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;Ljavax/servlet/FilterChain;)V",
|
||||
"framework": "servlet"
|
||||
},
|
||||
{
|
||||
"entrypointType": "ServletListener",
|
||||
"classFqcn": "org.springframework.web.context.ContextLoaderListener",
|
||||
"methodName": "contextInitialized",
|
||||
"methodDescriptor": "(Ljavax/servlet/ServletContextEvent;)V",
|
||||
"framework": "servlet"
|
||||
}
|
||||
],
|
||||
"expectedComponents": [
|
||||
{
|
||||
"componentType": "War",
|
||||
"name": "webapp.war"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Jni;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Reflection;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Resolver;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Signature;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SCANNER-ANALYZERS-JAVA-21-008: Entrypoint resolver and AOC writer.
|
||||
/// </summary>
|
||||
public sealed class JavaEntrypointResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public void Resolve_EmptyClassPath_ReturnsEmpty()
|
||||
{
|
||||
var classPath = new JavaClassPathAnalysis(
|
||||
ImmutableArray<JavaClassPathSegment>.Empty,
|
||||
ImmutableArray<JavaModuleDescriptor>.Empty,
|
||||
ImmutableArray<JavaClassDuplicate>.Empty,
|
||||
ImmutableArray<JavaSplitPackage>.Empty);
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var resolution = JavaEntrypointResolver.Resolve(
|
||||
classPath,
|
||||
signatureManifest: null,
|
||||
jniAnalysis: null,
|
||||
reflectionAnalysis: null,
|
||||
cancellationToken);
|
||||
|
||||
Assert.NotNull(resolution);
|
||||
Assert.Empty(resolution.Entrypoints);
|
||||
Assert.Empty(resolution.Components);
|
||||
Assert.Empty(resolution.Edges);
|
||||
Assert.Equal(0, resolution.Statistics.TotalEntrypoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_WithManifestMainClass_CreatesEntrypoint()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "app.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Main-Class: com.example.MainApp\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
|
||||
// Load archive for signature/manifest analysis
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/app.jar");
|
||||
var signatureManifest = JavaSignatureManifestAnalyzer.Analyze(javaArchive, cancellationToken);
|
||||
|
||||
var resolution = JavaEntrypointResolver.Resolve(
|
||||
classPath,
|
||||
signatureManifest,
|
||||
jniAnalysis: null,
|
||||
reflectionAnalysis: null,
|
||||
cancellationToken);
|
||||
|
||||
Assert.NotNull(resolution);
|
||||
Assert.Single(resolution.Entrypoints);
|
||||
var entrypoint = resolution.Entrypoints[0];
|
||||
Assert.Equal("com.example.MainApp", entrypoint.ClassFqcn);
|
||||
Assert.Equal("main", entrypoint.MethodName);
|
||||
Assert.Equal(JavaEntrypointType.MainClass, entrypoint.EntrypointType);
|
||||
Assert.True(entrypoint.Confidence >= 0.9);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_WithSpringBootStartClass_CreatesEntrypoint()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "boot.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Main-Class: org.springframework.boot.loader.JarLauncher\r\n");
|
||||
writer.Write("Start-Class: com.example.MyApplication\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/boot.jar");
|
||||
var signatureManifest = JavaSignatureManifestAnalyzer.Analyze(javaArchive, cancellationToken);
|
||||
|
||||
var resolution = JavaEntrypointResolver.Resolve(
|
||||
classPath,
|
||||
signatureManifest,
|
||||
jniAnalysis: null,
|
||||
reflectionAnalysis: null,
|
||||
cancellationToken);
|
||||
|
||||
Assert.NotNull(resolution);
|
||||
Assert.Equal(2, resolution.Entrypoints.Length); // Main-Class + Start-Class
|
||||
|
||||
var springEntry = resolution.Entrypoints.FirstOrDefault(e => e.EntrypointType == JavaEntrypointType.SpringBootStartClass);
|
||||
Assert.NotNull(springEntry);
|
||||
Assert.Equal("com.example.MyApplication", springEntry.ClassFqcn);
|
||||
Assert.Equal("spring-boot", springEntry.Framework);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_WithJavaAgent_CreatesAgentEntrypoints()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "agent.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Premain-Class: com.example.Agent\r\n");
|
||||
writer.Write("Agent-Class: com.example.Agent\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/agent.jar");
|
||||
var signatureManifest = JavaSignatureManifestAnalyzer.Analyze(javaArchive, cancellationToken);
|
||||
|
||||
var resolution = JavaEntrypointResolver.Resolve(
|
||||
classPath,
|
||||
signatureManifest,
|
||||
jniAnalysis: null,
|
||||
reflectionAnalysis: null,
|
||||
cancellationToken);
|
||||
|
||||
Assert.NotNull(resolution);
|
||||
Assert.Equal(2, resolution.Entrypoints.Length); // Premain + Agent
|
||||
|
||||
Assert.Contains(resolution.Entrypoints, e => e.EntrypointType == JavaEntrypointType.JavaAgentPremain);
|
||||
Assert.Contains(resolution.Entrypoints, e => e.EntrypointType == JavaEntrypointType.JavaAgentAttach);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_WithJniAnalysis_CreatesJniEdges()
|
||||
{
|
||||
var classPath = new JavaClassPathAnalysis(
|
||||
ImmutableArray<JavaClassPathSegment>.Empty,
|
||||
ImmutableArray<JavaModuleDescriptor>.Empty,
|
||||
ImmutableArray<JavaClassDuplicate>.Empty,
|
||||
ImmutableArray<JavaSplitPackage>.Empty);
|
||||
|
||||
var jniEdges = ImmutableArray.Create(
|
||||
new JavaJniEdge(
|
||||
SourceClass: "com.example.Native",
|
||||
SegmentIdentifier: "libs/native.jar",
|
||||
TargetLibrary: "mylib",
|
||||
Reason: JavaJniReason.SystemLoadLibrary,
|
||||
Confidence: JavaJniConfidence.High,
|
||||
MethodName: "loadNative",
|
||||
MethodDescriptor: "()V",
|
||||
InstructionOffset: 10,
|
||||
Details: "System.loadLibrary(\"mylib\")"));
|
||||
|
||||
var jniAnalysis = new JavaJniAnalysis(jniEdges, ImmutableArray<JavaJniWarning>.Empty);
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var resolution = JavaEntrypointResolver.Resolve(
|
||||
classPath,
|
||||
signatureManifest: null,
|
||||
jniAnalysis,
|
||||
reflectionAnalysis: null,
|
||||
cancellationToken);
|
||||
|
||||
Assert.NotNull(resolution);
|
||||
Assert.Single(resolution.Edges);
|
||||
var edge = resolution.Edges[0];
|
||||
Assert.Equal(JavaEdgeType.JniNativeLib, edge.EdgeType);
|
||||
Assert.Equal(JavaEdgeReason.SystemLoadLibrary, edge.Reason);
|
||||
Assert.True(edge.Confidence >= 0.9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_WithReflectionAnalysis_CreatesReflectionEdges()
|
||||
{
|
||||
var classPath = new JavaClassPathAnalysis(
|
||||
ImmutableArray<JavaClassPathSegment>.Empty,
|
||||
ImmutableArray<JavaModuleDescriptor>.Empty,
|
||||
ImmutableArray<JavaClassDuplicate>.Empty,
|
||||
ImmutableArray<JavaSplitPackage>.Empty);
|
||||
|
||||
var reflectEdges = ImmutableArray.Create(
|
||||
new JavaReflectionEdge(
|
||||
SourceClass: "com.example.Loader",
|
||||
SegmentIdentifier: "libs/app.jar",
|
||||
TargetType: "com.example.Plugin",
|
||||
Reason: JavaReflectionReason.ClassForName,
|
||||
Confidence: JavaReflectionConfidence.High,
|
||||
MethodName: "loadPlugin",
|
||||
MethodDescriptor: "()V",
|
||||
InstructionOffset: 20,
|
||||
Details: "Class.forName(\"com.example.Plugin\")"));
|
||||
|
||||
var reflectionAnalysis = new JavaReflectionAnalysis(reflectEdges, ImmutableArray<JavaReflectionWarning>.Empty);
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var resolution = JavaEntrypointResolver.Resolve(
|
||||
classPath,
|
||||
signatureManifest: null,
|
||||
jniAnalysis: null,
|
||||
reflectionAnalysis,
|
||||
cancellationToken);
|
||||
|
||||
Assert.NotNull(resolution);
|
||||
Assert.Single(resolution.Edges);
|
||||
var edge = resolution.Edges[0];
|
||||
Assert.Equal(JavaEdgeType.ReflectionLoad, edge.EdgeType);
|
||||
Assert.Equal(JavaEdgeReason.ClassForName, edge.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_WithClassPathManifest_CreatesClassPathEdges()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "app.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Main-Class: com.example.App\r\n");
|
||||
writer.Write("Class-Path: lib/dep1.jar lib/dep2.jar\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/app.jar");
|
||||
var signatureManifest = JavaSignatureManifestAnalyzer.Analyze(javaArchive, cancellationToken);
|
||||
|
||||
var resolution = JavaEntrypointResolver.Resolve(
|
||||
classPath,
|
||||
signatureManifest,
|
||||
jniAnalysis: null,
|
||||
reflectionAnalysis: null,
|
||||
cancellationToken);
|
||||
|
||||
Assert.NotNull(resolution);
|
||||
|
||||
// Should have 2 classpath edges (lib/dep1.jar, lib/dep2.jar)
|
||||
var cpEdges = resolution.Edges.Where(e => e.EdgeType == JavaEdgeType.ClasspathDependency).ToList();
|
||||
Assert.Equal(2, cpEdges.Count);
|
||||
Assert.All(cpEdges, e => Assert.Equal(JavaEdgeReason.ManifestClassPath, e.Reason));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Statistics_AreCalculatedCorrectly()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "app.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Main-Class: com.example.MainApp\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/app.jar");
|
||||
var signatureManifest = JavaSignatureManifestAnalyzer.Analyze(javaArchive, cancellationToken);
|
||||
|
||||
var resolution = JavaEntrypointResolver.Resolve(
|
||||
classPath,
|
||||
signatureManifest,
|
||||
jniAnalysis: null,
|
||||
reflectionAnalysis: null,
|
||||
cancellationToken);
|
||||
|
||||
Assert.NotNull(resolution.Statistics);
|
||||
Assert.Equal(resolution.Entrypoints.Length, resolution.Statistics.TotalEntrypoints);
|
||||
Assert.Equal(resolution.Components.Length, resolution.Statistics.TotalComponents);
|
||||
Assert.Equal(resolution.Edges.Length, resolution.Statistics.TotalEdges);
|
||||
Assert.True(resolution.Statistics.ResolutionDuration.TotalMilliseconds >= 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AocWriter_WritesValidNdjson()
|
||||
{
|
||||
var resolution = new JavaEntrypointResolution(
|
||||
Entrypoints: ImmutableArray.Create(
|
||||
new JavaResolvedEntrypoint(
|
||||
EntrypointId: "entry:12345678",
|
||||
ClassFqcn: "com.example.Main",
|
||||
MethodName: "main",
|
||||
MethodDescriptor: "([Ljava/lang/String;)V",
|
||||
EntrypointType: JavaEntrypointType.MainClass,
|
||||
SegmentIdentifier: "app.jar",
|
||||
Framework: null,
|
||||
Confidence: 0.95,
|
||||
ResolutionPath: ImmutableArray.Create("manifest:Main-Class"),
|
||||
Metadata: null)),
|
||||
Components: ImmutableArray.Create(
|
||||
new JavaResolvedComponent(
|
||||
ComponentId: "component:abcdef00",
|
||||
SegmentIdentifier: "app.jar",
|
||||
ComponentType: JavaComponentType.Jar,
|
||||
Name: "app",
|
||||
Version: "1.0.0",
|
||||
IsSigned: false,
|
||||
SignerFingerprint: null,
|
||||
MainClass: "com.example.Main",
|
||||
ModuleInfo: null)),
|
||||
Edges: ImmutableArray<JavaResolvedEdge>.Empty,
|
||||
Statistics: JavaResolutionStatistics.Empty,
|
||||
Warnings: ImmutableArray<JavaResolutionWarning>.Empty);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
await JavaEntrypointAocWriter.WriteNdjsonAsync(
|
||||
resolution,
|
||||
tenantId: "test-tenant",
|
||||
scanId: "scan-001",
|
||||
stream,
|
||||
cancellationToken);
|
||||
|
||||
stream.Position = 0;
|
||||
using var reader = new StreamReader(stream);
|
||||
var content = await reader.ReadToEndAsync(cancellationToken);
|
||||
|
||||
// Verify NDJSON format (one JSON object per line)
|
||||
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
Assert.True(lines.Length >= 4); // header + component + entrypoint + footer
|
||||
|
||||
// Verify each line is valid JSON
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var doc = System.Text.Json.JsonDocument.Parse(line);
|
||||
Assert.NotNull(doc.RootElement.GetProperty("recordType").GetString());
|
||||
}
|
||||
|
||||
// Verify header
|
||||
var headerDoc = System.Text.Json.JsonDocument.Parse(lines[0]);
|
||||
Assert.Equal("header", headerDoc.RootElement.GetProperty("recordType").GetString());
|
||||
Assert.Equal("test-tenant", headerDoc.RootElement.GetProperty("tenantId").GetString());
|
||||
|
||||
// Verify footer
|
||||
var footerDoc = System.Text.Json.JsonDocument.Parse(lines[^1]);
|
||||
Assert.Equal("footer", footerDoc.RootElement.GetProperty("recordType").GetString());
|
||||
Assert.StartsWith("sha256:", footerDoc.RootElement.GetProperty("contentHash").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentHash_IsDeterministic()
|
||||
{
|
||||
var resolution = new JavaEntrypointResolution(
|
||||
Entrypoints: ImmutableArray.Create(
|
||||
new JavaResolvedEntrypoint(
|
||||
EntrypointId: "entry:12345678",
|
||||
ClassFqcn: "com.example.Main",
|
||||
MethodName: "main",
|
||||
MethodDescriptor: "([Ljava/lang/String;)V",
|
||||
EntrypointType: JavaEntrypointType.MainClass,
|
||||
SegmentIdentifier: "app.jar",
|
||||
Framework: null,
|
||||
Confidence: 0.95,
|
||||
ResolutionPath: ImmutableArray.Create("manifest:Main-Class"),
|
||||
Metadata: null)),
|
||||
Components: ImmutableArray<JavaResolvedComponent>.Empty,
|
||||
Edges: ImmutableArray<JavaResolvedEdge>.Empty,
|
||||
Statistics: JavaResolutionStatistics.Empty,
|
||||
Warnings: ImmutableArray<JavaResolutionWarning>.Empty);
|
||||
|
||||
var hash1 = JavaEntrypointAocWriter.ComputeContentHash(resolution);
|
||||
var hash2 = JavaEntrypointAocWriter.ComputeContentHash(resolution);
|
||||
|
||||
Assert.Equal(hash1, hash2);
|
||||
Assert.StartsWith("sha256:", hash1);
|
||||
Assert.Equal(71, hash1.Length); // "sha256:" + 64 hex chars
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
using System.IO.Compression;
|
||||
using System.Threading;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Jni;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SCANNER-ANALYZERS-JAVA-21-006: JNI/native hint scanner with edge emission.
|
||||
/// </summary>
|
||||
public sealed class JavaJniAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Analyze_NativeMethod_ProducesEdge()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "jni.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var entry = archive.CreateEntry("com/example/Native.class");
|
||||
var bytes = JavaClassFileFactory.CreateNativeMethodClass("com/example/Native", "nativeMethod0");
|
||||
using var stream = entry.Open();
|
||||
stream.Write(bytes);
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
var analysis = JavaJniAnalyzer.Analyze(classPath, cancellationToken);
|
||||
|
||||
var edge = Assert.Single(analysis.Edges);
|
||||
Assert.Equal("com.example.Native", edge.SourceClass);
|
||||
Assert.Equal(JavaJniReason.NativeMethod, edge.Reason);
|
||||
Assert.Equal(JavaJniConfidence.High, edge.Confidence);
|
||||
Assert.Equal("nativeMethod0", edge.MethodName);
|
||||
Assert.Equal("()V", edge.MethodDescriptor);
|
||||
Assert.Null(edge.TargetLibrary);
|
||||
Assert.Equal(-1, edge.InstructionOffset);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_SystemLoadLibrary_ProducesEdgeWithLibraryName()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "loader.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var entry = archive.CreateEntry("com/example/Loader.class");
|
||||
var bytes = JavaClassFileFactory.CreateSystemLoadLibraryInvoker("com/example/Loader", "nativelib");
|
||||
using var stream = entry.Open();
|
||||
stream.Write(bytes);
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
var analysis = JavaJniAnalyzer.Analyze(classPath, cancellationToken);
|
||||
|
||||
var edge = Assert.Single(analysis.Edges);
|
||||
Assert.Equal("com.example.Loader", edge.SourceClass);
|
||||
Assert.Equal(JavaJniReason.SystemLoadLibrary, edge.Reason);
|
||||
Assert.Equal(JavaJniConfidence.High, edge.Confidence);
|
||||
Assert.Equal("nativelib", edge.TargetLibrary);
|
||||
Assert.Equal("loadNative", edge.MethodName);
|
||||
Assert.True(edge.InstructionOffset >= 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_SystemLoad_ProducesEdgeWithPath()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "pathloader.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var entry = archive.CreateEntry("com/example/PathLoader.class");
|
||||
var bytes = JavaClassFileFactory.CreateSystemLoadInvoker("com/example/PathLoader", "/usr/lib/libnative.so");
|
||||
using var stream = entry.Open();
|
||||
stream.Write(bytes);
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
var analysis = JavaJniAnalyzer.Analyze(classPath, cancellationToken);
|
||||
|
||||
var edge = Assert.Single(analysis.Edges);
|
||||
Assert.Equal("com.example.PathLoader", edge.SourceClass);
|
||||
Assert.Equal(JavaJniReason.SystemLoad, edge.Reason);
|
||||
Assert.Equal(JavaJniConfidence.High, edge.Confidence);
|
||||
Assert.Equal("/usr/lib/libnative.so", edge.TargetLibrary);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_MultipleJniUsages_ProducesMultipleEdges()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "multi.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
// Class with native method
|
||||
var nativeEntry = archive.CreateEntry("com/example/NativeWrapper.class");
|
||||
var nativeBytes = JavaClassFileFactory.CreateNativeMethodClass("com/example/NativeWrapper", "init");
|
||||
using (var stream = nativeEntry.Open())
|
||||
{
|
||||
stream.Write(nativeBytes);
|
||||
}
|
||||
|
||||
// Class with loadLibrary
|
||||
var loaderEntry = archive.CreateEntry("com/example/LibLoader.class");
|
||||
var loaderBytes = JavaClassFileFactory.CreateSystemLoadLibraryInvoker("com/example/LibLoader", "jniwrapper");
|
||||
using (var stream = loaderEntry.Open())
|
||||
{
|
||||
stream.Write(loaderBytes);
|
||||
}
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
var analysis = JavaJniAnalyzer.Analyze(classPath, cancellationToken);
|
||||
|
||||
Assert.Equal(2, analysis.Edges.Length);
|
||||
Assert.Contains(analysis.Edges, e => e.Reason == JavaJniReason.NativeMethod && e.SourceClass == "com.example.NativeWrapper");
|
||||
Assert.Contains(analysis.Edges, e => e.Reason == JavaJniReason.SystemLoadLibrary && e.TargetLibrary == "jniwrapper");
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_EmptyClassPath_ReturnsEmpty()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
var analysis = JavaJniAnalyzer.Analyze(classPath, cancellationToken);
|
||||
|
||||
Assert.Same(JavaJniAnalysis.Empty, analysis);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_EdgesIncludeReasonCodesAndConfidence()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "reasons.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var entry = archive.CreateEntry("com/example/JniClass.class");
|
||||
var bytes = JavaClassFileFactory.CreateSystemLoadLibraryInvoker("com/example/JniClass", "mylib");
|
||||
using var stream = entry.Open();
|
||||
stream.Write(bytes);
|
||||
}
|
||||
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
|
||||
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
|
||||
var analysis = JavaJniAnalyzer.Analyze(classPath, cancellationToken);
|
||||
|
||||
var edge = Assert.Single(analysis.Edges);
|
||||
|
||||
// Verify reason code is set
|
||||
Assert.Equal(JavaJniReason.SystemLoadLibrary, edge.Reason);
|
||||
|
||||
// Verify confidence is set
|
||||
Assert.Equal(JavaJniConfidence.High, edge.Confidence);
|
||||
|
||||
// Verify details are present
|
||||
Assert.NotNull(edge.Details);
|
||||
Assert.Contains("mylib", edge.Details);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Resolver;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Fixture-based tests for SCANNER-ANALYZERS-JAVA-21-009: Comprehensive fixtures with golden outputs.
|
||||
/// Each fixture tests a specific Java packaging scenario (modular, Spring Boot, WAR, EAR, etc.).
|
||||
/// </summary>
|
||||
public sealed class JavaResolverFixtureTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
private static readonly string FixturesBasePath = Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"Fixtures",
|
||||
"java",
|
||||
"resolver");
|
||||
|
||||
/// <summary>
|
||||
/// Tests JPMS modular application with module-info declarations.
|
||||
/// Verifies module requires/exports/opens/uses/provides edges.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Fixture_ModularApp_JpmsEdgesResolved()
|
||||
{
|
||||
var fixture = LoadFixture("modular-app");
|
||||
|
||||
// Verify expected entrypoint types
|
||||
var mainClassEntrypoint = fixture.ExpectedEntrypoints?
|
||||
.FirstOrDefault(e => e.EntrypointType == "MainClass");
|
||||
Assert.NotNull(mainClassEntrypoint);
|
||||
Assert.Equal("com.example.app.Main", mainClassEntrypoint.ClassFqcn);
|
||||
|
||||
var serviceProviderEntrypoint = fixture.ExpectedEntrypoints?
|
||||
.FirstOrDefault(e => e.EntrypointType == "ServiceProvider");
|
||||
Assert.NotNull(serviceProviderEntrypoint);
|
||||
Assert.Equal("com.example.lib.impl.DefaultProvider", serviceProviderEntrypoint.ClassFqcn);
|
||||
|
||||
// Verify JPMS edge types
|
||||
Assert.NotNull(fixture.ExpectedEdges);
|
||||
Assert.Contains(fixture.ExpectedEdges, e => e.EdgeType == "JpmsRequires");
|
||||
Assert.Contains(fixture.ExpectedEdges, e => e.EdgeType == "JpmsExports");
|
||||
Assert.Contains(fixture.ExpectedEdges, e => e.EdgeType == "JpmsOpens");
|
||||
Assert.Contains(fixture.ExpectedEdges, e => e.EdgeType == "JpmsUses");
|
||||
Assert.Contains(fixture.ExpectedEdges, e => e.EdgeType == "JpmsProvides");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests Spring Boot fat JAR with embedded dependencies.
|
||||
/// Verifies Start-Class entrypoint and Spring Boot loader detection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Fixture_SpringBootFat_StartClassResolved()
|
||||
{
|
||||
var fixture = LoadFixture("spring-boot-fat");
|
||||
|
||||
// Verify Spring Boot application entrypoint
|
||||
var springBootApp = fixture.ExpectedEntrypoints?
|
||||
.FirstOrDefault(e => e.EntrypointType == "SpringBootApplication");
|
||||
Assert.NotNull(springBootApp);
|
||||
Assert.Equal("com.example.demo.DemoApplication", springBootApp.ClassFqcn);
|
||||
Assert.Equal("spring-boot", springBootApp.Framework);
|
||||
|
||||
// Verify component type
|
||||
var component = fixture.ExpectedComponents?.FirstOrDefault();
|
||||
Assert.NotNull(component);
|
||||
Assert.Equal("SpringBootFatJar", component.ComponentType);
|
||||
Assert.Equal("com.example.demo.DemoApplication", component.StartClass);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests WAR archive with servlets, filters, and listeners from web.xml.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Fixture_War_ServletEntrypointsResolved()
|
||||
{
|
||||
var fixture = LoadFixture("war");
|
||||
|
||||
// Verify servlet entrypoints
|
||||
var servletEntrypoints = fixture.ExpectedEntrypoints?
|
||||
.Where(e => e.EntrypointType == "ServletClass")
|
||||
.ToList();
|
||||
Assert.NotNull(servletEntrypoints);
|
||||
Assert.Equal(2, servletEntrypoints.Count);
|
||||
|
||||
// Verify filter entrypoint
|
||||
var filterEntrypoint = fixture.ExpectedEntrypoints?
|
||||
.FirstOrDefault(e => e.EntrypointType == "ServletFilter");
|
||||
Assert.NotNull(filterEntrypoint);
|
||||
|
||||
// Verify listener entrypoint
|
||||
var listenerEntrypoint = fixture.ExpectedEntrypoints?
|
||||
.FirstOrDefault(e => e.EntrypointType == "ServletListener");
|
||||
Assert.NotNull(listenerEntrypoint);
|
||||
|
||||
// Verify component type
|
||||
var component = fixture.ExpectedComponents?.FirstOrDefault();
|
||||
Assert.NotNull(component);
|
||||
Assert.Equal("War", component.ComponentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests EAR archive with EJB modules and embedded WARs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Fixture_Ear_EjbEntrypointsResolved()
|
||||
{
|
||||
var fixture = LoadFixture("ear");
|
||||
|
||||
// Verify EJB session beans
|
||||
var sessionBeans = fixture.ExpectedEntrypoints?
|
||||
.Where(e => e.EntrypointType == "EjbSessionBean")
|
||||
.ToList();
|
||||
Assert.NotNull(sessionBeans);
|
||||
Assert.Equal(2, sessionBeans.Count);
|
||||
|
||||
// Verify message-driven bean
|
||||
var mdb = fixture.ExpectedEntrypoints?
|
||||
.FirstOrDefault(e => e.EntrypointType == "EjbMessageDrivenBean");
|
||||
Assert.NotNull(mdb);
|
||||
Assert.Equal("onMessage", mdb.MethodName);
|
||||
|
||||
// Verify EAR module edges
|
||||
var earModuleEdges = fixture.ExpectedEdges?
|
||||
.Where(e => e.EdgeType == "EarModule")
|
||||
.ToList();
|
||||
Assert.NotNull(earModuleEdges);
|
||||
Assert.Equal(2, earModuleEdges.Count);
|
||||
|
||||
// Verify component types
|
||||
Assert.NotNull(fixture.ExpectedComponents);
|
||||
Assert.Contains(fixture.ExpectedComponents, c => c.ComponentType == "Ear");
|
||||
Assert.Contains(fixture.ExpectedComponents, c => c.ComponentType == "War");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests multi-release JAR with version-specific classes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Fixture_MultiRelease_VersionedClassesDetected()
|
||||
{
|
||||
var fixture = LoadFixture("multi-release");
|
||||
|
||||
// Verify component is marked as multi-release
|
||||
var component = fixture.ExpectedComponents?.FirstOrDefault();
|
||||
Assert.NotNull(component);
|
||||
Assert.True(component.IsMultiRelease);
|
||||
Assert.NotNull(component.SupportedVersions);
|
||||
Assert.Contains(11, component.SupportedVersions);
|
||||
Assert.Contains(17, component.SupportedVersions);
|
||||
Assert.Contains(21, component.SupportedVersions);
|
||||
|
||||
// Verify expected metadata
|
||||
Assert.NotNull(fixture.ExpectedMetadata);
|
||||
Assert.True(fixture.ExpectedMetadata.TryGetProperty("multiRelease", out var mrProp));
|
||||
Assert.True(mrProp.GetBoolean());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests JNI-heavy application with native methods and System.load calls.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Fixture_JniHeavy_NativeEdgesResolved()
|
||||
{
|
||||
var fixture = LoadFixture("jni-heavy");
|
||||
|
||||
// Verify native method entrypoints
|
||||
var nativeMethods = fixture.ExpectedEntrypoints?
|
||||
.Where(e => e.EntrypointType == "NativeMethod")
|
||||
.ToList();
|
||||
Assert.NotNull(nativeMethods);
|
||||
Assert.Equal(3, nativeMethods.Count);
|
||||
|
||||
// Verify JNI load edges
|
||||
var jniLoadEdges = fixture.ExpectedEdges?
|
||||
.Where(e => e.EdgeType == "JniLoad")
|
||||
.ToList();
|
||||
Assert.NotNull(jniLoadEdges);
|
||||
Assert.True(jniLoadEdges.Count >= 2);
|
||||
Assert.Contains(jniLoadEdges, e => e.Reason == "SystemLoadLibrary");
|
||||
Assert.Contains(jniLoadEdges, e => e.Reason == "SystemLoad");
|
||||
|
||||
// Verify bundled native lib edges
|
||||
var bundledLibEdges = fixture.ExpectedEdges?
|
||||
.Where(e => e.EdgeType == "JniBundledLib")
|
||||
.ToList();
|
||||
Assert.NotNull(bundledLibEdges);
|
||||
Assert.True(bundledLibEdges.Count >= 1);
|
||||
|
||||
// Verify Graal JNI config edge
|
||||
var graalEdges = fixture.ExpectedEdges?
|
||||
.Where(e => e.EdgeType == "JniGraalConfig")
|
||||
.ToList();
|
||||
Assert.NotNull(graalEdges);
|
||||
Assert.True(graalEdges.Count >= 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests reflection-heavy application with Class.forName and ServiceLoader.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Fixture_ReflectionHeavy_ReflectionEdgesResolved()
|
||||
{
|
||||
var fixture = LoadFixture("reflection-heavy");
|
||||
|
||||
// Verify service provider entrypoints
|
||||
var serviceProviders = fixture.ExpectedEntrypoints?
|
||||
.Where(e => e.EntrypointType == "ServiceProvider")
|
||||
.ToList();
|
||||
Assert.NotNull(serviceProviders);
|
||||
Assert.Equal(2, serviceProviders.Count);
|
||||
|
||||
// Verify reflection edges
|
||||
var reflectionEdges = fixture.ExpectedEdges?
|
||||
.Where(e => e.EdgeType == "Reflection")
|
||||
.ToList();
|
||||
Assert.NotNull(reflectionEdges);
|
||||
Assert.Contains(reflectionEdges, e => e.Reason == "ClassForName");
|
||||
Assert.Contains(reflectionEdges, e => e.Reason == "ProxyNewInstance");
|
||||
|
||||
// Verify SPI edges
|
||||
var spiEdges = fixture.ExpectedEdges?
|
||||
.Where(e => e.EdgeType == "Spi")
|
||||
.ToList();
|
||||
Assert.NotNull(spiEdges);
|
||||
Assert.Contains(spiEdges, e => e.Reason == "ServiceLoaderLoad");
|
||||
Assert.Contains(spiEdges, e => e.Reason == "ServiceProviderImplementation");
|
||||
|
||||
// Verify resource lookup edges
|
||||
var resourceEdges = fixture.ExpectedEdges?
|
||||
.Where(e => e.EdgeType == "Resource")
|
||||
.ToList();
|
||||
Assert.NotNull(resourceEdges);
|
||||
Assert.True(resourceEdges.Count >= 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests signed JAR with certificate information.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Fixture_SignedJar_SignatureMetadataResolved()
|
||||
{
|
||||
var fixture = LoadFixture("signed-jar");
|
||||
|
||||
// Verify component is marked as signed
|
||||
var component = fixture.ExpectedComponents?.FirstOrDefault();
|
||||
Assert.NotNull(component);
|
||||
Assert.True(component.IsSigned);
|
||||
Assert.Equal(2, component.SignerCount);
|
||||
|
||||
// Verify primary signer info
|
||||
Assert.NotNull(component.PrimarySigner);
|
||||
Assert.Contains("SecureCorp", component.PrimarySigner.Subject);
|
||||
|
||||
// Verify sealed packages metadata
|
||||
Assert.NotNull(fixture.ExpectedMetadata);
|
||||
Assert.True(fixture.ExpectedMetadata.TryGetProperty("sealed", out var sealedProp));
|
||||
Assert.True(sealedProp.GetBoolean());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests MicroProfile application with JAX-RS, CDI, and MP Health.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Fixture_Microprofile_MpEntrypointsResolved()
|
||||
{
|
||||
var fixture = LoadFixture("microprofile");
|
||||
|
||||
// Verify JAX-RS resource entrypoints
|
||||
var jaxRsResources = fixture.ExpectedEntrypoints?
|
||||
.Where(e => e.EntrypointType == "JaxRsResource")
|
||||
.ToList();
|
||||
Assert.NotNull(jaxRsResources);
|
||||
Assert.Equal(2, jaxRsResources.Count);
|
||||
Assert.Contains(jaxRsResources, e => e.ClassFqcn == "com.example.api.UserResource");
|
||||
|
||||
// Verify CDI bean entrypoints
|
||||
var cdiBeans = fixture.ExpectedEntrypoints?
|
||||
.Where(e => e.EntrypointType == "CdiBean")
|
||||
.ToList();
|
||||
Assert.NotNull(cdiBeans);
|
||||
Assert.Equal(2, cdiBeans.Count);
|
||||
|
||||
// Verify MP health check entrypoints
|
||||
var healthChecks = fixture.ExpectedEntrypoints?
|
||||
.Where(e => e.EntrypointType == "MpHealthCheck")
|
||||
.ToList();
|
||||
Assert.NotNull(healthChecks);
|
||||
Assert.Equal(2, healthChecks.Count);
|
||||
|
||||
// Verify MP REST client entrypoint
|
||||
var restClient = fixture.ExpectedEntrypoints?
|
||||
.FirstOrDefault(e => e.EntrypointType == "MpRestClient");
|
||||
Assert.NotNull(restClient);
|
||||
|
||||
// Verify CDI injection edges
|
||||
var cdiEdges = fixture.ExpectedEdges?
|
||||
.Where(e => e.EdgeType == "CdiInjection")
|
||||
.ToList();
|
||||
Assert.NotNull(cdiEdges);
|
||||
Assert.True(cdiEdges.Count >= 2);
|
||||
}
|
||||
|
||||
private static ResolverFixture LoadFixture(string fixtureName)
|
||||
{
|
||||
var fixturePath = Path.Combine(FixturesBasePath, fixtureName, "fixture.json");
|
||||
if (!File.Exists(fixturePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Fixture not found: {fixturePath}");
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(fixturePath);
|
||||
var fixture = JsonSerializer.Deserialize<ResolverFixture>(json, JsonOptions);
|
||||
return fixture ?? throw new InvalidOperationException($"Failed to deserialize fixture: {fixtureName}");
|
||||
}
|
||||
|
||||
// Fixture model classes
|
||||
private sealed record ResolverFixture(
|
||||
string? Description,
|
||||
List<FixtureComponent>? Components,
|
||||
List<FixtureEntrypoint>? ExpectedEntrypoints,
|
||||
List<FixtureEdge>? ExpectedEdges,
|
||||
List<FixtureExpectedComponent>? ExpectedComponents,
|
||||
JsonElement ExpectedMetadata);
|
||||
|
||||
private sealed record FixtureComponent(
|
||||
string? JarPath,
|
||||
string? Packaging,
|
||||
FixtureModuleInfo? ModuleInfo,
|
||||
Dictionary<string, string>? Manifest);
|
||||
|
||||
private sealed record FixtureModuleInfo(
|
||||
string? ModuleName,
|
||||
bool IsOpen,
|
||||
List<string>? Requires,
|
||||
List<string>? Exports,
|
||||
List<string>? Opens,
|
||||
List<string>? Uses,
|
||||
List<string>? Provides);
|
||||
|
||||
private sealed record FixtureEntrypoint(
|
||||
string? EntrypointType,
|
||||
string? ClassFqcn,
|
||||
string? MethodName,
|
||||
string? MethodDescriptor,
|
||||
string? Framework);
|
||||
|
||||
private sealed record FixtureEdge(
|
||||
string? EdgeType,
|
||||
string? Source,
|
||||
string? Target,
|
||||
string? SourceModule,
|
||||
string? TargetModule,
|
||||
string? TargetPackage,
|
||||
string? ToModule,
|
||||
string? ServiceInterface,
|
||||
string? Implementation,
|
||||
string? Reason,
|
||||
string? Confidence);
|
||||
|
||||
private sealed record FixtureExpectedComponent(
|
||||
string? ComponentType,
|
||||
string? Name,
|
||||
string? MainClass,
|
||||
string? StartClass,
|
||||
bool IsSigned = false,
|
||||
int SignerCount = 0,
|
||||
FixtureSigner? PrimarySigner = null,
|
||||
bool IsMultiRelease = false,
|
||||
List<int>? SupportedVersions = null);
|
||||
|
||||
private sealed record FixtureSigner(
|
||||
string? Subject,
|
||||
string? Fingerprint);
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Signature;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SCANNER-ANALYZERS-JAVA-21-007: Signature and manifest metadata collector.
|
||||
/// </summary>
|
||||
public sealed class JavaSignatureManifestAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExtractLoaderAttributes_MainClass_ReturnsMainClass()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "app.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Main-Class: com.example.MainApp\r\n");
|
||||
writer.Write("Class-Path: lib/dep1.jar lib/dep2.jar\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/app.jar");
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var attributes = JavaSignatureManifestAnalyzer.ExtractLoaderAttributes(javaArchive, cancellationToken);
|
||||
|
||||
Assert.Equal("com.example.MainApp", attributes.MainClass);
|
||||
Assert.Equal("lib/dep1.jar lib/dep2.jar", attributes.ClassPath);
|
||||
Assert.True(attributes.HasEntrypoint);
|
||||
Assert.Equal("com.example.MainApp", attributes.PrimaryEntrypoint);
|
||||
Assert.Equal(2, attributes.ParsedClassPath.Length);
|
||||
Assert.Contains("lib/dep1.jar", attributes.ParsedClassPath);
|
||||
Assert.Contains("lib/dep2.jar", attributes.ParsedClassPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractLoaderAttributes_SpringBootFatJar_ReturnsStartClass()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "boot.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Main-Class: org.springframework.boot.loader.JarLauncher\r\n");
|
||||
writer.Write("Start-Class: com.example.MyApplication\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/boot.jar");
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var attributes = JavaSignatureManifestAnalyzer.ExtractLoaderAttributes(javaArchive, cancellationToken);
|
||||
|
||||
Assert.Equal("org.springframework.boot.loader.JarLauncher", attributes.MainClass);
|
||||
Assert.Equal("com.example.MyApplication", attributes.StartClass);
|
||||
Assert.True(attributes.HasEntrypoint);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractLoaderAttributes_JavaAgent_ReturnsAgentClasses()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "agent.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Premain-Class: com.example.Agent\r\n");
|
||||
writer.Write("Agent-Class: com.example.Agent\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/agent.jar");
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var attributes = JavaSignatureManifestAnalyzer.ExtractLoaderAttributes(javaArchive, cancellationToken);
|
||||
|
||||
Assert.Equal("com.example.Agent", attributes.PremainClass);
|
||||
Assert.Equal("com.example.Agent", attributes.AgentClass);
|
||||
Assert.True(attributes.HasEntrypoint);
|
||||
Assert.Equal("com.example.Agent", attributes.PrimaryEntrypoint);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractLoaderAttributes_MultiRelease_ReturnsTrue()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "mrjar.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Multi-Release: true\r\n");
|
||||
writer.Write("Automatic-Module-Name: com.example.mymodule\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/mrjar.jar");
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var attributes = JavaSignatureManifestAnalyzer.ExtractLoaderAttributes(javaArchive, cancellationToken);
|
||||
|
||||
Assert.True(attributes.MultiRelease);
|
||||
Assert.Equal("com.example.mymodule", attributes.AutomaticModuleName);
|
||||
Assert.False(attributes.HasEntrypoint);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractLoaderAttributes_NoManifest_ReturnsEmpty()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "empty.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
// Create an empty class file placeholder
|
||||
var entry = archive.CreateEntry("com/example/Empty.class");
|
||||
using var stream = entry.Open();
|
||||
stream.WriteByte(0xCA);
|
||||
stream.WriteByte(0xFE);
|
||||
stream.WriteByte(0xBA);
|
||||
stream.WriteByte(0xBE);
|
||||
}
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/empty.jar");
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var attributes = JavaSignatureManifestAnalyzer.ExtractLoaderAttributes(javaArchive, cancellationToken);
|
||||
|
||||
Assert.Null(attributes.MainClass);
|
||||
Assert.Null(attributes.ClassPath);
|
||||
Assert.False(attributes.HasEntrypoint);
|
||||
Assert.False(attributes.MultiRelease);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeSignatures_SignedJar_DetectsSignature()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "signed.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
// Create manifest
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using (var stream = manifestEntry.Open())
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8))
|
||||
{
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
// Create signature file (.SF)
|
||||
var sfEntry = archive.CreateEntry("META-INF/MYAPP.SF");
|
||||
using (var stream = sfEntry.Open())
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8))
|
||||
{
|
||||
writer.Write("Signature-Version: 1.0\r\n");
|
||||
writer.Write("SHA-256-Digest-Manifest: abc123=\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
// We don't create a real .RSA file since it requires valid PKCS#7 data
|
||||
// The test verifies the signature file is detected even without block
|
||||
}
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/signed.jar");
|
||||
var warnings = System.Collections.Immutable.ImmutableArray.CreateBuilder<SignatureWarning>();
|
||||
|
||||
var signatures = JavaSignatureManifestAnalyzer.AnalyzeSignatures(javaArchive, "libs/signed.jar", warnings);
|
||||
|
||||
Assert.Single(signatures);
|
||||
var sig = signatures[0];
|
||||
Assert.Equal("MYAPP", sig.SignerName);
|
||||
Assert.Equal("META-INF/MYAPP.SF", sig.SignatureFileEntry);
|
||||
Assert.Null(sig.SignatureBlockEntry); // No .RSA file created
|
||||
Assert.Equal(SignatureAlgorithm.Unknown, sig.Algorithm);
|
||||
Assert.Equal(SignatureConfidence.Low, sig.Confidence);
|
||||
Assert.Contains("SHA-256", sig.DigestAlgorithms);
|
||||
|
||||
// Should have warning about incomplete signature
|
||||
Assert.Single(warnings);
|
||||
Assert.Equal("INCOMPLETE_SIGNATURE", warnings[0].WarningCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeSignatures_UnsignedJar_ReturnsEmpty()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "unsigned.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/unsigned.jar");
|
||||
var warnings = System.Collections.Immutable.ImmutableArray.CreateBuilder<SignatureWarning>();
|
||||
|
||||
var signatures = JavaSignatureManifestAnalyzer.AnalyzeSignatures(javaArchive, "libs/unsigned.jar", warnings);
|
||||
|
||||
Assert.Empty(signatures);
|
||||
Assert.Empty(warnings);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_ArchiveWithManifest_ReturnsAnalysis()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "libs", "app.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var stream = manifestEntry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write("Manifest-Version: 1.0\r\n");
|
||||
writer.Write("Main-Class: com.example.App\r\n");
|
||||
writer.Write("\r\n");
|
||||
}
|
||||
|
||||
var javaArchive = JavaArchive.Load(jarPath, "libs/app.jar");
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var analysis = JavaSignatureManifestAnalyzer.Analyze(javaArchive, cancellationToken);
|
||||
|
||||
Assert.NotNull(analysis);
|
||||
Assert.False(analysis.IsSigned);
|
||||
Assert.Equal("com.example.App", analysis.LoaderAttributes.MainClass);
|
||||
Assert.True(analysis.LoaderAttributes.HasEntrypoint);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManifestLoaderAttributes_Empty_HasNoEntrypoint()
|
||||
{
|
||||
var empty = ManifestLoaderAttributes.Empty;
|
||||
|
||||
Assert.Null(empty.MainClass);
|
||||
Assert.Null(empty.StartClass);
|
||||
Assert.Null(empty.AgentClass);
|
||||
Assert.Null(empty.PremainClass);
|
||||
Assert.Null(empty.ClassPath);
|
||||
Assert.False(empty.HasEntrypoint);
|
||||
Assert.Null(empty.PrimaryEntrypoint);
|
||||
Assert.Empty(empty.ParsedClassPath);
|
||||
Assert.False(empty.MultiRelease);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,10 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
<!-- Force newer versions to override transitive dependencies -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
<!-- Force newer versions to override transitive dependencies -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,11 +5,11 @@ namespace StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
public static class JavaClassFileFactory
|
||||
{
|
||||
public static byte[] CreateClassForNameInvoker(string internalClassName, string targetClassName)
|
||||
{
|
||||
using var buffer = new MemoryStream();
|
||||
using var writer = new BigEndianWriter(buffer);
|
||||
|
||||
public static byte[] CreateClassForNameInvoker(string internalClassName, string targetClassName)
|
||||
{
|
||||
using var buffer = new MemoryStream();
|
||||
using var writer = new BigEndianWriter(buffer);
|
||||
|
||||
WriteClassFileHeader(writer, constantPoolCount: 16);
|
||||
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(internalClassName); // #1
|
||||
@@ -40,50 +40,50 @@ public static class JavaClassFileFactory
|
||||
|
||||
writer.WriteUInt16(0); // class attributes
|
||||
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] CreateClassResourceLookup(string internalClassName, string resourcePath)
|
||||
{
|
||||
using var buffer = new MemoryStream();
|
||||
using var writer = new BigEndianWriter(buffer);
|
||||
|
||||
WriteClassFileHeader(writer, constantPoolCount: 20);
|
||||
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(internalClassName); // #1
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(1); // #2
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/Object"); // #3
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(3); // #4
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("load"); // #5
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("()V"); // #6
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("Code"); // #7
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(resourcePath); // #8
|
||||
writer.WriteByte((byte)ConstantTag.String); writer.WriteUInt16(8); // #9
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/ClassLoader"); // #10
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(10); // #11
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("getSystemClassLoader"); // #12
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("()Ljava/lang/ClassLoader;"); // #13
|
||||
writer.WriteByte((byte)ConstantTag.NameAndType); writer.WriteUInt16(12); writer.WriteUInt16(13); // #14
|
||||
writer.WriteByte((byte)ConstantTag.Methodref); writer.WriteUInt16(11); writer.WriteUInt16(14); // #15
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("getResource"); // #16
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("(Ljava/lang/String;)Ljava/net/URL;"); // #17
|
||||
writer.WriteByte((byte)ConstantTag.NameAndType); writer.WriteUInt16(16); writer.WriteUInt16(17); // #18
|
||||
writer.WriteByte((byte)ConstantTag.Methodref); writer.WriteUInt16(11); writer.WriteUInt16(18); // #19
|
||||
|
||||
writer.WriteUInt16(0x0001); // public
|
||||
writer.WriteUInt16(2); // this class
|
||||
writer.WriteUInt16(4); // super class
|
||||
|
||||
writer.WriteUInt16(0); // interfaces
|
||||
writer.WriteUInt16(0); // fields
|
||||
writer.WriteUInt16(1); // methods
|
||||
|
||||
WriteResourceLookupMethod(writer, methodNameIndex: 5, descriptorIndex: 6, systemLoaderMethodRefIndex: 15, stringIndex: 9, getResourceMethodRefIndex: 19);
|
||||
|
||||
writer.WriteUInt16(0); // class attributes
|
||||
|
||||
return buffer.ToArray();
|
||||
}
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] CreateClassResourceLookup(string internalClassName, string resourcePath)
|
||||
{
|
||||
using var buffer = new MemoryStream();
|
||||
using var writer = new BigEndianWriter(buffer);
|
||||
|
||||
WriteClassFileHeader(writer, constantPoolCount: 20);
|
||||
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(internalClassName); // #1
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(1); // #2
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/Object"); // #3
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(3); // #4
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("load"); // #5
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("()V"); // #6
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("Code"); // #7
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(resourcePath); // #8
|
||||
writer.WriteByte((byte)ConstantTag.String); writer.WriteUInt16(8); // #9
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/ClassLoader"); // #10
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(10); // #11
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("getSystemClassLoader"); // #12
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("()Ljava/lang/ClassLoader;"); // #13
|
||||
writer.WriteByte((byte)ConstantTag.NameAndType); writer.WriteUInt16(12); writer.WriteUInt16(13); // #14
|
||||
writer.WriteByte((byte)ConstantTag.Methodref); writer.WriteUInt16(11); writer.WriteUInt16(14); // #15
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("getResource"); // #16
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("(Ljava/lang/String;)Ljava/net/URL;"); // #17
|
||||
writer.WriteByte((byte)ConstantTag.NameAndType); writer.WriteUInt16(16); writer.WriteUInt16(17); // #18
|
||||
writer.WriteByte((byte)ConstantTag.Methodref); writer.WriteUInt16(11); writer.WriteUInt16(18); // #19
|
||||
|
||||
writer.WriteUInt16(0x0001); // public
|
||||
writer.WriteUInt16(2); // this class
|
||||
writer.WriteUInt16(4); // super class
|
||||
|
||||
writer.WriteUInt16(0); // interfaces
|
||||
writer.WriteUInt16(0); // fields
|
||||
writer.WriteUInt16(1); // methods
|
||||
|
||||
WriteResourceLookupMethod(writer, methodNameIndex: 5, descriptorIndex: 6, systemLoaderMethodRefIndex: 15, stringIndex: 9, getResourceMethodRefIndex: 19);
|
||||
|
||||
writer.WriteUInt16(0); // class attributes
|
||||
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] CreateTcclChecker(string internalClassName)
|
||||
{
|
||||
@@ -161,11 +161,11 @@ public static class JavaClassFileFactory
|
||||
writer.WriteBytes(codeBytes);
|
||||
}
|
||||
|
||||
private static void WriteTcclMethod(BigEndianWriter writer, ushort methodNameIndex, ushort descriptorIndex, ushort currentThreadMethodRefIndex, ushort getContextMethodRefIndex)
|
||||
{
|
||||
writer.WriteUInt16(0x0009);
|
||||
writer.WriteUInt16(methodNameIndex);
|
||||
writer.WriteUInt16(descriptorIndex);
|
||||
private static void WriteTcclMethod(BigEndianWriter writer, ushort methodNameIndex, ushort descriptorIndex, ushort currentThreadMethodRefIndex, ushort getContextMethodRefIndex)
|
||||
{
|
||||
writer.WriteUInt16(0x0009);
|
||||
writer.WriteUInt16(methodNameIndex);
|
||||
writer.WriteUInt16(descriptorIndex);
|
||||
writer.WriteUInt16(1);
|
||||
|
||||
writer.WriteUInt16(7);
|
||||
@@ -186,46 +186,46 @@ public static class JavaClassFileFactory
|
||||
}
|
||||
|
||||
var codeBytes = codeBuffer.ToArray();
|
||||
writer.WriteUInt32((uint)codeBytes.Length);
|
||||
writer.WriteBytes(codeBytes);
|
||||
}
|
||||
|
||||
private static void WriteResourceLookupMethod(
|
||||
BigEndianWriter writer,
|
||||
ushort methodNameIndex,
|
||||
ushort descriptorIndex,
|
||||
ushort systemLoaderMethodRefIndex,
|
||||
ushort stringIndex,
|
||||
ushort getResourceMethodRefIndex)
|
||||
{
|
||||
writer.WriteUInt16(0x0009);
|
||||
writer.WriteUInt16(methodNameIndex);
|
||||
writer.WriteUInt16(descriptorIndex);
|
||||
writer.WriteUInt16(1);
|
||||
|
||||
writer.WriteUInt16(7);
|
||||
using var codeBuffer = new MemoryStream();
|
||||
using (var codeWriter = new BigEndianWriter(codeBuffer))
|
||||
{
|
||||
codeWriter.WriteUInt16(2);
|
||||
codeWriter.WriteUInt16(0);
|
||||
codeWriter.WriteUInt32(10);
|
||||
codeWriter.WriteByte(0xB8); // invokestatic
|
||||
codeWriter.WriteUInt16(systemLoaderMethodRefIndex);
|
||||
codeWriter.WriteByte(0x12); // ldc
|
||||
codeWriter.WriteByte((byte)stringIndex);
|
||||
codeWriter.WriteByte(0xB6); // invokevirtual
|
||||
codeWriter.WriteUInt16(getResourceMethodRefIndex);
|
||||
codeWriter.WriteByte(0x57);
|
||||
codeWriter.WriteByte(0xB1);
|
||||
codeWriter.WriteUInt16(0);
|
||||
codeWriter.WriteUInt16(0);
|
||||
}
|
||||
|
||||
var codeBytes = codeBuffer.ToArray();
|
||||
writer.WriteUInt32((uint)codeBytes.Length);
|
||||
writer.WriteBytes(codeBytes);
|
||||
}
|
||||
writer.WriteUInt32((uint)codeBytes.Length);
|
||||
writer.WriteBytes(codeBytes);
|
||||
}
|
||||
|
||||
private static void WriteResourceLookupMethod(
|
||||
BigEndianWriter writer,
|
||||
ushort methodNameIndex,
|
||||
ushort descriptorIndex,
|
||||
ushort systemLoaderMethodRefIndex,
|
||||
ushort stringIndex,
|
||||
ushort getResourceMethodRefIndex)
|
||||
{
|
||||
writer.WriteUInt16(0x0009);
|
||||
writer.WriteUInt16(methodNameIndex);
|
||||
writer.WriteUInt16(descriptorIndex);
|
||||
writer.WriteUInt16(1);
|
||||
|
||||
writer.WriteUInt16(7);
|
||||
using var codeBuffer = new MemoryStream();
|
||||
using (var codeWriter = new BigEndianWriter(codeBuffer))
|
||||
{
|
||||
codeWriter.WriteUInt16(2);
|
||||
codeWriter.WriteUInt16(0);
|
||||
codeWriter.WriteUInt32(10);
|
||||
codeWriter.WriteByte(0xB8); // invokestatic
|
||||
codeWriter.WriteUInt16(systemLoaderMethodRefIndex);
|
||||
codeWriter.WriteByte(0x12); // ldc
|
||||
codeWriter.WriteByte((byte)stringIndex);
|
||||
codeWriter.WriteByte(0xB6); // invokevirtual
|
||||
codeWriter.WriteUInt16(getResourceMethodRefIndex);
|
||||
codeWriter.WriteByte(0x57);
|
||||
codeWriter.WriteByte(0xB1);
|
||||
codeWriter.WriteUInt16(0);
|
||||
codeWriter.WriteUInt16(0);
|
||||
}
|
||||
|
||||
var codeBytes = codeBuffer.ToArray();
|
||||
writer.WriteUInt32((uint)codeBytes.Length);
|
||||
writer.WriteBytes(codeBytes);
|
||||
}
|
||||
|
||||
private sealed class BigEndianWriter : IDisposable
|
||||
{
|
||||
@@ -264,6 +264,153 @@ public static class JavaClassFileFactory
|
||||
public void Dispose() => _writer.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a class file with a native method declaration.
|
||||
/// </summary>
|
||||
public static byte[] CreateNativeMethodClass(string internalClassName, string nativeMethodName)
|
||||
{
|
||||
using var buffer = new MemoryStream();
|
||||
using var writer = new BigEndianWriter(buffer);
|
||||
|
||||
WriteClassFileHeader(writer, constantPoolCount: 8);
|
||||
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(internalClassName); // #1
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(1); // #2
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/Object"); // #3
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(3); // #4
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(nativeMethodName); // #5
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("()V"); // #6
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("Code"); // #7
|
||||
|
||||
writer.WriteUInt16(0x0001); // public
|
||||
writer.WriteUInt16(2); // this class
|
||||
writer.WriteUInt16(4); // super class
|
||||
|
||||
writer.WriteUInt16(0); // interfaces
|
||||
writer.WriteUInt16(0); // fields
|
||||
writer.WriteUInt16(1); // methods
|
||||
|
||||
// native method: access_flags = ACC_PUBLIC | ACC_NATIVE (0x0101)
|
||||
writer.WriteUInt16(0x0101);
|
||||
writer.WriteUInt16(5); // name
|
||||
writer.WriteUInt16(6); // descriptor
|
||||
writer.WriteUInt16(0); // no attributes (native methods have no Code)
|
||||
|
||||
writer.WriteUInt16(0); // class attributes
|
||||
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a class file with a System.loadLibrary call.
|
||||
/// </summary>
|
||||
public static byte[] CreateSystemLoadLibraryInvoker(string internalClassName, string libraryName)
|
||||
{
|
||||
using var buffer = new MemoryStream();
|
||||
using var writer = new BigEndianWriter(buffer);
|
||||
|
||||
WriteClassFileHeader(writer, constantPoolCount: 16);
|
||||
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(internalClassName); // #1
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(1); // #2
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/Object"); // #3
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(3); // #4
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("loadNative"); // #5
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("()V"); // #6
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("Code"); // #7
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(libraryName); // #8
|
||||
writer.WriteByte((byte)ConstantTag.String); writer.WriteUInt16(8); // #9
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/System"); // #10
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(10); // #11
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("loadLibrary"); // #12
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("(Ljava/lang/String;)V"); // #13
|
||||
writer.WriteByte((byte)ConstantTag.NameAndType); writer.WriteUInt16(12); writer.WriteUInt16(13); // #14
|
||||
writer.WriteByte((byte)ConstantTag.Methodref); writer.WriteUInt16(11); writer.WriteUInt16(14); // #15
|
||||
|
||||
writer.WriteUInt16(0x0001); // public
|
||||
writer.WriteUInt16(2); // this class
|
||||
writer.WriteUInt16(4); // super class
|
||||
|
||||
writer.WriteUInt16(0); // interfaces
|
||||
writer.WriteUInt16(0); // fields
|
||||
writer.WriteUInt16(1); // methods
|
||||
|
||||
WriteInvokeStaticMethod(writer, methodNameIndex: 5, descriptorIndex: 6, ldcIndex: 9, methodRefIndex: 15);
|
||||
|
||||
writer.WriteUInt16(0); // class attributes
|
||||
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a class file with a System.load call (loads by path).
|
||||
/// </summary>
|
||||
public static byte[] CreateSystemLoadInvoker(string internalClassName, string libraryPath)
|
||||
{
|
||||
using var buffer = new MemoryStream();
|
||||
using var writer = new BigEndianWriter(buffer);
|
||||
|
||||
WriteClassFileHeader(writer, constantPoolCount: 16);
|
||||
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(internalClassName); // #1
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(1); // #2
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/Object"); // #3
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(3); // #4
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("loadNative"); // #5
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("()V"); // #6
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("Code"); // #7
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(libraryPath); // #8
|
||||
writer.WriteByte((byte)ConstantTag.String); writer.WriteUInt16(8); // #9
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/System"); // #10
|
||||
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(10); // #11
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("load"); // #12
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("(Ljava/lang/String;)V"); // #13
|
||||
writer.WriteByte((byte)ConstantTag.NameAndType); writer.WriteUInt16(12); writer.WriteUInt16(13); // #14
|
||||
writer.WriteByte((byte)ConstantTag.Methodref); writer.WriteUInt16(11); writer.WriteUInt16(14); // #15
|
||||
|
||||
writer.WriteUInt16(0x0001); // public
|
||||
writer.WriteUInt16(2); // this class
|
||||
writer.WriteUInt16(4); // super class
|
||||
|
||||
writer.WriteUInt16(0); // interfaces
|
||||
writer.WriteUInt16(0); // fields
|
||||
writer.WriteUInt16(1); // methods
|
||||
|
||||
WriteInvokeStaticMethod(writer, methodNameIndex: 5, descriptorIndex: 6, ldcIndex: 9, methodRefIndex: 15);
|
||||
|
||||
writer.WriteUInt16(0); // class attributes
|
||||
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteInvokeStaticMethod(BigEndianWriter writer, ushort methodNameIndex, ushort descriptorIndex, ushort ldcIndex, ushort methodRefIndex)
|
||||
{
|
||||
writer.WriteUInt16(0x0009); // public static
|
||||
writer.WriteUInt16(methodNameIndex);
|
||||
writer.WriteUInt16(descriptorIndex);
|
||||
writer.WriteUInt16(1); // attributes_count
|
||||
|
||||
writer.WriteUInt16(7); // "Code"
|
||||
using var codeBuffer = new MemoryStream();
|
||||
using (var codeWriter = new BigEndianWriter(codeBuffer))
|
||||
{
|
||||
codeWriter.WriteUInt16(1); // max_stack
|
||||
codeWriter.WriteUInt16(0); // max_locals
|
||||
codeWriter.WriteUInt32(6); // code_length
|
||||
codeWriter.WriteByte(0x12); // ldc
|
||||
codeWriter.WriteByte((byte)ldcIndex);
|
||||
codeWriter.WriteByte(0xB8); // invokestatic
|
||||
codeWriter.WriteUInt16(methodRefIndex);
|
||||
codeWriter.WriteByte(0xB1); // return
|
||||
codeWriter.WriteUInt16(0); // exception table length
|
||||
codeWriter.WriteUInt16(0); // code attributes
|
||||
}
|
||||
|
||||
var codeBytes = codeBuffer.ToArray();
|
||||
writer.WriteUInt32((uint)codeBytes.Length);
|
||||
writer.WriteBytes(codeBytes);
|
||||
}
|
||||
|
||||
private enum ConstantTag : byte
|
||||
{
|
||||
Utf8 = 1,
|
||||
|
||||
Reference in New Issue
Block a user