forked from joernio/joern
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ruby] Implicit Require Handling (joernio#4484)
[Autoloading in Ruby](https://www.rubyguides.com/2019/08/autoloading-in-ruby/) is fairly common, however it's important for us to represent these import nodes explicitly. This pass adds a check for modules with no high-level require calls, to check call receivers and constructor allocations for types from other files, and add explicit require calls for them.
- Loading branch information
1 parent
ce5b8a4
commit d46a89d
Showing
4 changed files
with
166 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
...rontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/ImplicitRequirePass.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package io.joern.rubysrc2cpg.passes | ||
|
||
import io.joern.rubysrc2cpg.datastructures.{RubyProgramSummary, RubyType} | ||
import io.shiftleft.codepropertygraph.generated.nodes.* | ||
import io.shiftleft.codepropertygraph.generated.{Cpg, DispatchTypes, EdgeTypes, Operators} | ||
import io.shiftleft.passes.ForkJoinParallelCpgPass | ||
import io.shiftleft.semanticcpg.language.* | ||
|
||
import scala.collection.mutable | ||
|
||
/** In some Ruby frameworks, it is common to have an autoloader library that implicitly loads requirements onto the | ||
* stack. This pass makes these imports explicit. | ||
*/ | ||
class ImplicitRequirePass(cpg: Cpg, programSummary: RubyProgramSummary) extends ForkJoinParallelCpgPass[Method](cpg) { | ||
|
||
private val importCallName: String = "require" | ||
private val typeToPath = mutable.Map.empty[String, String] | ||
|
||
override def init(): Unit = { | ||
programSummary.pathToType.foreach { case (path, types) => | ||
types.foreach { typ => typeToPath.put(typ.name, path) } | ||
} | ||
} | ||
|
||
override def generateParts(): Array[Method] = | ||
cpg.method.isModule.whereNot(_.astChildren.isCall.nameExact(importCallName)).toArray | ||
|
||
/** Collects methods within a module. | ||
*/ | ||
private def findMethodsViaAstChildren(module: Method): Iterator[Method] = { | ||
Iterator(module) ++ module.astChildren.flatMap { | ||
case x: TypeDecl => x.method.flatMap(findMethodsViaAstChildren) | ||
case x: Method => Iterator(x) ++ x.astChildren.collectAll[Method].flatMap(findMethodsViaAstChildren) | ||
case _ => Iterator.empty | ||
} | ||
} | ||
|
||
override def runOnPart(builder: DiffGraphBuilder, part: Method): Unit = { | ||
findMethodsViaAstChildren(part).ast.isCall | ||
.flatMap { | ||
case x if x.name == Operators.alloc => | ||
x.argument.isIdentifier | ||
case x => | ||
x.receiver.isIdentifier | ||
} | ||
.map(i => i -> programSummary.matchingTypes(i.name)) | ||
.distinct | ||
.foreach { case (identifier, rubyTypes) => | ||
val requireCalls = rubyTypes.flatMap { rubyType => | ||
typeToPath.get(rubyType.name) match { | ||
case Some(path) | ||
if identifier.file.name | ||
.map(_.replace("\\", "/")) | ||
.headOption | ||
.exists(x => rubyType.name.startsWith(x)) => | ||
None // do not add an import to a file that defines the type | ||
case Some(path) => Option(createRequireCall(builder, rubyType, path)) | ||
case None => None | ||
} | ||
} | ||
val startIndex = part.block.astChildren.size | ||
requireCalls.zipWithIndex.foreach { case (call, idx) => | ||
call.order(startIndex + idx) | ||
builder.addEdge(part.block, call, EdgeTypes.AST) | ||
} | ||
} | ||
} | ||
|
||
private def createRequireCall(builder: DiffGraphBuilder, rubyType: RubyType, path: String): NewCall = { | ||
val requireCallNode = NewCall() | ||
.name(importCallName) | ||
.code(s"$importCallName '$path'") | ||
.methodFullName(s"__builtin:$importCallName") | ||
.dispatchType(DispatchTypes.DYNAMIC_DISPATCH) | ||
.typeFullName(Defines.Any) | ||
val receiverIdentifier = | ||
NewIdentifier().name(importCallName).code(importCallName).typeFullName(Defines.Any).argumentIndex(0).order(1) | ||
val pathLiteralNode = NewLiteral().code(s"'$path'").typeFullName("__builtin.String").argumentIndex(1).order(2) | ||
builder.addNode(requireCallNode) | ||
builder.addEdge(requireCallNode, receiverIdentifier, EdgeTypes.AST) | ||
builder.addEdge(requireCallNode, receiverIdentifier, EdgeTypes.ARGUMENT) | ||
builder.addEdge(requireCallNode, receiverIdentifier, EdgeTypes.RECEIVER) | ||
builder.addEdge(requireCallNode, pathLiteralNode, EdgeTypes.AST) | ||
builder.addEdge(requireCallNode, pathLiteralNode, EdgeTypes.ARGUMENT) | ||
requireCallNode | ||
} | ||
|
||
} |
16 changes: 8 additions & 8 deletions
16
joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/ImportsPass.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters