Skip to content

Commit

Permalink
✅ Call Site and Call Graph Tests (#56)
Browse files Browse the repository at this point in the history
* Call site and call graph test

* Set correct CPG version

* 🐛 Fixed incorrect call edge source bug on assignment vertex and created naive call edges if no call edge was produced by Soot

* 🎨 Cleaned up code a bit

Co-authored-by: David Baker Effendi <[email protected]>
  • Loading branch information
fabsx00 and DavidBakerEffendi authored Feb 8, 2021
1 parent 3764888 commit 2b4a848
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 15 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jgVersion=0.5.3
neo4jDriverVersion=4.2.0
khttpVersion=1.0.0
jacksonVersion=2.12.0
shiftleftVersion=1.3.57
shiftleftVersion=1.3.61
sootVersion=4.2.1
lz4Version=1.7.1

Expand Down
18 changes: 9 additions & 9 deletions plume/src/main/kotlin/io/github/plume/oss/graph/ASTBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class ASTBuilder(private val driver: IDriver) : IGraphBuilder {
// Build body
graph.body.units.filterNot { it is IdentityStmt }
.forEachIndexed { idx, u ->
projectUnit(u, idx)
projectUnit(u, idx + 1)
?.let {
runCatching {
driver.addEdge(
Expand Down Expand Up @@ -139,7 +139,7 @@ class ASTBuilder(private val driver: IDriver) : IGraphBuilder {
val callVertex = NewCallBuilder()
.name(unit.methodRef.name)
.signature(unit.methodRef.signature)
.code(unit.methodRef.subSignature.toString())
.code("${unit.methodRef.name}(${unit.args.joinToString()})")
.order(childIdx)
.dynamictypehintfullname(createScalaList(unit.methodRef.returnType.toQuotedString()))
.linenumber(Option.apply(currentLine))
Expand All @@ -152,8 +152,8 @@ class ASTBuilder(private val driver: IDriver) : IGraphBuilder {
// Create vertices for arguments
unit.args.forEachIndexed { i, arg ->
when (arg) {
is Local -> SootToPlumeUtil.createIdentifierVertex(arg, currentLine, currentCol, i)
is Constant -> SootToPlumeUtil.createLiteralVertex(arg, currentLine, currentCol, i)
is Local -> SootToPlumeUtil.createIdentifierVertex(arg, currentLine, currentCol, i + 1)
is Constant -> SootToPlumeUtil.createLiteralVertex(arg, currentLine, currentCol, i + 1)
else -> null
}?.let { expressionVertex ->
runCatching {
Expand Down Expand Up @@ -447,21 +447,21 @@ class ASTBuilder(private val driver: IDriver) : IGraphBuilder {
.signature("${expr.op1.type} $symbol ${expr.op2.type}")
.methodfullname(symbol)
.dispatchtype(STATIC_DISPATCH)
.order(2)
.argumentindex(2) // under an if-condition, the condition child will be after the two paths
.order(3)
.argumentindex(3) // under an if-condition, the condition child will be after the two paths
.typefullname(expr.type.toQuotedString())
.linenumber(Option.apply(currentLine))
.columnnumber(Option.apply(currentCol))
.dynamictypehintfullname(createScalaList(expr.op2.type.toQuotedString()))
.apply { conditionVertices.add(this) }
projectOp(expr.op1, 0)?.let {
projectOp(expr.op1, 1)?.let {
runCatching {
driver.addEdge(binOpBlock, it, AST)
}.onFailure { e -> logger.warn(e.message) }
conditionVertices.add(it)
addSootToPlumeAssociation(expr.op1, it)
}
projectOp(expr.op2, 1)?.let {
projectOp(expr.op2, 2)?.let {
runCatching {
driver.addEdge(binOpBlock, it, AST)
}.onFailure { e -> logger.warn(e.message) }
Expand All @@ -487,7 +487,7 @@ class ASTBuilder(private val driver: IDriver) : IGraphBuilder {
.linenumber(Option.apply(currentLine))
.columnnumber(Option.apply(currentCol))
.apply { castVertices.add(this) }
projectOp(expr.op, 0)?.let {
projectOp(expr.op, 1)?.let {
runCatching {
driver.addEdge(castBlock, it, AST); castVertices.add(it)
}.onFailure { e -> logger.warn(e.message) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ import io.github.plume.oss.Extractor.Companion.getSootAssociation
import io.github.plume.oss.drivers.IDriver
import io.github.plume.oss.util.SootToPlumeUtil.constructPhantom
import io.shiftleft.codepropertygraph.generated.EdgeTypes.CALL
import io.shiftleft.codepropertygraph.generated.NodeTypes.METHOD
import io.shiftleft.codepropertygraph.generated.nodes.NewCallBuilder
import io.shiftleft.codepropertygraph.generated.nodes.NewMethodBuilder
import org.apache.logging.log4j.LogManager
import soot.Scene
import soot.Unit
import soot.jimple.AssignStmt
import soot.jimple.IdentityStmt
import soot.jimple.InvokeStmt
import soot.jimple.toolkits.callgraph.Edge
import soot.toolkits.graph.BriefUnitGraph

Expand All @@ -50,15 +54,47 @@ class CallGraphBuilder(private val driver: IDriver) : IGraphBuilder {
private fun projectUnit(unit: Unit) {
val cg = Scene.v().callGraph
val edges = cg.edgesOutOf(unit) as Iterator<Edge>
// Attempt to use Soot calculated call graph edges, these are usually quite precise
edges.forEach { e: Edge ->
getSootAssociation(unit)?.firstOrNull()?.let { srcPlumeVertex ->
// If Soot points to the assignment as the call source then this is most likely from the rightOp. Let's
// hope this is not the source of a bug
val srcUnit = if (unit is AssignStmt) unit.rightOp else unit
getSootAssociation(srcUnit)?.firstOrNull()?.let { srcPlumeVertex ->
val tgtPlumeVertex = getSootAssociation(e.tgt.method())?.firstOrNull()
?: constructPhantom(e.tgt.method(), driver)
runCatching {
driver.addEdge(srcPlumeVertex, tgtPlumeVertex, CALL)
}.onFailure { e -> logger.warn(e.message) }
}
}
// If call graph analysis fails because there is no main method, we will need to figure out call edges ourselves
// We can do this by looking if our call unit does not have any outgoing CALL edges.
when (unit) {
is AssignStmt -> getSootAssociation(unit.rightOp)?.filterIsInstance<NewCallBuilder>()?.firstOrNull()
is InvokeStmt -> getSootAssociation(unit.invokeExpr)?.filterIsInstance<NewCallBuilder>()?.firstOrNull()
else -> null
}?.let { callV ->
driver.getNeighbours(callV).use { g ->
// If there is no outgoing call edge from this call, then we should attempt to find it's target method
if (g.node(callV.id())?.outE(CALL)?.hasNext() != true) {
val v = callV.build()
if (!g.nodes(METHOD).hasNext() && v.methodFullName().length > 1) {
val signature = v.methodFullName().substringAfter(' ')
val fullName = "${v.methodFullName().substringBefore(':')}.${
signature.substringAfter(' ').substringBefore('(')
}"
driver.getMethod(fullName, signature).use { mg ->
if (mg.nodes(METHOD).hasNext()) {
val mtdV = mg.nodes(METHOD).next()
// Since this method already exists, we don't need to build a new method, only provide
// an existing ID
driver.addEdge(callV, NewMethodBuilder().id(mtdV.id()), CALL)
}
}
}
}
}
}
}

private fun reconnectPriorCallGraphEdges(mtdV: NewMethodBuilder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ object SootToPlumeUtil {
childIdx: Int = 0
): NewMethodReturnBuilder =
NewMethodReturnBuilder()
.code("${type.toQuotedString()}")
.code(type.toQuotedString())
.evaluationstrategy(determineEvaluationStrategy(type.toQuotedString(), true))
.typefullname(type.toQuotedString())
.linenumber(Option.apply(currentLine))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class BasicInterproceduralTest {
mainMethod!!; fMethod!!; fCall!!
val assignVert = g.V(fCall.id()).next().`in`(ARGUMENT).next()
assertNotNull(assignVert)
assertTrue(g.V(assignVert.id()).next().out(CALL).asSequence().any { it.id() == fMethod.id() })
assertTrue(g.V(fCall.id()).next().out(CALL).asSequence().any { it.id() == fMethod.id() })
g.V(fCall.id()).next().out(ARGUMENT).asSequence().filterIsInstance<Literal>().firstOrNull()
?.let { assertEquals("5", it.code()) }
}
Expand All @@ -108,7 +108,7 @@ class BasicInterproceduralTest {
mainMethod!!; initMethod!!; fCall!!; fMethod!!
g.V(initMethod.id()).next().out(REF).asSequence().filterIsInstance<Method>().firstOrNull()
?.apply { assertNotNull(this); assertEquals(initMethod.name(), this.name()) }
assertTrue(g.V(fCall.id()).next().`in`(ARGUMENT).next().out(CALL).asSequence().any { it.id() == fMethod.id() })
assertTrue(g.V(fCall.id()).next().out(CALL).asSequence().any { it.id() == fMethod.id() })
}

@Test
Expand Down Expand Up @@ -139,7 +139,7 @@ class BasicInterproceduralTest {
mainMethod!!; initMethod!!; fCall!!; fMethod!!
g.V(initMethod.id()).next().out(CALL).asSequence().filterIsInstance<Method>().firstOrNull()
?.apply { assertNotNull(this); assertEquals(initMethod.name(), this.name()) }
assertNotNull(g.V(fCall.id()).next().`in`(ARGUMENT).next().out(CALL).asSequence().firstOrNull { it.id() == fMethod.id() })
assertNotNull(g.V(fCall.id()).next().out(CALL).asSequence().firstOrNull { it.id() == fMethod.id() })
g.V(fCall.id()).next().out(ARGUMENT).asSequence().filterIsInstance<Literal>().toList().let { lv ->
assertTrue(lv.any { it.code() == "5" })
assertTrue(lv.any { it.code() == "6" })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.github.plume.oss.querying

import io.github.plume.oss.PlumeCodeToCpgSuite
import io.shiftleft.codepropertygraph.generated.{Operators, nodes}
import io.shiftleft.semanticcpg.language.NoResolve
import io.shiftleft.semanticcpg.language._

class CallTests extends PlumeCodeToCpgSuite {

implicit val resolver = NoResolve

override val code = """
class Foo {
int add(int x, int y) {
return x + y;
}
int main(int argc, char argv) {
return add(argc, 3);
}
}
"""

"should contain a call node for `add` with correct fields" in {
val List(x) = cpg.call("add").l
x.code shouldBe "add(argc, 3)"
x.name shouldBe "add"
x.order shouldBe 1
x.methodInstFullName shouldBe None // Deprecated
x.methodFullName shouldBe "Foo: int add(int,int)"
x.argumentIndex shouldBe 1
// TODO x.signature
// x.typeFullName : deprecated
x.lineNumber shouldBe Some(8)
}

"should allow traversing from call to arguments" in {
cpg.call("add").argument.size shouldBe 2

val List(arg1) = cpg.call("add").argument(1).l
arg1.isInstanceOf[nodes.Identifier] shouldBe true
arg1.asInstanceOf[nodes.Identifier].name shouldBe "argc"
arg1.code shouldBe "argc"
arg1.order shouldBe 1
arg1.argumentIndex shouldBe 1

val List(arg2) = cpg.call("add").argument(2).l
arg2.isInstanceOf[nodes.Literal] shouldBe true
arg2.asInstanceOf[nodes.Literal].code shouldBe "3"
arg2.code shouldBe "3"
arg2.order shouldBe 2
arg2.argumentIndex shouldBe 2
}

"should allow traversing from call to surrounding method" in {
val List(x) = cpg.call("add").method.l
x.name shouldBe "main"
}

"should allow traversing from call to callee method" in {
val List(x) = cpg.call("add").callee.l
x.name shouldBe "add"
}

"should allow traversing from argument to parameter" in {
val List(x) = cpg.call("add").argument(1).parameter.l
x.name shouldBe "x"
}

}

0 comments on commit 2b4a848

Please sign in to comment.