diff --git a/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/http/HttpConceptBuilder.kt b/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/http/HttpConceptBuilder.kt index 30813897f24..e22ef8f4ae6 100644 --- a/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/http/HttpConceptBuilder.kt +++ b/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/http/HttpConceptBuilder.kt @@ -96,7 +96,6 @@ fun MetadataProvider.newHttpRequestHandler( * @param handler The handler name for the endpoint. * @param method The [HttpMethod] the created [HttpEndpoint] listens to. * @param path The path of the created [HttpEndpoint]. - * @param url The URL of the created [HttpEndpoint]. * @param authenticity The [Authenticity] method used by the [HttpEndpoint]. * @param authorization The [Authorization] mechanism defining access control. * @param httpRequestContext The [HttpRequestContext] providing additional contextual metadata. @@ -113,8 +112,8 @@ fun MetadataProvider.newHttpEndpoint( userInput: MutableList, handler: String?, method: HttpMethod, + arguments: List, path: String?, - url: String?, authenticity: Authenticity?, authorization: Authorization?, httpRequestContext: HttpRequestContext?, @@ -130,8 +129,8 @@ fun MetadataProvider.newHttpEndpoint( userInput = userInput, handler = handler, method = method, + arguments = arguments, path = path, - url = url, authenticity = authenticity, authorization = authorization, httpRequestContext = httpRequestContext, @@ -163,6 +162,7 @@ fun MetadataProvider.newHttpRequest( concept: HttpClient, arguments: List, method: HttpMethod, + url: String, call: String?, reqBody: String?, httpEndpoint: HttpEndpoint?, @@ -173,6 +173,7 @@ fun MetadataProvider.newHttpRequest( HttpRequest( arguments = arguments, method = method, + url = url, call = call, reqBody = reqBody, httpEndpoint = httpEndpoint, diff --git a/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/ontology/HttpEndpoint.kt b/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/ontology/HttpEndpoint.kt index 2c0d2beeba5..f9804705883 100644 --- a/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/ontology/HttpEndpoint.kt +++ b/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/ontology/HttpEndpoint.kt @@ -40,10 +40,10 @@ open class HttpEndpoint( var rateLimiting: RateLimiting?, var maxInputSize: Int?, var userInput: MutableList, + var arguments: List, var handler: String?, var method: HttpMethod, var path: String?, - var url: String?, var authenticity: Authenticity?, var authorization: Authorization?, var httpRequestContext: HttpRequestContext?, @@ -57,7 +57,6 @@ open class HttpEndpoint( other.handler == this.handler && other.method == this.method && other.path == this.path && - other.url == this.url && other.authenticity == this.authenticity && other.authorization == this.authorization && other.httpRequestContext == this.httpRequestContext && @@ -65,7 +64,8 @@ open class HttpEndpoint( other.transportEncryption == this.transportEncryption && other.rateLimiting == this.rateLimiting && other.maxInputSize == this.maxInputSize && - other.userInput == this.userInput + other.userInput == this.userInput && + other.arguments == this.arguments override fun hashCode(): Int = Objects.hash( @@ -73,7 +73,6 @@ open class HttpEndpoint( handler, method, path, - url, authenticity, authorization, httpRequestContext, @@ -82,6 +81,7 @@ open class HttpEndpoint( rateLimiting, maxInputSize, userInput, + arguments, ) } diff --git a/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/ontology/HttpRequest.kt b/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/ontology/HttpRequest.kt index fd792abcf50..5709c4f7797 100644 --- a/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/ontology/HttpRequest.kt +++ b/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/concepts/ontology/HttpRequest.kt @@ -35,9 +35,10 @@ import kotlin.String public open class HttpRequest( val arguments: List, val method: HttpMethod, + val url: String, public val call: String?, public val reqBody: String?, - public val httpEndpoint: HttpEndpoint?, + public var httpEndpoint: HttpEndpoint?, linkedConcept: HttpClient, underlyingNode: Node? = null, ) : HttpClientOperation(linkedConcept, underlyingNode) { @@ -48,8 +49,9 @@ public open class HttpRequest( other.reqBody == this.reqBody && other.httpEndpoint == this.httpEndpoint && other.arguments == this.arguments && - other.method == this.method + other.method == this.method && + other.url == this.url override fun hashCode(): Int = - Objects.hash(super.hashCode(), call, reqBody, httpEndpoint, arguments, method) + Objects.hash(super.hashCode(), call, reqBody, httpEndpoint, arguments, method, url) } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/DFGPass.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/DFGPass.kt index dceaa2f8a3f..3a9e2562858 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/DFGPass.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/DFGPass.kt @@ -141,7 +141,7 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) { is Throw -> handleThrow(node) // Declarations is Field -> handleField(node) - is Function -> handleFunction(node, functionSummaries) + is Function -> handleFunction(node, functionSummaries, inferDfgForUnresolvedSymbols) is Tuple -> handleTuple(node) is Variable -> handleVariable(node) } @@ -249,7 +249,11 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) { * Adds the DFG edge for a [Function]. The data flows from the return statement(s) to the * function. */ - protected fun handleFunction(node: Function, functionSummaries: DFGFunctionSummaries) { + protected fun handleFunction( + node: Function, + functionSummaries: DFGFunctionSummaries, + inferDfgForUnresolvedSymbols: Boolean, + ) { if (node.isInferred) { val summaryExists = with(functionSummaries) { addFlowsToFunctionDeclaration(node) } @@ -262,6 +266,32 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) { if (node is Method) { node.receiver?.let { node.prevDFGEdges += it } } + + // For inferred constructors, we also try to connect parameters to fields of the + // record. + if (inferDfgForUnresolvedSymbols && node is Constructor) { + val record = node.recordDeclaration ?: return + val fields = record.fields + + for (param in node.parameters) { + val matchingField = + fields.firstOrNull { it.name.localName == param.name.localName } + // Only add this edge if the field has no read usages. If it does, + // handleMemberExpression will create + // DFG edges for those reads, and our 'extra' edge leads to an infinite + // loop. + val hasReadUsages = + matchingField?.usages?.any { + it is MemberAccess && it.access == AccessValues.READ + } ?: false + if (matchingField != null && !hasReadUsages) { + param.nextDFGEdges += matchingField + } + } + fields.forEach { field -> + field.nextDFGEdges.add(node) { granularity = partial(field.name.localName) } + } + } } } else { node.allChildren().forEach { node.prevDFGEdges += it } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt index 1ebd0c9c4a3..07ecb919b3e 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt @@ -707,10 +707,12 @@ open class SymbolResolver(ctx: TranslationContext) : EOGStarterPass(ctx) { ) != IncompatibleSignature } + val argumentNames = constructExpression.argumentEdges.map { edge -> edge.name } + return constructorCandidate ?: recordDeclaration .startInference(ctx) - ?.createInferredConstructor(constructExpression.signature) + ?.createInferredConstructor(constructExpression.signature, argumentNames) } companion object { diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/inference/Inference.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/inference/Inference.kt index c928c847bc7..7adf2883f5f 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/inference/Inference.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/inference/Inference.kt @@ -174,11 +174,14 @@ class Inference internal constructor(val start: Node, override val ctx: Translat ) } - fun createInferredConstructor(signature: List): Constructor { + fun createInferredConstructor( + signature: List, + argumentNames: List = emptyList(), + ): Constructor { return inferInScopeOf(start) { val record = start as? Record val inferred = newConstructor(start.name.localName, record) - createInferredParameters(inferred, signature) + createInferredParameters(inferred, signature, argumentNames) scopeManager.addDeclaration(inferred) record?.constructors += inferred @@ -208,8 +211,16 @@ class Inference internal constructor(val start: Node, override val ctx: Translat method.receiver = receiver } - /** This function creates a [Parameter] for each parameter in the [function]'s [signature]. */ - private fun createInferredParameters(function: Function, signature: List) { + /** + * This function creates a [Parameter] for each parameter in the [function]'s [signature]. If + * [argumentNames] are provided, those names will be used for the parameters + * * instead of generating names from the types. + */ + private fun createInferredParameters( + function: Function, + signature: List, + argumentNames: List = emptyList(), + ) { // To save some unnecessary scopes, we only want to "enter" the function if it is necessary, // e.g., if we need to create parameters if (signature.isNotEmpty()) { @@ -217,7 +228,8 @@ class Inference internal constructor(val start: Node, override val ctx: Translat for (i in signature.indices) { val targetType = signature[i] ?: UnknownType.getUnknownType(function.language) - val paramName = generateParamName(i, targetType) + // Use the argument name if available, otherwise generate a name from the type + val paramName = argumentNames.getOrNull(i) ?: generateParamName(i, targetType) val param = newParameter(paramName, targetType, false) param.argumentIndex = i diff --git a/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/PythonAddDeclarationsPass.kt b/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/PythonAddDeclarationsPass.kt index f51e8cdfe3f..436278ecfa4 100644 --- a/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/PythonAddDeclarationsPass.kt +++ b/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/PythonAddDeclarationsPass.kt @@ -95,8 +95,10 @@ class PythonAddDeclarationsPass(ctx: TranslationContext) : ComponentPass(ctx), L } // If this is a member expression, and we do not know the base's type, we cannot create a - // declaration - if (ref is MemberAccess && ref.base.type is UnknownType) { + // declaration. However, we need to exclude the receiver (e.g. "self") because its type is + // not yet resolved at this point, but we still need to + // create field declarations for assignments". + if (ref is MemberAccess && ref.base.type is UnknownType && !ref.refersToReceiver) { return null } @@ -135,11 +137,14 @@ class PythonAddDeclarationsPass(ctx: TranslationContext) : ComponentPass(ctx), L when { // Check, whether we are referring to a "self.X", which would create a field scopeManager.isInRecord && scopeManager.isInFunction && ref.refersToReceiver -> { + val recordScope = scopeManager.firstScopeIsInstanceOrNull() + // If the field already exists in the record scope, do not create a duplicate + if (recordScope?.symbols?.containsKey(ref.name.localName) == true) { + return null + } // We need to temporarily jump into the scope of the current record to // add the field. These are instance attributes - scopeManager.withScope(scopeManager.firstScopeIsInstanceOrNull()) { - newField(ref.name) - } + scopeManager.withScope(recordScope) { newField(ref.name) } } scopeManager.isInRecord && scopeManager.isInFunction && ref is MemberAccess -> { // If this is any other member expression, we are usually not interested in diff --git a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/DFGTest.kt b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/DFGTest.kt index 9d058747a7f..ad61166a8c5 100644 --- a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/DFGTest.kt +++ b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/DFGTest.kt @@ -1022,4 +1022,50 @@ class DFGTest { "Expected DFG path from 'baz' through the constructor to return f", ) } + + @Test + fun testInferredConstructorArgDFG() { + val topLevel = Path.of("src", "test", "resources", "python") + val result = + analyze( + listOf(topLevel.resolve("inferred_construct_dfg.py").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(result) + + // Test case 1: A(b=b) + val fooFunc = result.functions["foo"] + assertNotNull(fooFunc) + val aRef = fooFunc.refs["a_var"] + assertNotNull(aRef) + + val classA = result.records["A"] + assertNotNull(classA) + + val fieldB = classA.fields["b"] + assertNotNull(fieldB) + + val pathsToFieldB = aRef.followDFGEdgesUntilHit { it == fieldB } + assertTrue( + pathsToFieldB.fulfilled.isNotEmpty(), + "Expected DFG path from 'a_var' through constructor to field b", + ) + + // Test case 2: B(x=some_other_var) + val barFunc = result.functions["bar"] + assertNotNull(barFunc) + + val someOtherVarRef = barFunc.refs["some_other_var"] + assertNotNull(someOtherVarRef) + + val pathsToFieldX = + someOtherVarRef.followDFGEdgesUntilHit(collectFailedPaths = true) { it == fieldB } + assertTrue( + pathsToFieldX.fulfilled.isNotEmpty(), + "Expected DFG path from 'some_other_var' through constructor to field 'b'", + ) + } } diff --git a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/SymbolResolverTest.kt b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/SymbolResolverTest.kt index fec6957433a..5e3a02e2241 100644 --- a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/SymbolResolverTest.kt +++ b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/SymbolResolverTest.kt @@ -31,8 +31,10 @@ import de.fraunhofer.aisec.cpg.graph.declarations.Method import de.fraunhofer.aisec.cpg.graph.statements.expressions.Call import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberAccess import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCall +import de.fraunhofer.aisec.cpg.graph.types.ObjectType import de.fraunhofer.aisec.cpg.test.analyze import de.fraunhofer.aisec.cpg.test.assertFullName +import de.fraunhofer.aisec.cpg.test.assertInvokes import de.fraunhofer.aisec.cpg.test.assertNotRefersTo import de.fraunhofer.aisec.cpg.test.assertRefersTo import java.io.File @@ -125,4 +127,48 @@ class SymbolResolverTest { assertNotNull(doSomething, "Expected to find a single call to 'do_something'") assertIs(doSomething, "'do_something' should be a member call") } + + @Test + fun testFieldCallResolution() { + val topLevel = File("src/test/resources/python/field_call_resolution.py") + val result = + analyze(listOf(topLevel), topLevel.toPath(), true) { + it.registerLanguage() + } + assertNotNull(result) + + val clientClass = result.records["Client"] + assertNotNull(clientClass) + + val serviceClass = result.records["Service"] + assertNotNull(serviceClass) + + val clientField = serviceClass.fields["client"] + assertIs(clientField, "Expected a field 'client'") + + val fieldType = clientField.type + assertIs(fieldType, "Expected field 'client' to have an ObjectType") + + val sendMethod = clientClass.methods["send"] + assertNotNull(sendMethod) + + val sendCall = result.calls["send"] + assertNotNull(sendCall) + assertIs(sendCall) + assertInvokes(sendCall, sendMethod) + } + + @Test + fun testFieldCallResolution2() { + val topLevel = File("src/test/resources/python/field_call_resolution.py") + val result = + analyze(listOf(topLevel), topLevel.toPath(), true) { + it.registerLanguage() + } + assertNotNull(result) + + // TODO: Not sure if this fix in PythonAddDeclarationsPass is correct, as in some cases like + // the following one it won't work + /** def __init__(self, client): self.client = client or Client() */ + } } diff --git a/cpg-language-python/src/test/resources/python/field_call_resolution.py b/cpg-language-python/src/test/resources/python/field_call_resolution.py new file mode 100644 index 00000000000..42a2e9eaee7 --- /dev/null +++ b/cpg-language-python/src/test/resources/python/field_call_resolution.py @@ -0,0 +1,12 @@ +class Client: + def send(self, data): + return data + + +class Service: + def __init__(self): + self.client = Client() + + def do_work(self): + result = self.client.send("hello") + return result \ No newline at end of file diff --git a/cpg-language-python/src/test/resources/python/inferred_construct_dfg.py b/cpg-language-python/src/test/resources/python/inferred_construct_dfg.py new file mode 100644 index 00000000000..00af69b15d3 --- /dev/null +++ b/cpg-language-python/src/test/resources/python/inferred_construct_dfg.py @@ -0,0 +1,15 @@ +from sqlalchemy.orm.declarative_base import Base + +class A(Base): + __tablename__ = 'a' + b = Column(Integer, nullable= False) + +def foo(): + a_var = 1 + obj = A(b=a_var) + return obj + +def bar(): + some_other_var = 42 + obj = A(b=some_other_var) + return obj \ No newline at end of file