Skip to content

Commit

Permalink
Combine Only and Never Evaluator (#875)
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 d0c45be commit 5587864
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ class CokoCpgBackend(config: BackendConfiguration) :
/**
* Ensures that all calls to the [ops] have arguments that fit the parameters specified in [ops]
*/
override fun only(vararg ops: Op): OnlyEvaluator = OnlyEvaluator(ops.toList())
override fun never(vararg ops: Op): NeverEvaluator = NeverEvaluator(ops.toList())
override fun only(vararg ops: Op): OnlyNeverEvaluator =
OnlyNeverEvaluator(ops.toList(), OnlyNeverEvaluator.Functionality.ONLY)
override fun never(vararg ops: Op): OnlyNeverEvaluator =
OnlyNeverEvaluator(ops.toList(), OnlyNeverEvaluator.Functionality.NEVER)
override fun whenever(
premise: Condition.() -> ConditionComponent,
assertionBlock: WheneverEvaluator.() -> Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,27 @@ private fun CallExpression.cpgCheckArgsSize(parameters: Array<*>, hasVarargs: Bo
} else {
parameters.size == arguments.size
}

/**
* To generate more interesting Findings
* we want to find key points where the forbidden operation influences other code.
* For this we traverse the DFG for a fixed amount of steps and search for all usages of declared values.
*/
fun Node.findUsages(depth: Int = 5): Collection<Node> {
val currentNodes: MutableSet<Node> = mutableSetOf(this)
val usages = mutableSetOf<Node>()
for (i in 0..depth) {
// The set will be empty if we found a usage or no further DFG for all branches
if (currentNodes.isEmpty()) {
break
}
for (current in currentNodes) {
currentNodes.remove(current)
when (current) {
is ValueDeclaration -> usages += current.usages
else -> currentNodes += current.nextDFG
}
}
}
return usages
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ 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.backends.cpg.coko.dsl.cpgGetNodes
import de.fraunhofer.aisec.codyze.backends.cpg.coko.dsl.findUsages
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
Expand All @@ -27,56 +28,66 @@ import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.dsl.Rule
import kotlin.reflect.full.findAnnotation

context(CokoCpgBackend)
class OnlyEvaluator(val ops: List<Op>) : Evaluator {
class OnlyNeverEvaluator(private val ops: List<Op>, private val functionality: Functionality) : Evaluator {

/** Default message if a violation is found */
private val defaultFailMessage: String by lazy {
// Try to model what the allowed calls look like with `toString` call of `Op`
"Only calls to ${ops.joinToString()} allowed."
"${if (functionality == Functionality.NEVER) "No" else "Only"} calls to ${ops.joinToString()} allowed."
}

/** Default message if node complies with rule */
private val defaultPassMessage = "Call is in compliance with rule"

override fun evaluate(context: EvaluationContext): List<CpgFinding> {
val correctNodes =
with(this@CokoCpgBackend) { ops.flatMap { it.cpgGetNodes() } }
.toSet()

val distinctOps = ops.toSet()
val allNodes =
with(this@CokoCpgBackend) { distinctOps.flatMap { it.cpgGetAllNodes() } }
.filter { it.location != null }
.toSet()

// `correctNodes` is a subset of `allNodes`
// we want to find nodes in `allNodes` that are not contained in `correctNodes` since they are violations
val violatingNodes = allNodes.minus(correctNodes)
// `matchingNodes` is a subset of `allNodes`
// we want to find nodes in `allNodes` that are not contained in `matchingNodes`
// since they are contrary Findings
val matchingNodes =
with(this@CokoCpgBackend) { ops.flatMap { it.cpgGetNodes() } }
.toSet()
val differingNodes = allNodes.minus(matchingNodes)

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

// define what violations and passes are, depending on selected functionality
val correctNodes = if (functionality == Functionality.NEVER) differingNodes else matchingNodes
val violatingNodes = if (functionality == Functionality.NEVER) matchingNodes else differingNodes
val findings = mutableListOf<CpgFinding>()

for (node in violatingNodes) {
findings.add(
CpgFinding(
message = "Violation against rule: \"${node.code}\". $failMessage",
kind = Finding.Kind.Fail,
node = node
node = node,
relatedNodes = node.findUsages()
)
)
}

for (node in correctNodes) {
findings.add(
CpgFinding(
message = "Complies with rule: \"${node.code}\". $passMessage",
kind = Finding.Kind.Pass,
node = node
node = node,
relatedNodes = node.findUsages()
)
)
}

return findings
}

enum class Functionality {
ONLY,
NEVER
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class NeverEvaluationTest {
findings.all { it.kind == Finding.Kind.Pass }
}

assertEquals(1, findings.size, "Found ${findings.size} finding(s) instead of one pass finding")
assertEquals(4, findings.size, "Found ${findings.size} finding(s) instead of one pass finding")
}
}

Expand Down

0 comments on commit 5587864

Please sign in to comment.