Skip to content

Commit

Permalink
Coko 'Precedes' Evaluator (#863)
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 83170da commit c14fbcd
Show file tree
Hide file tree
Showing 14 changed files with 498 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class CokoCpgBackend(config: BackendConfiguration) :
/** For each of the nodes in [this], there is a path to at least one of the nodes in [that]. */
override infix fun Op.followedBy(that: Op): FollowsEvaluator = FollowsEvaluator(ifOp = this, thenOp = that)

/** For each of the nodes in [that], there is a path from at least one of the nodes in [this]. */
override infix fun Op.precedes(that: Op): PrecedesEvaluator = PrecedesEvaluator(prevOp = this, thisOp = that)

/*
* Ensures the order of nodes as specified in the user configured [Order] object.
* The order evaluation starts at the given [baseNodes].
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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.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 de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.edge.Properties
import kotlin.reflect.full.findAnnotation

context(CokoCpgBackend)
class PrecedesEvaluator(val prevOp: Op, val thisOp: Op) : Evaluator {

private val defaultFailMessage: String by lazy {
"It is not preceded by any of these calls: $prevOp."
}

private val defaultPassMessage = ""

override fun evaluate(context: EvaluationContext): List<CpgFinding> {
val (unreachableThisNodes, thisNodes) =
with(this@CokoCpgBackend) { thisOp.cpgGetNodes().toSet() }
.partition { it.isUnreachable() }

val prevNodes = with(this@CokoCpgBackend) { prevOp.cpgGetNodes().toSet() }

val findings = mutableListOf<CpgFinding>()

// add all unreachable `this` nodes as NotApplicable findings
findings.addAll(
unreachableThisNodes.map {
CpgFinding(
message = "Rule is not applicable for \"${it.code}\" because it is unreachable",
kind = Finding.Kind.NotApplicable,
node = it
)
}
)

val ruleAnnotation = context.rule.findAnnotation<Rule>()
val failMessage = ruleAnnotation?.failMessage?.takeIf { it.isNotEmpty() } ?: defaultFailMessage
val passMessage = ruleAnnotation?.passMessage?.takeIf { it.isNotEmpty() } ?: defaultPassMessage

for (target in thisNodes) {
val paths = target.followPrevEOGEdgesUntilHit { prevNodes.contains(it) }

val newFindings =
if (paths.fulfilled.isNotEmpty() && paths.failed.isEmpty()) {
val availablePrevNodes = paths.fulfilled.mapNotNull { it.firstOrNull() }
// All paths starting from `from` end in one of the `that` nodes
listOf(
CpgFinding(
message = "Complies with rule: ${availablePrevNodes.joinToString(
prefix = "\"",
separator = "\", \"",
postfix = "\"",
transform = { node -> node.code ?: node.toString() }
)} precedes ${target.code}. $passMessage",
kind = Finding.Kind.Pass,
node = target,
relatedNodes = availablePrevNodes
)
)
} else {
// Some (or all) paths starting from `from` do not end in any of the `that` nodes
paths.failed.map { failedPath ->
// make a finding for each failed path
CpgFinding(
message =
"Violation against rule in execution path to \"${target.code}\". $failMessage",
kind = Finding.Kind.Fail,
node = target,
// improve: specify paths more precisely
// for example one branch passes and one fails skip part in path after branches are combined
relatedNodes = listOf(failedPath.first())
)
}
}

findings.addAll(newFindings)
}

return findings
}

/** Checks if this node is unreachable */
private fun Node.isUnreachable(): Boolean {
val prevPaths = this.followPrevEOGEdgesUntilHit {
it.prevEOGEdges.isNotEmpty() && it.prevEOGEdges.all {
edge ->
edge.getProperty(Properties.UNREACHABLE) == true
}
}
return prevPaths.fulfilled.isNotEmpty()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import kotlin.test.assertTrue

class FollowsEvaluationTest {

@Suppress("UNUSED")
class FooModel {
fun first() = op {
definition("Foo.first") {
Expand All @@ -45,6 +46,7 @@ class FollowsEvaluationTest {
fun f2() = op {}
}

@Suppress("UNUSED")
class BarModel {
fun second() = op {
definition("Bar.second") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import kotlin.test.assertTrue

class NeverEvaluationTest {

@Suppress("UNUSED")
class FooModel {
fun first(i: Any) = op {
definition("Foo.first") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import kotlin.test.assertTrue

class OnlyEvaluationTest {

@Suppress("UNUSED")
class FooModel {
fun first(i: Any) = op {
definition("Foo.fun") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import kotlin.test.assertEquals
* - [NfaDfaConstructionTest]
*/
class OrderEvaluationTest {
@Suppress("UNUSED")
class CokoOrderImpl {
fun constructor(value: Int?) = constructor("Botan") { signature(value) }
fun init() = op { "Botan.set_key" { signature(Wildcard) } }
Expand All @@ -46,6 +47,7 @@ class OrderEvaluationTest {
fun finish() = op { "Botan.finish" { signature(Wildcard) } }
}

@Suppress("UNUSED")
class OtherImpl {
fun foo() = op { definition("Botan.foo") { signature(Wildcard) } }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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

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.*
import kotlin.reflect.full.valueParameters
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class PrecedesEvaluationTest {

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

fun f2() = op {}
}

@Suppress("UNUSED")
class BarModel {
fun second() = op {
definition("Bar.second") {
signature()
}
}
}

@Test
fun `test simple precedes pass`() {
val okFindings = 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 follows fail`() {
val failFindings = 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 follows not applicable`() {
val notApplicableFindings = 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 {

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

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

val testFileResource = classLoader.getResource("PrecedesEvaluationTest/SimplePrecedes.java")
assertNotNull(testFileResource)
testFile = testFileResource.toURI().toPath()

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

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

with(backend) {
val evaluator = fooInstance.first() precedes barInstance.second()
findings = evaluator.evaluate(
EvaluationContext(
rule = ::dummyRule,
parameterMap = ::dummyRule.valueParameters.associateWith { listOf(fooInstance, barInstance) }
)
)
}
assertTrue("There were no findings which is unexpected") { findings.isNotEmpty() }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import java.util.Random;

public class SimpleFollows {

public void ok() {
Foo f = new Foo();
Bar b = new Bar();
f.first();
b.second();
}

public void branchOk() {
Foo f = new Foo();
Bar b = new Bar();
if(new Random().nextBoolean())
f.first();
else
f.first();
b.second();
}

public void unreachableSecondNotApplicable() {
Bar b = new Bar();
if(false) {
b.second(); // unreachable -> never executed so no `first()` is needed
}
}

// Should be ok because the `f.f2()` branch is unreachable
public void unreachableOk() {
Foo f = new Foo();
Bar b = new Bar();
if(false)
f.f2();
else
f.first();
b.second();
}

// should fail because `f.first()` is only called in one branch
public void branchFail() {
Foo f = new Foo();
Bar b = new Bar();
if(new Random().nextBoolean()) {
f.first();
}
b.second();
}

public void fail() {
Bar b = new Bar();
b.second();
}

// There is no `Bar.second()` so there should be no finding
public void noFinding() {
Foo f = new Foo();
f.first();
}

}

public class Foo {
public int first() {}

public void f2() {}
}

public class Bar {
public void second() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ interface CokoBackend : Backend {
/** For each of the nodes in [this], there is a path to at least one of the nodes in [that]. */
infix fun Op.followedBy(that: Op): Evaluator

/** For each of the nodes in [that], there is a path from at least one of the nodes in [this]. */
infix fun Op.precedes(that: Op): Evaluator

/** Ensures the order of nodes as specified in the user configured [Order] object */
fun order(
baseNodes: OrderToken,
Expand Down
Loading

0 comments on commit c14fbcd

Please sign in to comment.