Repository: joernio/joern (scala)

Issue:
jssrc2cpg: Require statements do not capture external module names
JavaScript frontend does not capture imported module names from external dependencies. Below is a test case one can use with proposed outputs. Right now, both `express` and `app` return `ANY` at this point.

```javascript
  "external modules imported via `require` should be resolved correctly" in {
    val cpg = code(
      """
        |const express = require('express')
        |const app = express()
      """.stripMargin,
      "sample.js"
    )

    implicit val callResolver: NoResolve.type = NoResolve
    cpg.identifier("express").typeFullName.l shouldBe List("express")
    cpg.identifier("app").typeFullName.l shouldBe List("express::constructor")
  }
 ```

Pull request:
There is a disconnect with `module.export` functions where the key is named and there is a lambda on the right (no name present). For example, calls to the following will not be successfully linked:

```javascript
module.exports = {
  sayhi: function() {            // this will be called anonymous
   console.log("Hello World!");
 },
  saybye: function() {           // this will be called anonymous1
   console.log("Good-bye!");
 }
}
```

The above is example can be found in test cases and the following changes are made:
- Set `METHOD_FULL_NAME` property on call nodes when they are linked to one callee
- Set `DYNAMIC_TYPE_HINT_FULL_NAME` of identifiers and referring locals receiving the result of require(s) to the path with `<export>::` prefix
- Using details from the above, will be able to resolve an exported function without an identifier

Resolves #1914

Patch:
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/RequirePass.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/RequirePass.scala
index 91159bfd3ec7..4fcd10ab303a 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/RequirePass.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/RequirePass.scala
@@ -1,13 +1,15 @@
 package io.joern.jssrc2cpg.passes
 
+import io.joern.jssrc2cpg.passes.RequirePass.JS_EXPORT_PREFIX
 import io.shiftleft.codepropertygraph.Cpg
-import io.shiftleft.codepropertygraph.generated.EdgeTypes
-import io.shiftleft.codepropertygraph.generated.nodes.Call
+import io.shiftleft.codepropertygraph.generated.nodes.{AstNode, Call, Identifier, Local}
+import io.shiftleft.codepropertygraph.generated.{EdgeTypes, PropertyNames}
 import io.shiftleft.passes.SimpleCpgPass
-import overflowdb.BatchedUpdate
 import io.shiftleft.semanticcpg.language._
+import overflowdb.BatchedUpdate
 import overflowdb.traversal.{NodeOps, Traversal}
 
+import java.io.File
 import java.nio.file.Paths
 
 /** This pass enhances the call graph and call nodes by interpreting assignments of the form `foo = require("module")`.
@@ -63,7 +65,7 @@ class RequirePass(cpg: Cpg) extends SimpleCpgPass(cpg) {
       .map(stripQuotes)
       .map { x =>
         val path = Paths.get(dirHoldingModule, x).toAbsolutePath.normalize.toString
-        if (path.endsWith(".mjs")) {
+        if (path.endsWith(".mjs") || path.endsWith(".js")) {
           path
         } else {
           path + ".js"
@@ -82,16 +84,65 @@ class RequirePass(cpg: Cpg) extends SimpleCpgPass(cpg) {
 
     val target: String = call.inAssignment.target.code.head
 
-    val nodesToPatch: List[Call] = call.file.method.ast.isCall.nameExact(target).dedup.l
+    val callsToPatch: List[Call] = call.file.method.ast.isCall.nameExact(target).dedup.l
+
+    val variableToPatch: VariableInformation = VariableInformation(
+      call.file.method.ast.isIdentifier
+        .nameExact(target)
+        .collect { case i: Identifier => Seq(i) ++ i.refsTo.collectAll[Local].toSeq }
+        .flatten
+        .collect { case i: AstNode => i }
+        .dedup
+        .toList
+    )
+
+    def relativeExport: String = fileToInclude.stripPrefix(dirHoldingModule + File.separator)
 
   }
 
+  /** Represents a local and all of its reference identifiers.
+    * @param nodes
+    *   an [[AstNode]] list of a local and its reference identifiers.
+    */
+  case class VariableInformation(nodes: List[AstNode]) {
+
+    /** Whether this variable has been set as immutable or not.
+      */
+    lazy val isImmutable: Boolean = nodes.exists(_.inAssignment.code.exists(_.startsWith("const ")))
+
+    /** The dynamic type hints attached to this variable. This assumes all nodes have the same property value at this
+      * time.
+      */
+    lazy val dynamicTypeHintFullName: Seq[String] = nodes.headOption
+      .map(_.property(PropertyNames.DYNAMIC_TYPE_HINT_FULL_NAME, IndexedSeq.empty[String]))
+      .getOrElse(IndexedSeq.empty[String])
+  }
+
   override def run(diffGraph: BatchedUpdate.DiffGraphBuilder): Unit = {
-    cpg.call("require").where(_.inAssignment.target).map(Require.apply(_)).foreach { require =>
+    val requires = cpg
+      .call("require")
+      .where(_.inAssignment.target)
+      .map(Require.apply(_))
+      .toSeq
+    // Set type hints of affected locals and identifiers
+    requires
+      .groupBy(require => require.variableToPatch)
+      .foreach { case (varInfo: VariableInformation, rs: Seq[Require]) =>
+        varInfo.nodes.foreach { n =>
+          diffGraph.setNodeProperty(
+            n.node,
+            if (varInfo.isImmutable) PropertyNames.TYPE_FULL_NAME else PropertyNames.DYNAMIC_TYPE_HINT_FULL_NAME,
+            if (varInfo.isImmutable) rs.map(x => s"$JS_EXPORT_PREFIX${x.relativeExport}").headOption.getOrElse("ANY")
+            else varInfo.dynamicTypeHintFullName ++ rs.map(x => s"$JS_EXPORT_PREFIX${x.relativeExport}")
+          )
+        }
+      }
+    // Set method full names of calls to patch
+    requires.foreach { require =>
       require.methodFullName.foreach { fullName =>
         cpg.method.fullNameExact(fullName).foreach { method =>
-          require.nodesToPatch.foreach { node =>
-            diffGraph.setNodeProperty(node, "METHOD_FULL_NAME", fullName)
+          require.callsToPatch.foreach { node =>
+            diffGraph.setNodeProperty(node, PropertyNames.METHOD_FULL_NAME, fullName)
             diffGraph.addEdge(node, method, EdgeTypes.CALL)
           }
         }
@@ -110,3 +161,7 @@ class RequirePass(cpg: Cpg) extends SimpleCpgPass(cpg) {
   }
 
 }
+
+object RequirePass {
+  val JS_EXPORT_PREFIX: String = "<export>::"
+}
diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/JavascriptCallLinker.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/JavascriptCallLinker.scala
index 1c0e94bd8d9a..43e35d24a627 100644
--- a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/JavascriptCallLinker.scala
+++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/JavascriptCallLinker.scala
@@ -1,14 +1,11 @@
 package io.joern.x2cpg.passes.frontend
 
 import io.shiftleft.codepropertygraph.Cpg
-import io.shiftleft.codepropertygraph.generated.DispatchTypes
-import io.shiftleft.codepropertygraph.generated.EdgeTypes
-import io.shiftleft.codepropertygraph.generated.Operators
-import io.shiftleft.codepropertygraph.generated.nodes
+import io.shiftleft.codepropertygraph.generated._
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, Identifier, Local}
 import io.shiftleft.passes.SimpleCpgPass
 import io.shiftleft.semanticcpg.language._
-import overflowdb.traversal.NodeOps
-import overflowdb.traversal.jIteratortoTraversal
+import overflowdb.traversal.{NodeOps, jIteratortoTraversal}
 
 import scala.collection.mutable
 
@@ -20,7 +17,8 @@ class JavascriptCallLinker(cpg: Cpg) extends SimpleCpgPass(cpg) {
   private type MethodsByNameAndFileType = mutable.HashMap[(String, String), nodes.Method]
   private type MethodsByFullNameType    = mutable.HashMap[String, nodes.Method]
 
-  private val JS_EXPORT_NAMES = IndexedSeq("module.exports", "exports")
+  private val JS_EXPORT_NAMES  = IndexedSeq("module.exports", "exports")
+  private val JS_EXPORT_PREFIX = "<export>::"
 
   private def isStaticSingleAssignmentLocal(ident: nodes.Identifier): Boolean =
     ident.refsTo
@@ -53,7 +51,7 @@ class JavascriptCallLinker(cpg: Cpg) extends SimpleCpgPass(cpg) {
             Some(assignee.name)
           case assignee: nodes.Call
               if assignee.methodFullName == Operators.fieldAccess &&
-                JS_EXPORT_NAMES.contains(assignee.argument(1).code) =>
+                (JS_EXPORT_NAMES.contains(assignee.argument(1).code) || assignee.code.startsWith("_tmp_")) =>
             Some(assignee.argument(2).asInstanceOf[nodes.FieldIdentifier].canonicalName)
           case _ => None
         }
@@ -76,15 +74,35 @@ class JavascriptCallLinker(cpg: Cpg) extends SimpleCpgPass(cpg) {
   ): Unit = {
     cpg.call.foreach { call =>
       if (call.dispatchType == DispatchTypes.STATIC_DISPATCH) {
+        // Case 1: This call is static so the methodFullName points us to our callee, typically reserved for <operator>
         methodsByFullName
           .get(call.methodFullName)
-          .foreach(diffGraph.addEdge(call, _, EdgeTypes.CALL))
+          .foreach { method =>
+            diffGraph.addEdge(call, method, EdgeTypes.CALL)
+          }
       } else {
-        getReceiverIdentifierName(call).foreach { name =>
-          for (
-            file   <- call.file.headOption;
-            method <- methodsByNameAndFile.get((file.name, name))
-          ) { diffGraph.addEdge(call, method, EdgeTypes.CALL) }
+        val imports = receiverImports(call)
+        if (imports.nonEmpty) {
+          // Case 2: This call's receiver is associated with a "require" statement's module.exports
+          val callees = imports.flatMap(rp => methodsByNameAndFile.get((rp, call.name)))
+          callees.foreach { method =>
+            diffGraph.addEdge(call, method, EdgeTypes.CALL)
+            diffGraph.setNodeProperty(call, PropertyNames.METHOD_FULL_NAME, method.fullName)
+          }
+          // If there are more than one call edges then it is unsound to set METHOD_FULL_NAME to something
+          if (callees.size > 1)
+            diffGraph.setNodeProperty(call, PropertyNames.METHOD_FULL_NAME, "<unknownFullName>")
+        } else {
+          // Case 3: We look for a definition of the function within the scope of the caller's file
+          getReceiverIdentifierName(call).foreach { name =>
+            for (
+              file   <- call.file.headOption;
+              method <- methodsByNameAndFile.get((file.name, name))
+            ) {
+              diffGraph.addEdge(call, method, EdgeTypes.CALL)
+              diffGraph.setNodeProperty(call, PropertyNames.METHOD_FULL_NAME, method.fullName)
+            }
+          }
         }
       }
     }
@@ -93,6 +111,33 @@ class JavascriptCallLinker(cpg: Cpg) extends SimpleCpgPass(cpg) {
   private def callReceiverOption(callNode: nodes.Call): Option[nodes.Expression] =
     callNode._receiverOut.nextOption().map(_.asInstanceOf[nodes.Expression])
 
+  /** If a call is made on a receiver that contains the result of a <code>require</code> then the
+    * [[io.joern.jssrc2cpg.passes.RequirePass]] should have marked the ref [[Local]] and [[Identifier]] nodes with the
+    * file name under <code>TYPE_FULL_NAME</code> (if immutable) or <code>DYNAMIC_TYPE_HINT_FULL_NAME</code> (if
+    * mutable).
+    * @param call
+    *   the call to investigate for a <code>require</code> reference.
+    * @return
+    *   the argument given to the declaring require(s) if present.
+    */
+  private def receiverImports(call: nodes.Call): Seq[String] = {
+    callReceiverOption(call)
+      .flatMap {
+        case c: Call if c.methodFullName == Operators.fieldAccess =>
+          c.argument(1).collectAll[Identifier].collectFirst {
+            case receiver: Identifier if receiver.dynamicTypeHintFullName.exists(_.startsWith(JS_EXPORT_PREFIX)) =>
+              receiver.dynamicTypeHintFullName
+                .filter(_.startsWith(JS_EXPORT_PREFIX))
+                .map(_.stripPrefix(JS_EXPORT_PREFIX))
+            case receiver: Identifier if receiver.typeFullName.startsWith(JS_EXPORT_PREFIX) =>
+              Seq(receiver.typeFullName.stripPrefix(JS_EXPORT_PREFIX))
+          }
+        case _ => None
+      }
+      .toSeq
+      .flatten
+  }
+
   private def fromFieldAccess(c: nodes.Call): Option[String] =
     if (c.methodFullName == Operators.fieldAccess && JS_EXPORT_NAMES.contains(c.argument(1).code)) {
       Some(c.argument(2).code)


Test patch:
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/CallLinkerPassTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/CallLinkerPassTest.scala
index 6335245d3107..202943b7d0d4 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/CallLinkerPassTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/CallLinkerPassTest.scala
@@ -2,6 +2,7 @@ package io.joern.jssrc2cpg.passes
 
 import io.joern.jssrc2cpg.testfixtures.DataFlowCodeToCpgSuite
 import io.shiftleft.codepropertygraph.Cpg
+import io.shiftleft.codepropertygraph.generated.nodes.Identifier
 import io.shiftleft.semanticcpg.language._
 
 class CallLinkerPassTest extends DataFlowCodeToCpgSuite {
@@ -24,6 +25,133 @@ class CallLinkerPassTest extends DataFlowCodeToCpgSuite {
 
       inside(cpg.method("sayhi").callIn(NoResolve).l) { case List(call) =>
         call.code shouldBe "sayhi()"
+        call.methodFullName should endWith(".js::program:sayhi")
+      }
+    }
+
+    "link exported anonymous functions across file boundaries correctly" in {
+      val cpg: Cpg = code(
+        """
+          |const bar = require('./bar.js');
+          |const baz = require('./baz.js');
+          |
+          |bar.sayhi();
+          |baz.sayhowdy();
+          |""".stripMargin,
+        "foo.js"
+      ).moreCode(
+        """
+          |module.exports = {
+          |  sayhi: function() {            // this will be called anonymous
+          |    console.log("Hello World!");
+          |  },
+          |  saybye: function() {           // this will be called anonymous1
+          |    console.log("Good-bye!");
+          |  }
+          |}
+          |""".stripMargin,
+        "bar.js"
+      ).moreCode(
+        """
+          |module.exports = {
+          |  sayhowdy: function() {       // this will be called anonymous
+          |    console.log("Howdy World!");
+          |  }
+          |}
+          |""".stripMargin,
+        "baz.js"
+      )
+
+      inside(cpg.method.fullNameExact("bar.js::program:anonymous").l) { case List(m) =>
+        m.name shouldBe "anonymous"
+        m.fullName shouldBe "bar.js::program:anonymous"
+      }
+
+      inside(cpg.method.fullNameExact("bar.js::program:anonymous").callIn(NoResolve).l) { case List(call) =>
+        call.code shouldBe "bar.sayhi()"
+        call.methodFullName shouldBe "bar.js::program:anonymous"
+        inside(call.expressionDown.isIdentifier.l) { case List(receiver: Identifier) =>
+          receiver.name shouldBe "bar"
+          receiver.typeFullName shouldBe "<export>::bar.js"
+        }
+      }
+
+      inside(cpg.method.fullNameExact("baz.js::program:anonymous").l) { case List(m) =>
+        m.name shouldBe "anonymous"
+        m.fullName shouldBe "baz.js::program:anonymous"
+      }
+
+      inside(cpg.method.fullNameExact("baz.js::program:anonymous").callIn(NoResolve).l) { case List(call) =>
+        call.code shouldBe "baz.sayhowdy()"
+        call.methodFullName shouldBe "baz.js::program:anonymous"
+        inside(call.expressionDown.isIdentifier.l) { case List(receiver: Identifier) =>
+          receiver.name shouldBe "baz"
+          receiver.typeFullName shouldBe "<export>::baz.js"
+        }
+      }
+    }
+
+    "link all calls in a conservative, sound, and flow-insensitive manner" in {
+      val cpg: Cpg = code(
+        """
+          |var barOrBaz = require('./bar.js');
+          |barOrBaz = require('./baz.js');
+          |
+          |barOrBaz.sayhi();
+          |""".stripMargin,
+        "foo.js"
+      ).moreCode(
+        """
+          |module.exports = {
+          |  sayhi: function() {            // this will be called anonymous
+          |    console.log("Hello World, love BAR");
+          |  }
+          |}
+          |""".stripMargin,
+        "bar.js"
+      ).moreCode(
+        """
+          |module.exports = {
+          |  sayhi: function() {            // this will be called anonymous
+          |    console.log("Howdy World, love BAZ");
+          |  }
+          |}
+          |""".stripMargin,
+        "baz.js"
+      )
+
+      // Because of flow-insensitivity, it could point to either "sayhi"
+      inside(cpg.call("sayhi").callee(NoResolve).l) { case List(m1, m2) =>
+        m1.fullName shouldBe "bar.js::program:anonymous"
+        m2.fullName shouldBe "baz.js::program:anonymous"
+      }
+
+      inside(cpg.method.fullNameExact("bar.js::program:anonymous").l) { case List(m) =>
+        m.name shouldBe "anonymous"
+        m.fullName shouldBe "bar.js::program:anonymous"
+      }
+
+      inside(cpg.method.fullNameExact("bar.js::program:anonymous").callIn(NoResolve).l) { case List(call) =>
+        call.code shouldBe "barOrBaz.sayhi()"
+        call.methodFullName shouldBe "<unknownFullName>"
+        inside(call.expressionDown.isIdentifier.l) { case List(receiver: Identifier) =>
+          receiver.name shouldBe "barOrBaz"
+          receiver.typeFullName shouldBe "ANY"
+        }
+      }
+
+      inside(cpg.method.fullNameExact("baz.js::program:anonymous").l) { case List(m) =>
+        m.name shouldBe "anonymous"
+        m.fullName shouldBe "baz.js::program:anonymous"
+      }
+
+      inside(cpg.method.fullNameExact("baz.js::program:anonymous").callIn(NoResolve).l) { case List(call) =>
+        call.code shouldBe "barOrBaz.sayhi()"
+        call.methodFullName shouldBe "<unknownFullName>"
+        inside(call.expressionDown.isIdentifier.l) { case List(receiver: Identifier) =>
+          receiver.name shouldBe "barOrBaz"
+          receiver.typeFullName shouldBe "ANY"
+        }
       }
     }