-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Florian Wendland <[email protected]>
- Loading branch information
1 parent
83170da
commit c14fbcd
Showing
14 changed files
with
498 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
.../main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/PrecedesEvaluator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
...nds/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/PrecedesEvaluationTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() } | ||
} | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
codyze-backends/cpg/src/test/resources/PrecedesEvaluationTest/SimplePrecedes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.