Skip to content

Commit

Permalink
Coko 'ArgumentOrigin' Evaluator (#873)
Browse files Browse the repository at this point in the history
Co-authored-by: Florian Wendland <[email protected]>
  • Loading branch information
CodingDepot and fwendland authored Sep 17, 2024
1 parent c14fbcd commit d0c45be
Show file tree
Hide file tree
Showing 6 changed files with 453 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import io.github.detekt.sarif4k.Artifact
import io.github.detekt.sarif4k.ArtifactLocation
import io.github.detekt.sarif4k.ToolComponent
import kotlin.io.path.absolutePathString
import kotlin.reflect.KFunction

typealias Nodes = Collection<Node>

Expand Down Expand Up @@ -84,6 +85,14 @@ class CokoCpgBackend(config: BackendConfiguration) :
order = Order().apply(block)
)

/** Verifies that the argument at [argPos] of [targetOp] stems from a call to [originOp] */
override fun argumentOrigin(targetOp: KFunction<Op>, argPos: Int, originOp: KFunction<Op>): ArgumentEvaluator =
ArgumentEvaluator(
targetCall = targetOp.getOp(),
argPos = argPos,
originCall = originOp.getOp()
)

/**
* Ensures that all calls to the [ops] have arguments that fit the parameters specified in [ops]
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2024, Fraunhofer AISEC. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.fraunhofer.aisec.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.cpgGetAllNodes
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.cpg.graph.declarations.VariableDeclaration
import de.fraunhofer.aisec.cpg.graph.followPrevEOGEdgesUntilHit
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.Reference

context(CokoCpgBackend)
class ArgumentEvaluator(val targetCall: Op, val argPos: Int, val originCall: Op) : Evaluator {
override fun evaluate(context: EvaluationContext): List<CpgFinding> {
// Get all good calls and the associated variables
val originCalls = originCall.cpgGetAllNodes()
val variables = originCalls.mapNotNull {
it.tryGetVariableDeclaration()
}
val findings = mutableListOf<CpgFinding>()
// Get all target calls using the variable and check whether it is in a good state
val targetCalls = targetCall.cpgGetAllNodes()
for (call in targetCalls) {
val arg: VariableDeclaration? =
(call.arguments.getOrNull(argPos) as? Reference)?.refersTo as? VariableDeclaration
if (arg in variables && arg?.allowsInvalidPaths(originCalls.toList(), call) == false) {
findings.add(
CpgFinding(
message = "Complies with rule: " +
"arg $argPos of \"${call.code}\" stems from a call to \"$originCall\"",
kind = Finding.Kind.Pass,
node = call,
relatedNodes = listOfNotNull(originCalls.firstOrNull { it.tryGetVariableDeclaration() == arg })
)
)
} else {
findings.add(
CpgFinding(
message = "Violation against rule: " +
"arg $argPos of \"${call.code}\" does not stem from a call to \"$originCall\"",
kind = Finding.Kind.Fail,
node = call,
relatedNodes = listOf()
)
)
}
}

return findings
}

/**
* Tries to resolve which variable is modified by a CallExpression
* @return The VariableExpression modified by the CallExpression or null
*/
private fun CallExpression.tryGetVariableDeclaration(): VariableDeclaration? {
return when (val nextDFG = this.nextDFG.firstOrNull()) {
is VariableDeclaration -> nextDFG
is Reference -> nextDFG.refersTo as? VariableDeclaration
else -> null
}
}

/**
* This method tries to get all possible CallExpressions that try to override the variable value
* @return The CallExpressions modifying the variable
*/
private fun VariableDeclaration.getOverrides(): List<Expression> {
val assignments = this.typeObservers.mapNotNull { (it as? Reference)?.prevDFG?.firstOrNull() }
// Consider overwrites caused by CallExpressions and Literals
return assignments.mapNotNull {
when (it) {
is CallExpression -> it
is Literal<*> -> it
else -> null
}
}
}

/**
* This method checks whether there are any paths with forbidden values for the variable that end in the target call
* @param allowedCalls The calls that set the variable to an allowed value
* @param targetCall The target call using the variable as an argument
* @return whether there is at least one path that allows an invalid value for the variable to reach the target
*/
private fun VariableDeclaration.allowsInvalidPaths(
allowedCalls: List<CallExpression>,
targetCall: CallExpression
): Boolean {
// Get every MemberCall that tries to override our variable, ignoring allowed calls
val interferingDeclarations = this.getOverrides().toMutableList() - allowedCalls.toSet()
// Check whether there is a path from any invalid call to our target call that is not overridden at least once
val targetToNoise = targetCall.followPrevEOGEdgesUntilHit { interferingDeclarations.contains(it) }.fulfilled
.filterNot { badPath -> allowedCalls.any { goodCall -> goodCall in badPath } }
return targetToNoise.isNotEmpty()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright (c) 2024, Fraunhofer AISEC. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.fraunhofer.aisec.codyze.backends.cpg

import de.fraunhofer.aisec.codyze.backends.cpg.coko.CokoCpgBackend
import de.fraunhofer.aisec.codyze.backends.cpg.coko.CpgFinding
import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.EvaluationContext
import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.Finding
import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.dsl.definition
import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.dsl.op
import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.dsl.signature
import de.fraunhofer.aisec.cpg.graph.Node
import de.fraunhofer.aisec.cpg.graph.scopes.FunctionScope
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import java.nio.file.Path
import kotlin.io.path.toPath
import kotlin.reflect.full.valueParameters
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class ArgumentEvaluationTest {
@Suppress("UNUSED")
class FooModel {
fun strong() = op {
definition("Foo.strong") {
signature()
}
}

fun weak() = op {
definition("Foo.weak") {
signature()
}
}
}

class BarModel {
fun critical(foundation: Any?) = op {
definition("Bar.critical") {
signature(foundation)
}
}
}

@Test
fun `test simple argument pass`() {
val okFindings = ArgumentEvaluationTest.findings.filter { it.kind == Finding.Kind.Pass }
for (finding in okFindings) {
// pass finding has to be in function that has "ok" in its name
assertTrue("Found PASS finding that was from function ${finding.node?.getFunction()} -> false negative") {
finding.node?.getFunction()?.contains(Regex(".*ok.*", RegexOption.IGNORE_CASE)) == true
}
}
}

@Test
fun `test simple argument fail`() {
val failFindings = ArgumentEvaluationTest.findings.filter { it.kind == Finding.Kind.Fail }
for (finding in failFindings) {
// fail finding should not be in function that has "ok" in its name
assertFalse("Found FAIL finding that was from function ${finding.node?.getFunction()} -> false positive") {
finding.node?.getFunction()?.contains(Regex(".*ok.*", RegexOption.IGNORE_CASE)) == true
}

// fail finding should not be in function that has "noFinding" in its name
assertFalse("Found FAIL finding that was from function ${finding.node?.getFunction()} -> false positive") {
finding.node?.getFunction()?.contains(Regex(".*noFinding.*", RegexOption.IGNORE_CASE)) == true
}
}
}

@Test
fun `test simple argument not applicable`() {
val notApplicableFindings = ArgumentEvaluationTest.findings.filter { it.kind == Finding.Kind.NotApplicable }
for (finding in notApplicableFindings) {
// notApplicable finding has to be in function that has "notApplicable" in its name
assertTrue(
"Found NotApplicable finding that was from function ${finding.node?.getFunction()} -> false negative"
) {
finding.node?.getFunction()?.contains(Regex(".*notApplicable.*", RegexOption.IGNORE_CASE)) == true
}
}
}

private fun Node.getFunction(): String? {
var scope = this.scope
while (scope != null) {
if (scope is FunctionScope) {
return scope.astNode?.name?.localName
}
scope = scope.parent
}
return null
}

companion object {

private lateinit var testFile: Path
lateinit var findings: List<CpgFinding>

@BeforeAll
@JvmStatic
fun startup() {
val classLoader = ArgumentEvaluationTest::class.java.classLoader

val testFileResource = classLoader.getResource("ArgumentEvaluationTest/SimpleArgument.java")
assertNotNull(testFileResource)
testFile = testFileResource.toURI().toPath()

val fooInstance = ArgumentEvaluationTest.FooModel()
val barInstance = ArgumentEvaluationTest.BarModel()

val backend = CokoCpgBackend(config = createCpgConfiguration(testFile))

with(backend) {
val evaluator = argumentOrigin(barInstance::critical, 0, fooInstance::strong)
findings = evaluator.evaluate(
EvaluationContext(
rule = ::dummyRule,
parameterMap = ::dummyRule.valueParameters.associateWith { listOf(fooInstance, barInstance) }
)
)
}
assertTrue("There were no findings which is unexpected") { findings.isNotEmpty() }
}
}
}
Loading

0 comments on commit d0c45be

Please sign in to comment.