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")