using System.IO.Compression; using Xunit; 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; /// /// Tests for SCANNER-ANALYZERS-JAVA-21-006: JNI/native hint scanner with edge emission. /// 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); } } }