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:
master
2025-12-10 19:13:29 +02:00
parent a3c7fe5e88
commit b7059d523e
369 changed files with 11125 additions and 14245 deletions

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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);
}
}

View File

@@ -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,
}

View File

@@ -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();
}