diff --git a/.gitignore b/.gitignore index 8dcaac49223..99d9338d5c1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ out .data/ logs /lsp/*.log -*.class *.dylib *.so diff --git a/build.gradle.kts b/build.gradle.kts index 205e0059fa2..d55860c422a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -150,3 +150,9 @@ val enableRubyFrontend: Boolean by extra { enableRubyFrontend.toBoolean() } project.logger.lifecycle("Ruby frontend is ${if (enableRubyFrontend) "enabled" else "disabled"}") + +val enableJVMFrontend: Boolean by extra { + val enableJVMFrontend: String? by project + enableJVMFrontend.toBoolean() +} +project.logger.lifecycle("JVM frontend is ${if (enableJVMFrontend) "enabled" else "disabled"}") diff --git a/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts b/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts index 7325c194819..10fef182f3c 100644 --- a/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts @@ -11,12 +11,17 @@ val enablePythonFrontend: Boolean by rootProject.extra val enableLLVMFrontend: Boolean by rootProject.extra val enableTypeScriptFrontend: Boolean by rootProject.extra val enableRubyFrontend: Boolean by rootProject.extra +val enableJVMFrontend: Boolean by rootProject.extra dependencies { if (enableJavaFrontend) { api(project(":cpg-language-java")) kover(project(":cpg-language-java")) } + if (enableJVMFrontend) { + api(project(":cpg-language-jvm")) + kover(project(":cpg-language-jvm")) + } if (enableCXXFrontend) { api(project(":cpg-language-cxx")) kover(project(":cpg-language-cxx")) diff --git a/configure_frontends.sh b/configure_frontends.sh index 7304b0b04c8..49e32337520 100755 --- a/configure_frontends.sh +++ b/configure_frontends.sh @@ -58,3 +58,5 @@ answerTypescript=$(ask "Do you want to enable the TypeScript frontend? (currentl setProperty "enableTypeScriptFrontend" $answerTypescript answerRuby=$(ask "Do you want to enable the Ruby frontend? (currently $(getProperty "enableRubyFrontend"))") setProperty "enableRubyFrontend" $answerRuby +answerJVM=$(ask "Do you want to enable the JVM frontend? (currently $(getProperty "enableJVMFrontend"))") +setProperty "enableJVMFrontend" $answerJVM diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt index 88d65ab47a1..77c6cb28f8f 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt @@ -614,6 +614,10 @@ val Node?.returns: List val Node?.assigns: List get() = this.allChildren() +/** Returns all [ProblemNode] children in this graph, starting with this [Node]. */ +val Node?.problems: List + get() = this.allChildren() + /** Returns all [Assignment] child edges in this graph, starting with this [Node]. */ val Node?.assignments: List get() { diff --git a/cpg-language-jvm/build.gradle.kts b/cpg-language-jvm/build.gradle.kts new file mode 100644 index 00000000000..e809f20033f --- /dev/null +++ b/cpg-language-jvm/build.gradle.kts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +plugins { + id("cpg.frontend-conventions") +} + +publishing { + publications { + named("cpg-language-jvm") { + pom { + artifactId = "cpg-language-jvm" + name.set("Code Property Graph - JVM bytecode Frontend") + description.set("A JVM bytecode frontend for the CPG") + } + } + } +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = listOf("-Xcontext-receivers") + } +} + +dependencies { + api(libs.bundles.sootup) +} diff --git a/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/DeclarationHandler.kt b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/DeclarationHandler.kt new file mode 100644 index 00000000000..97b57b5acdf --- /dev/null +++ b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/DeclarationHandler.kt @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.frontends.jvm + +import de.fraunhofer.aisec.cpg.frontends.Handler +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.* +import sootup.core.jimple.basic.Local +import sootup.core.model.SootClass +import sootup.core.model.SootField +import sootup.core.model.SootMethod +import sootup.java.core.JavaSootClass +import sootup.java.core.JavaSootField +import sootup.java.core.JavaSootMethod +import sootup.java.core.jimple.basic.JavaLocal + +class DeclarationHandler(frontend: JVMLanguageFrontend) : + Handler(::ProblemDeclaration, frontend) { + init { + map.put(SootClass::class.java) { handleClass(it as SootClass) } + map.put(JavaSootClass::class.java) { handleClass(it as SootClass) } + map.put(SootMethod::class.java) { handleMethod(it as SootMethod) } + map.put(JavaSootMethod::class.java) { handleMethod(it as SootMethod) } + map.put(SootField::class.java) { handleField(it as SootField) } + map.put(JavaSootField::class.java) { handleField(it as SootField) } + map.put(Local::class.java) { handleLocal(it as Local) } + map.put(JavaLocal::class.java) { handleLocal(it as Local) } + } + + private fun handleClass(sootClass: SootClass): RecordDeclaration { + val record = + newRecordDeclaration( + sootClass.getName(), + if (sootClass.isInterface()) { + "interface" + } else { + "class" + }, + rawNode = sootClass + ) + + // Collect super class + val o = sootClass.superclass + if (o.isPresent) { + record.addSuperClass(frontend.typeOf(o.get())) + } + + // Collect implemented interfaces + for (i in sootClass.interfaces) { + record.implementedInterfaces += frontend.typeOf(i) + } + + // Enter the class scope + frontend.scopeManager.enterScope(record) + + // Loop through all fields + for (sootField in sootClass.fields) { + val field = handle(sootField) + frontend.scopeManager.addDeclaration(field) + } + + // Loop through all methods + for (sootMethod in sootClass.methods) { + val method = handle(sootMethod) + frontend.scopeManager.addDeclaration(method) + } + + // Leave the class scope + frontend.scopeManager.leaveScope(record) + + return record + } + + private fun handleMethod(sootMethod: SootMethod): MethodDeclaration { + val record = frontend.scopeManager.currentRecord + + val method = + if (sootMethod.name == "") { + newConstructorDeclaration(sootMethod.name, record, rawNode = sootMethod) + } else { + newMethodDeclaration( + sootMethod.name, + sootMethod.isStatic, + frontend.scopeManager.currentRecord, + rawNode = sootMethod, + ) + } + + // Enter method scope + frontend.scopeManager.enterScope(method) + + // Add "@this" as the receiver + method.receiver = + newVariableDeclaration("@this", method.recordDeclaration?.toType() ?: unknownType()) + .implicit("@this") + frontend.scopeManager.addDeclaration(method.receiver) + + // Add method parameters + for ((index, type) in sootMethod.parameterTypes.withIndex()) { + val param = newParameterDeclaration("@parameter${index}", frontend.typeOf(type)) + frontend.scopeManager.addDeclaration(param) + } + + if (sootMethod.isConcrete) { + // Handle method body + method.body = frontend.statementHandler.handle(sootMethod.body) + } + + // Leave method scope + frontend.scopeManager.leaveScope(method) + + return method + } + + fun handleField(field: SootField): FieldDeclaration { + return newFieldDeclaration( + field.name, + frontend.typeOf(field.type), + field.modifiers.map { it.name.lowercase() }, + rawNode = field + ) + } + + private fun handleLocal(local: Local): VariableDeclaration { + return newVariableDeclaration(local.name, frontend.typeOf(local.type), rawNode = local) + } +} diff --git a/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/ExpressionHandler.kt b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/ExpressionHandler.kt new file mode 100644 index 00000000000..33b89115755 --- /dev/null +++ b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/ExpressionHandler.kt @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.frontends.jvm + +import de.fraunhofer.aisec.cpg.frontends.Handler +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* +import de.fraunhofer.aisec.cpg.graph.types.FunctionType +import de.fraunhofer.aisec.cpg.passes.SymbolResolver +import sootup.core.jimple.basic.Local +import sootup.core.jimple.basic.Value +import sootup.core.jimple.common.constant.* +import sootup.core.jimple.common.expr.* +import sootup.core.jimple.common.ref.* +import sootup.core.signatures.MethodSignature +import sootup.core.signatures.SootClassMemberSignature +import sootup.java.core.jimple.basic.JavaLocal + +class ExpressionHandler(frontend: JVMLanguageFrontend) : + Handler( + ::ProblemExpression, + frontend, + ) { + + init { + map.put(Local::class.java) { handleLocal(it as Local) } + map.put(JavaLocal::class.java) { handleLocal(it as Local) } + map.put(JThisRef::class.java) { handleThisRef(it as JThisRef) } + map.put(JParameterRef::class.java) { handleParameterRef(it as JParameterRef) } + map.put(JInstanceFieldRef::class.java) { handleInstanceFieldRef(it as JInstanceFieldRef) } + map.put(JStaticFieldRef::class.java) { handleStaticFieldRef(it as JStaticFieldRef) } + map.put(JArrayRef::class.java) { handleArrayRef(it as JArrayRef) } + map.put(JInterfaceInvokeExpr::class.java) { + handleInterfaceInvokeExpr(it as JInterfaceInvokeExpr) + } + map.put(JVirtualInvokeExpr::class.java) { + handleVirtualInvokeExpr(it as JVirtualInvokeExpr) + } + map.put(JDynamicInvokeExpr::class.java) { + handleDynamicInvokeExpr(it as JDynamicInvokeExpr) + } + map.put(JSpecialInvokeExpr::class.java) { handleSpecialInvoke(it as JSpecialInvokeExpr) } + map.put(JStaticInvokeExpr::class.java) { handleStaticInvoke(it as JStaticInvokeExpr) } + map.put(JNewExpr::class.java) { handleNewExpr(it as JNewExpr) } + map.put(JNewArrayExpr::class.java) { handleNewArrayExpr(it as JNewArrayExpr) } + map.put(JNewMultiArrayExpr::class.java) { + handleNewMultiArrayExpr(it as JNewMultiArrayExpr) + } + map.put(JCastExpr::class.java) { handleCastExpr(it as JCastExpr) } + + // Binary operators + // - Equality checks + map.put(JEqExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JNeExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JGeExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JGtExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JLeExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JLtExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + + // - Numeric comparisons + map.put(JCmpExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JCmplExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JCmpgExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + + // - Simple arithmetics + map.put(JAddExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JDivExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JMulExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JRemExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JSubExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + + // - Binary arithmetics + map.put(JAndExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JOrExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JShlExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JShrExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JUshrExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + map.put(JXorExpr::class.java) { handleAbstractBinopExpr(it as AbstractBinopExpr) } + + // Unary operator + map.put(JNegExpr::class.java) { handleNegExpr(it as JNegExpr) } + + // Special operators, which we need to model as binary/unary operators + map.put(JInstanceOfExpr::class.java) { handleInstanceOfExpr(it as JInstanceOfExpr) } + map.put(JLengthExpr::class.java) { handleLengthExpr(it as JLengthExpr) } + + // Constants + map.put(BooleanConstant::class.java) { handleBooleanConstant(it as BooleanConstant) } + map.put(FloatConstant::class.java) { handleFloatConstant(it as FloatConstant) } + map.put(DoubleConstant::class.java) { handleDoubleConstant(it as DoubleConstant) } + map.put(IntConstant::class.java) { handleIntConstant(it as IntConstant) } + map.put(LongConstant::class.java) { handleLongConstant(it as LongConstant) } + map.put(StringConstant::class.java) { handleStringConstant(it as StringConstant) } + map.put(NullConstant::class.java) { handleNullConstant(it as NullConstant) } + map.put(ClassConstant::class.java) { handleClassConstant(it as ClassConstant) } + } + + private fun handleLocal(local: Local): Expression { + // Apparently, a local can either be a reference to variable or a literal + return if (local.name.startsWith("\"")) { + val lit = newLiteral(local.name.substring(1, local.name.length - 2), rawNode = local) + lit.type = objectType("java.lang.String") + + lit + } else { + val ref = newReference(local.name, frontend.typeOf(local.type), rawNode = local) + + ref + } + } + + private fun handleThisRef(thisRef: JThisRef): Reference { + val ref = newReference("@this", frontend.typeOf(thisRef.type), rawNode = thisRef) + + return ref + } + + private fun handleParameterRef(parameterRef: JParameterRef): Reference { + val ref = + newReference( + "@parameter${parameterRef.index}", + frontend.typeOf(parameterRef.type), + rawNode = parameterRef + ) + + return ref + } + + private fun handleInstanceFieldRef(instanceFieldRef: JInstanceFieldRef): Reference { + val base = handle(instanceFieldRef.base) ?: newProblemExpression("missing base") + + val ref = + newMemberExpression( + instanceFieldRef.fieldSignature.name, + base, + frontend.typeOf(instanceFieldRef.fieldSignature.type), + rawNode = instanceFieldRef + ) + + return ref + } + + private fun handleStaticFieldRef(staticFieldRef: JStaticFieldRef) = + staticFieldRef.fieldSignature.toStaticRef() + + private fun handleArrayRef(arrayRef: JArrayRef): SubscriptExpression { + val sub = newSubscriptExpression(rawNode = arrayRef) + sub.arrayExpression = handle(arrayRef.base) ?: newProblemExpression("missing base") + sub.subscriptExpression = handle(arrayRef.index) ?: newProblemExpression("missing index") + + return sub + } + + private fun handleAbstractInstanceInvokeExpr( + invokeExpr: AbstractInstanceInvokeExpr + ): MemberCallExpression { + val base = handle(invokeExpr.base) ?: newProblemExpression("could not parse base") + // Not really necessary, but since we already have the type information, we can use it + base.type = frontend.typeOf(invokeExpr.methodSignature.declClassType) + + val callee = newMemberExpression(invokeExpr.methodSignature.name, base) + + val call = newMemberCallExpression(callee, rawNode = invokeExpr) + call.arguments = invokeExpr.args.mapNotNull { handle(it) } + + return call + } + + private fun handleVirtualInvokeExpr(invokeExpr: JVirtualInvokeExpr): MemberCallExpression { + return handleAbstractInstanceInvokeExpr(invokeExpr) + } + + private fun handleInterfaceInvokeExpr(invokeExpr: JInterfaceInvokeExpr): MemberCallExpression { + return handleAbstractInstanceInvokeExpr(invokeExpr) + } + + /** + * The difference between [JSpecialInvokeExpr] and a regular [JVirtualInvokeExpr] is that the + * invoked function is not part of the declared class, but rather it is a function of its base + * class(es). + * + * We currently can only model this as a regular call and hope that the [SymbolResolver] will + * pick the correct function. Maybe we can supply some kind of hint to the resolver to make this + * better. + */ + private fun handleSpecialInvoke(invokeExpr: JSpecialInvokeExpr): Expression { + // This is probably a constructor call + return if (invokeExpr.methodSignature.name == "") { + val type = frontend.typeOf(invokeExpr.methodSignature.declClassType) + val construct = newConstructExpression(rawNode = invokeExpr) + construct.callee = newReference(Name("", type.name)) + construct.type = type + + construct.arguments = invokeExpr.args.mapNotNull { handle(it) } + + construct + } else { + // Just a normal call + return handleAbstractInstanceInvokeExpr(invokeExpr) + } + } + + private fun handleDynamicInvokeExpr(dynamicInvokeExpr: AbstractInvokeExpr): CallExpression { + // Model this as a static call to the method. Not sure if this is really that good or if we + // want to somehow "call" the underlying bootstrap method. + // TODO(oxisto): This is actually somewhat related to a LambdaExpression, but not really + // sure ow to model this + val callee = dynamicInvokeExpr.methodSignature.toStaticRef() + val call = newCallExpression(callee, rawNode = dynamicInvokeExpr) + call.arguments = dynamicInvokeExpr.args.mapNotNull { handle(it) } + call.type = frontend.typeOf(dynamicInvokeExpr.methodSignature.type) + + return call + } + + private fun handleStaticInvoke(staticInvokeExpr: JStaticInvokeExpr): CallExpression { + val ref = staticInvokeExpr.methodSignature.toStaticRef() + + val call = newCallExpression(ref, rawNode = staticInvokeExpr) + call.arguments = staticInvokeExpr.args.mapNotNull { handle(it) } + call.type = frontend.typeOf(staticInvokeExpr.type) + + return call + } + + /** + * In the jimple IR, the "new" and the constructor calls are split into two expressions. This + * will only handle the "new" expression, a later call to "invokespecial" will handle the + * constructor call. + */ + private fun handleNewExpr(newExpr: JNewExpr) = + newNewExpression(frontend.typeOf(newExpr.type), rawNode = newExpr) + + private fun handleNewArrayExpr(newArrayExpr: JNewArrayExpr): NewArrayExpression { + val new = newNewArrayExpression(rawNode = newArrayExpr) + new.type = frontend.typeOf(newArrayExpr.type) + new.dimensions = listOfNotNull(handle(newArrayExpr.size)) + + return new + } + + private fun handleNewMultiArrayExpr(newMultiArrayExpr: JNewMultiArrayExpr): NewArrayExpression { + val new = newNewArrayExpression(rawNode = newMultiArrayExpr) + new.type = frontend.typeOf(newMultiArrayExpr.type) + new.dimensions = newMultiArrayExpr.sizes.mapNotNull { handle(it) } + + return new + } + + private fun handleCastExpr(castExpr: JCastExpr): CastExpression { + val cast = newCastExpression(rawNode = castExpr) + cast.expression = handle(castExpr.op) ?: newProblemExpression("missing expression") + cast.castType = frontend.typeOf(castExpr.type) + + return cast + } + + private fun handleAbstractBinopExpr(expr: AbstractBinopExpr): BinaryOperator { + val op = newBinaryOperator(expr.symbol.trim(), rawNode = expr) + op.lhs = handle(expr.op1) ?: newProblemExpression("missing lhs") + op.rhs = handle(expr.op2) ?: newProblemExpression("missing rhs") + op.type = frontend.typeOf(expr.type) + + return op + } + + private fun handleNegExpr(expr: AbstractUnopExpr): UnaryOperator { + val op = newUnaryOperator("-", postfix = false, prefix = true, rawNode = expr) + op.input = handle(expr.op) ?: newProblemExpression("missing input") + op.type = frontend.typeOf(expr.type) + + return op + } + + private fun handleInstanceOfExpr(instanceOfExpr: JInstanceOfExpr): BinaryOperator { + val op = newBinaryOperator("instanceof", rawNode = instanceOfExpr) + op.lhs = handle(instanceOfExpr.op) ?: newProblemExpression("missing lhs") + + val type = frontend.typeOf(instanceOfExpr.checkType) + op.rhs = newTypeExpression("", type, rawNode = type) + op.rhs.name = type.name + op.type = frontend.typeOf(instanceOfExpr.type) + + return op + } + + private fun handleLengthExpr(lengthExpr: JLengthExpr): UnaryOperator { + val op = newUnaryOperator("lengthof", prefix = true, postfix = false, rawNode = lengthExpr) + op.input = handle(lengthExpr.op) ?: newProblemExpression("missing input") + op.type = frontend.typeOf(lengthExpr.type) + + return op + } + + private fun handleBooleanConstant(constant: BooleanConstant) = + newLiteral( + constant.equalEqual(BooleanConstant.getTrue()), + primitiveType("boolean"), + rawNode = constant + ) + + private fun handleFloatConstant(constant: FloatConstant) = + newLiteral(constant.value, primitiveType("float"), rawNode = constant) + + private fun handleDoubleConstant(constant: DoubleConstant) = + newLiteral(constant.value, primitiveType("double"), rawNode = constant) + + private fun handleIntConstant(constant: IntConstant) = + newLiteral(constant.value, primitiveType("int"), rawNode = constant) + + private fun handleLongConstant(constant: LongConstant) = + newLiteral(constant.value, primitiveType("long"), rawNode = constant) + + private fun handleStringConstant(constant: StringConstant) = + newLiteral(constant.value, primitiveType("java.lang.String"), rawNode = constant) + + private fun handleNullConstant(constant: NullConstant) = + newLiteral(null, unknownType(), rawNode = constant) + + /** + * We need to keep the class name as a string, rather than a [Class], because otherwise we would + * try to find the specified class on the classpath, which can lead to unwanted results. + */ + private fun handleClassConstant(constant: ClassConstant) = + newLiteral(constant.value, primitiveType("java.lang.Class"), rawNode = constant) + + private fun MethodSignature.toStaticRef(): Reference { + // First, construct the name using . + val ref = (this as SootClassMemberSignature<*>).toStaticRef() + + // We can also provide a function type, since these are all statically known. This might + // help in inferring some (unknown) functions later + ref.type = + FunctionType( + this.name, + this.parameterTypes.map { frontend.typeOf(it) }, + listOf(frontend.typeOf(this.type)), + frontend.language + ) + + return ref + } + + private fun SootClassMemberSignature<*>.toStaticRef(): Reference { + // First, construct the name using . + val ref = newReference("${this.declClassType.fullyQualifiedName}.${this.name}") + + // Make it static + ref.isStaticAccess = true + + return ref + } +} diff --git a/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/JVMLanguage.kt b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/JVMLanguage.kt new file mode 100644 index 00000000000..f5bae138b7d --- /dev/null +++ b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/JVMLanguage.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.frontends.jvm + +import de.fraunhofer.aisec.cpg.frontends.Language +import de.fraunhofer.aisec.cpg.graph.types.* +import kotlin.reflect.KClass + +class JVMLanguage : Language() { + override val fileExtensions: List + get() = listOf("class", "java", "jimple", "jar") + + override val namespaceDelimiter: String + get() = "." + + override val frontend: KClass + get() = JVMLanguageFrontend::class + + override val builtInTypes: Map + get() = + mapOf( + "float" to FloatingPointType("float", 32, this), + "double" to FloatingPointType("double", 64, this), + "char" to IntegerType("char", 8, this, NumericType.Modifier.UNSIGNED), + "boolean" to BooleanType("boolean", 1, this), + "byte" to IntegerType("byte", 8, this), + "short" to IntegerType("short", 16, this), + "int" to IntegerType("int", 32, this), + "long" to IntegerType("long", 64, this), + "java.lang.String" to StringType("java.lang.String", this), + "java.lang.Class" to ObjectType("java.lang.Class", listOf(), true, this) + ) + + override val compoundAssignmentOperators: Set + get() = setOf() +} diff --git a/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/JVMLanguageFrontend.kt b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/JVMLanguageFrontend.kt new file mode 100644 index 00000000000..26f111e65e2 --- /dev/null +++ b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/JVMLanguageFrontend.kt @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.frontends.jvm + +import de.fraunhofer.aisec.cpg.TranslationContext +import de.fraunhofer.aisec.cpg.frontends.Language +import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.frontends.TranslationException +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.NamespaceDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.types.Type +import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation +import java.io.File +import sootup.core.model.Body +import sootup.core.model.SootMethod +import sootup.core.model.SourceType +import sootup.core.types.ArrayType +import sootup.core.types.UnknownType +import sootup.core.util.printer.NormalStmtPrinter +import sootup.java.bytecode.inputlocation.JavaClassPathAnalysisInputLocation +import sootup.java.bytecode.interceptors.* +import sootup.java.core.views.JavaView +import sootup.java.sourcecode.inputlocation.JavaSourcePathAnalysisInputLocation +import sootup.jimple.parser.JimpleAnalysisInputLocation +import sootup.jimple.parser.JimpleView + +typealias SootType = sootup.core.types.Type + +class JVMLanguageFrontend( + language: Language>, + ctx: TranslationContext +) : LanguageFrontend(language, ctx) { + + val declarationHandler = DeclarationHandler(this) + val statementHandler = StatementHandler(this) + val expressionHandler = ExpressionHandler(this) + + lateinit var view: JavaView + + var body: Body? = null + + var printer: NormalStmtPrinter? = null + + /** + * Because of a limitation in SootUp, we can only specify the whole classpath for soot to parse. + * But in the CPG we need to specify one file. In this case, we take the + * [TranslationConfiguration.topLevel] and hand it over to soot, which parses all appropriate + * files within this folder/classpath. This means that the returned [TranslationUnitDeclaration] + * will contain not just the content of one file but the whole directory. + */ + override fun parse(file: File): TranslationUnitDeclaration { + val view = + when (file.extension) { + "class" -> { + JavaView( + JavaClassPathAnalysisInputLocation( + ctx.config.topLevel!!.path, + SourceType.Library, + listOf( + NopEliminator(), + CastAndReturnInliner(), + UnreachableCodeEliminator(), + Aggregator(), + CopyPropagator(), + // ConditionalBranchFolder(), + EmptySwitchEliminator(), + TypeAssigner(), + LocalNameStandardizer() + ) + ) + ) + } + "jar" -> { + JavaView( + JavaClassPathAnalysisInputLocation( + file.path, + SourceType.Library, + listOf( + NopEliminator(), + CastAndReturnInliner(), + UnreachableCodeEliminator(), + Aggregator(), + CopyPropagator(), + // ConditionalBranchFolder(), + EmptySwitchEliminator(), + TypeAssigner(), + LocalNameStandardizer() + ) + ) + ) + } + "java" -> { + JavaView(JavaSourcePathAnalysisInputLocation(ctx.config.topLevel!!.path)) + } + "jimple" -> { + JimpleView(JimpleAnalysisInputLocation(ctx.config.topLevel!!.toPath())) + } + else -> { + throw TranslationException("unsupported file") + } + } + // This contains the whole directory + val tu = newTranslationUnitDeclaration(file.parent) + scopeManager.resetToGlobal(tu) + + val packages = mutableMapOf() + + for (sootClass in view.classes) { + // Create an appropriate namespace, if it does not already exist + val pkg = + packages.computeIfAbsent(sootClass.type.packageName.name) { + val pkg = newNamespaceDeclaration(it) + scopeManager.addDeclaration(pkg) + pkg + } + + // Enter namespace scope + scopeManager.enterScope(pkg) + + val decl = declarationHandler.handle(sootClass) + scopeManager.addDeclaration(decl) + + // Leave namespace scope + scopeManager.leaveScope(pkg) + + // We need to clear the processed because they need to be per-file and we only have one + // frontend for all files + clearProcessed() + } + + return tu + } + + override fun setComment(node: Node, astNode: Any) {} + + override fun locationOf(astNode: Any): PhysicalLocation? { + // We do not really have a location anyway. maybe in jimple? + return null + } + + override fun codeOf(astNode: Any): String? { + if (astNode is SootMethod && astNode.isConcrete) { + return astNode.body.toString() + } + // We do not really have a source anyway. maybe in jimple? + return "" + } + + override fun typeOf(type: SootType): Type { + return when (type) { + is UnknownType -> { + unknownType() + } + is ArrayType -> { + typeOf(type.baseType).array() + } + else -> { + // TODO(oxisto): primitive types + val out = objectType(type.toString()) + + out + } + } + } +} diff --git a/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/StatementHandler.kt b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/StatementHandler.kt new file mode 100644 index 00000000000..711b1c2308d --- /dev/null +++ b/cpg-language-jvm/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/StatementHandler.kt @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.frontends.jvm + +import de.fraunhofer.aisec.cpg.frontends.Handler +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.statements.* +import de.fraunhofer.aisec.cpg.graph.statements.expressions.AssignExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block +import de.fraunhofer.aisec.cpg.graph.statements.expressions.ProblemExpression +import sootup.core.jimple.common.stmt.* +import sootup.core.model.Body +import sootup.core.util.printer.NormalStmtPrinter + +class StatementHandler(frontend: JVMLanguageFrontend) : + Handler(::ProblemExpression, frontend) { + init { + map.put(Body::class.java) { handleBody(it as Body) } + map.put(JAssignStmt::class.java) { handleAbstractDefinitionStmt(it as JAssignStmt) } + map.put(JIdentityStmt::class.java) { handleAbstractDefinitionStmt(it as JIdentityStmt) } + map.put(JIfStmt::class.java) { handleIfStmt(it as JIfStmt) } + map.put(JGotoStmt::class.java) { handleGotoStmt(it as JGotoStmt) } + map.put(JInvokeStmt::class.java) { handleInvokeStmt(it as JInvokeStmt) } + map.put(JReturnStmt::class.java) { handleReturnStmt(it as JReturnStmt) } + map.put(JReturnVoidStmt::class.java) { handleReturnVoidStmt(it as JReturnVoidStmt) } + } + + private fun handleBody(body: Body): Block { + // The first block contains all our other blocks and this will be the one we return + val outerBlock = newBlock(rawNode = body) + + val printer = NormalStmtPrinter() + printer.initializeSootMethod(body.stmtGraph) + + frontend.printer = printer + frontend.body = body + + // Parse locals, these are always at the beginning of the function + for (local in body.locals) { + val decl = frontend.declarationHandler.handle(local) + + if (decl != null) { + // We need to wrap them into a declaration statement and put them into the outer + // block + val stmt = newDeclarationStatement(rawNode = local) + stmt.addToPropertyEdgeDeclaration(decl) + frontend.scopeManager.addDeclaration(decl) + outerBlock += stmt + } + } + + // Parse statements and segment them into (sub)-blocks. + var block = outerBlock + for (sootStmt in body.stmts) { + val label = printer.labels[sootStmt] + if (label != null) { + // If we have a label, we need to create a new label statement, that starts a new + // block + val stmt = newLabelStatement() + block = newBlock() + stmt.label = label + stmt.subStatement = block + + // We need to inform our processing system, since we do it outside of a handler, so + // the created goto statements will be informed about our new label + frontend.process(Any(), stmt) + + // Always add it to the outer block + outerBlock += stmt + } + + // Parse the statement + val stmt = handle(sootStmt) + if (stmt != null) { + block += stmt + } + } + + // Always return the outer block, since it comprises all the other sub-blocks. + return outerBlock + } + + private fun handleAbstractDefinitionStmt(defStmt: AbstractDefinitionStmt): AssignExpression { + val assign = newAssignExpression("=", rawNode = defStmt) + assign.lhs = listOfNotNull(frontend.expressionHandler.handle(defStmt.leftOp)) + assign.rhs = listOfNotNull(frontend.expressionHandler.handle(defStmt.rightOp)) + + return assign + } + + private fun handleIfStmt(ifStmt: JIfStmt): IfStatement { + val stmt = newIfStatement(rawNode = ifStmt) + stmt.condition = + frontend.expressionHandler.handle(ifStmt.condition) + ?: newProblemExpression("missing condition") + stmt.thenStatement = handleBranchingStmt(ifStmt) + + return stmt + } + + private fun handleGotoStmt(gotoStmt: JGotoStmt): GotoStatement { + return handleBranchingStmt(gotoStmt) + } + + private fun handleBranchingStmt(branchingStmt: BranchingStmt): GotoStatement { + val stmt = newGotoStatement(rawNode = branchingStmt) + + frontend.body?.let { + val target = branchingStmt.getTargetStmts(it).firstOrNull() + val label = frontend.printer?.labels?.get(target) + if (label != null) { + stmt.labelName = label + } + + // Register a predicate listener that informs us as soon as new label statement that + // matches our label name is created. + frontend.registerPredicateListener({ _, to -> + (to is LabelStatement && to.label == stmt.labelName) + }) { _, to -> + stmt.targetLabel = to as LabelStatement + } + } + + return stmt + } + + private fun handleInvokeStmt(invokeStmt: JInvokeStmt) = + frontend.expressionHandler.handle(invokeStmt.invokeExpr) + + private fun handleReturnStmt(returnStmt: JReturnStmt): ReturnStatement { + val stmt = newReturnStatement(rawNode = returnStmt) + stmt.returnValue = + frontend.expressionHandler.handle(returnStmt.op) + ?: newProblemExpression("missing return value") + + return stmt + } + + private fun handleReturnVoidStmt(returnStmt: JReturnVoidStmt) = + newReturnStatement(rawNode = returnStmt) +} diff --git a/cpg-language-jvm/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/JVMLanguageFrontendTest.kt b/cpg-language-jvm/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/JVMLanguageFrontendTest.kt new file mode 100644 index 00000000000..2577a63de9b --- /dev/null +++ b/cpg-language-jvm/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/jvm/JVMLanguageFrontendTest.kt @@ -0,0 +1,422 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.frontends.jvm + +import de.fraunhofer.aisec.cpg.* +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* +import de.fraunhofer.aisec.cpg.graph.types.PointerType +import de.fraunhofer.aisec.cpg.passes.EdgeCachePass +import de.fraunhofer.aisec.cpg.passes.astParent +import de.fraunhofer.aisec.cpg.test.analyze +import de.fraunhofer.aisec.cpg.test.analyzeAndGetFirstTU +import de.fraunhofer.aisec.cpg.test.assertFullName +import de.fraunhofer.aisec.cpg.test.assertInvokes +import de.fraunhofer.aisec.cpg.test.assertLiteralValue +import de.fraunhofer.aisec.cpg.test.assertLocalName +import de.fraunhofer.aisec.cpg.test.assertRefersTo +import java.nio.file.Path +import kotlin.test.* +import org.junit.jupiter.api.Disabled + +class JVMLanguageFrontendTest { + @Test + fun testHelloJimple() { + val topLevel = Path.of("src", "test", "resources", "jimple", "helloworld") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("HelloWorld.jimple").toFile()), + topLevel, + true + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val helloWorld = tu.records["HelloWorld"] + assertNotNull(helloWorld) + + val constructor = helloWorld.constructors.firstOrNull() + assertNotNull(constructor) + + // All references should be resolved (except Object., which should be a construct + // expression anyway) + val refs = constructor.refs.filter { it.name.toString() != "java.lang.Object." } + refs.forEach { + val refersTo = it.refersTo + assertNotNull(refersTo, "${it.name} could not be resolved") + assertFalse( + refersTo.isInferred, + "${it.name} should not be resolved to an inferred node" + ) + } + + val main = helloWorld.methods["main"] + assertNotNull(main) + assertTrue(main.isStatic) + + val param0 = main.refs["@parameter0"] + assertNotNull(param0) + + val refersTo = param0.refersTo + assertNotNull(refersTo) + assertFalse(refersTo.isInferred) + } + + @Test + fun testMethodsClass() { + // This will be our classpath + val topLevel = Path.of("src", "test", "resources", "class", "methods") + val tu = + analyzeAndGetFirstTU( + // We just need to specify one file to trigger the class byte loader + listOf(topLevel.resolve("mypackage/Adder.class").toFile()), + topLevel, + true + ) { + it.registerPass() + it.registerLanguage() + } + assertNotNull(tu) + assertEquals(0, tu.problems.size) + + val pkg = tu.namespaces["mypackage"] + assertNotNull(pkg) + + val adder = pkg.records["Adder"] + assertNotNull(adder) + + val add = adder.methods["add"] + assertNotNull(add) + + val main = pkg.methods["Main.main"] + assertNotNull(main) + + println(main.code) + + // r5 contains our adder + val r5 = main.variables["r5"] + assertNotNull(r5) + assertFullName("mypackage.Adder", r5.type) + + // r3 should be the result of the add call + val r3 = main.variables["r3"] + assertNotNull(r3) + + val r3ref = r3.usages.firstOrNull { it.access == AccessValues.WRITE } + assertNotNull(r3ref) + + // Call to add should be resolved + val call = r3ref.prevDFG.firstOrNull() + assertIs(call) + assertLocalName("add", call) + assertInvokes(call, add) + assertEquals(listOf("Integer", "Integer"), call.arguments.map { it.type.name.localName }) + + // All references (which are not part of a call) and not to the stdlib should be resolved + val refs = tu.refs + refs + .filter { it.astParent !is CallExpression } + .filter { !it.name.startsWith("java.") } + .forEach { + val refersTo = it.refersTo + assertNotNull(refersTo, "${it.name} could not be resolved") + assertFalse( + refersTo.isInferred, + "${it.name} should not be resolved to an inferred node" + ) + } + } + + @Test + fun testLiteralsClass() { + // This will be our classpath + val topLevel = Path.of("src", "test", "resources", "class", "literals") + val result = + analyze( + // We just need to specify one file to trigger the byte code loader + listOf(topLevel.resolve("mypackage/Literals.class").toFile()), + topLevel, + true + ) { + it.registerPass() + it.registerLanguage() + } + assertNotNull(result) + + result.methods.forEach { + println(it.name) + println(it.code) + } + + assertEquals(0, result.problems.size) + } + + @Test + fun testLiteralsJar() { + // This will be our classpath + val topLevel = Path.of("src", "test", "resources", "jar", "literals") + val tu = + analyzeAndGetFirstTU( + // In case of a jar, the jar is directly used as a class path + listOf(topLevel.resolve("literals.jar").toFile()), + topLevel, + true + ) { + it.registerPass() + it.registerLanguage() + } + assertNotNull(tu) + assertEquals(0, tu.problems.size) + tu.methods.forEach { println(it.code) } + } + + @Test + fun testInheritanceClass() { + // This will be our classpath + val topLevel = Path.of("src", "test", "resources", "class", "inheritance") + val tu = + analyzeAndGetFirstTU( + // In case of a jar, the jar is directly used as a class path + listOf(topLevel.resolve("mypackage/Application.class").toFile()), + topLevel, + true + ) { + it.registerPass() + it.registerLanguage() + } + assertNotNull(tu) + tu.methods.forEach { println(it.code) } + assertEquals(0, tu.problems.size) + + val myInterface = tu.records["mypackage.MyInterface"] + assertNotNull(myInterface) + assertEquals("interface", myInterface.kind) + + val baseClass = tu.records["mypackage.BaseClass"] + assertNotNull(baseClass) + + val extendedClass = tu.records["mypackage.ExtendedClass"] + assertNotNull(extendedClass) + assertContains(extendedClass.implementedInterfaces, myInterface.toType()) + assertContains(extendedClass.superTypeDeclarations, baseClass) + assertContains(extendedClass.superTypeDeclarations, myInterface) + + val anotherExtendedClass = tu.records["mypackage.AnotherExtendedClass"] + assertNotNull(anotherExtendedClass) + assertContains(anotherExtendedClass.superTypeDeclarations, baseClass) + + assertEquals( + baseClass.toType(), + listOf(extendedClass.toType(), anotherExtendedClass.toType()).commonType + ) + + val appInit = tu.methods["mypackage.Application."] + assertNotNull(appInit) + + val appDoSomething = tu.methods["mypackage.Application.doSomething"] + assertNotNull(appDoSomething) + assertLocalName("MyInterface", appDoSomething.parameters.firstOrNull()?.type) + + // Call doSomething in Application. with an object of ExtendedClass, which should + // fulfill the MyInterface of the needed parameter + val doSomethingCall1 = appInit.calls["doSomething"] + assertNotNull(doSomethingCall1) + assertLocalName("ExtendedClass", doSomethingCall1.arguments.firstOrNull()?.type) + assertInvokes(doSomethingCall1, appDoSomething) + + val extended = appInit.variables["r4"] + assertNotNull(extended) + + val getMyProperty = + appInit.calls[ + { + it.name.localName == "getMyProperty" && + it is MemberCallExpression && + it.base in extended.usages + }] + assertNotNull(getMyProperty) + assertInvokes(getMyProperty, baseClass.methods["getMyProperty"]) + + val setMyProperty = + appInit.calls[ + { + it.name.localName == "setMyProperty" && + it is MemberCallExpression && + it.base in extended.usages + }] + assertNotNull(setMyProperty) + assertInvokes(setMyProperty, extendedClass.methods["setMyProperty"]) + } + + @Test + fun testFieldsClass() { + // This will be our classpath + val topLevel = Path.of("src", "test", "resources", "class", "fields") + val tu = + analyzeAndGetFirstTU( + // We just need to specify one file to trigger the byte code loader + listOf(topLevel.resolve("mypackage/Fields.class").toFile()), + topLevel, + true + ) { + it.registerPass() + it.registerLanguage() + } + assertNotNull(tu) + assertEquals(0, tu.problems.size) + tu.methods.forEach { println(it.code) } + + val refs = tu.refs.filterIsInstance() + refs.forEach { + val refersTo = it.refersTo + assertNotNull(refersTo, "${it.name} could not be resolved") + assertFalse( + refersTo.isInferred, + "${it.name} should not be resolved to an inferred node" + ) + } + + val setACall = tu.calls["setA"] + assertNotNull(setACall) + + val lit10 = setACall.arguments.firstOrNull() + assertIs>(lit10) + assertLiteralValue(10, lit10) + } + + @Disabled + @Test + fun testLiteralsSource() { + // This will be our classpath + val topLevel = Path.of("src", "test", "resources", "class", "literals") + val tu = + analyzeAndGetFirstTU( + // We just need to specify one file to trigger the source code loader + listOf(topLevel.resolve("mypackage/Literals.java").toFile()), + topLevel, + true + ) { + it.registerPass() + it.registerLanguage() + } + assertNotNull(tu) + + val haveFun = tu.methods["haveFunWithLiterals"] + assertNotNull(haveFun) + + println(haveFun.code) + } + + @Test + fun testArraysClass() { + // This will be our classpath + val topLevel = Path.of("src", "test", "resources", "class", "arrays") + val tu = + analyzeAndGetFirstTU( + // We just need to specify one file to trigger the class byte loader + listOf(topLevel.resolve("mypackage/Arrays.class").toFile()), + topLevel, + true + ) { + it.registerPass() + it.registerLanguage() + } + assertNotNull(tu) + tu.methods.forEach { println(it.code) } + assertEquals(0, tu.problems.size) + + val create = tu.methods["create"] + assertNotNull(create) + + val r3 = create.variables["r3"] + assertNotNull(r3) + + var arrayType = r3.type + assertIs(arrayType) + assertTrue(arrayType.isArray) + assertFullName("mypackage.Element", arrayType.elementType) + + val r3write = r3.usages.firstOrNull { it.access == AccessValues.WRITE } + assertNotNull(r3write) + + var expr = r3write.prevDFG.singleOrNull() + assertIs(expr) + assertLiteralValue(2, expr.dimensions.singleOrNull()) + + var r1 = create.variables["r1"] + assertNotNull(r1) + assertEquals(arrayType.elementType, r1.type) + + val r2 = create.variables["r2"] + assertNotNull(r2) + assertEquals(arrayType.elementType, r2.type) + + val r2write = r2.usages.firstOrNull { it.access == AccessValues.WRITE } + assertNotNull(r2write) + + val prevDFG = r2write.prevDFG.singleOrNull() + assertIs(prevDFG) + assertRefersTo(prevDFG.arrayExpression, r3) + + val createMulti = tu.methods["createMulti"] + assertNotNull(createMulti) + + r1 = createMulti.variables["r1"] + assertNotNull(r1) + + arrayType = r1.type + assertIs(arrayType) + assertTrue(arrayType.isArray) + assertFullName("mypackage.Element", arrayType.elementType) + + val r1write = r1.usages.firstOrNull { it.access == AccessValues.WRITE } + assertNotNull(r1write) + + expr = r1write.prevDFG.singleOrNull() + assertIs(expr) + listOf(2, 10).forEachIndexed { index, i -> assertLiteralValue(i, expr.dimensions[index]) } + } + + @Disabled + @Test + fun testExceptional() { + // This will be our classpath + val topLevel = Path.of("src", "test", "resources", "class", "exception") + val tu = + analyzeAndGetFirstTU( + // We just need to specify one file to trigger the class byte loader + listOf(topLevel.resolve("mypackage/Exceptional.class").toFile()), + topLevel, + true + ) { + it.registerPass() + it.registerLanguage() + } + assertNotNull(tu) + tu.methods.forEach { println(it.code) } + } +} diff --git a/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Arrays.class b/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Arrays.class new file mode 100644 index 00000000000..194c12155ea Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Arrays.class differ diff --git a/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Arrays.java b/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Arrays.java new file mode 100644 index 00000000000..e91aeb19993 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Arrays.java @@ -0,0 +1,21 @@ +package mypackage; + +public class Arrays { + + public Element[] create() { + var arrays = new Element[2]; + arrays[0] = new Element(); + arrays[1] = arrays[0]; + + int len = arrays.length; + + return arrays; + } + + public Element[][] createMulti() { + var multi = new Element[2][10]; + + return multi; + } + +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Element.class b/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Element.class new file mode 100644 index 00000000000..80ab8317cc6 Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Element.class differ diff --git a/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Element.java b/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Element.java new file mode 100644 index 00000000000..06a2c9a0625 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/arrays/mypackage/Element.java @@ -0,0 +1,5 @@ +package mypackage; + +public class Element { + +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/fields/mypackage/Fields.class b/cpg-language-jvm/src/test/resources/class/fields/mypackage/Fields.class new file mode 100644 index 00000000000..3ef2237bee7 Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/fields/mypackage/Fields.class differ diff --git a/cpg-language-jvm/src/test/resources/class/fields/mypackage/Fields.java b/cpg-language-jvm/src/test/resources/class/fields/mypackage/Fields.java new file mode 100644 index 00000000000..3a7fed12110 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/fields/mypackage/Fields.java @@ -0,0 +1,18 @@ +package mypackage; + +public class Fields { + + private int a = 2; + + Fields() { + resetA(); + } + + public void setA(int a) { + this.a = a; + } + + private void resetA() { + setA(10); + } +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/AnotherExtendedClass.class b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/AnotherExtendedClass.class new file mode 100644 index 00000000000..144452d835c Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/AnotherExtendedClass.class differ diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/AnotherExtendedClass.java b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/AnotherExtendedClass.java new file mode 100644 index 00000000000..06fea507f49 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/AnotherExtendedClass.java @@ -0,0 +1,4 @@ +package mypackage; + +public class AnotherExtendedClass extends BaseClass { +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/Application.class b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/Application.class new file mode 100644 index 00000000000..0104bfda3e3 Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/Application.class differ diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/Application.java b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/Application.java new file mode 100644 index 00000000000..83416910826 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/Application.java @@ -0,0 +1,27 @@ +package mypackage; + +public class Application { + public Application() { + var extended = new ExtendedClass(); + int old = extended.getMyProperty(); + extended.setMyProperty(10); + doSomething(extended); + + BaseClass base; + if(Math.random() == 1.0) { + base = (BaseClass) extended; + } else { + base = new AnotherExtendedClass(); + } + base.setMyProperty(10); + + if(base instanceof ExtendedClass) { + System.out.println("Is extended!"); + } + } + + public void doSomething(MyInterface i) { + i.doSomething(); + } + +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/BaseClass.class b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/BaseClass.class new file mode 100644 index 00000000000..ce1f1370c17 Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/BaseClass.class differ diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/BaseClass.java b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/BaseClass.java new file mode 100644 index 00000000000..3962a24f171 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/BaseClass.java @@ -0,0 +1,14 @@ +package mypackage; + +public class BaseClass { + + public int getMyProperty() { + return myProperty; + } + + public void setMyProperty(int myProperty) { + this.myProperty = myProperty; + } + + protected int myProperty = 5; +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/ExtendedClass.class b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/ExtendedClass.class new file mode 100644 index 00000000000..21739710f47 Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/ExtendedClass.class differ diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/ExtendedClass.java b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/ExtendedClass.java new file mode 100644 index 00000000000..6dee7893f8c --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/ExtendedClass.java @@ -0,0 +1,18 @@ +package mypackage; + +public class ExtendedClass extends BaseClass implements MyInterface { + + public void setMyProperty(int myProperty) { + informSomebody(this.myProperty, myProperty); + super.setMyProperty(myProperty); + } + private void informSomebody(int oldValue, int newValue) { + System.out.println("We changed the value from " + oldValue + " to " + newValue); + } + + @Override + public void doSomething() { + + } + +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/MyInterface.class b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/MyInterface.class new file mode 100644 index 00000000000..45fc9318b3b Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/MyInterface.class differ diff --git a/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/MyInterface.java b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/MyInterface.java new file mode 100644 index 00000000000..0c08afdd7d6 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/inheritance/mypackage/MyInterface.java @@ -0,0 +1,7 @@ +package mypackage; + +public interface MyInterface { + + public void doSomething(); + +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/literals/mypackage/Literals.class b/cpg-language-jvm/src/test/resources/class/literals/mypackage/Literals.class new file mode 100644 index 00000000000..03362eb9160 Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/literals/mypackage/Literals.class differ diff --git a/cpg-language-jvm/src/test/resources/class/literals/mypackage/Literals.java b/cpg-language-jvm/src/test/resources/class/literals/mypackage/Literals.java new file mode 100644 index 00000000000..918c3befdb7 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/literals/mypackage/Literals.java @@ -0,0 +1,37 @@ +package mypackage; + +import java.util.function.Supplier; + +public class Literals { + + void haveFunWithLiterals() { + float f = 2; + double d = 4d; + String str = "mystring"; + short s = 10; + int i = 2000; + long l = 2000L; + boolean b = false; + Literals obj; + if(Math.random() == 10) { + obj = null; + } else { + obj = this; + } + + Integer i2 = 1000; + Long l2 = 1000L; + + Class clazz = Literals.class; + test(this::mySupplier); + } + + void test(Supplier s) { + s.get(); + } + + Integer mySupplier() { + return 1; + } + +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/methods/mypackage/Adder.class b/cpg-language-jvm/src/test/resources/class/methods/mypackage/Adder.class new file mode 100644 index 00000000000..9493eaf6a0b Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/methods/mypackage/Adder.class differ diff --git a/cpg-language-jvm/src/test/resources/class/methods/mypackage/Adder.java b/cpg-language-jvm/src/test/resources/class/methods/mypackage/Adder.java new file mode 100644 index 00000000000..954e22c6a45 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/methods/mypackage/Adder.java @@ -0,0 +1,11 @@ +package mypackage; + +import java.lang.Integer; + +public class Adder { + + Integer add(Integer a, Integer b) { + return a + b; + } + +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/class/methods/mypackage/Main.class b/cpg-language-jvm/src/test/resources/class/methods/mypackage/Main.class new file mode 100644 index 00000000000..4d448cfe92f Binary files /dev/null and b/cpg-language-jvm/src/test/resources/class/methods/mypackage/Main.class differ diff --git a/cpg-language-jvm/src/test/resources/class/methods/mypackage/Main.java b/cpg-language-jvm/src/test/resources/class/methods/mypackage/Main.java new file mode 100644 index 00000000000..fdad05b5f8d --- /dev/null +++ b/cpg-language-jvm/src/test/resources/class/methods/mypackage/Main.java @@ -0,0 +1,12 @@ +package mypackage; + +public class Main { + + public static void main(String[] args) { + var adder = new Adder(); + var sum = adder.add(1, 2); + + System.out.println(sum); + } + +} \ No newline at end of file diff --git a/cpg-language-jvm/src/test/resources/jar/literals/literals.jar b/cpg-language-jvm/src/test/resources/jar/literals/literals.jar new file mode 100644 index 00000000000..4931b86ad1e Binary files /dev/null and b/cpg-language-jvm/src/test/resources/jar/literals/literals.jar differ diff --git a/cpg-language-jvm/src/test/resources/jimple/helloworld/HelloWorld.jimple b/cpg-language-jvm/src/test/resources/jimple/helloworld/HelloWorld.jimple new file mode 100644 index 00000000000..256700b174a --- /dev/null +++ b/cpg-language-jvm/src/test/resources/jimple/helloworld/HelloWorld.jimple @@ -0,0 +1,22 @@ +public class HelloWorld extends java.lang.Object +{ + public void () + { + HelloWorld r0; + r0 := @this: HelloWorld; + specialinvoke r0.()>(); + return; + } + + public static void main(java.lang.String[]) + { + java.lang.String[] r0; + java.io.PrintStream $r1; + + r0 := @parameter0: java.lang.String[]; + $r1 = ; + virtualinvoke $r1.("Hello world!"); + return; + } +} diff --git a/cpg-language-jvm/src/test/resources/log4j2.xml b/cpg-language-jvm/src/test/resources/log4j2.xml new file mode 100644 index 00000000000..5b73082e2c0 --- /dev/null +++ b/cpg-language-jvm/src/test/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle.properties.example b/gradle.properties.example index c9e3c651dc3..f956f347309 100644 --- a/gradle.properties.example +++ b/gradle.properties.example @@ -7,4 +7,5 @@ enableGoFrontend=true enablePythonFrontend=true enableLLVMFrontend=true enableTypeScriptFrontend=true -enableRubyFrontend=true \ No newline at end of file +enableRubyFrontend=true +enableJVMFrontend=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a492044ab86..b0dedf6c7d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ log4j = "2.23.0" sonarqube = "5.0.0.4638" spotless = "6.25.0" nexus-publish = "2.0.0" +sootup = "1.2.0" [libraries] kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin"} @@ -55,6 +56,12 @@ sonarqube-gradle = { module = "org.sonarsource.scanner.gradle:sonarqube-gradle-p spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } nexus-publish-gradle = { module = "io.github.gradle-nexus:publish-plugin", version.ref = "nexus-publish" } +sootup-core = { module = "org.soot-oss:sootup.core", version.ref = "sootup" } +sootup-java-core = { module = "org.soot-oss:sootup.java.core", version.ref = "sootup" } +sootup-java-sourcecode = { module = "org.soot-oss:sootup.java.sourcecode", version.ref = "sootup" } +sootup-java-bytecode = { module = "org.soot-oss:sootup.java.bytecode", version.ref = "sootup" } +sootup-jimple-parser = { module = "org.soot-oss:sootup.jimple.parser", version.ref = "sootup" } + [bundles] log4j = ["log4j-impl", "log4j-core"] neo4j = ["neo4j-ogm-core", "neo4j-ogm-bolt-driver"] @@ -65,6 +72,7 @@ kotlin-scripting = [ "kotlin-scripting-dependencies", "kotlin-scripting-dependencies-maven-all", ] +sootup = ["sootup-core", "sootup-java-core", "sootup-java-sourcecode", "sootup-java-bytecode", "sootup-jimple-parser"] [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"} diff --git a/settings.gradle.kts b/settings.gradle.kts index b7a66ec8a10..afe927cdee2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,10 @@ val enableRubyFrontend: Boolean by extra { val enableRubyFrontend: String? by settings enableRubyFrontend.toBoolean() } +val enableJVMFrontend: Boolean by extra { + val enableJVMFrontend: String? by settings + enableJVMFrontend.toBoolean() +} if (enableJavaFrontend) include(":cpg-language-java") if (enableCXXFrontend) include(":cpg-language-cxx") @@ -44,4 +48,5 @@ if (enableGoFrontend) include(":cpg-language-go") if (enableLLVMFrontend) include(":cpg-language-llvm") if (enablePythonFrontend) include(":cpg-language-python") if (enableTypeScriptFrontend) include(":cpg-language-typescript") -if (enableRubyFrontend) include(":cpg-language-ruby") \ No newline at end of file +if (enableRubyFrontend) include(":cpg-language-ruby") +if (enableJVMFrontend) include(":cpg-language-jvm")