From 1772af582c2ad114b503cc13e8686b7eb6d8c3c8 Mon Sep 17 00:00:00 2001 From: Alexey Menshutin Date: Tue, 7 Nov 2023 11:19:13 +0300 Subject: [PATCH] Taint configuration feature support (#195) * 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 --- .github/workflows/release.yml | 1 + build.gradle.kts | 1 + .../kotlin/org/jacodb/api/ext/JcClasses.kt | 2 +- jacodb-taint-configuration/build.gradle.kts | 21 + .../jacodb/configuration/ConfigurationTrie.kt | 165 ++++++ .../org/jacodb/configuration/Position.kt | 43 ++ .../SerializedTaintConfigurationItem.kt | 76 +++ .../org/jacodb/configuration/TaintAction.kt | 63 +++ .../jacodb/configuration/TaintCondition.kt | 219 ++++++++ .../TaintConfigurationFeature.kt | 449 ++++++++++++++++ .../configuration/TaintConfigurationItem.kt | 52 ++ .../org/jacodb/configuration/TaintMark.kt | 24 + .../kotlin/org/jacodb/configuration/Util.kt | 341 +++++++++++++ .../jacodb/configuration/ConfigurationTest.kt | 122 +++++ .../org/jacodb/configuration/UtilTest.kt | 105 ++++ .../src/test/resources/testJsonConfig.json | 483 ++++++++++++++++++ settings.gradle.kts | 1 + 17 files changed, 2167 insertions(+), 1 deletion(-) create mode 100644 jacodb-taint-configuration/build.gradle.kts create mode 100644 jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/ConfigurationTrie.kt create mode 100644 jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/Position.kt create mode 100644 jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/SerializedTaintConfigurationItem.kt create mode 100644 jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintAction.kt create mode 100644 jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintCondition.kt create mode 100644 jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintConfigurationFeature.kt create mode 100644 jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintConfigurationItem.kt create mode 100644 jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintMark.kt create mode 100644 jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/Util.kt create mode 100644 jacodb-taint-configuration/src/test/kotlin/org/jacodb/configuration/ConfigurationTest.kt create mode 100644 jacodb-taint-configuration/src/test/kotlin/org/jacodb/configuration/UtilTest.kt create mode 100644 jacodb-taint-configuration/src/test/resources/testJsonConfig.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2752ee1d..638a6396b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts index 82d297107..1b1169c68 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -160,6 +160,7 @@ if (!repoUrl.isNullOrEmpty()) { project(":jacodb-core"), project(":jacodb-analysis"), project(":jacodb-approximations"), + project(":jacodb-taint-configuration"), ) ) { tasks { diff --git a/jacodb-api/src/main/kotlin/org/jacodb/api/ext/JcClasses.kt b/jacodb-api/src/main/kotlin/org/jacodb/api/ext/JcClasses.kt index a3188c31b..6bc4929ec 100644 --- a/jacodb-api/src/main/kotlin/org/jacodb/api/ext/JcClasses.kt +++ b/jacodb-api/src/main/kotlin/org/jacodb/api/ext/JcClasses.kt @@ -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" diff --git a/jacodb-taint-configuration/build.gradle.kts b/jacodb-taint-configuration/build.gradle.kts new file mode 100644 index 000000000..9acc300bf --- /dev/null +++ b/jacodb-taint-configuration/build.gradle.kts @@ -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() +} \ No newline at end of file diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/ConfigurationTrie.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/ConfigurationTrie.kt new file mode 100644 index 000000000..2ec4c9362 --- /dev/null +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/ConfigurationTrie.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import org.jacodb.api.JcClassOrInterface +import org.jacodb.api.ext.packageName + +class ConfigurationTrie( + configuration: List, + private val nameMatcher: (NameMatcher, String) -> Boolean +) { + private val unprocessedRules: MutableList = 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() + 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 { + initializeIfRequired() + + val results = mutableListOf() + + 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 + abstract val rules: MutableList + abstract val unmatchedRules: MutableList + } + + private class RootNode : Node() { + override val children: MutableMap = mutableMapOf() + override val value: String + get() = error("Must not be called for the root") + override val rules: MutableList = mutableListOf() + override val unmatchedRules: MutableList = mutableListOf() + } + + private data class NodeImpl( + override val value: String, + ) : Node() { + override val children: MutableMap = mutableMapOf() + override val rules: MutableList = mutableListOf() + override val unmatchedRules: MutableList = mutableListOf() + } + + private data class Leaf( + override val value: String + ) : Node() { + override val children: MutableMap + get() = error("Leaf nodes do not have children") + override val unmatchedRules: MutableList + get() = mutableListOf() + + override val rules: MutableList = mutableListOf() + } +} diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/Position.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/Position.kt new file mode 100644 index 000000000..14f353c80 --- /dev/null +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/Position.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +interface PositionResolver { + 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 diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/SerializedTaintConfigurationItem.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/SerializedTaintConfigurationItem.kt new file mode 100644 index 000000000..1636e6e05 --- /dev/null +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/SerializedTaintConfigurationItem.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 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, +) : SerializedTaintConfigurationItem + +@Serializable +@SerialName("MethodSource") +data class SerializedTaintMethodSource( + override val methodInfo: FunctionMatcher, + val condition: Condition, + val actionsAfter: List, +) : SerializedTaintConfigurationItem + +@Serializable +@SerialName("MethodSink") +data class SerializedTaintMethodSink( + val ruleNote: String, + val cwe: List, + override val methodInfo: FunctionMatcher, + val condition: Condition +) : SerializedTaintConfigurationItem + +@Serializable +@SerialName("PassThrough") +data class SerializedTaintPassThrough( + override val methodInfo: FunctionMatcher, + val condition: Condition, + val actionsAfter: List, +) : SerializedTaintConfigurationItem + +@Serializable +@SerialName("Cleaner") +data class SerializedTaintCleaner( + override val methodInfo: FunctionMatcher, + val condition: Condition, + val actionsAfter: List, +) : SerializedTaintConfigurationItem diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintAction.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintAction.kt new file mode 100644 index 000000000..d245fe137 --- /dev/null +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintAction.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +interface TaintActionVisitor { + 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 accept(visitor: TaintActionVisitor): 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 accept(visitor: TaintActionVisitor): R = visitor.visit(this) +} + +@Serializable +@SerialName("AssignMark") +data class AssignMark(val position: Position, val mark: TaintMark) : Action { + override fun accept(visitor: TaintActionVisitor): R = visitor.visit(this) +} + +@Serializable +@SerialName("RemoveAllMarks") +data class RemoveAllMarks(val position: Position) : Action { + override fun accept(visitor: TaintActionVisitor): R = visitor.visit(this) +} + +@Serializable +@SerialName("RemoveMark") +data class RemoveMark(val position: Position, val mark: TaintMark) : Action { + override fun accept(visitor: TaintActionVisitor): R = visitor.visit(this) +} + +@Serializable +@SerialName("CopyMark") +data class CopyMark(val from: Position, val to: Position, val mark: TaintMark) : Action { + override fun accept(visitor: TaintActionVisitor): R = visitor.visit(this) +} diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintCondition.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintCondition.kt new file mode 100644 index 000000000..81aa35fb1 --- /dev/null +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintCondition.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.jacodb.api.JcType + +interface ConditionVisitor { + fun visit(condition: And): R + fun visit(condition: Or): R + fun visit(condition: Not): R + fun visit(condition: IsConstant): R + fun visit(condition: IsType): R + fun visit(condition: AnnotationType): R + fun visit(condition: ConstantEq): R + fun visit(condition: ConstantLt): R + fun visit(condition: ConstantGt): R + fun visit(condition: ConstantMatches): R + fun visit(condition: SourceFunctionMatches): R + fun visit(condition: CallParameterContainsMark): R + fun visit(condition: ConstantTrue): R + + // extra conditions + fun visit(condition: TypeMatches): R +} + +interface Condition { + fun accept(conditionVisitor: ConditionVisitor): R +} + +@Serializable +@SerialName("And") +data class And(@SerialName("args") val args: List) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("Or") +data class Or(@SerialName("args") val args: List) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("Not") +data class Not(val condition: Condition) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("IsConstant") +data class IsConstant(val position: Position) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("IsType") +data class IsType( + @SerialName("position") val position: Position, + @SerialName("type") val typeMatcher: TypeMatcher +) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("AnnotationType") +data class AnnotationType( + @SerialName("position") val position: Position, + @SerialName("type") val typeMatcher: TypeMatcher +) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("ConstantEq") +data class ConstantEq( + @SerialName("position") val position: Position, + @SerialName("constant") val value: ConstantValue +) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("ConstantLt") +data class ConstantLt( + @SerialName("position") val position: Position, + @SerialName("constant") val value: ConstantValue +) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("ConstantGt") +data class ConstantGt( + @SerialName("position") val position: Position, + @SerialName("constant") val value: ConstantValue +) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("ConstantMatches") +data class ConstantMatches( + @SerialName("position") val position: Position, + @SerialName("pattern") val pattern: String +) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("SourceFunctionMatches") +data class SourceFunctionMatches( + @SerialName("position") val position: Position, + @SerialName("sourceFunction") val functionMatcher: FunctionMatcher +) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +// sink label +@Serializable +@SerialName("ContainsMark") +data class CallParameterContainsMark( + @SerialName("position") val position: Position, + @SerialName("mark") val mark: TaintMark +) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("ConstantTrue") +object ConstantTrue : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +// Extra conditions +@Serializable +@SerialName("TypeMatches") +data class TypeMatches(val position: Position, @SerialName("type") val type: JcType) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +sealed interface ConstantValue + +@Serializable +@SerialName("IntValue") +data class ConstantIntValue(val value: Int) : ConstantValue + +@Serializable +@SerialName("BoolValue") +data class ConstantBooleanValue(val value: Boolean) : ConstantValue + +@Serializable +@SerialName("StringValue") +data class ConstantStringValue(val value: String) : ConstantValue + +@Serializable +sealed interface NameMatcher + +@Serializable +@SerialName("NameMatches") +data class NamePatternMatcher(val pattern: String) : NameMatcher + +@Serializable +@SerialName("NameIsEqualTo") +data class NameExactMatcher(val name: String) : NameMatcher + +@Serializable +@SerialName("AnyNameMatches") +object AnyNameMatcher : NameMatcher + +@Serializable +sealed interface TypeMatcher + +@Serializable +@SerialName("ClassMatcher") +data class ClassMatcher( + @SerialName("packageMatcher") val pkg: NameMatcher, + @SerialName("classNameMatcher") val classNameMatcher: NameMatcher +) : TypeMatcher + +@Serializable +@SerialName("PrimitiveNameMatches") +data class PrimitiveNameMatcher(val name: String) : TypeMatcher + +@Serializable +@SerialName("AnyTypeMatches") +object AnyTypeMatcher : TypeMatcher + +@Serializable +@SerialName("FunctionMatches") +data class FunctionMatcher( + val cls: ClassMatcher, + val functionName: NameMatcher, + val parametersMatchers: List, + val returnTypeMatcher: TypeMatcher, + val applyToOverrides: Boolean, + val functionLabel: String?, + val modifier: Int, + val exclude: List +) + +@Serializable +@SerialName("ParameterMatches") +data class ParameterMatcher(val index: Int, val typeMatcher: TypeMatcher) diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintConfigurationFeature.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintConfigurationFeature.kt new file mode 100644 index 000000000..7f5d9317b --- /dev/null +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintConfigurationFeature.kt @@ -0,0 +1,449 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import org.jacodb.api.* +import org.jacodb.api.ext.* +import org.jacodb.impl.features.hierarchyExt +import java.nio.file.Path +import kotlin.io.path.readText + + +class TaintConfigurationFeature private constructor( + jsonConfig: String, + additionalSerializersModule: SerializersModule? +) : JcClasspathFeature { + private val rulesByClass: MutableMap> = hashMapOf() + private val rulesForMethod: MutableMap> = hashMapOf() + private val compiledRegex: MutableMap = hashMapOf() + + private val configurationTrie: ConfigurationTrie by lazy { + val serializers = additionalSerializersModule?.let { + SerializersModule { + include(defaultSerializationModule) + include(it) + } + } ?: defaultSerializationModule + + val json = Json { + classDiscriminator = CLASS_DISCRIMINATOR + serializersModule = serializers + prettyPrint = true + } + + val configuration = json.decodeFromString>(jsonConfig).map { + when (it) { + is SerializedTaintCleaner -> it.copy(condition = it.condition.accept(ConditionSimplifier)) + is SerializedTaintEntryPointSource -> it.copy(condition = it.condition.accept(ConditionSimplifier)) + is SerializedTaintMethodSink -> it.copy(condition = it.condition.accept(ConditionSimplifier)) + is SerializedTaintMethodSource -> it.copy(condition = it.condition.accept(ConditionSimplifier)) + is SerializedTaintPassThrough -> it.copy(condition = it.condition.accept(ConditionSimplifier)) + } + } + + ConfigurationTrie(configuration, ::matches) + } + + fun getConfigForMethod(method: JcMethod): List = + resolveConfigForMethod(method) + + private var primitiveTypesSet: Set? = null + + private fun primitiveTypes(method: JcMethod): Set { + if (primitiveTypesSet == null) { + val cp = method.enclosingClass.classpath + primitiveTypesSet = setOf( + cp.boolean, + cp.byte, + cp.short, + cp.int, + cp.long, + cp.char, + cp.float, + cp.double, + ) + } + return primitiveTypesSet!! + } + + + private fun resolveConfigForMethod(method: JcMethod): List { + val taintConfigurationItems = rulesForMethod[method] + if (taintConfigurationItems != null) { + return taintConfigurationItems + } + + val classRules = getClassRules(method.enclosingClass) + + val destination = mutableListOf() + + classRules.mapNotNullTo(destination) { + + val functionMatcher = it.methodInfo + + if (!functionMatcher.matches(method)) return@mapNotNullTo null + + it.resolveForMethod(method) + } + + method + .enclosingClass + .allSuperHierarchySequence + .distinct() + .map { getClassRules(it) } + .forEach { rules -> + rules.mapNotNullTo(destination) { + val methodInfo = it.methodInfo + if (!methodInfo.applyToOverrides || !methodInfo.matches(method)) return@mapNotNullTo null + + it.resolveForMethod(method) + } + } + + rulesForMethod[method] = destination.distinct() + + return rulesForMethod.getValue(method) + } + + private fun getClassRules(clazz: JcClassOrInterface) = rulesByClass.getOrPut(clazz) { + configurationTrie.getRulesForClass(clazz) + } + + private fun FunctionMatcher.matches(method: JcMethod): Boolean { + val functionNameMatcher = functionName + val functionName = if (method.isConstructor) "init^" else method.name + val functionNameMatches = matches(functionNameMatcher, functionName) + + if (!functionNameMatches) return false + + val parameterMatches = parametersMatchers.all { + val parameter = method.parameters.getOrNull(it.index) ?: return@all false + it.typeMatcher.matches(parameter.type) + } + + if (!parameterMatches) return false + + val returnTypeMatches = returnTypeMatcher.matches(method.returnType) + + if (!returnTypeMatches) return false + + // TODO add function's label processing + + require(modifier == -1) { + "Unexpected modifier matcher value $modifier" + } + + val isExcluded = exclude.any { it.matches(method) } + + return !isExcluded + } + + private fun ClassMatcher.matches(fqn: String) = matches( + fqn.substringBeforeLast(DOT_DELIMITER, missingDelimiterValue = ""), + fqn.substringAfterLast(DOT_DELIMITER, missingDelimiterValue = fqn) + ) + + private fun ClassMatcher.matches(pkgName: String, className: String): Boolean { + val packageMatches = matches(pkg, pkgName) + + if (!packageMatches) return false + + return matches(classNameMatcher, className) + } + + private fun matches(nameMatcher: NameMatcher, nameToBeMatched: String): Boolean = when (nameMatcher) { + AnyNameMatcher -> true + is NameExactMatcher -> nameToBeMatched == nameMatcher.name + is NamePatternMatcher -> { + compiledRegex.getOrPut(nameMatcher.pattern) { + nameMatcher.pattern.toRegex() + }.matches(nameToBeMatched) + } + } + + + private fun TypeMatcher.matches(typeName: TypeName): Boolean = matches(typeName.typeName) + + private fun TypeMatcher.matches(typeName: String): Boolean = + when (this) { + AnyTypeMatcher -> true + is ClassMatcher -> matches(typeName) + is PrimitiveNameMatcher -> name == typeName + } + + private fun SerializedTaintConfigurationItem.resolveForMethod(method: JcMethod): TaintConfigurationItem = + when (this) { + is SerializedTaintCleaner -> TaintCleaner(method, condition.resolve(method), actionsAfter.resolve(method)) + is SerializedTaintEntryPointSource -> TaintEntryPointSource( + method, + condition.resolve(method), + actionsAfter.resolve(method) + ) + + is SerializedTaintMethodSink -> TaintMethodSink(condition.resolve(method), method) + is SerializedTaintMethodSource -> TaintMethodSource( + method, + condition.resolve(method), + actionsAfter.resolve(method) + ) + + is SerializedTaintPassThrough -> TaintPassThrough( + method, + condition.resolve(method), + actionsAfter.resolve(method) + ) + } + + private fun Condition.resolve(method: JcMethod): Condition = accept(ConditionSpecializer(method)) + private fun List.resolve(method: JcMethod): List = flatMap { it.accept(ActionSpecializer(method)) } + + + private fun specializePosition(method: JcMethod, position: Position): List { + if (!inBounds(method, position)) return emptyList() + if (position !is AnyArgument) return listOf(position) + return method.parameters.indices.map { Argument(it) }.filter { inBounds(method, it) } + } + + private fun mkOr(conditions: List) = if (conditions.size == 1) conditions.single() else Or(conditions) + private fun mkAnd(conditions: List) = if (conditions.size == 1) conditions.single() else And(conditions) + + private fun inBounds(method: JcMethod, position: Position): Boolean = + when (position) { + AnyArgument -> method.parameters.isNotEmpty() + is Argument -> position.number in method.parameters.indices + Result -> method.returnType.typeName != PredefinedPrimitives.Void + ThisArgument -> !method.isStatic + } + + private inner class ActionSpecializer(val method: JcMethod) : TaintActionVisitor> { + override fun visit(action: CopyAllMarks): List { + val from = specializePosition(method, action.from) + val to = specializePosition(method, action.to) + + return from.flatMap { fst -> + to.mapNotNull { snd -> + if (fst == snd) return@mapNotNull null + + CopyAllMarks(fst, snd) + } + } + } + + override fun visit(action: CopyMark): List { + val from = specializePosition(method, action.from) + val to = specializePosition(method, action.to) + + return from.flatMap { fst -> + to.mapNotNull { snd -> + if (fst == snd) return@mapNotNull null + + action.copy(from = fst, to = snd) + } + } + } + + override fun visit(action: AssignMark): List = + specializePosition(method, action.position).map { action.copy(position = it) } + + override fun visit(action: RemoveAllMarks): List = + specializePosition(method, action.position).map { action.copy(position = it) } + + override fun visit(action: RemoveMark): List = + specializePosition(method, action.position).map { action.copy(position = it) } + } + + private inner class ConditionSpecializer(val method: JcMethod) : ConditionVisitor { + override fun visit(condition: And): Condition = mkAnd(condition.args.map { it.accept(this) }) + + override fun visit(condition: Or): Condition = mkOr(condition.args.map { it.accept(this) }) + + override fun visit(condition: Not): Condition = Not(condition.condition.accept(this)) + + override fun visit(condition: IsConstant): Condition = + mkOr(specializePosition(method, condition.position).map { condition.copy(position = it) }) + + override fun visit(condition: IsType): Condition { + val position = specializePosition(method, condition.position) + + val typeMatcher = condition.typeMatcher + if (typeMatcher is AnyTypeMatcher) { + return mkOr(position.map { ConstantTrue }) + } + + if (typeMatcher is PrimitiveNameMatcher) { + val types = primitiveTypes(method).filter { typeMatcher.matches(it.typeName) } + return mkOr(types.flatMap { type -> position.map { TypeMatches(it, type) } }) + } + + typeMatcher as ClassMatcher + + val pkgMatcher = typeMatcher.pkg + val clsMatcher = typeMatcher.classNameMatcher + val cp = method.enclosingClass.classpath + + if (pkgMatcher is NameExactMatcher && clsMatcher is NameExactMatcher) { + val type = cp.findTypeOrNull("${pkgMatcher.name}$DOT_DELIMITER${clsMatcher.name}") + ?: return mkOr(emptyList()) + return mkOr(position.map { TypeMatches(it, type) }) + } + + val alternatives = typeMatcher.extractAlternatives() + val disjuncts = mutableListOf() + + alternatives.forEach { classMatcher -> + val allClasses = runBlocking { + cp.hierarchyExt().findSubClasses(cp.objectClass, allHierarchy = true, includeOwn = true) + } + + val types = allClasses.filter { + matches(classMatcher.pkg, it.packageName) && matches(classMatcher.classNameMatcher, it.simpleName) + } + + disjuncts += types.flatMap { type -> position.map { TypeMatches(it, type.toType()) } }.toList() + } + + return mkOr(disjuncts) + } + + override fun visit(condition: AnnotationType): Condition = ConstantTrue // TODO("Not yet implemented") + + override fun visit(condition: ConstantEq): Condition = + mkOr(specializePosition(method, condition.position).map { condition.copy(position = it) }) + + override fun visit(condition: ConstantLt): Condition = + mkOr(specializePosition(method, condition.position).map { condition.copy(position = it) }) + + override fun visit(condition: ConstantGt): Condition = + mkOr(specializePosition(method, condition.position).map { condition.copy(position = it) }) + + override fun visit(condition: ConstantMatches): Condition = + mkOr(specializePosition(method, condition.position).map { condition.copy(position = it) }) + + override fun visit(condition: SourceFunctionMatches): Condition = ConstantTrue // TODO Not implemented yet + + override fun visit(condition: CallParameterContainsMark): Condition = + mkOr(specializePosition(method, condition.position).map { condition.copy(position = it) }) + + override fun visit(condition: ConstantTrue): Condition = condition + + override fun visit(condition: TypeMatches): Condition = error("Must not occur here") + } + + companion object { + fun fromPath( + configPath: Path, + serializersModule: SerializersModule? = null + ) = TaintConfigurationFeature(configPath.readText(), serializersModule) + + fun fromJson( + jsonConfig: String, + serializersModule: SerializersModule? = null + ) = TaintConfigurationFeature(jsonConfig, serializersModule) + + val defaultSerializationModule: SerializersModule + get() = SerializersModule { + polymorphic(Condition::class) { + subclass(And::class) + subclass(Or::class) + subclass(Not::class) + subclass(IsConstant::class) + subclass(IsType::class) + subclass(AnnotationType::class) + subclass(ConstantEq::class) + subclass(ConstantLt::class) + subclass(ConstantGt::class) + subclass(ConstantMatches::class) + subclass(SourceFunctionMatches::class) + subclass(CallParameterContainsMark::class) + subclass(ConstantTrue::class) + subclass(TypeMatches::class) + } + + polymorphic(Action::class) { + subclass(CopyAllMarks::class) + subclass(AssignMark::class) + subclass(RemoveAllMarks::class) + subclass(RemoveMark::class) + subclass(CopyMark::class) + } + } + + private const val CLASS_DISCRIMINATOR = "_" + } +} + +private object ConditionSimplifier : ConditionVisitor { + override fun visit(condition: And): Condition { + val unprocessed = condition.args.toMutableList() + val conjuncts = mutableListOf() + while (unprocessed.isNotEmpty()) { + val it = unprocessed.removeLast() + if (it is And) { + unprocessed.addAll(it.args) + continue + } + conjuncts += it.accept(this) + } + + return conjuncts.singleOrNull() ?: And(conjuncts.asReversed()) + } + + override fun visit(condition: Or): Condition { + val unprocessed = condition.args.toMutableList() + val conjuncts = mutableListOf() + while (unprocessed.isNotEmpty()) { + val it = unprocessed.removeLast() + if (it is Or) { + unprocessed.addAll(it.args) + continue + } + conjuncts += it.accept(this) + } + + return conjuncts.singleOrNull() ?: Or(conjuncts.asReversed()) + } + + override fun visit(condition: Not): Condition = Not(condition.condition.accept(this)) + + override fun visit(condition: IsConstant): Condition = condition + + override fun visit(condition: IsType): Condition = condition + + override fun visit(condition: AnnotationType): Condition = condition + + override fun visit(condition: ConstantEq): Condition = condition + + override fun visit(condition: ConstantLt): Condition = condition + + override fun visit(condition: ConstantGt): Condition = condition + + override fun visit(condition: ConstantMatches): Condition = condition + + override fun visit(condition: SourceFunctionMatches): Condition = condition + + override fun visit(condition: CallParameterContainsMark): Condition = condition + + override fun visit(condition: ConstantTrue): Condition = condition + + override fun visit(condition: TypeMatches): Condition = condition +} diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintConfigurationItem.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintConfigurationItem.kt new file mode 100644 index 000000000..fedfe2fd4 --- /dev/null +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintConfigurationItem.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import org.jacodb.api.JcField +import org.jacodb.api.JcMethod + +sealed interface TaintConfigurationItem + +data class TaintEntryPointSource( + val methodInfo: JcMethod, + val condition: Condition, + val actionsAfter: List, +) : TaintConfigurationItem + +data class TaintMethodSource( + val methodInfo: JcMethod, + val condition: Condition, + val actionsAfter: List, +) : TaintConfigurationItem + +data class TaintMethodSink( + val condition: Condition, + val methodInfo: JcMethod, +) : TaintConfigurationItem + +data class TaintPassThrough( + val methodInfo: JcMethod, + val condition: Condition, + val actionsAfter: List, +) : TaintConfigurationItem + +data class TaintCleaner( + val methodInfo: JcMethod, + val condition: Condition, + val actionsAfter: List, +) : TaintConfigurationItem + diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintMark.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintMark.kt new file mode 100644 index 000000000..954fa5b0b --- /dev/null +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/TaintMark.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("TaintMark") +data class TaintMark(val name: String) \ No newline at end of file diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/Util.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/Util.kt new file mode 100644 index 000000000..a8e3a2dd9 --- /dev/null +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/configuration/Util.kt @@ -0,0 +1,341 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import org.jacodb.api.JcClasspath + +fun JcClasspath.taintConfigurationFeature(): TaintConfigurationFeature = features + ?.singleOrNull { it is TaintConfigurationFeature } as? TaintConfigurationFeature + ?: error("No taint configuration feature found") + +fun ClassMatcher.extractAlternatives(): Set { + val unprocessedMatchers = mutableListOf(this) + val results = mutableSetOf() + + while (unprocessedMatchers.isNotEmpty()) { + var unprocessedMatcher = unprocessedMatchers.removeLast() + + var matchedPackageParts: List = emptyList() + var unmatchedPackagePart: String? = null + + var isAnyPackageAllowed = false + + when (val pkg = unprocessedMatcher.pkg) { + AnyNameMatcher -> isAnyPackageAllowed = true + is NameExactMatcher -> matchedPackageParts = pkg.name.split(DOT_DELIMITER) + is NamePatternMatcher -> { + val alternatives = unprocessedMatcher.extractAlternativesToIfRequired( + { prevClassMatcher, newMatcher -> prevClassMatcher.copy(pkg = newMatcher) }, + pkg, + ) + + if (alternatives.size != 1) { + unprocessedMatchers += alternatives + continue + } + + unprocessedMatcher = alternatives.single() + + val simplifiedName = unprocessedMatcher.pkg as NamePatternMatcher + val (matchedParts, unmatchedPart) = simplifiedName.splitRegex() + + matchedPackageParts = matchedParts + unmatchedPackagePart = unmatchedPart + } + } + + if (unmatchedPackagePart == null) { + val nameExactMatcher = NameExactMatcher(matchedPackageParts.joinToString(DOT_DELIMITER)) + unprocessedMatcher = unprocessedMatcher.copy(pkg = nameExactMatcher) + } + + when (val classNameMatcher = unprocessedMatcher.classNameMatcher) { + AnyNameMatcher -> results += unprocessedMatcher + is NameExactMatcher -> results += unprocessedMatcher + is NamePatternMatcher -> { + val alternatives = unprocessedMatcher.extractAlternativesToIfRequired( + { prevClassMatcher, newMatcher -> prevClassMatcher.copy(classNameMatcher = newMatcher) }, + classNameMatcher, + ) + + if (alternatives.size != 1) { + unprocessedMatchers += alternatives + continue + } + + val classPattern = alternatives.single().classNameMatcher as NamePatternMatcher + + if (classPattern.pattern == ALL_MATCH) { + results += unprocessedMatcher + continue + } + + val classPatternName = classPattern.pattern + .replace("\\.", "\$") + .replace("\\\$", "\$") + + val containsOnlyLettersOrDigits = classPatternName.all { + it.isLetterOrDigit() || it == '_' || it == '$' + } + + results += if (!containsOnlyLettersOrDigits || isAnyPackageAllowed) { + unprocessedMatcher + } else { + val classExactName = classPatternName.trimEnd('$') + + val cls = unprocessedMatcher.copy(classNameMatcher = NameExactMatcher(classExactName)) + cls + } + } + } + } + + return results +} + +internal fun NamePatternMatcher.splitRegex(): SplitRegex { + val possibleParts = pattern.split("\\.") + val matchedParts = mutableListOf() + var unmatchedPart: String? = null + + for ((i, part) in possibleParts.withIndex()) { + if (part.all { char -> char.isLetterOrDigit() }) { + matchedParts += part + continue + } + + unmatchedPart = possibleParts + .subList(i, possibleParts.size) + .joinToString("\\.") + + break + } + + return SplitRegex(matchedParts, unmatchedPart) +} + +internal data class SplitRegex(val matchedParts: List, val unmatchedPart: String?) + +private inline fun ClassMatcher.extractAlternativesToIfRequired( + classMatcherModifier: (ClassMatcher, NameMatcher) -> ClassMatcher, + namePatternMatcher: NamePatternMatcher +): List { + val alternatives = namePatternMatcher.pattern.extractAlternatives() + + if (alternatives.singleOrNull() == namePatternMatcher.pattern) return listOf(this) + + val alternativeMatchers = alternatives.map { NamePatternMatcher(it) } + + return alternativeMatchers.map { classMatcherModifier(this, it) } +} + + +fun String.extractAlternatives(): Set { + val queue = mutableListOf(this) + val result = hashSetOf() + + while (queue.isNotEmpty()) { + val last = queue.removeLast() + + if (ALTERNATIVE_MARK !in last) { + result += last + continue + } + + queue += last.splitTheMostNestedAlternatives() + } + + return result + .flatMapTo(hashSetOf()) { it.splitOnQuestionMark() } + .mapTo(hashSetOf()) { it.removeRedundantParentheses() } +} + +fun String.splitOnQuestionMark(): Set { + if (QUESTION_MARK !in this) return setOf(this) + + val queue = mutableListOf(this) + val result = hashSetOf() + + questionMarkSplitter@ while (queue.isNotEmpty()) { + val nextElement = queue.removeLast() + + var questionMarkIndex = nextElement.indexOf(QUESTION_MARK) + while (questionMarkIndex > 0) { + val prevSymbol = nextElement[questionMarkIndex - 1] + when { + prevSymbol.isLetterOrDigit() -> { + val prefix = nextElement.substring(0, questionMarkIndex - 1) + val suffix = nextElement.substring(questionMarkIndex + 1) + queue += "$prefix$suffix" + queue += "$prefix$prevSymbol$suffix" + continue@questionMarkSplitter + } + + prevSymbol == ')' -> { + var balance = 1 + var index = questionMarkIndex - 1 + + while (balance > 0 && index > 0) { + index-- + when (nextElement[index]) { + ')' -> balance++ + '(' -> balance-- + else -> continue + } + } + + if (balance != 0) { + questionMarkIndex = nextElement.indexOf(QUESTION_MARK, questionMarkIndex + 1) + continue + } + + val prefix = nextElement.substring(0, index) + val suffix = nextElement.substring(questionMarkIndex + 1) + + val optionalPart = nextElement.substring(index, questionMarkIndex) + + queue += "$prefix$suffix" + queue += "$prefix$optionalPart$suffix" + continue@questionMarkSplitter + } + + else -> questionMarkIndex = nextElement.indexOf(QUESTION_MARK, questionMarkIndex + 1) + } + } + + result += nextElement + } + + return result +} + + +private fun String.removeRedundantParentheses(): String { + val openParentheses = mutableListOf>() + val redundantValues = hashSetOf() + + for ((i, char) in withIndex()) { + when (char) { + '(' -> openParentheses += i to false + + ')' -> { + val nextSymbolIsImportant = if (i == lastIndex) { + false + } else { + get(i + 1) in SYMBOLS_FORBIDDING_TO_REMOVE_PARENTHESES + } + + val (index, contaminated) = openParentheses.removeLast() + + if (!contaminated && !nextSymbolIsImportant) { + redundantValues += index + redundantValues += i + } + } + + else -> { + val (index, contaminated) = openParentheses.lastOrNull() ?: continue + if (contaminated) continue + + if (char.isLetterOrDigit() || char == '.' || char == '\\' || char == '$' || char == '_') continue + + openParentheses.removeLast() + openParentheses += index to true + } + } + } + + return buildString { + for ((i, char) in this@removeRedundantParentheses.withIndex()) { + if (i in redundantValues) continue + + append(char) + } + } +} + +private fun String.splitTheMostNestedAlternatives(): List { + val openIndices = mutableListOf() + val matchingIndicesByLevel: MutableMap>> = mutableMapOf() + val currentBracketsContainsAlternativeMark = mutableListOf(false) + + var levels = 0 + + for ((i, char) in this.withIndex()) { + when (char) { + '(' -> { + openIndices += i + currentBracketsContainsAlternativeMark += false + } + + ')' -> { + val matchingIndex = openIndices.removeLast() + + if (currentBracketsContainsAlternativeMark.removeLast()) { + val level = openIndices.size + 1 + + levels = maxOf(level, levels) + + matchingIndicesByLevel + .getOrPut(level) { mutableListOf() } + .add(matchingIndex to i) + } + } + + ALTERNATIVE_MARK -> { + if (!currentBracketsContainsAlternativeMark.last()) { + currentBracketsContainsAlternativeMark.removeLast() + currentBracketsContainsAlternativeMark += true + } + } + + else -> continue + } + } + + if (levels == 0) { + return split(ALTERNATIVE_MARK) + } + + val brackets = matchingIndicesByLevel.getValue(levels) + + val firstBracketEntry = brackets.first().first + val prefix = substring(0, firstBracketEntry) + + var prefixesValues = mutableListOf(prefix) + + for ((i, indices) in brackets.withIndex()) { + val (start, end) = indices + val alternatives = substring(start + 1, end).split(ALTERNATIVE_MARK) + + val nextIndex = brackets.getOrNull(i + 1)?.first ?: length + val suffix = substring(end + 1, nextIndex) + + val prefixesCopy = prefixesValues + prefixesValues = mutableListOf() + + alternatives.flatMapTo(prefixesValues) { a -> prefixesCopy.map { "$it($a)$suffix" } } + } + + return prefixesValues +} + +internal const val DOT_DELIMITER = "." +internal const val ALL_MATCH = ".*" +private const val QUESTION_MARK = '?' +private const val ALTERNATIVE_MARK = '|' +private const val SYMBOLS_FORBIDDING_TO_REMOVE_PARENTHESES = "*?{+" \ No newline at end of file diff --git a/jacodb-taint-configuration/src/test/kotlin/org/jacodb/configuration/ConfigurationTest.kt b/jacodb-taint-configuration/src/test/kotlin/org/jacodb/configuration/ConfigurationTest.kt new file mode 100644 index 000000000..27b6c9996 --- /dev/null +++ b/jacodb-taint-configuration/src/test/kotlin/org/jacodb/configuration/ConfigurationTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import kotlinx.coroutines.runBlocking +import org.jacodb.api.JcClasspath +import org.jacodb.api.ext.constructors +import org.jacodb.api.ext.findClass +import org.jacodb.api.ext.methods +import org.jacodb.api.ext.objectType +import org.jacodb.impl.features.classpaths.UnknownClasses +import org.jacodb.impl.features.classpaths.VirtualLocation +import org.jacodb.impl.features.classpaths.virtual.JcVirtualClassImpl +import org.jacodb.impl.features.classpaths.virtual.JcVirtualMethodImpl +import org.jacodb.impl.features.classpaths.virtual.JcVirtualParameter +import org.jacodb.impl.types.TypeNameImpl +import org.jacodb.testing.BaseTest +import org.jacodb.testing.WithDB +import org.jacodb.testing.allClasspath +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class ConfigurationTest : BaseTest() { + companion object : WithDB() + + override val cp: JcClasspath = runBlocking { + val configPath = "/testJsonConfig.json" + val testConfig = this::class.java.getResourceAsStream(configPath) + ?: error("No such resource found: $configPath") + val configJson = testConfig.bufferedReader().readText() + val configurationFeature = TaintConfigurationFeature.fromJson(configJson) + val features = listOf(configurationFeature, UnknownClasses) + db.classpath(allClasspath, features) + } + + private val taintFeature = cp.taintConfigurationFeature() + + @Test + fun testVirtualMethod() { + val virtualParameter = JcVirtualParameter(0, TypeNameImpl(cp.objectType.typeName)) + + val method = JcVirtualMethodImpl( + name = "setValue", + returnType = TypeNameImpl(cp.objectType.typeName), + parameters = listOf(virtualParameter), + description = "" + ) + + val clazz = JcVirtualClassImpl( + name = "com.service.model.SimpleRequest", + initialFields = emptyList(), + initialMethods = listOf(method) + ) + clazz.bind(cp, VirtualLocation()) + + method.bind(clazz) + + val configs = taintFeature.getConfigForMethod(method) + val rule = configs.single() as TaintPassThrough + + assertEquals(ConstantTrue, rule.condition) + assertEquals(2, rule.actionsAfter.size) + } + + @Test + fun testSinkMethod() { + val method = cp.findClass().methods.first { it.name == "store" } + val rules = taintFeature.getConfigForMethod(method) + + assertTrue(rules.singleOrNull() != null) + } + + @Test + fun testSourceMethod() { + val method = cp.findClass().methods.first { it.name == "getProperty" } + val rules = taintFeature.getConfigForMethod(method) + + assertTrue(rules.singleOrNull() != null) + } + + @Test + fun testCleanerMethod() { + val method = cp.findClass>().methods.first() { it.name == "clear" } + val rules = taintFeature.getConfigForMethod(method) + + assertTrue(rules.singleOrNull() != null) + } + + @Test + fun testParametersMatches() { + val method = cp.findClass().constructors.first { + it.parameters.singleOrNull()?.type?.typeName == "java.lang.String" + } + val rules = taintFeature.getConfigForMethod(method) + + assertTrue(rules.singleOrNull() != null) + } + + @Test + fun testPrimitiveParametersInMatcher() { + val method = cp.findClass().methods.first { + it.name.startsWith("write") && it.parameters.firstOrNull()?.type?.typeName == "int" + } + val rules = taintFeature.getConfigForMethod(method) + + assertTrue(rules.singleOrNull() != null) + } +} diff --git a/jacodb-taint-configuration/src/test/kotlin/org/jacodb/configuration/UtilTest.kt b/jacodb-taint-configuration/src/test/kotlin/org/jacodb/configuration/UtilTest.kt new file mode 100644 index 000000000..9018bed59 --- /dev/null +++ b/jacodb-taint-configuration/src/test/kotlin/org/jacodb/configuration/UtilTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.configuration + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class UtilTest { + @Test + fun testExtractAlternatives() { + val pkgMatcher = NamePatternMatcher("(org\\.jacodb)|.*\\.org\\.company") + val classNameMatcher = NamePatternMatcher(".*Re(quest|sponse)|ClassName|.*|Class(A)*") + + val classMatcher = ClassMatcher(pkgMatcher, classNameMatcher) + val alternatives = classMatcher.extractAlternatives() + + require(alternatives.size == 10) + + val extractPackageMatcher = alternatives.count { + it.pkg is NameExactMatcher && (it.pkg as NameExactMatcher).name == "org.jacodb" + } + require(extractPackageMatcher == 5) + + val patternPackageMatcher = alternatives.count { + it.pkg is NamePatternMatcher && (it.pkg as NamePatternMatcher).pattern == ".*\\.org\\.company" + } + require(patternPackageMatcher == 5) + + val requestClassMatcher = alternatives.count { + it.classNameMatcher is NamePatternMatcher && (it.classNameMatcher as NamePatternMatcher).pattern == ".*Request" + } + require(requestClassMatcher == 2) + + val responseClassMatcher = alternatives.count { + it.classNameMatcher is NamePatternMatcher && (it.classNameMatcher as NamePatternMatcher).pattern == ".*Response" + } + require(responseClassMatcher == 2) + + val classNameMatcherValue = alternatives.count { + it.classNameMatcher is NameExactMatcher && (it.classNameMatcher as NameExactMatcher).name == "ClassName" + } + require(classNameMatcherValue == 2) + + val allClassNamesMatcher = alternatives.count { + it.classNameMatcher is NamePatternMatcher && (it.classNameMatcher as NamePatternMatcher).pattern == ".*" + } + require(allClassNamesMatcher == 2) + + val classAPattern = alternatives.count { + it.classNameMatcher is NamePatternMatcher && (it.classNameMatcher as NamePatternMatcher).pattern == "Class(A)*" + } + require(classAPattern == 2) + } + + @Test + fun splitOnQuestionMarkWithoutTheMark() { + val emptyLine = "" + val simpleLine = "abc" + + assertSame(emptyLine, emptyLine.splitOnQuestionMark().single()) + assertSame(simpleLine, simpleLine.splitOnQuestionMark().single()) + } + + @Test + fun splitOnQuestionMarkTest() { + val line = "zxc?(asd(qwe)?(bnm))?" + + val result = line.splitOnQuestionMark() + + assertEquals(6, result.size) + + assertTrue(result.singleOrNull { it == "zxc(asd(qwe)(bnm))" } != null) + assertTrue(result.singleOrNull { it == "zxc(asd(bnm))" } != null) + assertTrue(result.singleOrNull { it == "zxc" } != null) + assertTrue(result.singleOrNull { it == "zx" } != null) + assertTrue(result.singleOrNull { it == "zx(asd(qwe)(bnm))" } != null) + assertTrue(result.singleOrNull { it == "zx(asd(bnm))" } != null) + } + + @Test + fun incorrectQuestionMarkSequences() { + val line = "abc)?" + assertSame(line, line.splitOnQuestionMark().single()) + } + + @Test + fun questionMarkOnGroup() { + val line = "[a-z]?" + assertSame(line, line.splitOnQuestionMark().single()) + } +} \ No newline at end of file diff --git a/jacodb-taint-configuration/src/test/resources/testJsonConfig.json b/jacodb-taint-configuration/src/test/resources/testJsonConfig.json new file mode 100644 index 000000000..5f5485ca2 --- /dev/null +++ b/jacodb-taint-configuration/src/test/resources/testJsonConfig.json @@ -0,0 +1,483 @@ +[ + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "com.service.model" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*Request" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "set.*|init\\^" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "AnyArgument" + }, + "to": { + "_": "This" + } + }, + { + "_": "CopyMark", + "from": { + "_": "This" + }, + "to": { + "_": "Result" + }, + "mark": { + "name": "VALIDATED_CROSS_SITE_SCRIPTING_REFLECTED" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*Base64.*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": ".*encode.*" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyMark", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + }, + "mark": { + "name": "BASE64_ENCODED" + } + } + ] + }, + { + "_": "MethodSink", + "ruleNote": "System Information Leak", + "cwe": [ + 497 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.util" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Properties" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "store|save" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "And", + "args": [ + { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "SYSTEMINFO" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "Argument", + "number": 1 + }, + "mark": { + "name": "SYSTEMINFO" + } + } + ] + }, + { + "_": "Not", + "condition": { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "A" + } + } + ] + } + } + ] + } + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "System" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "getProperty" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "And", + "args": [ + { + "_": "IsType", + "position": { + "_": "Argument", + "number": 0 + }, + "type": { + "_": "ClassMatcher", + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "String(Builder)?|StringBuffer" + } + } + }, + { + "_": "Not", + "condition": { + "_": "ConstantMatches", + "position": { + "_": "Argument", + "number": 0 + }, + "pattern": "(line.separator|file.separator|user.dir|user.home|os.name)$" + } + }, + { + "_": "Not", + "condition": { + "_": "SourceFunctionMatches", + "position": { + "_": "Argument", + "number": 0 + }, + "sourceFunction": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "System" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "(lineSeparator)$" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + } + } + } + ] + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "PROPERTY" + } + }, + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "SYSTEMINFO" + } + } + ] + }, + { + "_": "Cleaner", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": "java\\..*|org\\..*\\.collections" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*List|.*Set" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "clear" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "RemoveAllMarks", + "position": { + "_": "This" + } + }, + { + "_": "RemoveMark", + "position": { + "_": "This" + }, + "mark": { + "name": "Mark" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "StringBuilder" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "init^" + }, + "parametersMatchers": [ + { + "index": 0, + "typeMatcher": { + "_": "ClassMatcher", + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "String" + } + } + } + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "This" + } + } + ] + }, + { + "_": "MethodSink", + "ruleNote": "System Information Leak", + "cwe": [ + 497 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.io" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "Writer|OutputStream" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "write.*" + }, + "parametersMatchers": [ + { + "index": 0, + "typeMatcher": { + "_": "PrimitiveNameMatches", + "name": "int" + } + } + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "And", + "args": [ + { + "_": "IsType", + "position": { + "_": "Argument", + "number": 0 + }, + "type": { + "_": "PrimitiveNameMatches", + "name": "int" + } + }, + { + "_": "IsConstant", + "position": { + "_": "Argument", + "number": 0 + } + }, + { + "_": "ConstantEq", + "position": { + "_": "Argument", + "number": 0 + }, + "constant": { + "_": "IntValue", + "value": 0 + } + } + ] + } + } +] \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0d7d94b90..d8aec8ff1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,3 +26,4 @@ include("jacodb-examples") include("jacodb-benchmarks") include("jacodb-cli") include("jacodb-approximations") +include("jacodb-taint-configuration")