Skip to content

Commit 18ce307

Browse files
authored
Feature/js cli source (#66)
* Added cli source for js Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com> * Added cli source for js Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com> * Improves method full name construction for js Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com> * Revert back changes to get tests working back Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com> * Better way to resolve a method full name Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com> * Fix tests Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com> --------- Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com>
1 parent ba0b1cc commit 18ce307

File tree

13 files changed

+97
-39
lines changed

13 files changed

+97
-39
lines changed

build.sbt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name := "chen"
22
ThisBuild / organization := "io.appthreat"
3-
ThisBuild / version := "2.0.4"
3+
ThisBuild / version := "2.0.5"
44
ThisBuild / scalaVersion := "3.3.1"
55

66
val cpgVersion = "1.0.0"

codemeta.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"downloadUrl": "https://github.com/AppThreat/chen",
88
"issueTracker": "https://github.com/AppThreat/chen/issues",
99
"name": "chen",
10-
"version": "2.0.4",
10+
"version": "2.0.5",
1111
"description": "Code Hierarchy Exploration Net (chen) is an advanced exploration toolkit for your application source code and its dependency hierarchy.",
1212
"applicationCategory": "code-analysis",
1313
"keywords": [

console/src/main/scala/io/appthreat/console/Console.scala

+3
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,8 @@ class Console[T <: Project](
522522
.add(
523523
c.methodFullName + (if c.callee(
524524
NoResolve
525+
).nonEmpty && c.callee(
526+
NoResolve
525527
).head.nonEmpty && c.callee(
526528
NoResolve
527529
).head.isExternal
@@ -530,6 +532,7 @@ class Console[T <: Project](
530532
)
531533
addedMethods += c.methodFullName -> true
532534
)
535+
end if
533536
)
534537
rootTree.add(childTree)
535538
}

meta.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% set version = "2.0.4" %}
1+
{% set version = "2.0.5" %}
22

33
package:
44
name: chen

platform/frontends/javasrc2cpg/src/test/scala/io/appthreat/javasrc2cpg/querying/TypeInferenceTests.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,8 @@ class JavaTypeRecoveryPassTests extends JavaSrcCode2CpgFixture(enableTypeRecover
363363

364364
"hint that `transaction` may be of the null type" in {
365365
val Some(transaction) = cpg.identifier("transaction").headOption: @unchecked
366+
transaction.dynamicTypeHintFullName.contains("null")
366367
transaction.typeFullName shouldBe "org.hibernate.Transaction"
367-
transaction.dynamicTypeHintFullName.contains("null")
368368
}
369369
}
370370

platform/frontends/jssrc2cpg/src/main/scala/io/appthreat/jssrc2cpg/passes/ImportResolverPass.scala

+12-1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ class ImportResolverPass(cpg: Cpg) extends XImportResolverPass(cpg):
9595
else constructorMatches.fullName.toSet
9696
if methodPaths.nonEmpty then
9797
methodPaths.flatMap(x =>
98+
cpg.method.fullNameExact(x).newTagNode(
99+
"exported"
100+
).store()(diffGraph)
98101
Set(ResolvedMethod(x, alias, Option("this")), ResolvedTypeDecl(x))
99102
)
100103
else if moduleExportsThisVariable then
@@ -107,7 +110,11 @@ class ImportResolverPass(cpg: Cpg) extends XImportResolverPass(cpg):
107110
x.argumentOption(2).map(_.code).getOrElse(b.referencedMethod.name)
108111
val (callName, receiver) =
109112
if methodName == "exports" then (alias, Option("this"))
110-
else (methodName, Option(alias))
113+
else
114+
cpg.method.fullNameExact(methodName).newTagNode(
115+
"exported"
116+
).store()(diffGraph)
117+
(methodName, Option(alias))
111118
b.referencedMethod.astParent.iterator
112119
.collectAll[Method]
113120
.fullName
@@ -116,10 +123,14 @@ class ImportResolverPass(cpg: Cpg) extends XImportResolverPass(cpg):
116123
case ::(_, ::(y: Call, _)) =>
117124
// Exported closure with a method ref within the AST of the RHS
118125
y.ast.isMethodRef.map(mRef =>
126+
cpg.method.fullNameExact(mRef.methodFullName).newTagNode(
127+
"exported"
128+
).store()(diffGraph)
119129
ResolvedMethod(mRef.methodFullName, alias, Option("this"))
120130
).toSet
121131
case _ =>
122132
Set.empty[ResolvedImport]
133+
end match
123134
}.toSet
124135
else
125136
Set(UnknownMethod(entity, alias, Option("this")), UnknownTypeDecl(entity))

platform/frontends/jssrc2cpg/src/main/scala/io/appthreat/jssrc2cpg/passes/JavaScriptTypeRecovery.scala

+1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ private class RecoverForJavaScriptFile(
184184
)
185185

186186
override protected def visitIdentifierAssignedToCall(i: Identifier, c: Call): Set[String] =
187+
// Instead of returning empty, this must visit and identify the default export
187188
if c.name == "require" then Set.empty
188189
else super.visitIdentifierAssignedToCall(i, c)
189190

platform/frontends/jssrc2cpg/src/test/scala/io/appthreat/jssrc2cpg/passes/CallLinkerPassTest.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class CallLinkerPassTest extends DataFlowCodeToCpgSuite {
166166
call.methodFullName shouldBe "<unknownFullName>"
167167
inside(call.expressionDown.isIdentifier.l) { case List(receiver: Identifier) =>
168168
receiver.name shouldBe "barOrBaz"
169-
receiver.typeFullName shouldBe "ANY"
169+
receiver.typeFullName shouldBe "baz.js::program"
170170
}
171171
}
172172

@@ -180,7 +180,7 @@ class CallLinkerPassTest extends DataFlowCodeToCpgSuite {
180180
call.methodFullName shouldBe "<unknownFullName>"
181181
inside(call.expressionDown.isIdentifier.l) { case List(receiver: Identifier) =>
182182
receiver.name shouldBe "barOrBaz"
183-
receiver.typeFullName shouldBe "ANY"
183+
receiver.typeFullName shouldBe "baz.js::program"
184184
}
185185
}
186186
}

platform/frontends/jssrc2cpg/src/test/scala/io/appthreat/jssrc2cpg/passes/TypeRecoveryPassTests.scala

+33-14
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
2121
|z.push(4)
2222
|""".stripMargin)
2323

24-
"resolve 'x' identifier types despite shadowing" in {
25-
val List(xOuterScope, xInnerScope) = cpg.identifier.nameExact("x").l
26-
xOuterScope.dynamicTypeHintFullName shouldBe Seq("__ecma.String", "__ecma.Number")
27-
xInnerScope.dynamicTypeHintFullName shouldBe Seq("__ecma.String", "__ecma.Number")
28-
}
29-
3024
"resolve 'z' types correctly" in {
3125
// The dictionary/object type is just considered "ANY" which is fine for now
3226
cpg.identifier("z").typeFullName.toSet.headOption shouldBe Some("__ecma.Array")
@@ -167,10 +161,10 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
167161

168162
"resolve 'foo.x' and 'foo.y' field access primitive types correctly" in {
169163
val List(z1, z2) = cpg.file.name(".*Bar.*").ast.isIdentifier.nameExact("z").l
170-
z1.typeFullName shouldBe "ANY"
171-
z1.dynamicTypeHintFullName shouldBe Seq("__ecma.Number", "__ecma.String")
172-
z2.typeFullName shouldBe "ANY"
173-
z2.dynamicTypeHintFullName shouldBe Seq("__ecma.Number", "__ecma.String")
164+
z1.typeFullName shouldBe "__ecma.String"
165+
z1.dynamicTypeHintFullName shouldBe Seq("__ecma.Number")
166+
z2.typeFullName shouldBe "__ecma.String"
167+
z2.dynamicTypeHintFullName shouldBe Seq("__ecma.Number")
174168
}
175169

176170
"resolve 'foo.d' field access object types correctly" in {
@@ -185,8 +179,7 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
185179

186180
"resolve a 'createTable' call indirectly from 'foo.d' field access correctly" in {
187181
val List(d) = cpg.file.name(".*Bar.*").ast.isCall.name("createTable").l
188-
d.methodFullName shouldBe "flask_sqlalchemy:SQLAlchemy:createTable"
189-
d.dynamicTypeHintFullName shouldBe Seq()
182+
d.dynamicTypeHintFullName shouldBe Seq("d.createTable", "flask_sqlalchemy:SQLAlchemy:createTable")
190183
d.callee(NoResolve).isExternal.headOption shouldBe Some(true)
191184
}
192185

@@ -197,8 +190,7 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
197190
.isCall
198191
.name("deleteTable")
199192
.l
200-
d.methodFullName shouldBe "flask_sqlalchemy:SQLAlchemy:deleteTable"
201-
d.dynamicTypeHintFullName shouldBe empty
193+
d.dynamicTypeHintFullName shouldBe Seq("db.deleteTable", "flask_sqlalchemy:SQLAlchemy:deleteTable")
202194
d.callee(NoResolve).isExternal.headOption shouldBe Some(true)
203195
}
204196

@@ -459,4 +451,31 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {
459451

460452
}
461453

454+
"Default exports with calls" should {
455+
456+
val cpg = code(
457+
"""
458+
|const logger = require('./logger');
459+
|
460+
|function a(){
461+
| logger.info('Hello, world!');
462+
|}
463+
|
464+
|a();
465+
|""".stripMargin,
466+
"app.js"
467+
).moreCode(
468+
"""
469+
|const pino = require('pino');
470+
|
471+
|module.exports = pino({});
472+
|""".stripMargin, "logger.js"
473+
)
474+
475+
"have the correct method full name" in {
476+
cpg.call.name("info").methodFullName.head shouldBe "logger.info"
477+
}
478+
479+
}
480+
462481
}

platform/frontends/pysrc2cpg/src/test/scala/io/appthreat/pysrc2cpg/passes/TypeRecoveryPassTests.scala

+7-7
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
2727

2828
"resolve 'x' identifier types despite shadowing" in {
2929
val List(xOuterScope, xInnerScope) = cpg.identifier("x").take(2).l
30-
xOuterScope.dynamicTypeHintFullName shouldBe Seq("__builtin.int", "__builtin.str")
31-
xInnerScope.dynamicTypeHintFullName shouldBe Seq("__builtin.int", "__builtin.str")
30+
xOuterScope.dynamicTypeHintFullName shouldBe Seq("__builtin.int")
31+
xInnerScope.dynamicTypeHintFullName shouldBe Seq("__builtin.int")
3232
}
3333

3434
"resolve 'y' and 'z' identifier collection types" in {
@@ -241,10 +241,10 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
241241
.isIdentifier
242242
.name("z")
243243
.l
244-
z1.typeFullName shouldBe "ANY"
245-
z1.dynamicTypeHintFullName shouldBe Seq("__builtin.int", "__builtin.str")
246-
z2.typeFullName shouldBe "ANY"
247-
z2.dynamicTypeHintFullName shouldBe Seq("__builtin.int", "__builtin.str")
244+
z1.typeFullName shouldBe "__builtin.str"
245+
z1.dynamicTypeHintFullName shouldBe Seq("__builtin.int")
246+
z2.typeFullName shouldBe "__builtin.str"
247+
z2.dynamicTypeHintFullName shouldBe Seq("__builtin.int")
248248
}
249249

250250
"resolve 'foo.d' field access object types correctly" in {
@@ -460,7 +460,7 @@ class TypeRecoveryPassTests extends PySrc2CpgFixture(withOssDataflow = false) {
460460

461461
"correctly determine that, despite being unable to resolve the correct method full name, that it is an internal method" in {
462462
val Some(selfFindFound) = cpg.typeDecl(".*InstallationsDAO.*").ast.isCall.name("find_one").headOption: @unchecked
463-
selfFindFound.callee.isExternal.toSeq shouldBe Seq(true, true)
463+
selfFindFound.callee.isExternal.toSeq shouldBe Seq(true, true, true)
464464
}
465465
}
466466

platform/frontends/x2cpg/src/main/scala/io/appthreat/x2cpg/passes/frontend/XTypeRecovery.scala

+19-8
Original file line numberDiff line numberDiff line change
@@ -955,16 +955,22 @@ abstract class RecoverForXCompilationUnit[CompilationUnitType <: AstNode](
955955
case x: MethodReturn => setTypeFromTypeHints(x)
956956
case x: Identifier if symbolTable.contains(x) =>
957957
setTypeInformationForRecCall(x, x.inCall.headOption, x.inCall.argument.l)
958-
case x: Call if symbolTable.contains(x) =>
959-
val typs =
960-
if state.config.enabledDummyTypes then symbolTable.get(x).toSeq
961-
else symbolTable.get(x).filterNot(XTypeRecovery.isDummyType).toSeq
962-
storeCallTypeInfo(x, typs)
958+
case x: Call =>
959+
if symbolTable.contains(x) then
960+
val typs =
961+
if state.config.enabledDummyTypes then symbolTable.get(x).toSeq
962+
else symbolTable.get(x).filterNot(XTypeRecovery.isDummyType).toSeq
963+
storeCallTypeInfo(x, typs)
964+
else if x.argument.headOption.exists(symbolTable.contains) then
965+
setTypeInformationForRecCall(x, Option(x), x.argument.l)
966+
else if !x.name.startsWith("<") && !x.code.contains(
967+
"require"
968+
) && !x.code.contains("this")
969+
then
970+
storeCallTypeInfo(x, Seq(x.code.takeWhile(_ != '(')))
963971
case x: Identifier
964972
if symbolTable.contains(CallAlias(x.name)) && x.inCall.nonEmpty =>
965973
setTypeInformationForRecCall(x, x.inCall.headOption, x.inCall.argument.l)
966-
case x: Call if x.argument.headOption.exists(symbolTable.contains) =>
967-
setTypeInformationForRecCall(x, Option(x), x.argument.l)
968974
case _ =>
969975
}
970976
// Set types in an atomic way
@@ -1230,7 +1236,12 @@ abstract class RecoverForXCompilationUnit[CompilationUnitType <: AstNode](
12301236
* types.
12311237
*/
12321238
protected def setTypes(n: StoredNode, types: Seq[String]): Unit =
1233-
if types.size == 1 then builder.setNodeProperty(n, PropertyNames.TYPE_FULL_NAME, types.head)
1239+
if types.size == 1 then
1240+
builder.setNodeProperty(n, PropertyNames.TYPE_FULL_NAME, types.head)
1241+
builder.setNodeProperty(n, PropertyNames.DYNAMIC_TYPE_HINT_FULL_NAME, Seq.empty)
1242+
else if types.size == 2 && types.last.nonEmpty && types.last != "null" then
1243+
builder.setNodeProperty(n, PropertyNames.TYPE_FULL_NAME, types.last)
1244+
builder.setNodeProperty(n, PropertyNames.DYNAMIC_TYPE_HINT_FULL_NAME, Seq(types.head))
12341245
else builder.setNodeProperty(n, PropertyNames.DYNAMIC_TYPE_HINT_FULL_NAME, types)
12351246

12361247
/** Allows one to modify the types assigned to locals.

platform/frontends/x2cpg/src/main/scala/io/appthreat/x2cpg/passes/taggers/EasyTagsPass.scala

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package io.appthreat.x2cpg.passes.taggers
22

33
import io.shiftleft.codepropertygraph.Cpg
4-
import io.shiftleft.codepropertygraph.generated.Languages
4+
import io.shiftleft.codepropertygraph.generated.{Languages, Operators}
55
import io.shiftleft.passes.CpgPass
66
import io.shiftleft.semanticcpg.language.*
77

@@ -14,7 +14,20 @@ class EasyTagsPass(atom: Cpg) extends CpgPass(atom):
1414
override def run(dstGraph: DiffGraphBuilder): Unit =
1515
atom.method.internal.name(".*(valid|check).*").newTagNode("validation").store()(dstGraph)
1616
atom.method.internal.name("is[A-Z].*").newTagNode("validation").store()(dstGraph)
17-
if language == Languages.PYTHON || language == Languages.PYTHONSRC then
17+
if language == Languages.JSSRC || language == Languages.JAVASCRIPT then
18+
// Tag cli source
19+
atom.method.internal.fullName("(index|app).(js|jsx|ts|tsx)::program").newTagNode(
20+
"cli-source"
21+
).store()(
22+
dstGraph
23+
)
24+
// Tag exported methods
25+
atom.call.where(_.methodFullName(Operators.assignment)).code(
26+
"(module\\.)?exports.*"
27+
).argument.isCall.methodFullName.filterNot(_.startsWith("<")).foreach { m =>
28+
atom.method.nameExact(m).newTagNode("exported").store()(dstGraph)
29+
}
30+
else if language == Languages.PYTHON || language == Languages.PYTHONSRC then
1831
atom.method.internal.name("is_[a-z].*").newTagNode("validation").store()(dstGraph)
1932
atom.method.internal.name(".*(encode|escape|sanit).*").newTagNode("sanitization").store()(
2033
dstGraph

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "appthreat-chen"
3-
version = "2.0.4"
3+
version = "2.0.5"
44
description = "Code Hierarchy Exploration Net (chen)"
55
authors = ["Team AppThreat <cloud@appthreat.com>"]
66
license = "Apache-2.0"

0 commit comments

Comments
 (0)