diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index ae8000639..2c4ebcbbb 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -29,7 +29,8 @@ object Versions { const val kotlinx_metadata = "0.5.0" const val kotlinx_serialization = "1.4.1" const val licenser = "0.6.1" - const val mockito = "4.8.0" + const val mockk = "1.13.3" + const val sarif4k = "0.5.0" const val shadow = "8.1.1" const val slf4j = "1.7.36" const val soot_utbot_fork = "4.4.0-FORK-2" @@ -134,6 +135,11 @@ object Libs { ) // https://github.com/Kotlin/kotlinx.serialization + val kotlinx_serialization_core = dep( + group = "org.jetbrains.kotlinx", + name = "kotlinx-serialization-core", + version = Versions.kotlinx_serialization + ) val kotlinx_serialization_json = dep( group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", @@ -199,11 +205,11 @@ object Libs { version = Versions.jdot ) - // https://github.com/mockito/mockito - val mockito_core = dep( - group = "org.mockito", - name = "mockito-core", - version = Versions.mockito + // https://github.com/mockk/mockk + val mockk = dep( + group = "io.mockk", + name = "mockk", + version = Versions.mockk ) // https://github.com/JetBrains/java-annotations @@ -275,6 +281,13 @@ object Libs { name = "cwe${cweNum}", version = Versions.juliet ) + + // https://github.com/detekt/sarif4k + val sarif4k = dep( + group = "io.github.detekt.sarif4k", + name = "sarif4k", + version = Versions.sarif4k + ) } object Plugins { @@ -320,4 +333,4 @@ object Plugins { fun PluginDependenciesSpec.id(plugin: Plugins.ProjectPlugin) { id(plugin.id).version(plugin.version) -} \ No newline at end of file +} diff --git a/jacodb-analysis/build.gradle.kts b/jacodb-analysis/build.gradle.kts index e4e2daf47..b469f17db 100644 --- a/jacodb-analysis/build.gradle.kts +++ b/jacodb-analysis/build.gradle.kts @@ -6,14 +6,20 @@ plugins { dependencies { api(project(":jacodb-core")) api(project(":jacodb-api")) + api(project(":jacodb-taint-configuration")) implementation(Libs.kotlin_logging) implementation(Libs.slf4j_simple) implementation(Libs.kotlinx_coroutines_core) implementation(Libs.kotlinx_serialization_json) + api(Libs.sarif4k) testImplementation(testFixtures(project(":jacodb-core"))) testImplementation(project(":jacodb-api")) + testImplementation(kotlin("test")) + testImplementation(Libs.mockk) + + // Additional deps for analysis: testImplementation(files("src/test/resources/pointerbench.jar")) testImplementation(Libs.joda_time) testImplementation(Libs.juliet_support) diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/AnalysisMain.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/AnalysisMain.kt deleted file mode 100644 index 1016d5c4b..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/AnalysisMain.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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. - */ - -@file:JvmName("AnalysisMain") -package org.jacodb.analysis - -import kotlinx.serialization.Serializable -import mu.KLogging -import org.jacodb.analysis.engine.IfdsUnitRunnerFactory -import org.jacodb.analysis.engine.MainIfdsUnitManager -import org.jacodb.analysis.engine.SummaryStorage -import org.jacodb.analysis.engine.UnitResolver -import org.jacodb.analysis.engine.VulnerabilityInstance -import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph - -internal val logger = object : KLogging() {}.logger - -typealias AnalysesOptions = Map - -@Serializable -data class AnalysisConfig(val analyses: Map) - - -/** - * This is the entry point for every analysis. - * Calling this function will find all vulnerabilities reachable from [methods]. - * - * @param graph instance of [JcApplicationGraph] that provides mixture of CFG and call graph - * (called supergraph in RHS95). - * Usually built by [newApplicationGraphForAnalysis]. - * - * @param unitResolver instance of [UnitResolver] which splits all methods into groups of methods, called units. - * Units are analyzed concurrently, one unit will be analyzed with one call to [IfdsUnitRunnerFactory.newRunner] method. - * In general, larger units mean more precise, but also more resource-consuming analysis, so [unitResolver] allows - * to reach compromise. - * It is guaranteed that [SummaryStorage] passed to all units is the same, so they can share information through it. - * However, the order of launching and terminating analysis for units is an implementation detail and may vary even for - * consecutive calls of this method with same arguments. - * - * @param ifdsUnitRunnerFactory an [IfdsUnitRunnerFactory] instance that will be launched for each unit. - * This is the main argument that defines the analysis. - * - * @param methods the list of method for analysis. - * Each vulnerability will only be reported if it is reachable from one of these. - * - * @param timeoutMillis the maximum time for analysis. - * Note that this does not include time for precalculations - * (like searching for reachable methods and splitting them into units) and postcalculations (like restoring traces), so - * the actual running time of this method may be longer. - */ -fun runAnalysis( - graph: JcApplicationGraph, - unitResolver: UnitResolver<*>, - ifdsUnitRunnerFactory: IfdsUnitRunnerFactory, - methods: List, - timeoutMillis: Long = Long.MAX_VALUE -): List { - return MainIfdsUnitManager(graph, unitResolver, ifdsUnitRunnerFactory, methods, timeoutMillis).analyze() -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Condition.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Condition.kt new file mode 100644 index 000000000..92a765463 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Condition.kt @@ -0,0 +1,185 @@ +/* + * 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.analysis.config + +import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.analysis.ifds.ElementAccessor +import org.jacodb.analysis.ifds.Maybe +import org.jacodb.analysis.ifds.onSome +import org.jacodb.analysis.ifds.toPath +import org.jacodb.analysis.taint.Tainted +import org.jacodb.api.cfg.JcBool +import org.jacodb.api.cfg.JcConstant +import org.jacodb.api.cfg.JcInt +import org.jacodb.api.cfg.JcStringConstant +import org.jacodb.api.cfg.JcValue +import org.jacodb.api.ext.isAssignable +import org.jacodb.taint.configuration.And +import org.jacodb.taint.configuration.AnnotationType +import org.jacodb.taint.configuration.ConditionVisitor +import org.jacodb.taint.configuration.ConstantBooleanValue +import org.jacodb.taint.configuration.ConstantEq +import org.jacodb.taint.configuration.ConstantGt +import org.jacodb.taint.configuration.ConstantIntValue +import org.jacodb.taint.configuration.ConstantLt +import org.jacodb.taint.configuration.ConstantMatches +import org.jacodb.taint.configuration.ConstantStringValue +import org.jacodb.taint.configuration.ConstantTrue +import org.jacodb.taint.configuration.ContainsMark +import org.jacodb.taint.configuration.IsConstant +import org.jacodb.taint.configuration.IsType +import org.jacodb.taint.configuration.Not +import org.jacodb.taint.configuration.Or +import org.jacodb.taint.configuration.PositionResolver +import org.jacodb.taint.configuration.SourceFunctionMatches +import org.jacodb.taint.configuration.TypeMatches + +open class BasicConditionEvaluator( + internal val positionResolver: PositionResolver>, +) : ConditionVisitor { + + override fun visit(condition: ConstantTrue): Boolean { + return true + } + + override fun visit(condition: Not): Boolean { + return !condition.arg.accept(this) + } + + override fun visit(condition: And): Boolean { + return condition.args.all { it.accept(this) } + } + + override fun visit(condition: Or): Boolean { + return condition.args.any { it.accept(this) } + } + + override fun visit(condition: IsConstant): Boolean { + positionResolver.resolve(condition.position).onSome { return it is JcConstant } + return false + } + + override fun visit(condition: IsType): Boolean { + // Note: TaintConfigurationFeature.ConditionSpecializer is responsible for + // expanding IsType condition upon parsing the taint configuration. + error("Unexpected condition: $condition") + } + + override fun visit(condition: AnnotationType): Boolean { + // Note: TaintConfigurationFeature.ConditionSpecializer is responsible for + // expanding AnnotationType condition upon parsing the taint configuration. + error("Unexpected condition: $condition") + } + + override fun visit(condition: ConstantEq): Boolean { + positionResolver.resolve(condition.position).onSome { value -> + return when (val constant = condition.value) { + is ConstantBooleanValue -> { + value is JcBool && value.value == constant.value + } + + is ConstantIntValue -> { + value is JcInt && value.value == constant.value + } + + is ConstantStringValue -> { + // TODO: if 'value' is not string, convert it to string and compare with 'constant.value' + value is JcStringConstant && value.value == constant.value + } + } + } + return false + } + + override fun visit(condition: ConstantLt): Boolean { + positionResolver.resolve(condition.position).onSome { value -> + return when (val constant = condition.value) { + is ConstantIntValue -> { + value is JcInt && value.value < constant.value + } + + else -> error("Unexpected constant: $constant") + } + } + return false + } + + override fun visit(condition: ConstantGt): Boolean { + positionResolver.resolve(condition.position).onSome { value -> + return when (val constant = condition.value) { + is ConstantIntValue -> { + value is JcInt && value.value > constant.value + } + + else -> error("Unexpected constant: $constant") + } + } + return false + } + + override fun visit(condition: ConstantMatches): Boolean { + positionResolver.resolve(condition.position).onSome { value -> + val re = condition.pattern.toRegex() + return re.matches(value.toString()) + } + return false + } + + override fun visit(condition: SourceFunctionMatches): Boolean { + TODO("Not implemented yet") + } + + override fun visit(condition: ContainsMark): Boolean { + error("This visitor does not support condition $condition. Use FactAwareConditionEvaluator instead") + } + + override fun visit(condition: TypeMatches): Boolean { + positionResolver.resolve(condition.position).onSome { value -> + return value.type.isAssignable(condition.type) + } + return false + } +} + +class FactAwareConditionEvaluator( + private val fact: Tainted, + positionResolver: PositionResolver>, +) : BasicConditionEvaluator(positionResolver) { + + override fun visit(condition: ContainsMark): Boolean { + if (fact.mark != condition.mark) return false + positionResolver.resolve(condition.position).onSome { value -> + val variable = value.toPath() + + // FIXME: Adhoc for arrays + val variableWithoutStars = variable.removeTrailingElementAccessors() + val factWithoutStars = fact.variable.removeTrailingElementAccessors() + if (variableWithoutStars == factWithoutStars) return true + + return variable == fact.variable + } + return false + } + + private fun AccessPath.removeTrailingElementAccessors(): AccessPath { + val accesses = accesses.toMutableList() + while (accesses.lastOrNull() is ElementAccessor) { + accesses.removeLast() + } + return AccessPath(value, accesses) + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Position.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Position.kt new file mode 100644 index 000000000..a6e238a36 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Position.kt @@ -0,0 +1,103 @@ +/* + * 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.analysis.config + +import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.analysis.ifds.ElementAccessor +import org.jacodb.analysis.ifds.Maybe +import org.jacodb.analysis.ifds.fmap +import org.jacodb.analysis.ifds.toMaybe +import org.jacodb.analysis.ifds.toPathOrNull +import org.jacodb.analysis.util.getArgument +import org.jacodb.analysis.util.thisInstance +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcMethod +import org.jacodb.api.cfg.JcAssignInst +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.cfg.JcInstanceCallExpr +import org.jacodb.api.cfg.JcValue +import org.jacodb.api.ext.cfg.callExpr +import org.jacodb.taint.configuration.AnyArgument +import org.jacodb.taint.configuration.Argument +import org.jacodb.taint.configuration.Position +import org.jacodb.taint.configuration.PositionResolver +import org.jacodb.taint.configuration.Result +import org.jacodb.taint.configuration.ResultAnyElement +import org.jacodb.taint.configuration.This + +class CallPositionToAccessPathResolver( + private val callStatement: JcInst, +) : PositionResolver> { + private val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + + override fun resolve(position: Position): Maybe = when (position) { + AnyArgument -> Maybe.none() + is Argument -> callExpr.args[position.index].toPathOrNull().toMaybe() + This -> (callExpr as? JcInstanceCallExpr)?.instance?.toPathOrNull().toMaybe() + Result -> (callStatement as? JcAssignInst)?.lhv?.toPathOrNull().toMaybe() + ResultAnyElement -> (callStatement as? JcAssignInst)?.lhv?.toPathOrNull().toMaybe() + .fmap { it / ElementAccessor } + } +} + +class CallPositionToJcValueResolver( + private val callStatement: JcInst, +) : PositionResolver> { + private val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + + override fun resolve(position: Position): Maybe = when (position) { + AnyArgument -> Maybe.none() + is Argument -> Maybe.some(callExpr.args[position.index]) + This -> (callExpr as? JcInstanceCallExpr)?.instance.toMaybe() + Result -> (callStatement as? JcAssignInst)?.lhv.toMaybe() + ResultAnyElement -> Maybe.none() + } +} + +class EntryPointPositionToJcValueResolver( + val cp: JcClasspath, + val method: JcMethod, +) : PositionResolver> { + override fun resolve(position: Position): Maybe = when (position) { + This -> Maybe.some(method.thisInstance) + + is Argument -> { + val p = method.parameters[position.index] + cp.getArgument(p).toMaybe() + } + + AnyArgument, Result, ResultAnyElement -> error("Unexpected $position") + } +} + +class EntryPointPositionToAccessPathResolver( + val cp: JcClasspath, + val method: JcMethod, +) : PositionResolver> { + override fun resolve(position: Position): Maybe = when (position) { + This -> method.thisInstance.toPathOrNull().toMaybe() + + is Argument -> { + val p = method.parameters[position.index] + cp.getArgument(p)?.toPathOrNull().toMaybe() + } + + AnyArgument, Result, ResultAnyElement -> error("Unexpected $position") + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/TaintAction.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/TaintAction.kt new file mode 100644 index 000000000..59403b099 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/TaintAction.kt @@ -0,0 +1,70 @@ +/* + * 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.analysis.config + +import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.analysis.ifds.Maybe +import org.jacodb.analysis.ifds.fmap +import org.jacodb.analysis.ifds.map +import org.jacodb.analysis.taint.Tainted +import org.jacodb.taint.configuration.AssignMark +import org.jacodb.taint.configuration.CopyAllMarks +import org.jacodb.taint.configuration.CopyMark +import org.jacodb.taint.configuration.PositionResolver +import org.jacodb.taint.configuration.RemoveAllMarks +import org.jacodb.taint.configuration.RemoveMark + +class TaintActionEvaluator( + private val positionResolver: PositionResolver>, +) { + fun evaluate(action: CopyAllMarks, fact: Tainted): Maybe> = + positionResolver.resolve(action.from).map { from -> + if (from != fact.variable) return@map Maybe.none() + positionResolver.resolve(action.to).fmap { to -> + setOf(fact, fact.copy(variable = to)) + } + } + + fun evaluate(action: CopyMark, fact: Tainted): Maybe> { + if (fact.mark != action.mark) return Maybe.none() + return positionResolver.resolve(action.from).map { from -> + if (from != fact.variable) return@map Maybe.none() + positionResolver.resolve(action.to).fmap { to -> + setOf(fact, fact.copy(variable = to)) + } + } + } + + fun evaluate(action: AssignMark): Maybe> = + positionResolver.resolve(action.position).fmap { variable -> + setOf(Tainted(variable, action.mark)) + } + + fun evaluate(action: RemoveAllMarks, fact: Tainted): Maybe> = + positionResolver.resolve(action.position).map { variable -> + if (variable != fact.variable) return@map Maybe.none() + Maybe.some(emptySet()) + } + + fun evaluate(action: RemoveMark, fact: Tainted): Maybe> { + if (fact.mark != action.mark) return Maybe.none() + return positionResolver.resolve(action.position).map { variable -> + if (variable != fact.variable) return@map Maybe.none() + Maybe.some(emptySet()) + } + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AbstractAnalyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AbstractAnalyzer.kt deleted file mode 100644 index bf5bbbc4c..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AbstractAnalyzer.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.analysis.engine - -import org.jacodb.api.analysis.JcApplicationGraph -import java.util.concurrent.ConcurrentHashMap - -/** - * Handlers of [AbstractAnalyzer] produce some common events like new [SummaryEdgeFact]s, new [CrossUnitCallFact]s, etc. - * Inheritors may override all these handlers, and also they can extend them by calling the super-method and adding - * their own event - * - * @property verticesWithTraceGraphNeeded For all vertices added to this set, - * a [TraceGraphFact] will be produced at [handleIfdsResult] - * - * @property isMainAnalyzer Iff this property is set to true, handlers will - * 1. Produce [NewSummaryFact] events with [SummaryEdgeFact]s and [CrossUnitCallFact]s - * 2. Will produce [EdgeForOtherRunnerQuery] for each cross-unit call - * - * Usually this should be set to true for forward analyzers (which are expected to tell anything they found), - * but in backward analyzers this should be set to false - */ -abstract class AbstractAnalyzer(private val graph: JcApplicationGraph) : Analyzer { - protected val verticesWithTraceGraphNeeded: MutableSet = ConcurrentHashMap.newKeySet() - - abstract val isMainAnalyzer: Boolean - - /** - * If the edge is start-to-end and [isMainAnalyzer] is true, - * produces a [NewSummaryFact] with this summary edge. - * Otherwise, returns empty list. - */ - override fun handleNewEdge(edge: IfdsEdge): List { - return if (isMainAnalyzer && edge.v.statement in graph.exitPoints(edge.method)) { - listOf(NewSummaryFact(SummaryEdgeFact(edge))) - } else { - emptyList() - } - } - - /** - * If [isMainAnalyzer] is set to true, produces a [NewSummaryFact] with given [fact] - * and also produces [EdgeForOtherRunnerQuery] - */ - override fun handleNewCrossUnitCall(fact: CrossUnitCallFact): List { - return if (isMainAnalyzer) { - verticesWithTraceGraphNeeded.add(fact.callerVertex) - listOf(NewSummaryFact(fact), EdgeForOtherRunnerQuery(IfdsEdge(fact.calleeVertex, fact.calleeVertex))) - } else { - emptyList() - } - } - - /** - * Produces trace graphs for all vertices added to [verticesWithTraceGraphNeeded] - */ - override fun handleIfdsResult(ifdsResult: IfdsResult): List { - val traceGraphs = verticesWithTraceGraphNeeded.map { - ifdsResult.resolveTraceGraph(it) - } - - return traceGraphs.map { - NewSummaryFact(TraceGraphFact(it)) - } - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AnalyzerFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AnalyzerFactory.kt deleted file mode 100644 index 97faab9fe..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AnalyzerFactory.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.analysis.engine - -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcInst - -/** - * Interface for flow functions -- mappings of kind DomainFact -> Collection of DomainFacts - */ -fun interface FlowFunctionInstance { - fun compute(fact: DomainFact): Collection -} - -/** - * An interface with which facts appearing in analyses should be marked - */ -interface DomainFact - -/** - * A special [DomainFact] that always holds - */ -object ZEROFact : DomainFact { - override fun toString() = "[ZERO fact]" -} - -/** - * Implementations of the interface should provide all four kinds of flow functions mentioned in RHS95, - * thus fully describing how the facts are propagated through the supergraph. - */ -interface FlowFunctionsSpace { - /** - * @return facts that may hold when analysis is started from [startStatement] - * (these are needed to initiate worklist in ifds analysis) - */ - fun obtainPossibleStartFacts(startStatement: JcInst): Collection - fun obtainSequentFlowFunction(current: JcInst, next: JcInst): FlowFunctionInstance - fun obtainCallToStartFlowFunction(callStatement: JcInst, callee: JcMethod): FlowFunctionInstance - fun obtainCallToReturnFlowFunction(callStatement: JcInst, returnSite: JcInst): FlowFunctionInstance - fun obtainExitToReturnSiteFlowFunction(callStatement: JcInst, returnSite: JcInst, exitStatement: JcInst): FlowFunctionInstance -} - -/** - * [Analyzer] interface describes how facts are propagated and how [AnalysisDependentEvent]s are produced by these facts during - * the run of tabulation algorithm by [BaseIfdsUnitRunner]. - * - * Note that methods and properties of this interface may be accessed concurrently from different threads, - * so the implementations should be thread-safe. - * - * @property flowFunctions a [FlowFunctionsSpace] instance that describes how facts are generated and propagated - * during run of tabulation algorithm. - */ -interface Analyzer { - val flowFunctions: FlowFunctionsSpace - - /** - * This method is called by [BaseIfdsUnitRunner] each time a new path edge is found. - * - * @return [AnalysisDependentEvent]s that are produced by this edge. - * Usually these are [NewSummaryFact] events with [SummaryEdgeFact] or [VulnerabilityLocation] facts - */ - fun handleNewEdge(edge: IfdsEdge): List - - /** - * This method is called by [BaseIfdsUnitRunner] each time a new cross-unit called is observed. - * - * @return [AnalysisDependentEvent]s that are produced by this [fact]. - */ - fun handleNewCrossUnitCall(fact: CrossUnitCallFact): List - - /** - * This method is called once by [BaseIfdsUnitRunner] when the propagation of facts is finished - * (normally or due to cancellation). - * - * @return [AnalysisDependentEvent]s that should be processed after the facts propagation was completed - * (usually these are some [NewSummaryFact]s). - */ - fun handleIfdsResult(ifdsResult: IfdsResult): List -} - -/** - * A functional interface that allows to produce [Analyzer] by [JcApplicationGraph]. - * - * It simplifies instantiation of [IfdsUnitRunnerFactory]s because this way you don't have to pass graph and reversed - * graph to analyzers' constructors directly, relying on runner to do it by itself. - */ -fun interface AnalyzerFactory { - fun newAnalyzer(graph: JcApplicationGraph): Analyzer -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BaseIfdsUnitRunnerFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BaseIfdsUnitRunnerFactory.kt deleted file mode 100644 index d4d0f5d51..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BaseIfdsUnitRunnerFactory.kt +++ /dev/null @@ -1,262 +0,0 @@ -/* - * 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.analysis.engine - -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.isActive -import kotlinx.coroutines.withContext -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.ApplicationGraph -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcInst -import java.util.concurrent.ConcurrentHashMap - -/** - * This is a basic [IfdsUnitRunnerFactory], which creates one [BaseIfdsUnitRunner] for each [newRunner] call. - * - * @property analyzerFactory used to build [Analyzer] instance, which then will be used by launched [BaseIfdsUnitRunner]. - */ -class BaseIfdsUnitRunnerFactory(private val analyzerFactory: AnalyzerFactory) : IfdsUnitRunnerFactory { - override fun newRunner( - graph: JcApplicationGraph, - manager: IfdsUnitManager, - unitResolver: UnitResolver, - unit: UnitType, - startMethods: List - ): IfdsUnitRunner { - val analyzer = analyzerFactory.newAnalyzer(graph) - return BaseIfdsUnitRunner(graph, analyzer, manager, unitResolver, unit, startMethods) - } -} - -/** - * Encapsulates launch of tabulation algorithm, described in RHS95, for one unit - */ -private class BaseIfdsUnitRunner( - private val graph: ApplicationGraph, - private val analyzer: Analyzer, - private val manager: IfdsUnitManager, - private val unitResolver: UnitResolver, - unit: UnitType, - private val startMethods: List -) : AbstractIfdsUnitRunner(unit) { - - private val pathEdges: MutableSet = ConcurrentHashMap.newKeySet() - private val summaryEdges: MutableMap> = mutableMapOf() - private val callSitesOf: MutableMap> = mutableMapOf() - private val pathEdgesPreds: MutableMap> = ConcurrentHashMap() - - private val flowSpace = analyzer.flowFunctions - - /** - * Queue containing all unprocessed path edges. - */ - private val workList = Channel(Channel.UNLIMITED) - - /** - * This method should be called each time new path edge is observed. - * It will check if the edge is new and, if success, add it to [workList] - * and summarize all [SummaryFact]s produces by the edge. - * - * @param edge the new path edge - * @param pred the description of predecessor of the edge - */ - private suspend fun propagate(edge: IfdsEdge, pred: PathEdgePredecessor): Boolean { - require(unitResolver.resolve(edge.method) == unit) - - pathEdgesPreds.computeIfAbsent(edge) { - ConcurrentHashMap.newKeySet() - }.add(pred) - - if (pathEdges.add(edge)) { - workList.send(edge) - analyzer.handleNewEdge(edge).forEach { - manager.handleEvent(it, this) - } - return true - } - return false - } - - private val JcMethod.isExtern: Boolean - get() = unitResolver.resolve(this) != unit - - /** - * Implementation of tabulation algorithm, based on RHS95. It slightly differs from the original in the following: - * - * - We do not analyze the whole supergraph (represented by [graph]), but only the methods that belong to our [unit]; - * - Path edges are added to [workList] not only by the main cycle, but they can also be obtained from [manager]; - * - By summary edge we understand the path edge from the start node of the method to its exit node; - * - The supergraph is explored dynamically, and we do not inverse flow functions when new summary edge is found, i.e. - * the extension from Chapter 4 of NLR10 is implemented. - */ - private suspend fun runTabulationAlgorithm(): Unit = coroutineScope { - while (isActive) { - val curEdge = workList.tryReceive().getOrNull() ?: run { - manager.handleEvent(QueueEmptinessChanged(true), this@BaseIfdsUnitRunner) - workList.receive().also { - manager.handleEvent(QueueEmptinessChanged(false), this@BaseIfdsUnitRunner) - } - } - - val (u, v) = curEdge - val (curVertex, curFact) = v - - val callees = graph.callees(curVertex).toList() - val curVertexIsCall = callees.isNotEmpty() - val curVertexIsExit = curVertex in graph.exitPoints(graph.methodOf(curVertex)) - - if (curVertexIsCall) { - for (returnSite in graph.successors(curVertex)) { - // Propagating through call-to-return-site edges (in RHS95 it is done in lines 17-19) - for (fact in flowSpace.obtainCallToReturnFlowFunction(curVertex, returnSite).compute(curFact)) { - val newEdge = IfdsEdge(u, IfdsVertex(returnSite, fact)) - propagate(newEdge, PathEdgePredecessor(curEdge, PredecessorKind.Sequent)) - } - - for (callee in callees) { - val factsAtStart = flowSpace.obtainCallToStartFlowFunction(curVertex, callee).compute(curFact) - for (sPoint in graph.entryPoint(callee)) { - for (sFact in factsAtStart) { - val sVertex = IfdsVertex(sPoint, sFact) - - val handleExitVertex: suspend (IfdsVertex) -> Unit = { (eStatement, eFact) -> - val finalFacts = flowSpace - .obtainExitToReturnSiteFlowFunction(curVertex, returnSite, eStatement) - .compute(eFact) - for (finalFact in finalFacts) { - val summaryEdge = IfdsEdge(IfdsVertex(sPoint, sFact), IfdsVertex(eStatement, eFact)) - val newEdge = IfdsEdge(u, IfdsVertex(returnSite, finalFact)) - propagate(newEdge, PathEdgePredecessor(curEdge, PredecessorKind.ThroughSummary(summaryEdge))) - } - } - - if (callee.isExtern) { - // Notify about cross-unit call - analyzer.handleNewCrossUnitCall(CrossUnitCallFact(v, sVertex)).forEach { - manager.handleEvent(it, this@BaseIfdsUnitRunner) - } - - // Waiting for exit vertices and handling them - val exitVertices = flow { - manager.handleEvent( - SubscriptionForSummaryEdges(callee, this@flow), - this@BaseIfdsUnitRunner - ) - } - exitVertices - .filter { it.u == sVertex } - .map { it.v } - .onEach(handleExitVertex) - .launchIn(this) - } else { - // Save info about call for summary-facts that will be found later - callSitesOf.getOrPut(sVertex) { mutableSetOf() }.add(curEdge) - - // Initiating analysis for callee - val nextEdge = IfdsEdge(sVertex, sVertex) - propagate(nextEdge, PathEdgePredecessor(curEdge, PredecessorKind.CallToStart)) - - // Handling already-found summary edges - // .toList() is needed below to avoid ConcurrentModificationException - for (exitVertex in summaryEdges[sVertex].orEmpty().toList()) { - handleExitVertex(exitVertex) - } - } - } - } - } - } - } else { - if (curVertexIsExit) { - // Propagating through newly found summary edge, similar to lines 22-31 of RHS95 - // TODO: rewrite this in a more reactive way - for (callerEdge in callSitesOf[u].orEmpty()) { - val callerStatement = callerEdge.v.statement - for (returnSite in graph.successors(callerStatement)) { - for (returnSiteFact in flowSpace.obtainExitToReturnSiteFlowFunction(callerStatement, returnSite, curVertex).compute(curFact)) { - val returnSiteVertex = IfdsVertex(returnSite, returnSiteFact) - val newEdge = IfdsEdge(callerEdge.u, returnSiteVertex) - propagate(newEdge, PathEdgePredecessor(callerEdge, PredecessorKind.ThroughSummary(curEdge))) - } - } - } - summaryEdges.getOrPut(curEdge.u) { mutableSetOf() }.add(curEdge.v) - } - - // Simple propagation through intraprocedural edge, as in lines 34-36 of RHS95 - // Note that generally speaking, exit vertices may have successors (in case of exceptional flow, etc.), - // so this part should be done for exit vertices as well - for (nextInst in graph.successors(curVertex)) { - val nextFacts = flowSpace.obtainSequentFlowFunction(curVertex, nextInst).compute(curFact) - for (nextFact in nextFacts) { - val newEdge = IfdsEdge(u, IfdsVertex(nextInst, nextFact)) - propagate(newEdge, PathEdgePredecessor(curEdge, PredecessorKind.Sequent)) - } - } - } - } - } - - private val ifdsResult: IfdsResult by lazy { - val allEdges = pathEdges.toList() - - val resultFacts = allEdges.groupBy({ it.v.statement }) { - it.v.domainFact - }.mapValues { (_, facts) -> facts.toSet() } - - IfdsResult(allEdges, resultFacts, pathEdgesPreds) - } - - /** - * Performs some initialization and runs tabulation algorithm, sending all relevant events to [manager]. - */ - override suspend fun run() = coroutineScope { - try { - // Adding initial facts to workList - for (method in startMethods) { - require(unitResolver.resolve(method) == unit) - for (sPoint in graph.entryPoint(method)) { - for (sFact in flowSpace.obtainPossibleStartFacts(sPoint)) { - val vertex = IfdsVertex(sPoint, sFact) - val edge = IfdsEdge(vertex, vertex) - propagate(edge, PathEdgePredecessor(edge, PredecessorKind.NoPredecessor)) - } - } - } - - runTabulationAlgorithm() - } finally { - withContext(NonCancellable) { - analyzer.handleIfdsResult(ifdsResult).forEach { - manager.handleEvent(it, this@BaseIfdsUnitRunner) - } - } - } - } - - override suspend fun submitNewEdge(edge: IfdsEdge) { - propagate(edge, PathEdgePredecessor(edge, PredecessorKind.Unknown)) - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BidiIfdsUnitRunnerFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BidiIfdsUnitRunnerFactory.kt deleted file mode 100644 index d472941fd..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BidiIfdsUnitRunnerFactory.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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.analysis.engine - -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import org.jacodb.analysis.graph.reversed -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph - -/** - * This factory produces composite runners. Each of them launches two runners (backward and forward) - * on the same unit with the same startMethods. - * - * Backward runner is launched on the reversed application graph, - * while forward runner is launched on the direct graph. - * - * Both runners will be given their own managers with the following policy: - * - all [NewSummaryFact] events are delegated to the outer manager - * - [EdgeForOtherRunnerQuery] events are submitted to the other runner if the corresponding edge - * belongs to the same unit, otherwise they are transmitted to the outer manager (for forward runner) - * or ignored (for backward runner) - * - Queue is thought to be empty when queues of both forward and backward runners are empty. - * The [QueueEmptinessChanged] event is sent to outer manager correspondingly. - * - [SubscriptionForSummaryEdges] event is delegated to the outer manager for forward runner and - * is ignored for backward runner - * - * @param forwardRunnerFactory a factory that produces forward runner for each [newRunner] call - * @param backwardRunnerFactory a factory that produces backward runner for each [newRunner] call - * @param isParallel if true, the produced composite runner will launch backward and forward runners in parallel. - * Otherwise, the backward runner will be executed first, and after it, the forward runner will be executed. - */ -class BidiIfdsUnitRunnerFactory( - private val forwardRunnerFactory: IfdsUnitRunnerFactory, - private val backwardRunnerFactory: IfdsUnitRunnerFactory, - private val isParallel: Boolean = true -) : IfdsUnitRunnerFactory { - private inner class BidiIfdsUnitRunner( - graph: JcApplicationGraph, - private val manager: IfdsUnitManager, - private val unitResolver: UnitResolver, - unit: UnitType, - startMethods: List, - ) : AbstractIfdsUnitRunner(unit) { - - @Volatile - private var forwardQueueIsEmpty: Boolean = false - - @Volatile - private var backwardQueueIsEmpty: Boolean = false - - private val forwardManager: IfdsUnitManager = object : IfdsUnitManager by manager { - - override suspend fun handleEvent(event: IfdsUnitRunnerEvent, runner: IfdsUnitRunner) { - when (event) { - is EdgeForOtherRunnerQuery -> { - if (unitResolver.resolve(event.edge.method) == unit) { - backwardRunner.submitNewEdge(event.edge) - } else { - manager.handleEvent(event, this@BidiIfdsUnitRunner) - } - } - is NewSummaryFact -> { - manager.handleEvent(event, this@BidiIfdsUnitRunner) - } - is QueueEmptinessChanged -> { - forwardQueueIsEmpty = event.isEmpty - val newEvent = QueueEmptinessChanged(backwardQueueIsEmpty && forwardQueueIsEmpty) - manager.handleEvent(newEvent, this@BidiIfdsUnitRunner) - } - is SubscriptionForSummaryEdges -> { - manager.handleEvent(event, this@BidiIfdsUnitRunner) - } - } - } - } - - private val backwardManager: IfdsUnitManager = object : IfdsUnitManager { - override suspend fun handleEvent(event: IfdsUnitRunnerEvent, runner: IfdsUnitRunner) { - when (event) { - is EdgeForOtherRunnerQuery -> { - if (unitResolver.resolve(event.edge.method) == unit) { - forwardRunner.submitNewEdge(event.edge) - } - } - is NewSummaryFact -> { - manager.handleEvent(event, this@BidiIfdsUnitRunner) - } - is QueueEmptinessChanged -> { - backwardQueueIsEmpty = event.isEmpty - if (!isParallel && event.isEmpty) { - runner.job?.cancel() ?: error("Runner job is not instantiated") - } - val newEvent = - QueueEmptinessChanged(backwardQueueIsEmpty && forwardQueueIsEmpty) - manager.handleEvent(newEvent, this@BidiIfdsUnitRunner) - } - - is SubscriptionForSummaryEdges -> {} - } - } - } - - private val backwardRunner: IfdsUnitRunner = backwardRunnerFactory - .newRunner(graph.reversed, backwardManager, unitResolver, unit, startMethods) - - private val forwardRunner: IfdsUnitRunner = forwardRunnerFactory - .newRunner(graph, forwardManager, unitResolver, unit, startMethods) - - override suspend fun submitNewEdge(edge: IfdsEdge) { - forwardRunner.submitNewEdge(edge) - } - - override suspend fun run() = coroutineScope { - val backwardRunnerJob: Job = backwardRunner.launchIn(this) - val forwardRunnerJob: Job - - if (isParallel) { - forwardRunnerJob = forwardRunner.launchIn(this) - - backwardRunnerJob.join() - forwardRunnerJob.join() - } else { - backwardRunnerJob.join() - - forwardRunnerJob = forwardRunner.launchIn(this) - forwardRunnerJob.join() - } - } - } - - override fun newRunner( - graph: JcApplicationGraph, - manager: IfdsUnitManager, - unitResolver: UnitResolver, - unit: UnitType, - startMethods: List - ): IfdsUnitRunner = BidiIfdsUnitRunner(graph, manager, unitResolver, unit, startMethods) -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsEdge.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsEdge.kt deleted file mode 100644 index 3e081a9b7..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsEdge.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.analysis.engine - -import org.jacodb.api.JcMethod - -/** - * Represents a directed (from [u] to [v]) edge between two ifds vertices - */ -data class IfdsEdge(val u: IfdsVertex, val v: IfdsVertex) { - init { - require(u.method == v.method) - } - - val method: JcMethod - get() = u.method -} - -sealed interface PredecessorKind { - object NoPredecessor : PredecessorKind - object Unknown : PredecessorKind - object Sequent : PredecessorKind - object CallToStart : PredecessorKind - class ThroughSummary(val summaryEdge: IfdsEdge) : PredecessorKind -} - -/** - * Contains info about predecessor of path edge. - * Used mainly to restore traces. - */ -data class PathEdgePredecessor( - val predEdge: IfdsEdge, - val kind: PredecessorKind -) \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsResult.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsResult.kt deleted file mode 100644 index e03d9f22c..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsResult.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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.analysis.engine -import org.jacodb.api.cfg.JcInst - -/** - * Aggregates all facts and edges found by tabulation algorithm - */ -class IfdsResult( - val pathEdges: List, - val resultFacts: Map>, - val pathEdgesPreds: Map> -) { - private inner class TraceGraphBuilder(private val sink: IfdsVertex) { - private val sources: MutableSet = mutableSetOf() - private val edges: MutableMap> = mutableMapOf() - private val visited: MutableSet = mutableSetOf() - - private fun addEdge(from: IfdsVertex, to: IfdsVertex) { - if (from != to) { - edges.getOrPut(from) { mutableSetOf() }.add(to) - } - } - - private fun dfs(e: IfdsEdge, lastVertex: IfdsVertex, stopAtMethodStart: Boolean) { - if (e in visited) { - return - } - - visited.add(e) - - if (stopAtMethodStart && e.u == e.v) { - addEdge(e.u, lastVertex) - return - } - - val (_, v) = e - if (v.domainFact == ZEROFact) { - addEdge(v, lastVertex) - sources.add(v) - return - } - - for (pred in pathEdgesPreds[e].orEmpty()) { - when (pred.kind) { - is PredecessorKind.CallToStart -> { - if (!stopAtMethodStart) { - addEdge(pred.predEdge.v, lastVertex) - dfs(pred.predEdge, pred.predEdge.v, false) - } - } - is PredecessorKind.Sequent -> { - if (pred.predEdge.v.domainFact == v.domainFact) { - dfs(pred.predEdge, lastVertex, stopAtMethodStart) - } else { - addEdge(pred.predEdge.v, lastVertex) - dfs(pred.predEdge, pred.predEdge.v, stopAtMethodStart) - } - } - is PredecessorKind.ThroughSummary -> { - val summaryEdge = pred.kind.summaryEdge - addEdge(summaryEdge.v, lastVertex) // Return to next vertex - addEdge(pred.predEdge.v, summaryEdge.u) // Call to start - dfs(summaryEdge, summaryEdge.v, true) // Expand summary edge - dfs(pred.predEdge, pred.predEdge.v, stopAtMethodStart) // Continue normal analysis - } - is PredecessorKind.Unknown -> { - addEdge(pred.predEdge.v, lastVertex) - if (pred.predEdge.u != pred.predEdge.v) { - // TODO: ideally, we should analyze the place from which the edge was given to ifds, - // for now we just go to method start - dfs(IfdsEdge(pred.predEdge.u, pred.predEdge.u), pred.predEdge.v, stopAtMethodStart) - } - } - is PredecessorKind.NoPredecessor -> { - sources.add(v) - addEdge(pred.predEdge.v, lastVertex) - } - } - } - } - - fun build(): TraceGraph { - val initEdges = pathEdges.filter { it.v == sink } - initEdges.forEach { - dfs(it, it.v, false) - } - return TraceGraph(sink, sources, edges) - } - } - - /** - * Builds a graph with traces to given [vertex]. - */ - fun resolveTraceGraph(vertex: IfdsVertex): TraceGraph { - return TraceGraphBuilder(vertex).build() - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitManager.kt deleted file mode 100644 index fb09509de..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitManager.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.analysis.engine - -import kotlinx.coroutines.flow.FlowCollector -import org.jacodb.api.JcMethod - -/** - * Implementations of this interface manage one or more runners and should be responsible for: - * - communication between different runners, i.e. they - * should submit received [EdgeForOtherRunnerQuery] to proper runners via [IfdsUnitRunner.submitNewEdge] call - * - providing runners with summaries for other units - * - saving the [NewSummaryFact]s produced by runners - * - managing lifecycles of the launched runners - */ -interface IfdsUnitManager { - suspend fun handleEvent(event: IfdsUnitRunnerEvent, runner: IfdsUnitRunner) -} - - -// TODO: provide visitor for this interface -sealed interface IfdsUnitRunnerEvent - -data class QueueEmptinessChanged(val isEmpty: Boolean) : IfdsUnitRunnerEvent - -/** - * @property method the method for which summary edges the subscription is queried - * @property collector the [FlowCollector] to which queried summary edges should be sent to, - * somewhat similar to a callback - */ -data class SubscriptionForSummaryEdges(val method: JcMethod, val collector: FlowCollector) : IfdsUnitRunnerEvent - -/** - * A common interface for all events that are allowed to be produced by [Analyzer] - * (all others may be produced only in runners directly) - */ -sealed interface AnalysisDependentEvent : IfdsUnitRunnerEvent - -data class NewSummaryFact(val fact: SummaryFact) : AnalysisDependentEvent -data class EdgeForOtherRunnerQuery(val edge: IfdsEdge) : AnalysisDependentEvent \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitRunnerFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitRunnerFactory.kt deleted file mode 100644 index fb00efe7c..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitRunnerFactory.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.analysis.engine - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph - -/** - * Represents a runner and allows to manipulate it. - * - * By convention, runners are created by instances of [IfdsUnitRunnerFactory], - * but this creation doesn't launch anything. - * The work itself is launched by [launchIn] method, which should be called exactly once. - * This method returns a [Job] instance representing a launched coroutine. - * This [Job] could also be further obtained via [job] property. - * - * It is not recommended to implement this interface directly, instead, - * [AbstractIfdsUnitRunner] should be extended. - */ -interface IfdsUnitRunner { - val unit: UnitType - val job: Job? - - fun launchIn(scope: CoroutineScope): Job - - /** - * Submits a new [IfdsEdge] to runner's queue. Should be called only after [launchIn]. - * Note that this method can be called from different threads. - */ - suspend fun submitNewEdge(edge: IfdsEdge) -} - -/** - * [AbstractIfdsUnitRunner] contains proper implementation of [launchIn] method and [job] property. - * Inheritors should only implement [submitNewEdge] and a suspendable [run] method. - * The latter is the main method of runner, that should do all its work. - */ -abstract class AbstractIfdsUnitRunner(final override val unit: UnitType) : IfdsUnitRunner { - /** - * The main method of the runner, which will be called by [launchIn] - */ - protected abstract suspend fun run() - - private var _job: Job? = null - - final override val job: Job? by ::_job - - final override fun launchIn(scope: CoroutineScope): Job = scope.launch(start = CoroutineStart.LAZY) { - run() - }.also { - _job = it - it.start() - } -} - -/** - * Produces a runner for any given unit. - */ -interface IfdsUnitRunnerFactory { - /** - * Produces a runner for given [unit], using given [startMethods] as entry points. - * All start methods should belong to the [unit]. - * Note that this method DOES NOT START runner's job. - * - * @param graph provides supergraph for application (including methods, belonging to other units) - * - * @param manager [IfdsUnitManager] instance that will manage the produced runner. - * - * @param unitResolver will be used to get units of methods observed during analysis. - */ - fun newRunner( - graph: JcApplicationGraph, - manager: IfdsUnitManager, - unitResolver: UnitResolver, - unit: UnitType, - startMethods: List - ) : IfdsUnitRunner -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/MainIfdsUnitManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/MainIfdsUnitManager.kt deleted file mode 100644 index 42128e9e1..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/MainIfdsUnitManager.kt +++ /dev/null @@ -1,267 +0,0 @@ -/* - * 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.analysis.engine - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.delay -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.isActive -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeoutOrNull -import org.jacodb.analysis.logger -import org.jacodb.analysis.runAnalysis -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -import java.util.concurrent.ConcurrentHashMap - -/** - * This manager launches and manages [IfdsUnitRunner]s for all units, reachable from [startMethods]. - * It also merges [TraceGraph]s from different units giving a complete [TraceGraph] for each vulnerability. - * See [runAnalysis] for more info. - */ -class MainIfdsUnitManager( - private val graph: JcApplicationGraph, - private val unitResolver: UnitResolver, - private val ifdsUnitRunnerFactory: IfdsUnitRunnerFactory, - private val startMethods: List, - private val timeoutMillis: Long -) : IfdsUnitManager { - - private val foundMethods: MutableMap> = mutableMapOf() - private val crossUnitCallers: MutableMap> = mutableMapOf() - - private val summaryEdgesStorage = SummaryStorageImpl() - private val tracesStorage = SummaryStorageImpl() - private val crossUnitCallsStorage = SummaryStorageImpl() - private val vulnerabilitiesStorage = SummaryStorageImpl() - - private val aliveRunners: MutableMap> = ConcurrentHashMap() - private val queueEmptiness: MutableMap = mutableMapOf() - private val dependencies: MutableMap> = mutableMapOf() - private val dependenciesRev: MutableMap> = mutableMapOf() - - private fun getAllCallees(method: JcMethod): Set { - val result = mutableSetOf() - for (inst in method.flowGraph().instructions) { - graph.callees(inst).forEach { - result.add(it) - } - } - return result - } - - private fun addStart(method: JcMethod) { - val unit = unitResolver.resolve(method) - if (method in foundMethods[unit].orEmpty()) { - return - } - - foundMethods.getOrPut(unit) { mutableSetOf() }.add(method) - val dependencies = getAllCallees(method) - dependencies.forEach { addStart(it) } - } - - private val IfdsVertex.traceGraph: TraceGraph - get() = tracesStorage - .getCurrentFacts(method) - .map { it.graph } - .singleOrNull { it.sink == this } - ?: TraceGraph.bySink(this) - - /** - * Launches [IfdsUnitRunner] for each observed unit, handles respective jobs, - * and gathers results into list of vulnerabilities, restoring full traces - */ - fun analyze(): List = runBlocking(Dispatchers.Default) { - withTimeoutOrNull(timeoutMillis) { - logger.info { "Searching for units to analyze..." } - startMethods.forEach { - ensureActive() - addStart(it) - } - - val allUnits = foundMethods.keys.toList() - logger.info { "Starting analysis. Number of found units: ${allUnits.size}" } - - val progressLoggerJob = launch { - while (isActive) { - delay(1000) - val totalCount = allUnits.size - val aliveCount = aliveRunners.size - - logger.info { - "Current progress: ${totalCount - aliveCount} / $totalCount units completed" - } - } - } - - launch { - dispatchDependencies() - } - - // TODO: do smth smarter here - val allJobs = allUnits.map { unit -> - val runner = ifdsUnitRunnerFactory.newRunner( - graph, - this@MainIfdsUnitManager, - unitResolver, - unit, - foundMethods[unit]!!.toList() - ) - aliveRunners[unit] = runner - runner.launchIn(this) - } - - allJobs.joinAll() - eventChannel.close() - progressLoggerJob.cancel() - } - - logger.info { "All jobs completed, gathering results..." } - - val foundVulnerabilities = foundMethods.values.flatten().flatMap { method -> - vulnerabilitiesStorage.getCurrentFacts(method) - } - - foundMethods.values.flatten().forEach { method -> - for (crossUnitCall in crossUnitCallsStorage.getCurrentFacts(method)) { - val calledMethod = graph.methodOf(crossUnitCall.calleeVertex.statement) - crossUnitCallers.getOrPut(calledMethod) { mutableSetOf() }.add(crossUnitCall) - } - } - - logger.info { "Restoring traces..." } - - foundVulnerabilities - .map { VulnerabilityInstance(it.vulnerabilityDescription, extendTraceGraph(it.sink.traceGraph)) } - .filter { - it.traceGraph.sources.any { source -> - graph.methodOf(source.statement) in startMethods || source.domainFact == ZEROFact - } - } - } - - private val TraceGraph.methods: List - get() { - return (edges.keys.map { graph.methodOf(it.statement) } + - listOf(graph.methodOf(sink.statement))).distinct() - } - - /** - * Given a [traceGraph], searches for other traceGraphs (from different units) - * and merges them into given if they extend any path leading to sink. - * - * This method allows to restore traces that pass through several units. - */ - private fun extendTraceGraph(traceGraph: TraceGraph): TraceGraph { - var result = traceGraph - val methodQueue: MutableSet = traceGraph.methods.toMutableSet() - val addedMethods: MutableSet = methodQueue.toMutableSet() - while (methodQueue.isNotEmpty()) { - val method = methodQueue.first() - methodQueue.remove(method) - for (callFact in crossUnitCallers[method].orEmpty()) { - // TODO: merge calleeVertices here - val sFacts = setOf(callFact.calleeVertex) - val upGraph = callFact.callerVertex.traceGraph - val newValue = result.mergeWithUpGraph(upGraph, sFacts) - if (result != newValue) { - result = newValue - for (nMethod in upGraph.methods) { - if (nMethod !in addedMethods) { - addedMethods.add(nMethod) - methodQueue.add(nMethod) - } - } - } - } - } - return result - } - - override suspend fun handleEvent(event: IfdsUnitRunnerEvent, runner: IfdsUnitRunner) { - when (event) { - is EdgeForOtherRunnerQuery -> { - val otherRunner = aliveRunners[unitResolver.resolve(event.edge.method)] ?: return - if (otherRunner.job?.isActive == true) { - otherRunner.submitNewEdge(event.edge) - } - } - is NewSummaryFact -> { - when (val fact = event.fact) { - is CrossUnitCallFact -> crossUnitCallsStorage.send(fact) - is SummaryEdgeFact -> summaryEdgesStorage.send(fact) - is TraceGraphFact -> tracesStorage.send(fact) - is VulnerabilityLocation -> vulnerabilitiesStorage.send(fact) - } - } - is QueueEmptinessChanged -> { - eventChannel.send(Pair(event, runner)) - } - is SubscriptionForSummaryEdges -> { - eventChannel.send(Pair(event, runner)) - summaryEdgesStorage.getFacts(event.method).map { - it.edge - }.collect(event.collector) - } - } - } - - // Used to linearize all events that change dependencies or queue emptiness of runners - private val eventChannel: Channel>> = - Channel(capacity = Int.MAX_VALUE) - - private suspend fun dispatchDependencies() = eventChannel.consumeEach { (event, runner) -> - when (event) { - is SubscriptionForSummaryEdges -> { - dependencies.getOrPut(runner.unit) { mutableSetOf() } - .add(unitResolver.resolve(event.method)) - dependenciesRev.getOrPut(unitResolver.resolve(event.method)) { mutableSetOf() } - .add(runner.unit) - } - is QueueEmptinessChanged -> { - if (runner.unit !in aliveRunners) { - return@consumeEach - } - queueEmptiness[runner.unit] = event.isEmpty - if (event.isEmpty) { - val toDelete = mutableListOf(runner.unit) - while (toDelete.isNotEmpty()) { - val current = toDelete.removeLast() - if (current in aliveRunners && - dependencies[runner.unit].orEmpty().all { queueEmptiness[it] != false } - ) { - aliveRunners[current]!!.job?.cancel() ?: error("Runner's job is not instantiated") - aliveRunners.remove(current) - for (next in dependenciesRev[current].orEmpty()) { - if (queueEmptiness[next] == true) { - toDelete.add(next) - } - } - } - } - } - } - else -> error("Unexpected event for dependencies dispatcher") - } - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/SummaryStorage.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/SummaryStorage.kt deleted file mode 100644 index 77b12ded4..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/SummaryStorage.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.analysis.engine - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import org.jacodb.analysis.sarif.VulnerabilityDescription -import org.jacodb.api.JcMethod -import java.util.concurrent.ConcurrentHashMap - -/** - * A common interface for anything that should be remembered and used - * after the analysis of some unit is completed. - */ -sealed interface SummaryFact { - val method: JcMethod -} - -/** - * [SummaryFact] that denotes a possible vulnerability at [sink] - */ -data class VulnerabilityLocation(val vulnerabilityDescription: VulnerabilityDescription, val sink: IfdsVertex) : SummaryFact { - override val method: JcMethod = sink.method -} - -/** - * Denotes some start-to-end edge that should be saved for the method - */ -data class SummaryEdgeFact(val edge: IfdsEdge) : SummaryFact { - override val method: JcMethod = edge.method -} - -/** - * Saves info about cross-unit call. - * This info could later be used to restore full [TraceGraph]s - */ -data class CrossUnitCallFact(val callerVertex: IfdsVertex, val calleeVertex: IfdsVertex) : SummaryFact { - override val method: JcMethod = callerVertex.method -} - -/** - * Wraps a [TraceGraph] that should be saved for some sink - */ -data class TraceGraphFact(val graph: TraceGraph) : SummaryFact { - override val method: JcMethod = graph.sink.method -} - -/** - * Contains summaries for many methods and allows to update them and subscribe for them. - */ -interface SummaryStorage { - /** - * Adds [fact] to summary of its method - */ - fun send(fact: T) - - /** - * @return a flow with all facts summarized for the given [method]. - * Already received facts, along with the facts that will be sent to this storage later, - * will be emitted to the returned flow. - */ - fun getFacts(method: JcMethod): Flow - - /** - * @return a list will all facts summarized for the given [method] so far. - */ - fun getCurrentFacts(method: JcMethod): List - - /** - * A list of all methods for which summaries are not empty. - */ - val knownMethods: List -} - -class SummaryStorageImpl : SummaryStorage { - private val summaries: MutableMap> = ConcurrentHashMap() - private val outFlows: MutableMap> = ConcurrentHashMap() - - override fun send(fact: T) { - if (summaries.computeIfAbsent(fact.method) { ConcurrentHashMap.newKeySet() }.add(fact)) { - val outFlow = outFlows.computeIfAbsent(fact.method) { MutableSharedFlow(replay = Int.MAX_VALUE) } - require(outFlow.tryEmit(fact)) - } - } - - override fun getFacts(method: JcMethod): SharedFlow { - return outFlows.computeIfAbsent(method) { - MutableSharedFlow(replay = Int.MAX_VALUE).also { flow -> - summaries[method].orEmpty().forEach { fact -> - require(flow.tryEmit(fact)) - } - } - } - } - - override fun getCurrentFacts(method: JcMethod): List { - return getFacts(method).replayCache - } - - override val knownMethods: List - get() = summaries.keys.toList() -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/TraceGraph.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/TraceGraph.kt deleted file mode 100644 index 059a788f3..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/TraceGraph.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.analysis.engine - -/** - * A directed graph with selected [sink] and [sources], where each path from one of [sources] to [sink] is a trace. - * - * @property sink is usually some interesting vertex that we want to reach (e.g. vertex that produces vulnerability) - * @property sources are the entry points, e.g. the vertices with [ZEROFact] or method starts - */ -data class TraceGraph( - val sink: IfdsVertex, - val sources: Set, - val edges: Map>, -) { - - private fun getAllTraces(curTrace: MutableList): Sequence> = sequence { - val v = curTrace.last() - - if (v == sink) { - yield(curTrace.toList()) - return@sequence - } - - for (u in edges[v].orEmpty()) { - if (u !in curTrace) { - curTrace.add(u) - yieldAll(getAllTraces(curTrace)) - curTrace.removeLast() - } - } - } - - /** - * Returns a sequence with all traces from [sources] to [sink] - */ - fun getAllTraces(): Sequence> = sequence { - sources.forEach { - yieldAll(getAllTraces(mutableListOf(it))) - } - } - - /** - * Merges two graphs. - * - * [sink] will be chosen from receiver, and edges from both graphs will be merged. - * Also, all edges from [upGraph]'s sink to [entryPoints] will be added - * (these are edges "connecting" [upGraph] with receiver). - * - * Informally, this method extends receiver's traces from one side using [upGraph]. - */ - fun mergeWithUpGraph(upGraph: TraceGraph, entryPoints: Set): TraceGraph { - val validEntryPoints = entryPoints.intersect(edges.keys).ifEmpty { - return this - } - - val newSources = sources + upGraph.sources - - val newEdges = edges.toMutableMap() - for ((source, dests) in upGraph.edges) { - newEdges[source] = newEdges.getOrDefault(source, emptySet()) + dests - } - newEdges[upGraph.sink] = newEdges.getOrDefault(upGraph.sink, emptySet()) + validEntryPoints - return TraceGraph(sink, newSources, newEdges) - } - - companion object { - fun bySink(sink: IfdsVertex) = TraceGraph(sink, setOf(sink), emptyMap()) - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/UnitResolver.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/UnitResolver.kt deleted file mode 100644 index 8b882699d..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/UnitResolver.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.analysis.engine - -import org.jacodb.analysis.library.MethodUnitResolver -import org.jacodb.analysis.library.PackageUnitResolver -import org.jacodb.analysis.library.SingletonUnitResolver -import org.jacodb.analysis.library.getClassUnitResolver -import org.jacodb.analysis.runAnalysis -import org.jacodb.api.JcMethod - -/** - * Sets a mapping from [JcMethod] to abstract domain [UnitType]. - * - * Therefore, it splits all methods into units, containing one or more method each - * (unit is a set of methods with same value of [UnitType] returned by [resolve]). - * - * To get more info about how it is used in analysis, see [runAnalysis]. - */ -fun interface UnitResolver { - fun resolve(method: JcMethod): UnitType - - companion object { - fun getByName(name: String): UnitResolver<*> { - return when (name) { - "method" -> MethodUnitResolver - "class" -> getClassUnitResolver(false) - "package" -> PackageUnitResolver - "singleton" -> SingletonUnitResolver - else -> error("Unknown unit resolver $name") - } - } - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt index af7ea04c2..e4dfdd9e7 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt @@ -15,8 +15,10 @@ */ @file:JvmName("ApplicationGraphFactory") + package org.jacodb.analysis.graph +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.future.future import org.jacodb.api.JcClasspath @@ -36,18 +38,24 @@ suspend fun JcClasspath.newApplicationGraphForAnalysis(bannedPackagePrefixes: Li } } -fun JcClasspath.asyncNewApplicationGraphForAnalysis( - bannedPackagePrefixes: List? = null -): CompletableFuture { - return GlobalScope.future { +/** + * Async adapter for calling [newApplicationGraphForAnalysis] from Java. + * + * See also: [answer on StackOverflow](https://stackoverflow.com/a/52887677/3592218). + */ +@OptIn(DelicateCoroutinesApi::class) +fun JcClasspath.newApplicationGraphForAnalysisAsync( + bannedPackagePrefixes: List? = null, +): CompletableFuture = + GlobalScope.future { newApplicationGraphForAnalysis(bannedPackagePrefixes) } -} val defaultBannedPackagePrefixes: List = listOf( "kotlin.", "java.", "jdk.internal.", "sun.", + "com.sun.", "javax.", -) \ No newline at end of file +) diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardApplicationGraphs.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardGraphs.kt similarity index 80% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardApplicationGraphs.kt rename to jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardGraphs.kt index 6dfaf7168..4d728aed1 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardApplicationGraphs.kt +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardGraphs.kt @@ -15,6 +15,7 @@ */ @file:JvmName("BackwardApplicationGraphs") + package org.jacodb.analysis.graph import org.jacodb.api.JcClasspath @@ -24,19 +25,21 @@ import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.cfg.JcInst private class BackwardApplicationGraph( - val forward: ApplicationGraph + val forward: ApplicationGraph, ) : ApplicationGraph { - override fun predecessors(node: Statement) = forward.successors(node) + init { + require(forward !is BackwardApplicationGraph) + } + + override fun predecessors(node: Statement) = forward.successors(node) override fun successors(node: Statement) = forward.predecessors(node) override fun callees(node: Statement) = forward.callees(node) - override fun callers(method: Method) = forward.callers(method) - override fun entryPoint(method: Method) = forward.exitPoints(method) - - override fun exitPoints(method: Method) = forward.entryPoint(method) + override fun entryPoints(method: Method) = forward.exitPoints(method) + override fun exitPoints(method: Method) = forward.entryPoints(method) override fun methodOf(node: Statement) = forward.methodOf(node) } @@ -48,8 +51,14 @@ val ApplicationGraph.reversed BackwardApplicationGraph(this) } -private class BackwardJcApplicationGraph(val forward: JcApplicationGraph) : - JcApplicationGraph, ApplicationGraph by BackwardApplicationGraph(forward) { +internal class BackwardJcApplicationGraph(val forward: JcApplicationGraph) : + JcApplicationGraph, + ApplicationGraph by BackwardApplicationGraph(forward) { + + init { + require(forward !is BackwardJcApplicationGraph) + } + override val classpath: JcClasspath get() = forward.classpath } @@ -59,4 +68,4 @@ val JcApplicationGraph.reversed: JcApplicationGraph this.forward } else { BackwardJcApplicationGraph(this) - } \ No newline at end of file + } diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcApplicationGraphImpl.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcApplicationGraphImpl.kt index a53a08454..2909cfd65 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcApplicationGraphImpl.kt +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcApplicationGraphImpl.kt @@ -28,48 +28,45 @@ import org.jacodb.impl.features.SyncUsagesExtension */ open class JcApplicationGraphImpl( override val classpath: JcClasspath, - private val usages: SyncUsagesExtension + private val usages: SyncUsagesExtension, ) : JcApplicationGraph { - private val methods = mutableSetOf() - override fun predecessors(node: JcInst): Sequence { - return node.location.method.flowGraph().predecessors(node).asSequence() + - node.location.method.flowGraph().throwers(node).asSequence() + val graph = node.location.method.flowGraph() + val predecessors = graph.predecessors(node) + val throwers = graph.throwers(node) + return predecessors.asSequence() + throwers.asSequence() } override fun successors(node: JcInst): Sequence { - return node.location.method.flowGraph().successors(node).asSequence() + - node.location.method.flowGraph().catchers(node).asSequence() + val graph = node.location.method.flowGraph() + val successors = graph.successors(node) + val catchers = graph.catchers(node) + return successors.asSequence() + catchers.asSequence() } override fun callees(node: JcInst): Sequence { - return node.callExpr?.method?.method?.let { - methods.add(it) - sequenceOf(it) - } ?: emptySequence() + val callExpr = node.callExpr ?: return emptySequence() + return sequenceOf(callExpr.method.method) } override fun callers(method: JcMethod): Sequence { - methods.add(method) return usages.findUsages(method).flatMap { - it.flowGraph().instructions.filter { inst -> - inst.callExpr?.method?.method == method - }.asSequence() + it.flowGraph().instructions.asSequence().filter { inst -> + val callExpr = inst.callExpr ?: return@filter false + callExpr.method.method == method + } } } - - override fun entryPoint(method: JcMethod): Sequence { - methods.add(method) + override fun entryPoints(method: JcMethod): Sequence { return method.flowGraph().entries.asSequence() } override fun exitPoints(method: JcMethod): Sequence { - methods.add(method) return method.flowGraph().exits.asSequence() } override fun methodOf(node: JcInst): JcMethod { - return node.location.method.also { methods.add(it) } + return node.location.method } -} \ No newline at end of file +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcNoopInst.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcNoopInst.kt new file mode 100644 index 000000000..3d693800a --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcNoopInst.kt @@ -0,0 +1,33 @@ +/* + * 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.analysis.graph + +import org.jacodb.api.cfg.JcExpr +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.cfg.JcInstLocation +import org.jacodb.api.cfg.JcInstVisitor + +data class JcNoopInst(override val location: JcInstLocation) : JcInst { + override val operands: List + get() = emptyList() + + override fun accept(visitor: JcInstVisitor): T { + return visitor.visitExternalJcInst(this) + } + + override fun toString(): String = "noop" +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt index 1f3fcd708..efc65b24b 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt @@ -20,10 +20,7 @@ import kotlinx.coroutines.runBlocking import org.jacodb.api.JcClassType import org.jacodb.api.JcMethod import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcExpr import org.jacodb.api.cfg.JcInst -import org.jacodb.api.cfg.JcInstLocation -import org.jacodb.api.cfg.JcInstVisitor import org.jacodb.api.cfg.JcVirtualCallExpr import org.jacodb.api.ext.cfg.callExpr import org.jacodb.api.ext.isSubClassOf @@ -33,14 +30,14 @@ import org.jacodb.impl.features.hierarchyExt /** * This is adopted specially for IFDS [JcApplicationGraph] that * 1. Ignores method calls matching [bannedPackagePrefixes] (i.e., treats them as simple instructions with no callees) - * 2. In [callers] returns only callsites that were visited before + * 2. In [callers] returns only call sites that were visited before * 3. Adds a special [JcNoopInst] instruction to the beginning of each method * (because backward analysis may want for method to start with neutral instruction) */ internal class SimplifiedJcApplicationGraph( - private val impl: JcApplicationGraphImpl, + private val graph: JcApplicationGraph, private val bannedPackagePrefixes: List, -) : JcApplicationGraph by impl { +) : JcApplicationGraph by graph { private val hierarchyExtension = runBlocking { classpath.hierarchyExt() } @@ -49,47 +46,55 @@ internal class SimplifiedJcApplicationGraph( private val cache: MutableMap> = mutableMapOf() - private fun getOverrides(method: JcMethod): List { - return if (cache.containsKey(method)) { - cache[method]!! - } else { - val res = hierarchyExtension.findOverrides(method).toList() - cache[method] = res - res - } - } - // For backward analysis we may want for method to start with "neutral" operation => // we add noop to the beginning of every method private fun getStartInst(method: JcMethod): JcNoopInst { - val methodEntryLineNumber = method.flowGraph().entries.firstOrNull()?.lineNumber - return JcNoopInst(JcInstLocationImpl(method, -1, methodEntryLineNumber?.let { it - 1 } ?: -1)) + val lineNumber = method.flowGraph().entries.firstOrNull()?.lineNumber?.let { it - 1 } ?: -1 + return JcNoopInst(JcInstLocationImpl(method, -1, lineNumber)) } override fun predecessors(node: JcInst): Sequence { val method = methodOf(node) - return if (node == getStartInst(method)) { - emptySequence() - } else { - if (node in impl.entryPoint(method)) { + return when (node) { + getStartInst(method) -> { + emptySequence() + } + + in graph.entryPoints(method) -> { sequenceOf(getStartInst(method)) - } else { - impl.predecessors(node) + } + + else -> { + graph.predecessors(node) } } } override fun successors(node: JcInst): Sequence { val method = methodOf(node) - return if (node == getStartInst(method)) { - impl.entryPoint(method) + return when (node) { + getStartInst(method) -> { + graph.entryPoints(method) + } + + else -> { + graph.successors(node) + } + } + } + + private fun getOverrides(method: JcMethod): List { + return if (cache.containsKey(method)) { + cache[method]!! } else { - impl.successors(node) + val res = hierarchyExtension.findOverrides(method).toList() + cache[method] = res + res } } private fun calleesUnmarked(node: JcInst): Sequence { - val callees = impl.callees(node).filterNot { callee -> + val callees = graph.callees(node).filterNot { callee -> bannedPackagePrefixes.any { callee.enclosingClass.name.startsWith(it) } } @@ -101,8 +106,8 @@ internal class SimplifiedJcApplicationGraph( val allOverrides = getOverrides(callee) .filter { it.enclosingClass isSubClassOf instanceClass || - // TODO: use only down-most override here - instanceClass isSubClassOf it.enclosingClass + // TODO: use only down-most override here + instanceClass isSubClassOf it.enclosingClass } // TODO: maybe filter inaccessible methods here? @@ -112,8 +117,8 @@ internal class SimplifiedJcApplicationGraph( override fun callees(node: JcInst): Sequence { return calleesUnmarked(node).also { - it.forEach { - visitedCallers.getOrPut(it) { mutableSetOf() }.add(node) + it.forEach { method -> + visitedCallers.getOrPut(method) { mutableSetOf() }.add(node) } } } @@ -123,22 +128,22 @@ internal class SimplifiedJcApplicationGraph( * In IFDS we don't need all method callers, we need only method callers which we visited earlier. */ // TODO: Think if this optimization is really needed - override fun callers(method: JcMethod): Sequence = visitedCallers.getOrDefault(method, mutableSetOf()).asSequence() - - override fun entryPoint(method: JcMethod): Sequence = sequenceOf(getStartInst(method)) - - companion object { + override fun callers(method: JcMethod): Sequence = + visitedCallers[method].orEmpty().asSequence() + + override fun entryPoints(method: JcMethod): Sequence = try { + sequenceOf(getStartInst(method)) + } catch (e: Throwable) { + // we couldn't find instructions list + // TODO: maybe fix flowGraph() + emptySequence() } -} - -data class JcNoopInst(override val location: JcInstLocation): JcInst { - override val operands: List - get() = emptyList() - - override fun accept(visitor: JcInstVisitor): T { - return visitor.visitExternalJcInst(this) + override fun exitPoints(method: JcMethod): Sequence = try { + graph.exitPoints(method) + } catch (e: Throwable) { + // we couldn't find instructions list + // TODO: maybe fix flowGraph() + emptySequence() } - - override fun toString(): String = "noop" -} \ No newline at end of file +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/AccessPath.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/AccessPath.kt new file mode 100644 index 000000000..9210b0dd7 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/AccessPath.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.analysis.ifds + +import org.jacodb.api.JcField +import org.jacodb.api.cfg.JcArrayAccess +import org.jacodb.api.cfg.JcCastExpr +import org.jacodb.api.cfg.JcExpr +import org.jacodb.api.cfg.JcFieldRef +import org.jacodb.api.cfg.JcSimpleValue +import org.jacodb.api.cfg.JcValue + +/** + * This class is used to represent an access path that is needed for problems + * where dataflow facts could be correlated with variables/values + * (such as NPE, uninitialized variable, etc.) + */ +data class AccessPath internal constructor( + val value: JcSimpleValue?, // null for static field + val accesses: List, +) { + init { + if (value == null) { + require(accesses.isNotEmpty()) + val a = accesses[0] + require(a is FieldAccessor) + require(a.field.isStatic) + } + } + + val isOnHeap: Boolean + get() = accesses.isNotEmpty() + + val isStatic: Boolean + get() = value == null + + fun limit(n: Int): AccessPath = AccessPath(value, accesses.take(n)) + + operator fun div(accesses: List): AccessPath { + for (accessor in accesses) { + if (accessor is FieldAccessor && accessor.field.isStatic) { + throw IllegalArgumentException("Unexpected static field: ${accessor.field}") + } + } + + return AccessPath(value, this.accesses + accesses) + } + + operator fun div(accessor: Accessor): AccessPath { + if (accessor is FieldAccessor && accessor.field.isStatic) { + throw IllegalArgumentException("Unexpected static field: ${accessor.field}") + } + + return AccessPath(value, this.accesses + accessor) + } + + operator fun minus(other: AccessPath): List? { + if (value != other.value) return null + if (accesses.take(other.accesses.size) != other.accesses) return null + return accesses.drop(other.accesses.size) + } + + override fun toString(): String { + return value.toString() + accesses.joinToString("") { it.toSuffix() } + } + + companion object { + fun from(value: JcSimpleValue): AccessPath = AccessPath(value, emptyList()) + + fun from(field: JcField): AccessPath { + require(field.isStatic) { "Expected static field" } + return AccessPath(null, listOf(FieldAccessor(field))) + } + } +} + +fun JcExpr.toPathOrNull(): AccessPath? = when (this) { + is JcValue -> toPathOrNull() + is JcCastExpr -> operand.toPathOrNull() + else -> null +} + +fun JcValue.toPathOrNull(): AccessPath? = when (this) { + is JcSimpleValue -> AccessPath.from(this) + + is JcArrayAccess -> { + array.toPathOrNull()?.let { + it / ElementAccessor + } + } + + is JcFieldRef -> { + val instance = instance + if (instance == null) { + AccessPath.from(field.field) + } else { + instance.toPathOrNull()?.let { + it / FieldAccessor(field.field) + } + } + } + + else -> null +} + +fun JcValue.toPath(): AccessPath { + return toPathOrNull() ?: error("Unable to build access path for value $this") +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Accessors.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Accessors.kt new file mode 100644 index 000000000..885582f36 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Accessors.kt @@ -0,0 +1,35 @@ +/* + * 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.analysis.ifds + +import org.jacodb.api.JcField + +sealed interface Accessor { + fun toSuffix(): String +} + +data class FieldAccessor( + val field: JcField, +) : Accessor { + override fun toSuffix(): String = ".${field.name}" + override fun toString(): String = field.name +} + +object ElementAccessor : Accessor { + override fun toSuffix(): String = "[*]" + override fun toString(): String = "*" +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Analyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Analyzer.kt new file mode 100644 index 000000000..f38a9be90 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Analyzer.kt @@ -0,0 +1,30 @@ +/* + * 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.analysis.ifds + +interface Analyzer { + val flowFunctions: FlowFunctions + + fun handleNewEdge( + edge: Edge, + ): List + + fun handleCrossUnitCall( + caller: Vertex, + callee: Vertex, + ): List +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/Accessors.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Edge.kt similarity index 67% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/Accessors.kt rename to jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Edge.kt index 10956d45d..7492be3ea 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/Accessors.kt +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Edge.kt @@ -14,20 +14,18 @@ * limitations under the License. */ -package org.jacodb.analysis.paths +package org.jacodb.analysis.ifds -import org.jacodb.api.JcField +import org.jacodb.api.JcMethod -sealed interface Accessor - -data class FieldAccessor(val field: JcField) : Accessor { - override fun toString(): String { - return field.name +data class Edge( + val from: Vertex, + val to: Vertex, +) { + init { + require(from.method == to.method) } -} -object ElementAccessor : Accessor { - override fun toString(): String { - return "*" - } -} \ No newline at end of file + val method: JcMethod + get() = from.method +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/FlowFunctions.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/FlowFunctions.kt new file mode 100644 index 000000000..7a93eaee7 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/FlowFunctions.kt @@ -0,0 +1,108 @@ +/* + * 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.analysis.ifds + +import org.jacodb.api.JcMethod +import org.jacodb.api.cfg.JcInst + +fun interface FlowFunction { + fun compute(fact: Fact): Collection +} + +interface FlowFunctions { + + /** + * Method for obtaining initial domain facts at the method entrypoint. + * Commonly, it is only `listOf(Zero)`. + */ + fun obtainPossibleStartFacts(method: JcMethod): Collection + + /** + * Sequent flow function. + * + * ``` + * [ DO() ] :: current + * | + * | (sequent edge) + * | + * [ DO() ] + * ``` + */ + fun obtainSequentFlowFunction( + current: JcInst, + next: JcInst, + ): FlowFunction + + /** + * Call-to-return-site flow function. + * + * ``` + * [ CALL p ] :: callStatement + * : + * : (call-to-return-site edge) + * : + * [ RETURN FROM p ] :: returnSite + * ``` + */ + fun obtainCallToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, + ): FlowFunction + + /** + * Call-to-start flow function. + * + * ``` + * [ CALL p ] :: callStatement + * : \ + * : \ (call-to-start edge) + * : \ + * : [ START p ] + * : | + * : [ EXIT p ] + * : / + * : / + * [ RETURN FROM p ] + * ``` + */ + fun obtainCallToStartFlowFunction( + callStatement: JcInst, + calleeStart: JcInst, + ): FlowFunction + + /** + * Exit-to-return-site flow function. + * + * ``` + * [ CALL p ] :: callStatement + * : \ + * : \ + * : [ START p ] + * : | + * : [ EXIT p ] :: exitStatement + * : / + * : / (exit-to-return-site edge) + * : / + * [ RETURN FROM p ] :: returnSite + * ``` + */ + fun obtainExitToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, + exitStatement: JcInst, + ): FlowFunction +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/IfdsResult.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/IfdsResult.kt new file mode 100644 index 000000000..0584541b1 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/IfdsResult.kt @@ -0,0 +1,130 @@ +/* + * 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.analysis.ifds + +import org.jacodb.api.cfg.JcInst + +/** + * Aggregates all facts and edges found by the tabulation algorithm. + */ +class IfdsResult internal constructor( + val pathEdgesBySink: Map, Collection>>, + val facts: Map>, + val reasons: Map, Set>>, + val zeroFact: Fact?, +) { + constructor( + pathEdges: Collection>, + facts: Map>, + reasons: Map, Set>>, + zeroFact: Fact?, + ) : this( + pathEdges.groupByTo(HashMap()) { it.to }, + facts, + reasons, + zeroFact + ) + + fun buildTraceGraph(sink: Vertex): TraceGraph { + val sources: MutableSet> = hashSetOf() + val edges: MutableMap, MutableSet>> = hashMapOf() + val unresolvedCrossUnitCalls: MutableMap, MutableSet>> = hashMapOf() + val visited: MutableSet, Vertex>> = hashSetOf() + + fun addEdge( + from: Vertex, + to: Vertex, + ) { + if (from != to) { + edges.getOrPut(from) { hashSetOf() }.add(to) + } + } + + fun dfs( + edge: Edge, + lastVertex: Vertex, + stopAtMethodStart: Boolean, + ) { + if (!visited.add(edge to lastVertex)) { + return + } + + // Note: loop-edge represents method start + if (stopAtMethodStart && edge.from == edge.to) { + addEdge(edge.from, lastVertex) + return + } + + val vertex = edge.to + if (vertex.fact == zeroFact) { + addEdge(vertex, lastVertex) + sources.add(vertex) + return + } + + for (reason in reasons[edge].orEmpty()) { + when (reason) { + is Reason.Sequent -> { + val predEdge = reason.edge + if (predEdge.to.fact == vertex.fact) { + dfs(predEdge, lastVertex, stopAtMethodStart) + } else { + addEdge(predEdge.to, lastVertex) + dfs(predEdge, predEdge.to, stopAtMethodStart) + } + } + + is Reason.CallToStart -> { + val predEdge = reason.edge + if (!stopAtMethodStart) { + addEdge(predEdge.to, lastVertex) + dfs(predEdge, predEdge.to, false) + } + } + + is Reason.ThroughSummary -> { + val predEdge = reason.edge + val summaryEdge = reason.summaryEdge + addEdge(summaryEdge.to, lastVertex) // Return to next vertex + addEdge(predEdge.to, summaryEdge.from) // Call to start + dfs(summaryEdge, summaryEdge.to, true) // Expand summary edge + dfs(predEdge, predEdge.to, stopAtMethodStart) // Continue normal analysis + } + + is Reason.CrossUnitCall -> { + addEdge(edge.to, lastVertex) + unresolvedCrossUnitCalls.getOrPut(reason.caller) { hashSetOf() }.add(edge.to) + } + + is Reason.External -> { + TODO("External reason is not supported yet") + } + + is Reason.Initial -> { + sources.add(vertex) + addEdge(edge.to, lastVertex) + } + } + } + } + + for (edge in pathEdgesBySink[sink].orEmpty()) { + dfs(edge, edge.to, false) + } + return TraceGraph(sink, sources, edges, unresolvedCrossUnitCalls) + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Manager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Manager.kt new file mode 100644 index 000000000..cc7b2c897 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Manager.kt @@ -0,0 +1,39 @@ +/* + * 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.analysis.ifds + +import kotlinx.coroutines.CoroutineScope +import org.jacodb.api.JcMethod + +interface Manager { + fun handleEvent(event: Event) + + fun handleControlEvent(event: ControlEvent) + + fun subscribeOnSummaryEdges( + method: JcMethod, + scope: CoroutineScope, + handler: (Edge) -> Unit, + ) +} + +sealed interface ControlEvent + +data class QueueEmptinessChanged( + val runner: Runner<*>, + val isEmpty: Boolean, +) : ControlEvent diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt new file mode 100644 index 000000000..be4356362 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt @@ -0,0 +1,60 @@ +/* + * 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.analysis.ifds + +@JvmInline +value class Maybe private constructor( + private val rawValue: Any?, +) { + val isSome: Boolean get() = rawValue !== NONE_VALUE + val isNone: Boolean get() = rawValue === NONE_VALUE + + fun getOrThrow(): T { + check(isSome) { "Maybe is None" } + @Suppress("UNCHECKED_CAST") + return rawValue as T + } + + companion object { + private val NONE_VALUE = Any() + private val NONE = Maybe(NONE_VALUE) + + fun none(): Maybe = NONE + + fun some(value: T): Maybe = Maybe(value) + + fun from(value: T?): Maybe = if (value == null) none() else some(value) + } +} + +inline fun Maybe.map(body: (T) -> Maybe): Maybe = + if (isNone) Maybe.none() else body(getOrThrow()) + +inline fun Maybe.fmap(body: (T) -> R): Maybe = + if (isNone) Maybe.none() else Maybe.some(body(getOrThrow())) + +inline fun Maybe.onSome(body: (T) -> Unit): Maybe { + if (isSome) body(getOrThrow()) + return this +} + +inline fun Maybe.onNone(body: () -> Unit): Maybe { + if (isNone) body() + return this +} + +fun T?.toMaybe(): Maybe = Maybe.from(this) diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Reason.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Reason.kt new file mode 100644 index 000000000..c6600cd62 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Reason.kt @@ -0,0 +1,40 @@ +/* + * 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.analysis.ifds + +sealed interface Reason { + object Initial : Reason + + object External : Reason + + data class CrossUnitCall( + val caller: Vertex, + ) : Reason + + data class Sequent( + val edge: Edge, + ) : Reason + + data class CallToStart( + val edge: Edge, + ) : Reason + + data class ThroughSummary( + val edge: Edge, + val summaryEdge: Edge, + ) : Reason +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Runner.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Runner.kt new file mode 100644 index 000000000..a8fc4f916 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Runner.kt @@ -0,0 +1,257 @@ +/* + * 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.analysis.ifds + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.getOrElse +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import org.jacodb.analysis.graph.JcNoopInst +import org.jacodb.analysis.taint.TaintZeroFact +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.ext.cfg.callExpr +import java.util.concurrent.ConcurrentHashMap + +private val logger = mu.KotlinLogging.logger {} + +interface Runner { + val unit: UnitType + + suspend fun run(startMethods: List) + fun submitNewEdge(edge: Edge, reason: Reason) + fun getIfdsResult(): IfdsResult +} + +class UniRunner( + private val graph: JcApplicationGraph, + private val analyzer: Analyzer, + private val manager: Manager, + private val unitResolver: UnitResolver, + override val unit: UnitType, + private val zeroFact: Fact?, +) : Runner { + + private val flowSpace: FlowFunctions = analyzer.flowFunctions + private val workList: Channel> = Channel(Channel.UNLIMITED) + private val reasons = ConcurrentHashMap, MutableSet>>() + internal val pathEdges: MutableSet> = ConcurrentHashMap.newKeySet() + + private val summaryEdges: MutableMap, MutableSet>> = hashMapOf() + private val callerPathEdgeOf: MutableMap, MutableSet>> = hashMapOf() + + private val queueIsEmpty = QueueEmptinessChanged(runner = this, isEmpty = true) + private val queueIsNotEmpty = QueueEmptinessChanged(runner = this, isEmpty = false) + + override suspend fun run(startMethods: List) { + for (method in startMethods) { + addStart(method) + } + + tabulationAlgorithm() + } + + private fun addStart(method: JcMethod) { + require(unitResolver.resolve(method) == unit) + val startFacts = flowSpace.obtainPossibleStartFacts(method) + for (startFact in startFacts) { + for (start in graph.entryPoints(method)) { + val vertex = Vertex(start, startFact) + val edge = Edge(vertex, vertex) // loop + propagate(edge, Reason.Initial) + } + } + } + + override fun submitNewEdge(edge: Edge, reason: Reason) { + propagate(edge, reason) + } + + private fun propagate( + edge: Edge, + reason: Reason, + ): Boolean { + require(unitResolver.resolve(edge.method) == unit) { + "Propagated edge must be in the same unit" + } + + reasons.computeIfAbsent(edge) { ConcurrentHashMap.newKeySet() }.add(reason) + + // Handle only NEW edges: + if (pathEdges.add(edge)) { + val doPrintOnlyForward = true + val doPrintZero = false + if (!doPrintOnlyForward || edge.from.statement is JcNoopInst) { + if (doPrintZero || edge.to.fact != TaintZeroFact) { + logger.trace { "Propagating edge=$edge in method=${edge.method.name} with reason=${reason}" } + } + } + + // Send edge to analyzer/manager: + for (event in analyzer.handleNewEdge(edge)) { + manager.handleEvent(event) + } + + // Add edge to worklist: + workList.trySend(edge).getOrThrow() + + return true + } + + return false + } + + private suspend fun tabulationAlgorithm() = coroutineScope { + while (isActive) { + val edge = workList.tryReceive().getOrElse { + manager.handleControlEvent(queueIsEmpty) + val edge = workList.receive() + manager.handleControlEvent(queueIsNotEmpty) + edge + } + tabulationAlgorithmStep(edge, this@coroutineScope) + } + } + + private val JcMethod.isExtern: Boolean + get() = unitResolver.resolve(this) != unit + + private fun tabulationAlgorithmStep( + currentEdge: Edge, + scope: CoroutineScope, + ) { + val (startVertex, currentVertex) = currentEdge + val (current, currentFact) = currentVertex + + val currentCallees = graph.callees(current).toList() + val currentIsCall = current.callExpr != null + val currentIsExit = current in graph.exitPoints(current.location.method) + + if (currentIsCall) { + // Propagate through the call-to-return-site edge: + for (returnSite in graph.successors(current)) { + val factsAtReturnSite = flowSpace + .obtainCallToReturnSiteFlowFunction(current, returnSite) + .compute(currentFact) + for (returnSiteFact in factsAtReturnSite) { + val returnSiteVertex = Vertex(returnSite, returnSiteFact) + val newEdge = Edge(startVertex, returnSiteVertex) + propagate(newEdge, Reason.Sequent(currentEdge)) + } + } + + // Propagate through the call: + for (callee in currentCallees) { + for (calleeStart in graph.entryPoints(callee)) { + val factsAtCalleeStart = flowSpace + .obtainCallToStartFlowFunction(current, calleeStart) + .compute(currentFact) + for (calleeStartFact in factsAtCalleeStart) { + val calleeStartVertex = Vertex(calleeStart, calleeStartFact) + + if (callee.isExtern) { + // Initialize analysis of callee: + for (event in analyzer.handleCrossUnitCall(currentVertex, calleeStartVertex)) { + manager.handleEvent(event) + } + + // Subscribe on summary edges: + manager.subscribeOnSummaryEdges(callee, scope) { summaryEdge -> + if (summaryEdge.from == calleeStartVertex) { + handleSummaryEdge(currentEdge, summaryEdge) + } else { + logger.trace { "Skipping unsuitable summary edge: $summaryEdge" } + } + } + } else { + // Save info about the call for summary edges that will be found later: + callerPathEdgeOf.getOrPut(calleeStartVertex) { hashSetOf() }.add(currentEdge) + + // Initialize analysis of callee: + run { + val newEdge = Edge(calleeStartVertex, calleeStartVertex) // loop + propagate(newEdge, Reason.CallToStart(currentEdge)) + } + + // Handle already-found summary edges: + for (exitVertex in summaryEdges[calleeStartVertex].orEmpty()) { + val summaryEdge = Edge(calleeStartVertex, exitVertex) + handleSummaryEdge(currentEdge, summaryEdge) + } + } + } + } + } + } else { + if (currentIsExit) { + // Propagate through the summary edge: + for (callerPathEdge in callerPathEdgeOf[startVertex].orEmpty()) { + handleSummaryEdge(currentEdge = callerPathEdge, summaryEdge = currentEdge) + } + + // Add new summary edge: + summaryEdges.getOrPut(startVertex) { hashSetOf() }.add(currentVertex) + } + + // Simple (sequential) propagation to the next instruction: + for (next in graph.successors(current)) { + val factsAtNext = flowSpace + .obtainSequentFlowFunction(current, next) + .compute(currentFact) + for (nextFact in factsAtNext) { + val nextVertex = Vertex(next, nextFact) + val newEdge = Edge(startVertex, nextVertex) + propagate(newEdge, Reason.Sequent(currentEdge)) + } + } + } + } + + private fun handleSummaryEdge( + currentEdge: Edge, + summaryEdge: Edge, + ) { + val (startVertex, currentVertex) = currentEdge + val caller = currentVertex.statement + for (returnSite in graph.successors(caller)) { + val (exit, exitFact) = summaryEdge.to + val finalFacts = flowSpace + .obtainExitToReturnSiteFlowFunction(caller, returnSite, exit) + .compute(exitFact) + for (returnSiteFact in finalFacts) { + val returnSiteVertex = Vertex(returnSite, returnSiteFact) + val newEdge = Edge(startVertex, returnSiteVertex) + propagate(newEdge, Reason.ThroughSummary(currentEdge, summaryEdge)) + } + } + } + + private fun getFinalFacts(): Map> { + val resultFacts: MutableMap> = hashMapOf() + for (edge in pathEdges) { + resultFacts.getOrPut(edge.to.statement) { hashSetOf() }.add(edge.to.fact) + } + return resultFacts + } + + override fun getIfdsResult(): IfdsResult { + val facts = getFinalFacts() + return IfdsResult(pathEdges, facts, reasons, zeroFact) + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Summary.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Summary.kt new file mode 100644 index 000000000..3f0c90206 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Summary.kt @@ -0,0 +1,103 @@ +/* + * 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.analysis.ifds + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import org.jacodb.api.JcMethod +import java.util.concurrent.ConcurrentHashMap + +/** + * A common interface for anything that should be remembered + * and used after the analysis of some unit is completed. + */ +interface Summary { + val method: JcMethod +} + +interface SummaryEdge : Summary { + val edge: Edge + + override val method: JcMethod + get() = edge.method +} + +interface Vulnerability : Summary { + val message: String + val sink: Vertex + + override val method: JcMethod + get() = sink.method +} + +/** + * Contains summaries for many methods and allows to update them and subscribe for them. + */ +interface SummaryStorage { + /** + * A list of all methods for which summaries are not empty. + */ + val knownMethods: List + + /** + * Adds [fact] to summary of its method. + */ + fun add(fact: T) + + /** + * @return a flow with all facts summarized for the given [method]. + * Already received facts, along with the facts that will be sent to this storage later, + * will be emitted to the returned flow. + */ + fun getFacts(method: JcMethod): Flow + + /** + * @return a list will all facts summarized for the given [method] so far. + */ + fun getCurrentFacts(method: JcMethod): List +} + +class SummaryStorageImpl : SummaryStorage { + private val summaries = ConcurrentHashMap>() + private val outFlows = ConcurrentHashMap>() + + override val knownMethods: List + get() = summaries.keys.toList() + + private fun getFlow(method: JcMethod): MutableSharedFlow { + return outFlows.computeIfAbsent(method) { + MutableSharedFlow(replay = Int.MAX_VALUE) + } + } + + override fun add(fact: T) { + val isNew = summaries.computeIfAbsent(fact.method) { ConcurrentHashMap.newKeySet() }.add(fact) + if (isNew) { + val flow = getFlow(fact.method) + check(flow.tryEmit(fact)) + } + } + + override fun getFacts(method: JcMethod): SharedFlow { + return getFlow(method) + } + + override fun getCurrentFacts(method: JcMethod): List { + return getFacts(method).replayCache + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/TraceGraph.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/TraceGraph.kt new file mode 100644 index 000000000..60a83d380 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/TraceGraph.kt @@ -0,0 +1,66 @@ +/* + * 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.analysis.ifds + +data class TraceGraph( + val sink: Vertex, + val sources: MutableSet>, + val edges: MutableMap, MutableSet>>, + val unresolvedCrossUnitCalls: Map, Set>>, +) { + /** + * Returns all traces from [sources] to [sink]. + */ + fun getAllTraces(): Sequence>> = sequence { + for (v in sources) { + yieldAll(getAllTraces(mutableListOf(v))) + } + } + + private fun getAllTraces( + trace: MutableList>, + ): Sequence>> = sequence { + val v = trace.last() + if (v == sink) { + yield(trace.toList()) // copy list + return@sequence + } + for (u in edges[v].orEmpty()) { + if (u !in trace) { + trace.add(u) + yieldAll(getAllTraces(trace)) + trace.removeLast() + } + } + } + + /** + * Merges [upGraph] into this graph. + */ + fun mergeWithUpGraph( + upGraph: TraceGraph, + entryPoints: Set>, + ) { + sources.addAll(upGraph.sources) + + for (edge in upGraph.edges) { + edges.getOrPut(edge.key) { hashSetOf() }.addAll(edge.value) + } + + edges.getOrPut(upGraph.sink) { hashSetOf() }.addAll(entryPoints) + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/UnitResolver.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/UnitResolver.kt new file mode 100644 index 000000000..605257f0d --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/UnitResolver.kt @@ -0,0 +1,92 @@ +/* + * 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.analysis.ifds + +import org.jacodb.api.JcClassOrInterface +import org.jacodb.api.JcMethod +import org.jacodb.api.ext.packageName + +interface UnitType + +data class MethodUnit(val method: JcMethod) : UnitType { + override fun toString(): String { + return "MethodUnit(${method.name})" + } +} + +data class ClassUnit(val clazz: JcClassOrInterface) : UnitType { + override fun toString(): String { + return "ClassUnit(${clazz.simpleName})" + } +} + +data class PackageUnit(val packageName: String) : UnitType { + override fun toString(): String { + return "PackageUnit($packageName)" + } +} + +object UnknownUnit : UnitType { + override fun toString(): String = javaClass.simpleName +} + +object SingletonUnit : UnitType { + override fun toString(): String = javaClass.simpleName +} + +/** + * Sets a mapping from [JcMethod] to abstract domain [UnitType]. + * + * Therefore, it splits all methods into units, containing one or more method each + * (unit is a set of methods with same value of [UnitType] returned by [resolve]). + */ +fun interface UnitResolver { + + fun resolve(method: JcMethod): UnitType + + companion object { + fun getByName(name: String): UnitResolver = when (name) { + "method" -> MethodUnitResolver + "class" -> ClassUnitResolver(false) + "package" -> PackageUnitResolver + "singleton" -> SingletonUnitResolver + else -> error("Unknown unit resolver '$name'") + } + } +} + +val MethodUnitResolver = UnitResolver { method -> + MethodUnit(method) +} + +@Suppress("FunctionName") +fun ClassUnitResolver(includeNested: Boolean) = UnitResolver { method -> + val clazz = if (includeNested) { + generateSequence(method.enclosingClass) { it.outerClass }.last() + } else { + method.enclosingClass + } + ClassUnit(clazz) +} + +val PackageUnitResolver = UnitResolver { method -> + PackageUnit(method.enclosingClass.packageName) +} + +val SingletonUnitResolver = UnitResolver { _ -> + SingletonUnit +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsVertex.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Vertex.kt similarity index 87% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsVertex.kt rename to jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Vertex.kt index ba0f73c8e..df82fb813 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsVertex.kt +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Vertex.kt @@ -14,12 +14,15 @@ * limitations under the License. */ -package org.jacodb.analysis.engine +package org.jacodb.analysis.ifds import org.jacodb.api.JcMethod import org.jacodb.api.cfg.JcInst -data class IfdsVertex(val statement: JcInst, val domainFact: DomainFact) { +data class Vertex( + val statement: JcInst, + val fact: Fact, +) { val method: JcMethod get() = statement.location.method -} \ No newline at end of file +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/NullAssumptionAnalysis.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/NullAssumptionAnalysis.kt index 62c0cffa1..610c92e2c 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/NullAssumptionAnalysis.kt +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/NullAssumptionAnalysis.kt @@ -228,4 +228,4 @@ enum class NullableState { UNKNOWN, NULL, NON_NULL -} \ No newline at end of file +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/RunnersLibrary.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/RunnersLibrary.kt deleted file mode 100644 index eeb479408..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/RunnersLibrary.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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. - */ - -@file:JvmName("RunnersLibrary") -package org.jacodb.analysis.library - -import org.jacodb.analysis.engine.BaseIfdsUnitRunnerFactory -import org.jacodb.analysis.engine.BidiIfdsUnitRunnerFactory -import org.jacodb.analysis.library.analyzers.AliasAnalyzerFactory -import org.jacodb.analysis.library.analyzers.NpeAnalyzerFactory -import org.jacodb.analysis.library.analyzers.NpePrecalcBackwardAnalyzerFactory -import org.jacodb.analysis.library.analyzers.SqlInjectionAnalyzerFactory -import org.jacodb.analysis.library.analyzers.SqlInjectionBackwardAnalyzerFactory -import org.jacodb.analysis.library.analyzers.TaintAnalysisNode -import org.jacodb.analysis.library.analyzers.TaintNode -import org.jacodb.analysis.library.analyzers.UnusedVariableAnalyzerFactory -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcInst - -//TODO: add docs here -val UnusedVariableRunnerFactory = BaseIfdsUnitRunnerFactory(UnusedVariableAnalyzerFactory) - -fun newSqlInjectionRunnerFactory(maxPathLength: Int = 5) = BidiIfdsUnitRunnerFactory( - BaseIfdsUnitRunnerFactory(SqlInjectionAnalyzerFactory(maxPathLength)), - BaseIfdsUnitRunnerFactory(SqlInjectionBackwardAnalyzerFactory(maxPathLength)), -) - -fun newNpeRunnerFactory(maxPathLength: Int = 5) = BidiIfdsUnitRunnerFactory( - BaseIfdsUnitRunnerFactory(NpeAnalyzerFactory(maxPathLength)), - BaseIfdsUnitRunnerFactory(NpePrecalcBackwardAnalyzerFactory(maxPathLength)), - isParallel = false -) - -fun newAliasRunnerFactory( - generates: (JcInst) -> List, - sanitizes: (JcExpr, TaintNode) -> Boolean, - sinks: (JcInst) -> List, - maxPathLength: Int = 5 -) = BaseIfdsUnitRunnerFactory(AliasAnalyzerFactory(generates, sanitizes, sinks, maxPathLength)) \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/UnitResolversLibrary.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/UnitResolversLibrary.kt deleted file mode 100644 index 67556e70b..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/UnitResolversLibrary.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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. - */ - -@file:JvmName("UnitResolversLibrary") -package org.jacodb.analysis.library - -import org.jacodb.analysis.engine.UnitResolver -import org.jacodb.api.JcClassOrInterface -import org.jacodb.api.JcMethod -import org.jacodb.api.ext.packageName - -val MethodUnitResolver = UnitResolver { method -> method } -val PackageUnitResolver = UnitResolver { method -> method.enclosingClass.packageName } -val SingletonUnitResolver = UnitResolver { _ -> Unit } - -fun getClassUnitResolver(includeNested: Boolean): UnitResolver { - return ClassUnitResolver(includeNested) -} - -private class ClassUnitResolver(private val includeNested: Boolean): UnitResolver { - override fun resolve(method: JcMethod): JcClassOrInterface { - return if (includeNested) { - generateSequence(method.enclosingClass) { it.outerClass }.last() - } else { - method.enclosingClass - } - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/AbstractTaintBackwardFunctions.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/AbstractTaintBackwardFunctions.kt deleted file mode 100644 index 5b21a6794..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/AbstractTaintBackwardFunctions.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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.analysis.library.analyzers - -import org.jacodb.analysis.engine.DomainFact -import org.jacodb.analysis.engine.FlowFunctionInstance -import org.jacodb.analysis.engine.FlowFunctionsSpace -import org.jacodb.analysis.engine.ZEROFact -import org.jacodb.analysis.paths.startsWith -import org.jacodb.analysis.paths.toPath -import org.jacodb.analysis.paths.toPathOrNull -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcAssignInst -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.cfg.JcInstanceCallExpr -import org.jacodb.api.cfg.JcReturnInst -import org.jacodb.api.cfg.JcValue -import org.jacodb.api.ext.cfg.callExpr - -abstract class AbstractTaintBackwardFunctions( - protected val graph: JcApplicationGraph, - protected val maxPathLength: Int, -) : FlowFunctionsSpace { - - override fun obtainPossibleStartFacts(startStatement: JcInst): Collection { - return listOf(ZEROFact) - } - - abstract fun transmitBackDataFlow(from: JcValue, to: JcExpr, atInst: JcInst, fact: DomainFact, dropFact: Boolean): List - - abstract fun transmitDataFlowAtNormalInst(inst: JcInst, nextInst: JcInst, fact: DomainFact): List - - override fun obtainSequentFlowFunction(current: JcInst, next: JcInst) = FlowFunctionInstance { fact -> - // fact.activation != current needed here to jump over assignment where the fact appeared - if (current is JcAssignInst && (fact !is TaintNode || fact.activation != current)) { - transmitBackDataFlow(current.lhv, current.rhv, current, fact, dropFact = false) - } else { - transmitDataFlowAtNormalInst(current, next, fact) - } - } - - override fun obtainCallToStartFlowFunction( - callStatement: JcInst, - callee: JcMethod - ): FlowFunctionInstance = FlowFunctionInstance { fact -> - val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") - - buildList { - // TODO: think about activation point handling for statics here - if (fact == ZEROFact || (fact is TaintNode && fact.variable.isStatic)) { - add(fact) - } - - if (callStatement is JcAssignInst) { - graph.entryPoint(callee).filterIsInstance().forEach { returnInst -> - returnInst.returnValue?.let { - addAll(transmitBackDataFlow(callStatement.lhv, it, callStatement, fact, dropFact = true)) - } - } - } - - if (callExpr is JcInstanceCallExpr) { - val thisInstance = callee.thisInstance - addAll(transmitBackDataFlow(callExpr.instance, thisInstance, callStatement, fact, dropFact = true)) - } - - val formalParams = graph.classpath.getFormalParamsOf(callee) - - callExpr.args.zip(formalParams).forEach { (actual, formal) -> - // FilterNot is needed for reasons described in comment for symmetric case in - // AbstractTaintForwardFunctions.obtainExitToReturnSiteFlowFunction - addAll(transmitBackDataFlow(actual, formal, callStatement, fact, dropFact = true) - .filterNot { it is TaintNode && !it.variable.isOnHeap }) - } - } - } - - override fun obtainCallToReturnFlowFunction( - callStatement: JcInst, - returnSite: JcInst - ): FlowFunctionInstance = FlowFunctionInstance { fact -> - if (fact !is TaintNode) { - return@FlowFunctionInstance if (fact == ZEROFact) { - listOf(fact) - } else { - emptyList() - } - } - - val factPath = fact.variable - val callExpr = callStatement.callExpr ?: error("CallStatement is expected to contain callExpr") - - // TODO: check that this is legal - if (fact.activation == callStatement) { - return@FlowFunctionInstance listOf(fact) - } - - if (fact.variable.isStatic) { - return@FlowFunctionInstance emptyList() - } - - callExpr.args.forEach { - if (fact.variable.startsWith(it.toPathOrNull())) { - return@FlowFunctionInstance emptyList() - } - } - - if (callExpr is JcInstanceCallExpr) { - if (factPath.startsWith(callExpr.instance.toPathOrNull())) { - return@FlowFunctionInstance emptyList() - } - } - - if (callStatement is JcAssignInst) { - val lhvPath = callStatement.lhv.toPath() - if (factPath.startsWith(lhvPath)) { - return@FlowFunctionInstance emptyList() - } - } - - transmitDataFlowAtNormalInst(callStatement, returnSite, fact) - } - - override fun obtainExitToReturnSiteFlowFunction( - callStatement: JcInst, - returnSite: JcInst, - exitStatement: JcInst - ): FlowFunctionInstance = FlowFunctionInstance { fact -> - val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") - val actualParams = callExpr.args - val callee = graph.methodOf(exitStatement) - val formalParams = graph.classpath.getFormalParamsOf(callee) - - buildList { - formalParams.zip(actualParams).forEach { (formal, actual) -> - addAll(transmitBackDataFlow(formal, actual, exitStatement, fact, dropFact = true)) - } - - if (callExpr is JcInstanceCallExpr) { - addAll(transmitBackDataFlow(callee.thisInstance, callExpr.instance, exitStatement, fact, dropFact = true)) - } - - if (fact is TaintNode && fact.variable.isStatic) { - add(fact) - } - } - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/AbstractTaintForwardFunctions.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/AbstractTaintForwardFunctions.kt deleted file mode 100644 index 3edc9efa6..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/AbstractTaintForwardFunctions.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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.analysis.library.analyzers - -import org.jacodb.analysis.engine.DomainFact -import org.jacodb.analysis.engine.FlowFunctionInstance -import org.jacodb.analysis.engine.FlowFunctionsSpace -import org.jacodb.analysis.engine.ZEROFact -import org.jacodb.analysis.paths.startsWith -import org.jacodb.analysis.paths.toPathOrNull -import org.jacodb.api.JcClasspath -import org.jacodb.api.JcMethod -import org.jacodb.api.cfg.JcAssignInst -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.cfg.JcInstanceCallExpr -import org.jacodb.api.cfg.JcReturnInst -import org.jacodb.api.cfg.JcValue -import org.jacodb.api.ext.cfg.callExpr - -abstract class AbstractTaintForwardFunctions( - protected val cp: JcClasspath -) : FlowFunctionsSpace { - - abstract fun transmitDataFlow(from: JcExpr, to: JcValue, atInst: JcInst, fact: DomainFact, dropFact: Boolean): List - - abstract fun transmitDataFlowAtNormalInst(inst: JcInst, nextInst: JcInst, fact: DomainFact): List - - override fun obtainSequentFlowFunction(current: JcInst, next: JcInst) = FlowFunctionInstance { fact -> - if (fact is TaintNode && fact.activation == current) { - listOf(fact.activatedCopy) - } else if (current is JcAssignInst) { - transmitDataFlow(current.rhv, current.lhv, current, fact, dropFact = false) - } else { - transmitDataFlowAtNormalInst(current, next, fact) - } - } - - override fun obtainCallToStartFlowFunction( - callStatement: JcInst, - callee: JcMethod - ) = FlowFunctionInstance { fact -> - if (fact is TaintNode && fact.activation == callStatement) { - return@FlowFunctionInstance emptyList() - } - - val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") - val actualParams = callExpr.args - val formalParams = cp.getFormalParamsOf(callee) - buildList { - formalParams.zip(actualParams).forEach { (formal, actual) -> - addAll(transmitDataFlow(actual, formal, callStatement, fact, dropFact = true)) - } - - if (callExpr is JcInstanceCallExpr) { - addAll(transmitDataFlow(callExpr.instance, callee.thisInstance, callStatement, fact, dropFact = true)) - } - - if (fact == ZEROFact || (fact is TaintNode && fact.variable.isStatic)) { - add(fact) - } - } - } - - override fun obtainCallToReturnFlowFunction( - callStatement: JcInst, - returnSite: JcInst - ) = FlowFunctionInstance { fact -> - if (fact == ZEROFact) { - return@FlowFunctionInstance listOf(fact) - } - - if (fact !is TaintNode || fact.variable.isStatic) { - return@FlowFunctionInstance emptyList() - } - - if (fact.activation == callStatement) { - return@FlowFunctionInstance listOf(fact.activatedCopy) - } - - val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") - val actualParams = callExpr.args - - actualParams.mapNotNull { it.toPathOrNull() }.forEach { - if (fact.variable.startsWith(it)) { - return@FlowFunctionInstance emptyList() // Will be handled by summary edge - } - } - - if (callExpr is JcInstanceCallExpr) { - if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { - return@FlowFunctionInstance emptyList() // Will be handled by summary edge - } - } - - if (callStatement is JcAssignInst && fact.variable.startsWith(callStatement.lhv.toPathOrNull())) { - return@FlowFunctionInstance emptyList() - } - - transmitDataFlowAtNormalInst(callStatement, returnSite, fact) - } - - override fun obtainExitToReturnSiteFlowFunction( - callStatement: JcInst, - returnSite: JcInst, - exitStatement: JcInst - ): FlowFunctionInstance = FlowFunctionInstance { fact -> - val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") - val actualParams = callExpr.args - val callee = exitStatement.location.method - // TODO: maybe we can always use fact instead of updatedFact here - val updatedFact = if (fact is TaintNode && fact.activation?.location?.method == callee) { - fact.updateActivation(callStatement) - } else { - fact - } - val formalParams = cp.getFormalParamsOf(callee) - - buildList { - if (fact is TaintNode && fact.variable.isOnHeap) { - // If there is some method A.f(formal: T) that is called like A.f(actual) then - // 1. For all g^k, k >= 1, we should propagate back from formal.g^k to actual.g^k (as they are on heap) - // 2. We shouldn't propagate from formal to actual (as formal is local) - // Second case is why we need check for isOnHeap - // TODO: add test for handling of 2nd case - formalParams.zip(actualParams).forEach { (formal, actual) -> - addAll(transmitDataFlow(formal, actual, exitStatement, updatedFact, dropFact = true)) - } - } - - if (callExpr is JcInstanceCallExpr) { - addAll(transmitDataFlow(callee.thisInstance, callExpr.instance, exitStatement, updatedFact, dropFact = true)) - } - - if (callStatement is JcAssignInst && exitStatement is JcReturnInst) { - exitStatement.returnValue?.let { // returnValue can be null here in some weird cases, for e.g. lambda - addAll(transmitDataFlow(it, callStatement.lhv, exitStatement, updatedFact, dropFact = true)) - } - } - - if (fact is TaintNode && fact.variable.isStatic) { - add(fact) - } - } - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/AliasAnalyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/AliasAnalyzer.kt deleted file mode 100644 index 7ceef46b9..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/AliasAnalyzer.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.analysis.library.analyzers - -import org.jacodb.analysis.engine.AnalysisDependentEvent -import org.jacodb.analysis.engine.AnalyzerFactory -import org.jacodb.analysis.engine.DomainFact -import org.jacodb.analysis.engine.IfdsResult -import org.jacodb.analysis.engine.IfdsVertex -import org.jacodb.analysis.sarif.VulnerabilityDescription -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcInst - -fun AliasAnalyzerFactory( - generates: (JcInst) -> List, - sanitizes: (JcExpr, TaintNode) -> Boolean, - sinks: (JcInst) -> List, - maxPathLength: Int = 5 -) = AnalyzerFactory { graph -> - AliasAnalyzer(graph, generates, sanitizes, sinks, maxPathLength) -} - -private class AliasAnalyzer( - graph: JcApplicationGraph, - override val generates: (JcInst) -> List, - override val sanitizes: (JcExpr, TaintNode) -> Boolean, - override val sinks: (JcInst) -> List, - maxPathLength: Int, -) : TaintAnalyzer(graph, maxPathLength) { - override fun generateDescriptionForSink(sink: IfdsVertex): VulnerabilityDescription = TODO() - - override fun handleIfdsResult(ifdsResult: IfdsResult): List = TODO() -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/NpeAnalyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/NpeAnalyzer.kt deleted file mode 100644 index b8af34e37..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/NpeAnalyzer.kt +++ /dev/null @@ -1,302 +0,0 @@ -/* - * 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.analysis.library.analyzers - -import org.jacodb.analysis.engine.AbstractAnalyzer -import org.jacodb.analysis.engine.AnalysisDependentEvent -import org.jacodb.analysis.engine.AnalyzerFactory -import org.jacodb.analysis.engine.CrossUnitCallFact -import org.jacodb.analysis.engine.DomainFact -import org.jacodb.analysis.engine.EdgeForOtherRunnerQuery -import org.jacodb.analysis.engine.FlowFunctionsSpace -import org.jacodb.analysis.engine.IfdsEdge -import org.jacodb.analysis.engine.NewSummaryFact -import org.jacodb.analysis.engine.VulnerabilityLocation -import org.jacodb.analysis.engine.ZEROFact -import org.jacodb.analysis.paths.AccessPath -import org.jacodb.analysis.paths.ElementAccessor -import org.jacodb.analysis.paths.FieldAccessor -import org.jacodb.analysis.paths.isDereferencedAt -import org.jacodb.analysis.paths.minus -import org.jacodb.analysis.paths.startsWith -import org.jacodb.analysis.paths.toPath -import org.jacodb.analysis.paths.toPathOrNull -import org.jacodb.analysis.sarif.SarifMessage -import org.jacodb.analysis.sarif.VulnerabilityDescription -import org.jacodb.api.JcArrayType -import org.jacodb.api.JcClasspath -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcArgument -import org.jacodb.api.cfg.JcCallExpr -import org.jacodb.api.cfg.JcConstant -import org.jacodb.api.cfg.JcEqExpr -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcIfInst -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.cfg.JcNeqExpr -import org.jacodb.api.cfg.JcNewArrayExpr -import org.jacodb.api.cfg.JcNewExpr -import org.jacodb.api.cfg.JcNullConstant -import org.jacodb.api.cfg.JcValue -import org.jacodb.api.cfg.locals -import org.jacodb.api.cfg.values -import org.jacodb.api.ext.fields -import org.jacodb.api.ext.isNullable - -fun NpeAnalyzerFactory(maxPathLength: Int) = AnalyzerFactory { graph -> - NpeAnalyzer(graph, maxPathLength) -} - -class NpeAnalyzer(graph: JcApplicationGraph, maxPathLength: Int) : AbstractAnalyzer(graph) { - override val flowFunctions: FlowFunctionsSpace = NpeForwardFunctions(graph.classpath, maxPathLength) - - override val isMainAnalyzer: Boolean - get() = true - - companion object { - const val ruleId: String = "npe-deref" - } - - override fun handleNewEdge(edge: IfdsEdge): List = buildList { - val (inst, fact0) = edge.v - - if (fact0 is NpeTaintNode && fact0.activation == null && fact0.variable.isDereferencedAt(inst)) { - val message = "Dereference of possibly-null ${fact0.variable}" - val desc = VulnerabilityDescription(SarifMessage(message), ruleId) - add(NewSummaryFact((VulnerabilityLocation(desc, edge.v)))) - verticesWithTraceGraphNeeded.add(edge.v) - } - - addAll(super.handleNewEdge(edge)) - } - - override fun handleNewCrossUnitCall(fact: CrossUnitCallFact): List = buildList { - add(EdgeForOtherRunnerQuery(IfdsEdge(fact.calleeVertex, fact.calleeVertex))) - addAll(super.handleNewCrossUnitCall(fact)) - } -} - -private class NpeForwardFunctions( - cp: JcClasspath, - private val maxPathLength: Int -) : AbstractTaintForwardFunctions(cp) { - - private val JcIfInst.pathComparedWithNull: AccessPath? - get() { - val expr = condition - return if (expr.rhv is JcNullConstant) { - expr.lhv.toPathOrNull()?.limit(maxPathLength) - } else if (expr.lhv is JcNullConstant) { - expr.rhv.toPathOrNull()?.limit(maxPathLength) - } else { - null - } - } - - override fun transmitDataFlow(from: JcExpr, to: JcValue, atInst: JcInst, fact: DomainFact, dropFact: Boolean): List { - val default = if (dropFact && fact != ZEROFact) emptyList() else listOf(fact) - val toPath = to.toPathOrNull()?.limit(maxPathLength) ?: return default - - if (fact == ZEROFact) { - return if (from is JcNullConstant || (from is JcCallExpr && from.method.method.treatAsNullable)) { - listOf(ZEROFact, NpeTaintNode(toPath)) // taint is generated here - } else if (from is JcNewArrayExpr && (from.type as JcArrayType).elementType.nullable != false) { - val arrayElemPath = AccessPath.fromOther(toPath, List((from.type as JcArrayType).dimensions) { ElementAccessor }) - listOf(ZEROFact, NpeTaintNode(arrayElemPath.limit(maxPathLength))) - } else { - listOf(ZEROFact) - } - } - - if (fact !is NpeTaintNode) { - return emptyList() - } - - val factPath = fact.variable - if (factPath.isDereferencedAt(atInst)) { - return emptyList() - } - - if (from is JcNewExpr || from is JcNewArrayExpr || from is JcConstant || (from is JcCallExpr && !from.method.method.treatAsNullable)) { - return if (factPath.startsWith(toPath)) { - emptyList() // new kills the fact here - } else { - default - } - } - - // TODO: slightly differs from original paper, think what's correct - val fromPath = from.toPathOrNull()?.limit(maxPathLength) ?: return default - return normalFactFlow(fact, fromPath, toPath, dropFact, maxPathLength) - } - - override fun transmitDataFlowAtNormalInst(inst: JcInst, nextInst: JcInst, fact: DomainFact): List { - val factPath = when (fact) { - is NpeTaintNode -> fact.variable - ZEROFact -> null - else -> return emptyList() - } - - if (factPath.isDereferencedAt(inst)) { - return emptyList() - } - - if (inst !is JcIfInst) { - return listOf(fact) - } - - // Following are some ad-hoc magic for if statements to change facts after instructions like if (x != null) - val nextInstIsTrueBranch = nextInst.location.index == inst.trueBranch.index - if (fact == ZEROFact) { - if (inst.pathComparedWithNull != null) { - if ((inst.condition is JcEqExpr && nextInstIsTrueBranch) || - (inst.condition is JcNeqExpr && !nextInstIsTrueBranch) - ) { - // This is a hack: instructions like `return null` in branch of next will be considered only if - // the fact holds (otherwise we could not get there) - return listOf(NpeTaintNode(inst.pathComparedWithNull!!)) - } - } - return listOf(ZEROFact) - } - - fact as NpeTaintNode - - // This handles cases like if (x != null) expr1 else expr2, where edges to expr1 and to expr2 should be different - // (because x == null will be held at expr2 but won't be held at expr1) - val expr = inst.condition - if (inst.pathComparedWithNull != fact.variable) { - return listOf(fact) - } - - return if ((expr is JcEqExpr && nextInstIsTrueBranch) || (expr is JcNeqExpr && !nextInstIsTrueBranch)) { - // comparedPath is null in this branch - listOf(ZEROFact) - } else { - emptyList() - } - } - - override fun obtainPossibleStartFacts(startStatement: JcInst): Collection { - val result = mutableListOf(ZEROFact) -// return result - - val method = startStatement.location.method - - // Note that here and below we intentionally don't expand fields because this may cause - // an increase of false positives and significant performance drop - - // Possibly null arguments - result += method.flowGraph().locals - .filterIsInstance() - .filter { method.parameters[it.index].isNullable != false } - .map { NpeTaintNode(AccessPath.fromLocal(it)) } - - // Possibly null statics - // TODO: handle statics in a more general manner - result += method.enclosingClass.fields - .filter { it.isNullable != false && it.isStatic } - .map { NpeTaintNode(AccessPath.fromStaticField(it)) } - - val thisInstance = method.thisInstance - - // Possibly null public non-final fields - result += method.enclosingClass.fields - .filter { it.isNullable != false && !it.isStatic && it.isPublic && !it.isFinal } - .map { - NpeTaintNode( - AccessPath.fromOther(AccessPath.fromLocal(thisInstance), listOf(FieldAccessor(it))) - ) - } - - return result - } -} - -fun NpePrecalcBackwardAnalyzerFactory(maxPathLength: Int) = AnalyzerFactory { graph -> - NpePrecalcBackwardAnalyzer(graph, maxPathLength) -} - -private class NpePrecalcBackwardAnalyzer(val graph: JcApplicationGraph, maxPathLength: Int) : AbstractAnalyzer(graph) { - override val flowFunctions: FlowFunctionsSpace = NpePrecalcBackwardFunctions(graph, maxPathLength) - - override val isMainAnalyzer: Boolean - get() = false - - override fun handleNewEdge(edge: IfdsEdge): List = buildList { - if (edge.v.statement in graph.exitPoints(edge.method)) { - add(EdgeForOtherRunnerQuery((IfdsEdge(edge.v, edge.v)))) - } - } -} - -class NpePrecalcBackwardFunctions(graph: JcApplicationGraph, maxPathLength: Int) - : AbstractTaintBackwardFunctions(graph, maxPathLength) { - override fun transmitBackDataFlow(from: JcValue, to: JcExpr, atInst: JcInst, fact: DomainFact, dropFact: Boolean): List { - val thisInstance = atInst.location.method.thisInstance.toPath() - if (fact == ZEROFact) { - val derefs = atInst.values - .mapNotNull { it.toPathOrNull() } - .filter { it.isDereferencedAt(atInst) } - .filterNot { it == thisInstance } - .map { NpeTaintNode(it) } - return listOf(ZEROFact) + derefs - } - - if (fact !is TaintNode) { - return emptyList() - } - - val factPath = (fact as? TaintNode)?.variable - val default = if (dropFact) emptyList() else listOf(fact) - val toPath = to.toPathOrNull() ?: return default - val fromPath = from.toPathOrNull() ?: return default - - val diff = factPath.minus(fromPath) - if (diff != null) { - return listOf(fact.moveToOtherPath(AccessPath.fromOther(toPath, diff).limit(maxPathLength))).filterNot { - it.variable == thisInstance - } - } - return default - } - - override fun transmitDataFlowAtNormalInst(inst: JcInst, nextInst: JcInst, fact: DomainFact): List = - listOf(fact) - - override fun obtainPossibleStartFacts(startStatement: JcInst): List { - val values = startStatement.values - return listOf(ZEROFact) + values - .mapNotNull { it.toPathOrNull() } - .filterNot { it == startStatement.location.method.thisInstance.toPath() } - .map { NpeTaintNode(it) } - } -} - -private val JcMethod.treatAsNullable: Boolean - get() { - if (isNullable == true) { - return true - } - return "${enclosingClass.name}.$name" in knownNullableMethods - } - -private val knownNullableMethods = listOf( - "java.lang.System.getProperty", - "java.util.Properties.getProperty" -) \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/SqlInjectionAnalyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/SqlInjectionAnalyzer.kt deleted file mode 100644 index 0b779cb67..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/SqlInjectionAnalyzer.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.analysis.library.analyzers - -import org.jacodb.analysis.engine.AnalyzerFactory -import org.jacodb.analysis.engine.IfdsVertex -import org.jacodb.analysis.sarif.SarifMessage -import org.jacodb.analysis.sarif.VulnerabilityDescription -import org.jacodb.api.analysis.JcApplicationGraph - -class SqlInjectionAnalyzer( - graph: JcApplicationGraph, - maxPathLength: Int -) : TaintAnalyzer(graph, maxPathLength) { - override val generates = isSourceMethodToGenerates(sqlSourceMatchers.asMethodMatchers) - override val sanitizes = isSanitizeMethodToSanitizes(sqlSanitizeMatchers.asMethodMatchers) - override val sinks = isSinkMethodToSinks(sqlSinkMatchers.asMethodMatchers) - - companion object { - private const val ruleId: String = "SQL-injection" - private val vulnerabilityMessage = SarifMessage("SQL query with unchecked injection") - - val vulnerabilityDescription = VulnerabilityDescription(vulnerabilityMessage, ruleId) - } - - override fun generateDescriptionForSink(sink: IfdsVertex): VulnerabilityDescription = vulnerabilityDescription -} - -class SqlInjectionBackwardAnalyzer( - graph: JcApplicationGraph, - maxPathLength: Int -) : TaintBackwardAnalyzer(graph, maxPathLength) { - override val generates = isSourceMethodToGenerates(sqlSourceMatchers.asMethodMatchers) - override val sinks = isSinkMethodToSinks(sqlSinkMatchers.asMethodMatchers) -} - -fun SqlInjectionAnalyzerFactory(maxPathLength: Int) = AnalyzerFactory { graph -> - SqlInjectionAnalyzer(graph, maxPathLength) -} - -fun SqlInjectionBackwardAnalyzerFactory(maxPathLength: Int) = AnalyzerFactory { graph -> - SqlInjectionBackwardAnalyzer(graph, maxPathLength) -} - -private val sqlSourceMatchers = listOf( - "java\\.io.+", - "java\\.lang\\.System\\#getenv", - "java\\.sql\\.ResultSet#get.+" -) - -private val sqlSanitizeMatchers = listOf( - "java\\.sql\\.Statement#set.*", - "java\\.sql\\.PreparedStatement#set.*" -) - -private val sqlSinkMatchers = listOf( - "java\\.sql\\.Statement#execute.*", - "java\\.sql\\.PreparedStatement#execute.*", -) \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/TaintAnalyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/TaintAnalyzer.kt deleted file mode 100644 index 3ed7b27b3..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/TaintAnalyzer.kt +++ /dev/null @@ -1,269 +0,0 @@ -/* - * 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.analysis.library.analyzers - -import org.jacodb.analysis.engine.AbstractAnalyzer -import org.jacodb.analysis.engine.AnalysisDependentEvent -import org.jacodb.analysis.engine.DomainFact -import org.jacodb.analysis.engine.EdgeForOtherRunnerQuery -import org.jacodb.analysis.engine.FlowFunctionsSpace -import org.jacodb.analysis.engine.IfdsEdge -import org.jacodb.analysis.engine.IfdsVertex -import org.jacodb.analysis.engine.NewSummaryFact -import org.jacodb.analysis.engine.VulnerabilityLocation -import org.jacodb.analysis.engine.ZEROFact -import org.jacodb.analysis.paths.AccessPath -import org.jacodb.analysis.paths.minus -import org.jacodb.analysis.paths.startsWith -import org.jacodb.analysis.paths.toPath -import org.jacodb.analysis.paths.toPathOrNull -import org.jacodb.analysis.sarif.VulnerabilityDescription -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcArgument -import org.jacodb.api.cfg.JcAssignInst -import org.jacodb.api.cfg.JcCallExpr -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.cfg.JcInstanceCallExpr -import org.jacodb.api.cfg.JcValue -import org.jacodb.api.cfg.locals -import org.jacodb.api.cfg.values -import org.jacodb.api.ext.cfg.callExpr - -fun isSourceMethodToGenerates(isSourceMethod: (JcMethod) -> Boolean): (JcInst) -> List { - return generates@{ inst: JcInst -> - val callExpr = inst.callExpr?.takeIf { isSourceMethod(it.method.method) } ?: return@generates emptyList() - if (inst is JcAssignInst && isSourceMethod(callExpr.method.method)) { - listOf(TaintAnalysisNode(inst.lhv.toPath())) - } else { - emptyList() - } - } -} - -fun isSinkMethodToSinks(isSinkMethod: (JcMethod) -> Boolean): (JcInst) -> List { - return sinks@{ inst: JcInst -> - val callExpr = inst.callExpr?.takeIf { isSinkMethod(it.method.method) } ?: return@sinks emptyList() - callExpr.values - .mapNotNull { it.toPathOrNull() } - .map { TaintAnalysisNode(it) } - } -} - -fun isSanitizeMethodToSanitizes(isSanitizeMethod: (JcMethod) -> Boolean): (JcExpr, TaintNode) -> Boolean { - return { expr: JcExpr, fact: TaintNode -> - if (expr !is JcCallExpr) { - false - } else { - if (isSanitizeMethod(expr.method.method) && fact.activation == null) { - expr.values.any { - it.toPathOrNull().startsWith(fact.variable) || fact.variable.startsWith(it.toPathOrNull()) - } - } else { - false - } - } - } -} - -internal val List.asMethodMatchers: (JcMethod) -> Boolean - get() = { method: JcMethod -> - any { it.toRegex().matches("${method.enclosingClass.name}#${method.name}") } - } - -abstract class TaintAnalyzer( - graph: JcApplicationGraph, - maxPathLength: Int -) : AbstractAnalyzer(graph) { - abstract val generates: (JcInst) -> List - abstract val sanitizes: (JcExpr, TaintNode) -> Boolean - abstract val sinks: (JcInst) -> List - - override val flowFunctions: FlowFunctionsSpace by lazy { - TaintForwardFunctions(graph, maxPathLength, generates, sanitizes) - } - - override val isMainAnalyzer: Boolean - get() = true - - protected abstract fun generateDescriptionForSink(sink: IfdsVertex): VulnerabilityDescription - - override fun handleNewEdge(edge: IfdsEdge): List = buildList { - if (edge.v.domainFact in sinks(edge.v.statement)) { - val desc = generateDescriptionForSink(edge.v) - add(NewSummaryFact(VulnerabilityLocation(desc, edge.v))) - verticesWithTraceGraphNeeded.add(edge.v) - } - } -} - -abstract class TaintBackwardAnalyzer( - val graph: JcApplicationGraph, - maxPathLength: Int -) : AbstractAnalyzer(graph) { - abstract val generates: (JcInst) -> List - abstract val sinks: (JcInst) -> List - - override val isMainAnalyzer: Boolean - get() = false - - override val flowFunctions: FlowFunctionsSpace by lazy { - TaintBackwardFunctions(graph, generates, sinks, maxPathLength) - } - - override fun handleNewEdge(edge: IfdsEdge): List = buildList { - if (edge.v.statement in graph.exitPoints(edge.method)) { - add(EdgeForOtherRunnerQuery(IfdsEdge(edge.v, edge.v))) - } - } -} - -private class TaintForwardFunctions( - graph: JcApplicationGraph, - private val maxPathLength: Int, - private val generates: (JcInst) -> List, - private val sanitizes: (JcExpr, TaintNode) -> Boolean -) : AbstractTaintForwardFunctions(graph.classpath) { - override fun transmitDataFlow(from: JcExpr, to: JcValue, atInst: JcInst, fact: DomainFact, dropFact: Boolean): List { - if (fact == ZEROFact) { - return listOf(ZEROFact) + generates(atInst) - } - - if (fact !is TaintNode) { - return emptyList() - } - - val default = if (dropFact || (sanitizes(from, fact) && fact.variable == (from as? JcInstanceCallExpr)?.instance?.toPath())) { - emptyList() - } else { - listOf(fact) - } - - val toPath = to.toPathOrNull()?.limit(maxPathLength) ?: return default - val newPossibleTaint = if (sanitizes(from, fact)) emptyList() else listOf(fact.moveToOtherPath(toPath)) - - val fromPath = from.toPathOrNull() - if (fromPath != null) { - return if (sanitizes(from, fact)) { - default - } else if (fromPath.startsWith(fact.variable)) { - default + newPossibleTaint - } else { - normalFactFlow(fact, fromPath, toPath, dropFact, maxPathLength) - } - } - - if (from.values.any { it.toPathOrNull().startsWith(fact.variable) || fact.variable.startsWith(it.toPathOrNull()) }) { - val instanceOrNull = (from as? JcInstanceCallExpr)?.instance - if (instanceOrNull != null && !sanitizes(from, fact)) { - val instancePath = instanceOrNull.toPathOrNull() - if (instancePath != null) { - return default + newPossibleTaint + fact.moveToOtherPath(instancePath) - } - } - return default + newPossibleTaint - } else if (fact.variable.startsWith(toPath)) { - return emptyList() - } - return default - } - - override fun transmitDataFlowAtNormalInst(inst: JcInst, nextInst: JcInst, fact: DomainFact): List { - if (fact == ZEROFact) { - return generates(inst) + listOf(ZEROFact) - } - - if (fact !is TaintNode) { - return emptyList() - } - - val callExpr = inst.callExpr ?: return listOf(fact) - val instance = (callExpr as? JcInstanceCallExpr)?.instance ?: return listOf(fact) - val factIsPassed = callExpr.values.any { - it.toPathOrNull().startsWith(fact.variable) || fact.variable.startsWith(it.toPathOrNull()) - } - - if (instance.toPath() == fact.variable && sanitizes(callExpr, fact)) { - return emptyList() - } - - return if (factIsPassed && !sanitizes(callExpr, fact)) { - listOf(fact) + fact.moveToOtherPath(instance.toPath()) - } else { - listOf(fact) - } - } - - override fun obtainPossibleStartFacts(startStatement: JcInst): Collection { - val method = startStatement.location.method - - // Possibly null arguments - return listOf(ZEROFact) + method.flowGraph().locals - .filterIsInstance() - .map { TaintAnalysisNode(AccessPath.fromLocal(it)) } - } -} - - -private class TaintBackwardFunctions( - graph: JcApplicationGraph, - val generates: (JcInst) -> List, - val sinks: (JcInst) -> List, - maxPathLength: Int, -) : AbstractTaintBackwardFunctions(graph, maxPathLength) { - override fun transmitBackDataFlow(from: JcValue, to: JcExpr, atInst: JcInst, fact: DomainFact, dropFact: Boolean): List { - if (fact == ZEROFact) { - return listOf(ZEROFact) + sinks(atInst) - } - - if (fact !is TaintAnalysisNode) { - return emptyList() - } - - val factPath = fact.variable - val default = if (dropFact || fact in generates(atInst)) emptyList() else listOf(fact) - val fromPath = from.toPathOrNull() ?: return default - val toPath = to.toPathOrNull() - - if (toPath != null) { - val diff = factPath.minus(fromPath) - if (diff != null) { - return listOf(fact.moveToOtherPath(AccessPath.fromOther(toPath, diff).limit(maxPathLength))) - } - } else if (factPath.startsWith(fromPath) || (to is JcInstanceCallExpr && factPath.startsWith(to.instance.toPath()))) { - return to.values.mapNotNull { it.toPathOrNull() }.map { TaintAnalysisNode(it) } - } - return default - } - - override fun transmitDataFlowAtNormalInst(inst: JcInst, nextInst: JcInst, fact: DomainFact): List { - if (fact == ZEROFact) { - return listOf(fact) + sinks(inst) - } - if (fact !is TaintAnalysisNode) { - return emptyList() - } - - val callExpr = inst.callExpr as? JcInstanceCallExpr ?: return listOf(fact) - if (fact.variable.startsWith(callExpr.instance.toPath())) { - return inst.values.mapNotNull { it.toPathOrNull() }.map { TaintAnalysisNode(it) } - } - - return listOf(fact) - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/TaintNode.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/TaintNode.kt deleted file mode 100644 index fcb815c6d..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/TaintNode.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.analysis.library.analyzers - -import org.jacodb.analysis.engine.DomainFact -import org.jacodb.analysis.paths.AccessPath -import org.jacodb.api.cfg.JcInst - -/** - * Abstract implementation for [DomainFact] that can be used for analysis where dataflow facts correlate with - * variables/values - * - * @property activation is the activation point, as described in ARF14. Null value means that activation point was - * passed (so, for analyses that do not use backward runner to taint aliases, [activation] will always be null). - */ -abstract class TaintNode(val variable: AccessPath, val activation: JcInst? = null): DomainFact { - protected abstract val nodeType: String - - abstract fun updateActivation(newActivation: JcInst?): TaintNode - - abstract fun moveToOtherPath(newPath: AccessPath): TaintNode - - val activatedCopy: TaintNode - get() = updateActivation(null) - - override fun toString(): String { - return "[$nodeType]: $variable, activation point=$activation" - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TaintNode - - if (variable != other.variable) return false - if (activation != other.activation) return false - - return true - } - - override fun hashCode(): Int { - var result = variable.hashCode() - result = 31 * result + (activation?.hashCode() ?: 0) - return result - } -} - -class NpeTaintNode(variable: AccessPath, activation: JcInst? = null): TaintNode(variable, activation) { - override val nodeType: String - get() = "NPE" - - override fun updateActivation(newActivation: JcInst?): NpeTaintNode { - return NpeTaintNode(variable, newActivation) - } - - override fun moveToOtherPath(newPath: AccessPath): TaintNode { - return NpeTaintNode(newPath, activation) - } -} - -data class UnusedVariableNode(val variable: AccessPath, val initStatement: JcInst): DomainFact - -class TaintAnalysisNode(variable: AccessPath, activation: JcInst? = null): TaintNode(variable, activation) { - override val nodeType: String - get() = "Taint analysis" - - override fun updateActivation(newActivation: JcInst?): TaintAnalysisNode { - return TaintAnalysisNode(variable, newActivation) - } - - override fun moveToOtherPath(newPath: AccessPath): TaintNode { - return TaintAnalysisNode(newPath, activation) - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/UnusedVariableAnalyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/UnusedVariableAnalyzer.kt deleted file mode 100644 index 2b7086c9a..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/UnusedVariableAnalyzer.kt +++ /dev/null @@ -1,194 +0,0 @@ -/* - * 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.analysis.library.analyzers - -import org.jacodb.analysis.engine.AbstractAnalyzer -import org.jacodb.analysis.engine.AnalysisDependentEvent -import org.jacodb.analysis.engine.AnalyzerFactory -import org.jacodb.analysis.engine.CrossUnitCallFact -import org.jacodb.analysis.engine.DomainFact -import org.jacodb.analysis.engine.FlowFunctionInstance -import org.jacodb.analysis.engine.FlowFunctionsSpace -import org.jacodb.analysis.engine.IfdsResult -import org.jacodb.analysis.engine.IfdsVertex -import org.jacodb.analysis.engine.NewSummaryFact -import org.jacodb.analysis.engine.VulnerabilityLocation -import org.jacodb.analysis.engine.ZEROFact -import org.jacodb.analysis.paths.AccessPath -import org.jacodb.analysis.paths.toPath -import org.jacodb.analysis.paths.toPathOrNull -import org.jacodb.analysis.sarif.SarifMessage -import org.jacodb.analysis.sarif.VulnerabilityDescription -import org.jacodb.api.JcClasspath -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcArrayAccess -import org.jacodb.api.cfg.JcAssignInst -import org.jacodb.api.cfg.JcBranchingInst -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.cfg.JcLocal -import org.jacodb.api.cfg.JcSpecialCallExpr -import org.jacodb.api.cfg.JcStaticCallExpr -import org.jacodb.api.cfg.JcTerminatingInst -import org.jacodb.api.cfg.values -import org.jacodb.api.ext.cfg.callExpr - - -class UnusedVariableAnalyzer(val graph: JcApplicationGraph) : AbstractAnalyzer(graph) { - override val flowFunctions: FlowFunctionsSpace = UnusedVariableForwardFunctions(graph.classpath) - - override val isMainAnalyzer: Boolean - get() = true - - companion object { - const val ruleId: String = "unused-variable" - private val vulnerabilityMessage = SarifMessage("Assigned value is unused") - - val vulnerabilityDescription = VulnerabilityDescription(vulnerabilityMessage, ruleId) - } - - private fun AccessPath.isUsedAt(expr: JcExpr): Boolean { - return this in expr.values.map { it.toPathOrNull() } - } - - private fun AccessPath.isUsedAt(inst: JcInst): Boolean { - val callExpr = inst.callExpr - - if (callExpr != null) { - // Don't count constructor calls as usages - if (callExpr.method.method.isConstructor && isUsedAt((callExpr as JcSpecialCallExpr).instance)) { - return false - } - - return isUsedAt(callExpr) - } - if (inst is JcAssignInst) { - if (inst.lhv is JcArrayAccess && isUsedAt((inst.lhv as JcArrayAccess))) { - return true - } - return isUsedAt(inst.rhv) && (inst.lhv !is JcLocal || inst.rhv !is JcLocal) - } - if (inst is JcTerminatingInst || inst is JcBranchingInst) { - return inst.operands.any { isUsedAt(it) } - } - return false - } - - override fun handleNewCrossUnitCall(fact: CrossUnitCallFact): List { - return emptyList() - } - - override fun handleIfdsResult(ifdsResult: IfdsResult): List = buildList { - val used: MutableMap = mutableMapOf() - ifdsResult.resultFacts.forEach { (inst, facts) -> - facts.filterIsInstance().forEach { fact -> - if (fact.initStatement !in used) { - used[fact.initStatement] = false - } - - if (fact.variable.isUsedAt(inst)) { - used[fact.initStatement] = true - } - } - } - used.filterValues { !it }.keys.map { - add( - NewSummaryFact(VulnerabilityLocation(vulnerabilityDescription, IfdsVertex(it, ZEROFact))) - ) - } - } -} - -val UnusedVariableAnalyzerFactory = AnalyzerFactory { graph -> - UnusedVariableAnalyzer(graph) -} - -private class UnusedVariableForwardFunctions( - val classpath: JcClasspath -) : FlowFunctionsSpace { - - override fun obtainPossibleStartFacts(startStatement: JcInst): Collection { - return listOf(ZEROFact) - } - - override fun obtainSequentFlowFunction(current: JcInst, next: JcInst) = FlowFunctionInstance { fact -> - if (current !is JcAssignInst) { - return@FlowFunctionInstance listOf(fact) - } - - if (fact == ZEROFact) { - val toPath = current.lhv.toPathOrNull() ?: return@FlowFunctionInstance listOf(ZEROFact) - return@FlowFunctionInstance if (!toPath.isOnHeap) { - listOf(ZEROFact, UnusedVariableNode(toPath, current)) - } else { - listOf(ZEROFact) - } - } - - if (fact !is UnusedVariableNode) { - return@FlowFunctionInstance emptyList() - } - - val default = if (fact.variable == current.lhv.toPathOrNull()) emptyList() else listOf(fact) - val fromPath = current.rhv.toPathOrNull() ?: return@FlowFunctionInstance default - val toPath = current.lhv.toPathOrNull() ?: return@FlowFunctionInstance default - - if (fromPath.isOnHeap || toPath.isOnHeap) { - return@FlowFunctionInstance default - } - - if (fromPath == fact.variable) { - return@FlowFunctionInstance default.plus(UnusedVariableNode(toPath, fact.initStatement)) - } - default - } - - override fun obtainCallToStartFlowFunction(callStatement: JcInst, callee: JcMethod) = FlowFunctionInstance { fact -> - val callExpr = callStatement.callExpr ?: error("Call expr is expected to be not-null") - val formalParams = classpath.getFormalParamsOf(callee) - - if (fact == ZEROFact) { - // We don't show unused parameters for virtual calls - if (callExpr !is JcStaticCallExpr && callExpr !is JcSpecialCallExpr) { - return@FlowFunctionInstance listOf(ZEROFact) - } - return@FlowFunctionInstance formalParams.map { UnusedVariableNode(it.toPath(), callStatement) }.plus(ZEROFact) - } - - if (fact !is UnusedVariableNode) { - return@FlowFunctionInstance emptyList() - } - - emptyList() - } - - override fun obtainCallToReturnFlowFunction(callStatement: JcInst, returnSite: JcInst) = - obtainSequentFlowFunction(callStatement, returnSite) - - override fun obtainExitToReturnSiteFlowFunction( - callStatement: JcInst, - returnSite: JcInst, - exitStatement: JcInst - ) = FlowFunctionInstance { fact -> - if (fact == ZEROFact) { - listOf(ZEROFact) - } else { - emptyList() - } - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/Util.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/Util.kt deleted file mode 100644 index ce5b20210..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/library/analyzers/Util.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.analysis.library.analyzers - -import org.jacodb.analysis.paths.AccessPath -import org.jacodb.analysis.paths.minus -import org.jacodb.analysis.paths.startsWith -import org.jacodb.api.JcClasspath -import org.jacodb.api.JcMethod -import org.jacodb.api.cfg.JcArgument -import org.jacodb.api.cfg.JcThis -import org.jacodb.api.ext.toType - -val JcMethod.thisInstance: JcThis - get() = JcThis(enclosingClass.toType()) - -fun JcClasspath.getFormalParamsOf(method: JcMethod): List { - return method.parameters.map { - JcArgument.of(it.index, it.name, findTypeOrNull(it.type.typeName)!!) - } -} - -fun normalFactFlow(fact: TaintNode, fromPath: AccessPath, toPath: AccessPath, dropFact: Boolean, maxPathLength: Int): List { - val factPath = fact.variable - val default = if (dropFact) emptyList() else listOf(fact) - - // Second clause is important here as it saves from false positive aliases, see - // #AnalysisTest.`dereferencing copy of value saved before null assignment produce no npe` - val diff = factPath.minus(fromPath) - if (diff != null && (fact.activation == null || fromPath != factPath)) { - return default - .plus(fact.moveToOtherPath(AccessPath.fromOther(toPath, diff).limit(maxPathLength))) - .distinct() - } - - if (factPath.startsWith(toPath)) { - return emptyList() - } - - return default -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeAnalyzers.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeAnalyzers.kt new file mode 100644 index 000000000..ccd671aa4 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeAnalyzers.kt @@ -0,0 +1,106 @@ +/* + * 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.analysis.npe + +import org.jacodb.analysis.config.CallPositionToJcValueResolver +import org.jacodb.analysis.config.FactAwareConditionEvaluator +import org.jacodb.analysis.ifds.Analyzer +import org.jacodb.analysis.ifds.Reason +import org.jacodb.analysis.taint.EdgeForOtherRunner +import org.jacodb.analysis.taint.NewSummaryEdge +import org.jacodb.analysis.taint.NewVulnerability +import org.jacodb.analysis.taint.TaintEdge +import org.jacodb.analysis.taint.TaintEvent +import org.jacodb.analysis.taint.TaintDomainFact +import org.jacodb.analysis.taint.TaintVertex +import org.jacodb.analysis.taint.Tainted +import org.jacodb.analysis.taint.TaintVulnerability +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.ext.cfg.callExpr +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.jacodb.taint.configuration.TaintMark +import org.jacodb.taint.configuration.TaintMethodSink + +private val logger = mu.KotlinLogging.logger {} + +class NpeAnalyzer( + private val graph: JcApplicationGraph, +) : Analyzer { + + override val flowFunctions: ForwardNpeFlowFunctions by lazy { + ForwardNpeFlowFunctions(graph.classpath, graph) + } + + private val taintConfigurationFeature: TaintConfigurationFeature? + get() = flowFunctions.taintConfigurationFeature + + private fun isExitPoint(statement: JcInst): Boolean { + return statement in graph.exitPoints(statement.location.method) + } + + override fun handleNewEdge( + edge: TaintEdge, + ): List = buildList { + if (isExitPoint(edge.to.statement)) { + add(NewSummaryEdge(edge)) + } + + if (edge.to.fact is Tainted && edge.to.fact.mark == TaintMark.NULLNESS) { + if (edge.to.fact.variable.isDereferencedAt(edge.to.statement)) { + val message = "NPE" // TODO + val vulnerability = TaintVulnerability(message, sink = edge.to) + logger.info { "Found sink=${vulnerability.sink} in ${vulnerability.method}" } + add(NewVulnerability(vulnerability)) + } + } + + run { + val callExpr = edge.to.statement.callExpr ?: return@run + val callee = callExpr.method.method + + val config = taintConfigurationFeature?.getConfigForMethod(callee) ?: return@run + + // TODO: not always we want to skip sinks on Zero facts. + // Some rules might have ConstantTrue or just true (when evaluated with Zero fact) condition. + if (edge.to.fact !is Tainted) { + return@run + } + + // Determine whether 'edge.to' is a sink via config: + val conditionEvaluator = FactAwareConditionEvaluator( + edge.to.fact, + CallPositionToJcValueResolver(edge.to.statement), + ) + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + logger.trace { "Found sink at ${edge.to} in ${edge.method} on $item" } + val message = item.ruleNote + val vulnerability = TaintVulnerability(message, sink = edge.to, rule = item) + add(NewVulnerability(vulnerability)) + } + } + } + } + + override fun handleCrossUnitCall( + caller: TaintVertex, + callee: TaintVertex, + ): List = buildList { + add(EdgeForOtherRunner(TaintEdge(callee, callee), Reason.CrossUnitCall(caller))) + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeFlowFunctions.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeFlowFunctions.kt new file mode 100644 index 000000000..f64c922a4 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeFlowFunctions.kt @@ -0,0 +1,625 @@ +/* + * 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.analysis.npe + +import org.jacodb.analysis.config.BasicConditionEvaluator +import org.jacodb.analysis.config.CallPositionToAccessPathResolver +import org.jacodb.analysis.config.CallPositionToJcValueResolver +import org.jacodb.analysis.config.EntryPointPositionToAccessPathResolver +import org.jacodb.analysis.config.EntryPointPositionToJcValueResolver +import org.jacodb.analysis.config.FactAwareConditionEvaluator +import org.jacodb.analysis.config.TaintActionEvaluator +import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.analysis.ifds.ElementAccessor +import org.jacodb.analysis.ifds.FlowFunction +import org.jacodb.analysis.ifds.FlowFunctions +import org.jacodb.analysis.ifds.onSome +import org.jacodb.analysis.ifds.toPath +import org.jacodb.analysis.ifds.toPathOrNull +import org.jacodb.analysis.taint.TaintDomainFact +import org.jacodb.analysis.taint.TaintZeroFact +import org.jacodb.analysis.taint.Tainted +import org.jacodb.analysis.util.getArgumentsOf +import org.jacodb.analysis.util.startsWith +import org.jacodb.analysis.util.thisInstance +import org.jacodb.api.JcArrayType +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcArgument +import org.jacodb.api.cfg.JcAssignInst +import org.jacodb.api.cfg.JcCallExpr +import org.jacodb.api.cfg.JcDynamicCallExpr +import org.jacodb.api.cfg.JcEqExpr +import org.jacodb.api.cfg.JcExpr +import org.jacodb.api.cfg.JcIfInst +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.cfg.JcInstanceCallExpr +import org.jacodb.api.cfg.JcNeqExpr +import org.jacodb.api.cfg.JcNewArrayExpr +import org.jacodb.api.cfg.JcNullConstant +import org.jacodb.api.cfg.JcReturnInst +import org.jacodb.api.cfg.JcThis +import org.jacodb.api.cfg.JcValue +import org.jacodb.api.ext.cfg.callExpr +import org.jacodb.api.ext.findTypeOrNull +import org.jacodb.api.ext.isNullable +import org.jacodb.taint.configuration.AssignMark +import org.jacodb.taint.configuration.CopyAllMarks +import org.jacodb.taint.configuration.CopyMark +import org.jacodb.taint.configuration.RemoveAllMarks +import org.jacodb.taint.configuration.RemoveMark +import org.jacodb.taint.configuration.TaintCleaner +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.jacodb.taint.configuration.TaintEntryPointSource +import org.jacodb.taint.configuration.TaintMark +import org.jacodb.taint.configuration.TaintMethodSource +import org.jacodb.taint.configuration.TaintPassThrough + +private val logger = mu.KotlinLogging.logger {} + +class ForwardNpeFlowFunctions( + private val cp: JcClasspath, + private val graph: JcApplicationGraph, +) : FlowFunctions { + + internal val taintConfigurationFeature: TaintConfigurationFeature? by lazy { + cp.features + ?.singleOrNull { it is TaintConfigurationFeature } + ?.let { it as TaintConfigurationFeature } + } + + override fun obtainPossibleStartFacts( + method: JcMethod, + ): Collection = buildSet { + addAll(obtainPossibleStartFactsBasic(method)) + + // Possibly null arguments: + for (p in method.parameters.filter { it.isNullable != false }) { + val t = cp.findTypeOrNull(p.type)!! + val arg = JcArgument.of(p.index, p.name, t) + val path = arg.toPath() + add(Tainted(path, TaintMark.NULLNESS)) + } + } + + private fun obtainPossibleStartFactsBasic( + method: JcMethod, + ): Collection = buildSet { + // Zero (reachability) fact always present at entrypoint: + add(TaintZeroFact) + + // Extract initial facts from the config: + val config = taintConfigurationFeature?.getConfigForMethod(method) + if (config != null) { + val conditionEvaluator = BasicConditionEvaluator(EntryPointPositionToJcValueResolver(cp, method)) + val actionEvaluator = TaintActionEvaluator(EntryPointPositionToAccessPathResolver(cp, method)) + + // Handle EntryPointSource config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is AssignMark -> actionEvaluator.evaluate(action) + else -> error("$action is not supported for $item") + } + result.onSome { addAll(it) } + } + } + } + } + } + + private fun transmitTaintAssign( + fact: Tainted, + from: JcExpr, + to: JcValue, + ): Collection { + val toPath = to.toPath() + val fromPath = from.toPathOrNull() + + if (fact.mark == TaintMark.NULLNESS) { + // TODO: consider + // if (from is JcNewExpr + // || from is JcNewArrayExpr + // || from is JcConstant + // || (from is JcCallExpr && from.method.method.isNullable != true)) + if (fact.variable.startsWith(toPath)) { + // NULLNESS is overridden: + return emptySet() + } + } + + if (fromPath != null) { + // Adhoc taint array: + if (fromPath.accesses.isNotEmpty() + && fromPath.accesses.last() is ElementAccessor + && fromPath == (fact.variable / ElementAccessor) + ) { + val newTaint = fact.copy(variable = toPath) + return setOf(fact, newTaint) + } + + val tail = fact.variable - fromPath + if (tail != null) { + // Both 'from' and 'to' are tainted now: + val newPath = toPath / tail + val newTaint = fact.copy(variable = newPath) + return setOf(fact, newTaint) + } + } + + return buildSet { + if (from is JcNullConstant) { + add(Tainted(toPath, TaintMark.NULLNESS)) + } + + if (fact.variable.startsWith(toPath)) { + // 'to' was (sub-)tainted, but it is now overridden by 'from': + return@buildSet + } else { + // Neither 'from' nor 'to' are tainted: + add(fact) + } + } + } + + private fun transmitTaintNormal( + fact: Tainted, + inst: JcInst, + ): List { + // Pass-through: + return listOf(fact) + } + + private fun generates(inst: JcInst): Collection = buildList { + if (inst is JcAssignInst) { + val toPath = inst.lhv.toPath() + val from = inst.rhv + if (from is JcNullConstant || (from is JcCallExpr && from.method.method.isNullable == true)) { + add(Tainted(toPath, TaintMark.NULLNESS)) + } else if (from is JcNewArrayExpr && (from.type as JcArrayType).elementType.nullable != false) { + val accessors = List((from.type as JcArrayType).dimensions) { ElementAccessor } + val path = toPath / accessors + add(Tainted(path, TaintMark.NULLNESS)) + } + } + } + + private val JcIfInst.pathComparedWithNull: AccessPath? + get() { + val expr = condition + return if (expr.rhv is JcNullConstant) { + expr.lhv.toPathOrNull() + } else if (expr.lhv is JcNullConstant) { + expr.rhv.toPathOrNull() + } else { + null + } + } + + override fun obtainSequentFlowFunction( + current: JcInst, + next: JcInst, + ) = FlowFunction { fact -> + if (fact is Tainted && fact.mark == TaintMark.NULLNESS) { + if (fact.variable.isDereferencedAt(current)) { + return@FlowFunction emptySet() + } + } + + if (current is JcIfInst) { + val nextIsTrueBranch = next.location.index == current.trueBranch.index + val pathComparedWithNull = current.pathComparedWithNull + if (fact == TaintZeroFact) { + if (pathComparedWithNull != null) { + if ((current.condition is JcEqExpr && nextIsTrueBranch) || + (current.condition is JcNeqExpr && !nextIsTrueBranch) + ) { + // This is a hack: instructions like `return null` in branch of next will be considered only if + // the fact holds (otherwise we could not get there) + // Note the absence of 'Zero' here! + return@FlowFunction listOf(Tainted(pathComparedWithNull, TaintMark.NULLNESS)) + } + } + } else if (fact is Tainted && fact.mark == TaintMark.NULLNESS) { + val expr = current.condition + if (pathComparedWithNull != fact.variable) { + return@FlowFunction listOf(fact) + } + if ((expr is JcEqExpr && nextIsTrueBranch) || (expr is JcNeqExpr && !nextIsTrueBranch)) { + // comparedPath is null in this branch + return@FlowFunction listOf(TaintZeroFact) + } else { + return@FlowFunction emptyList() + } + } + } + + if (fact is TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + generates(current) + } + check(fact is Tainted) + + if (current is JcAssignInst) { + transmitTaintAssign(fact, from = current.rhv, to = current.lhv) + } else { + transmitTaintNormal(fact, current) + } + } + + private fun transmitTaint( + fact: Tainted, + at: JcInst, + from: JcValue, + to: JcValue, + ): Collection = buildSet { + if (fact.mark == TaintMark.NULLNESS) { + if (fact.variable.isDereferencedAt(at)) { + return@buildSet + } + } + + val fromPath = from.toPath() + val toPath = to.toPath() + + val tail = (fact.variable - fromPath) ?: return@buildSet + val newPath = toPath / tail + val newTaint = fact.copy(variable = newPath) + add(newTaint) + } + + private fun transmitTaintArgumentActualToFormal( + fact: Tainted, + at: JcInst, + from: JcValue, // actual + to: JcValue, // formal + ): Collection = transmitTaint(fact, at, from, to) + + private fun transmitTaintArgumentFormalToActual( + fact: Tainted, + at: JcInst, + from: JcValue, // formal + to: JcValue, // actual + ): Collection = transmitTaint(fact, at, from, to) + + private fun transmitTaintInstanceToThis( + fact: Tainted, + at: JcInst, + from: JcValue, // instance + to: JcThis, // this + ): Collection = transmitTaint(fact, at, from, to) + + private fun transmitTaintThisToInstance( + fact: Tainted, + at: JcInst, + from: JcThis, // this + to: JcValue, // instance + ): Collection = transmitTaint(fact, at, from, to) + + private fun transmitTaintReturn( + fact: Tainted, + at: JcInst, + from: JcValue, + to: JcValue, + ): Collection = transmitTaint(fact, at, from, to) + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, // FIXME: unused? + ) = FlowFunction { fact -> + if (fact is Tainted && fact.mark == TaintMark.NULLNESS) { + if (fact.variable.isDereferencedAt(callStatement)) { + return@FlowFunction emptySet() + } + } + + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + val callee = callExpr.method.method + + // FIXME: handle taint pass-through on invokedynamic-based String concatenation: + if (fact is Tainted + && callExpr is JcDynamicCallExpr + && callee.enclosingClass.name == "java.lang.invoke.StringConcatFactory" + && callStatement is JcAssignInst + ) { + for (arg in callExpr.args) { + if (arg.toPath() == fact.variable) { + return@FlowFunction setOf( + fact, + fact.copy(variable = callStatement.lhv.toPath()) + ) + } + } + return@FlowFunction setOf(fact) + } + + val config = taintConfigurationFeature?.getConfigForMethod(callee) + + if (fact == TaintZeroFact) { + return@FlowFunction buildSet { + add(TaintZeroFact) + + if (callStatement is JcAssignInst) { + val toPath = callStatement.lhv.toPath() + val from = callStatement.rhv + if (from is JcNullConstant || (from is JcCallExpr && from.method.method.isNullable == true)) { + add(Tainted(toPath, TaintMark.NULLNESS)) + } else if (from is JcNewArrayExpr && (from.type as JcArrayType).elementType.nullable != false) { + val size = (from.type as JcArrayType).dimensions + val accessors = List(size) { ElementAccessor } + val path = toPath / accessors + add(Tainted(path, TaintMark.NULLNESS)) + } + } + + if (config != null) { + val conditionEvaluator = BasicConditionEvaluator(CallPositionToJcValueResolver(callStatement)) + val actionEvaluator = TaintActionEvaluator(CallPositionToAccessPathResolver(callStatement)) + + // Handle MethodSource config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is AssignMark -> actionEvaluator.evaluate(action) + else -> error("$action is not supported for $item") + } + result.onSome { + addAll(it) + } + } + } + } + } + } + } + check(fact is Tainted) + + if (config != null) { + // FIXME: adhoc + if (callee.enclosingClass.name == "java.lang.StringBuilder" && callee.name == "append") { + // Skip rules for StringBuilder::append in NPE analysis. + } else { + val facts = mutableSetOf() + val conditionEvaluator = FactAwareConditionEvaluator(fact, CallPositionToJcValueResolver(callStatement)) + val actionEvaluator = TaintActionEvaluator(CallPositionToAccessPathResolver(callStatement)) + var defaultBehavior = true + + // Handle PassThrough config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is CopyMark -> actionEvaluator.evaluate(action, fact) + is CopyAllMarks -> actionEvaluator.evaluate(action, fact) + is RemoveMark -> actionEvaluator.evaluate(action, fact) + is RemoveAllMarks -> actionEvaluator.evaluate(action, fact) + else -> error("$action is not supported for $item") + } + result.onSome { + facts += it + defaultBehavior = false + } + } + } + } + + // Handle Cleaner config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is RemoveMark -> actionEvaluator.evaluate(action, fact) + is RemoveAllMarks -> actionEvaluator.evaluate(action, fact) + else -> error("$action is not supported for $item") + } + result.onSome { + facts += it + defaultBehavior = false + } + } + } + } + + if (!defaultBehavior) { + if (facts.size > 0) { + logger.trace { "Got ${facts.size} facts from config for $callee: $facts" } + } + return@FlowFunction facts + } else { + // Fall back to the default behavior, as if there were no config at all. + } + } + } + + // FIXME: adhoc for constructors: + if (callee.isConstructor) { + return@FlowFunction listOf(fact) + } + + // TODO: CONSIDER REFACTORING THIS + // Default behavior for "analyzable" method calls is to remove ("temporarily") + // all the marks from the 'instance' and arguments, in order to allow them "pass through" + // the callee (when it is going to be analyzed), i.e. through "call-to-start" and + // "exit-to-return" flow functions. + // When we know that we are NOT going to analyze the callee, we do NOT need + // to remove any marks from 'instance' and arguments. + // Currently, "analyzability" of the callee depends on the fact that the callee + // is "accessible" through the JcApplicationGraph::callees(). + if (callee in graph.callees(callStatement)) { + + if (fact.variable.isStatic) { + return@FlowFunction emptyList() + } + + for (actual in callExpr.args) { + // Possibly tainted actual parameter: + if (fact.variable.startsWith(actual.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + if (callExpr is JcInstanceCallExpr) { + // Possibly tainted instance: + if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + } + + if (callStatement is JcAssignInst) { + // Possibly tainted lhv: + if (fact.variable.startsWith(callStatement.lhv.toPathOrNull())) { + return@FlowFunction emptyList() // Overridden by rhv + } + } + + // The "most default" behaviour is encapsulated here: + transmitTaintNormal(fact, callStatement) + } + + override fun obtainCallToStartFlowFunction( + callStatement: JcInst, + calleeStart: JcInst, + ) = FlowFunction { fact -> + val callee = calleeStart.location.method + + if (fact == TaintZeroFact) { + return@FlowFunction obtainPossibleStartFactsBasic(callee) + } + check(fact is Tainted) + + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + + buildSet { + // Transmit facts on arguments (from 'actual' to 'formal'): + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll( + transmitTaintArgumentActualToFormal( + fact = fact, + at = callStatement, + from = actual, + to = formal + ) + ) + } + + // Transmit facts on instance (from 'instance' to 'this'): + if (callExpr is JcInstanceCallExpr) { + addAll( + transmitTaintInstanceToThis( + fact = fact, + at = callStatement, + from = callExpr.instance, + to = callee.thisInstance + ) + ) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + } + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, // unused + exitStatement: JcInst, + ) = FlowFunction { fact -> + // TODO: do we even need to return non-empty list for zero fact here? + if (fact == TaintZeroFact) { + // return@FlowFunction listOf(Zero) + return@FlowFunction buildSet { + add(TaintZeroFact) + if (exitStatement is JcReturnInst && callStatement is JcAssignInst) { + // Note: returnValue can be null here in some weird cases, e.g. in lambda. + exitStatement.returnValue?.let { returnValue -> + if (returnValue is JcNullConstant) { + val toPath = callStatement.lhv.toPath() + add(Tainted(toPath, TaintMark.NULLNESS)) + } + } + } + } + } + check(fact is Tainted) + + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + val callee = exitStatement.location.method + + buildSet { + // Transmit facts on arguments (from 'formal' back to 'actual'), if they are passed by-ref: + if (fact.variable.isOnHeap) { + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll( + transmitTaintArgumentFormalToActual( + fact = fact, + at = callStatement, + from = formal, + to = actual + ) + ) + } + } + + // Transmit facts on instance (from 'this' to 'instance'): + if (callExpr is JcInstanceCallExpr) { + addAll( + transmitTaintThisToInstance( + fact = fact, + at = callStatement, + from = callee.thisInstance, + to = callExpr.instance + ) + ) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + + // Transmit facts on return value (from 'returnValue' to 'lhv'): + if (exitStatement is JcReturnInst && callStatement is JcAssignInst) { + // Note: returnValue can be null here in some weird cases, e.g. in lambda. + exitStatement.returnValue?.let { returnValue -> + addAll( + transmitTaintReturn( + fact = fact, + at = callStatement, + from = returnValue, + to = callStatement.lhv + ) + ) + } + } + } + } +} + +// TODO: class BackwardNpeFlowFunctions diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeManager.kt new file mode 100644 index 000000000..1b72e58ef --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeManager.kt @@ -0,0 +1,62 @@ +/* + * 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.analysis.npe + +import org.jacodb.analysis.ifds.UniRunner +import org.jacodb.analysis.ifds.UnitResolver +import org.jacodb.analysis.ifds.UnitType +import org.jacodb.analysis.ifds.UnknownUnit +import org.jacodb.analysis.taint.TaintManager +import org.jacodb.analysis.taint.TaintRunner +import org.jacodb.analysis.taint.TaintZeroFact +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph + +private val logger = mu.KotlinLogging.logger {} + +class NpeManager( + graph: JcApplicationGraph, + unitResolver: UnitResolver, +) : TaintManager(graph, unitResolver, useBidiRunner = false) { + + override fun newRunner( + unit: UnitType, + ): TaintRunner { + check(unit !in runnerForUnit) { "Runner for $unit already exists" } + + val analyzer = NpeAnalyzer(graph) + val runner = UniRunner( + graph = graph, + analyzer = analyzer, + manager = this@NpeManager, + unitResolver = unitResolver, + unit = unit, + zeroFact = TaintZeroFact + ) + + runnerForUnit[unit] = runner + return runner + } + + override fun addStart(method: JcMethod) { + logger.info { "Adding start method: $method" } + val unit = unitResolver.resolve(method) + if (unit == UnknownUnit) return + methodsForUnit.getOrPut(unit) { hashSetOf() }.add(method) + // Note: DO NOT add deps here! + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/Utils.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/Utils.kt new file mode 100644 index 000000000..3a0bbd27e --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/Utils.kt @@ -0,0 +1,57 @@ +/* + * 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.analysis.npe + +import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.analysis.ifds.toPathOrNull +import org.jacodb.analysis.util.startsWith +import org.jacodb.api.cfg.JcExpr +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.cfg.JcInstanceCallExpr +import org.jacodb.api.cfg.JcLengthExpr +import org.jacodb.api.cfg.values + +fun AccessPath?.isDereferencedAt(expr: JcExpr): Boolean { + if (this == null) { + return false + } + + if (expr is JcInstanceCallExpr) { + val instancePath = expr.instance.toPathOrNull() + if (instancePath.startsWith(this)) { + return true + } + } + + if (expr is JcLengthExpr) { + val arrayPath = expr.array.toPathOrNull() + if (arrayPath.startsWith(this)) { + return true + } + } + + return expr.values + .mapNotNull { it.toPathOrNull() } + .any { + (it - this)?.isNotEmpty() == true + } +} + +fun AccessPath?.isDereferencedAt(inst: JcInst): Boolean { + if (this == null) return false + return inst.operands.any { isDereferencedAt(it) } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/AccessPath.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/AccessPath.kt deleted file mode 100644 index 4ba003ea8..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/AccessPath.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.analysis.paths - -import org.jacodb.api.JcField -import org.jacodb.api.cfg.JcLocal - -/** - * This class is used to represent an access path that is needed for problems - * where dataflow facts could be correlated with variables/values (such as NPE, uninitialized variable, etc.) - */ -data class AccessPath private constructor(val value: JcLocal?, val accesses: List) { - companion object { - - fun fromLocal(value: JcLocal) = AccessPath(value, listOf()) - - fun fromStaticField(field: JcField): AccessPath { - if (!field.isStatic) { - throw IllegalArgumentException("Expected static field") - } - - return AccessPath(null, listOf(FieldAccessor(field))) - } - - fun fromOther(other: AccessPath, accesses: List): AccessPath { - if (accesses.any { it is FieldAccessor && it.field.isStatic }) { - throw IllegalArgumentException("Unexpected static field") - } - - return AccessPath(other.value, other.accesses.plus(accesses)) - } - } - - fun limit(n: Int) = AccessPath(value, accesses.take(n)) - - val isOnHeap: Boolean - get() = accesses.isNotEmpty() - - val isStatic: Boolean - get() = value == null - - override fun toString(): String { - var str = value.toString() - for (accessor in accesses) { - str += ".$accessor" - } - return str - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/Util.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/Util.kt deleted file mode 100644 index 139d23bb5..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/Util.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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.analysis.paths - -import org.jacodb.api.cfg.JcArrayAccess -import org.jacodb.api.cfg.JcCastExpr -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcFieldRef -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.cfg.JcInstanceCallExpr -import org.jacodb.api.cfg.JcLengthExpr -import org.jacodb.api.cfg.JcLocal -import org.jacodb.api.cfg.JcValue -import org.jacodb.api.cfg.values - -internal fun JcExpr.toPathOrNull(): AccessPath? { - if (this is JcCastExpr) { - return operand.toPathOrNull() - } - if (this is JcLocal) { - return AccessPath.fromLocal(this) - } - - if (this is JcArrayAccess) { - return array.toPathOrNull()?.let { - AccessPath.fromOther(it, listOf(ElementAccessor)) - } - } - - if (this is JcFieldRef) { - val instance = instance // enables smart cast - - return if (instance == null) { - AccessPath.fromStaticField(field.field) - } else { - instance.toPathOrNull()?.let { - AccessPath.fromOther(it, listOf(FieldAccessor(field.field))) - } - } - } - return null -} - -internal fun JcValue.toPath(): AccessPath { - return toPathOrNull() ?: error("Unable to build access path for value $this") -} - -internal fun AccessPath?.minus(other: AccessPath): List? { - if (this == null) { - return null - } - if (value != other.value) { - return null - } - if (accesses.take(other.accesses.size) != other.accesses) { - return null - } - - return accesses.drop(other.accesses.size) -} - -internal fun AccessPath?.startsWith(other: AccessPath?): Boolean { - if (this == null || other == null) { - return false - } - - return minus(other) != null -} - -fun AccessPath?.isDereferencedAt(expr: JcExpr): Boolean { - if (this == null) { - return false - } - - (expr as? JcInstanceCallExpr)?.let { - val instancePath = it.instance.toPathOrNull() - if (instancePath.startsWith(this)) { - return true - } - } - - (expr as? JcLengthExpr)?.let { - val arrayPath = it.array.toPathOrNull() - if (arrayPath.startsWith(this)) { - return true - } - } - - return expr.values - .mapNotNull { it.toPathOrNull() } - .any { - it.minus(this)?.isNotEmpty() == true - } -} - -fun AccessPath?.isDereferencedAt(inst: JcInst): Boolean { - if (this == null) { - return false - } - - return inst.operands.any { isDereferencedAt(it) } -} \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/DataClasses.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/DataClasses.kt deleted file mode 100644 index 9a6e43201..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/DataClasses.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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.analysis.sarif - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.encodeToStream -import org.jacodb.analysis.engine.IfdsVertex -import org.jacodb.analysis.engine.VulnerabilityInstance -import org.jacodb.api.JcMethod -import org.jacodb.api.cfg.JcInst -import java.io.OutputStream -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.io.path.Path - -@Serializable -data class SarifMessage(val text: String) - -@Serializable -data class SarifArtifactLocation(val uri: String) - -@Serializable -data class SarifRegion(val startLine: Int) - -@Serializable -data class SarifPhysicalLocation(val artifactLocation: SarifArtifactLocation, val region: SarifRegion) - -@Serializable -data class SarifLogicalLocation(val fullyQualifiedName: String) - -@Serializable -data class SarifLocation(val physicalLocation: SarifPhysicalLocation, val logicalLocations: List) { - companion object { - private val JcMethod.fullyQualifiedName: String - get() = "${enclosingClass.name}#${name}" - - private val currentPath: Path = Paths.get("").toAbsolutePath() - - fun fromInst(inst: JcInst): SarifLocation = SarifLocation( - physicalLocation = SarifPhysicalLocation( - SarifArtifactLocation("${currentPath.relativize(Path(inst.location.method.declaration.location.path))}"), - SarifRegion(inst.location.lineNumber) - ), - logicalLocations = listOf( - SarifLogicalLocation(inst.location.method.fullyQualifiedName) - ) - ) - } -} - -@Serializable -data class SarifDomainFact(val text: String) - -@Serializable -data class SarifState(val domainFact: SarifDomainFact) - -@Serializable -data class SarifThreadFlowLocation(val location: SarifLocation, val state: SarifState) - -@Serializable -data class SarifThreadFlow(val locations: List) - -@Serializable -data class SarifCodeFlow(val threadFlows: List) { - companion object { - fun fromTrace(trace: List): SarifCodeFlow { - val threadFlow = trace.map { - SarifThreadFlowLocation( - SarifLocation.fromInst(it.statement), - SarifState(SarifDomainFact(it.domainFact.toString())) - ) - } - return SarifCodeFlow(listOf(SarifThreadFlow(threadFlow))) - } - } -} - -@Serializable -data class SarifResult( - val ruleId: String, - val message: SarifMessage, - val level: SarifSeverityLevel, - val locations: List, - val codeFlows: List -) { - companion object { - fun fromVulnerabilityInstance(instance: VulnerabilityInstance, maxPathsCount: Int): SarifResult = SarifResult( - instance.vulnerabilityDescription.ruleId, - instance.vulnerabilityDescription.message, - instance.vulnerabilityDescription.level, - listOf(SarifLocation.fromInst(instance.traceGraph.sink.statement)), - instance.traceGraph.getAllTraces().take(maxPathsCount).map { SarifCodeFlow.fromTrace(it) }.toList() - ) - } -} - -@Serializable -data class SarifDriver( - val name: String, - val version: String, - val informationUri: String, -) - -@Serializable -data class SarifTool(val driver: SarifDriver) - -val IfdsTool = SarifTool( - SarifDriver( - name = "JaCo-IFDS", - version = "1.2.0", - informationUri = "https://github.com/UnitTestBot/jacodb/blob/develop/jacodb-analysis/README.md" - ) -) - -@Serializable -data class SarifRun(val tool: SarifTool, val results: List) - -@OptIn(ExperimentalSerializationApi::class) -@Serializable -data class SarifReport( - val version: String, - - @SerialName("\$schema") - val schema: String, - - val runs: List -) { - fun encodeToStream(stream: OutputStream) { - json.encodeToStream(this, stream) - } - - companion object { - private const val defaultVersion = - "2.1.0" - private const val defaultSchema = - "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json" - - private const val defaultPathsCount: Int = 3 - - private val json = Json { - prettyPrint = true - encodeDefaults = false - } - - fun fromVulnerabilities( - vulnerabilities: List, - pathsCount: Int = defaultPathsCount - ): SarifReport = SarifReport( - version = defaultVersion, - schema = defaultSchema, - runs = listOf( - SarifRun( - IfdsTool, - vulnerabilities.map { SarifResult.fromVulnerabilityInstance(it, pathsCount) } - ) - ) - ) - } -} - -@Serializable -enum class SarifSeverityLevel { - @SerialName("error") ERROR, - @SerialName("warning") WARNING, - @SerialName("note") NOTE -} - -data class VulnerabilityDescription( - val message: SarifMessage, - val ruleId: String, - val level: SarifSeverityLevel = SarifSeverityLevel.WARNING -) \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Sarif.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Sarif.kt new file mode 100644 index 000000000..4101a14b8 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Sarif.kt @@ -0,0 +1,150 @@ +/* + * 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.analysis.sarif + +import io.github.detekt.sarif4k.ArtifactLocation +import io.github.detekt.sarif4k.CodeFlow +import io.github.detekt.sarif4k.Location +import io.github.detekt.sarif4k.LogicalLocation +import io.github.detekt.sarif4k.Message +import io.github.detekt.sarif4k.MultiformatMessageString +import io.github.detekt.sarif4k.PhysicalLocation +import io.github.detekt.sarif4k.Region +import io.github.detekt.sarif4k.Result +import io.github.detekt.sarif4k.Run +import io.github.detekt.sarif4k.SarifSchema210 +import io.github.detekt.sarif4k.ThreadFlow +import io.github.detekt.sarif4k.ThreadFlowLocation +import io.github.detekt.sarif4k.Tool +import io.github.detekt.sarif4k.ToolComponent +import io.github.detekt.sarif4k.Version +import org.jacodb.analysis.ifds.Vertex +import org.jacodb.api.JcMethod +import org.jacodb.api.cfg.JcInst +import java.io.File + +private const val SARIF_SCHEMA = + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json" +private const val JACODB_INFORMATION_URI = + "https://github.com/UnitTestBot/jacodb/blob/develop/jacodb-analysis/README.md" +private const val DEFAULT_PATH_COUNT = 3 + +fun sarifReportFromVulnerabilities( + vulnerabilities: List>, + maxPathsCount: Int = DEFAULT_PATH_COUNT, + isDeduplicate: Boolean = true, + sourceFileResolver: SourceFileResolver = SourceFileResolver { null }, +): SarifSchema210 { + return SarifSchema210( + schema = SARIF_SCHEMA, + version = Version.The210, + runs = listOf( + Run( + tool = Tool( + driver = ToolComponent( + name = "jacodb-analysis", + organization = "UnitTestBot", + version = "1.4.5", + informationURI = JACODB_INFORMATION_URI, + ) + ), + results = vulnerabilities.map { instance -> + Result( + ruleID = instance.description.ruleId, + message = Message( + text = instance.description.message + ), + level = instance.description.level, + locations = listOf(instToSarifLocation(instance.traceGraph.sink.statement, sourceFileResolver)), + codeFlows = instance.traceGraph + .getAllTraces() + .take(maxPathsCount) + .map { traceToSarifCodeFlow(it, sourceFileResolver, isDeduplicate) } + .toList(), + ) + } + ) + ) + ) +} + +private val JcMethod.fullyQualifiedName: String + get() = "${enclosingClass.name}#${name}" + +private fun instToSarifLocation(inst: JcInst, sourceFileResolver: SourceFileResolver): Location { + val sourceLocation = sourceFileResolver.resolve(inst) + ?: run { + val registeredLocation = inst.location.method.declaration.location + val classFile = inst.location.method.enclosingClass.name + .replace('.', '/') + ".class" + File(registeredLocation.path).resolve(classFile).path + } + return Location( + physicalLocation = PhysicalLocation( + artifactLocation = ArtifactLocation( + uri = sourceLocation + ), + region = Region( + startLine = inst.location.lineNumber.toLong() + ) + ), + logicalLocations = listOf( + LogicalLocation( + fullyQualifiedName = inst.location.method.fullyQualifiedName + ) + ) + ) +} + +private fun traceToSarifCodeFlow( + trace: List>, + sourceFileResolver: SourceFileResolver, + isDeduplicate: Boolean = true, +): CodeFlow { + return CodeFlow( + threadFlows = listOf( + ThreadFlow( + locations = trace.map { + ThreadFlowLocation( + location = instToSarifLocation(it.statement, sourceFileResolver), + state = mapOf( + "fact" to MultiformatMessageString( + text = it.fact.toString() + ) + ) + ) + }.let { + if (isDeduplicate) it.deduplicate() else it + } + ) + ) + ) +} + +private fun List.deduplicate(): List { + if (isEmpty()) return emptyList() + + return listOf(first()) + zipWithNext { a, b -> + val aLine = a.location!!.physicalLocation!!.region!!.startLine!! + val bLine = b.location!!.physicalLocation!!.region!!.startLine!! + if (aLine != bLine) { + b + } else { + null + } + }.filterNotNull() +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/SourceFileResolver.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/SourceFileResolver.kt new file mode 100644 index 000000000..c30f15486 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/SourceFileResolver.kt @@ -0,0 +1,23 @@ +/* + * 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.analysis.sarif + +import org.jacodb.api.cfg.JcInst + +fun interface SourceFileResolver { + fun resolve(inst: JcInst): String? +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Vulnerability.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Vulnerability.kt new file mode 100644 index 000000000..6d83a5ac2 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Vulnerability.kt @@ -0,0 +1,31 @@ +/* + * 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.analysis.sarif + +import io.github.detekt.sarif4k.Level +import org.jacodb.analysis.ifds.TraceGraph + +data class VulnerabilityInstance( + val traceGraph: TraceGraph, + val description: VulnerabilityDescription, +) + +data class VulnerabilityDescription( + val ruleId: String?, + val message: String?, + val level: Level = Level.Warning, +) diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/VulnerabilityInstance.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Sarif.kt similarity index 61% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/VulnerabilityInstance.kt rename to jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Sarif.kt index 35583b981..7d2029b36 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/VulnerabilityInstance.kt +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Sarif.kt @@ -14,17 +14,20 @@ * limitations under the License. */ -package org.jacodb.analysis.engine +package org.jacodb.analysis.taint +import org.jacodb.analysis.ifds.TraceGraph import org.jacodb.analysis.sarif.VulnerabilityDescription +import org.jacodb.analysis.sarif.VulnerabilityInstance -/** - * Represents a vulnerability (issue) found by analysis - * - * @property vulnerabilityDescription type of vulnerability as a string (e.g. "Possible NPE", "Unused variable") - * @property traceGraph contains sink, sources and traces that lead to occurrence of vulnerability - */ -data class VulnerabilityInstance( - val vulnerabilityDescription: VulnerabilityDescription, - val traceGraph: TraceGraph -) \ No newline at end of file +fun TaintVulnerability.toSarif( + graph: TraceGraph, +): VulnerabilityInstance { + return VulnerabilityInstance( + graph, + VulnerabilityDescription( + ruleId = null, + message = rule?.ruleNote + ) + ) +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintAnalyzers.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintAnalyzers.kt new file mode 100644 index 000000000..ae59345cb --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintAnalyzers.kt @@ -0,0 +1,116 @@ +/* + * 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.analysis.taint + +import org.jacodb.analysis.config.CallPositionToJcValueResolver +import org.jacodb.analysis.config.FactAwareConditionEvaluator +import org.jacodb.analysis.ifds.Analyzer +import org.jacodb.analysis.ifds.Edge +import org.jacodb.analysis.ifds.Reason +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.ext.cfg.callExpr +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.jacodb.taint.configuration.TaintMethodSink + +private val logger = mu.KotlinLogging.logger {} + +class TaintAnalyzer( + private val graph: JcApplicationGraph, +) : Analyzer { + + override val flowFunctions: ForwardTaintFlowFunctions by lazy { + ForwardTaintFlowFunctions(graph.classpath, graph) + } + + private val taintConfigurationFeature: TaintConfigurationFeature? + get() = flowFunctions.taintConfigurationFeature + + private fun isExitPoint(statement: JcInst): Boolean { + return statement in graph.exitPoints(statement.location.method) + } + + override fun handleNewEdge( + edge: TaintEdge, + ): List = buildList { + if (isExitPoint(edge.to.statement)) { + add(NewSummaryEdge(edge)) + } + + run { + val callExpr = edge.to.statement.callExpr ?: return@run + val callee = callExpr.method.method + + val config = taintConfigurationFeature?.getConfigForMethod(callee) ?: return@run + + // TODO: not always we want to skip sinks on Zero facts. + // Some rules might have ConstantTrue or just true (when evaluated with Zero fact) condition. + if (edge.to.fact !is Tainted) { + return@run + } + + // Determine whether 'edge.to' is a sink via config: + val conditionEvaluator = FactAwareConditionEvaluator( + edge.to.fact, + CallPositionToJcValueResolver(edge.to.statement), + ) + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + val message = item.ruleNote + val vulnerability = TaintVulnerability(message, sink = edge.to, rule = item) + logger.info { "Found sink=${vulnerability.sink} in ${vulnerability.method}" } + add(NewVulnerability(vulnerability)) + } + } + } + } + + override fun handleCrossUnitCall( + caller: TaintVertex, + callee: TaintVertex, + ): List = buildList { + add(EdgeForOtherRunner(TaintEdge(callee, callee), Reason.CrossUnitCall(caller))) + } +} + +class BackwardTaintAnalyzer( + private val graph: JcApplicationGraph, +) : Analyzer { + + override val flowFunctions: BackwardTaintFlowFunctions by lazy { + BackwardTaintFlowFunctions(graph.classpath, graph) + } + + private fun isExitPoint(statement: JcInst): Boolean { + return statement in graph.exitPoints(statement.location.method) + } + + override fun handleNewEdge( + edge: TaintEdge, + ): List = buildList { + if (isExitPoint(edge.to.statement)) { + add(EdgeForOtherRunner(Edge(edge.to, edge.to), reason = Reason.External)) + } + } + + override fun handleCrossUnitCall( + caller: TaintVertex, + callee: TaintVertex, + ): List { + return emptyList() + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintBidiRunner.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintBidiRunner.kt new file mode 100644 index 000000000..dbc516663 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintBidiRunner.kt @@ -0,0 +1,144 @@ +/* + * 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.analysis.taint + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.jacodb.analysis.ifds.ControlEvent +import org.jacodb.analysis.ifds.Edge +import org.jacodb.analysis.ifds.IfdsResult +import org.jacodb.analysis.ifds.Manager +import org.jacodb.analysis.ifds.QueueEmptinessChanged +import org.jacodb.analysis.ifds.Reason +import org.jacodb.analysis.ifds.UnitResolver +import org.jacodb.analysis.ifds.UnitType +import org.jacodb.api.JcMethod + +class TaintBidiRunner( + val manager: TaintManager, + val unitResolver: UnitResolver, + override val unit: UnitType, + newForwardRunner: (Manager) -> TaintRunner, + newBackwardRunner: (Manager) -> TaintRunner, +) : TaintRunner { + + @Volatile + private var forwardQueueIsEmpty: Boolean = false + + @Volatile + private var backwardQueueIsEmpty: Boolean = false + + private val forwardManager: Manager = + object : Manager { + override fun handleEvent(event: TaintEvent) { + when (event) { + is EdgeForOtherRunner -> { + if (unitResolver.resolve(event.edge.method) == unit) { + // Submit new edge directly to the backward runner: + backwardRunner.submitNewEdge(event.edge, event.reason) + } else { + // Submit new edge via the manager: + manager.handleEvent(event) + } + } + + else -> manager.handleEvent(event) + } + } + + override fun handleControlEvent(event: ControlEvent) { + when (event) { + is QueueEmptinessChanged -> { + forwardQueueIsEmpty = event.isEmpty + val newEvent = QueueEmptinessChanged(event.runner, forwardQueueIsEmpty && backwardQueueIsEmpty) + manager.handleControlEvent(newEvent) + } + } + } + + override fun subscribeOnSummaryEdges( + method: JcMethod, + scope: CoroutineScope, + handler: (TaintEdge) -> Unit, + ) { + manager.subscribeOnSummaryEdges(method, scope, handler) + } + } + + private val backwardManager: Manager = + object : Manager { + override fun handleEvent(event: TaintEvent) { + when (event) { + is EdgeForOtherRunner -> { + check(unitResolver.resolve(event.edge.method) == unit) + // Submit new edge directly to the forward runner: + forwardRunner.submitNewEdge(event.edge, event.reason) + } + + else -> manager.handleEvent(event) + } + } + + override fun handleControlEvent(event: ControlEvent) { + when (event) { + is QueueEmptinessChanged -> { + backwardQueueIsEmpty = event.isEmpty + val newEvent = QueueEmptinessChanged(event.runner, forwardQueueIsEmpty && backwardQueueIsEmpty) + manager.handleControlEvent(newEvent) + } + } + } + + override fun subscribeOnSummaryEdges( + method: JcMethod, + scope: CoroutineScope, + handler: (TaintEdge) -> Unit, + ) { + // TODO: ignore? + manager.subscribeOnSummaryEdges(method, scope, handler) + } + } + + val forwardRunner: TaintRunner = newForwardRunner(forwardManager) + val backwardRunner: TaintRunner = newBackwardRunner(backwardManager) + + init { + check(forwardRunner.unit == unit) + check(backwardRunner.unit == unit) + } + + override fun submitNewEdge(edge: Edge, reason: Reason) { + forwardRunner.submitNewEdge(edge, reason) + } + + override suspend fun run(startMethods: List) = coroutineScope { + val backwardRunnerJob = launch(start = CoroutineStart.LAZY) { backwardRunner.run(startMethods) } + val forwardRunnerJob = launch(start = CoroutineStart.LAZY) { forwardRunner.run(startMethods) } + + backwardRunnerJob.start() + forwardRunnerJob.start() + + backwardRunnerJob.join() + forwardRunnerJob.join() + } + + override fun getIfdsResult(): IfdsResult { + return forwardRunner.getIfdsResult() + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintEvents.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintEvents.kt new file mode 100644 index 000000000..ce1e9c8dc --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintEvents.kt @@ -0,0 +1,39 @@ +/* + * 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.analysis.taint + +import org.jacodb.analysis.ifds.Reason + +sealed interface TaintEvent + +data class NewSummaryEdge( + val edge: TaintEdge, +) : TaintEvent + +data class NewVulnerability( + val vulnerability: TaintVulnerability, +) : TaintEvent + +data class EdgeForOtherRunner( + val edge: TaintEdge, + val reason: Reason +) : TaintEvent { + init { + // TODO: remove this check + check(edge.from == edge.to) { "Edge for another runner must be a loop" } + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFacts.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFacts.kt new file mode 100644 index 000000000..0fec8b62c --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFacts.kt @@ -0,0 +1,31 @@ +/* + * 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.analysis.taint + +import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.taint.configuration.TaintMark + +sealed interface TaintDomainFact + +object TaintZeroFact : TaintDomainFact { + override fun toString(): String = "Zero" +} + +data class Tainted( + val variable: AccessPath, + val mark: TaintMark, +) : TaintDomainFact diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFlowFunctions.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFlowFunctions.kt new file mode 100644 index 000000000..20bc9b455 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFlowFunctions.kt @@ -0,0 +1,686 @@ +/* + * 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.analysis.taint + +import org.jacodb.analysis.config.BasicConditionEvaluator +import org.jacodb.analysis.config.CallPositionToAccessPathResolver +import org.jacodb.analysis.config.CallPositionToJcValueResolver +import org.jacodb.analysis.config.EntryPointPositionToAccessPathResolver +import org.jacodb.analysis.config.EntryPointPositionToJcValueResolver +import org.jacodb.analysis.config.FactAwareConditionEvaluator +import org.jacodb.analysis.config.TaintActionEvaluator +import org.jacodb.analysis.ifds.ElementAccessor +import org.jacodb.analysis.ifds.FlowFunction +import org.jacodb.analysis.ifds.FlowFunctions +import org.jacodb.analysis.ifds.onSome +import org.jacodb.analysis.ifds.toPath +import org.jacodb.analysis.ifds.toPathOrNull +import org.jacodb.analysis.util.getArgumentsOf +import org.jacodb.analysis.util.startsWith +import org.jacodb.analysis.util.thisInstance +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcArrayAccess +import org.jacodb.api.cfg.JcAssignInst +import org.jacodb.api.cfg.JcDynamicCallExpr +import org.jacodb.api.cfg.JcExpr +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.cfg.JcInstanceCallExpr +import org.jacodb.api.cfg.JcReturnInst +import org.jacodb.api.cfg.JcThis +import org.jacodb.api.cfg.JcValue +import org.jacodb.api.ext.cfg.callExpr +import org.jacodb.taint.configuration.AssignMark +import org.jacodb.taint.configuration.CopyAllMarks +import org.jacodb.taint.configuration.CopyMark +import org.jacodb.taint.configuration.RemoveAllMarks +import org.jacodb.taint.configuration.RemoveMark +import org.jacodb.taint.configuration.TaintCleaner +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.jacodb.taint.configuration.TaintEntryPointSource +import org.jacodb.taint.configuration.TaintMethodSource +import org.jacodb.taint.configuration.TaintPassThrough + +private val logger = mu.KotlinLogging.logger {} + +class ForwardTaintFlowFunctions( + private val cp: JcClasspath, + private val graph: JcApplicationGraph, +) : FlowFunctions { + + internal val taintConfigurationFeature: TaintConfigurationFeature? by lazy { + cp.features + ?.singleOrNull { it is TaintConfigurationFeature } + ?.let { it as TaintConfigurationFeature } + } + + override fun obtainPossibleStartFacts( + method: JcMethod, + ): Collection = buildSet { + // Zero (reachability) fact always present at entrypoint: + add(TaintZeroFact) + + // Extract initial facts from the config: + val config = taintConfigurationFeature?.getConfigForMethod(method) + if (config != null) { + val conditionEvaluator = BasicConditionEvaluator(EntryPointPositionToJcValueResolver(cp, method)) + val actionEvaluator = TaintActionEvaluator(EntryPointPositionToAccessPathResolver(cp, method)) + + // Handle EntryPointSource config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is AssignMark -> actionEvaluator.evaluate(action) + else -> error("$action is not supported for $item") + } + result.onSome { addAll(it) } + } + } + } + } + } + + private fun transmitTaintAssign( + fact: Tainted, + from: JcExpr, + to: JcValue, + ): Collection { + val toPath = to.toPath() + val fromPath = from.toPathOrNull() + + if (fromPath != null) { + // Adhoc taint array: + if (fromPath.accesses.isNotEmpty() + && fromPath.accesses.last() is ElementAccessor + && fromPath == (fact.variable / ElementAccessor) + ) { + val newTaint = fact.copy(variable = toPath) + return setOf(fact, newTaint) + } + + val tail = fact.variable - fromPath + if (tail != null) { + // Both 'from' and 'to' are tainted now: + val newPath = toPath / tail + val newTaint = fact.copy(variable = newPath) + return setOf(fact, newTaint) + } + } + + if (fact.variable.startsWith(toPath)) { + // FIXME: pass-through tainted arrays + if (to is JcArrayAccess) { + return setOf(fact) + } + // 'to' was (sub-)tainted, but it is now overridden by 'from': + return emptySet() + } else { + // Neither 'from' nor 'to' are tainted: + return setOf(fact) + } + } + + private fun transmitTaintNormal( + fact: Tainted, + inst: JcInst, + ): List { + // Pass-through: + return listOf(fact) + } + + override fun obtainSequentFlowFunction( + current: JcInst, + next: JcInst, + ) = FlowFunction { fact -> + if (fact is TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + if (current is JcAssignInst) { + transmitTaintAssign(fact, from = current.rhv, to = current.lhv) + } else { + transmitTaintNormal(fact, current) + } + } + + private fun transmitTaint( + fact: Tainted, + from: JcValue, + to: JcValue, + ): Collection = buildSet { + val fromPath = from.toPath() + val toPath = to.toPath() + + val tail = (fact.variable - fromPath) ?: return@buildSet + val newPath = toPath / tail + val newTaint = fact.copy(variable = newPath) + add(newTaint) + } + + private fun transmitTaintArgumentActualToFormal( + fact: Tainted, + from: JcValue, // actual + to: JcValue, // formal + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintArgumentFormalToActual( + fact: Tainted, + from: JcValue, // formal + to: JcValue, // actual + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintInstanceToThis( + fact: Tainted, + from: JcValue, // instance + to: JcThis, // this + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintThisToInstance( + fact: Tainted, + from: JcThis, // this + to: JcValue, // instance + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintReturn( + fact: Tainted, + from: JcValue, + to: JcValue, + ): Collection = transmitTaint(fact, from, to) + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, // FIXME: unused? + ) = FlowFunction { fact -> + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + val callee = callExpr.method.method + + // FIXME: handle taint pass-through on invokedynamic-based String concatenation: + if (fact is Tainted + && callExpr is JcDynamicCallExpr + && callee.enclosingClass.name == "java.lang.invoke.StringConcatFactory" + && callStatement is JcAssignInst + ) { + for (arg in callExpr.args) { + if (arg.toPath() == fact.variable) { + return@FlowFunction setOf( + fact, + fact.copy(variable = callStatement.lhv.toPath()) + ) + } + } + return@FlowFunction setOf(fact) + } + + val config = taintConfigurationFeature?.getConfigForMethod(callee) + + if (fact == TaintZeroFact) { + return@FlowFunction buildSet { + add(TaintZeroFact) + + if (config != null) { + val conditionEvaluator = BasicConditionEvaluator(CallPositionToJcValueResolver(callStatement)) + val actionEvaluator = TaintActionEvaluator(CallPositionToAccessPathResolver(callStatement)) + + // Handle MethodSource config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is AssignMark -> actionEvaluator.evaluate(action) + else -> error("$action is not supported for $item") + } + result.onSome { addAll(it) } + } + } + } + } + } + } + check(fact is Tainted) + + if (config != null) { + val facts = mutableSetOf() + val conditionEvaluator = FactAwareConditionEvaluator(fact, CallPositionToJcValueResolver(callStatement)) + val actionEvaluator = TaintActionEvaluator(CallPositionToAccessPathResolver(callStatement)) + var defaultBehavior = true + + // Handle PassThrough config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is CopyMark -> actionEvaluator.evaluate(action, fact) + is CopyAllMarks -> actionEvaluator.evaluate(action, fact) + is RemoveMark -> actionEvaluator.evaluate(action, fact) + is RemoveAllMarks -> actionEvaluator.evaluate(action, fact) + else -> error("$action is not supported for $item") + } + result.onSome { + facts += it + defaultBehavior = false + } + } + } + } + + // Handle Cleaner config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is RemoveMark -> actionEvaluator.evaluate(action, fact) + is RemoveAllMarks -> actionEvaluator.evaluate(action, fact) + else -> error("$action is not supported for $item") + } + result.onSome { + facts += it + defaultBehavior = false + } + } + } + } + + if (!defaultBehavior) { + if (facts.size > 0) { + logger.trace { "Got ${facts.size} facts from config for $callee: $facts" } + } + return@FlowFunction facts + } else { + // Fall back to the default behavior, as if there were no config at all. + } + } + + // FIXME: adhoc for constructors: + if (callee.isConstructor) { + return@FlowFunction listOf(fact) + } + + // TODO: CONSIDER REFACTORING THIS + // Default behavior for "analyzable" method calls is to remove ("temporarily") + // all the marks from the 'instance' and arguments, in order to allow them "pass through" + // the callee (when it is going to be analyzed), i.e. through "call-to-start" and + // "exit-to-return" flow functions. + // When we know that we are NOT going to analyze the callee, we do NOT need + // to remove any marks from 'instance' and arguments. + // Currently, "analyzability" of the callee depends on the fact that the callee + // is "accessible" through the JcApplicationGraph::callees(). + if (callee in graph.callees(callStatement)) { + + if (fact.variable.isStatic) { + return@FlowFunction emptyList() + } + + for (actual in callExpr.args) { + // Possibly tainted actual parameter: + if (fact.variable.startsWith(actual.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + if (callExpr is JcInstanceCallExpr) { + // Possibly tainted instance: + if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + } + + if (callStatement is JcAssignInst) { + // Possibly tainted lhv: + if (fact.variable.startsWith(callStatement.lhv.toPathOrNull())) { + return@FlowFunction emptyList() // Overridden by rhv + } + } + + // The "most default" behaviour is encapsulated here: + transmitTaintNormal(fact, callStatement) + } + + override fun obtainCallToStartFlowFunction( + callStatement: JcInst, + calleeStart: JcInst, + ) = FlowFunction { fact -> + val callee = calleeStart.location.method + + if (fact == TaintZeroFact) { + return@FlowFunction obtainPossibleStartFacts(callee) + } + check(fact is Tainted) + + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + + buildSet { + // Transmit facts on arguments (from 'actual' to 'formal'): + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll(transmitTaintArgumentActualToFormal(fact, from = actual, to = formal)) + } + + // Transmit facts on instance (from 'instance' to 'this'): + if (callExpr is JcInstanceCallExpr) { + addAll(transmitTaintInstanceToThis(fact, from = callExpr.instance, to = callee.thisInstance)) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + } + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, // unused + exitStatement: JcInst, + ) = FlowFunction { fact -> + if (fact == TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + val callee = exitStatement.location.method + + buildSet { + // Transmit facts on arguments (from 'formal' back to 'actual'), if they are passed by-ref: + if (fact.variable.isOnHeap) { + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll(transmitTaintArgumentFormalToActual(fact, from = formal, to = actual)) + } + } + + // Transmit facts on instance (from 'this' to 'instance'): + if (callExpr is JcInstanceCallExpr) { + addAll(transmitTaintThisToInstance(fact, from = callee.thisInstance, to = callExpr.instance)) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + + // Transmit facts on return value (from 'returnValue' to 'lhv'): + if (exitStatement is JcReturnInst && callStatement is JcAssignInst) { + // Note: returnValue can be null here in some weird cases, e.g. in lambda. + exitStatement.returnValue?.let { returnValue -> + addAll(transmitTaintReturn(fact, from = returnValue, to = callStatement.lhv)) + } + } + } + } +} + +class BackwardTaintFlowFunctions( + private val project: JcClasspath, + private val graph: JcApplicationGraph, +) : FlowFunctions { + + override fun obtainPossibleStartFacts( + method: JcMethod, + ): Collection { + return listOf(TaintZeroFact) + } + + private fun transmitTaintBackwardAssign( + fact: Tainted, + from: JcValue, + to: JcExpr, + ): Collection { + val fromPath = from.toPath() + val toPath = to.toPathOrNull() + + if (toPath != null) { + val tail = fact.variable - fromPath + if (tail != null) { + // Both 'from' and 'to' are tainted now: + val newPath = toPath / tail + val newTaint = fact.copy(variable = newPath) + return setOf(fact, newTaint) + } + + if (fact.variable.startsWith(toPath)) { + // 'to' was (sub-)tainted, but it is now overridden by 'from': + return emptySet() + } + } + + // Pass-through: + return setOf(fact) + } + + private fun transmitTaintBackwardNormal( + fact: Tainted, + inst: JcInst, + ): List { + // Pass-through: + return listOf(fact) + } + + override fun obtainSequentFlowFunction( + current: JcInst, + next: JcInst, + ) = FlowFunction { fact -> + if (fact is TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + if (current is JcAssignInst) { + transmitTaintBackwardAssign(fact, from = current.lhv, to = current.rhv) + } else { + transmitTaintBackwardNormal(fact, current) + } + } + + private fun transmitTaint( + fact: Tainted, + from: JcValue, + to: JcValue, + ): Collection = buildSet { + val fromPath = from.toPath() + val toPath = to.toPath() + + val tail = (fact.variable - fromPath) ?: return@buildSet + val newPath = toPath / tail + val newTaint = fact.copy(variable = newPath) + add(newTaint) + } + + private fun transmitTaintArgumentActualToFormal( + fact: Tainted, + from: JcValue, // actual + to: JcValue, // formal + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintArgumentFormalToActual( + fact: Tainted, + from: JcValue, // formal + to: JcValue, // actual + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintInstanceToThis( + fact: Tainted, + from: JcValue, // instance + to: JcThis, // this + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintThisToInstance( + fact: Tainted, + from: JcThis, // this + to: JcValue, // instance + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintReturn( + fact: Tainted, + from: JcValue, + to: JcValue, + ): Collection = transmitTaint(fact, from, to) + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, // FIXME: unused? + ) = FlowFunction { fact -> + // TODO: pass-through on invokedynamic-based String concatenation + + if (fact == TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + val callee = callExpr.method.method + + if (callee in graph.callees(callStatement)) { + + if (fact.variable.isStatic) { + return@FlowFunction emptyList() + } + + for (actual in callExpr.args) { + // Possibly tainted actual parameter: + if (fact.variable.startsWith(actual.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + if (callExpr is JcInstanceCallExpr) { + // Possibly tainted instance: + if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + } + + if (callStatement is JcAssignInst) { + // Possibly tainted rhv: + if (fact.variable.startsWith(callStatement.rhv.toPathOrNull())) { + return@FlowFunction emptyList() // Overridden by lhv + } + } + + // The "most default" behaviour is encapsulated here: + transmitTaintBackwardNormal(fact, callStatement) + } + + override fun obtainCallToStartFlowFunction( + callStatement: JcInst, + calleeStart: JcInst, + ) = FlowFunction { fact -> + val callee = calleeStart.location.method + + if (fact == TaintZeroFact) { + return@FlowFunction obtainPossibleStartFacts(callee) + } + check(fact is Tainted) + + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + + buildSet { + // Transmit facts on arguments (from 'actual' to 'formal'): + val actualParams = callExpr.args + val formalParams = project.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll(transmitTaintArgumentActualToFormal(fact, from = actual, to = formal)) + } + + // Transmit facts on instance (from 'instance' to 'this'): + if (callExpr is JcInstanceCallExpr) { + addAll(transmitTaintInstanceToThis(fact, from = callExpr.instance, to = callee.thisInstance)) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + + // Transmit facts on return value (from 'returnValue' to 'lhv'): + if (calleeStart is JcReturnInst && callStatement is JcAssignInst) { + // Note: returnValue can be null here in some weird cases, e.g. in lambda. + calleeStart.returnValue?.let { returnValue -> + addAll( + transmitTaintReturn( + fact = fact, + from = callStatement.lhv, + to = returnValue + ) + ) + } + } + } + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, + exitStatement: JcInst, + ) = FlowFunction { fact -> + if (fact == TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + val callee = exitStatement.location.method + + buildSet { + // Transmit facts on arguments (from 'formal' back to 'actual'), if they are passed by-ref: + if (fact.variable.isOnHeap) { + val actualParams = callExpr.args + val formalParams = project.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll( + transmitTaintArgumentFormalToActual( + fact = fact, + from = formal, + to = actual + ) + ) + } + } + + // Transmit facts on instance (from 'this' to 'instance'): + if (callExpr is JcInstanceCallExpr) { + addAll( + transmitTaintThisToInstance( + fact = fact, + from = callee.thisInstance, + to = callExpr.instance + ) + ) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + } + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintManager.kt new file mode 100644 index 000000000..f81a1599c --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintManager.kt @@ -0,0 +1,319 @@ +/* + * 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.analysis.taint + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import org.jacodb.analysis.graph.reversed +import org.jacodb.analysis.ifds.ControlEvent +import org.jacodb.analysis.ifds.IfdsResult +import org.jacodb.analysis.ifds.Manager +import org.jacodb.analysis.ifds.QueueEmptinessChanged +import org.jacodb.analysis.ifds.SummaryStorageImpl +import org.jacodb.analysis.ifds.TraceGraph +import org.jacodb.analysis.ifds.UniRunner +import org.jacodb.analysis.ifds.UnitResolver +import org.jacodb.analysis.ifds.UnitType +import org.jacodb.analysis.ifds.UnknownUnit +import org.jacodb.analysis.ifds.Vertex +import org.jacodb.analysis.util.getPathEdges +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.ExperimentalTime +import kotlin.time.TimeSource + +private val logger = mu.KotlinLogging.logger {} + +open class TaintManager( + protected val graph: JcApplicationGraph, + protected val unitResolver: UnitResolver, + private val useBidiRunner: Boolean = false, +) : Manager { + + protected val methodsForUnit: MutableMap> = hashMapOf() + protected val runnerForUnit: MutableMap = hashMapOf() + private val queueIsEmpty = ConcurrentHashMap() + + private val summaryEdgesStorage = SummaryStorageImpl() + private val vulnerabilitiesStorage = SummaryStorageImpl() + + private val stopRendezvous = Channel(Channel.RENDEZVOUS) + + protected open fun newRunner( + unit: UnitType, + ): TaintRunner { + check(unit !in runnerForUnit) { "Runner for $unit already exists" } + + logger.debug { "Creating a new runner for $unit" } + val runner = if (useBidiRunner) { + TaintBidiRunner( + manager = this@TaintManager, + unitResolver = unitResolver, + unit = unit, + { manager -> + val analyzer = TaintAnalyzer(graph) + UniRunner( + graph = graph, + analyzer = analyzer, + manager = manager, + unitResolver = unitResolver, + unit = unit, + zeroFact = TaintZeroFact + ) + }, + { manager -> + val analyzer = BackwardTaintAnalyzer(graph) + UniRunner( + graph = graph.reversed, + analyzer = analyzer, + manager = manager, + unitResolver = unitResolver, + unit = unit, + zeroFact = TaintZeroFact + ) + } + ) + } else { + val analyzer = TaintAnalyzer(graph) + UniRunner( + graph = graph, + analyzer = analyzer, + manager = this@TaintManager, + unitResolver = unitResolver, + unit = unit, + zeroFact = TaintZeroFact + ) + } + + runnerForUnit[unit] = runner + return runner + } + + private fun getAllCallees(method: JcMethod): Set { + val result: MutableSet = hashSetOf() + for (inst in method.flowGraph().instructions) { + result += graph.callees(inst) + } + return result + } + + protected open fun addStart(method: JcMethod) { + logger.info { "Adding start method: $method" } + val unit = unitResolver.resolve(method) + if (unit == UnknownUnit) return + val isNew = methodsForUnit.getOrPut(unit) { hashSetOf() }.add(method) + if (isNew) { + for (dep in getAllCallees(method)) { + addStart(dep) + } + } + } + + @JvmName("analyze") // needed for Java interop because of inline class (Duration) + @OptIn(ExperimentalTime::class) + fun analyze( + startMethods: List, + timeout: Duration = 3600.seconds, + ): List = runBlocking(Dispatchers.Default) { + val timeStart = TimeSource.Monotonic.markNow() + + // Add start methods: + for (method in startMethods) { + addStart(method) + } + + // Determine all units: + val allUnits = methodsForUnit.keys.toList() + logger.info { + "Starting analysis of ${ + methodsForUnit.values.sumOf { it.size } + } methods in ${allUnits.size} units" + } + + // Spawn runner jobs: + val allJobs = allUnits.map { unit -> + // Create the runner: + val runner = newRunner(unit) + + // Start the runner: + launch(start = CoroutineStart.LAZY) { + val methods = methodsForUnit[unit]!!.toList() + runner.run(methods) + } + } + + // Spawn progress job: + val progress = launch(Dispatchers.IO) { + while (isActive) { + delay(1.seconds) + logger.info { + "Progress: propagated ${ + runnerForUnit.values.sumOf { it.getPathEdges().size } + } path edges" + } + } + } + + // Spawn stopper job: + val stopper = launch(Dispatchers.IO) { + stopRendezvous.receive() + logger.info { "Stopping all runners..." } + allJobs.forEach { it.cancel() } + } + + // Start all runner jobs: + val timeStartJobs = TimeSource.Monotonic.markNow() + allJobs.forEach { it.start() } + + // Await all runners: + withTimeoutOrNull(timeout) { + allJobs.joinAll() + } ?: run { + logger.info { "Timeout!" } + allJobs.forEach { it.cancel() } + allJobs.joinAll() + } + progress.cancelAndJoin() + stopper.cancelAndJoin() + logger.info { + "All ${allJobs.size} jobs completed in %.1f s".format( + timeStartJobs.elapsedNow().toDouble(DurationUnit.SECONDS) + ) + } + + // Extract found vulnerabilities (sinks): + val foundVulnerabilities = vulnerabilitiesStorage.knownMethods + .flatMap { method -> + vulnerabilitiesStorage.getCurrentFacts(method) + } + if (logger.isDebugEnabled) { + logger.debug { "Total found ${foundVulnerabilities.size} vulnerabilities" } + for (vulnerability in foundVulnerabilities) { + logger.debug { "$vulnerability in ${vulnerability.method}" } + } + } + logger.info { "Total sinks: ${foundVulnerabilities.size}" } + logger.info { + "Total propagated ${ + runnerForUnit.values.sumOf { it.getPathEdges().size } + } path edges" + } + logger.info { + "Analysis done in %.1f s".format( + timeStart.elapsedNow().toDouble(DurationUnit.SECONDS) + ) + } + foundVulnerabilities + } + + override fun handleEvent(event: TaintEvent) { + when (event) { + is NewSummaryEdge -> { + summaryEdgesStorage.add(TaintSummaryEdge(event.edge)) + } + + is NewVulnerability -> { + vulnerabilitiesStorage.add(event.vulnerability) + } + + is EdgeForOtherRunner -> { + val method = event.edge.method + val unit = unitResolver.resolve(method) + val otherRunner = runnerForUnit[unit] ?: run { + // error("No runner for $unit") + logger.trace { "Ignoring event=$event for non-existing runner for unit=$unit" } + return + } + otherRunner.submitNewEdge(event.edge, event.reason) + } + } + } + + override fun handleControlEvent(event: ControlEvent) { + when (event) { + is QueueEmptinessChanged -> { + queueIsEmpty[event.runner.unit] = event.isEmpty + if (event.isEmpty) { + if (runnerForUnit.keys.all { queueIsEmpty[it] == true }) { + logger.debug { "All runners are empty" } + stopRendezvous.trySend(Unit).getOrNull() + } + } + } + } + } + + override fun subscribeOnSummaryEdges( + method: JcMethod, + scope: CoroutineScope, + handler: (TaintEdge) -> Unit, + ) { + summaryEdgesStorage + .getFacts(method) + .onEach { handler(it.edge) } + .launchIn(scope) + } + + fun vulnerabilityTraceGraph(vulnerability: TaintVulnerability): TraceGraph { + val result = getIfdsResultForMethod(vulnerability.method) + val initialGraph = result.buildTraceGraph(vulnerability.sink) + val resultGraph = initialGraph.copy(unresolvedCrossUnitCalls = emptyMap()) + + val resolvedCrossUnitEdges = hashSetOf, Vertex>>() + val unresolvedCrossUnitCalls = initialGraph.unresolvedCrossUnitCalls.entries.toMutableList() + while (unresolvedCrossUnitCalls.isNotEmpty()) { + val (caller, callees) = unresolvedCrossUnitCalls.removeLast() + + val unresolvedCallees = hashSetOf>() + for (callee in callees) { + if (resolvedCrossUnitEdges.add(caller to callee)) { + unresolvedCallees.add(callee) + } + } + + if (unresolvedCallees.isEmpty()) continue + + val callerResult = getIfdsResultForMethod(caller.method) + val callerGraph = callerResult.buildTraceGraph(caller) + resultGraph.mergeWithUpGraph(callerGraph, unresolvedCallees) + unresolvedCrossUnitCalls += callerGraph.unresolvedCrossUnitCalls.entries + } + + return resultGraph + } + + private fun getIfdsResultForMethod(method: JcMethod): IfdsResult { + val unit = unitResolver.resolve(method) + val runner = runnerForUnit[unit] ?: error("No runner for $unit") + return runner.getIfdsResult() + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintSummaries.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintSummaries.kt new file mode 100644 index 000000000..bb70b5349 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintSummaries.kt @@ -0,0 +1,31 @@ +/* + * 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.analysis.taint + +import org.jacodb.analysis.ifds.SummaryEdge +import org.jacodb.analysis.ifds.Vulnerability +import org.jacodb.taint.configuration.TaintMethodSink + +data class TaintSummaryEdge( + override val edge: TaintEdge, +) : SummaryEdge + +data class TaintVulnerability( + override val message: String, + override val sink: TaintVertex, + val rule: TaintMethodSink? = null, +) : Vulnerability diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Types.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Types.kt new file mode 100644 index 000000000..88be4cd3a --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Types.kt @@ -0,0 +1,25 @@ +/* + * 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.analysis.taint + +import org.jacodb.analysis.ifds.Edge +import org.jacodb.analysis.ifds.Runner +import org.jacodb.analysis.ifds.Vertex + +typealias TaintVertex = Vertex +typealias TaintEdge = Edge +typealias TaintRunner = Runner diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Sarif.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Sarif.kt new file mode 100644 index 000000000..85f1136f2 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Sarif.kt @@ -0,0 +1,28 @@ +/* + * 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.analysis.unused + +import org.jacodb.analysis.ifds.TraceGraph +import org.jacodb.analysis.sarif.VulnerabilityDescription +import org.jacodb.analysis.sarif.VulnerabilityInstance + +fun UnusedVariableVulnerability.toSarif(): VulnerabilityInstance { + return VulnerabilityInstance( + TraceGraph(sink, mutableSetOf(sink), mutableMapOf(), emptyMap()), + VulnerabilityDescription(ruleId = null, message = message) + ) +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableAnalyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableAnalyzer.kt new file mode 100644 index 000000000..2acf1b730 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableAnalyzer.kt @@ -0,0 +1,46 @@ +/* + * 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.analysis.unused + +import org.jacodb.analysis.ifds.Analyzer +import org.jacodb.analysis.ifds.Edge +import org.jacodb.analysis.ifds.Vertex +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst + +class UnusedVariableAnalyzer( + private val graph: JcApplicationGraph, +) : Analyzer { + + override val flowFunctions: UnusedVariableFlowFunctions by lazy { + UnusedVariableFlowFunctions(graph) + } + + private fun isExitPoint(statement: JcInst): Boolean { + return statement in graph.exitPoints(statement.location.method) + } + + override fun handleNewEdge(edge: Edge): List = buildList { + if (isExitPoint(edge.to.statement)) { + add(NewSummaryEdge(edge)) + } + } + + override fun handleCrossUnitCall(caller: Vertex, callee: Vertex): List { + return emptyList() + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableEvents.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableEvents.kt new file mode 100644 index 000000000..7073277dd --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableEvents.kt @@ -0,0 +1,25 @@ +/* + * 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.analysis.unused + +import org.jacodb.analysis.ifds.Edge + +sealed interface Event + +data class NewSummaryEdge( + val edge: Edge, +) : Event diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFacts.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFacts.kt new file mode 100644 index 000000000..4def264ad --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFacts.kt @@ -0,0 +1,31 @@ +/* + * 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.analysis.unused + +import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.api.cfg.JcInst + +sealed interface UnusedVariableDomainFact + +object UnusedVariableZeroFact : UnusedVariableDomainFact { + override fun toString(): String = "Zero" +} + +data class UnusedVariable( + val variable: AccessPath, + val initStatement: JcInst, +) : UnusedVariableDomainFact diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFlowFunctions.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFlowFunctions.kt new file mode 100644 index 000000000..bd9e3289a --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFlowFunctions.kt @@ -0,0 +1,120 @@ +/* + * 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.analysis.unused + +import org.jacodb.analysis.ifds.FlowFunction +import org.jacodb.analysis.ifds.FlowFunctions +import org.jacodb.analysis.ifds.toPath +import org.jacodb.analysis.ifds.toPathOrNull +import org.jacodb.analysis.util.getArgumentsOf +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcAssignInst +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.cfg.JcSpecialCallExpr +import org.jacodb.api.cfg.JcStaticCallExpr +import org.jacodb.api.ext.cfg.callExpr + +class UnusedVariableFlowFunctions( + private val graph: JcApplicationGraph, +) : FlowFunctions { + private val cp: JcClasspath + get() = graph.classpath + + override fun obtainPossibleStartFacts( + method: JcMethod, + ): Collection { + return setOf(UnusedVariableZeroFact) + } + + override fun obtainSequentFlowFunction( + current: JcInst, + next: JcInst, + ) = FlowFunction { fact -> + if (current !is JcAssignInst) { + return@FlowFunction setOf(fact) + } + + if (fact == UnusedVariableZeroFact) { + val toPath = current.lhv.toPath() + if (!toPath.isOnHeap) { + return@FlowFunction setOf(UnusedVariableZeroFact, UnusedVariable(toPath, current)) + } else { + return@FlowFunction setOf(UnusedVariableZeroFact) + } + } + check(fact is UnusedVariable) + + val toPath = current.lhv.toPath() + val default = if (toPath == fact.variable) emptySet() else setOf(fact) + val fromPath = current.rhv.toPathOrNull() + ?: return@FlowFunction default + + if (fromPath.isOnHeap || toPath.isOnHeap) { + return@FlowFunction default + } + + if (fromPath == fact.variable) { + return@FlowFunction default + fact.copy(variable = toPath) + } + + default + } + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, + ) = obtainSequentFlowFunction(callStatement, returnSite) + + override fun obtainCallToStartFlowFunction( + callStatement: JcInst, + calleeStart: JcInst, + ) = FlowFunction { fact -> + val callExpr = callStatement.callExpr + ?: error("Call statement should have non-null callExpr") + + if (fact == UnusedVariableZeroFact) { + if (callExpr !is JcStaticCallExpr && callExpr !is JcSpecialCallExpr) { + return@FlowFunction setOf(UnusedVariableZeroFact) + } + return@FlowFunction buildSet { + add(UnusedVariableZeroFact) + val callee = calleeStart.location.method + val formalParams = cp.getArgumentsOf(callee) + for (formal in formalParams) { + add(UnusedVariable(formal.toPath(), callStatement)) + } + } + } + check(fact is UnusedVariable) + + emptySet() + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, + exitStatement: JcInst, + ) = FlowFunction { fact -> + if (fact == UnusedVariableZeroFact) { + setOf(UnusedVariableZeroFact) + } else { + emptySet() + } + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableManager.kt new file mode 100644 index 000000000..ea3a99d01 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableManager.kt @@ -0,0 +1,260 @@ +/* + * 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.analysis.unused + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import org.jacodb.analysis.ifds.ControlEvent +import org.jacodb.analysis.ifds.Edge +import org.jacodb.analysis.ifds.Manager +import org.jacodb.analysis.ifds.QueueEmptinessChanged +import org.jacodb.analysis.ifds.Runner +import org.jacodb.analysis.ifds.SummaryStorageImpl +import org.jacodb.analysis.ifds.UniRunner +import org.jacodb.analysis.ifds.UnitResolver +import org.jacodb.analysis.ifds.UnitType +import org.jacodb.analysis.ifds.UnknownUnit +import org.jacodb.analysis.ifds.Vertex +import org.jacodb.analysis.util.getPathEdges +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.ExperimentalTime +import kotlin.time.TimeSource + +private val logger = mu.KotlinLogging.logger {} + +class UnusedVariableManager( + private val graph: JcApplicationGraph, + private val unitResolver: UnitResolver, +) : Manager { + + private val methodsForUnit: MutableMap> = hashMapOf() + private val runnerForUnit: MutableMap> = hashMapOf() + private val queueIsEmpty = ConcurrentHashMap() + + private val summaryEdgesStorage = SummaryStorageImpl() + private val vulnerabilitiesStorage = SummaryStorageImpl() + + private val stopRendezvous = Channel(Channel.RENDEZVOUS) + + private fun newRunner( + unit: UnitType, + ): Runner { + check(unit !in runnerForUnit) { "Runner for $unit already exists" } + + logger.debug { "Creating a new runner for $unit" } + val analyzer = UnusedVariableAnalyzer(graph) + val runner = UniRunner( + graph = graph, + analyzer = analyzer, + manager = this@UnusedVariableManager, + unitResolver = unitResolver, + unit = unit, + zeroFact = UnusedVariableZeroFact + ) + + runnerForUnit[unit] = runner + return runner + } + + private fun getAllCallees(method: JcMethod): Set { + val result: MutableSet = hashSetOf() + for (inst in method.flowGraph().instructions) { + result += graph.callees(inst) + } + return result + } + + private fun addStart(method: JcMethod) { + logger.info { "Adding start method: $method" } + val unit = unitResolver.resolve(method) + if (unit == UnknownUnit) return + val isNew = methodsForUnit.getOrPut(unit) { hashSetOf() }.add(method) + if (isNew) { + for (dep in getAllCallees(method)) { + addStart(dep) + } + } + } + + @JvmName("analyze") // needed for Java interop because of inline class (Duration) + @OptIn(ExperimentalTime::class) + fun analyze( + startMethods: List, + timeout: Duration = 3600.seconds, + ): List = runBlocking { + val timeStart = TimeSource.Monotonic.markNow() + + // Add start methods: + for (method in startMethods) { + addStart(method) + } + + // Determine all units: + val allUnits = methodsForUnit.keys.toList() + logger.info { + "Starting analysis of ${ + methodsForUnit.values.sumOf { it.size } + } methods in ${allUnits.size} units" + } + + // Spawn runner jobs: + val allJobs = allUnits.map { unit -> + // Create the runner: + val runner = newRunner(unit) + + // Start the runner: + launch(start = CoroutineStart.LAZY) { + val methods = methodsForUnit[unit]!!.toList() + runner.run(methods) + } + } + + // Spawn progress job: + val progress = launch(Dispatchers.IO) { + while (isActive) { + delay(1.seconds) + logger.info { + "Progress: propagated ${ + runnerForUnit.values.sumOf { it.getPathEdges().size } + } path edges" + } + } + } + + // Spawn stopper job: + val stopper = launch(Dispatchers.IO) { + stopRendezvous.receive() + logger.info { "Stopping all runners..." } + allJobs.forEach { it.cancel() } + } + + // Start all runner jobs: + val timeStartJobs = TimeSource.Monotonic.markNow() + allJobs.forEach { it.start() } + + // Await all runners: + withTimeoutOrNull(timeout) { + allJobs.joinAll() + } ?: run { + logger.info { "Timeout!" } + allJobs.forEach { it.cancel() } + allJobs.joinAll() + } + progress.cancelAndJoin() + stopper.cancelAndJoin() + logger.info { + "All ${allJobs.size} jobs completed in %.1f s".format( + timeStartJobs.elapsedNow().toDouble(DurationUnit.SECONDS) + ) + } + + // Extract found vulnerabilities (sinks): + val foundVulnerabilities = allUnits.flatMap { unit -> + val runner = runnerForUnit[unit] ?: error("No runner for $unit") + val result = runner.getIfdsResult() + val allFacts = result.facts + + val used = hashMapOf() + for ((inst, facts) in allFacts) { + for (fact in facts) { + if (fact is UnusedVariable) { + used.putIfAbsent(fact.initStatement, false) + if (fact.variable.isUsedAt(inst)) { + used[fact.initStatement] = true + } + } + + } + } + used.filterValues { !it }.keys.map { + UnusedVariableVulnerability( + message = "Assigned value is unused", + sink = Vertex(it, UnusedVariableZeroFact) + ) + } + } + + if (logger.isDebugEnabled) { + logger.debug { "Total found ${foundVulnerabilities.size} vulnerabilities" } + for (vulnerability in foundVulnerabilities) { + logger.debug { "$vulnerability in ${vulnerability.method}" } + } + } + logger.info { "Total sinks: ${foundVulnerabilities.size}" } + logger.info { + "Total propagated ${ + runnerForUnit.values.sumOf { it.getPathEdges().size } + } path edges" + } + logger.info { + "Analysis done in %.1f s".format( + timeStart.elapsedNow().toDouble(DurationUnit.SECONDS) + ) + } + foundVulnerabilities + } + + override fun handleEvent(event: Event) { + when (event) { + is NewSummaryEdge -> { + summaryEdgesStorage.add(UnusedVariableSummaryEdge(event.edge)) + } + } + } + + override fun handleControlEvent(event: ControlEvent) { + when (event) { + is QueueEmptinessChanged -> { + queueIsEmpty[event.runner.unit] = event.isEmpty + if (event.isEmpty) { + if (runnerForUnit.keys.all { queueIsEmpty[it] == true }) { + logger.debug { "All runners are empty" } + stopRendezvous.trySend(Unit).getOrNull() + } + } + } + } + } + + override fun subscribeOnSummaryEdges( + method: JcMethod, + scope: CoroutineScope, + handler: (Edge) -> Unit, + ) { + summaryEdgesStorage + .getFacts(method) + .onEach { handler(it.edge) } + .launchIn(scope) + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableSummaries.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableSummaries.kt new file mode 100644 index 000000000..91d203504 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableSummaries.kt @@ -0,0 +1,31 @@ +/* + * 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.analysis.unused + +import org.jacodb.analysis.ifds.Edge +import org.jacodb.analysis.ifds.SummaryEdge +import org.jacodb.analysis.ifds.Vertex +import org.jacodb.analysis.ifds.Vulnerability + +data class UnusedVariableSummaryEdge( + override val edge: Edge, +) : SummaryEdge + +data class UnusedVariableVulnerability( + override val message: String, + override val sink: Vertex, +) : Vulnerability diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Utils.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Utils.kt new file mode 100644 index 000000000..b21293831 --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Utils.kt @@ -0,0 +1,57 @@ +/* + * 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.analysis.unused + +import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.analysis.ifds.toPathOrNull +import org.jacodb.api.cfg.JcArrayAccess +import org.jacodb.api.cfg.JcAssignInst +import org.jacodb.api.cfg.JcBranchingInst +import org.jacodb.api.cfg.JcExpr +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.cfg.JcLocal +import org.jacodb.api.cfg.JcSpecialCallExpr +import org.jacodb.api.cfg.JcTerminatingInst +import org.jacodb.api.cfg.values +import org.jacodb.api.ext.cfg.callExpr + +internal fun AccessPath.isUsedAt(expr: JcExpr): Boolean { + return expr.values.any { it.toPathOrNull() == this } +} + +internal fun AccessPath.isUsedAt(inst: JcInst): Boolean { + val callExpr = inst.callExpr + + if (callExpr != null) { + // Don't count constructor calls as usages + if (callExpr.method.method.isConstructor && isUsedAt((callExpr as JcSpecialCallExpr).instance)) { + return false + } + + return isUsedAt(callExpr) + } + if (inst is JcAssignInst) { + if (inst.lhv is JcArrayAccess && isUsedAt((inst.lhv as JcArrayAccess))) { + return true + } + return isUsedAt(inst.rhv) && (inst.lhv !is JcLocal || inst.rhv !is JcLocal) + } + if (inst is JcTerminatingInst || inst is JcBranchingInst) { + return inst.operands.any { isUsedAt(it) } + } + return false +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/util/Utils.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/util/Utils.kt new file mode 100644 index 000000000..6b6bb924e --- /dev/null +++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/util/Utils.kt @@ -0,0 +1,57 @@ +/* + * 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.analysis.util + +import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.analysis.ifds.Edge +import org.jacodb.analysis.ifds.Runner +import org.jacodb.analysis.ifds.UniRunner +import org.jacodb.analysis.taint.TaintBidiRunner +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcMethod +import org.jacodb.api.JcParameter +import org.jacodb.api.cfg.JcArgument +import org.jacodb.api.cfg.JcThis +import org.jacodb.api.ext.toType + +val JcMethod.thisInstance: JcThis + get() = JcThis(enclosingClass.toType()) + +fun JcClasspath.getArgument(param: JcParameter): JcArgument? { + val t = findTypeOrNull(param.type.typeName) ?: return null + return JcArgument.of(param.index, param.name, t) +} + +fun JcClasspath.getArgumentsOf(method: JcMethod): List { + return method.parameters.map { getArgument(it)!! } +} + +internal fun Runner<*>.getPathEdges(): Set> = when (this) { + is UniRunner<*, *> -> pathEdges + is TaintBidiRunner -> forwardRunner.getPathEdges() + backwardRunner.getPathEdges() + else -> error("Cannot extract pathEdges for $this") +} + +fun AccessPath?.startsWith(other: AccessPath?): Boolean { + if (this == null || other == null) { + return false + } + if (this.value != other.value) { + return false + } + return this.accesses.take(other.accesses.size) == other.accesses +} diff --git a/jacodb-analysis/src/test/java/org/jacodb/analysis/impl/JavaAnalysisApiTest.java b/jacodb-analysis/src/test/java/org/jacodb/analysis/impl/JavaAnalysisApiTest.java index 5f5199df2..022f7a1c6 100644 --- a/jacodb-analysis/src/test/java/org/jacodb/analysis/impl/JavaAnalysisApiTest.java +++ b/jacodb-analysis/src/test/java/org/jacodb/analysis/impl/JavaAnalysisApiTest.java @@ -16,12 +16,11 @@ package org.jacodb.analysis.impl; -import org.jacodb.analysis.AnalysisMain; -import org.jacodb.analysis.engine.IfdsUnitRunnerFactory; -import org.jacodb.analysis.engine.UnitResolver; +import kotlin.time.DurationUnit; import org.jacodb.analysis.graph.ApplicationGraphFactory; -import org.jacodb.analysis.library.RunnersLibrary; -import org.jacodb.analysis.library.UnitResolversLibrary; +import org.jacodb.analysis.ifds.UnitResolver; +import org.jacodb.analysis.ifds.UnitResolverKt; +import org.jacodb.analysis.taint.TaintManager; import org.jacodb.api.JcClassOrInterface; import org.jacodb.api.JcClasspath; import org.jacodb.api.JcDatabase; @@ -40,6 +39,9 @@ import java.util.List; import java.util.concurrent.ExecutionException; +import static kotlin.time.DurationKt.toDuration; + + public class JavaAnalysisApiTest { private static JcClasspath classpath; @@ -56,18 +58,11 @@ public void testJavaAnalysisApi() throws ExecutionException, InterruptedExceptio List methodsToAnalyze = analyzedClass.getDeclaredMethods(); JcApplicationGraph applicationGraph = ApplicationGraphFactory - .asyncNewApplicationGraphForAnalysis(classpath, null) + .newApplicationGraphForAnalysisAsync(classpath, null) .get(); - UnitResolver resolver = UnitResolversLibrary.getMethodUnitResolver(); - IfdsUnitRunnerFactory runner = RunnersLibrary.getUnusedVariableRunnerFactory(); - - AnalysisMain.runAnalysis( - applicationGraph, - resolver, - runner, - methodsToAnalyze, - Integer.MAX_VALUE - ); + UnitResolver unitResolver = UnitResolverKt.getMethodUnitResolver(); + TaintManager manager = new TaintManager(applicationGraph, unitResolver, false); + manager.analyze(methodsToAnalyze, toDuration(30, DurationUnit.SECONDS)); } @Test @@ -76,8 +71,8 @@ public void testCustomBannedPackagesApi() throws ExecutionException, Interrupted bannedPackages.add("my.package.that.wont.be.analyzed"); JcApplicationGraph customGraph = ApplicationGraphFactory - .asyncNewApplicationGraphForAnalysis(classpath, bannedPackages) + .newApplicationGraphForAnalysisAsync(classpath, bannedPackages) .get(); Assertions.assertNotNull(customGraph); } -} \ No newline at end of file +} diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/AliasAnalysisTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/AliasAnalysisTest.kt deleted file mode 100644 index f090469c2..000000000 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/AliasAnalysisTest.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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.analysis.impl - -import kotlinx.coroutines.runBlocking -import org.jacodb.analysis.graph.defaultBannedPackagePrefixes -import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.analysis.library.MethodUnitResolver -import org.jacodb.analysis.library.analyzers.TaintAnalysisNode -import org.jacodb.analysis.library.analyzers.TaintNode -import org.jacodb.analysis.library.newAliasRunnerFactory -import org.jacodb.analysis.paths.toPath -import org.jacodb.analysis.runAnalysis -import org.jacodb.api.JcMethod -import org.jacodb.api.cfg.JcAssignInst -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.ext.cfg.callExpr -import org.jacodb.api.ext.findClass -import org.jacodb.testing.BaseTest -import org.jacodb.testing.WithGlobalDB -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.MethodSource -import java.util.stream.Stream -import kotlin.streams.asStream - -@Disabled("Needs modifications after introduction of summaries") -class AliasAnalysisTest : BaseTest() { - companion object : WithGlobalDB() { - - @JvmStatic - fun provideForPointerBenchBasic(): Stream = listOf( - Arguments.of("Branching1", listOf("38"), listOf("35")), - Arguments.of("Interprocedural1", listOf("48", "49"), listOf("41", "42")), - Arguments.of("Interprocedural2", listOf("51", "52"), listOf("43", "44", "58")), - Arguments.of("Parameter1", listOf("35", "arg$0"), emptyList()), - Arguments.of("Parameter2", listOf("37", "arg$0"), emptyList()), - Arguments.of("ReturnValue1", listOf("41", "42"), emptyList()), - Arguments.of("ReturnValue2", listOf("43", "45"), listOf("44")), - Arguments.of("ReturnValue3", listOf("46"), listOf("44", "45", "47")), - Arguments.of("SimpleAlias1", listOf("37", "38"), emptyList()) - // Loops1 Loops2 and Recursion1 are not tested because it is difficult to represent them as taint problem - ) - .asSequence() - .asStream() - - @JvmStatic - fun provideForPointerBenchGeneralJava(): Stream = listOf( - Arguments.of("Exception1", listOf("37", "38"), emptyList()), - - // Null1 and Null2 are not tested because it is difficult to represent them as taint problems - // Exception2 isn't tested because it needs analysis for possibly thrown exceptions - - Arguments.of("Interface1", listOf("40", "45"), listOf("38", "42", "43")), - Arguments.of("OuterClass1", listOf("55", "51"), listOf("50", "53")), - Arguments.of("StaticVariables1", listOf("39", "42", "StaticVariables1.a"), emptyList()), - Arguments.of("SuperClasses1", listOf("38", "42"), listOf("37", "40")), - ) - .asSequence() - .asStream() - - @JvmStatic - fun provideForPointerBenchCornerCases(): Stream = listOf( - Arguments.of("AccessPath1", listOf("38.f", "39.f"), listOf("38", "39")), - Arguments.of("ContextSensitivity1", listOf("arg$0", "arg$1"), emptyList()), - Arguments.of("ContextSensitivity2", listOf("arg$0", "arg$1"), emptyList()), - Arguments.of("ContextSensitivity3", listOf("arg$0", "arg$1"), emptyList()), - Arguments.of("FieldSensitivity1", listOf("42", "46"), listOf("43", "44")), - Arguments.of("FieldSensitivity2", listOf("43", "47"), listOf("44", "45")), - Arguments.of("ObjectSensitivity1", listOf("39", "45"), listOf("37", "41", "42", "44")), - Arguments.of("ObjectSensitivity2", listOf("39", "44"), listOf("37", "41", "43")), - Arguments.of("StrongUpdate1", listOf("43", "44"), listOf("37", "38")), - Arguments.of("StrongUpdate2", listOf("44"), listOf("41")), - ) - .asSequence() - .asStream() - } - - @ParameterizedTest - @MethodSource("provideForPointerBenchBasic") - fun testBasic(className: String, must: List, notMay: List) { - testPointerBench("pointerbench.basic.$className", must, notMay) - } - - - @ParameterizedTest - @MethodSource("provideForPointerBenchGeneralJava") - fun testGeneralJava(className: String, must: List, notMay: List) { - testPointerBench("pointerbench.generalJava.$className", must, notMay) - } - - @ParameterizedTest - @MethodSource("provideForPointerBenchCornerCases") - fun testCornerCases(className: String, must: List, notMay: List) { - testPointerBench("pointerbench.cornerCases.$className", must, notMay) - } - - private fun testPointerBench(className: String, must: List, notMay: List) { - val main = cp.findClass(className).declaredMethods.single { it.name == "main" } - - val res = findTaints(main) - println("For $className:\nOur result: $res\nmust: $must\nnotMay: $notMay") - - val notFoundFromMust = must.filter { it !in res } - assertEquals(emptyList(), notFoundFromMust) - - val foundFromNotMay = notMay.filter { it in res } - assertEquals(emptyList(), foundFromNotMay) - } - - private fun generates(inst: JcInst): List { - return if (inst is JcAssignInst && - inst.callExpr?.method?.name == "taint" && - inst.callExpr?.method?.method?.enclosingClass?.simpleName == "Benchmark" - ) { - listOf(TaintAnalysisNode(inst.lhv.toPath())) - } else { - emptyList() - } - } - - private fun isSanitizer(expr: JcExpr, fact: TaintNode): Boolean = TODO() - - private fun sinks(inst: JcInst): List = TODO() - - private fun findTaints(method: JcMethod): List { - val bannedPackagePrefixes = defaultBannedPackagePrefixes - .plus("pointerbench.benchmark.internal") - - val graph = runBlocking { - cp.newApplicationGraphForAnalysis( - defaultBannedPackagePrefixes + bannedPackagePrefixes - ) - } - - val result = runAnalysis( - graph, - MethodUnitResolver, - newAliasRunnerFactory(::generates, ::isSanitizer, ::sinks), - listOf(method) - ) - - return result.map { it.traceGraph.sink.statement.toString() } - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/BaseAnalysisTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/BaseAnalysisTest.kt index 3e4800614..e85491853 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/BaseAnalysisTest.kt +++ b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/BaseAnalysisTest.kt @@ -18,41 +18,52 @@ package org.jacodb.analysis.impl import juliet.support.AbstractTestCase import kotlinx.coroutines.runBlocking -import org.jacodb.analysis.engine.VulnerabilityInstance -import org.jacodb.api.JcClassOrInterface +import org.jacodb.analysis.graph.newApplicationGraphForAnalysis +import org.jacodb.analysis.ifds.Vulnerability +import org.jacodb.api.JcClasspath import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.ext.findClass import org.jacodb.api.ext.methods import org.jacodb.impl.features.classpaths.UnknownClasses import org.jacodb.impl.features.hierarchyExt +import org.jacodb.taint.configuration.TaintConfigurationFeature import org.jacodb.testing.BaseTest import org.jacodb.testing.WithGlobalDB import org.jacodb.testing.allClasspath -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.params.provider.Arguments import java.util.stream.Stream import kotlin.streams.asStream +private val logger = mu.KotlinLogging.logger {} + abstract class BaseAnalysisTest : BaseTest() { + companion object : WithGlobalDB(UnknownClasses) { - @JvmStatic - fun provideClassesForJuliet(cweNum: Int, cweSpecificBans: List = emptyList()): Stream = runBlocking { + + fun getJulietClasses( + cweNum: Int, + cweSpecificBans: List = emptyList(), + ): Sequence = runBlocking { val cp = db.classpath(allClasspath) val hierarchyExt = cp.hierarchyExt() val baseClass = cp.findClass() - val classes = hierarchyExt.findSubClasses(baseClass, false) - classes.toArguments("CWE${cweNum}_", cweSpecificBans) + hierarchyExt.findSubClasses(baseClass, false) + .map { it.name } + .filter { it.contains("CWE${cweNum}_") } + .filterNot { className -> (commonJulietBans + cweSpecificBans).any { className.contains(it) } } + .sorted() } - private fun Sequence.toArguments(cwe: String, cweSpecificBans: List): Stream = this - .map { it.name } - .filter { it.contains(cwe) } - .filterNot { className -> (commonJulietBans + cweSpecificBans).any { className.contains(it) } } -// .filter { it.contains("_68") } - .sorted() - .map { Arguments.of(it) } - .asStream() + @JvmStatic + fun provideClassesForJuliet( + cweNum: Int, + cweSpecificBans: List = emptyList(), + ): Stream = + getJulietClasses(cweNum, cweSpecificBans) + .map { Arguments.of(it) } + .asStream() private val commonJulietBans = listOf( // TODO: containers not supported @@ -75,42 +86,45 @@ abstract class BaseAnalysisTest : BaseTest() { ) } - protected abstract fun launchAnalysis(methods: List): List - - protected inline fun testOneAnalysisOnOneMethod( - vulnerabilityType: String, - methodName: String, - expectedLocations: Collection, - ) { - val method = cp.findClass().declaredMethods.single { it.name == methodName } - val sinks = findSinks(method, vulnerabilityType) - - // TODO: think about better assertions here - assertEquals(expectedLocations.size, sinks.size) - expectedLocations.forEach { expected -> - assertTrue(sinks.any { it.contains(expected) }) { - "$expected unmatched in:\n${sinks.joinToString("\n")}" - } + override val cp: JcClasspath = runBlocking { + val configFileName = "config_small.json" + val configResource = this.javaClass.getResourceAsStream("/$configFileName") + if (configResource != null) { + val configJson = configResource.bufferedReader().readText() + val configurationFeature = TaintConfigurationFeature.fromJson(configJson) + db.classpath(allClasspath, listOf(configurationFeature) + classpathFeatures) + } else { + super.cp } } - protected fun testSingleJulietClass(vulnerabilityType: String, className: String) { - val clazz = cp.findClass(className) - val goodMethod = clazz.methods.single { it.name == "good" } - val badMethod = clazz.methods.single { it.name == "bad" } + protected val graph: JcApplicationGraph by lazy { + runBlocking { + cp.newApplicationGraphForAnalysis() + } + } - val goodIssues = findSinks(goodMethod, vulnerabilityType) - val badIssues = findSinks(badMethod, vulnerabilityType) + protected fun testSingleJulietClass(className: String, findSinks: (JcMethod) -> List>) { + logger.info { className } - assertTrue(goodIssues.isEmpty()) - assertTrue(badIssues.isNotEmpty()) - } + val clazz = cp.findClass(className) + val badMethod = clazz.methods.single { it.name == "bad" } + val goodMethod = clazz.methods.single { it.name == "good" } - protected fun findSinks(method: JcMethod, vulnerabilityType: String): Set { - val sinks = launchAnalysis(listOf(method)) - .filter { it.vulnerabilityDescription.ruleId == vulnerabilityType } - .map { it.traceGraph.sink.toString() } + logger.info { "Searching for sinks in BAD method: $badMethod" } + val badIssues = findSinks(badMethod) + logger.info { "Total ${badIssues.size} issues in BAD method" } + for (issue in badIssues) { + logger.debug { " - $issue" } + } + assertTrue(badIssues.isNotEmpty()) { "Must find some sinks in 'bad' for $className" } - return sinks.toSet() + logger.info { "Searching for sinks in GOOD method: $goodMethod" } + val goodIssues = findSinks(goodMethod) + logger.info { "Total ${goodIssues.size} issues in GOOD method" } + for (issue in goodIssues) { + logger.debug { " - $issue" } + } + assertTrue(goodIssues.isEmpty()) { "Must NOT find any sinks in 'good' for $className" } } -} \ No newline at end of file +} diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/ConditionEvaluatorTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/ConditionEvaluatorTest.kt new file mode 100644 index 000000000..0611e483b --- /dev/null +++ b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/ConditionEvaluatorTest.kt @@ -0,0 +1,338 @@ +/* + * 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.analysis.impl + +import io.mockk.every +import io.mockk.mockk +import org.jacodb.analysis.config.BasicConditionEvaluator +import org.jacodb.analysis.config.FactAwareConditionEvaluator +import org.jacodb.analysis.ifds.Maybe +import org.jacodb.analysis.ifds.toMaybe +import org.jacodb.analysis.ifds.toPath +import org.jacodb.analysis.taint.Tainted +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcPrimitiveType +import org.jacodb.api.JcType +import org.jacodb.api.PredefinedPrimitive +import org.jacodb.api.PredefinedPrimitives +import org.jacodb.api.cfg.JcBool +import org.jacodb.api.cfg.JcInt +import org.jacodb.api.cfg.JcStringConstant +import org.jacodb.api.cfg.JcThis +import org.jacodb.api.cfg.JcValue +import org.jacodb.taint.configuration.And +import org.jacodb.taint.configuration.AnnotationType +import org.jacodb.taint.configuration.Argument +import org.jacodb.taint.configuration.Condition +import org.jacodb.taint.configuration.ConditionVisitor +import org.jacodb.taint.configuration.ConstantBooleanValue +import org.jacodb.taint.configuration.ConstantEq +import org.jacodb.taint.configuration.ConstantGt +import org.jacodb.taint.configuration.ConstantIntValue +import org.jacodb.taint.configuration.ConstantLt +import org.jacodb.taint.configuration.ConstantMatches +import org.jacodb.taint.configuration.ConstantStringValue +import org.jacodb.taint.configuration.ConstantTrue +import org.jacodb.taint.configuration.ContainsMark +import org.jacodb.taint.configuration.IsConstant +import org.jacodb.taint.configuration.IsType +import org.jacodb.taint.configuration.Not +import org.jacodb.taint.configuration.Or +import org.jacodb.taint.configuration.Position +import org.jacodb.taint.configuration.SourceFunctionMatches +import org.jacodb.taint.configuration.TaintMark +import org.jacodb.taint.configuration.This +import org.jacodb.taint.configuration.TypeMatches +import org.junit.jupiter.api.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ConditionEvaluatorTest { + + private val cp = mockk() + + private val intType: JcPrimitiveType = PredefinedPrimitive(cp, PredefinedPrimitives.Int) + private val boolType: JcPrimitiveType = PredefinedPrimitive(cp, PredefinedPrimitives.Boolean) + private val stringType = mockk { + every { classpath } returns cp + } + + private val intArg: Position = Argument(0) + private val intValue = JcInt(42, intType) + + private val boolArg: Position = Argument(1) + private val boolValue = JcBool(true, boolType) + + private val stringArg: Position = Argument(2) + private val stringValue = JcStringConstant("test", stringType) + + private val thisPos: Position = This + private val thisValue = JcThis(type = mockk()) + + private val positionResolver: (position: Position) -> Maybe = { position -> + when (position) { + intArg -> intValue + boolArg -> boolValue + stringArg -> stringValue + thisPos -> thisValue + else -> null + }.toMaybe() + } + private val evaluator: ConditionVisitor = BasicConditionEvaluator(positionResolver) + + @Test + fun `True is true`() { + val condition = ConstantTrue + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `Not(True) is false`() { + val condition = Not(ConstantTrue) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `Not(Not(True)) is true`() { + val condition = Not(Not(ConstantTrue)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `And(True) is true`() { + val condition = And(listOf(ConstantTrue, ConstantTrue, ConstantTrue)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `And(Not(True)) is false`() { + val condition = And(listOf(ConstantTrue, ConstantTrue, Not(ConstantTrue))) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `Or(Not(True)) is false`() { + val condition = Or(listOf(Not(ConstantTrue), Not(ConstantTrue), Not(ConstantTrue))) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `Or(True) is true`() { + val condition = Or(listOf(Not(ConstantTrue), Not(ConstantTrue), ConstantTrue)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `IsConstant(int) is true`() { + val condition = IsConstant(intArg) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `IsConstant(bool) is true`() { + val condition = IsConstant(boolArg) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `IsConstant(this) is false`() { + val condition = IsConstant(thisPos) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `IsConstant(unresolved) is false`() { + val condition = IsConstant(position = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `IsType in unexpected`() { + val condition = mockk() + assertFailsWith { + evaluator.visit(condition) + } + } + + @Test + fun `AnnotationType in unexpected`() { + val condition = mockk() + assertFailsWith { + evaluator.visit(condition) + } + } + + @Test + fun `ConstantEq(intArg(42), 42) is true`() { + val condition = ConstantEq(intArg, ConstantIntValue(42)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(intArg(42), 999) is false`() { + val condition = ConstantEq(intArg, ConstantIntValue(999)) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(boolArg(true), true) is true`() { + val condition = ConstantEq(boolArg, ConstantBooleanValue(true)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(boolArg(true), false) is false`() { + val condition = ConstantEq(boolArg, ConstantBooleanValue(false)) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(stringArg('test'), 'test') is true`() { + val condition = ConstantEq(stringArg, ConstantStringValue("test")) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(stringArg('test'), 'other') is false`() { + val condition = ConstantEq(stringArg, ConstantStringValue("other")) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(unresolved, any) is false`() { + val condition = ConstantEq(position = mockk(), value = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantLt(intArg(42), 999) is true`() { + val condition = ConstantLt(intArg, ConstantIntValue(999)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantLt(intArg(42), 5) is false`() { + val condition = ConstantLt(intArg, ConstantIntValue(5)) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantLt(unresolved, any) is false`() { + val condition = ConstantLt(position = mockk(), value = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantGt(intArg(42), 5) is true`() { + val condition = ConstantGt(intArg, ConstantIntValue(5)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantGt(intArg(42), 999) is false`() { + val condition = ConstantGt(intArg, ConstantIntValue(999)) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantGt(unresolved, any) is false`() { + val condition = ConstantGt(position = mockk(), value = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(intArg(42), '42') is true`() { + val condition = ConstantMatches(intArg, "42") + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(intArg(42), 'd+') is true`() { + val condition = ConstantMatches(intArg, "\\d+") + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(stringArg('test'), 'test') is true`() { + val condition = ConstantMatches(stringArg, "\"test\"") + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(stringArg('test'), 'w+') is true`() { + val condition = ConstantMatches(stringArg, "\"\\w+\"") + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(unresolved, any) is false`() { + val condition = ConstantMatches(position = mockk(), pattern = ".*") + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `SourceFunctionMatches is not implemented yet`() { + val condition = mockk() + assertFailsWith { + evaluator.visit(condition) + } + } + + @Test + fun `ContainsMark is not supported by basic evaluator`() { + val condition = mockk() + assertFailsWith { + evaluator.visit(condition) + } + } + + @Test + fun `TypeMatches(intArg, Int) is true`() { + val condition = TypeMatches(intArg, intType) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `TypeMatches(boolArg, Boolean) is true`() { + val condition = TypeMatches(boolArg, boolType) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `TypeMatches(stringArg, String) is true`() { + val condition = TypeMatches(stringArg, stringType) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `TypeMatches(unresolved, any) is false`() { + val condition = TypeMatches(position = mockk(), type = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `FactAwareConditionEvaluator supports ContainsMark`() { + val fact = Tainted(intValue.toPath(), TaintMark("FOO")) + val factAwareEvaluator = FactAwareConditionEvaluator(fact, positionResolver) + assertTrue(factAwareEvaluator.visit(ContainsMark(intArg, TaintMark("FOO")))) + assertFalse(factAwareEvaluator.visit(ContainsMark(intArg, TaintMark("BAR")))) + assertFalse(factAwareEvaluator.visit(ContainsMark(stringArg, TaintMark("FOO")))) + assertFalse(factAwareEvaluator.visit(ContainsMark(stringArg, TaintMark("BAR")))) + assertFalse(factAwareEvaluator.visit(ContainsMark(position = mockk(), TaintMark("FOO")))) + } +} diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/NpeAnalysisTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsNpeTest.kt similarity index 72% rename from jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/NpeAnalysisTest.kt rename to jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsNpeTest.kt index 01fec83a2..cbbf4e952 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/NpeAnalysisTest.kt +++ b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsNpeTest.kt @@ -17,19 +17,20 @@ package org.jacodb.analysis.impl import kotlinx.coroutines.runBlocking -import org.jacodb.analysis.engine.VulnerabilityInstance import org.jacodb.analysis.graph.JcApplicationGraphImpl -import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.analysis.library.SingletonUnitResolver -import org.jacodb.analysis.library.analyzers.NpeAnalyzer -import org.jacodb.analysis.library.newNpeRunnerFactory -import org.jacodb.analysis.runAnalysis +import org.jacodb.analysis.ifds.SingletonUnitResolver +import org.jacodb.analysis.npe.NpeManager +import org.jacodb.analysis.taint.TaintManager +import org.jacodb.analysis.taint.TaintVulnerability import org.jacodb.api.JcMethod import org.jacodb.api.ext.constructors import org.jacodb.api.ext.findClass +import org.jacodb.impl.features.InMemoryHierarchy +import org.jacodb.impl.features.Usages import org.jacodb.impl.features.usagesExt +import org.jacodb.testing.WithDB import org.jacodb.testing.analysis.NpeExamples -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -37,9 +38,13 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.util.* import java.util.stream.Stream +import kotlin.time.Duration.Companion.seconds -class NpeAnalysisTest : BaseAnalysisTest() { - companion object { +private val logger = mu.KotlinLogging.logger {} + +class IfdsNpeTest : BaseAnalysisTest() { + + companion object : WithDB(Usages, InMemoryHierarchy) { @JvmStatic fun provideClassesForJuliet476(): Stream = provideClassesForJuliet(476, listOf("null_check_after_deref")) @@ -47,15 +52,13 @@ class NpeAnalysisTest : BaseAnalysisTest() { @JvmStatic fun provideClassesForJuliet690(): Stream = provideClassesForJuliet(690) - - private const val vulnerabilityType = NpeAnalyzer.ruleId } @Test fun `fields resolving should work through interfaces`() = runBlocking { val graph = JcApplicationGraphImpl(cp, cp.usagesExt()) val callers = graph.callers(cp.findClass().constructors[2]) - println(callers.toList().size) + logger.debug { "callers: ${callers.toList().size}" } } @Test @@ -81,6 +84,12 @@ class NpeAnalysisTest : BaseAnalysisTest() { testOneMethod("checkedAccess", emptyList()) } + @Disabled("Aliasing") + @Test + fun `no NPE after checked access with field`() { + testOneMethod("checkedAccessWithField", emptyList()) + } + @Test fun `consecutive NPEs handled properly`() { testOneMethod( @@ -187,34 +196,45 @@ class NpeAnalysisTest : BaseAnalysisTest() { testOneMethod("nullAssignmentToCopy", emptyList()) } + private fun findSinks(method: JcMethod): List { + val unitResolver = SingletonUnitResolver + val manager = NpeManager(graph, unitResolver) + return manager.analyze(listOf(method), timeout = 30.seconds) + } + @ParameterizedTest @MethodSource("provideClassesForJuliet476") fun `test on Juliet's CWE 476`(className: String) { - testJuliet(className) + testSingleJulietClass(className, ::findSinks) } @ParameterizedTest @MethodSource("provideClassesForJuliet690") fun `test on Juliet's CWE 690`(className: String) { - testJuliet(className) + testSingleJulietClass(className, ::findSinks) } @Test - fun `analyse something`() { - val testingMethod = cp.findClass().declaredMethods.single { it.name == "id" } - val results = testingMethod.flowGraph() - print(results) - } + fun `test on specific Juliet's testcase`() { + // val className = "juliet.testcases.CWE476_NULL_Pointer_Dereference.CWE476_NULL_Pointer_Dereference__Integer_01" + // val className = "juliet.testcases.CWE690_NULL_Deref_From_Return.CWE690_NULL_Deref_From_Return__Class_StringBuilder_01" + val className = + "juliet.testcases.CWE690_NULL_Deref_From_Return.CWE690_NULL_Deref_From_Return__Properties_getProperty_equals_01" - private fun testJuliet(className: String) = testSingleJulietClass(vulnerabilityType, className) + testSingleJulietClass(className, ::findSinks) + } - private inline fun testOneMethod(methodName: String, expectedLocations: Collection) = - testOneAnalysisOnOneMethod(vulnerabilityType, methodName, expectedLocations) + private inline fun testOneMethod( + methodName: String, + expectedLocations: Collection, + ) { + val method = cp.findClass().declaredMethods.single { it.name == methodName } + val sinks = findSinks(method) - override fun launchAnalysis(methods: List): List { - val graph = runBlocking { - cp.newApplicationGraphForAnalysis() + // TODO: think about better assertions here + Assertions.assertEquals(expectedLocations.size, sinks.size) + expectedLocations.forEach { expected -> + Assertions.assertTrue(sinks.map { it.sink.toString() }.any { it.contains(expected) }) } - return runAnalysis(graph, SingletonUnitResolver, newNpeRunnerFactory(), methods) } -} \ No newline at end of file +} diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsSqlTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsSqlTest.kt new file mode 100644 index 000000000..aa88a286f --- /dev/null +++ b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsSqlTest.kt @@ -0,0 +1,110 @@ +/* + * 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.analysis.impl + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.jacodb.analysis.ifds.ClassUnitResolver +import org.jacodb.analysis.ifds.SingletonUnitResolver +import org.jacodb.analysis.sarif.sarifReportFromVulnerabilities +import org.jacodb.analysis.taint.TaintManager +import org.jacodb.analysis.taint.toSarif +import org.jacodb.api.ext.findClass +import org.jacodb.api.ext.methods +import org.jacodb.impl.features.InMemoryHierarchy +import org.jacodb.impl.features.Usages +import org.jacodb.testing.WithDB +import org.jacodb.testing.analysis.SqlInjectionExamples +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.time.Duration.Companion.seconds + +private val logger = mu.KotlinLogging.logger {} + +class IfdsSqlTest : BaseAnalysisTest() { + + companion object : WithDB(Usages, InMemoryHierarchy) { + @JvmStatic + fun provideClassesForJuliet89(): Stream = provideClassesForJuliet(89, specificBansCwe89) + + private val specificBansCwe89: List = listOf( + // Not working yet (#156) + "s03", "s04" + ) + } + + private val myJson = Json { + prettyPrint = true + } + + @Test + fun `simple SQL injection`() { + val methodName = "bad" + val method = cp.findClass().declaredMethods.single { it.name == methodName } + val methods = listOf(method) + val unitResolver = SingletonUnitResolver + val manager = TaintManager(graph, unitResolver) + val sinks = manager.analyze(methods, timeout = 30.seconds) + assertTrue(sinks.isNotEmpty()) + val sink = sinks.first() + val graph = manager.vulnerabilityTraceGraph(sink) + val trace = graph.getAllTraces().first() + assertTrue(trace.isNotEmpty()) + } + + @ParameterizedTest + @MethodSource("provideClassesForJuliet89") + fun `test on Juliet's CWE 89`(className: String) { + testSingleJulietClass(className) { method -> + val unitResolver = SingletonUnitResolver + val manager = TaintManager(graph, unitResolver) + manager.analyze(listOf(method), timeout = 30.seconds) + } + } + + @Test + fun `test on specific Juliet instance`() { + val className = "juliet.testcases.CWE89_SQL_Injection.s01.CWE89_SQL_Injection__connect_tcp_execute_01" + testSingleJulietClass(className) { method -> + val unitResolver = SingletonUnitResolver + val manager = TaintManager(graph, unitResolver) + manager.analyze(listOf(method), timeout = 30.seconds) + } + } + + @Test + fun `test bidirectional runner and other stuff`() { + val className = "juliet.testcases.CWE89_SQL_Injection.s01.CWE89_SQL_Injection__Environment_executeBatch_51a" + val clazz = cp.findClass(className) + val badMethod = clazz.methods.single { it.name == "bad" } + val unitResolver = ClassUnitResolver(true) + val manager = TaintManager(graph, unitResolver, useBidiRunner = true) + val sinks = manager.analyze(listOf(badMethod), timeout = 30.seconds) + assertTrue(sinks.isNotEmpty()) + val sink = sinks.first() + val graph = manager.vulnerabilityTraceGraph(sink) + val trace = graph.getAllTraces().first() + assertTrue(trace.isNotEmpty()) + val sarif = sarifReportFromVulnerabilities(listOf(sink.toSarif(graph))) + val sarifJson = myJson.encodeToString(sarif) + logger.info { "SARIF:\n$sarifJson" } + } +} diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsUnusedTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsUnusedTest.kt new file mode 100644 index 000000000..e20442d2c --- /dev/null +++ b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsUnusedTest.kt @@ -0,0 +1,79 @@ +/* + * 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.analysis.impl + +import org.jacodb.analysis.ifds.SingletonUnitResolver +import org.jacodb.analysis.unused.UnusedVariableManager +import org.jacodb.api.ext.findClass +import org.jacodb.api.ext.methods +import org.jacodb.impl.features.InMemoryHierarchy +import org.jacodb.impl.features.Usages +import org.jacodb.testing.WithDB +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.time.Duration.Companion.seconds + +class IfdsUnusedTest : BaseAnalysisTest() { + + companion object : WithDB(Usages, InMemoryHierarchy) { + @JvmStatic + fun provideClassesForJuliet563(): Stream = provideClassesForJuliet( + 563, listOf( + // Unused variables are already optimized out by cfg + "unused_uninit_variable_", + "unused_init_variable_int", + "unused_init_variable_long", + "unused_init_variable_String_", + + // Unused variable is generated by cfg (!!) + "unused_value_StringBuilder_17", + + // Expected answers are strange, seems to be problem in tests + "_12", + + // The variable isn't expected to be detected as unused actually + "_81" + ) + ) + } + + @ParameterizedTest + @MethodSource("provideClassesForJuliet563") + fun `test on Juliet's CWE 563`(className: String) { + testSingleJulietClass(className) { method -> + val unitResolver = SingletonUnitResolver + val manager = UnusedVariableManager(graph, unitResolver) + manager.analyze(listOf(method), timeout = 30.seconds) + } + } + + @Test + fun `test on specific Juliet instance`() { + val className = + "juliet.testcases.CWE563_Unused_Variable.CWE563_Unused_Variable__unused_init_variable_StringBuilder_01" + val clazz = cp.findClass(className) + val badMethod = clazz.methods.single { it.name == "bad" } + val unitResolver = SingletonUnitResolver + val manager = UnusedVariableManager(graph, unitResolver) + val sinks = manager.analyze(listOf(badMethod), timeout = 30.seconds) + Assertions.assertTrue(sinks.isNotEmpty()) + } +} diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/JodaDateTimeAnalysisTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/JodaDateTimeAnalysisTest.kt index 803ed6003..8b3cb41c2 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/JodaDateTimeAnalysisTest.kt +++ b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/JodaDateTimeAnalysisTest.kt @@ -17,44 +17,73 @@ package org.jacodb.analysis.impl import kotlinx.coroutines.runBlocking -import org.jacodb.analysis.engine.IfdsUnitRunnerFactory -import org.jacodb.analysis.engine.UnitResolver import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.analysis.library.MethodUnitResolver -import org.jacodb.analysis.library.UnusedVariableRunnerFactory -import org.jacodb.analysis.library.getClassUnitResolver -import org.jacodb.analysis.library.newNpeRunnerFactory -import org.jacodb.analysis.runAnalysis -import org.jacodb.analysis.sarif.SarifReport +import org.jacodb.analysis.ifds.SingletonUnitResolver +import org.jacodb.analysis.npe.NpeManager +import org.jacodb.analysis.taint.TaintManager +import org.jacodb.analysis.unused.UnusedVariableManager +import org.jacodb.api.JcClasspath +import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.ext.findClass +import org.jacodb.taint.configuration.TaintConfigurationFeature import org.jacodb.testing.BaseTest import org.jacodb.testing.WithGlobalDB +import org.jacodb.testing.allClasspath import org.joda.time.DateTime import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.seconds + +private val logger = mu.KotlinLogging.logger {} class JodaDateTimeAnalysisTest : BaseTest() { + companion object : WithGlobalDB() - private fun testOne(unitResolver: UnitResolver, ifdsUnitRunnerFactory: IfdsUnitRunnerFactory) { - val clazz = cp.findClass() - val result = runAnalysis(graph, unitResolver, ifdsUnitRunnerFactory, clazz.declaredMethods, 60000L) + override val cp: JcClasspath = runBlocking { + val configFileName = "config_small.json" + val configResource = this.javaClass.getResourceAsStream("/$configFileName") + if (configResource != null) { + val configJson = configResource.bufferedReader().readText() + val configurationFeature = TaintConfigurationFeature.fromJson(configJson) + db.classpath(allClasspath, listOf(configurationFeature) + classpathFeatures) + } else { + super.cp + } + } - println("Vulnerabilities found: ${result.size}") - println("Generated report:") - SarifReport.fromVulnerabilities(result).encodeToStream(System.out) + private val graph: JcApplicationGraph by lazy { + runBlocking { + cp.newApplicationGraphForAnalysis() + } } @Test - fun `test Unused variable analysis`() { - testOne(getClassUnitResolver(false), UnusedVariableRunnerFactory) + fun `test taint analysis`() { + val clazz = cp.findClass() + val methods = clazz.declaredMethods + val unitResolver = SingletonUnitResolver + val manager = TaintManager(graph, unitResolver) + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Vulnerabilities found: ${sinks.size}" } } @Test fun `test NPE analysis`() { - testOne(MethodUnitResolver, newNpeRunnerFactory()) + val clazz = cp.findClass() + val methods = clazz.declaredMethods + val unitResolver = SingletonUnitResolver + val manager = NpeManager(graph, unitResolver) + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Vulnerabilities found: ${sinks.size}" } } - private val graph = runBlocking { - cp.newApplicationGraphForAnalysis() + @Test + fun `test unused variables analysis`() { + val clazz = cp.findClass() + val methods = clazz.declaredMethods + val unitResolver = SingletonUnitResolver + val manager = UnusedVariableManager(graph, unitResolver) + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Unused variables found: ${sinks.size}" } } -} \ No newline at end of file +} diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/SqlInjectionAnalysisTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/SqlInjectionAnalysisTest.kt deleted file mode 100644 index da9697fbc..000000000 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/SqlInjectionAnalysisTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.analysis.impl - -import kotlinx.coroutines.runBlocking -import org.jacodb.analysis.engine.VulnerabilityInstance -import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.analysis.library.SingletonUnitResolver -import org.jacodb.analysis.library.analyzers.SqlInjectionAnalyzer -import org.jacodb.analysis.library.newSqlInjectionRunnerFactory -import org.jacodb.analysis.runAnalysis -import org.jacodb.api.JcMethod -import org.jacodb.impl.features.InMemoryHierarchy -import org.jacodb.impl.features.Usages -import org.jacodb.testing.WithDB -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.MethodSource -import java.util.stream.Stream - -class SqlInjectionAnalysisTest : BaseAnalysisTest() { - companion object : WithDB(Usages, InMemoryHierarchy) { - @JvmStatic - fun provideClassesForJuliet89(): Stream = provideClassesForJuliet(89, listOf( - // Not working yet (#156) - "s03", "s04" - )) - - private val vulnerabilityType = SqlInjectionAnalyzer.vulnerabilityDescription.ruleId - } - - @ParameterizedTest - @MethodSource("provideClassesForJuliet89") - fun `test on Juliet's CWE 89`(className: String) { - testSingleJulietClass(vulnerabilityType, className) - } - - override fun launchAnalysis(methods: List): List { - val graph = runBlocking { - cp.newApplicationGraphForAnalysis() - } - return runAnalysis(graph, SingletonUnitResolver, newSqlInjectionRunnerFactory(), methods) - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/TaintFlowFunctionsTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/TaintFlowFunctionsTest.kt new file mode 100644 index 000000000..e37c753b4 --- /dev/null +++ b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/TaintFlowFunctionsTest.kt @@ -0,0 +1,225 @@ +/* + * 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.analysis.impl + +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.jacodb.analysis.ifds.FlowFunctions +import org.jacodb.analysis.util.getArgument +import org.jacodb.analysis.ifds.toPath +import org.jacodb.analysis.taint.ForwardTaintFlowFunctions +import org.jacodb.analysis.taint.TaintDomainFact +import org.jacodb.analysis.taint.Tainted +import org.jacodb.analysis.taint.TaintZeroFact +import org.jacodb.api.JcClassType +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcArgument +import org.jacodb.api.cfg.JcAssignInst +import org.jacodb.api.cfg.JcCallExpr +import org.jacodb.api.cfg.JcCallInst +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.cfg.JcLocal +import org.jacodb.api.cfg.JcLocalVar +import org.jacodb.api.cfg.JcReturnInst +import org.jacodb.api.ext.cfg.callExpr +import org.jacodb.api.ext.findTypeOrNull +import org.jacodb.api.ext.packageName +import org.jacodb.impl.features.InMemoryHierarchy +import org.jacodb.impl.features.Usages +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.jacodb.taint.configuration.TaintMark +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 TaintFlowFunctionsTest : BaseTest() { + + companion object : WithDB(Usages, InMemoryHierarchy) + + override val cp: JcClasspath = runBlocking { + val configFileName = "config_test.json" + val configResource = this.javaClass.getResourceAsStream("/$configFileName") + if (configResource != null) { + val configJson = configResource.bufferedReader().readText() + val configurationFeature = TaintConfigurationFeature.fromJson(configJson) + db.classpath(allClasspath, listOf(configurationFeature) + classpathFeatures) + } else { + super.cp + } + } + + private val graph: JcApplicationGraph = mockk { + every { callees(any()) } answers { + sequenceOf(arg(0).callExpr!!.method.method) + } + } + + private val stringType = cp.findTypeOrNull() as JcClassType + + private val testMethod = mockk { + every { name } returns "test" + every { enclosingClass } returns mockk(relaxed = true) { + every { packageName } returns "com.example" + every { simpleName } returns "Example" + every { name } returns "com.example.Example" + every { superClass } returns null + every { interfaces } returns emptyList() + } + every { isConstructor } returns false + every { returnType } returns mockk(relaxed = true) + every { parameters } returns listOf( + mockk(relaxed = true) { + every { index } returns 0 + every { type } returns mockk { + every { typeName } returns "java.lang.String" + } + } + ) + } + + @Test + fun `test obtain start facts`() { + val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) + val facts = flowSpace.obtainPossibleStartFacts(testMethod).toList() + val arg0 = cp.getArgument(testMethod.parameters[0])!! + val arg0Taint = Tainted(arg0.toPath(), TaintMark("EXAMPLE")) + Assertions.assertEquals(listOf(TaintZeroFact, arg0Taint), facts) + } + + @Test + fun `test sequential flow function assign mark`() { + // "x := y", where 'y' is tainted, should result in both 'x' and 'y' to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val y: JcLocal = JcLocalVar(2, "y", stringType) + val inst = JcAssignInst(location = mockk(), lhv = x, rhv = y) + val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) + val f = flowSpace.obtainSequentFlowFunction(inst, next = mockk()) + val yTaint = Tainted(y.toPath(), TaintMark("TAINT")) + val xTaint = Tainted(x.toPath(), TaintMark("TAINT")) + val facts = f.compute(yTaint).toList() + Assertions.assertEquals(listOf(yTaint, xTaint), facts) + } + + @Test + fun `test call flow function assign mark`() { + // "x := test(...)", where 'test' is a source, should result in 'x' to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val callStatement = JcAssignInst(location = mockk(), lhv = x, rhv = mockk { + every { method } returns mockk { + every { method } returns testMethod + } + }) + val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) + val f = flowSpace.obtainCallToReturnSiteFlowFunction(callStatement, returnSite = mockk()) + val xTaint = Tainted(x.toPath(), TaintMark("EXAMPLE")) + val facts = f.compute(TaintZeroFact).toList() + Assertions.assertEquals(listOf(TaintZeroFact, xTaint), facts) + } + + @Test + fun `test call flow function remove mark`() { + // "test(x)", where 'x' is tainted, should result in 'x' NOT to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val callStatement = JcCallInst(location = mockk(), callExpr = mockk { + every { method } returns mockk { + every { method } returns testMethod + } + every { args } returns listOf(x) + }) + val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) + val f = flowSpace.obtainCallToReturnSiteFlowFunction(callStatement, returnSite = mockk()) + val xTaint = Tainted(x.toPath(), TaintMark("REMOVE")) + val facts = f.compute(xTaint).toList() + Assertions.assertTrue(facts.isEmpty()) + } + + @Test + fun `test call flow function copy mark`() { + // "y := test(x)" should result in 'y' to be tainted only when 'x' is tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val y: JcLocal = JcLocalVar(2, "y", stringType) + val callStatement = JcAssignInst(location = mockk(), lhv = y, rhv = mockk { + every { method } returns mockk { + every { method } returns testMethod + } + every { args } returns listOf(x) + }) + val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) + val f = flowSpace.obtainCallToReturnSiteFlowFunction(callStatement, returnSite = mockk()) + val xTaint = Tainted(x.toPath(), TaintMark("COPY")) + val yTaint = Tainted(y.toPath(), TaintMark("COPY")) + val facts = f.compute(xTaint).toList() + Assertions.assertEquals(listOf(xTaint, yTaint), facts) // copy from x to y + val other: JcLocal = JcLocalVar(10, "other", stringType) + val otherTaint = Tainted(other.toPath(), TaintMark("OTHER")) + val facts2 = f.compute(otherTaint).toList() + Assertions.assertEquals(listOf(otherTaint), facts2) // pass-through + } + + @Test + fun `test call to start flow function`() { + // "test(x)", where 'x' is tainted, should result in 'x' (formal argument of 'test') to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val callStatement = JcCallInst(location = mockk(), callExpr = mockk { + every { method } returns mockk { + every { method } returns testMethod + } + every { args } returns listOf(x) + }) + val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) + val f = flowSpace.obtainCallToStartFlowFunction(callStatement, calleeStart = mockk { + every { location } returns mockk { + every { method } returns testMethod + } + }) + val xTaint = Tainted(x.toPath(), TaintMark("TAINT")) + val arg0: JcArgument = cp.getArgument(testMethod.parameters[0])!! + val arg0Taint = Tainted(arg0.toPath(), TaintMark("TAINT")) + val facts = f.compute(xTaint).toList() + Assertions.assertEquals(listOf(arg0Taint), facts) + val other: JcLocal = JcLocalVar(10, "other", stringType) + val otherTaint = Tainted(other.toPath(), TaintMark("TAINT")) + val facts2 = f.compute(otherTaint).toList() + Assertions.assertTrue(facts2.isEmpty()) + } + + @Test + fun `test exit flow function`() { + // "x := test()" + "return y", where 'y' is tainted, should result in 'x' to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val callStatement = JcAssignInst(location = mockk(), lhv = x, rhv = mockk { + every { method } returns mockk { + every { method } returns testMethod + } + }) + val y: JcLocal = JcLocalVar(1, "y", stringType) + val exitStatement = JcReturnInst(location = mockk { + every { method } returns testMethod + }, returnValue = y) + val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) + val f = flowSpace.obtainExitToReturnSiteFlowFunction(callStatement, returnSite = mockk(), exitStatement) + val yTaint = Tainted(y.toPath(), TaintMark("TAINT")) + val xTaint = Tainted(x.toPath(), TaintMark("TAINT")) + val facts = f.compute(yTaint).toList() + Assertions.assertEquals(listOf(xTaint), facts) + } +} diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/UnusedVariableTest.kt b/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/UnusedVariableTest.kt deleted file mode 100644 index b8884a88b..000000000 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/UnusedVariableTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.analysis.impl - -import kotlinx.coroutines.runBlocking -import org.jacodb.analysis.engine.VulnerabilityInstance -import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.analysis.library.SingletonUnitResolver -import org.jacodb.analysis.library.UnusedVariableRunnerFactory -import org.jacodb.analysis.library.analyzers.UnusedVariableAnalyzer -import org.jacodb.analysis.runAnalysis -import org.jacodb.api.JcMethod -import org.jacodb.impl.features.InMemoryHierarchy -import org.jacodb.impl.features.Usages -import org.jacodb.testing.WithDB -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.MethodSource -import java.util.* -import java.util.stream.Stream - -class UnusedVariableTest : BaseAnalysisTest() { - companion object : WithDB(Usages, InMemoryHierarchy) { - @JvmStatic - fun provideClassesForJuliet563(): Stream = provideClassesForJuliet(563, listOf( - // Unused variables are already optimized out by cfg - "unused_uninit_variable_", "unused_init_variable_int", "unused_init_variable_long", "unused_init_variable_String_", - - // Unused variable is generated by cfg (!!) - "unused_value_StringBuilder_17", - - // Expected answers are strange, seems to be problem in tests - "_12", - - // The variable isn't expected to be detected as unused actually - "_81" - )) - - private const val vulnerabilityType = UnusedVariableAnalyzer.ruleId - } - - @ParameterizedTest - @MethodSource("provideClassesForJuliet563") - fun `test on Juliet's CWE 563`(className: String) { - testSingleJulietClass(vulnerabilityType, className) - } - - override fun launchAnalysis(methods: List): List { - val graph = runBlocking { - cp.newApplicationGraphForAnalysis() - } - return runAnalysis(graph, SingletonUnitResolver, UnusedVariableRunnerFactory, methods) - } -} \ No newline at end of file diff --git a/jacodb-analysis/src/test/resources/additional.json b/jacodb-analysis/src/test/resources/additional.json new file mode 100644 index 000000000..4c883436e --- /dev/null +++ b/jacodb-analysis/src/test/resources/additional.json @@ -0,0 +1,607 @@ +[ + { + "_": "EntryPointSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": ".*" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "AnnotationType", + "position": { + "_": "Argument", + "number": 0 + }, + "type": { + "_": "ClassMatcher", + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "org.springframework.web.bind.annotation" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "RequestParam" + } + } + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "XSS" + } + } + ] + }, + { + "_": "EntryPointSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": ".*" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "AnnotationType", + "position": { + "_": "Argument", + "number": 1 + }, + "type": { + "_": "ClassMatcher", + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "org.springframework.web.bind.annotation" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "RequestParam" + } + } + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Argument", + "number": 1 + }, + "mark": { + "name": "XSS" + } + } + ] + }, + { + "_": "EntryPointSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": ".*" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "AnnotationType", + "position": { + "_": "Argument", + "number": 2 + }, + "type": { + "_": "ClassMatcher", + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "org.springframework.web.bind.annotation" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "RequestParam" + } + } + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Argument", + "number": 2 + }, + "mark": { + "name": "XSS" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.net" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "URLDecoder" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "decode" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Connection" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "prepareCall|prepareStatement" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "MethodSink", + "ruleNote": "SQL Injection", + "cwe": [ + 89 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "PreparedStatement" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "execute|executeQuery|executeUpdate|executeLargeUpdate" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "NETWORK" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ARGS" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "LDAP" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "FORM" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "WEBSERVICE" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "PROPERTY" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ENVIRONMENT" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ICC" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "STREAM" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "FILE_SYSTEM" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "JSON" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "STDIN" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "DATABASE" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "CHANNEL" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "WEB" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "CONSOLE" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "XML" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "XSS" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "GUI_FORM" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "NAMING" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "REGISTRY" + } + } + ] + } + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.util" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Map" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "get" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.util" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Map" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "put|replace" + }, + "applyToOverrides": true, + "exclude": [], + "functionLabel": null, + "modifier": -1, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + } + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 1 + }, + "to": { + "_": "This" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Base64" + }, + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "org.apache.commons.codec.binary" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "decodeBase64" + }, + "applyToOverrides": true, + "exclude": [], + "functionLabel": null, + "modifier": -1, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + } + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + } +] diff --git a/jacodb-analysis/src/test/resources/config_small.json b/jacodb-analysis/src/test/resources/config_small.json new file mode 100644 index 000000000..b9d648e9d --- /dev/null +++ b/jacodb-analysis/src/test/resources/config_small.json @@ -0,0 +1,706 @@ +[ + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "System" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "getenv" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "ENVIRONMENT" + } + } + ] + }, + { + "_": "MethodSink", + "ruleNote": "SQL Injection", + "cwe": [ + 89 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "Statement" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "execute|executeQuery|executeUpdate|executeLargeUpdate" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "UNTRUSTED" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "INJECTION" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "ENVIRONMENT" + } + } + ] + } + }, + { + "_": "MethodSink", + "ruleNote": "SQL Injection", + "cwe": [ + 89 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "Statement" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "executeBatch|executeLargeBatch" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "UNTRUSTED" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "INJECTION" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ENVIRONMENT" + } + } + ] + } + }, + { + "_": "MethodSink", + "ruleNote": "SQL Injection", + "cwe": [ + 89 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "PreparedStatement" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "execute|executeQuery|executeUpdate|executeLargeUpdate" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "UNTRUSTED" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "INJECTION" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ENVIRONMENT" + } + } + ] + } + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "String" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "split" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "ResultAnyElement" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "String" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "concat" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + }, + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "StringBuilder" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "append" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "This" + } + }, + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + }, + + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "StringBuilder" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "toString" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Statement" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "addBatch" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "This" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "ResultSet" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "get.*" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "UNTRUSTED" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Connection" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "prepareStatement" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.io" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "BufferedReader" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "readLine" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "UNTRUSTED" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.util" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Properties" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "getProperty" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "NULLNESS" + } + } + ] + }, + { + "_": "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": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "NULLNESS" + } + } + ] + } +] diff --git a/jacodb-analysis/src/test/resources/config_test.json b/jacodb-analysis/src/test/resources/config_test.json new file mode 100644 index 000000000..86260231e --- /dev/null +++ b/jacodb-analysis/src/test/resources/config_test.json @@ -0,0 +1,174 @@ +[ + { + "_": "EntryPointSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "com.example" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "test" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "EXAMPLE" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "com.example" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "test" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "EXAMPLE" + } + } + ] + }, + { + "_": "Cleaner", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "com.example" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "test" + }, + "applyToOverrides": true, + "exclude": [], + "functionLabel": null, + "modifier": -1, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + } + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "RemoveMark", + "mark": { + "name": "REMOVE" + }, + "position": { + "_": "Argument", + "number": 0 + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "com.example" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "test" + }, + "applyToOverrides": true, + "exclude": [], + "functionLabel": null, + "modifier": -1, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + } + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyMark", + "mark": { + "name": "COPY" + }, + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + } +] diff --git a/jacodb-analysis/src/test/resources/simplelogger.properties b/jacodb-analysis/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..de8ce921f --- /dev/null +++ b/jacodb-analysis/src/test/resources/simplelogger.properties @@ -0,0 +1,34 @@ +# SLF4J's SimpleLogger configuration file +# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. + +# Default logging detail level for all instances of SimpleLogger. +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, defaults to "info". +org.slf4j.simpleLogger.defaultLogLevel=info + +# Logging detail level for a SimpleLogger instance named "xxxxx". +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, the default logging detail level is used. +org.slf4j.simpleLogger.log.org.jacodb.analysis.ifds=debug + +# Set to true if you want the current date and time to be included in output messages. +# Default is false, and will output the number of milliseconds elapsed since startup. +org.slf4j.simpleLogger.showDateTime=true + +# The date and time format to be used in the output messages. +# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. +# If the format is not specified or is invalid, the default format is used. +# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. +org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss + +# Set to true if you want to output the current thread name. +# Defaults to true. +org.slf4j.simpleLogger.showThreadName=false + +# Set to true if you want the Logger instance name to be included in output messages. +# Defaults to true. +org.slf4j.simpleLogger.showLogName=true + +# Set to true if you want the last component of the name to be included in output messages. +# Defaults to false. +org.slf4j.simpleLogger.showShortLogName=true diff --git a/jacodb-api/src/main/kotlin/org/jacodb/api/analysis/ApplicationGraph.kt b/jacodb-api/src/main/kotlin/org/jacodb/api/analysis/ApplicationGraph.kt index 000da830a..0b413e5fe 100644 --- a/jacodb-api/src/main/kotlin/org/jacodb/api/analysis/ApplicationGraph.kt +++ b/jacodb-api/src/main/kotlin/org/jacodb/api/analysis/ApplicationGraph.kt @@ -26,7 +26,7 @@ interface ApplicationGraph { fun callees(node: Statement): Sequence fun callers(method: Method): Sequence - fun entryPoint(method: Method): Sequence + fun entryPoints(method: Method): Sequence fun exitPoints(method: Method): Sequence fun methodOf(node: Statement): Method diff --git a/jacodb-cli/build.gradle.kts b/jacodb-cli/build.gradle.kts index e39e519d0..56a8dfec6 100644 --- a/jacodb-cli/build.gradle.kts +++ b/jacodb-cli/build.gradle.kts @@ -1,3 +1,7 @@ +plugins { + kotlin("plugin.serialization") +} + dependencies { api(project(":jacodb-core")) api(project(":jacodb-analysis")) diff --git a/jacodb-cli/src/main/kotlin/org/jacodb/cli/main.kt b/jacodb-cli/src/main/kotlin/org/jacodb/cli/main.kt index 24900399d..2bbee2bbc 100644 --- a/jacodb-cli/src/main/kotlin/org/jacodb/cli/main.kt +++ b/jacodb-cli/src/main/kotlin/org/jacodb/cli/main.kt @@ -21,19 +21,21 @@ import kotlinx.cli.ArgType import kotlinx.cli.default import kotlinx.cli.required import kotlinx.coroutines.runBlocking +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json -import mu.KLogging -import org.jacodb.analysis.AnalysisConfig -import org.jacodb.analysis.engine.UnitResolver -import org.jacodb.analysis.engine.VulnerabilityInstance +import kotlinx.serialization.json.encodeToStream import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.analysis.library.MethodUnitResolver -import org.jacodb.analysis.library.UnusedVariableRunnerFactory -import org.jacodb.analysis.library.newNpeRunnerFactory -import org.jacodb.analysis.library.newSqlInjectionRunnerFactory -import org.jacodb.analysis.runAnalysis -import org.jacodb.analysis.sarif.SarifReport +import org.jacodb.analysis.ifds.SingletonUnitResolver +import org.jacodb.analysis.ifds.UnitResolver +import org.jacodb.analysis.npe.NpeManager +import org.jacodb.analysis.sarif.VulnerabilityInstance +import org.jacodb.analysis.sarif.sarifReportFromVulnerabilities +import org.jacodb.analysis.taint.TaintManager +import org.jacodb.analysis.taint.toSarif +import org.jacodb.analysis.unused.UnusedVariableManager +import org.jacodb.analysis.unused.toSarif import org.jacodb.api.JcClassOrInterface import org.jacodb.api.JcClassProcessingTask import org.jacodb.api.JcMethod @@ -43,35 +45,54 @@ import org.jacodb.impl.features.Usages import org.jacodb.impl.jacodb import java.io.File import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration.Companion.seconds -private val logger = object : KLogging() {}.logger +private val logger = mu.KotlinLogging.logger {} class AnalysisMain { fun run(args: List) = main(args.toTypedArray()) } -fun launchAnalysesByConfig(config: AnalysisConfig, graph: JcApplicationGraph, methods: List): List> { +typealias AnalysesOptions = Map + +@Serializable +data class AnalysisConfig(val analyses: Map) + +fun launchAnalysesByConfig( + config: AnalysisConfig, + graph: JcApplicationGraph, + methods: List, +): List>> { return config.analyses.mapNotNull { (analysis, options) -> val unitResolver = options["UnitResolver"]?.let { UnitResolver.getByName(it) - } ?: MethodUnitResolver + } ?: SingletonUnitResolver + + when (analysis) { + "NPE" -> { + val manager = NpeManager(graph, unitResolver) + manager.analyze(methods, timeout = 60.seconds).map { it.toSarif(manager.vulnerabilityTraceGraph(it)) } + } + + "Unused" -> { + val manager = UnusedVariableManager(graph, unitResolver) + manager.analyze(methods, timeout = 60.seconds).map { it.toSarif() } + } + + "SQL" -> { + val manager = TaintManager(graph, unitResolver) + manager.analyze(methods, timeout = 60.seconds).map { it.toSarif(manager.vulnerabilityTraceGraph(it)) } + } - val runner = when (analysis) { - "NPE" -> newNpeRunnerFactory() - "Unused" -> UnusedVariableRunnerFactory - "SQL" -> newSqlInjectionRunnerFactory() else -> { logger.error { "Unknown analysis type: $analysis" } return@mapNotNull null } } - - logger.info { "Launching analysis $analysis" } - runAnalysis(graph, unitResolver, runner, methods) } } - +@OptIn(ExperimentalSerializationApi::class) fun main(args: Array) { val parser = ArgParser("taint-analysis") val configFilePath by parser.option( @@ -150,9 +171,12 @@ fun main(args: Array) { } val vulnerabilities = launchAnalysesByConfig(config, graph, startJcMethods).flatten() - val report = SarifReport.fromVulnerabilities(vulnerabilities) + val report = sarifReportFromVulnerabilities(vulnerabilities) + val prettyJson = Json { + prettyPrint = true + } outputFile.outputStream().use { fileOutputStream -> - report.encodeToStream(fileOutputStream) + prettyJson.encodeToStream(report, fileOutputStream) } -} \ No newline at end of file +} diff --git a/jacodb-core/build.gradle.kts b/jacodb-core/build.gradle.kts index 5eab623c7..b800d11c4 100644 --- a/jacodb-core/build.gradle.kts +++ b/jacodb-core/build.gradle.kts @@ -48,7 +48,6 @@ dependencies { testFixturesImplementation(platform(Libs.junit_bom)) testFixturesImplementation(Libs.junit_jupiter) testFixturesImplementation(Libs.guava) - testFixturesImplementation(Libs.mockito_core) testFixturesImplementation(Libs.jetbrains_annotations) testFixturesImplementation(Libs.kotlinx_coroutines_core) } diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/JcGraphImpl.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/JcGraphImpl.kt index b7d28f775..68dd869fe 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/JcGraphImpl.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/JcGraphImpl.kt @@ -47,8 +47,7 @@ class JcGraphImpl( private val exceptionResolver = JcExceptionResolver(classpath) override val entry: JcInst get() = instructions.first() - override val exits: List get() = instructions.filterIsInstance() - + override val exits: List by lazy { instructions.filterIsInstance() } /** * returns a map of possible exceptions that may be thrown from this method @@ -106,15 +105,15 @@ class JcGraphImpl( /** * `successors` and `predecessors` represent normal control flow */ - override fun successors(node: JcInst): Set = successorMap.getOrDefault(node, emptySet()) - override fun predecessors(node: JcInst): Set = predecessorMap.getOrDefault(node, emptySet()) + override fun successors(node: JcInst): Set = successorMap[node] ?: emptySet() + override fun predecessors(node: JcInst): Set = predecessorMap[node] ?: emptySet() /** * `throwers` and `catchers` represent control flow when an exception occurs * `throwers` returns an empty set for every instruction except `JcCatchInst` */ - override fun throwers(node: JcInst): Set = throwPredecessors.getOrDefault(node, emptySet()) - override fun catchers(node: JcInst): Set = throwSuccessors.getOrDefault(node, emptySet()) + override fun throwers(node: JcInst): Set = throwPredecessors[node] ?: emptySet() + override fun catchers(node: JcInst): Set = throwSuccessors[node] ?: emptySet() override fun previous(inst: JcInstRef): JcInst = previous(inst(inst)) override fun next(inst: JcInstRef): JcInst = next(inst(inst)) diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/JcInstListImpl.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/JcInstListImpl.kt index 8fe7a5485..cc7a4b5d2 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/JcInstListImpl.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/JcInstListImpl.kt @@ -58,7 +58,7 @@ class JcMutableInstListImpl(instructions: List) : JcInstListImpl) { val index = _instructions.indexOf(inst) assert(index >= 0) diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/JcUnknownClass.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/JcUnknownClass.kt index 20577c5c9..ca0442165 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/JcUnknownClass.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/JcUnknownClass.kt @@ -40,28 +40,30 @@ class JcUnknownClass(override var classpath: JcClasspath, name: String) : JcVirt class JcUnknownMethod( enclosingClass: JcClassOrInterface, name: String, + access: Int, description: String, returnType: TypeName, params: List ) : JcVirtualMethodImpl( name, + access, returnType = returnType, parameters = params.mapIndexed { index, typeName -> JcVirtualParameter(index, typeName) }, description = description ) { companion object { - fun method(type: JcClassOrInterface, name: String, description: String): JcMethod { + fun method(type: JcClassOrInterface, name: String, access: Int, description: String): JcMethod { val methodType = Type.getMethodType(description) val returnType = TypeNameImpl(methodType.returnType.className.jcdbName()) val paramsType = methodType.argumentTypes.map { TypeNameImpl(it.className.jcdbName()) } - return JcUnknownMethod(type, name, description, returnType, paramsType) + return JcUnknownMethod(type, name, access, description, returnType, paramsType) } - fun typedMethod(type: JcClassType, name: String, description: String): JcTypedMethod { + fun typedMethod(type: JcClassType, name: String, access: Int, description: String): JcTypedMethod { return JcTypedMethodImpl( type, - method(type.jcClass, name, description), + method(type.jcClass, name, access, description), JcSubstitutorImpl.empty ) } diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/JcUnknownType.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/JcUnknownType.kt index bbda79e3e..03e2d2d85 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/JcUnknownType.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/JcUnknownType.kt @@ -59,30 +59,36 @@ class JcUnknownType(override var classpath: JcClasspath, private val name: Strin open class JcUnknownClassLookup(val clazz: JcClassOrInterface) : JcLookup { - override fun specialMethod(name: String, description: String): JcMethod = method(name, description) - override fun staticMethod(name: String, description: String): JcMethod = method(name, description) + override fun specialMethod(name: String, description: String): JcMethod = + JcUnknownMethod.method(clazz, name, access = Opcodes.ACC_PUBLIC, description) + + override fun staticMethod(name: String, description: String): JcMethod = + JcUnknownMethod.method(clazz, name, access = Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC, description) override fun field(name: String, typeName: TypeName?): JcField { return JcUnknownField(clazz, name, typeName ?: TypeNameImpl(OBJECT_CLASS)) } override fun method(name: String, description: String): JcMethod { - return JcUnknownMethod.method(clazz, name, description) + return JcUnknownMethod.method(clazz, name, access = Opcodes.ACC_PUBLIC, description) } } open class JcUnknownTypeLookup(val type: JcClassType) : JcLookup { - override fun specialMethod(name: String, description: String): JcTypedMethod = method(name, description) - override fun staticMethod(name: String, description: String): JcTypedMethod = method(name, description) + override fun specialMethod(name: String, description: String): JcTypedMethod = + JcUnknownMethod.typedMethod(type, name, access = Opcodes.ACC_PUBLIC, description) + + override fun staticMethod(name: String, description: String): JcTypedMethod = + JcUnknownMethod.typedMethod(type, name, access = Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC, description) override fun field(name: String, typeName: TypeName?): JcTypedField { return JcUnknownField.typedField(type, name, typeName ?: TypeNameImpl(OBJECT_CLASS)) } override fun method(name: String, description: String): JcTypedMethod { - return JcUnknownMethod.typedMethod(type, name, description) + return JcUnknownMethod.typedMethod(type, name, access = Opcodes.ACC_PUBLIC, description) } -} \ No newline at end of file +} diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/MethodInstructionsFeature.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/MethodInstructionsFeature.kt index 02e18cfab..c403bafb1 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/MethodInstructionsFeature.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/features/classpaths/MethodInstructionsFeature.kt @@ -39,7 +39,7 @@ class MethodInstructionsFeature( private val JcMethod.methodFeatures get() = enclosingClass.classpath.features?.filterIsInstance().orEmpty() - + @Synchronized override fun flowGraph(method: JcMethod): JcMethodExtFeature.JcFlowGraphResult { return JcFlowGraphResultImpl(method, JcGraphImpl(method, method.instList.instructions)) } diff --git a/jacodb-core/src/testFixtures/java/org/jacodb/testing/analysis/NpeExamples.java b/jacodb-core/src/testFixtures/java/org/jacodb/testing/analysis/NpeExamples.java index 6679be09d..4476b58fc 100644 --- a/jacodb-core/src/testFixtures/java/org/jacodb/testing/analysis/NpeExamples.java +++ b/jacodb-core/src/testFixtures/java/org/jacodb/testing/analysis/NpeExamples.java @@ -105,7 +105,7 @@ private void foo(ContainerOfSimpleClass z) { SimpleClassWithField x = z.g; x.field = null; } - + int npeOnLength() { String x = "abc"; String y = "def"; @@ -135,6 +135,19 @@ int checkedAccess(String x) { return -1; } + int checkedAccessWithField() { + SimpleClassWithField x = new SimpleClassWithField("abc"); + int s = 0; + if (x.field != null) { + s += x.field.length(); + } + x.field = null; + if (x.field != null) { + s += x.field.length(); + } + return s; + } + int consecutiveNPEs(String x, boolean flag) { int a = 0; int b = 0; @@ -299,4 +312,4 @@ int noNPEAfterAliasing() { // (because backward alias analysis can only propagate facts back, but can't kill them) return x.field.length(); } -} \ No newline at end of file +} diff --git a/jacodb-core/src/testFixtures/java/org/jacodb/testing/analysis/SqlInjectionExamples.java b/jacodb-core/src/testFixtures/java/org/jacodb/testing/analysis/SqlInjectionExamples.java new file mode 100644 index 000000000..68cd6b4e1 --- /dev/null +++ b/jacodb-core/src/testFixtures/java/org/jacodb/testing/analysis/SqlInjectionExamples.java @@ -0,0 +1,47 @@ +/* + * 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.testing.analysis; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +@SuppressWarnings("ALL") +public class SqlInjectionExamples { + + void bad() { + String data = System.getenv("USER"); + try ( + Connection dbConnection = DriverManager.getConnection("", "", ""); + Statement sqlStatement = dbConnection.createStatement(); + ) { + boolean result = sqlStatement.execute("insert into users (status) values ('updated') where name='" + data + "'"); + + if (result) { + System.out.println("User '" + data + "' updated successfully"); + } else { + System.out.println("Unable to update records for user '" + data + "'"); + } + } catch (SQLException e) { + System.err.println("Error: " + e); + } finally { + System.out.println("OK!"); + } + } + +} diff --git a/jacodb-taint-configuration/build.gradle.kts b/jacodb-taint-configuration/build.gradle.kts index 9acc300bf..87707cc30 100644 --- a/jacodb-taint-configuration/build.gradle.kts +++ b/jacodb-taint-configuration/build.gradle.kts @@ -11,9 +11,11 @@ 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") + implementation(Libs.kotlinx_serialization_core) + implementation(Libs.kotlinx_serialization_json) // for local tests only + + testImplementation(Libs.kotlin_logging) } tasks.test { diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/Position.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/Position.kt index 78ec0f1a9..b5b625608 100644 --- a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/Position.kt +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/Position.kt @@ -19,7 +19,7 @@ package org.jacodb.taint.configuration import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -interface PositionResolver { +fun interface PositionResolver { fun resolve(position: Position): R } @@ -28,16 +28,28 @@ sealed interface Position @Serializable @SerialName("Argument") -data class Argument(val number: Int) : Position +data class Argument(@SerialName("number") val index: Int) : Position @Serializable @SerialName("AnyArgument") -object AnyArgument : Position +object AnyArgument : Position { + override fun toString(): String = javaClass.simpleName +} @Serializable @SerialName("This") -object ThisArgument : Position +object This : Position { + override fun toString(): String = javaClass.simpleName +} @Serializable @SerialName("Result") -object Result : Position +object Result : Position { + override fun toString(): String = javaClass.simpleName +} + +@Serializable +@SerialName("ResultAnyElement") +object ResultAnyElement : Position { + override fun toString(): String = javaClass.simpleName +} diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintAction.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintAction.kt index b3b6210f7..9b9506958 100644 --- a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintAction.kt +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintAction.kt @@ -18,8 +18,11 @@ package org.jacodb.taint.configuration import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass -interface TaintActionVisitor { +interface TaintActionVisitor { fun visit(action: CopyAllMarks): R fun visit(action: CopyMark): R fun visit(action: AssignMark): R @@ -31,33 +34,59 @@ interface Action { fun accept(visitor: TaintActionVisitor): R } +val actionModule = SerializersModule { + polymorphic(Action::class) { + subclass(CopyAllMarks::class) + subclass(CopyMark::class) + subclass(AssignMark::class) + subclass(RemoveAllMarks::class) + subclass(RemoveMark::class) + } +} + // 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 { +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 { +@SerialName("CopyMark") +data class CopyMark( + val mark: TaintMark, + val from: Position, + val to: Position, +) : Action { override fun accept(visitor: TaintActionVisitor): R = visitor.visit(this) } @Serializable -@SerialName("RemoveAllMarks") -data class RemoveAllMarks(val position: Position) : Action { +@SerialName("AssignMark") +data class AssignMark( + val mark: TaintMark, + 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 { +@SerialName("RemoveAllMarks") +data class RemoveAllMarks( + val position: Position, +) : 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 { +@SerialName("RemoveMark") +data class RemoveMark( + val mark: TaintMark, + val position: Position, +) : Action { override fun accept(visitor: TaintActionVisitor): R = visitor.visit(this) } diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintCondition.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintCondition.kt index a775a55be..e037856de 100644 --- a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintCondition.kt +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintCondition.kt @@ -18,12 +18,16 @@ package org.jacodb.taint.configuration import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass import org.jacodb.api.JcType -interface ConditionVisitor { +interface ConditionVisitor { + fun visit(condition: ConstantTrue): R + fun visit(condition: Not): R 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 @@ -32,10 +36,7 @@ interface ConditionVisitor { 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: ContainsMark): R fun visit(condition: TypeMatches): R } @@ -43,35 +44,70 @@ interface Condition { fun accept(conditionVisitor: ConditionVisitor): R } +val conditionModule = SerializersModule { + polymorphic(Condition::class) { + subclass(ConstantTrue::class) + subclass(Not::class) + subclass(And::class) + subclass(Or::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(ContainsMark::class) + subclass(TypeMatches::class) + } +} + @Serializable -@SerialName("And") -data class And(@SerialName("args") val args: List) : Condition { +@SerialName("ConstantTrue") +object ConstantTrue : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) + + override fun toString(): String = javaClass.simpleName } @Serializable -@SerialName("Or") -data class Or(@SerialName("args") val args: List) : Condition { +@SerialName("Not") +data class Not( + @SerialName("condition") val arg: Condition, +) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } @Serializable -@SerialName("Not") -data class Not(val condition: Condition) : Condition { +@SerialName("And") +data class And( + val args: List, +) : Condition { + override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) +} + +@Serializable +@SerialName("Or") +data class Or( + val args: List, +) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } @Serializable @SerialName("IsConstant") -data class IsConstant(val position: Position) : Condition { +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 + val position: Position, + @SerialName("type") val typeMatcher: TypeMatcher, ) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } @@ -79,8 +115,8 @@ data class IsType( @Serializable @SerialName("AnnotationType") data class AnnotationType( - @SerialName("position") val position: Position, - @SerialName("type") val typeMatcher: TypeMatcher + val position: Position, + @SerialName("type") val typeMatcher: TypeMatcher, ) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } @@ -88,8 +124,8 @@ data class AnnotationType( @Serializable @SerialName("ConstantEq") data class ConstantEq( - @SerialName("position") val position: Position, - @SerialName("constant") val value: ConstantValue + val position: Position, + @SerialName("constant") val value: ConstantValue, ) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } @@ -97,8 +133,8 @@ data class ConstantEq( @Serializable @SerialName("ConstantLt") data class ConstantLt( - @SerialName("position") val position: Position, - @SerialName("constant") val value: ConstantValue + val position: Position, + @SerialName("constant") val value: ConstantValue, ) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } @@ -106,8 +142,8 @@ data class ConstantLt( @Serializable @SerialName("ConstantGt") data class ConstantGt( - @SerialName("position") val position: Position, - @SerialName("constant") val value: ConstantValue + val position: Position, + @SerialName("constant") val value: ConstantValue, ) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } @@ -115,8 +151,8 @@ data class ConstantGt( @Serializable @SerialName("ConstantMatches") data class ConstantMatches( - @SerialName("position") val position: Position, - @SerialName("pattern") val pattern: String + val position: Position, + val pattern: String, ) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } @@ -124,32 +160,27 @@ data class ConstantMatches( @Serializable @SerialName("SourceFunctionMatches") data class SourceFunctionMatches( - @SerialName("position") val position: Position, - @SerialName("sourceFunction") val functionMatcher: FunctionMatcher + 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 +data class ContainsMark( + val position: Position, + 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 { +data class TypeMatches( + val position: Position, + val type: JcType, +) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } @@ -172,34 +203,42 @@ data class ConstantStringValue(val value: String) : ConstantValue sealed interface NameMatcher @Serializable -@SerialName("NameMatches") -data class NamePatternMatcher(val pattern: String) : NameMatcher +@SerialName("NameIsEqualTo") +data class NameExactMatcher( + val name: String, +) : NameMatcher @Serializable -@SerialName("NameIsEqualTo") -data class NameExactMatcher(val name: String) : NameMatcher +@SerialName("NameMatches") +data class NamePatternMatcher( + val pattern: String, +) : NameMatcher @Serializable @SerialName("AnyNameMatches") -object AnyNameMatcher : NameMatcher +object AnyNameMatcher : NameMatcher { + override fun toString(): String = javaClass.simpleName +} @Serializable sealed interface TypeMatcher +@Serializable +@SerialName("PrimitiveNameMatches") +data class PrimitiveNameMatcher(val name: String) : TypeMatcher + @Serializable @SerialName("ClassMatcher") data class ClassMatcher( @SerialName("packageMatcher") val pkg: NameMatcher, - @SerialName("classNameMatcher") val classNameMatcher: NameMatcher + val classNameMatcher: NameMatcher, ) : TypeMatcher -@Serializable -@SerialName("PrimitiveNameMatches") -data class PrimitiveNameMatcher(val name: String) : TypeMatcher - @Serializable @SerialName("AnyTypeMatches") -object AnyTypeMatcher : TypeMatcher +object AnyTypeMatcher : TypeMatcher { + override fun toString(): String = javaClass.simpleName +} @Serializable @SerialName("FunctionMatches") @@ -211,9 +250,12 @@ data class FunctionMatcher( val applyToOverrides: Boolean, val functionLabel: String?, val modifier: Int, - val exclude: List + val exclude: List, ) @Serializable @SerialName("ParameterMatches") -data class ParameterMatcher(val index: Int, val typeMatcher: TypeMatcher) +data class ParameterMatcher( + val index: Int, + val typeMatcher: TypeMatcher, +) diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintConfigurationFeature.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintConfigurationFeature.kt index 1412322bb..f653d3292 100644 --- a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintConfigurationFeature.kt +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintConfigurationFeature.kt @@ -19,29 +19,38 @@ package org.jacodb.taint.configuration 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.api.JcClassOrInterface +import org.jacodb.api.JcClasspathFeature +import org.jacodb.api.JcMethod +import org.jacodb.api.JcPrimitiveType +import org.jacodb.api.PredefinedPrimitives +import org.jacodb.api.TypeName +import org.jacodb.api.ext.allSuperHierarchySequence +import org.jacodb.api.ext.boolean +import org.jacodb.api.ext.byte +import org.jacodb.api.ext.char +import org.jacodb.api.ext.double +import org.jacodb.api.ext.float +import org.jacodb.api.ext.int +import org.jacodb.api.ext.long +import org.jacodb.api.ext.short +import org.jacodb.impl.cfg.util.isArray import java.nio.file.Path import kotlin.io.path.readText - class TaintConfigurationFeature private constructor( jsonConfig: String, - additionalSerializersModule: SerializersModule? + 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 serializers = SerializersModule { + include(defaultSerializationModule) + additionalSerializersModule?.let { include(it) } + } val json = Json { classDiscriminator = CLASS_DISCRIMINATOR @@ -49,19 +58,36 @@ class TaintConfigurationFeature private constructor( 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)) + val configuration = json + .decodeFromString>(jsonConfig) + .map { + when (it) { + is SerializedTaintEntryPointSource -> it.copy( + condition = it.condition.accept(conditionSimplifier) + ) + + is SerializedTaintMethodSource -> it.copy( + condition = it.condition.accept(conditionSimplifier) + ) + + is SerializedTaintMethodSink -> it.copy( + condition = it.condition.accept(conditionSimplifier) + ) + + is SerializedTaintPassThrough -> it.copy( + condition = it.condition.accept(conditionSimplifier) + ) + + is SerializedTaintCleaner -> it.copy( + condition = it.condition.accept(conditionSimplifier) + ) + } } - } ConfigurationTrie(configuration, ::matches) } + @Synchronized fun getConfigForMethod(method: JcMethod): List = resolveConfigForMethod(method) @@ -84,7 +110,6 @@ class TaintConfigurationFeature private constructor( return primitiveTypesSet!! } - private fun resolveConfigForMethod(method: JcMethod): List { val taintConfigurationItems = rulesForMethod[method] if (taintConfigurationItems != null) { @@ -100,7 +125,6 @@ class TaintConfigurationFeature private constructor( val functionMatcher = it.methodInfo if (!functionMatcher.matches(method)) return@mapNotNullTo null - it.resolveForMethod(method) } @@ -113,14 +137,13 @@ class TaintConfigurationFeature private constructor( 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) + val rules = destination.distinct() + rulesForMethod[method] = rules + return rules } private fun getClassRules(clazz: JcClassOrInterface) = rulesByClass.getOrPut(clazz) { @@ -131,18 +154,15 @@ class TaintConfigurationFeature private constructor( 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 @@ -152,7 +172,6 @@ class TaintConfigurationFeature private constructor( } val isExcluded = exclude.any { it.matches(method) } - return !isExcluded } @@ -163,9 +182,7 @@ class TaintConfigurationFeature private constructor( private fun ClassMatcher.matches(pkgName: String, className: String): Boolean { val packageMatches = matches(pkg, pkgName) - if (!packageMatches) return false - return matches(classNameMatcher, className) } @@ -179,7 +196,6 @@ class TaintConfigurationFeature private constructor( } } - private fun TypeMatcher.matches(typeName: TypeName): Boolean = matches(typeName.typeName) private fun TypeMatcher.matches(typeName: String): Boolean = @@ -191,57 +207,82 @@ class TaintConfigurationFeature private constructor( 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 SerializedTaintMethodSink -> TaintMethodSink( + method, + ruleNote, + cwe, + condition.resolve(method) + ) + is SerializedTaintPassThrough -> TaintPassThrough( method, condition.resolve(method), actionsAfter.resolve(method) ) + + is SerializedTaintCleaner -> TaintCleaner( + 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 Condition.resolve(method: JcMethod): Condition = this + .accept(ConditionSpecializer(method)) + .accept(conditionSimplifier) + + private fun List.resolve(method: JcMethod): List = + flatMap { it.accept(ActionSpecializer(method)) } + private fun specializePosition(method: JcMethod, position: Position): List = when { + !inBounds(method, position) -> emptyList() + position !is AnyArgument -> listOf(position) + else -> method.parameters.indices.map { Argument(it) }.filter { inBounds(method, it) } + } + + private fun mkTrue(): Condition = ConstantTrue + private fun mkFalse(): Condition = Not(ConstantTrue) - 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) = when (conditions.size) { + 0 -> mkFalse() + 1 -> conditions.single() + else -> Or(conditions) } - 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 mkAnd(conditions: List) = when (conditions.size) { + 0 -> mkTrue() + 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 + is Argument -> position.index in method.parameters.indices + This -> !method.isStatic Result -> method.returnType.typeName != PredefinedPrimitives.Void - ThisArgument -> !method.isStatic + ResultAnyElement -> method.returnType.isArray } 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) } } @@ -250,11 +291,9 @@ class TaintConfigurationFeature private constructor( 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) } } @@ -271,19 +310,22 @@ class TaintConfigurationFeature private constructor( } 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: 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: Or): Condition = + mkOr(condition.args.map { it.accept(this) }) - override fun visit(condition: Not): Condition = Not(condition.condition.accept(this)) + override fun visit(condition: Not): Condition = + Not(condition.arg.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 }) } @@ -341,7 +383,28 @@ class TaintConfigurationFeature private constructor( return mkOr(disjuncts) } - override fun visit(condition: AnnotationType): Condition = ConstantTrue // TODO("Not yet implemented") + override fun visit(condition: AnnotationType): Condition { + val positions = specializePosition(method, condition.position) + return if (positions.any { methodAnnotationMatches(it, condition.typeMatcher) }) { + mkTrue() + } else { + mkFalse() + } + } + + private fun methodAnnotationMatches(position: Position, matcher: TypeMatcher): Boolean { + when (position) { + is Argument -> { + val annotations = method.parameters.getOrNull(position.index)?.annotations + return annotations?.any { matcher.matches(it.name) } ?: false + } + + Result -> TODO("What does it mean?") + This -> TODO("What does it mean?") + AnyArgument -> error("Must not occur here") + ResultAnyElement -> error("Must not occur here") + } + } override fun visit(condition: ConstantEq): Condition = mkOr(specializePosition(method, condition.position).map { condition.copy(position = it) }) @@ -357,7 +420,7 @@ class TaintConfigurationFeature private constructor( override fun visit(condition: SourceFunctionMatches): Condition = ConstantTrue // TODO Not implemented yet - override fun visit(condition: CallParameterContainsMark): Condition = + override fun visit(condition: ContainsMark): Condition = mkOr(specializePosition(method, condition.position).map { condition.copy(position = it) }) override fun visit(condition: ConstantTrue): Condition = condition @@ -365,101 +428,82 @@ class TaintConfigurationFeature private constructor( 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 val conditionSimplifier = object : ConditionVisitor { + override fun visit(condition: And): Condition { + val queue = ArrayDeque(condition.args) + val args = mutableListOf() + while (queue.isNotEmpty()) { + val it = queue.removeFirst().accept(this) + if (it is ConstantTrue) { + // skip + } else if (it is Not && it.arg is ConstantTrue) { + return mkFalse() + } else if (it is And) { + queue += it.args + } else { + args += it } } + return mkAnd(args) + } - 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 + override fun visit(condition: Or): Condition { + val queue = ArrayDeque(condition.args) + val args = mutableListOf() + while (queue.isNotEmpty()) { + val it = queue.removeFirst().accept(this) + if (it is ConstantTrue) { + return mkTrue() + } else if (it is Not && it.arg is ConstantTrue) { + // skip + } else if (it is Or) { + queue += it.args + } else { + args += it + } } - conjuncts += it.accept(this) + return mkOr(args) } - 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 + override fun visit(condition: Not): Condition { + val arg = condition.arg.accept(this) + return if (arg is Not) { + // Eliminate double negation: + arg.arg + } else { + Not(arg) } - conjuncts += it.accept(this) } - return conjuncts.singleOrNull() ?: Or(conjuncts.asReversed()) + 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: ContainsMark): Condition = condition + override fun visit(condition: ConstantTrue): Condition = condition + override fun visit(condition: TypeMatches): Condition = condition } - 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 + companion object { + fun fromJson( + jsonConfig: String, + serializersModule: SerializersModule? = null, + ) = TaintConfigurationFeature(jsonConfig, serializersModule) - override fun visit(condition: CallParameterContainsMark): Condition = condition + fun fromPath( + configPath: Path, + serializersModule: SerializersModule? = null, + ) = fromJson(configPath.readText(), serializersModule) - override fun visit(condition: ConstantTrue): Condition = condition + val defaultSerializationModule = SerializersModule { + include(conditionModule) + include(actionModule) + } - override fun visit(condition: TypeMatches): Condition = condition + private const val CLASS_DISCRIMINATOR = "_" + } } diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintConfigurationItem.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintConfigurationItem.kt index 9b6ad6585..6828d85e4 100644 --- a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintConfigurationItem.kt +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintConfigurationItem.kt @@ -21,31 +21,32 @@ import org.jacodb.api.JcMethod sealed interface TaintConfigurationItem data class TaintEntryPointSource( - val methodInfo: JcMethod, + val method: JcMethod, val condition: Condition, val actionsAfter: List, ) : TaintConfigurationItem data class TaintMethodSource( - val methodInfo: JcMethod, + val method: JcMethod, val condition: Condition, val actionsAfter: List, ) : TaintConfigurationItem data class TaintMethodSink( + val method: JcMethod, + val ruleNote: String, + val cwe: List, val condition: Condition, - val methodInfo: JcMethod, ) : TaintConfigurationItem data class TaintPassThrough( - val methodInfo: JcMethod, + val method: JcMethod, val condition: Condition, val actionsAfter: List, ) : TaintConfigurationItem data class TaintCleaner( - val methodInfo: JcMethod, + val method: JcMethod, val condition: Condition, val actionsAfter: List, ) : TaintConfigurationItem - diff --git a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintMark.kt b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintMark.kt index 5192fa73d..52c2c7aed 100644 --- a/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintMark.kt +++ b/jacodb-taint-configuration/src/main/kotlin/org/jacodb/taint/configuration/TaintMark.kt @@ -16,9 +16,13 @@ package org.jacodb.taint.configuration -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -@SerialName("TaintMark") -data class TaintMark(val name: String) \ No newline at end of file +data class TaintMark(val name: String) { + override fun toString(): String = name + + companion object { + val NULLNESS: TaintMark = TaintMark("NULLNESS") + } +}