Skip to content

Commit

Permalink
Merge branch 'main' into rh/onlynever
Browse files Browse the repository at this point in the history
  • Loading branch information
fwendland authored Sep 17, 2024
2 parents d566d46 + d0c45be commit f137a7a
Show file tree
Hide file tree
Showing 30 changed files with 1,003 additions and 39 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/upgrade.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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 @@ -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].
Expand All @@ -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<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,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()
}
}
Loading

0 comments on commit f137a7a

Please sign in to comment.