From 5587864f2095659dc42a753343b4e22047aa8ae1 Mon Sep 17 00:00:00 2001 From: Robert Haimerl Date: Tue, 17 Sep 2024 14:50:53 +0200 Subject: [PATCH] Combine Only and Never Evaluator (#875) Co-authored-by: Florian Wendland --- .../backends/cpg/coko/CokoCpgBackend.kt | 6 +- .../cpg/coko/dsl/ImplementationDsl.kt | 24 ++++++ .../cpg/coko/evaluators/NeverEvaluator.kt | 76 ------------------- ...OnlyEvaluator.kt => OnlyNeverEvaluator.kt} | 37 +++++---- .../backends/cpg/NeverEvaluationTest.kt | 2 +- 5 files changed, 53 insertions(+), 92 deletions(-) delete mode 100644 codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/NeverEvaluator.kt rename codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/{OnlyEvaluator.kt => OnlyNeverEvaluator.kt} (73%) diff --git a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/CokoCpgBackend.kt b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/CokoCpgBackend.kt index 46b453d2d..bd7f362a5 100644 --- a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/CokoCpgBackend.kt +++ b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/CokoCpgBackend.kt @@ -96,8 +96,10 @@ class CokoCpgBackend(config: BackendConfiguration) : /** * Ensures that all calls to the [ops] have arguments that fit the parameters specified in [ops] */ - override fun only(vararg ops: Op): OnlyEvaluator = OnlyEvaluator(ops.toList()) - override fun never(vararg ops: Op): NeverEvaluator = NeverEvaluator(ops.toList()) + override fun only(vararg ops: Op): OnlyNeverEvaluator = + OnlyNeverEvaluator(ops.toList(), OnlyNeverEvaluator.Functionality.ONLY) + override fun never(vararg ops: Op): OnlyNeverEvaluator = + OnlyNeverEvaluator(ops.toList(), OnlyNeverEvaluator.Functionality.NEVER) override fun whenever( premise: Condition.() -> ConditionComponent, assertionBlock: WheneverEvaluator.() -> Unit diff --git a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/dsl/ImplementationDsl.kt b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/dsl/ImplementationDsl.kt index 9de960be9..02265e435 100644 --- a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/dsl/ImplementationDsl.kt +++ b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/dsl/ImplementationDsl.kt @@ -230,3 +230,27 @@ private fun CallExpression.cpgCheckArgsSize(parameters: Array<*>, hasVarargs: Bo } else { parameters.size == arguments.size } + +/** + * To generate more interesting Findings + * we want to find key points where the forbidden operation influences other code. + * For this we traverse the DFG for a fixed amount of steps and search for all usages of declared values. + */ +fun Node.findUsages(depth: Int = 5): Collection { + val currentNodes: MutableSet = mutableSetOf(this) + val usages = mutableSetOf() + for (i in 0..depth) { + // The set will be empty if we found a usage or no further DFG for all branches + if (currentNodes.isEmpty()) { + break + } + for (current in currentNodes) { + currentNodes.remove(current) + when (current) { + is ValueDeclaration -> usages += current.usages + else -> currentNodes += current.nextDFG + } + } + } + return usages +} diff --git a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/NeverEvaluator.kt b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/NeverEvaluator.kt deleted file mode 100644 index 5b0cf63da..000000000 --- a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/NeverEvaluator.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.fraunhofer.aisec.codyze.backends.cpg.coko.evaluators - -import de.fraunhofer.aisec.codyze.backends.cpg.coko.CokoCpgBackend -import de.fraunhofer.aisec.codyze.backends.cpg.coko.CpgFinding -import de.fraunhofer.aisec.codyze.backends.cpg.coko.dsl.cpgGetNodes -import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.EvaluationContext -import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.Evaluator -import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.Finding -import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.dsl.Op -import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.dsl.Rule -import kotlin.reflect.full.findAnnotation - -context(CokoCpgBackend) -class NeverEvaluator(val forbiddenOps: List) : Evaluator { - - /** Default message if a violation is found */ - private val defaultFailMessage: String by lazy { - "Calls to ${forbiddenOps.joinToString()} are not allowed." - } - - /** Default message if the analyzed code complies with rule */ - private val defaultPassMessage by lazy { - "No calls to ${forbiddenOps.joinToString()} found which is in compliance with rule." - } - - override fun evaluate(context: EvaluationContext): Collection { - val ruleAnnotation = context.rule.findAnnotation() - val failMessage = ruleAnnotation?.failMessage?.takeIf { it.isNotEmpty() } ?: defaultFailMessage - val passMessage = ruleAnnotation?.passMessage?.takeIf { it.isNotEmpty() } ?: defaultPassMessage - - val findings = mutableListOf() - - for (op in forbiddenOps) { - val nodes = op.cpgGetNodes() - - if (nodes.isNotEmpty()) { - // This means there are calls to the forbidden op, so Fail findings are added - for (node in nodes) { - findings.add( - CpgFinding( - message = "Violation against rule: \"${node.code}\". $failMessage", - kind = Finding.Kind.Fail, - node = node - ) - ) - } - } - } - - // If there are no findings, there were no violations, so a Pass finding is added - if (findings.isEmpty()) { - findings.add( - CpgFinding( - message = passMessage, - kind = Finding.Kind.Pass, - ) - ) - } - return findings - } -} diff --git a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/OnlyEvaluator.kt b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/OnlyNeverEvaluator.kt similarity index 73% rename from codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/OnlyEvaluator.kt rename to codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/OnlyNeverEvaluator.kt index bec29b8ec..a8278dfa1 100644 --- a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/OnlyEvaluator.kt +++ b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/OnlyNeverEvaluator.kt @@ -19,6 +19,7 @@ import de.fraunhofer.aisec.codyze.backends.cpg.coko.CokoCpgBackend import de.fraunhofer.aisec.codyze.backends.cpg.coko.CpgFinding import de.fraunhofer.aisec.codyze.backends.cpg.coko.dsl.cpgGetAllNodes import de.fraunhofer.aisec.codyze.backends.cpg.coko.dsl.cpgGetNodes +import de.fraunhofer.aisec.codyze.backends.cpg.coko.dsl.findUsages import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.EvaluationContext import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.Evaluator import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.Finding @@ -27,56 +28,66 @@ import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.dsl.Rule import kotlin.reflect.full.findAnnotation context(CokoCpgBackend) -class OnlyEvaluator(val ops: List) : Evaluator { +class OnlyNeverEvaluator(private val ops: List, private val functionality: Functionality) : Evaluator { /** Default message if a violation is found */ private val defaultFailMessage: String by lazy { // Try to model what the allowed calls look like with `toString` call of `Op` - "Only calls to ${ops.joinToString()} allowed." + "${if (functionality == Functionality.NEVER) "No" else "Only"} calls to ${ops.joinToString()} allowed." } /** Default message if node complies with rule */ private val defaultPassMessage = "Call is in compliance with rule" override fun evaluate(context: EvaluationContext): List { - val correctNodes = - with(this@CokoCpgBackend) { ops.flatMap { it.cpgGetNodes() } } - .toSet() - val distinctOps = ops.toSet() val allNodes = with(this@CokoCpgBackend) { distinctOps.flatMap { it.cpgGetAllNodes() } } + .filter { it.location != null } .toSet() - // `correctNodes` is a subset of `allNodes` - // we want to find nodes in `allNodes` that are not contained in `correctNodes` since they are violations - val violatingNodes = allNodes.minus(correctNodes) + // `matchingNodes` is a subset of `allNodes` + // we want to find nodes in `allNodes` that are not contained in `matchingNodes` + // since they are contrary Findings + val matchingNodes = + with(this@CokoCpgBackend) { ops.flatMap { it.cpgGetNodes() } } + .toSet() + val differingNodes = allNodes.minus(matchingNodes) val ruleAnnotation = context.rule.findAnnotation() val failMessage = ruleAnnotation?.failMessage?.takeIf { it.isNotEmpty() } ?: defaultFailMessage val passMessage = ruleAnnotation?.passMessage?.takeIf { it.isNotEmpty() } ?: defaultPassMessage + // define what violations and passes are, depending on selected functionality + val correctNodes = if (functionality == Functionality.NEVER) differingNodes else matchingNodes + val violatingNodes = if (functionality == Functionality.NEVER) matchingNodes else differingNodes val findings = mutableListOf() + for (node in violatingNodes) { findings.add( CpgFinding( message = "Violation against rule: \"${node.code}\". $failMessage", kind = Finding.Kind.Fail, - node = node + node = node, + relatedNodes = node.findUsages() ) ) } - for (node in correctNodes) { findings.add( CpgFinding( message = "Complies with rule: \"${node.code}\". $passMessage", kind = Finding.Kind.Pass, - node = node + node = node, + relatedNodes = node.findUsages() ) ) } - return findings } + + enum class Functionality { + ONLY, + NEVER + } } diff --git a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/NeverEvaluationTest.kt b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/NeverEvaluationTest.kt index 5359169ea..38b2ae4b4 100644 --- a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/NeverEvaluationTest.kt +++ b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/NeverEvaluationTest.kt @@ -92,7 +92,7 @@ class NeverEvaluationTest { findings.all { it.kind == Finding.Kind.Pass } } - assertEquals(1, findings.size, "Found ${findings.size} finding(s) instead of one pass finding") + assertEquals(4, findings.size, "Found ${findings.size} finding(s) instead of one pass finding") } }