Skip to content

Commit

Permalink
Taint configuration feature support (#195)
Browse files Browse the repository at this point in the history
* Add taint configuration
* Use kotlinx serialization
* Store rules in a trie
* Small refactorings
* First tests
* Add some tests and fix some bugs
* Add more tests
* Add config into release script
  • Loading branch information
CaelmBleidd authored Nov 7, 2023
1 parent ed17f6b commit 1772af5
Show file tree
Hide file tree
Showing 17 changed files with 2,167 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
files: |
jacodb-api/build/libs/jacodb-api-${{inputs.semVer}}.jar
jacodb-approximations/build/libs/jacodb-approximations-${{inputs.semVer}}.jar
jacodb-taint-configuration/build/libs/jacodb-taint-configuration-${{inputs.semVer}}.jar
jacodb-analysis/build/libs/jacodb-analysis-${{inputs.semVer}}.jar
jacodb-core/build/libs/jacodb-core-${{inputs.semVer}}.jar
jacodb-cli/build/libs/jacodb-cli-${{inputs.semVer}}.jar
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ if (!repoUrl.isNullOrEmpty()) {
project(":jacodb-core"),
project(":jacodb-analysis"),
project(":jacodb-approximations"),
project(":jacodb-taint-configuration"),
)
) {
tasks {
Expand Down
2 changes: 1 addition & 1 deletion jacodb-api/src/main/kotlin/org/jacodb/api/ext/JcClasses.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ fun JcClassOrInterface.toType(): JcClassType {
return classpath.typeOf(this) as JcClassType
}

val JcClassOrInterface.packageName get() = name.substringBeforeLast(".")
val JcClassOrInterface.packageName get() = name.substringBeforeLast(".", missingDelimiterValue = "")

const val JAVA_OBJECT = "java.lang.Object"

Expand Down
21 changes: 21 additions & 0 deletions jacodb-taint-configuration/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
plugins {
id("java")
kotlin("plugin.serialization")
}

repositories {
mavenCentral()
}

dependencies {
implementation(project(":jacodb-api"))
implementation(project(":jacodb-core"))
implementation(testFixtures(project(":jacodb-core")))
implementation(Libs.kotlinx_serialization_json)

testImplementation(group = "io.github.microutils", name = "kotlin-logging", version = "1.8.3")
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.jacodb.configuration

import org.jacodb.api.JcClassOrInterface
import org.jacodb.api.ext.packageName

class ConfigurationTrie(
configuration: List<SerializedTaintConfigurationItem>,
private val nameMatcher: (NameMatcher, String) -> Boolean
) {
private val unprocessedRules: MutableList<SerializedTaintConfigurationItem> = configuration.toMutableList()
private val rootNode: RootNode = RootNode()

private fun initializeIfRequired() {
if (unprocessedRules.isEmpty()) return

while (unprocessedRules.isNotEmpty()) {
var configurationRule = unprocessedRules.removeLast()
val classMatcher = configurationRule.methodInfo.cls

val alternativeClassMatchers = classMatcher.extractAlternatives()
if (alternativeClassMatchers.size != 1) {
alternativeClassMatchers.forEach {
val updatedMethodInfo = configurationRule.methodInfo.copy(cls = it)
unprocessedRules += configurationRule.updateMethodInfo(updatedMethodInfo)
}

continue
}

val simplifiedClassMatcher = alternativeClassMatchers.single()
val updatedMethodInfo = configurationRule.methodInfo.copy(cls = simplifiedClassMatcher)
configurationRule = configurationRule.updateMethodInfo(updatedMethodInfo)

var currentNode: Node = rootNode

val (simplifiedPkgMatcher, simplifiedClassNameMatcher) = simplifiedClassMatcher

var matchedPackageNameParts = emptyList<String>()
var unmatchedPackageNamePart: String? = null

when (simplifiedPkgMatcher) {
AnyNameMatcher -> {
currentNode.unmatchedRules += configurationRule
continue
}

is NameExactMatcher -> matchedPackageNameParts = simplifiedPkgMatcher.name.split(DOT_DELIMITER)
is NamePatternMatcher -> {
val (matchedParts, unmatchedParts) = simplifiedPkgMatcher.splitRegex()
matchedPackageNameParts = matchedParts
unmatchedPackageNamePart = unmatchedParts
}
}

for (part in matchedPackageNameParts) {
currentNode = currentNode.children[part] ?: NodeImpl(part).also { currentNode.children += part to it }
}

if (unmatchedPackageNamePart != null && unmatchedPackageNamePart != ALL_MATCH) {
currentNode.unmatchedRules += configurationRule
continue
}

when (simplifiedClassNameMatcher) {
AnyNameMatcher -> currentNode.rules += configurationRule

is NameExactMatcher -> if (unmatchedPackageNamePart == null) {
val name = simplifiedClassNameMatcher.name
currentNode = currentNode.children[name] ?: Leaf(name).also { currentNode.children += name to it }
currentNode.rules += configurationRule
} else {
// case for patterns like ".*\.Request"
currentNode.unmatchedRules += configurationRule
}

is NamePatternMatcher -> {
val classPattern = simplifiedClassNameMatcher.pattern

if (classPattern == ALL_MATCH) {
currentNode.rules += configurationRule
continue
}

currentNode.unmatchedRules += configurationRule
}
}
}
}

fun getRulesForClass(clazz: JcClassOrInterface): List<SerializedTaintConfigurationItem> {
initializeIfRequired()

val results = mutableListOf<SerializedTaintConfigurationItem>()

val className = clazz.simpleName
val packageName = clazz.packageName
val nameParts = clazz.name.split(DOT_DELIMITER)

var currentNode: Node = rootNode

for (i in 0..nameParts.size) {
results += currentNode.unmatchedRules.filter {
val classMatcher = it.methodInfo.cls
nameMatcher(classMatcher.pkg, packageName) && nameMatcher(classMatcher.classNameMatcher, className)
}

results += currentNode.rules

// We must process rules containing in the leaf, therefore, we have to spin one more iteration
currentNode = nameParts.getOrNull(i)?.let { currentNode.children[it] } ?: break
}

return results
}

private sealed class Node {
abstract val value: String
abstract val children: MutableMap<String, Node>
abstract val rules: MutableList<SerializedTaintConfigurationItem>
abstract val unmatchedRules: MutableList<SerializedTaintConfigurationItem>
}

private class RootNode : Node() {
override val children: MutableMap<String, Node> = mutableMapOf()
override val value: String
get() = error("Must not be called for the root")
override val rules: MutableList<SerializedTaintConfigurationItem> = mutableListOf()
override val unmatchedRules: MutableList<SerializedTaintConfigurationItem> = mutableListOf()
}

private data class NodeImpl(
override val value: String,
) : Node() {
override val children: MutableMap<String, Node> = mutableMapOf()
override val rules: MutableList<SerializedTaintConfigurationItem> = mutableListOf()
override val unmatchedRules: MutableList<SerializedTaintConfigurationItem> = mutableListOf()
}

private data class Leaf(
override val value: String
) : Node() {
override val children: MutableMap<String, Node>
get() = error("Leaf nodes do not have children")
override val unmatchedRules: MutableList<SerializedTaintConfigurationItem>
get() = mutableListOf()

override val rules: MutableList<SerializedTaintConfigurationItem> = mutableListOf()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.jacodb.configuration

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

interface PositionResolver<R> {
fun resolve(position: Position): R
}

@Serializable
sealed interface Position

@Serializable
@SerialName("Argument")
data class Argument(val number: Int) : Position

@Serializable
@SerialName("AnyArgument")
object AnyArgument : Position

@Serializable
@SerialName("This")
object ThisArgument : Position

@Serializable
@SerialName("Result")
object Result : Position
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.jacodb.configuration

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable


@Serializable
sealed interface SerializedTaintConfigurationItem {
val methodInfo: FunctionMatcher

fun updateMethodInfo(updatedMethodInfo: FunctionMatcher): SerializedTaintConfigurationItem =
when (this) {
is SerializedTaintCleaner -> copy(methodInfo = updatedMethodInfo)
is SerializedTaintEntryPointSource -> copy(methodInfo = updatedMethodInfo)
is SerializedTaintMethodSink -> copy(methodInfo = updatedMethodInfo)
is SerializedTaintMethodSource -> copy(methodInfo = updatedMethodInfo)
is SerializedTaintPassThrough -> copy(methodInfo = updatedMethodInfo)
}
}

@Serializable
@SerialName("EntryPointSource")
data class SerializedTaintEntryPointSource(
override val methodInfo: FunctionMatcher,
val condition: Condition,
val actionsAfter: List<Action>,
) : SerializedTaintConfigurationItem

@Serializable
@SerialName("MethodSource")
data class SerializedTaintMethodSource(
override val methodInfo: FunctionMatcher,
val condition: Condition,
val actionsAfter: List<Action>,
) : SerializedTaintConfigurationItem

@Serializable
@SerialName("MethodSink")
data class SerializedTaintMethodSink(
val ruleNote: String,
val cwe: List<Int>,
override val methodInfo: FunctionMatcher,
val condition: Condition
) : SerializedTaintConfigurationItem

@Serializable
@SerialName("PassThrough")
data class SerializedTaintPassThrough(
override val methodInfo: FunctionMatcher,
val condition: Condition,
val actionsAfter: List<Action>,
) : SerializedTaintConfigurationItem

@Serializable
@SerialName("Cleaner")
data class SerializedTaintCleaner(
override val methodInfo: FunctionMatcher,
val condition: Condition,
val actionsAfter: List<Action>,
) : SerializedTaintConfigurationItem
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.jacodb.configuration

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

interface TaintActionVisitor<R> {
fun visit(action: CopyAllMarks): R
fun visit(action: CopyMark): R
fun visit(action: AssignMark): R
fun visit(action: RemoveAllMarks): R
fun visit(action: RemoveMark): R
}

interface Action {
fun <R> accept(visitor: TaintActionVisitor<R>): R
}

// TODO add marks for aliases (if you pass an object and return it from the function)
@Serializable
@SerialName("CopyAllMarks")
data class CopyAllMarks(val from: Position, val to: Position) : Action {
override fun <R> accept(visitor: TaintActionVisitor<R>): R = visitor.visit(this)
}

@Serializable
@SerialName("AssignMark")
data class AssignMark(val position: Position, val mark: TaintMark) : Action {
override fun <R> accept(visitor: TaintActionVisitor<R>): R = visitor.visit(this)
}

@Serializable
@SerialName("RemoveAllMarks")
data class RemoveAllMarks(val position: Position) : Action {
override fun <R> accept(visitor: TaintActionVisitor<R>): R = visitor.visit(this)
}

@Serializable
@SerialName("RemoveMark")
data class RemoveMark(val position: Position, val mark: TaintMark) : Action {
override fun <R> accept(visitor: TaintActionVisitor<R>): R = visitor.visit(this)
}

@Serializable
@SerialName("CopyMark")
data class CopyMark(val from: Position, val to: Position, val mark: TaintMark) : Action {
override fun <R> accept(visitor: TaintActionVisitor<R>): R = visitor.visit(this)
}
Loading

0 comments on commit 1772af5

Please sign in to comment.