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,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>
|
||||
|
||||
Reference in New Issue
Block a user