diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20f0475d1..0d3c521c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: # store version in GitHub environment file echo "version=$VERSION" >> $GITHUB_ENV - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: Build ${{ env.version }} run: ./gradlew :codyze-cli:build -x check --parallel -Pversion=${{ env.version }} - name: Push Release Docker Image diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 2bea31a01..b37675113 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -20,7 +20,7 @@ jobs: distribution: "temurin" java-version: 17 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: Generate coverage report run: ./gradlew testCodeCoverageReport --continue - name: Archive test reports diff --git a/.github/workflows/detekt.yml b/.github/workflows/detekt.yml index c574ccedd..1dbdb872e 100644 --- a/.github/workflows/detekt.yml +++ b/.github/workflows/detekt.yml @@ -26,7 +26,7 @@ jobs: java-version: 17 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: Run analysis run: ./gradlew detektMain detektTest --continue diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bf2f96fbc..5c42e9a25 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ jobs: distribution: "temurin" java-version: 17 - name: 'Setup Gradle' - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: 'Build API pages' run: | ./gradlew dokkaHtmlMultiModule \ diff --git a/.github/workflows/upgrade.yml b/.github/workflows/upgrade.yml index 4fbdc1247..e1588a874 100644 --- a/.github/workflows/upgrade.yml +++ b/.github/workflows/upgrade.yml @@ -38,7 +38,7 @@ jobs: distribution: "temurin" java-version: ${{ matrix.java-lts }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: Build and Test id: build-and-test run: ./gradlew build --parallel 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 901c9f156..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 @@ -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 @@ -61,6 +62,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]. @@ -81,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, argPos: Int, originOp: KFunction): 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] */ diff --git a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/ArgumentEvaluator.kt b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/ArgumentEvaluator.kt new file mode 100644 index 000000000..f4261b8df --- /dev/null +++ b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/ArgumentEvaluator.kt @@ -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 { + // Get all good calls and the associated variables + val originCalls = originCall.cpgGetAllNodes() + val variables = originCalls.mapNotNull { + it.tryGetVariableDeclaration() + } + val findings = mutableListOf() + // 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 { + 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, + 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() + } +} diff --git a/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/PrecedesEvaluator.kt b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/PrecedesEvaluator.kt new file mode 100644 index 000000000..3707d9982 --- /dev/null +++ b/codyze-backends/cpg/src/main/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/coko/evaluators/PrecedesEvaluator.kt @@ -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 { + val (unreachableThisNodes, thisNodes) = + with(this@CokoCpgBackend) { thisOp.cpgGetNodes().toSet() } + .partition { it.isUnreachable() } + + val prevNodes = with(this@CokoCpgBackend) { prevOp.cpgGetNodes().toSet() } + + val findings = mutableListOf() + + // 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() + 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() + } +} diff --git a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/ArgumentEvaluationTest.kt b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/ArgumentEvaluationTest.kt new file mode 100644 index 000000000..93df712c2 --- /dev/null +++ b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/ArgumentEvaluationTest.kt @@ -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 + + @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() } + } + } +} diff --git a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/FollowsEvaluationTest.kt b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/FollowsEvaluationTest.kt index 231f0b736..463ed59b9 100644 --- a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/FollowsEvaluationTest.kt +++ b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/FollowsEvaluationTest.kt @@ -35,6 +35,7 @@ import kotlin.test.assertTrue class FollowsEvaluationTest { + @Suppress("UNUSED") class FooModel { fun first() = op { definition("Foo.first") { @@ -45,6 +46,7 @@ class FollowsEvaluationTest { fun f2() = op {} } + @Suppress("UNUSED") class BarModel { fun second() = op { definition("Bar.second") { 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 b8495d07e..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 @@ -32,6 +32,7 @@ import kotlin.test.assertTrue class NeverEvaluationTest { + @Suppress("UNUSED") class FooModel { fun first(i: Any) = op { definition("Foo.first") { diff --git a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/OnlyEvaluationTest.kt b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/OnlyEvaluationTest.kt index 5b99e2a31..5277615c2 100644 --- a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/OnlyEvaluationTest.kt +++ b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/OnlyEvaluationTest.kt @@ -32,6 +32,7 @@ import kotlin.test.assertTrue class OnlyEvaluationTest { + @Suppress("UNUSED") class FooModel { fun first(i: Any) = op { definition("Foo.fun") { diff --git a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/OrderEvaluationTest.kt b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/OrderEvaluationTest.kt index e7f8fa616..49a2b0390 100644 --- a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/OrderEvaluationTest.kt +++ b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/OrderEvaluationTest.kt @@ -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) } } @@ -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) } } } diff --git a/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/PrecedesEvaluationTest.kt b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/PrecedesEvaluationTest.kt new file mode 100644 index 000000000..6c590b79d --- /dev/null +++ b/codyze-backends/cpg/src/test/kotlin/de/fraunhofer/aisec/codyze/backends/cpg/PrecedesEvaluationTest.kt @@ -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 + + @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() } + } + } +} diff --git a/codyze-backends/cpg/src/test/resources/ArgumentEvaluationTest/SimpleArgument.java b/codyze-backends/cpg/src/test/resources/ArgumentEvaluationTest/SimpleArgument.java new file mode 100644 index 000000000..4be7bae4d --- /dev/null +++ b/codyze-backends/cpg/src/test/resources/ArgumentEvaluationTest/SimpleArgument.java @@ -0,0 +1,162 @@ +import java.util.Random; + +public class SimpleArgument { + + public void ok() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object strong = f.strong(); + b.critical(strong); + } + + public void branchOk() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object strong; + if(new Random().nextBoolean()) + strong = f.strong(); + else + strong = f.strong(); + + b.critical(strong); + } + + public void unreachableFirstNotApplicable() { + Foo f = new Foo(); + Bar b = new Bar(); + + if(false) { + b.critical(null) // unreachable -> never executed so no `strong()` is needed + } + } + + // Should be ok because the `f.weak()` branch is unreachable + public void unreachableOk() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object strong; + if(false) + strong = f.weak(); + else + strong = f.strong(); + + b.critical(strong); + } + + // Should be ok as the final value is strong + public void overwriteOk() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object strong; + strong = f.weak(); + strong = f.strong(); + + b.critical(strong); + } + + // Ok even though we do not use a CallExpression to initialize + public void overwrite2Ok() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object strong; + strong = 46; + strong = f.strong(); + + b.critical(strong); + } + + // Ok since the value is only overwritten later + public void repurposeOk() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object strong; + strong = f.strong(); + + b.critical(strong); + + strong = f.weak(); + } + + public void fail() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object weak = f.weak(); + b.critical(weak); + } + + // should fail because `f.strong()` is only called in one branch TODO + public void branchFail() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object unknown; + if(new Random().nextBoolean()) { + unknown = f.strong(); + } else { + unknown = f.weak(); + } + + b.critical(unknown); + } + + // Should fail as the final value is weak TODO + public void overwriteFail() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object weak; + weak = f.strong(); + weak = f.weak(); + + b.critical(weak); + } + + // Should fail even though we do not use a CallExpression to overwrite + public void overwrite2Fail() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object weak; + weak = f.strong(); + weak = 2; + + b.critical(weak); + } + + // Fail since the value is only set correctly afterwards + public void repurposeFail() { + Foo f = new Foo(); + Bar b = new Bar(); + + Object weak; + weak = f.weak(); + + b.critical(weak); + + weak = f.strong(); + } + + // There is no `Bar.critical()` so there should be no finding + public void noFinding() { + Foo f = new Foo(); + Object strong = f.strong(); + } + +} + +public class Foo { + public Object strong() {} + + public Object weak() {} +} + +public class Bar { + public void critical(Object foundation) {} +} diff --git a/codyze-backends/cpg/src/test/resources/PrecedesEvaluationTest/SimplePrecedes.java b/codyze-backends/cpg/src/test/resources/PrecedesEvaluationTest/SimplePrecedes.java new file mode 100644 index 000000000..8c08b1cd7 --- /dev/null +++ b/codyze-backends/cpg/src/test/resources/PrecedesEvaluationTest/SimplePrecedes.java @@ -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() {} +} \ No newline at end of file diff --git a/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/VersionProviderTest.kt b/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/VersionProviderTest.kt index 5af5d0bf0..87e423576 100644 --- a/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/VersionProviderTest.kt +++ b/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/VersionProviderTest.kt @@ -16,7 +16,7 @@ package de.fraunhofer.aisec.codyze.core import org.junit.jupiter.api.Test -import java.io.FileOutputStream +import java.io.File import java.util.Properties import kotlin.test.assertEquals @@ -34,7 +34,7 @@ class VersionProviderTest { // change property s.t. internal check fails val oldValue = properties.setProperty("project.name", "test") as String - FileOutputStream(propFile.file).use { + File(propFile.toURI()).outputStream().use { properties.store(it, null) } @@ -49,7 +49,7 @@ class VersionProviderTest { // restore original properties file properties.setProperty("project.name", oldValue) - FileOutputStream(propFile.file).use { + File(propFile.toURI()).outputStream().use { properties.store(it, null) } } diff --git a/codyze-plugins/build.gradle.kts b/codyze-plugins/build.gradle.kts index affcd9577..ccb44dda9 100644 --- a/codyze-plugins/build.gradle.kts +++ b/codyze-plugins/build.gradle.kts @@ -25,8 +25,8 @@ dependencies { implementation("com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0") // https://mvnrepository.com/artifact/net.sourceforge.pmd/ - implementation("net.sourceforge.pmd:pmd-core:7.3.0") - implementation("net.sourceforge.pmd:pmd-java:7.3.0") + implementation("net.sourceforge.pmd:pmd-core:7.5.0") + implementation("net.sourceforge.pmd:pmd-java:7.5.0") } publishing { diff --git a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/PluginTest.kt b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/PluginTest.kt index 3270bc2eb..4813b33a8 100644 --- a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/PluginTest.kt +++ b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/PluginTest.kt @@ -21,6 +21,8 @@ import io.github.detekt.sarif4k.Result import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import java.io.File +import kotlin.io.path.deleteExisting +import kotlin.io.path.toPath import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -31,12 +33,19 @@ abstract class PluginTest { open val expectedSuccess: Boolean = true abstract val expectedResults: List + val reportUri by lazy { + // use an existing anchor in the resources directory + PluginTest::class.java.classLoader.getResource("targets")!!.toURI() + .resolve("..") + .resolve("generatedReports") + .resolve(resultFileName) + } + @Test fun testResults() { scanFiles() - val resultURI = PluginTest::class.java.classLoader.getResource("generatedReports/$resultFileName")?.toURI() - assertNotNull(resultURI) - val run = extractLastRun(File(resultURI)) + + val run = extractLastRun(File(reportUri)) assertNotNull(run) var results = run.results @@ -58,9 +67,8 @@ abstract class PluginTest { @Test fun testInvocation() { scanFiles() - val resultURI = PluginTest::class.java.classLoader.getResource("generatedReports/$resultFileName")?.toURI() - assertNotNull(resultURI) - val run = extractLastRun(File(resultURI)) + + val run = extractLastRun(File(reportUri)) assertNotNull(run) if (!run.invocations.isNullOrEmpty()) { @@ -72,9 +80,8 @@ abstract class PluginTest { @AfterEach fun cleanup() { - val resultURI = PluginTest::class.java.classLoader.getResource("generatedReports/$resultFileName")?.toURI() - if (resultURI != null) { - File(resultURI).delete() + reportUri.runCatching { + this.toPath().deleteExisting() } } diff --git a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/CompiledPluginTest.kt b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/CompiledPluginTest.kt index 80c92c34e..6b1979360 100644 --- a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/CompiledPluginTest.kt +++ b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/CompiledPluginTest.kt @@ -16,24 +16,32 @@ package de.fraunhofer.aisec.codyze.plugins.compiled import de.fraunhofer.aisec.codyze.plugins.PluginTest -import java.nio.file.Path +import java.io.File +import kotlin.io.path.toPath import kotlin.test.assertNotNull abstract class CompiledPluginTest : PluginTest() { override fun scanFiles() { - val libPath = PluginTest::class.java.classLoader.getResource("targets/libs/demo-cloud-service-1.0.0.jar")?.path + // certain that resources are available + val libPath = + PluginTest::class.java.classLoader.getResource("targets/libs/demo-cloud-service-1.0.0.jar")!! + .toURI().toPath() val contextPaths = listOf( - PluginTest::class.java.classLoader.getResource("targets/libs/bcpkix-jdk18on-1.75.jar")?.path, - PluginTest::class.java.classLoader.getResource("targets/libs/bcprov-jdk18on-1.75.jar")?.path, - PluginTest::class.java.classLoader.getResource("targets/libs/bctls-jdk18on-1.75.jar")?.path, - PluginTest::class.java.classLoader.getResource("targets/libs/bcutil-jdk18on-1.75.jar")?.path + PluginTest::class.java.classLoader.getResource("targets/libs/bcpkix-jdk18on-1.75.jar")!! + .toURI().toPath(), + PluginTest::class.java.classLoader.getResource("targets/libs/bcprov-jdk18on-1.75.jar")!! + .toURI().toPath(), + PluginTest::class.java.classLoader.getResource("targets/libs/bctls-jdk18on-1.75.jar")!! + .toURI().toPath(), + PluginTest::class.java.classLoader.getResource("targets/libs/bcutil-jdk18on-1.75.jar")!! + .toURI().toPath() ) assertNotNull(libPath) plugin.execute( - listOf(Path.of(libPath)), - contextPaths.map { Path.of(it!!) }, - Path.of(libPath).parent.parent.parent.resolve("generatedReports").resolve(resultFileName).toFile() + listOf(libPath), + contextPaths, + File(reportUri) ) } } diff --git a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/SourcePluginTest.kt b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/SourcePluginTest.kt index 1e0d7a479..bcdbadf32 100644 --- a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/SourcePluginTest.kt +++ b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/SourcePluginTest.kt @@ -16,18 +16,19 @@ package de.fraunhofer.aisec.codyze.plugins.source import de.fraunhofer.aisec.codyze.plugins.PluginTest -import java.nio.file.Path +import java.io.File +import kotlin.io.path.toPath import kotlin.test.assertNotNull abstract class SourcePluginTest : PluginTest() { override fun scanFiles() { - val sourcePath = PluginTest::class.java.classLoader.getResource("targets/TlsServer.java")?.path + val sourcePath = PluginTest::class.java.classLoader.getResource("targets/TlsServer.java")!!.toURI().toPath() assertNotNull(sourcePath) plugin.execute( - listOf(Path.of(sourcePath)), + listOf(sourcePath), listOf(), - Path.of(sourcePath).parent.parent.resolve("generatedReports").resolve(resultFileName).toFile() + File(reportUri) ) } } diff --git a/codyze-specification-languages/coko/coko-core/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguages/coko/core/CokoBackend.kt b/codyze-specification-languages/coko/coko-core/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguages/coko/core/CokoBackend.kt index 09f00d662..230d3d708 100644 --- a/codyze-specification-languages/coko/coko-core/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguages/coko/core/CokoBackend.kt +++ b/codyze-specification-languages/coko/coko-core/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguages/coko/core/CokoBackend.kt @@ -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, @@ -71,4 +74,11 @@ interface CokoBackend : Backend { ): WheneverEvaluator fun whenever(premise: ConditionComponent, assertionBlock: WheneverEvaluator.() -> Unit): WheneverEvaluator + + /** Verifies that the argument at [argPos] of [targetOp] stems from a call to [originOp] */ + fun argumentOrigin( + targetOp: KFunction, + argPos: Int, + originOp: KFunction, + ): Evaluator } diff --git a/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/Test.java b/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/Test.java new file mode 100644 index 000000000..f0083f15b --- /dev/null +++ b/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/Test.java @@ -0,0 +1,65 @@ +/* + * 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. + */ +import java.util.Random; +import java.lang.System; + +class Test { + int kelvin = null; + int celsius = null; + + private void setKelvin(int tempInKelvin) { + // MUST NOT BE ZERO! + // HOWEVER, I'D REALLY HATE TO CHECK THIS HERE + this.kelvin = tempInKelvin; + } + + // MUST NOT BE CALLED BEFORE `setKelvin` + // BUT IMPLEMENTING FAIL-SAFES IS FOR NERDS + private int calculateCelsius() { + this.celsius = this.kelvin - 272; + } + + // EAT: Extremely Accurate Temperature + private int measureTemperatureInK() { + return Random.nextInt(250, 300); + } + + // covered by 11 Unit tests and manually reviewed by 4 developers + public void goodCall() { + int temp = measureTemperatureInK(); + setKelvin(temp); + calculateCelsius(); + System.out.println("The current temperature is " + celsius + "°C"); + } + + // TODO: force merge, should work, might rename later + public void badCall() { + float rnd = Random.nextFloat(); + int temp = measureTemperatureInK(); + if (rnd < 0.33) { + // set Kelvin to zero, break the world + setKelvin(0); + calculateCelsius(); + } else if (rnd < 0.66) { + // we only need Celsius + calculateCelsius(); + } else { + // Kelvin is superior anyways + setKelvin(temp); + } + System.out.println("The current temperature is " + celsius + "°C"); + } +} \ No newline at end of file diff --git a/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/altmodel.codyze.kts b/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/altmodel.codyze.kts new file mode 100644 index 000000000..0f6d94c3b --- /dev/null +++ b/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/altmodel.codyze.kts @@ -0,0 +1,31 @@ +class TestModel { + fun setKelvin(temp: Any?) = op { + definition("Test.setKelvin") { + signature(temp) + } + } + + fun getCelsius() = op { + definition("Test.calculateCelsius") { + signature() + } + } + + fun call() = op { + definition("Test.badCall") { + signature() + } + } +} + +@Rule("Must not call kelvin with 0") +fun preventZeroKelvin(test: TestModel) = + never(test.setKelvin(0)) + +@Rule("Must call kelvin before celsius") +fun forceKelvinBeforeCelsius(test: TestModel) = + test.setKelvin(Wildcard) precedes test.getCelsius() + +@Rule("Must call celsius after kelvin") +fun forceCelsiusAfterKelvin(test: TestModel) = + test.setKelvin(Wildcard) followedBy test.getCelsius() \ No newline at end of file diff --git a/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/kotlinimpl.codyze.kts b/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/kotlinimpl.codyze.kts new file mode 100644 index 000000000..f06498b24 --- /dev/null +++ b/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/kotlinimpl.codyze.kts @@ -0,0 +1,30 @@ +@file:Import("model.codyze.kts") + +plugins { id("cpg") } + +class KotlinKelvin: SetKelvin { + override fun kelvin( + temp: Int? + ) = op { + definition("Test.setKelvin") { + signature(temp) + } + } +} + +class KotlinCelsius: SetCelsius { + override fun celsius() = op { + definition("Test.calculateCelsius") { + signature() + } + } +} + +class BadCall: Call { + override fun call() = op { + definition("Test.badCall") { + signature() + } + } +} + diff --git a/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/model.codyze.kts b/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/model.codyze.kts new file mode 100644 index 000000000..520c5e114 --- /dev/null +++ b/codyze-specification-languages/coko/coko-dsl/src/test/resources/kotlin/model.codyze.kts @@ -0,0 +1,24 @@ +interface SetKelvin { + fun kelvin(temp: Int?): Op +} + +interface SetCelsius { + fun celsius(): Op +} + +interface Call { + fun call(): Op +} + + +@Rule("Must not call kelvin with 0") +fun preventZeroKelvin(kelv: SetKelvin) = + never(kelv.kelvin(0)) + + +@Rule("Must call kelvin before celsius") +fun forceKelvinBeforeCelsius(kelv: SetKelvin, cels: SetCelsius, call: Call) = + order(call.call()) { + - some(kelv::kelvin) + - maybe(cels::celsius) + } diff --git a/docs/Coko/rules.md b/docs/Coko/rules.md index 879ff4e38..b62949624 100644 --- a/docs/Coko/rules.md +++ b/docs/Coko/rules.md @@ -47,7 +47,6 @@ Therefore, it takes one `Op` as argument. @Rule fun `only calls to first with 1 allowed`(foo: Foo) = only(foo.first(1)) - ``` @@ -89,7 +88,6 @@ fun `order of Foo`(foo: Foo) = - foo.first(...) // (3)! maybe(foo::second) // (4)! } - ``` 1. This starts the type-safe builder for the order. @@ -107,7 +105,17 @@ Compared to the `order` evaluator, `followedBy` is more flexible because `Ops` f @Rule fun `if first then second`(foo: Foo, bar: Bar) = foo.first(Wildcard) followedBy bar.second() +``` +## Precedes Evaluator +The `precedes` evaluator is the logical counterpart to the `followedBy` evaluator, implementing a logical reverse implication. +Similar to the previous evaluator, it takes two `Ops`. Whenever the second `Op` is called, the first `Op` must have been called before. +In contrast to the `followedBy` evaluator, the second `Op`acts as the trigger for the rule and no finding is generated when only the first `Op` is encountered in any given context. + +```kotlin title="Rule example using precedes" +@Rule +fun `always first before second`(foo: Foo, bar: Bar) = + foo.first(Wildcard) precedes bar.second() ``` ## Never Evaluator @@ -118,5 +126,20 @@ It takes one `Op` as argument. @Rule fun `never call second with 1`(foo: Foo) = never(foo.second(1)) +``` + +## Argument Evaluator +The `argumentOrigin` evaluator is used to trace back the argument of a call to a specific method call. +It takes three arguments: + - The target `Op` whose argument we want to verify + - The position of the argument in question (0-based indexing) + - The origin `Op` which should have produced the argument +The evaluator will then try to check whether the argument of the target `Op` was always produced by a call to the origin `Op`. +If this is not the case or the Evaluator lacks information to clearly determine the origin of the argument, it will generate a finding. + +```kotlin title="Rule example using argumentOrigin" +@Rule +fun `only call Foo::critical with argument produced by Bar::strong`(foo: Foo, bar: Bar) = + argumentOrigin(Foo::critical, 0, Bar::strong) ``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e45750a2d..ff0e3f1e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,14 +3,14 @@ kotlin = "1.9.25" cpg = "8.3.0" koin = "3.5.6" koin-test = "3.5.6" -detekt = "1.23.6" +detekt = "1.23.7" spotless = "6.25.0" dokka = "1.9.20" [libraries] sarif4k = { module = "io.github.detekt.sarif4k:sarif4k", version = "0.6.0"} # The code can be found here: https://github.com/detekt/sarif4k. It was generated using https://app.quicktype.io/ -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.1"} +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.2"} kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin"} # CPG official releases @@ -29,7 +29,7 @@ cpg-language-java = { module = "de.fraunhofer.aisec:cpg-language-java", version. #cpg-language-go = { module = "com.github.Fraunhofer-AISEC.cpg:cpg-language-go", version.ref = "cpg"} kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version = "7.0.0" } -log4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version = "2.23.1"} +log4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version = "2.24.0"} clikt = { module = "com.github.ajalt.clikt:clikt", version = "4.4.0"} koin = { module = "io.insert-koin:koin-core", version.ref = "koin"} koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin-test"} @@ -41,7 +41,7 @@ kotlin-scripting-dependencies = { module = "org.jetbrains.kotlin:kotlin-scriptin detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt"} # test -junit-bom = { module = "org.junit:junit-bom", version = "5.10.3" } +junit-bom = { module = "org.junit:junit-bom", version = "5.11.0" } mockk = { module = "io.mockk:mockk", version = "1.13.12"} # this is necessary for the plugins to be used in the buildSrc folder diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197..a4b76b953 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e5..0aaefbcaf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME