diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/MultiValueEvaluator.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/MultiValueEvaluator.kt new file mode 100644 index 0000000000..1ce91bb668 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/MultiValueEvaluator.kt @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2022, 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.analysis + +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.ValueEvaluator +import de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.negate +import de.fraunhofer.aisec.cpg.graph.statements.ForStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* +import de.fraunhofer.aisec.cpg.passes.astParent +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * This [ValueEvaluator] can resolve multiple possible values of a node. + * + * It requires running the [EdgeCachePass] after the translation to add all necessary edges. + */ +class MultiValueEvaluator : ValueEvaluator() { + companion object { + const val MAX_DEPTH: Int = 20 + } + + override val log: Logger + get() = LoggerFactory.getLogger(MultiValueEvaluator::class.java) + + override fun evaluate(node: Any?): Any? { + val result = evaluateInternal(node as? Node, 0) + return if (result is List<*> && result.all { r -> r is Number }) + ConcreteNumberSet(result.map { r -> (r as Number).toLong() }.toMutableSet()) + else result + } + + /** Tries to evaluate this node. Anything can happen. */ + override fun evaluateInternal(node: Node?, depth: Int): Any? { + if (node == null) { + return null + } + + if (depth > MAX_DEPTH) { + return cannotEvaluate(node, this) + } + // Add the expression to the current path + this.path += node + + when (node) { + is FieldDeclaration -> { + return evaluateInternal(node.initializer, depth + 1) + } + is ArrayCreationExpression -> return evaluateInternal(node.initializer, depth + 1) + is VariableDeclaration -> return evaluateInternal(node.initializer, depth + 1) + // For a literal, we can just take its value, and we are finished + is Literal<*> -> return node.value + is DeclaredReferenceExpression -> return handleDeclaredReferenceExpression(node, depth) + is UnaryOperator -> return handleUnaryOp(node, depth) + is BinaryOperator -> return handleBinaryOperator(node, depth) + // Casts are just a wrapper in this case, we are interested in the inner expression + is CastExpression -> return this.evaluateInternal(node.expression, depth + 1) + is ArraySubscriptionExpression -> return handleArraySubscriptionExpression(node, depth) + // While we are not handling different paths of variables with If statements, we can + // easily be partly path-sensitive in a conditional expression + is ConditionalExpression -> return handleConditionalExpression(node, depth) + } + + // At this point, we cannot evaluate, and we are calling our [cannotEvaluate] hook, maybe + // this helps + return cannotEvaluate(node, this) + } + + /** + * We are handling some basic arithmetic binary operations and string operations that are more + * or less language-independent. + */ + override fun handleBinaryOperator(expr: BinaryOperator, depth: Int): Any? { + // Resolve lhs + val lhsValue = evaluateInternal(expr.lhs, depth + 1) + // Resolve rhs + val rhsValue = evaluateInternal(expr.rhs, depth + 1) + + if (lhsValue !is List<*> && rhsValue !is List<*>) { + return computeBinaryOpEffect(lhsValue, rhsValue, expr) + } + + val result = mutableListOf() + if (lhsValue is List<*>) { + for (lhs in lhsValue) { + if (rhsValue is List<*>) { + result.addAll(rhsValue.map { r -> computeBinaryOpEffect(lhs, r, expr) }) + } else { + result.add(computeBinaryOpEffect(lhs, rhsValue, expr)) + } + } + } else { + result.addAll( + (rhsValue as List<*>).map { r -> computeBinaryOpEffect(lhsValue, r, expr) } + ) + } + + return result + } + + override fun handleConditionalExpression(expr: ConditionalExpression, depth: Int): Any? { + val result = mutableListOf() + val elseResult = evaluateInternal(expr.elseExpr, depth + 1) + val thenResult = evaluateInternal(expr.thenExpr, depth + 1) + if (thenResult is List<*>) result.addAll(thenResult) else result.add(thenResult) + if (elseResult is List<*>) result.addAll(elseResult) else result.add(elseResult) + return result + } + + override fun handleUnaryOp(expr: UnaryOperator, depth: Int): Any? { + return when (expr.operatorCode) { + "-" -> { + when (val input = evaluateInternal(expr.input, depth + 1)) { + is List<*> -> input.map { n -> (n as? Number)?.negate() } + is Number -> input.negate() + else -> cannotEvaluate(expr, this) + } + } + "++" -> { + if (expr.astParent is ForStatement) { + evaluateInternal(expr.input, depth + 1) + } else { + when (val input = evaluateInternal(expr.input, depth + 1)) { + is Number -> input.toLong() + 1 + is List<*> -> input.map { n -> (n as? Number)?.toLong()?.plus(1) } + else -> cannotEvaluate(expr, this) + } + } + } + "*" -> evaluateInternal(expr.input, depth + 1) + "&" -> evaluateInternal(expr.input, depth + 1) + else -> cannotEvaluate(expr, this) + } + } + + /** + * Tries to compute the value of a reference. It therefore checks the incoming data flow edges. + * + * In contrast to the implementation of [ValueEvaluator], this one can handle more than one + * value. + */ + override fun handleDeclaredReferenceExpression( + expr: DeclaredReferenceExpression, + depth: Int + ): List { + // For a reference, we are interested in its last assignment into the reference + // denoted by the previous DFG edge + val prevDFG = expr.prevDFG + + if (prevDFG.size == 1) { + // There's only one incoming DFG edge, so we follow this one. + val internalRes = evaluateInternal(prevDFG.first(), depth + 1) + return if (internalRes is List<*>) internalRes else mutableListOf(internalRes) + } + + // We are only interested in expressions + val expressions = prevDFG.filterIsInstance() + + if ( + expressions.size == 2 && + expressions.all { e -> + (e.astParent?.astParent as? ForStatement)?.initializerStatement == e || + (e.astParent as? ForStatement)?.iterationStatement == e + } + ) { + return handleSimpleLoopVariable(expr, depth) + } + + val result = mutableListOf() + if (expressions.isEmpty()) { + // No previous expression?? Let's try with a variable declaration and its initialization + val decl = prevDFG.filterIsInstance() + for (declaration in decl) { + val res = evaluateInternal(declaration, depth + 1) + if (res is Collection<*>) { + result.addAll(res) + } else { + result.add(res) + } + } + } + + for (expression in expressions) { + val res = evaluateInternal(expression, depth + 1) + if (res is Collection<*>) { + result.addAll(res) + } else { + result.add(res) + } + } + return result + } + + private fun handleSimpleLoopVariable( + expr: DeclaredReferenceExpression, + depth: Int + ): List { + val loop = + expr.prevDFG.firstOrNull { e -> e.astParent is ForStatement }?.astParent + as? ForStatement + if (loop == null || loop.condition !is BinaryOperator) return listOf() + + var loopVar = + evaluateInternal(loop.initializerStatement.declarations.first(), depth) as? Number + + if (loopVar == null) return listOf() + + val cond = loop.condition as BinaryOperator + val result = mutableListOf() + var lhs = + if ((cond.lhs as? DeclaredReferenceExpression)?.refersTo == expr.refersTo) { + loopVar + } else { + evaluateInternal(cond.lhs, depth + 1) + } + var rhs = + if ((cond.rhs as? DeclaredReferenceExpression)?.refersTo == expr.refersTo) { + loopVar + } else { + evaluateInternal(cond.rhs, depth + 1) + } + + var comparisonResult = computeBinaryOpEffect(lhs, rhs, cond) + while (comparisonResult == true) { + result.add( + loopVar + ) // We skip the last iteration on purpose because that last operation will be added by + // the statement which made us end up here. + + val loopOp = loop.iterationStatement + loopVar = + when (loopOp) { + is BinaryOperator -> { + val opLhs = + if ( + (loopOp.lhs as? DeclaredReferenceExpression)?.refersTo == + expr.refersTo + ) { + loopVar + } else { + loopOp.lhs + } + val opRhs = + if ( + (loopOp.rhs as? DeclaredReferenceExpression)?.refersTo == + expr.refersTo + ) { + loopVar + } else { + loopOp.rhs + } + computeBinaryOpEffect(opLhs, opRhs, loopOp) as? Number + } + is UnaryOperator -> { + computeUnaryOpEffect( + if ( + (loopOp.input as? DeclaredReferenceExpression)?.refersTo == + expr.refersTo + ) { + loopVar!! + } else { + loopOp.input + }, + loopOp + ) + as? Number + } + else -> { + null + } + } + if (loopVar == null) { + return result + } + // result.add(loopVar) + + if ((cond.lhs as? DeclaredReferenceExpression)?.refersTo == expr.refersTo) { + lhs = loopVar + } + if ((cond.rhs as? DeclaredReferenceExpression)?.refersTo == expr.refersTo) { + rhs = loopVar + } + comparisonResult = computeBinaryOpEffect(lhs, rhs, cond) + } + return result + } + + private fun computeUnaryOpEffect(input: Any, expr: UnaryOperator): Any? { + return when (expr.operatorCode) { + "-" -> { + when (input) { + is List<*> -> input.map { n -> (n as? Number)?.negate() } + is Number -> input.negate() + else -> cannotEvaluate(expr, this) + } + } + "++" -> { + when (input) { + is Number -> input.toLong() + 1 + is List<*> -> input.map { n -> (n as? Number)?.toLong()?.plus(1) } + else -> cannotEvaluate(expr, this) + } + } + else -> cannotEvaluate(expr, this) + } + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/NumberSet.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/NumberSet.kt new file mode 100644 index 0000000000..b9cd312214 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/NumberSet.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022, 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.analysis + +abstract class NumberSet { + abstract fun min(): Long + abstract fun max(): Long + abstract fun addValue(value: Long) + abstract fun maybe(value: Long): Boolean + abstract fun clear() +} + +class Interval : NumberSet() { + private var min: Long = Long.MAX_VALUE + private var max: Long = Long.MIN_VALUE + + override fun addValue(value: Long) { + if (value < min) { + min = value + } + if (value > max) { + max = value + } + } + override fun min(): Long { + return min + } + override fun max(): Long { + return max + } + override fun maybe(value: Long): Boolean { + return value in min..max + } + override fun clear() { + min = Long.MAX_VALUE + max = Long.MIN_VALUE + } +} + +class ConcreteNumberSet(var values: MutableSet = mutableSetOf()) : NumberSet() { + override fun addValue(value: Long) { + values.add(value) + } + override fun min(): Long { + return values.minOrNull() ?: Long.MAX_VALUE + } + override fun max(): Long { + return values.maxOrNull() ?: Long.MIN_VALUE + } + override fun maybe(value: Long): Boolean { + return value in values + } + override fun clear() { + values.clear() + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/SizeEvaluator.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/SizeEvaluator.kt new file mode 100644 index 0000000000..77ef652dfa --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/SizeEvaluator.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022, 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.analysis + +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.ValueEvaluator +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Simple evaluation of the size of an object. Right now, it can only support a statically given + * size of arrays and strings. + */ +class SizeEvaluator : ValueEvaluator() { + override val log: Logger + get() = LoggerFactory.getLogger(SizeEvaluator::class.java) + + override fun evaluate(node: Any?): Any? { + if (node is String) { + return node.length + } + val result = evaluateInternal(node as? Node, 0) + return result + } + + override fun evaluateInternal(node: Node?, depth: Int): Any? { + // Add the expression to the current path + node?.let { this.path += it } + + return when (node) { + is ArrayCreationExpression -> + if (node.initializer != null) { + evaluateInternal(node.initializer, depth + 1) + } else { + evaluateInternal(node.dimensions.firstOrNull(), depth + 1) + } + is VariableDeclaration -> evaluateInternal(node.initializer, depth + 1) + is DeclaredReferenceExpression -> evaluateInternal(node.refersTo, depth + 1) + // For a literal, we can just take its value, and we are finished + is Literal<*> -> if (node.value is String) (node.value as String).length else node.value + is ArraySubscriptionExpression -> evaluate(node.arrayExpression) + else -> cannotEvaluate(node, this) + } + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluator.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluator.kt index 9e3a58150a..6aa4d6cfe2 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluator.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluator.kt @@ -39,15 +39,15 @@ class CouldNotResolve * * The result can be retrieved in two ways: * * The result of the [resolve] function is a JVM object which represents the constant value - * * Furthermore, after the execution of [evaluate], the latest evaluation path can be retrieved in - * the [path] property of the evaluator. + * * Furthermore, after the execution of [evaluateInternal], the latest evaluation path can be + * retrieved in the [path] property of the evaluator. * * It contains some advanced mechanics such as resolution of values of arrays, if they contain * literal values. Furthermore, its behaviour can be adjusted by implementing the [cannotEvaluate] * function, which is called when the default behaviour would not be able to resolve the value. This * way, language specific features such as string formatting can be modelled. */ -class ValueEvaluator( +open class ValueEvaluator( /** * Contains a reference to a function that gets called if the value cannot be resolved by the * standard behaviour. @@ -61,31 +61,37 @@ class ValueEvaluator( } } ) { - private val log: Logger + protected open val log: Logger get() = LoggerFactory.getLogger(ValueEvaluator::class.java) - /** This property contains the path of the latest execution of [evaluate]. */ + /** This property contains the path of the latest execution of [evaluateInternal]. */ val path: MutableList = mutableListOf() + open fun evaluate(node: Any?): Any? { + if (node !is Node) return node + + return evaluateInternal(node as? Node, 0) + } + /** Tries to evaluate this node. Anything can happen. */ - fun evaluate(node: Node?): Any? { + protected open fun evaluateInternal(node: Node?, depth: Int): Any? { // Add the expression to the current path node?.let { this.path += it } when (node) { - is ArrayCreationExpression -> return evaluate(node.initializer) - is VariableDeclaration -> return evaluate(node.initializer) + is ArrayCreationExpression -> return evaluateInternal(node.initializer, depth + 1) + is VariableDeclaration -> return evaluateInternal(node.initializer, depth + 1) // For a literal, we can just take its value, and we are finished is Literal<*> -> return node.value - is DeclaredReferenceExpression -> return handleDeclaredReferenceExpression(node) - is UnaryOperator -> return handleUnaryOp(node) - is BinaryOperator -> return handleBinaryOperator(node) + is DeclaredReferenceExpression -> return handleDeclaredReferenceExpression(node, depth) + is UnaryOperator -> return handleUnaryOp(node, depth) + is BinaryOperator -> return handleBinaryOperator(node, depth) // Casts are just a wrapper in this case, we are interested in the inner expression - is CastExpression -> return this.evaluate(node.expression) - is ArraySubscriptionExpression -> handleArraySubscriptionExpression(node) + is CastExpression -> return this.evaluateInternal(node.expression, depth + 1) + is ArraySubscriptionExpression -> return handleArraySubscriptionExpression(node, depth) // While we are not handling different paths of variables with If statements, we can // easily be partly path-sensitive in a conditional expression - is ConditionalExpression -> handleConditionalExpression(node) + is ConditionalExpression -> return handleConditionalExpression(node, depth) } // At this point, we cannot evaluate, and we are calling our [cannotEvaluate] hook, maybe @@ -97,12 +103,20 @@ class ValueEvaluator( * We are handling some basic arithmetic binary operations and string operations that are more * or less language-independent. */ - private fun handleBinaryOperator(expr: BinaryOperator): Any? { + protected open fun handleBinaryOperator(expr: BinaryOperator, depth: Int): Any? { // Resolve lhs - val lhsValue = evaluate(expr.lhs) + val lhsValue = evaluateInternal(expr.lhs, depth + 1) // Resolve rhs - val rhsValue = evaluate(expr.rhs) + val rhsValue = evaluateInternal(expr.rhs, depth + 1) + return computeBinaryOpEffect(lhsValue, rhsValue, expr) + } + + protected fun computeBinaryOpEffect( + lhsValue: Any?, + rhsValue: Any?, + expr: BinaryOperator + ): Any? { return when (expr.operatorCode) { "+" -> handlePlus(lhsValue, rhsValue, expr) "-" -> handleMinus(lhsValue, rhsValue, expr) @@ -160,6 +174,7 @@ class ValueEvaluator( private fun handleDiv(lhsValue: Any?, rhsValue: Any?, expr: BinaryOperator): Any? { return when { + rhsValue == 0 -> cannotEvaluate(expr, this) lhsValue is Int && (rhsValue is Double || rhsValue is Float) -> lhsValue / (rhsValue as Number).toDouble() lhsValue is Int && rhsValue is Number -> lhsValue / rhsValue.toLong() @@ -242,21 +257,22 @@ class ValueEvaluator( * We handle some basic unary operators. These also affect pointers and dereferences for * languages that support them. */ - private fun handleUnaryOp(expr: UnaryOperator): Any? { + protected open fun handleUnaryOp(expr: UnaryOperator, depth: Int): Any? { return when (expr.operatorCode) { "-" -> { - when (val input = evaluate(expr.input)) { - is Int -> -input - is Long -> -input - is Short -> -input - is Byte -> -input - is Double -> -input - is Float -> -input + when (val input = evaluateInternal(expr.input, depth + 1)) { + is Number -> input.negate() else -> cannotEvaluate(expr, this) } } - "*" -> evaluate(expr.input) - "&" -> evaluate(expr.input) + "++" -> { + when (val input = evaluateInternal(expr.input, depth + 1)) { + is Number -> input.toLong() + 1 + else -> cannotEvaluate(expr, this) + } + } + "*" -> evaluateInternal(expr.input, depth + 1) + "&" -> evaluateInternal(expr.input, depth + 1) else -> cannotEvaluate(expr, this) } } @@ -266,20 +282,24 @@ class ValueEvaluator( * basically the case if the base of the subscript expression is a list of [KeyValueExpression] * s. */ - private fun handleArraySubscriptionExpression(expr: ArraySubscriptionExpression): Any? { + protected fun handleArraySubscriptionExpression( + expr: ArraySubscriptionExpression, + depth: Int + ): Any? { val array = (expr.arrayExpression as? DeclaredReferenceExpression)?.refersTo as? VariableDeclaration val ile = array?.initializer as? InitializerListExpression ile?.let { - return evaluate( + return evaluateInternal( it.initializers .filterIsInstance(KeyValueExpression::class.java) .firstOrNull { kve -> (kve.key as? Literal<*>)?.value == (expr.subscriptExpression as? Literal<*>)?.value } - ?.value + ?.value, + depth + 1 ) } if (array?.initializer is Literal<*>) { @@ -287,22 +307,22 @@ class ValueEvaluator( } if (expr.arrayExpression is ArraySubscriptionExpression) { - return evaluate(expr.arrayExpression) + return evaluateInternal(expr.arrayExpression, depth + 1) } return cannotEvaluate(expr, this) } - private fun handleConditionalExpression(expr: ConditionalExpression): Any? { + protected open fun handleConditionalExpression(expr: ConditionalExpression, depth: Int): Any? { // Assume that condition is a binary operator if (expr.condition is BinaryOperator) { - val lhs = evaluate((expr.condition as? BinaryOperator)?.lhs) - val rhs = evaluate((expr.condition as? BinaryOperator)?.rhs) + val lhs = evaluateInternal((expr.condition as? BinaryOperator)?.lhs, depth) + val rhs = evaluateInternal((expr.condition as? BinaryOperator)?.rhs, depth) return if (lhs == rhs) { - evaluate(expr.thenExpr) + evaluateInternal(expr.thenExpr, depth + 1) } else { - evaluate(expr.elseExpr) + evaluateInternal(expr.elseExpr, depth + 1) } } @@ -313,14 +333,17 @@ class ValueEvaluator( * Tries to compute the constant value of a reference. It therefore checks the incoming data * flow edges. */ - private fun handleDeclaredReferenceExpression(expr: DeclaredReferenceExpression): Any? { + protected open fun handleDeclaredReferenceExpression( + expr: DeclaredReferenceExpression, + depth: Int + ): Any? { // For a reference, we are interested into its last assignment into the reference // denoted by the previous DFG edge val prevDFG = expr.prevDFG if (prevDFG.size == 1) // There's only one incoming DFG edge, so we follow this one. - return evaluate(prevDFG.first()) + return evaluateInternal(prevDFG.first(), depth + 1) // We are only interested in expressions val expressions = prevDFG.filterIsInstance() @@ -345,10 +368,22 @@ class ValueEvaluator( ) return cannotEvaluate(expr, this) } - return evaluate(decl.firstOrNull()) + return evaluateInternal(decl.firstOrNull(), depth + 1) } - return evaluate(expressions.firstOrNull()) + return evaluateInternal(expressions.firstOrNull(), depth + 1) + } +} + +internal fun Number.negate(): Number { + return when (this) { + is Int -> -this + is Long -> -this + is Short -> -this + is Byte -> -this + is Double -> -this + is Float -> -this + else -> 0 } } @@ -357,7 +392,7 @@ class ValueEvaluator( * and compares an arbitrary [Number] with another [Number] using the dedicated compareTo functions * for the individual implementations of [Number], such as [Int.compareTo]. */ -private fun Number.compareTo(other: T): Int { +fun Number.compareTo(other: T): Int { return when { this is Byte && other is Double -> this.compareTo(other) this is Byte && other is Float -> this.compareTo(other) diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/EvaluateExtensions.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/EvaluateExtensions.kt index cc038e231d..86bf86e4b4 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/EvaluateExtensions.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/EvaluateExtensions.kt @@ -29,12 +29,12 @@ import de.fraunhofer.aisec.cpg.graph.declarations.Declaration import de.fraunhofer.aisec.cpg.graph.statements.expressions.ArrayCreationExpression import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression -fun Expression.evaluate(): Any? { - return ValueEvaluator().evaluate(this) +fun Expression.evaluate(evaluator: ValueEvaluator = ValueEvaluator()): Any? { + return evaluator.evaluate(this) } -fun Declaration.evaluate(): Any? { - return ValueEvaluator().evaluate(this) +fun Declaration.evaluate(evaluator: ValueEvaluator = ValueEvaluator()): Any? { + return evaluator.evaluate(this) } val ArrayCreationExpression.capacity: Int diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt new file mode 100644 index 0000000000..8a5e77c47f --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt @@ -0,0 +1,423 @@ +/* + * Copyright (c) 2022, 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.query + +import de.fraunhofer.aisec.cpg.ExperimentalGraph +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.analysis.MultiValueEvaluator +import de.fraunhofer.aisec.cpg.analysis.NumberSet +import de.fraunhofer.aisec.cpg.analysis.SizeEvaluator +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression +import de.fraunhofer.aisec.cpg.graph.types.Type + +/** + * Evaluates if the conditions specified in [mustSatisfy] hold for all nodes in the graph. + * + * The optional argument [sel] can be used to filter nodes for which the condition has to be + * fulfilled. This filter should be rather simple in most cases since its evaluation is not part of + * the resulting reasoning chain. + * + * This method can be used similar to the logical implication to test "sel => mustSatisfy". + */ +@ExperimentalGraph +inline fun Node.allExtended( + noinline sel: ((T) -> Boolean)? = null, + noinline mustSatisfy: (T) -> QueryTree +): QueryTree { + var nodes = + if (this is TranslationResult) { + this.graph.nodes.filterIsInstance() + } else { + this.astChildren.filterIsInstance() + } + + // filter the nodes according to the selector + if (sel != null) { + nodes = nodes.filter(sel) + } + + val queryChildren = + nodes.map { n -> + val res = mustSatisfy(n) + res.stringRepresentation = "Starting at $n: " + res.stringRepresentation + res + } + return QueryTree(queryChildren.all { it.value }, queryChildren.toMutableList(), "all") +} + +/** + * Evaluates if the conditions specified in [mustSatisfy] hold for all nodes in the graph. The + * optional argument [sel] can be used to filter nodes for which the condition has to be fulfilled. + * + * This method can be used similar to the logical implication to test "sel => mustSatisfy". + */ +@ExperimentalGraph +inline fun Node.all( + noinline sel: ((T) -> Boolean)? = null, + noinline mustSatisfy: (T) -> Boolean +): Pair> { + var nodes = + if (this is TranslationResult) { + this.graph.nodes.filterIsInstance() + } else { + this.astChildren.filterIsInstance() + } + + // filter the nodes according to the selector + if (sel != null) { + nodes = nodes.filter(sel) + } + + val failedNodes = nodes.filterNot(mustSatisfy) as List + return Pair(failedNodes.isEmpty(), failedNodes) +} + +/** + * Evaluates if the conditions specified in [mustSatisfy] hold for at least one node in the graph. + * + * The optional argument [sel] can be used to filter nodes which are considered during the + * evaluation. This filter should be rather simple in most cases since its evaluation is not part of + * the resulting reasoning chain. + */ +@ExperimentalGraph +inline fun Node.existsExtended( + noinline sel: ((T) -> Boolean)? = null, + noinline mustSatisfy: (T) -> QueryTree +): QueryTree { + var nodes = + if (this is TranslationResult) { + this.graph.nodes.filterIsInstance() + } else { + this.astChildren.filterIsInstance() + } + + // filter the nodes according to the selector + if (sel != null) { + nodes = nodes.filter(sel) + } + + val queryChildren = + nodes.map { n -> + val res = mustSatisfy(n) + res.stringRepresentation = "Starting at $n: " + res.stringRepresentation + res + } + return QueryTree(queryChildren.any { it.value }, queryChildren.toMutableList(), "exists") +} + +/** + * Evaluates if the conditions specified in [mustSatisfy] hold for at least one node in the graph. + * The optional argument [sel] can be used to filter nodes which are considered during the + * evaluation. + */ +@ExperimentalGraph +inline fun Node.exists( + noinline sel: ((T) -> Boolean)? = null, + noinline mustSatisfy: (T) -> Boolean +): Pair> { + var nodes = + if (this is TranslationResult) { + this.graph.nodes.filterIsInstance() + } else { + this.astChildren.filterIsInstance() + } + + // filter the nodes according to the selector + if (sel != null) { + nodes = nodes.filter(sel) + } + + val queryChildren = nodes.filter(mustSatisfy) as List + return Pair(queryChildren.isNotEmpty(), queryChildren) +} + +/** + * Evaluates the size of a node. The implementation is very, very basic! + * + * @eval can be used to specify the evaluator but this method has to interpret the result correctly! + */ +fun sizeof(n: Node?, eval: ValueEvaluator = SizeEvaluator()): QueryTree { + // The cast could potentially go wrong, but if it's not an int, it's not really a size + return QueryTree(eval.evaluate(n) as? Int ?: -1, mutableListOf(), "sizeof($n)") +} + +/** + * Retrieves the minimal value of the node. + * + * @eval can be used to specify the evaluator but this method has to interpret the result correctly! + */ +fun min(n: Node?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree { + val evalRes = eval.evaluate(n) + if (evalRes is Number) { + return QueryTree(evalRes, mutableListOf(QueryTree(n)), "min($n)") + } + // Extend this when we have other evaluators. + return QueryTree((evalRes as? NumberSet)?.min() ?: -1, mutableListOf(), "min($n)") +} + +/** + * Retrieves the minimal value of the nodes in the list. + * + * @eval can be used to specify the evaluator but this method has to interpret the result correctly! + */ +fun min(n: List?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree { + var result = Long.MAX_VALUE + if (n == null) return QueryTree(result, mutableListOf(QueryTree(null))) + + for (node in n) { + val evalRes = eval.evaluate(node) + if (evalRes is Number && evalRes.toLong() < result) { + result = evalRes.toLong() + } else if (evalRes is NumberSet && evalRes.min() < result) { + result = evalRes.min() + } + // Extend this when we have other evaluators. + } + return QueryTree(result, mutableListOf(), "min($n)") +} + +/** + * Retrieves the maximal value of the nodes in the list. + * + * @eval can be used to specify the evaluator but this method has to interpret the result correctly! + */ +fun max(n: List?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree { + var result = Long.MIN_VALUE + if (n == null) return QueryTree(result, mutableListOf(QueryTree(null))) + + for (node in n) { + val evalRes = eval.evaluate(node) + if (evalRes is Number && evalRes.toLong() > result) { + result = evalRes.toLong() + } else if (evalRes is NumberSet && evalRes.max() > result) { + result = evalRes.max() + } + // Extend this when we have other evaluators. + } + return QueryTree(result, mutableListOf(), "max($n)") +} + +/** + * Retrieves the maximal value of the node. + * + * @eval can be used to specify the evaluator but this method has to interpret the result correctly! + */ +fun max(n: Node?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree { + val evalRes = eval.evaluate(n) + if (evalRes is Number) { + return QueryTree(evalRes, mutableListOf(QueryTree(n))) + } + // Extend this when we have other evaluators. + return QueryTree((evalRes as? NumberSet)?.max() ?: -1, mutableListOf(), "max($n)") +} + +/** Checks if a data flow is possible between the nodes [from] as a source and [to] as sink. */ +fun dataFlow(from: Node, to: Node): QueryTree { + val evalRes = from.followNextDFGEdgesUntilHit { it == to } + val allPaths = evalRes.fulfilled.map { QueryTree(it) }.toMutableList() + allPaths.addAll(evalRes.failed.map { QueryTree(it) }) + return QueryTree( + evalRes.fulfilled.isNotEmpty(), + allPaths.toMutableList(), + "data flow from $from to $to" + ) +} + +/** Checks if a path of execution flow is possible between the nodes [from] and [to]. */ +fun executionPath(from: Node, to: Node): QueryTree { + val evalRes = from.followNextEOGEdgesUntilHit { it == to } + val allPaths = evalRes.fulfilled.map { QueryTree(it) }.toMutableList() + allPaths.addAll(evalRes.failed.map { QueryTree(it) }) + return QueryTree( + evalRes.fulfilled.isNotEmpty(), + allPaths.toMutableList(), + "executionPath($from, $to)" + ) +} + +/** + * Checks if a path of execution flow is possible starting at the node [from] and fulfilling the + * requirement specified in [predicate]. + */ +fun executionPath(from: Node, predicate: (Node) -> Boolean): QueryTree { + val evalRes = from.followNextEOGEdgesUntilHit(predicate) + val allPaths = evalRes.fulfilled.map { QueryTree(it) }.toMutableList() + allPaths.addAll(evalRes.failed.map { QueryTree(it) }) + return QueryTree( + evalRes.fulfilled.isNotEmpty(), + allPaths.toMutableList(), + "executionPath($from, $predicate)" + ) +} + +/** + * Checks if a path of execution flow is possible ending at the node [to] and fulfilling the + * requirement specified in [predicate]. + */ +fun executionPathBackwards(to: Node, predicate: (Node) -> Boolean): QueryTree { + val evalRes = to.followPrevEOGEdgesUntilHit(predicate) + val allPaths = evalRes.fulfilled.map { QueryTree(it) }.toMutableList() + allPaths.addAll(evalRes.failed.map { QueryTree(it) }) + return QueryTree( + evalRes.fulfilled.isNotEmpty(), + allPaths.toMutableList(), + "executionPathBackwards($to, $predicate)" + ) +} + +/** Calls [ValueEvaluator.evaluate] for this expression, thus trying to resolve a constant value. */ +operator fun Expression?.invoke(): QueryTree { + return QueryTree(this?.evaluate(), mutableListOf(QueryTree(this))) +} + +/** + * Determines the maximal value. Only works for a couple of types! TODO: This method needs + * improvement! It only works for Java types! + */ +fun maxSizeOfType(type: Type): QueryTree { + val maxVal = + when (type.typeName) { + "byte" -> Byte.MAX_VALUE + "short" -> Short.MAX_VALUE + "int" -> Int.MAX_VALUE + "long" -> Long.MAX_VALUE + "float" -> Float.MAX_VALUE + "double" -> Double.MAX_VALUE + else -> Long.MAX_VALUE + } + return QueryTree(maxVal, mutableListOf(QueryTree(type)), "maxSizeOfType($type)") +} + +/** + * Determines the minimal value. Only works for a couple of types! TODO: This method needs + * improvement! It only works for Java types! + */ +fun minSizeOfType(type: Type): QueryTree { + val maxVal = + when (type.typeName) { + "byte" -> Byte.MIN_VALUE + "short" -> Short.MIN_VALUE + "int" -> Int.MIN_VALUE + "long" -> Long.MIN_VALUE + "float" -> Float.MIN_VALUE + "double" -> Double.MIN_VALUE + else -> Long.MIN_VALUE + } + return QueryTree(maxVal, mutableListOf(QueryTree(type)), "minSizeOfType($type)") +} + +/** The size of this expression. It uses the default argument for `eval` of [size] */ +val Expression.size: QueryTree + get() { + return sizeof(this) + } + +/** + * The minimal integer value of this expression. It uses the default argument for `eval` of [min] + */ +val Expression.min: QueryTree + get() { + return min(this) + } + +/** + * The maximal integer value of this expression. It uses the default argument for `eval` of [max] + */ +val Expression.max: QueryTree + get() { + return max(this) + } + +/** Calls [ValueEvaluator.evaluate] for this expression, thus trying to resolve a constant value. */ +val Expression.value: QueryTree + get() { + return QueryTree(evaluate(), mutableListOf(), "$this") + } + +/** + * Calls [ValueEvaluator.evaluate] for this expression, thus trying to resolve a constant value. The + * result is interpreted as an integer. + */ +val Expression.intValue: QueryTree? + get() { + val evalRes = evaluate() as? Int ?: return null + return QueryTree(evalRes, mutableListOf(), "$this") + } + +/** + * Does some magic to identify if the value which is in [from] also reaches [to]. To do so, it goes + * some data flow steps backwards in the graph (ideally to find the last assignment) and then + * follows this value to the node [to]. + */ +fun allNonLiteralsFromFlowTo(from: Node, to: Node, allPaths: List>): QueryTree { + return when (from) { + is CallExpression -> { + val prevEdges = + from.prevDFG + .fold( + mutableListOf(), + { l, e -> + if (e !is Literal<*>) { + l.add(e) + } + l + } + ) + .toMutableList() + prevEdges.addAll(from.arguments) + // For a call, we collect the incoming data flows (typically only the arguments) + val prevQTs = prevEdges.map { allNonLiteralsFromFlowTo(it, to, allPaths) } + QueryTree(prevQTs.all { it.value }, prevQTs.toMutableList()) + } + is Literal<*> -> + QueryTree(true, mutableListOf(QueryTree(from)), "DF Irrelevant for Literal node") + else -> { + // We go one step back to see if that one goes into to but also check that no assignment + // to from happens in the paths between from and to + val prevQTs = from.prevDFG.map { dataFlow(it, to) } + var noAssignmentToFrom = + allPaths.none { + it.any { it2 -> + if (it2 is Assignment) { + val prevMemberFrom = (from as? MemberExpression)?.prevDFG + val nextMemberTo = (it2.target as? MemberExpression)?.nextDFG + it2.target == from || + prevMemberFrom != null && + nextMemberTo != null && + prevMemberFrom.any { it3 -> nextMemberTo.contains(it3) } + } else { + false + } + } + } + QueryTree(prevQTs.all { it.value } && noAssignmentToFrom, prevQTs.toMutableList()) + } + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt new file mode 100644 index 0000000000..76b2e62d66 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2022, 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.query + +import de.fraunhofer.aisec.cpg.graph.compareTo + +/** + * Holds the [value] to which the statements have been evaluated. The [children] define previous + * steps of the evaluation, thus building a tree of all steps of the evaluation recursively until we + * reach the nodes of the CPG. This is necessary if we want to store all steps which are performed + * when evaluating a query. It helps to make the reasoning of the query more understandable to the + * user and gives an analyst the maximum of information available. + * + * Numerous methods allow to evaluate the queries while keeping track of all the steps. Currently, + * the following operations are supported: + * - **eq**: Equality of two values. + * - **ne**: Inequality of two values. + * - **IN**: Checks if a value is contained in a [Collection] + * - **IS**: Checks if a value implements a type ([Class]). + * + * Additionally, some functions are available only for certain types of values. + * + * For boolean values: + * - **and**: Logical and operation (&&) + * - **or**: Logical or operation (||) + * - **xor**: Logical exclusive or operation (xor) + * - **implies**: Logical implication + * + * For numeric values: + * - **gt**: Grater than (>) + * - **ge**: Grater than or equal (>=) + * - **lt**: Less than (<) + * - **le**: Less than or equal (<=) + */ +open class QueryTree( + open var value: T, + open val children: MutableList> = mutableListOf(), + open var stringRepresentation: String = "" +) : Comparable> { + fun printNicely(depth: Int = 0): String { + var res = + " ".repeat(depth) + + "$stringRepresentation (==> $value)\n" + + "--------".repeat(depth + 1) + if (children.isNotEmpty()) { + res += "\n" + children.forEach { c -> + val next = c.printNicely(depth + 2) + if (next.isNotEmpty()) res += next + "\n" + "--------".repeat(depth + 1) + "\n" + } + } + return res + } + + /** Checks for equality of two [QueryTree]s. */ + infix fun eq(other: QueryTree): QueryTree { + val result = this.value == other.value + return QueryTree(result, mutableListOf(this, other), "${this.value} == ${other.value}") + } + + /** + * Checks for equality of a [QueryTree] with a value of the same type (e.g. useful to check for + * constants). + */ + infix fun eq(other: T): QueryTree { + val result = this.value == other + return QueryTree(result, mutableListOf(this, QueryTree(other)), "${this.value} == $value") + } + + /** Checks for inequality of two [QueryTree]s. */ + infix fun ne(other: QueryTree): QueryTree { + val result = this.value != other.value + return QueryTree(result, mutableListOf(this, other), "${this.value} != ${other.value}") + } + + /** + * Checks for inequality of a [QueryTree] with a value of the same type (e.g. useful to check + * for constants). + */ + infix fun ne(other: T): QueryTree { + val result = this.value != other + return QueryTree(result, mutableListOf(this, QueryTree(other)), "${this.value} != $value") + } + + /** Checks if the value is contained in the collection of the other [QueryTree]. */ + infix fun IN(other: QueryTree>): QueryTree { + val result = other.value.contains(this.value) + return QueryTree(result, mutableListOf(this, other), "${this.value} in ${other.value}") + } + + /** Checks if the value is contained in the collection [other]. */ + infix fun IN(other: Collection<*>): QueryTree { + val result = other.contains(this.value) + return QueryTree(result, mutableListOf(this, QueryTree(other)), "${this.value} in $other") + } + + /** Checks if the value is a member of the type of the other [QueryTree]. */ + infix fun IS(other: QueryTree>): QueryTree { + val result = other.value.isInstance(this.value) + return QueryTree(result, mutableListOf(this, other), "${this.value} is ${other.value}") + } + + /** Checks if the value is a member of the type of [oter]. */ + infix fun IS(other: Class<*>): QueryTree { + val result = other.isInstance(this.value) + return QueryTree(result, mutableListOf(this, QueryTree(other)), "${this.value} is $other") + } + + override fun hashCode(): Int { + return value?.hashCode() ?: 0 + } + + override fun equals(other: Any?): Boolean { + if (other is QueryTree<*>) { + return this.value?.equals(other.value) ?: false + } + + return super.equals(other) + } + + override fun compareTo(other: QueryTree): Int { + if (this.value is Number && other.value is Number) { + return (this.value as Number).compareTo(other.value as Number) + } else if (this.value is Comparable<*> && other.value is Comparable<*>) { + return (this.value as Comparable).compareTo(other.value as Any) + } + throw QueryException("Cannot compare objects of type ${this.value} and ${other.value}") + } + + public operator fun compareTo(other: Number): Int { + if (this.value is Number) { + return (this.value as Number).compareTo(other) + } + throw QueryException("Cannot compare objects of type ${this.value} and $other") + } +} + +/** Performs a logical and (&&) operation between the values of two [QueryTree]s. */ +infix fun QueryTree.and(other: QueryTree): QueryTree { + return QueryTree( + this.value && other.value, + mutableListOf(this, other), + stringRepresentation = "${this.value} && ${other.value}" + ) +} + +/** Performs a logical or (||) operation between the values of two [QueryTree]s. */ +infix fun QueryTree.or(other: QueryTree): QueryTree { + return QueryTree( + this.value || other.value, + mutableListOf(this, other), + stringRepresentation = "${this.value} || ${other.value}" + ) +} + +/** Performs a logical xor operation between the values of two [QueryTree]s. */ +infix fun QueryTree.xor(other: QueryTree): QueryTree { + return QueryTree( + this.value xor other.value, + mutableListOf(this, other), + stringRepresentation = "${this.value} xor ${other.value}" + ) +} + +/** Evaluates a logical implication (->) operation between the values of two [QueryTree]s. */ +infix fun QueryTree.implies(other: QueryTree): QueryTree { + return QueryTree( + !this.value || other.value, + mutableListOf(this, other), + stringRepresentation = "${this.value} => ${other.value}" + ) +} + +/** Evaluates a logical implication (->) operation between the values of two [QueryTree]s. */ +infix fun QueryTree.implies(other: Lazy>): QueryTree { + return QueryTree( + !this.value || other.value.value, + if (!this.value) mutableListOf(this) else mutableListOf(this, other.value), + stringRepresentation = + if (!this.value) "false => XYZ" else "${this.value} => ${other.value}" + ) +} + +/** Compares the numeric values of two [QueryTree]s for this being "greater than" (>) [other]. */ +infix fun QueryTree.gt(other: QueryTree): QueryTree { + val result = this.value.compareTo(other.value) > 0 + return QueryTree(result, mutableListOf(this, other), "${this.value} > ${other.value}") +} + +/** + * Compares the numeric values of a [QueryTree] and another number for this being "greater than" (>) + * [other]. + */ +infix fun QueryTree.gt(other: S): QueryTree { + val result = this.value.compareTo(other) > 0 + return QueryTree(result, mutableListOf(this, QueryTree(other)), "${this.value} > $other") +} + +/** + * Compares the numeric values of two [QueryTree]s for this being "greater than or equal" (>=) + * [other]. + */ +infix fun QueryTree.ge(other: QueryTree): QueryTree { + val result = this.value.compareTo(other.value) >= 0 + return QueryTree(result, mutableListOf(this, other), "${this.value} >= ${other.value}") +} + +/** + * Compares the numeric values of a [QueryTree] and another number for this being "greater than or + * equal" (>=) [other]. + */ +infix fun QueryTree.ge(other: S): QueryTree { + val result = this.value.compareTo(other) >= 0 + return QueryTree(result, mutableListOf(this, QueryTree(other)), "${this.value} >= $other") +} + +/** Compares the numeric values of two [QueryTree]s for this being "less than" (<) [other]. */ +infix fun QueryTree.lt(other: QueryTree): QueryTree { + val result = this.value.compareTo(other.value) < 0 + return QueryTree(result, mutableListOf(this, other), "${this.value} < ${other.value}") +} + +/** + * Compares the numeric values of a [QueryTree] and another number for this being "less than" (<) + * [other]. + */ +infix fun QueryTree.lt(other: S): QueryTree { + val result = this.value.compareTo(other) < 0 + return QueryTree(result, mutableListOf(this, QueryTree(other)), "${this.value} < $other") +} + +/** + * Compares the numeric values of two [QueryTree]s for this being "less than or equal" (=) [other]. + */ +infix fun QueryTree.le(other: QueryTree): QueryTree { + val result = this.value.compareTo(other.value) <= 0 + return QueryTree(result, mutableListOf(this, other), "${this.value} <= ${other.value}") +} + +/** + * Compares the numeric values of a [QueryTree] and another number for this being "less than or + * equal" (<=) [other]. + */ +infix fun QueryTree.le(other: S): QueryTree { + val result = this.value.compareTo(other) <= 0 + return QueryTree(result, mutableListOf(this, QueryTree(other)), "${this.value} <= $other") +} + +/** Negates the value of [arg] and returns the resulting [QueryTree]. */ +fun not(arg: QueryTree): QueryTree { + val result = !arg.value + return QueryTree(result, mutableListOf(arg), "! ${arg.value}") +} + +/** Negates the value of [arg] and returns the resulting [QueryTree]. */ +fun not(arg: Boolean): QueryTree { + val result = !arg + return QueryTree(result, mutableListOf(QueryTree(arg)), "! ${arg}") +} + +/** + * This is a small wrapper to create a [QueryTree] containing a constant value, so that it can be + * used to in comparison with other [QueryTree] objects. + */ +fun > const(n: T): QueryTree { + return QueryTree(n, stringRepresentation = "$n") +} + +/** + * This is a small wrapper to create a [QueryTree] containing a constant value, so that it can be + * used to in comparison with other [QueryTree] objects. + */ +fun const(n: T): QueryTree { + return QueryTree(n, stringRepresentation = "$n") +} + +class QueryException(override val message: String) : Exception(message) diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/README.md b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/README.md new file mode 100644 index 0000000000..9c25867dd7 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/README.md @@ -0,0 +1,113 @@ +# The Query API + +The Query API provides a way validate if nodes in the graph fulfil certain requirements. It is a mixture of typical logical expressions (e.g. and, or, xor, implies), quantors (e.g. forall, exists), comparisons (e.g. <, >, ==, !=), some special operations (e.g., `in` to check for collections or `is` for types) and a couple of operations. + +## Operation modes +The Query API has two modes of operations which determine the depth of the output: +1. The detailed mode reasons about every single step performed to check if the query is fulfilled. +2. The less detailed mode only provides the final output (true, false) and the nodes which serve as input. + +To use the detailed mode, it is necessary to use specific operators in a textual representation whereas the other modes relies on the operators as known from any programming language. + +The following example output from the test case `testMemcpyTooLargeQuery2` shows the difference: + +**Less detailed:** +``` +[CallExpression[name=memcpy,location=vulnerable.cpp(3:5-3:38),type=UNKNOWN,base=]] +``` +**Detailed mode:** +``` +all (==> false) +-------- + Starting at CallExpression[name=memcpy,location=vulnerable.cpp(3:5-3:38),type=UNKNOWN,base=]: 5 > 11 (==> false) +------------------------ + sizeof(DeclaredReferenceExpression[DeclaredReferenceExpression[name=array,location=vulnerable.cpp(3:12-3:17),type=PointerType[name=char[]]],refersTo=VariableDeclaration[name=array,location=vulnerable.cpp(2:10-2:28),initializer=Literal[location=vulnerable.cpp(2:21-2:28),type=PointerType[name=char[]],value=hello]]]) (==> 5) +---------------------------------------- +------------------------ + sizeof(Literal[location=vulnerable.cpp(3:19-3:32),type=PointerType[name=char[]],value=Hello world]) (==> 11) +---------------------------------------- +------------------------ +-------- +``` + +## Operators of the detailed mode +Numerous methods allow to evaluate the queries while keeping track of all the steps. Currently, the following operations are supported: +- **eq**: Equality of two values. +- **ne**: Inequality of two values. +- **IN**: Checks if a value is contained in a [Collection] +- **IS**: Checks if a value implements a type ([Class]). + +Additionally, some functions are available only for certain types of values. + +For boolean values: +- **and**: Logical and operation (&&) +- **or**: Logical or operation (||) +- **xor**: Logical exclusive or operation (xor) +- **implies**: Logical implication + +For numeric values: +- **gt**: Grater than (>) +- **ge**: Grater than or equal (>=) +- **lt**: Less than (<) +- **le**: Less than or equal (<=) + +**Note:** The detailed mode and its operators require the user to take care of the correct order. I.e., the user has to put the brackets! + +## Operators of the less detailed mode +Numerous methods allow to evaluate the queries: +- **==**: Equality of two values. +- **!=**: Inequality of two values. +- **in** : Checks if a value is contained in a [Collection]. The value of a query tree has to be accessed by the property `value`. +- **is**: Checks if a value implements a type ([Class]). The value of a query tree has to be accessed by the property `value`. +- **&&**: Logical and operation +- **||**: Logical or operation +- **xor**: Logical exclusive or operation +- **>**: Grater than +- **>=**: Grater than or equal +- **<**: Less than +- **<=**: Less than or equal + +## Functions of the Query API +Since these operators cannot cover all interesting values, we provide an initial set of analyses and functions to use them. These are: +- **min(n: Node)**: Minimal value of a node +- **max(n: Node)**: Maximal value of a node +- **sizeof(n: Node)**: The length of an array or string +- **dataFlow(from: Node, to: Node)**: Checks if a data flow is possible between the nodes `from` as a source and `to` as sink. +- **executionPath(from: Node, to: Node)**: Checks if a path of execution flow is possible between the nodes `from` and `to`. +- **executionPath(from: Node, predicate: (Node) -> Boolean)**: Checks if a path of execution flow is possible starting at node `from` and fulfilling the requirement specified in `predicate`. + +## Running a query +The query can use any of these operators and functions and additionally operate on the fields of a node. To simplify the generation of queries, we provide an initial set of extensions for certain nodes. + +An example for such a query could look as follows for the detailed mode: +```kotlin +val memcpyTooLargeQuery = { node: CallExpression -> + sizeof(node.arguments[0]) gt sizeof(node.arguments[1]) +} +``` +The same query in the less detailed mode: +```kotlin +val memcpyTooLargeQuery = { node: CallExpression -> + sizeof(node.arguments[0]) > sizeof(node.arguments[1]) +} +``` + +After assembling a query of the respective operators and functions, we want to run it for a subset of nodes in the graph. We therefore provide two operators: `all` (or `allExtended` for the detailed output) and `exists` (or `existsExtended` for the detailed output). Both are used in a similar way. +They enable the user to optionally specify conditions to determine on which nodes we want to run a query (e.g., only on `CallExpression`s which call a function called "memcpy"). + +The following snippets use the queries from above to run them on all calls of the function "memcpy" contained in the `TranslationResult` `result`: +```kotlin +val queryTreeResult = + result.allExtended( + { it.name == "memcpy" }, + { sizeof(it.arguments[0]) gt sizeof(it.arguments[1]) } + ) +``` +Less detailled: +```kotlin +val queryTreeResult = + result.all( + { it.name == "memcpy" }, + { sizeof(it.arguments[0]) > sizeof(it.arguments[1]) } + ) +``` \ No newline at end of file diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/MultiValueEvaluatorTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/MultiValueEvaluatorTest.kt new file mode 100644 index 0000000000..a108c7269c --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/MultiValueEvaluatorTest.kt @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2022, 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.analysis + +import de.fraunhofer.aisec.cpg.TestUtils +import de.fraunhofer.aisec.cpg.graph.bodyOrNull +import de.fraunhofer.aisec.cpg.graph.byNameOrNull +import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration +import de.fraunhofer.aisec.cpg.graph.evaluate +import de.fraunhofer.aisec.cpg.graph.statements.CompoundStatement +import de.fraunhofer.aisec.cpg.graph.statements.DeclarationStatement +import de.fraunhofer.aisec.cpg.graph.statements.ForStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.passes.EdgeCachePass +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class MultiValueEvaluatorTest { + @Test + fun testSingleValue() { + val topLevel = Path.of("src", "test", "resources", "value_evaluation") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("example.cpp").toFile()), + topLevel, + true + ) + + assertNotNull(tu) + + val main = tu.byNameOrNull("main") + assertNotNull(main) + + val b = main.bodyOrNull()?.singleDeclaration + assertNotNull(b) + + var value = b.evaluate() + assertEquals(2L, value) + + val printB = main.bodyOrNull() + assertNotNull(printB) + + val evaluator = MultiValueEvaluator() + value = evaluator.evaluate(printB.arguments.firstOrNull()) as ConcreteNumberSet + assertEquals(value.min(), value.max()) + assertEquals(2L, value.min()) + + val path = evaluator.path + assertEquals(4, path.size) + + val printA = main.bodyOrNull(1) + assertNotNull(printA) + + value = evaluator.evaluate(printA.arguments.firstOrNull()) as ConcreteNumberSet + assertEquals(value.min(), value.max()) + assertEquals(2, value.min()) + + val c = main.bodyOrNull(2)?.singleDeclaration + assertNotNull(c) + + value = evaluator.evaluate(c) + assertEquals(3L, value) + + val d = main.bodyOrNull(3)?.singleDeclaration + assertNotNull(d) + + value = evaluator.evaluate(d) + assertEquals(2L, value) + + val e = main.bodyOrNull(4)?.singleDeclaration + assertNotNull(e) + value = evaluator.evaluate(e) + assertEquals(3.5, value) + + val f = main.bodyOrNull(5)?.singleDeclaration + assertNotNull(f) + value = evaluator.evaluate(f) + assertEquals(10L, value) + + val g = main.bodyOrNull(6)?.singleDeclaration + assertNotNull(g) + value = evaluator.evaluate(g) as ConcreteNumberSet + assertEquals(value.min(), value.max()) + assertEquals(-3L, value.min()) + + val i = main.bodyOrNull(8)?.singleDeclaration + assertNotNull(i) + value = evaluator.evaluate(i) + assertFalse(value as Boolean) + + val j = main.bodyOrNull(9)?.singleDeclaration + assertNotNull(j) + value = evaluator.evaluate(j) + assertFalse(value as Boolean) + + val k = main.bodyOrNull(10)?.singleDeclaration + assertNotNull(k) + value = evaluator.evaluate(k) + assertFalse(value as Boolean) + + val l = main.bodyOrNull(11)?.singleDeclaration + assertNotNull(l) + value = evaluator.evaluate(l) + assertFalse(value as Boolean) + + val m = main.bodyOrNull(12)?.singleDeclaration + assertNotNull(m) + value = evaluator.evaluate(m) + assertFalse(value as Boolean) + } + + @Test + fun testMultipleValues() { + val topLevel = Path.of("src", "test", "resources", "value_evaluation") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("cfexample.cpp").toFile()), + topLevel, + true + ) + + assertNotNull(tu) + + val main = tu.byNameOrNull("main") + assertNotNull(main) + + val b = main.bodyOrNull()?.singleDeclaration + assertNotNull(b) + + var printB = main.bodyOrNull() + assertNotNull(printB) + + val evaluator = MultiValueEvaluator() + var value = printB.arguments.firstOrNull()?.evaluate() + assertTrue(value is String) // could not evaluate + + value = evaluator.evaluate(printB.arguments.firstOrNull()) as ConcreteNumberSet + assertEquals(setOf(1, 2), value.values) + + printB = main.bodyOrNull(1) + assertNotNull(printB) + value = evaluator.evaluate(printB.arguments.firstOrNull()) as ConcreteNumberSet + assertEquals(setOf(0, 1, 2), value.values) + + printB = main.bodyOrNull(2) + assertNotNull(printB) + value = evaluator.evaluate(printB.arguments.firstOrNull()) as ConcreteNumberSet + assertEquals(setOf(0, 1, 2, 4), value.values) + + printB = main.bodyOrNull(3) + assertNotNull(printB) + value = evaluator.evaluate(printB.arguments.firstOrNull()) as ConcreteNumberSet + assertEquals(setOf(-4, -2, -1, 0, 1, 2, 4), value.values) + + printB = main.bodyOrNull(4) + assertNotNull(printB) + value = evaluator.evaluate(printB.arguments.firstOrNull()) as ConcreteNumberSet + assertEquals(setOf(3, 6), value.values) + } + + @Test + fun testLoop() { + val topLevel = Path.of("src", "test", "resources", "value_evaluation") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("cfexample.cpp").toFile()), + topLevel, + true + ) { it.registerPass(EdgeCachePass()) } + + assertNotNull(tu) + + val main = tu.byNameOrNull("loop") + assertNotNull(main) + + val forLoop = main.bodyOrNull() + assertNotNull(forLoop) + + val evaluator = MultiValueEvaluator() + val iVar = ((forLoop.statement as CompoundStatement).statements[0] as BinaryOperator).rhs + val value = evaluator.evaluate(iVar) as ConcreteNumberSet + assertEquals(setOf(0, 1, 2, 3, 4, 5), value.values) + } + + @Test + fun testInterval() { + val interval = Interval() + interval.addValue(0) + assertEquals(0, interval.min()) + assertEquals(0, interval.max()) + interval.addValue(3) + interval.addValue(2) + assertEquals(0, interval.min()) + assertEquals(3, interval.max()) + interval.addValue(-5) + assertEquals(-5, interval.min()) + assertEquals(3, interval.max()) + interval.clear() + assertEquals(Long.MAX_VALUE, interval.min()) + assertEquals(Long.MIN_VALUE, interval.max()) + } + + @Test + fun testConcreteNumberSet() { + val values = ConcreteNumberSet() + values.addValue(0) + assertEquals(setOf(0), values.values) + values.addValue(3) + values.addValue(2) + assertEquals(setOf(0, 2, 3), values.values) + assertEquals(0, values.min()) + assertEquals(3, values.max()) + values.addValue(-5) + assertEquals(setOf(-5, 0, 2, 3), values.values) + assertEquals(-5, values.min()) + assertEquals(3, values.max()) + assertTrue(values.maybe(3)) + assertFalse(values.maybe(1)) + values.clear() + assertEquals(Long.MAX_VALUE, values.min()) + assertEquals(Long.MIN_VALUE, values.max()) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/SizeEvaluatorTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/SizeEvaluatorTest.kt new file mode 100644 index 0000000000..796b327277 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/SizeEvaluatorTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2022, 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.analysis + +import de.fraunhofer.aisec.cpg.TestUtils +import de.fraunhofer.aisec.cpg.graph.bodyOrNull +import de.fraunhofer.aisec.cpg.graph.byNameOrNull +import de.fraunhofer.aisec.cpg.graph.declarations.MethodDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.CompoundStatement +import de.fraunhofer.aisec.cpg.graph.statements.DeclarationStatement +import de.fraunhofer.aisec.cpg.graph.statements.ForStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class SizeEvaluatorTest { + @Test + fun testArraySize() { + val topLevel = Path.of("src", "test", "resources", "value_evaluation") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("size.java").toFile()), + topLevel, + true + ) + + assertNotNull(tu) + + val mainClass = tu.byNameOrNull("MainClass") + assertNotNull(mainClass) + val main = mainClass.byNameOrNull("main") + assertNotNull(main) + + val array = main.bodyOrNull()?.singleDeclaration + assertNotNull(array) + + val evaluator = SizeEvaluator() + var value = evaluator.evaluate(array) + assertEquals(3, value) + + val printCall = main.bodyOrNull(0) + assertNotNull(printCall) + + value = evaluator.evaluate(printCall.arguments.firstOrNull()) as Int + assertEquals(3, value) + } + + @Test + fun testArraySizeFromSubscript() { + val topLevel = Path.of("src", "test", "resources", "value_evaluation") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("size.java").toFile()), + topLevel, + true + ) + + assertNotNull(tu) + + val mainClass = tu.byNameOrNull("MainClass") + assertNotNull(mainClass) + val main = mainClass.byNameOrNull("main") + assertNotNull(main) + + val array = main.bodyOrNull()?.singleDeclaration + assertNotNull(array) + + val evaluator = SizeEvaluator() + var value = evaluator.evaluate(array) + assertEquals(3, value) + + val forLoop = main.bodyOrNull(0) + assertNotNull(forLoop) + + val subscriptExpr = + ((forLoop.statement as CompoundStatement).statements[0] as BinaryOperator).lhs + + value = evaluator.evaluate(subscriptExpr) as Int + assertEquals(3, value) + } + + @Test + fun testStringSize() { + val topLevel = Path.of("src", "test", "resources", "value_evaluation") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("size.java").toFile()), + topLevel, + true + ) + + assertNotNull(tu) + + val mainClass = tu.byNameOrNull("MainClass") + assertNotNull(mainClass) + val main = mainClass.byNameOrNull("main") + assertNotNull(main) + val printCall = main.bodyOrNull(1) + assertNotNull(printCall) + + val evaluator = SizeEvaluator() + val value = evaluator.evaluate(printCall.arguments.firstOrNull()) as Int + assertEquals(5, value) + + val strValue = evaluator.evaluate("abcd") as Int + assertEquals(4, strValue) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/QueryTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/QueryTest.kt new file mode 100644 index 0000000000..0f9dde0b1a --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/QueryTest.kt @@ -0,0 +1,951 @@ +/* + * Copyright (c) 2022, 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.query + +import de.fraunhofer.aisec.cpg.ExperimentalGraph +import de.fraunhofer.aisec.cpg.TranslationConfiguration +import de.fraunhofer.aisec.cpg.TranslationManager +import de.fraunhofer.aisec.cpg.analysis.MultiValueEvaluator +import de.fraunhofer.aisec.cpg.analysis.NumberSet +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* +import de.fraunhofer.aisec.cpg.passes.EdgeCachePass +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.jupiter.api.Test + +@ExperimentalGraph +class QueryTest { + @Test + fun testMemcpyTooLargeQuery2() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all( + { it.name == "memcpy" }, + { sizeof(it.arguments[0]) > sizeof(it.arguments[1]) } + ) + + assertFalse(queryTreeResult.first) + + val queryTreeResult2: QueryTree = + result.allExtended( + { it.name == "memcpy" }, + { sizeof(it.arguments[0]) gt sizeof(it.arguments[1]) } + ) + + assertFalse(queryTreeResult2.value) + println(queryTreeResult2.printNicely()) + + // result.calls.name("memcpy").all { n -> sizeof(n.arguments[0]) >= sizeof(n.arguments[1]) } + } + + @Test + fun testMemcpyTooLargeQuery() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "memcpy" }) { + it.arguments[0].size > it.arguments[1].size + } + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[0].size gt it.arguments[1].size } + ) + + assertFalse(queryTreeResult2.value) + } + + @Test + fun testMemcpyTooLargeQueryImplies() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.allExtended( + mustSatisfy = { + (const("memcpy") eq it.name) implies + (lazy { it.arguments[0].size gt it.arguments[1].size }) + } + ) + + assertFalse(queryTreeResult.value) + } + + @Test + fun testUseAfterFree() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "free" }) { outer -> + !executionPath(outer) { + (it as? DeclaredReferenceExpression)?.refersTo == + (outer.arguments[0] as? DeclaredReferenceExpression)?.refersTo + } + .value + } + + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.name == "free" }, + { outer -> + not( + executionPath(outer) { + (it as? DeclaredReferenceExpression)?.refersTo == + (outer.arguments[0] as? DeclaredReferenceExpression)?.refersTo + } + ) + } + ) + + assertFalse(queryTreeResult2.value) + } + + @Test + fun testDoubleFree() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "free" }) { outer -> + !executionPath(outer) { + (it as? CallExpression)?.name == "free" && + ((it as? CallExpression)?.arguments?.getOrNull(0) + as? DeclaredReferenceExpression) + ?.refersTo == + (outer.arguments[0] as? DeclaredReferenceExpression)?.refersTo + } + .value + } + assertFalse(queryTreeResult.first) + println(queryTreeResult.second) + + val queryTreeResult2 = + result.allExtended( + { it.name == "free" }, + { outer -> + not( + executionPath(outer) { + (it as? CallExpression)?.name == "free" && + ((it as? CallExpression)?.arguments?.getOrNull(0) + as? DeclaredReferenceExpression) + ?.refersTo == + (outer.arguments[0] as? DeclaredReferenceExpression)?.refersTo + } + ) + } + ) + + assertFalse(queryTreeResult2.value) + println(queryTreeResult2.printNicely()) + } + + @Test + fun testParameterGreaterThanOrEqualConst() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "memcpy" }) { + it.arguments[2].intValue!! >= const(11) + } + assertTrue(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! ge 11 } + ) + + assertTrue(queryTreeResult2.value) + + val queryTreeResult3 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! ge const(11) } + ) + + assertTrue(queryTreeResult3.value) + } + + @Test + fun testParameterGreaterThanConst() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "memcpy" }) { + it.arguments[2].intValue!! > const(11) + } + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! gt 11 } + ) + + assertFalse(queryTreeResult2.value) + + val queryTreeResult3 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! gt const(11) } + ) + + assertFalse(queryTreeResult3.value) + } + + @Test + fun testParameterLessThanOrEqualConst() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "memcpy" }) { + it.arguments[2].intValue!! <= const(11) + } + assertTrue(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! le 11 } + ) + + assertTrue(queryTreeResult2.value) + + val queryTreeResult3 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! le const(11) } + ) + + assertTrue(queryTreeResult3.value) + } + + @Test + fun testParameterEqualsConst() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "memcpy" }) { + it.arguments[2].intValue!! == const(11) + } + assertTrue(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! eq 11 } + ) + + assertTrue(queryTreeResult2.value) + + val queryTreeResult3 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! eq const(11) } + ) + + assertTrue(queryTreeResult3.value) + } + + @Test + fun testParameterLessThanConst() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "memcpy" }) { + it.arguments[2].intValue!! < const(11) + } + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! lt 11 } + ) + + assertFalse(queryTreeResult2.value) + + val queryTreeResult3 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! lt const(11) } + ) + + assertFalse(queryTreeResult3.value) + } + + @Test + fun testParameterNotEqualsConst() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "memcpy" }) { + it.arguments[2].intValue!! != const(11) + } + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! ne 11 } + ) + + assertFalse(queryTreeResult2.value) + + val queryTreeResult3 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! ne const(11) } + ) + + assertFalse(queryTreeResult3.value) + } + + @Test + fun testParameterIn() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all({ it.name == "memcpy" }) { + it.arguments[2].intValue!!.value in listOf(11, 2, 3) + } + assertTrue(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! IN listOf(11, 2, 3) } + ) + + assertTrue(queryTreeResult2.value) + + val queryTreeResult3 = + result.allExtended( + { it.name == "memcpy" }, + { it.arguments[2].intValue!! IN const(listOf(11, 2, 3)) } + ) + + assertTrue(queryTreeResult3.value) + } + + @Test + fun testAssign() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/assign.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all(mustSatisfy = { (it.value.invoke() as QueryTree) < 5 }) + assertTrue(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + mustSatisfy = { it.value.invoke() as QueryTree lt 5 } + ) + + assertTrue(queryTreeResult2.value) + } + + @Test + fun testOutOfBoundsQuery() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/array.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all( + mustSatisfy = { + max(it.subscriptExpression) < min(it.arraySize) && + min(it.subscriptExpression) >= 0 + } + ) + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + mustSatisfy = { + (max(it.subscriptExpression) lt min(it.arraySize)) and + (min(it.subscriptExpression) ge 0) + } + ) + assertFalse(queryTreeResult2.value) + println(queryTreeResult2.printNicely()) + } + + @Test + fun testOutOfBoundsQueryExists() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/array.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.exists( + mustSatisfy = { + max(it.subscriptExpression) >= min(it.arraySize) || + min(it.subscriptExpression) < 0 + } + ) + assertTrue(queryTreeResult.first) + + val queryTreeResult2 = + result.existsExtended( + mustSatisfy = { + (it.subscriptExpression.max ge it.arraySize.min) or + (it.subscriptExpression.min lt 0) + } + ) + assertTrue(queryTreeResult2.value) + println(queryTreeResult2.printNicely()) + } + + @Test + fun testOutOfBoundsQuery2() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/array2.cpp")) + .defaultPasses() + .defaultLanguages() + .registerPass(EdgeCachePass()) + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all( + mustSatisfy = { + max(it.subscriptExpression) < min(it.arraySize) && + min(it.subscriptExpression) >= 0 + } + ) + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + mustSatisfy = { + (max(it.subscriptExpression) lt min(it.arraySize)) and + (min(it.subscriptExpression) ge 0) + } + ) + assertFalse(queryTreeResult2.value) + println(queryTreeResult2.printNicely()) + } + + @Test + fun testOutOfBoundsQuery3() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/array3.cpp")) + .defaultPasses() + .defaultLanguages() + .registerPass(EdgeCachePass()) + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all( + mustSatisfy = { + max(it.subscriptExpression) < + min( + ((it.arrayExpression as DeclaredReferenceExpression).refersTo + as VariableDeclaration) + .followPrevDFGEdgesUntilHit { node -> + node is ArrayCreationExpression + } + .fulfilled + .map { it2 -> + (it2.last() as ArrayCreationExpression).dimensions[0] + } + ) && min(it.subscriptExpression) > 0 + } + ) + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + mustSatisfy = { + (max(it.subscriptExpression) lt + min( + ((it.arrayExpression as DeclaredReferenceExpression).refersTo + as VariableDeclaration) + .followPrevDFGEdgesUntilHit { node -> + node is ArrayCreationExpression + } + .fulfilled + .map { it2 -> + (it2.last() as ArrayCreationExpression).dimensions[0] + } + )) and (min(it.subscriptExpression) ge 0) + } + ) + assertFalse(queryTreeResult2.value) + println(queryTreeResult2.printNicely()) + } + + @Test + fun testOutOfBoundsQueryCorrect() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/array_correct.cpp")) + .defaultPasses() + .defaultLanguages() + .registerPass(EdgeCachePass()) + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all( + mustSatisfy = { + val max_sub = max(it.subscriptExpression) + val min_dim = min(it.arraySize) + val min_sub = min(it.subscriptExpression) + return@all max_sub < min_dim && min_sub >= 0 + } + ) + assertTrue(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + mustSatisfy = { + val max_sub = max(it.subscriptExpression) + val min_dim = min(it.arraySize) + val min_sub = min(it.subscriptExpression) + return@allExtended (max_sub lt min_dim) and (min_sub ge 0) + } + ) + assertTrue(queryTreeResult2.value) + println(queryTreeResult2.printNicely()) + } + + @Test + fun testDivisionBy0() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all( + { it.operatorCode == "/" }, + { !(it.rhs.evaluate(MultiValueEvaluator()) as NumberSet).maybe(0) } + ) + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.operatorCode == "/" }, + { not((it.rhs.evaluate(MultiValueEvaluator()) as NumberSet).maybe(0)) } + ) + + assertFalse(queryTreeResult2.value) + } + + @Test + fun testIntOverflowAssignment() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/vulnerable.cpp")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all( + { it.target?.type?.isPrimitive == true }, + { + max(it.value) <= maxSizeOfType(it.target!!.type) && + min(it.value) >= minSizeOfType(it.target!!.type) + } + ) + assertFalse(queryTreeResult.first) + + val queryTreeResult2 = + result.allExtended( + { it.target?.type?.isPrimitive == true }, + { + (max(it.value) le maxSizeOfType(it.target!!.type)) and + (min(it.value) ge minSizeOfType(it.target!!.type)) + } + ) + + println(queryTreeResult2.printNicely()) + assertFalse(queryTreeResult2.value) + } + + @Test + fun testDataFlowRequirement() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/Dataflow.java")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.all( + { it.name == "toString" }, + { n1 -> + result + .all( + { it.name == "print" }, + { n2 -> dataFlow(n1, n2.parameters[0]).value } + ) + .first + } + ) + + assertTrue(queryTreeResult.first) + assertEquals(0, queryTreeResult.second.size) + + val queryTreeResultExtended = + result.allExtended( + { it.name == "toString" }, + { n1 -> + result.allExtended( + { it.name == "print" }, + { n2 -> dataFlow(n1, n2.parameters[0]) } + ) + } + ) + + assertTrue(queryTreeResultExtended.value) + assertEquals(1, queryTreeResultExtended.children.size) + + val queryTreeResult2 = + result.all( + { it.name == "test" }, + { n1 -> + result + .all( + { it.name == "print" }, + { n2 -> dataFlow(n1 as Node, n2.parameters[0]).value } + ) + .first + } + ) + + assertTrue(queryTreeResult2.first) + assertEquals(0, queryTreeResult2.second.size) + + val queryTreeResult2Extended = + result.allExtended( + { it.name == "test" }, + { n1 -> + result.allExtended( + { it.name == "print" }, + { n2 -> dataFlow(n1 as Node, n2.parameters[0]) } + ) + } + ) + + assertTrue(queryTreeResult2Extended.value) + assertEquals(1, queryTreeResult2Extended.children.size) + } + + @Test + fun testClomplexDFGAndEOGRequirement() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/ComplexDataflow.java")) + .defaultPasses() + .defaultLanguages() + .registerPass(EdgeCachePass()) + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.allExtended( + { it.name == "highlyCriticalOperation" }, + { n1 -> + val loggingQueryForward = + executionPath(n1, { (it as? CallExpression)?.fqn == "Logger.log" }) + val loggingQueryBackwards = + executionPathBackwards(n1, { (it as? CallExpression)?.fqn == "Logger.log" }) + val allChildren = loggingQueryForward.children + allChildren.addAll(loggingQueryBackwards.children) + val allPaths = + allChildren + .map { (it.value as? List<*>) } + .filter { it != null && it.last() is CallExpression } + val allCalls = allPaths.map { it?.last() as CallExpression } + val dataFlowPaths = + allCalls.map { + allNonLiteralsFromFlowTo( + n1.arguments[0], + it.arguments[1], + allPaths as List> + ) + } + val dataFlowQuery = + QueryTree(dataFlowPaths.all { it.value }, dataFlowPaths.toMutableList()) + + return@allExtended (loggingQueryForward or loggingQueryBackwards) and + dataFlowQuery + } + ) + + println(queryTreeResult.printNicely()) + assertTrue(queryTreeResult.value) + assertEquals(1, queryTreeResult.children.size) + } + + @Test + fun testClomplexDFGAndEOGRequirement2() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/ComplexDataflow2.java")) + .defaultPasses() + .defaultLanguages() + .registerPass(EdgeCachePass()) + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.allExtended( + { it.name == "highlyCriticalOperation" }, + { n1 -> + val loggingQueryForward = + executionPath(n1, { (it as? CallExpression)?.fqn == "Logger.log" }) + val loggingQueryBackwards = + executionPathBackwards(n1, { (it as? CallExpression)?.fqn == "Logger.log" }) + val allChildren = loggingQueryForward.children + allChildren.addAll(loggingQueryBackwards.children) + val allPaths = + allChildren + .map { (it.value as? List<*>) } + .filter { it != null && it.last() is CallExpression } + val allCalls = allPaths.map { it?.last() as CallExpression } + val dataFlowPaths = + allCalls.map { + allNonLiteralsFromFlowTo( + n1.arguments[0], + it.arguments[1], + allPaths as List> + ) + } + val dataFlowQuery = + QueryTree(dataFlowPaths.all { it.value }, dataFlowPaths.toMutableList()) + + return@allExtended (loggingQueryForward or loggingQueryBackwards) and + dataFlowQuery + } + ) + + println(queryTreeResult.printNicely()) + assertTrue(queryTreeResult.value) + assertEquals(1, queryTreeResult.children.size) + } + + @Test + fun testClomplexDFGAndEOGRequirement3() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/query/ComplexDataflow3.java")) + .defaultPasses() + .defaultLanguages() + .registerPass(EdgeCachePass()) + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val queryTreeResult = + result.allExtended( + { it.name == "highlyCriticalOperation" }, + { n1 -> + val loggingQueryForward = + executionPath(n1, { (it as? CallExpression)?.fqn == "Logger.log" }) + val loggingQueryBackwards = + executionPathBackwards(n1, { (it as? CallExpression)?.fqn == "Logger.log" }) + val allChildren = loggingQueryForward.children + allChildren.addAll(loggingQueryBackwards.children) + val allPaths = + allChildren + .map { (it.value as? List<*>) } + .filter { it != null && it.last() is CallExpression } + val allCalls = allPaths.map { it?.last() as CallExpression } + val dataFlowPaths = + allCalls.map { + allNonLiteralsFromFlowTo( + n1.arguments[0], + it.arguments[1], + allPaths as List> + ) + } + val dataFlowQuery = + QueryTree(dataFlowPaths.all { it.value }, dataFlowPaths.toMutableList()) + + return@allExtended (loggingQueryForward or loggingQueryBackwards) and + dataFlowQuery + } + ) + + println(queryTreeResult.printNicely()) + assertFalse(queryTreeResult.value) + assertEquals(1, queryTreeResult.children.size) + } +} diff --git a/cpg-analysis/src/test/resources/query/ComplexDataflow.java b/cpg-analysis/src/test/resources/query/ComplexDataflow.java new file mode 100644 index 0000000000..20805bcfb6 --- /dev/null +++ b/cpg-analysis/src/test/resources/query/ComplexDataflow.java @@ -0,0 +1,17 @@ +public class Dataflow { + static Logger logger = Logger.getLogger("DataflowLogger"); + + public int a; + + public static void highlyCriticalOperation(String s) { + System.out.println(s); + } + + + public static void main(String[] args) { + Dataflow sc = new Dataflow(); + sc.a = 5; + Dataflow.highlyCriticalOperation(Integer.toString(sc.a)); + logger.log(Level.INFO, "put " + sc.a + " into highlyCriticalOperation()"); + } +} \ No newline at end of file diff --git a/cpg-analysis/src/test/resources/query/ComplexDataflow2.java b/cpg-analysis/src/test/resources/query/ComplexDataflow2.java new file mode 100644 index 0000000000..9ea961f7f9 --- /dev/null +++ b/cpg-analysis/src/test/resources/query/ComplexDataflow2.java @@ -0,0 +1,17 @@ +public class Dataflow { + static Logger logger = Logger.getLogger("DataflowLogger"); + + public int a; + + public static void highlyCriticalOperation(String s) { + System.out.println(s); + } + + + public static void main(String[] args) { + Dataflow sc = new Dataflow(); + sc.a = 5; + logger.log(Level.INFO, "put " + sc.a + " into highlyCriticalOperation()"); + Dataflow.highlyCriticalOperation(Integer.toString(sc.a)); + } +} \ No newline at end of file diff --git a/cpg-analysis/src/test/resources/query/ComplexDataflow3.java b/cpg-analysis/src/test/resources/query/ComplexDataflow3.java new file mode 100644 index 0000000000..6600d5d654 --- /dev/null +++ b/cpg-analysis/src/test/resources/query/ComplexDataflow3.java @@ -0,0 +1,18 @@ +public class Dataflow { + static Logger logger = Logger.getLogger("DataflowLogger"); + + public int a; + + public static void highlyCriticalOperation(String s) { + System.out.println(s); + } + + + public static void main(String[] args) { + Dataflow sc = new Dataflow(); + sc.a = 5; + logger.log(Level.INFO, "put " + sc.a + " into highlyCriticalOperation()"); + sc.a = 3; + Dataflow.highlyCriticalOperation(Integer.toString(sc.a)); + } +} \ No newline at end of file diff --git a/cpg-analysis/src/test/resources/query/Dataflow.java b/cpg-analysis/src/test/resources/query/Dataflow.java new file mode 100644 index 0000000000..4f905fac8e --- /dev/null +++ b/cpg-analysis/src/test/resources/query/Dataflow.java @@ -0,0 +1,20 @@ +public class Dataflow { + public String toString() { + return "Dataflow: attr=" + attr; + } + + public String test() { return "abcd"; } + + public int print(String s) { + System.out.println(s); + } + + + public static void main(String[] args) { + Dataflow sc = new Dataflow(); + String s = sc.toString(); + sc.print(s); + + sc.print(sc.test()); + } +} \ No newline at end of file diff --git a/cpg-console/src/test/resources/array.cpp b/cpg-analysis/src/test/resources/query/array.cpp similarity index 100% rename from cpg-console/src/test/resources/array.cpp rename to cpg-analysis/src/test/resources/query/array.cpp diff --git a/cpg-analysis/src/test/resources/query/array2.cpp b/cpg-analysis/src/test/resources/query/array2.cpp new file mode 100644 index 0000000000..940a178f4f --- /dev/null +++ b/cpg-analysis/src/test/resources/query/array2.cpp @@ -0,0 +1,8 @@ +int main() { + char* c = new char[4]; + int a = 0; + for(int i = 0; i <= 4; i++) { + a = a + c[i]; + } + return a; +} diff --git a/cpg-analysis/src/test/resources/query/array3.cpp b/cpg-analysis/src/test/resources/query/array3.cpp new file mode 100644 index 0000000000..df8cae0295 --- /dev/null +++ b/cpg-analysis/src/test/resources/query/array3.cpp @@ -0,0 +1,12 @@ +int main() { + char* c; + if(5 > 4) + c = new char[4]; + else + c = new char[5]; + int a = 0; + for(int i = 0; i <= 4; i++) { + a = a + c[i]; + } + return a; +} diff --git a/cpg-analysis/src/test/resources/query/array_correct.cpp b/cpg-analysis/src/test/resources/query/array_correct.cpp new file mode 100644 index 0000000000..735b7324e2 --- /dev/null +++ b/cpg-analysis/src/test/resources/query/array_correct.cpp @@ -0,0 +1,8 @@ +int main() { + char* c = new char[4]; + int a = 0; + for(int i = 0; i < 4; i++) { + a = a + c[i]; + } + return a; +} diff --git a/cpg-analysis/src/test/resources/query/assign.cpp b/cpg-analysis/src/test/resources/query/assign.cpp new file mode 100644 index 0000000000..2b4588416a --- /dev/null +++ b/cpg-analysis/src/test/resources/query/assign.cpp @@ -0,0 +1,6 @@ +int main() { + int a = 4; + // int a, b = 4; // this is broken, a is missing an initializer + + a = 3; +} \ No newline at end of file diff --git a/cpg-analysis/src/test/resources/query/vulnerable.cpp b/cpg-analysis/src/test/resources/query/vulnerable.cpp new file mode 100644 index 0000000000..dcaa510dbb --- /dev/null +++ b/cpg-analysis/src/test/resources/query/vulnerable.cpp @@ -0,0 +1,18 @@ +int main() { + char array[6] = "hello"; + memcpy(array, "Hello world", 11); + printf(array); + free(array); + free(array); + + short a = 2; + if(array == "hello") { + a = 0; + } + + double x = 5/a; + + int b = 2147483648; + b = 2147483648; + long c = -10000; +} \ No newline at end of file diff --git a/cpg-analysis/src/test/resources/value_evaluation/cfexample.cpp b/cpg-analysis/src/test/resources/value_evaluation/cfexample.cpp new file mode 100644 index 0000000000..bb9181b48b --- /dev/null +++ b/cpg-analysis/src/test/resources/value_evaluation/cfexample.cpp @@ -0,0 +1,38 @@ +#include +#include + +int main() { + srand(time(NULL)); + int b = 1; + if(rand() < 10) { + b = b+1; + } + println(b); // {1, 2} + + if(rand() > 5) { + b = b-1; + } + println(b); // {0, 1, 2} + + if(rand() > 3) { + b = b*2; + } + println(b); // {0, 1, 2, 4} + + if(rand() < 4) { + b = -b; + } + println(b); // {-4, -2, -1, 0, 1, 2, 4} + + int a = b < 2 ? 3 : 5++; + println(a); // {3, 6} + return 0; +} + +int loop() { + int array[6]; + for(int i = 0; i < 6; i++) { + array[i] = i; + } + return 0; +} \ No newline at end of file diff --git a/cpg-analysis/src/test/resources/value_evaluation/size.java b/cpg-analysis/src/test/resources/value_evaluation/size.java new file mode 100644 index 0000000000..5bde3a1edd --- /dev/null +++ b/cpg-analysis/src/test/resources/value_evaluation/size.java @@ -0,0 +1,13 @@ +public class MainClass { + public static void main(String[] args) { + int[] array = new int[3]; + for(int i = 0; i < array.length; i++) { + array[i] = i; + } + System.out.println(array[1]); + + String str = "abcde"; + System.out.println(str); + return 0; + } + } \ No newline at end of file diff --git a/cpg-console/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/AnalysisTest.kt b/cpg-console/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/AnalysisTest.kt deleted file mode 100644 index f85770fa89..0000000000 --- a/cpg-console/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/AnalysisTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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. - * - * $$$$$$\ $$$$$$$\ $$$$$$\ - * $$ __$$\ $$ __$$\ $$ __$$\ - * $$ / \__|$$ | $$ |$$ / \__| - * $$ | $$$$$$$ |$$ |$$$$\ - * $$ | $$ ____/ $$ |\_$$ | - * $$ | $$\ $$ | $$ | $$ | - * \$$$$$ |$$ | \$$$$$ | - * \______/ \__| \______/ - * - */ -package de.fraunhofer.aisec.cpg.analysis - -import de.fraunhofer.aisec.cpg.TranslationConfiguration -import de.fraunhofer.aisec.cpg.TranslationManager -import de.fraunhofer.aisec.cpg.console.fancyCode -import de.fraunhofer.aisec.cpg.graph.body -import de.fraunhofer.aisec.cpg.graph.byNameOrNull -import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration -import de.fraunhofer.aisec.cpg.graph.statements.DeclarationStatement -import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression -import java.io.File -import kotlin.test.Test -import kotlin.test.assertNotNull - -class AnalysisTest { - @Test - fun testOutOfBounds() { - val config = - TranslationConfiguration.builder() - .sourceLocations(File("src/test/resources/array.cpp")) - .defaultPasses() - .defaultLanguages() - .build() - - val analyzer = TranslationManager.builder().config(config).build() - val result = analyzer.analyze().get() - - OutOfBoundsCheck().run(result) - } - - @Test - fun testNullPointer() { - val config = - TranslationConfiguration.builder() - .sourceLocations(File("src/test/resources/Array.java")) - .defaultPasses() - .defaultLanguages() - .build() - - val analyzer = TranslationManager.builder().config(config).build() - val result = analyzer.analyze().get() - - NullPointerCheck().run(result) - } - - @Test - fun testAttribute() { - val config = - TranslationConfiguration.builder() - .sourceLocations(File("src/test/resources/Array.java")) - .defaultPasses() - .defaultLanguages() - .build() - - val analyzer = TranslationManager.builder().config(config).build() - val result = analyzer.analyze().get() - val tu = result.translationUnits.first() - - val main = tu.byNameOrNull("Array.main", true) - assertNotNull(main) - val call = main.body(0) - - var code = call.fancyCode(showNumbers = false) - - // assertEquals("obj.\u001B[36mdoSomething\u001B[0m();", code) - println(code) - - var decl = main.body(0) - code = decl.fancyCode(showNumbers = false) - println(code) - - decl = main.body(1) - code = decl.fancyCode(showNumbers = false) - println(code) - - code = main.fancyCode(showNumbers = false) - println(code) - - code = call.fancyCode(3, true) - println(code) - } -} diff --git a/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/Assignment.kt b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/Assignment.kt new file mode 100644 index 0000000000..af91b40677 --- /dev/null +++ b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/Assignment.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022, 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.graph + +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator +import de.fraunhofer.aisec.cpg.graph.statements.expressions.DeclaredReferenceExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression + +/** An assignment assigns a certain value (usually an [Expression]) to a certain target. */ +interface Assignment { + /** + * The target of this assignment. Note that this is intentionally nullable, because while + * [BinaryOperator] implements [Assignment], not all binary operations are assignments. Thus, + * the target is only non-null for operations that have a == operator. + */ + val target: AssignmentTarget? + + /** + * The value expression that is assigned to the target. This is intentionally nullable for the + * same reason as [target]. + */ + val value: Expression? +} + +/** + * The target of an assignment. The target is usually either a [VariableDeclaration] or a + * [DeclaredReferenceExpression]. + */ +interface AssignmentTarget : HasType { + val name: String +} diff --git a/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/Extensions.kt b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/Extensions.kt index 72ff4ba2af..21c7ac45bc 100644 --- a/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/Extensions.kt +++ b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/Extensions.kt @@ -25,31 +25,37 @@ */ package de.fraunhofer.aisec.cpg.graph +import de.fraunhofer.aisec.cpg.ExperimentalGraph import de.fraunhofer.aisec.cpg.TranslationResult import de.fraunhofer.aisec.cpg.graph.declarations.Declaration import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration import de.fraunhofer.aisec.cpg.graph.edge.PropertyEdge import de.fraunhofer.aisec.cpg.graph.statements.CompoundStatement +import de.fraunhofer.aisec.cpg.graph.statements.IfStatement import de.fraunhofer.aisec.cpg.graph.statements.Statement +import de.fraunhofer.aisec.cpg.graph.statements.SwitchStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* import de.fraunhofer.aisec.cpg.helpers.SubgraphWalker +import de.fraunhofer.aisec.cpg.passes.astParent @JvmName("allNodes") -fun TranslationResult.all(): List { - return this.all() +fun TranslationResult.allChildren(): List { + return this.allChildren() } -inline fun TranslationResult.all(): List { +inline fun TranslationResult.allChildren(): List { val children = SubgraphWalker.flattenAST(this) return children.filterIsInstance() } -@JvmName("allNodes") -fun Node.all(): List { - return this.all() +@JvmName("allChildrenNodes") +fun Node.allChildren(): List { + return this.allChildren() } -inline fun Node.all(): List { +inline fun Node.allChildren(): List { val children = SubgraphWalker.flattenAST(this) return children.filterIsInstance() @@ -136,6 +142,215 @@ class StatementNotFound : Exception() class DeclarationNotFound(message: String) : Exception(message) +class FulfilledAndFailedPaths(val fulfilled: List>, val failed: List>) { + operator fun component1(): List> = fulfilled + operator fun component2(): List> = failed +} + +/** + * Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled] + * contains all possible shortest data flow paths between the end node [this] and the starting node + * fulfilling [predicate]. The paths are represented as lists of nodes. Paths which do not end at + * such a node are included in [FulfilledAndFailedPaths.failed]. + * + * Hence, if "fulfilled" is a non-empty list, a data flow from [this] to such a node is **possible + * but not mandatory**. If the list "failed" is empty, the data flow is mandatory. + */ +fun Node.followPrevDFGEdgesUntilHit(predicate: (Node) -> Boolean): FulfilledAndFailedPaths { + val fulfilledPaths = mutableListOf>() + val failedPaths = mutableListOf>() + val worklist = mutableListOf>() + worklist.add(listOf(this)) + + while (worklist.isNotEmpty()) { + val currentPath = worklist.removeFirst() + if (currentPath.last().prevDFG.isEmpty()) { + // No further nodes in the path and the path criteria are not satisfied. + failedPaths.add(currentPath) + continue + } + + for (prev in currentPath.last().prevDFG) { + // Copy the path for each outgoing DFG edge and add the prev node + val nextPath = mutableListOf() + nextPath.addAll(currentPath) + nextPath.add(prev) + + if (predicate(prev)) { + fulfilledPaths.add(nextPath) + continue // Don't add this path anymore. The requirement is satisfied. + } + // The prev node is new in the current path (i.e., there's no loop), so we add the path + // with the next step to the worklist. + if (!currentPath.contains(prev)) { + worklist.add(nextPath) + } + } + } + + return FulfilledAndFailedPaths(fulfilledPaths, failedPaths) +} + +/** + * Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled] + * contains all possible shortest data flow paths between the starting node [this] and the end node + * fulfilling [predicate]. The paths are represented as lists of nodes. Paths which do not end at + * such a node are included in [FulfilledAndFailedPaths.failed]. + * + * Hence, if "fulfilled" is a non-empty list, a data flow from [this] to such a node is **possible + * but not mandatory**. If the list "failed" is empty, the data flow is mandatory. + */ +fun Node.followNextDFGEdgesUntilHit(predicate: (Node) -> Boolean): FulfilledAndFailedPaths { + // Looks complicated but at least it's not recursive... + // result: List of paths (between from and to) + val fulfilledPaths = mutableListOf>() + // failedPaths: All the paths which do not satisfy "predicate" + val failedPaths = mutableListOf>() + // The list of paths where we're not done yet. + val worklist = mutableListOf>() + worklist.add(listOf(this)) // We start only with the "from" node (=this) + + while (worklist.isNotEmpty()) { + val currentPath = worklist.removeFirst() + // The last node of the path is where we continue. We get all of its outgoing DFG edges and + // follow them + if (currentPath.last().nextDFG.isEmpty()) { + // No further nodes in the path and the path criteria are not satisfied. + failedPaths.add(currentPath) + continue + } + + for (next in currentPath.last().nextDFG) { + // Copy the path for each outgoing DFG edge and add the next node + val nextPath = mutableListOf() + nextPath.addAll(currentPath) + nextPath.add(next) + if (predicate(next)) { + // We ended up in the node fulfilling "predicate", so we're done for this path. Add + // the path to the results. + fulfilledPaths.add(nextPath) + continue // Don't add this path anymore. The requirement is satisfied. + } + // The next node is new in the current path (i.e., there's no loop), so we add the path + // with the next step to the worklist. + if (!currentPath.contains(next)) { + worklist.add(nextPath) + } + } + } + + return FulfilledAndFailedPaths(fulfilledPaths, failedPaths) +} + +/** + * Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled] + * contains all possible shortest evaluation paths between the starting node [this] and the end node + * fulfilling [predicate]. The paths are represented as lists of nodes. Paths which do not end at + * such a node are included in [FulfilledAndFailedPaths.failed]. + * + * Hence, if "fulfilled" is a non-empty list, the execution of a statement fulfilling the predicate + * is possible after executing [this] **possible but not mandatory**. If the list "failed" is empty, + * such a statement is always executed. + */ +fun Node.followNextEOGEdgesUntilHit(predicate: (Node) -> Boolean): FulfilledAndFailedPaths { + // Looks complicated but at least it's not recursive... + // result: List of paths (between from and to) + val fulfilledPaths = mutableListOf>() + // failedPaths: All the paths which do not satisfy "predicate" + val failedPaths = mutableListOf>() + // The list of paths where we're not done yet. + val worklist = mutableListOf>() + worklist.add(listOf(this)) // We start only with the "from" node (=this) + + while (worklist.isNotEmpty()) { + val currentPath = worklist.removeFirst() + // The last node of the path is where we continue. We get all of its outgoing DFG edges and + // follow them + if (currentPath.last().nextEOG.isEmpty()) { + // No further nodes in the path and the path criteria are not satisfied. + failedPaths.add(currentPath) + continue // Don't add this path any more. The requirement is satisfied. + } + + for (next in currentPath.last().nextEOG) { + // Copy the path for each outgoing DFG edge and add the next node + val nextPath = mutableListOf() + nextPath.addAll(currentPath) + nextPath.add(next) + if (predicate(next)) { + // We ended up in the node "to", so we're done. Add the path to the results. + fulfilledPaths.add(nextPath) + continue // Don't add this path anymore. The requirement is satisfied. + } + // The next node is new in the current path (i.e., there's no loop), so we add the path + // with the next step to the worklist. + if (!currentPath.contains(next)) { + worklist.add(nextPath) + } + } + } + + return FulfilledAndFailedPaths(fulfilledPaths, failedPaths) +} + +/** + * Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled] + * contains all possible shortest evaluation paths between the end node [this] and the start node + * fulfilling [predicate]. The paths are represented as lists of nodes. Paths which do not end at + * such a node are included in [FulfilledAndFailedPaths.failed]. + * + * Hence, if "fulfilled" is a non-empty list, the execution of a statement fulfilling the predicate + * is possible after executing [this] **possible but not mandatory**. If the list "failed" is empty, + * such a statement is always executed. + */ +fun Node.followPrevEOGEdgesUntilHit(predicate: (Node) -> Boolean): FulfilledAndFailedPaths { + // Looks complicated but at least it's not recursive... + // result: List of paths (between from and to) + val fulfilledPaths = mutableListOf>() + // failedPaths: All the paths which do not satisfy "predicate" + val failedPaths = mutableListOf>() + // The list of paths where we're not done yet. + val worklist = mutableListOf>() + worklist.add(listOf(this)) // We start only with the "from" node (=this) + + while (worklist.isNotEmpty()) { + val currentPath = worklist.removeFirst() + // The last node of the path is where we continue. We get all of its outgoing DFG edges and + // follow them + if (currentPath.last().prevEOG.isEmpty()) { + // No further nodes in the path and the path criteria are not satisfied. + failedPaths.add(currentPath) + continue // Don't add this path any more. The requirement is satisfied. + } + + for (next in currentPath.last().prevEOG) { + // Copy the path for each outgoing DFG edge and add the next node + val nextPath = mutableListOf() + nextPath.addAll(currentPath) + nextPath.add(next) + if (predicate(next)) { + // We ended up in the node "to", so we're done. Add the path to the results. + fulfilledPaths.add(nextPath) + continue // Don't add this path anymore. The requirement is satisfied. + } + // The next node is new in the current path (i.e., there's no loop), so we add the path + // with the next step to the worklist. + if (!currentPath.contains(next)) { + worklist.add(nextPath) + } + } + } + + return FulfilledAndFailedPaths(fulfilledPaths, failedPaths) +} + +/** + * Returns a list of edges which are form the evaluation order between the starting node [this] and + * an edge fulfilling [predicate]. If the return value is not `null`, a path from [this] to such an + * edge is **possible but not mandatory**. + * + * It returns only a single possible path even if multiple paths are possible. + */ fun Node.followPrevEOG(predicate: (PropertyEdge<*>) -> Boolean): List>? { val path = mutableListOf>() @@ -159,6 +374,13 @@ fun Node.followPrevEOG(predicate: (PropertyEdge<*>) -> Boolean): List Boolean): MutableList? { val path = mutableListOf() @@ -179,3 +401,80 @@ fun Node.followPrevDFG(predicate: (Node) -> Boolean): MutableList? { return null } + +/** Returns all [CallExpression]s in this graph. */ +@OptIn(ExperimentalGraph::class) +val TranslationResult.calls: List + get() = this.graph.nodes.filterIsInstance() + +/** Returns all [CallExpression]s in this graph which call a method with the given [name]. */ +@OptIn(ExperimentalGraph::class) +fun TranslationResult.callsByName(name: String): List { + return SubgraphWalker.flattenAST(this).filter { node -> + (node as? CallExpression)?.invokes?.any { it.name == name } == true + } as List +} + +/** Set of all functions which are called from this function */ +val FunctionDeclaration.callees: Set + get() { + + return SubgraphWalker.flattenAST(this.body) + .filterIsInstance() + .map { it.invokes } + .foldRight( + mutableListOf(), + { l, res -> + res.addAll(l) + res + } + ) + .toSet() + } + +/** Set of all functions calling [function] */ +@OptIn(ExperimentalGraph::class) +fun TranslationResult.callersOf(function: FunctionDeclaration): Set { + return this.graph.nodes + .filterIsInstance() + .filter { function in it.callees } + .toSet() +} + +/** All nodes which depend on this if statement */ +fun IfStatement.controls(): List { + val result = mutableListOf() + result.addAll(SubgraphWalker.flattenAST(this.thenStatement)) + result.addAll(SubgraphWalker.flattenAST(this.elseStatement)) + return result +} + +/** All nodes which depend on this if statement */ +fun Node.controlledBy(): List { + val result = mutableListOf() + var checkedNode: Node = this + while (checkedNode !is FunctionDeclaration) { + checkedNode = checkedNode.astParent!! + if (checkedNode is IfStatement || checkedNode is SwitchStatement) { + result.add(checkedNode) + } + } + return result +} + +/** + * Filters a list of [CallExpression]s for expressions which call a method with the given [name]. + */ +fun List.filterByName(name: String): List { + return this.filter { n -> n.invokes.any { it.name == name } } +} + +/** + * Returns the expression specifying the dimension (i.e., size) of the array during its + * initialization. + */ +val ArraySubscriptionExpression.arraySize: Expression + get() = + (((this.arrayExpression as DeclaredReferenceExpression).refersTo as VariableDeclaration) + .initializer as ArrayCreationExpression) + .dimensions[0] diff --git a/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/declarations/VariableDeclaration.java b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/declarations/VariableDeclaration.java index 7484e25203..efa804ee2d 100644 --- a/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/declarations/VariableDeclaration.java +++ b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/declarations/VariableDeclaration.java @@ -25,12 +25,8 @@ */ package de.fraunhofer.aisec.cpg.graph.declarations; -import de.fraunhofer.aisec.cpg.graph.HasInitializer; -import de.fraunhofer.aisec.cpg.graph.HasType; +import de.fraunhofer.aisec.cpg.graph.*; import de.fraunhofer.aisec.cpg.graph.HasType.TypeListener; -import de.fraunhofer.aisec.cpg.graph.Node; -import de.fraunhofer.aisec.cpg.graph.SubGraph; -import de.fraunhofer.aisec.cpg.graph.TypeManager; import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression; import de.fraunhofer.aisec.cpg.graph.statements.expressions.InitializerListExpression; import de.fraunhofer.aisec.cpg.graph.types.Type; @@ -40,7 +36,8 @@ import org.neo4j.ogm.annotation.Relationship; /** Represents the declaration of a variable. */ -public class VariableDeclaration extends ValueDeclaration implements TypeListener, HasInitializer { +public class VariableDeclaration extends ValueDeclaration + implements TypeListener, HasInitializer, Assignment, AssignmentTarget { /** The (optional) initializer of the declaration. */ @SubGraph("AST") @@ -196,4 +193,16 @@ public boolean equals(Object o) { public int hashCode() { return super.hashCode(); } + + @Nullable + @Override + public AssignmentTarget getTarget() { + return this; + } + + @Nullable + @Override + public Expression getValue() { + return initializer; + } } diff --git a/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/statements/expressions/BinaryOperator.java b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/statements/expressions/BinaryOperator.java index 94c472c710..5f33fc8be0 100644 --- a/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/statements/expressions/BinaryOperator.java +++ b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/statements/expressions/BinaryOperator.java @@ -25,23 +25,20 @@ */ package de.fraunhofer.aisec.cpg.graph.statements.expressions; -import de.fraunhofer.aisec.cpg.graph.AccessValues; -import de.fraunhofer.aisec.cpg.graph.HasType; +import de.fraunhofer.aisec.cpg.graph.*; import de.fraunhofer.aisec.cpg.graph.HasType.TypeListener; -import de.fraunhofer.aisec.cpg.graph.Node; -import de.fraunhofer.aisec.cpg.graph.SubGraph; -import de.fraunhofer.aisec.cpg.graph.TypeManager; import de.fraunhofer.aisec.cpg.graph.types.Type; import de.fraunhofer.aisec.cpg.graph.types.TypeParser; import java.util.*; import org.apache.commons.lang3.builder.ToStringBuilder; +import org.jetbrains.annotations.Nullable; import org.neo4j.ogm.annotation.Transient; /** * A binary operation expression, such as "a + b". It consists of a left hand expression (lhs), a * right hand expression (rhs) and an operatorCode. */ -public class BinaryOperator extends Expression implements TypeListener { +public class BinaryOperator extends Expression implements TypeListener, Assignment { /** The left hand expression. */ @SubGraph("AST") @@ -244,4 +241,27 @@ public boolean equals(Object o) { public int hashCode() { return super.hashCode(); } + + @Nullable + @Override + public AssignmentTarget getTarget() { + // We only want to supply a target if this is an assignment + return isAssignment() + ? (lhs instanceof AssignmentTarget ? (AssignmentTarget) lhs : null) + : null; + } + + @Nullable + @Override + public Expression getValue() { + return isAssignment() ? rhs : null; + } + + public boolean isAssignment() { + // TODO(oxisto): We need to discuss, if the other operators are also assignments and if we + // really want them + return this.operatorCode.equals("=") + /*||this.operatorCode.equals("+=") ||this.operatorCode.equals("-=") + ||this.operatorCode.equals("/=") ||this.operatorCode.equals("*=")*/ ; + } } diff --git a/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/statements/expressions/DeclaredReferenceExpression.java b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/statements/expressions/DeclaredReferenceExpression.java index 2e6ed6ac46..2895a5d03b 100644 --- a/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/statements/expressions/DeclaredReferenceExpression.java +++ b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/graph/statements/expressions/DeclaredReferenceExpression.java @@ -42,7 +42,8 @@ * DeclaredReferenceExpression}s, one for the variable a and one for variable b * , which have been previously been declared. */ -public class DeclaredReferenceExpression extends Expression implements TypeListener { +public class DeclaredReferenceExpression extends Expression + implements TypeListener, AssignmentTarget { /** The {@link Declaration}s this expression might refer to. */ @Relationship(value = "REFERS_TO") diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/EdgeCachePass.kt b/cpg-core/src/main/java/de/fraunhofer/aisec/cpg/passes/EdgeCachePass.kt similarity index 100% rename from cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/EdgeCachePass.kt rename to cpg-core/src/main/java/de/fraunhofer/aisec/cpg/passes/EdgeCachePass.kt diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/ShortcutsTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/ShortcutsTest.kt new file mode 100644 index 0000000000..1a0fa87a1f --- /dev/null +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/ShortcutsTest.kt @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2022, 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.graph + +import de.fraunhofer.aisec.cpg.TranslationConfiguration +import de.fraunhofer.aisec.cpg.TranslationManager +import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.MethodDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.CompoundStatement +import de.fraunhofer.aisec.cpg.graph.statements.DeclarationStatement +import de.fraunhofer.aisec.cpg.graph.statements.IfStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.ConstructExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.NewExpression +import de.fraunhofer.aisec.cpg.passes.EdgeCachePass +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ShortcutsTest { + @Test + fun followDFGUntilHitTest() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/Dataflow.java")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val toStringCall = result.callsByName("toString")[0] + val printDecl = + result.translationUnits[0] + .byNameOrNull("Dataflow") + ?.byNameOrNull("print") + + val (fulfilled, failed) = + toStringCall.followNextDFGEdgesUntilHit { it == printDecl!!.parameters[0] } + + assertEquals(1, fulfilled.size) + assertEquals( + 1, + failed.size + ) // For some reason, the flow to the VariableDeclaration doesn't end in the call to print() + } + + @Test + fun testCalls() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/ShortcutClass.java")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val actual = result.calls + + val expected = mutableListOf() + val classDecl = + result.translationUnits.firstOrNull()?.declarations?.firstOrNull() as RecordDeclaration + val main = classDecl.byNameOrNull("main") + assertNotNull(main) + expected.add( + ((((main.body as CompoundStatement).statements[0] as DeclarationStatement) + .declarations[0] + as VariableDeclaration) + .initializer as NewExpression) + .initializer as ConstructExpression + ) + expected.add((main.body as CompoundStatement).statements[1] as MemberCallExpression) + expected.add((main.body as CompoundStatement).statements[2] as MemberCallExpression) + + val print = classDecl.byNameOrNull("print") + assertNotNull(print) + expected.add(print.bodyOrNull(0)!!) + expected.add(print.bodyOrNull(0)?.arguments?.get(0) as CallExpression) + + assertTrue(expected.containsAll(actual)) + assertTrue(actual.containsAll(expected)) + + assertEquals( + listOf((main.body as CompoundStatement).statements[1] as MemberCallExpression), + expected.filterByName("print") + ) + } + + @Test + fun testCallsByName() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/ShortcutClass.java")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val actual = result.callsByName("print") + + val expected = mutableListOf() + val classDecl = + result.translationUnits.firstOrNull()?.declarations?.firstOrNull() as RecordDeclaration + val main = classDecl.byNameOrNull("main") + assertNotNull(main) + expected.add((main.body as CompoundStatement).statements[1] as MemberCallExpression) + assertTrue(expected.containsAll(actual)) + assertTrue(actual.containsAll(expected)) + } + + @Test + fun testCalleesOf() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/ShortcutClass.java")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val expected = mutableListOf() + val classDecl = + result.translationUnits.firstOrNull()?.declarations?.firstOrNull() as RecordDeclaration + + val print = classDecl.byNameOrNull("print") + assertNotNull(print) + expected.add(print) + + val magic = classDecl.byNameOrNull("magic") + assertNotNull(magic) + expected.add(magic) + + val main = classDecl.byNameOrNull("main") + assertNotNull(main) + val actual = main.callees + + expected.add( + (((((main.body as CompoundStatement).statements[0] as DeclarationStatement) + .declarations[0] + as VariableDeclaration) + .initializer as NewExpression) + .initializer as ConstructExpression) + .constructor!! + ) + + assertTrue(expected.containsAll(actual)) + assertTrue(actual.containsAll(expected)) + } + + @Test + fun testCallersOf() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/ShortcutClass.java")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val classDecl = + result.translationUnits.firstOrNull()?.declarations?.firstOrNull() as RecordDeclaration + val print = classDecl.byNameOrNull("print") + assertNotNull(print) + + val actual = result.callersOf(print) + + val expected = mutableListOf() + val main = classDecl.byNameOrNull("main") + assertNotNull(main) + expected.add(main) + assertTrue(expected.containsAll(actual)) + assertTrue(actual.containsAll(expected)) + } + + @Test + fun testControls() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/ShortcutClass.java")) + .defaultPasses() + .defaultLanguages() + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val expected = mutableListOf() + val classDecl = + result.translationUnits.firstOrNull()?.declarations?.firstOrNull() as RecordDeclaration + val magic = classDecl.byNameOrNull("magic") + assertNotNull(magic) + val ifStatement = (magic.body as CompoundStatement).statements[0] as IfStatement + + val actual = ifStatement.controls() + expected.add(ifStatement.thenStatement) + val thenStatement = + (ifStatement.thenStatement as CompoundStatement).statements[0] as IfStatement + expected.add(thenStatement) + expected.add(thenStatement.condition) + expected.add((thenStatement.condition as BinaryOperator).lhs) + expected.add(((thenStatement.condition as BinaryOperator).lhs as MemberExpression).base) + expected.add((thenStatement.condition as BinaryOperator).rhs) + val nestedThen = thenStatement.thenStatement as CompoundStatement + expected.add(nestedThen) + expected.add(nestedThen.statements[0]) + expected.add((nestedThen.statements[0] as BinaryOperator).lhs) + expected.add(((nestedThen.statements[0] as BinaryOperator).lhs as MemberExpression).base) + expected.add((nestedThen.statements[0] as BinaryOperator).rhs) + val nestedElse = thenStatement.elseStatement as CompoundStatement + expected.add(nestedElse) + expected.add(nestedElse.statements[0]) + expected.add((nestedElse.statements[0] as BinaryOperator).lhs) + expected.add(((nestedElse.statements[0] as BinaryOperator).lhs as MemberExpression).base) + expected.add((nestedElse.statements[0] as BinaryOperator).rhs) + + expected.add(ifStatement.elseStatement) + expected.add((ifStatement.elseStatement as CompoundStatement).statements[0]) + expected.add( + ((ifStatement.elseStatement as CompoundStatement).statements[0] as BinaryOperator).lhs + ) + expected.add( + (((ifStatement.elseStatement as CompoundStatement).statements[0] as BinaryOperator).lhs + as MemberExpression) + .base + ) + expected.add( + ((ifStatement.elseStatement as CompoundStatement).statements[0] as BinaryOperator).rhs + ) + + assertTrue(expected.containsAll(actual)) + assertTrue(actual.containsAll(expected)) + } + + @Test + fun testControlledBy() { + val config = + TranslationConfiguration.builder() + .sourceLocations(File("src/test/resources/ShortcutClass.java")) + .defaultPasses() + .defaultLanguages() + .registerPass(EdgeCachePass()) + .build() + + val analyzer = TranslationManager.builder().config(config).build() + val result = analyzer.analyze().get() + + val expected = mutableListOf() + val classDecl = + result.translationUnits.firstOrNull()?.declarations?.firstOrNull() as RecordDeclaration + val magic = classDecl.byNameOrNull("magic") + assertNotNull(magic) + + // get the statement attr = 3; + val ifStatement = (magic.body as CompoundStatement).statements[0] as IfStatement + val thenStatement = + (ifStatement.thenStatement as CompoundStatement).statements[0] as IfStatement + val nestedThen = thenStatement.thenStatement as CompoundStatement + val interestingNode = nestedThen.statements[0] + val actual = interestingNode.controlledBy() + + expected.add(ifStatement) + expected.add(thenStatement) + + assertTrue(expected.containsAll(actual)) + assertTrue(actual.containsAll(expected)) + } +} diff --git a/cpg-core/src/test/resources/Dataflow.java b/cpg-core/src/test/resources/Dataflow.java new file mode 100644 index 0000000000..4f905fac8e --- /dev/null +++ b/cpg-core/src/test/resources/Dataflow.java @@ -0,0 +1,20 @@ +public class Dataflow { + public String toString() { + return "Dataflow: attr=" + attr; + } + + public String test() { return "abcd"; } + + public int print(String s) { + System.out.println(s); + } + + + public static void main(String[] args) { + Dataflow sc = new Dataflow(); + String s = sc.toString(); + sc.print(s); + + sc.print(sc.test()); + } +} \ No newline at end of file diff --git a/cpg-core/src/test/resources/ShortcutClass.java b/cpg-core/src/test/resources/ShortcutClass.java new file mode 100644 index 0000000000..3179341049 --- /dev/null +++ b/cpg-core/src/test/resources/ShortcutClass.java @@ -0,0 +1,29 @@ +public class ShortcutClass { + private int attr = 0; + + public String toString() { + return "ShortcutClass: attr=" + attr; + } + + public int print() { + System.out.println(this.toString()); + } + + public void magic(int b) { + if(b > 5) { + if(attr == 2) { + attr = 3; + } else { + attr = 2; + } + } else { + attr = b; + } + } + + public static void main(String[] args) { + ShortcutClass sc = new ShortcutClass(); + sc.print(); + sc.magic(3); + } +} \ No newline at end of file