Skip to content

Commit

Permalink
JS (#85)
Browse files Browse the repository at this point in the history
Include framework-input parameter to framework parameter flows

Signed-off-by: Prabhu Subramanian <prabhu@appthreat.com>
  • Loading branch information
prabhu authored Oct 22, 2023
1 parent 3fc6e13 commit 799554c
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 63 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name := "atom"
ThisBuild / organization := "io.appthreat"
ThisBuild / version := "1.5.0"
ThisBuild / version := "1.5.1"
ThisBuild / scalaVersion := "3.3.1"

val chenVersion = "0.0.19"
val chenVersion = "0.0.20"

lazy val atom = Projects.atom

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ class DataFlowSlicing {
private val nodeCache = new TrieMap[Long, SliceNode]()
private var language: Option[String] = _

def calculateDataFlowSlice(cpg: Cpg, config: DataFlowConfig): Option[DataFlowSlice] = {
language = cpg.metaData.language.headOption
def calculateDataFlowSlice(atom: Cpg, config: DataFlowConfig): Option[DataFlowSlice] = {
language = atom.metaData.language.headOption
excludeOperatorCalls.set(config.excludeOperatorCalls)

val dataFlowSlice = (config.fileFilter match {
case Some(fileRegex) => cpg.call.where(_.file.name(fileRegex))
case None => cpg.call
case Some(fileRegex) => atom.call.where(_.file.name(fileRegex))
case None => atom.call
})
.where(c => c.callee.isExternal)
.flatMap {
Expand Down
55 changes: 42 additions & 13 deletions src/main/scala/io/appthreat/atom/slicing/ReachableSlicing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.appthreat.dataflowengineoss.language.*
import io.appthreat.dataflowengineoss.queryengine.{EngineConfig, EngineContext}
import io.appthreat.dataflowengineoss.semanticsloader.Semantics
import io.shiftleft.codepropertygraph.Cpg
import io.shiftleft.codepropertygraph.generated.Languages
import io.shiftleft.codepropertygraph.generated.nodes.*
import io.shiftleft.semanticcpg.language.*

Expand All @@ -17,17 +18,41 @@ object ReachableSlicing {
val engineConfig = EngineConfig()
implicit val context: EngineContext = EngineContext(semantics, engineConfig)
private implicit val finder: NodeExtensionFinder = DefaultNodeExtensionFinder
private val API_TAG = "api"
private def API_TAG = "api"
private def FRAMEWORK_TAG = "framework"

def calculateReachableSlice(atom: Cpg, config: ReachablesConfig): ReachableSlice = {
val language = atom.metaData.language.head
def source = atom.tag.name(config.sourceTag).parameter
def sink = atom.ret.where(_.tag.name(config.sinkTag))
var flowsList = sink.reachableByFlows(source).map(toSlice).toList
// If we did not identify any flows from input to output, fallback to looking for
// flows between two apis
if (flowsList.isEmpty) {
flowsList =
atom.tag.name(API_TAG).parameter.reachableByFlows(atom.tag.name(API_TAG).parameter).map(toSlice).toList
flowsList ++=
atom.tag
.name(FRAMEWORK_TAG)
.method
.parameter
.reachableByFlows(atom.tag.name(config.sourceTag).parameter)
.map(toSlice)
.toList
flowsList ++=
atom.tag.name(API_TAG).parameter.reachableByFlows(atom.tag.name(API_TAG).parameter).map(toSlice).toList
// For JavaScript, we need flows between arguments of call nodes to track callbacks and middlewares
if (language == Languages.JSSRC || language == Languages.JAVASCRIPT) {
def jsCallSource = atom.tag.name(config.sourceTag).call.argument.isIdentifier
def jsFrameworkIdentifier = atom.tag.name(FRAMEWORK_TAG).identifier
def jsFrameworkParameter = atom.tag.name(FRAMEWORK_TAG).parameter
def jsSink = atom.tag.name(config.sinkTag).call.argument.isIdentifier
flowsList ++= jsSink
.reachableByFlows(jsCallSource, jsFrameworkIdentifier, jsFrameworkParameter)
.map(toSlice)
.toList
flowsList ++= atom.tag
.name(FRAMEWORK_TAG)
.call
.argument
.reachableByFlows(jsFrameworkParameter)
.map(toSlice)
.toList
}
ReachableSlice(flowsList)
}
Expand All @@ -39,7 +64,7 @@ object ReachableSlicing {
private def toSlice(path: Path) = {
val tableRows = ArrayBuffer[SliceNode]()
val addedPaths = mutable.Set[String]()
var purls = mutable.Set[String]()
val purls = mutable.Set[String]()
path.elements.foreach { astNode =>
val lineNumber = astNode.lineNumber.getOrElse("").toString
val fileName = astNode.file.name.headOption.getOrElse("").replace("<unknown>", "")
Expand All @@ -58,6 +83,7 @@ object ReachableSlicing {
)
astNode match {
case _: MethodReturn =>
case _: Block =>
case methodParameterIn: MethodParameterIn =>
val methodName = methodParameterIn.method.name
if (tags.isEmpty && methodParameterIn.method.tag.nonEmpty) {
Expand Down Expand Up @@ -104,7 +130,9 @@ object ReachableSlicing {
if (!addedPaths.contains(s"${fileName}#${lineNumber}") && identifier.inCall.nonEmpty) {
sliceNode = sliceNode.copy(
name = identifier.name,
code = if (identifier.inCall.nonEmpty) identifier.inCall.head.code else identifier.code,
code =
if (identifier.inCall.nonEmpty) identifier.inCall.head.code
else identifier.code,
parentMethodName = methodName,
parentMethodSignature = identifier.method.signature,
parentPackageName = identifier.method.location.packageName,
Expand All @@ -122,24 +150,25 @@ object ReachableSlicing {
case call: Call =>
if (!call.code.startsWith("<operator") || !call.methodFullName.startsWith("<operator")) {
if (
tags.isEmpty && call.callee(NoResolve).head.isExternal && !call.methodFullName.startsWith(
"<operator"
) && !call.name
tags.isEmpty && call.callee(NoResolve).nonEmpty && call
.callee(NoResolve)
.head
.isExternal && !call.methodFullName.startsWith("<operator") && !call.name
.startsWith("<operator") && !call.methodFullName.startsWith("new ")
) {
tags = tagAsString(call.callee(NoResolve).head.tag)
purls ++= purlsFromTag(call.callee(NoResolve).head.tag)
}
var isExternal =
if (
call.callee(NoResolve).head.isExternal && !call.name
call.callee(NoResolve).nonEmpty && call.callee(NoResolve).head.isExternal && !call.name
.startsWith("<operator") && !call.methodFullName.startsWith("new ")
) true
else false
if (call.methodFullName.startsWith("<operator")) isExternal = false
sliceNode = sliceNode.copy(
name = call.name,
fullName = call.callee(NoResolve).head.fullName,
fullName = if (call.callee(NoResolve).nonEmpty) call.callee(NoResolve).head.fullName else "",
code = call.code,
isExternal = isExternal,
parentMethodName = call.method.name,
Expand Down
50 changes: 26 additions & 24 deletions src/main/scala/io/appthreat/atom/slicing/UsageSlicing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,42 +24,44 @@ object UsageSlicing {

/** Generates object slices from the given CPG.
*
* @param cpg
* the CPG to slice.
* @param atom
* the atom to slice.
* @return
* a set of object slices.
*/
def calculateUsageSlice(cpg: Cpg, config: UsagesConfig): ProgramSlice = {
def calculateUsageSlice(atom: Cpg, config: UsagesConfig): ProgramSlice = {
implicit val implicitConfig: UsagesConfig = config
excludeOperatorCalls.set(config.excludeOperatorCalls)

def getDeclarations: Traversal[Declaration] = (config.fileFilter match {
case Some(fileName) => cpg.file.nameExact(fileName).method
case None => cpg.method
case Some(fileName) => atom.file.nameExact(fileName).method
case None => atom.method
}).withMethodNameFilter.withMethodParameterFilter.withMethodAnnotationFilter.declaration

def typeMap = TrieMap.from(cpg.typeDecl.map(f => (f.name, f.fullName)).toMap)
val slices = usageSlices(cpg, () => getDeclarations, typeMap)
val language = cpg.metaData.language.headOption
val userDefTypes = userDefinedTypes(cpg)
def typeMap = TrieMap.from(atom.typeDecl.map(f => (f.name, f.fullName)).toMap)
val slices = usageSlices(atom, () => getDeclarations, typeMap)
val language = atom.metaData.language.headOption
val userDefTypes = userDefinedTypes(atom)
if (language.get == Languages.NEWC || language.get == Languages.C)
ProgramUsageSlice(slices ++ importsAsSlices(cpg), userDefTypes)
ProgramUsageSlice(slices ++ importsAsSlices(atom), userDefTypes)
else
ProgramUsageSlice(slices, userDefTypes)
}

import io.shiftleft.semanticcpg.codedumper.CodeDumper.dump

private def usageSlices(cpg: Cpg, getDeclIdentifiers: () => Traversal[Declaration], typeMap: TrieMap[String, String])(
implicit config: UsagesConfig
): List[MethodUsageSlice] = {
val language = cpg.metaData.language.headOption
val root = cpg.metaData.root.headOption
private def usageSlices(
atom: Cpg,
getDeclIdentifiers: () => Traversal[Declaration],
typeMap: TrieMap[String, String]
)(implicit config: UsagesConfig): List[MethodUsageSlice] = {
val language = atom.metaData.language.headOption
val root = atom.metaData.root.headOption
getDeclIdentifiers()
.to(LazyList)
.filterNot(a => a.name.equals("*"))
.filter(a => !a.name.startsWith("_tmp_") && atLeastNCalls(a, config.minNumCalls))
.map(a => exec.submit(new TrackUsageTask(cpg, a, typeMap)))
.map(a => exec.submit(new TrackUsageTask(atom, a, typeMap)))
.flatMap(TimedGet)
.groupBy { case (scope, _) => scope }
.view
Expand All @@ -85,8 +87,8 @@ object UsageSlicing {
.toList
}

private def importsAsSlices(cpg: Cpg): List[MethodUsageSlice] = {
cpg.imports.l.map(im => {
private def importsAsSlices(atom: Cpg): List[MethodUsageSlice] = {
atom.imports.l.map(im => {
MethodUsageSlice(
code = if (im.code.nonEmpty) im.code.replaceAll("\\s*", "") else "",
fullName = im.importedEntity.get,
Expand Down Expand Up @@ -142,12 +144,12 @@ object UsageSlicing {

/** Discovers internally defined types.
*
* @param cpg
* @param atom
* the CPG to query for types.
* @return
* a list of user defined types.
*/
def userDefinedTypes(cpg: Cpg): List[UserDefinedType] = {
def userDefinedTypes(atom: Cpg): List[UserDefinedType] = {

def generateUDT(typeDecl: TypeDecl): UserDefinedType = {
UserDefinedType(
Expand All @@ -173,14 +175,14 @@ object UsageSlicing {
)
}

cpg.typeDecl
atom.typeDecl
.filterNot(t => t.isExternal || t.name.matches("(:program|<module>|<init>|<meta>|<body>|<global>|<clinit>)"))
.map(generateUDT)
.filter(udt => udt.fields.nonEmpty || udt.procedures.nonEmpty)
.l
}

private class TrackUsageTask(cpg: Cpg, tgt: Declaration, typeMap: TrieMap[String, String])(implicit
private class TrackUsageTask(atom: Cpg, tgt: Declaration, typeMap: TrieMap[String, String])(implicit
config: UsagesConfig
) extends Callable[Option[(Method, ObjectUsageSlice)]] {

Expand Down Expand Up @@ -290,7 +292,7 @@ object UsageSlicing {
* an API call if present.
*/
private def exprToObservedCall(baseCall: Call): Option[ObservedCall] = {
val language = cpg.metaData.language.headOption
val language = atom.metaData.language.headOption
val isMemberInvocation = baseCall.name.equals(Operators.fieldAccess)
val isConstructor =
baseCall.name.equals(Operators.alloc) || baseCall.ast.isCall.nameExact(Operators.alloc).nonEmpty
Expand Down Expand Up @@ -353,7 +355,7 @@ object UsageSlicing {
baseCall.argumentOut
.flatMap {
case x: Call if !DefComponent.unresolvedCallPattern.matcher(x.methodFullName).matches() =>
cpg.method.fullNameExact(x.methodFullName).methodReturn.typeFullName.headOption
atom.method.fullNameExact(x.methodFullName).methodReturn.typeFullName.headOption
case x: Call =>
x.callee(resolver).methodReturn.typeFullName.headOption
case _ => None
Expand Down
43 changes: 25 additions & 18 deletions wrapper/nodejs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions wrapper/nodejs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@appthreat/atom",
"version": "1.5.0",
"version": "1.5.1",
"description": "Create atom (⚛) representation for your application, packages and libraries",
"exports": "./index.js",
"type": "module",
Expand All @@ -14,7 +14,7 @@
"yargs": "^17.7.2"
},
"devDependencies": {
"eslint": "^8.51.0"
"eslint": "^8.52.0"
},
"bin": {
"atom": "./index.js",
Expand Down

0 comments on commit 799554c

Please sign in to comment.