Skip to content

Commit

Permalink
Added support for Fresh (#215)
Browse files Browse the repository at this point in the history
This is for: ShiftLeftSecurity/product#11136

Co-authored-by: Malte Kraus <[email protected]>
  • Loading branch information
max-leuthaeuser and maltek committed Oct 18, 2022
1 parent 4082f94 commit 829ef11
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 32 deletions.
11 changes: 7 additions & 4 deletions src/main/scala/io/shiftleft/js2cpg/core/Js2Cpg.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import better.files.File.LinkOptions
import io.shiftleft.js2cpg.cpg.passes._
import io.shiftleft.js2cpg.io.FileDefaults._
import io.shiftleft.js2cpg.io.FileUtils
import io.shiftleft.js2cpg.parser.PackageJsonParser
import io.shiftleft.js2cpg.parser.{FreshJsonParser, PackageJsonParser}
import io.shiftleft.js2cpg.preprocessing.NuxtTranspiler
import io.shiftleft.js2cpg.preprocessing.TranspilationRunner
import io.shiftleft.js2cpg.util.MemoryMetrics
Expand Down Expand Up @@ -196,9 +196,12 @@ class Js2Cpg {
new ConfigPass(configFiles(config, List(HTML_SUFFIX)), cpg, report).createAndApply()
}

if (config.includeConfigs) {
new ConfigPass(configFiles(config, CONFIG_FILES), cpg, report).createAndApply()
}
val freshJsons = FreshJsonParser.findImportMapPaths(config, includeDenoConfig = true).map(_.normalize)

val cfgFiles =
configFiles(config, CONFIG_FILES)
.filter(p => config.includeConfigs || freshJsons.contains(p._1.normalize))
new ConfigPass(cfgFiles, cpg, report).createAndApply()

cpg.close()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,31 @@ import io.shiftleft.codepropertygraph.Cpg
import io.shiftleft.codepropertygraph.generated.nodes.NewDependency
import io.shiftleft.js2cpg.core.Config
import io.shiftleft.js2cpg.io.FileUtils
import io.shiftleft.js2cpg.parser.PackageJsonParser
import io.shiftleft.js2cpg.parser.{FreshJsonParser, PackageJsonParser}
import io.shiftleft.passes.SimpleCpgPass

import java.nio.file.Paths

class DependenciesPass(cpg: Cpg, config: Config) extends SimpleCpgPass(cpg) {

override def run(diffGraph: DiffGraphBuilder): Unit = {
private def dependenciesForPackageJsons(): Map[String, String] = {
val packagesJsons =
(FileUtils
.getFileTree(Paths.get(config.srcDir), config, List(".json"))
.filter(_.toString.endsWith(PackageJsonParser.PACKAGE_JSON_FILENAME)) :+
config.createPathForPackageJson()).toSet
packagesJsons.flatMap(p => PackageJsonParser.dependencies(p)).toMap
}

val dependencies: Map[String, String] =
packagesJsons.flatMap(p => PackageJsonParser.dependencies(p)).toMap
private def dependenciesForFreshJsons(): Map[String, String] = {
FreshJsonParser
.findImportMapPaths(config, includeDenoConfig = false)
.flatMap(p => FreshJsonParser.dependencies(p))
.toMap
}

override def run(diffGraph: DiffGraphBuilder): Unit = {
val dependencies = dependenciesForPackageJsons() ++ dependenciesForFreshJsons()
dependencies.foreach { case (name, version) =>
val dep = NewDependency()
.name(name)
Expand Down
80 changes: 80 additions & 0 deletions src/main/scala/io/shiftleft/js2cpg/parser/FreshJsonParser.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.shiftleft.js2cpg.parser

import java.nio.file.{Files, Path, Paths}
import io.shiftleft.js2cpg.io.FileUtils
import org.slf4j.LoggerFactory
import com.fasterxml.jackson.databind.ObjectMapper
import io.shiftleft.js2cpg.core.Config
import io.shiftleft.js2cpg.preprocessing.TypescriptTranspiler

import scala.collection.concurrent.TrieMap
import scala.util.Try
import scala.jdk.CollectionConverters._

object FreshJsonParser {
private val logger = LoggerFactory.getLogger(FreshJsonParser.getClass)

private val cachedDependencies: TrieMap[Path, Map[String, String]] = TrieMap.empty

private def dropLastSlash(str: String): String = str.takeRight(1) match {
case "/" => str.dropRight(1)
case _ => str
}

private def cleanKey(key: String): String =
dropLastSlash(key).replaceFirst("\\$", "")

private def extractVersion(str: String): String = {
val dropped = dropLastSlash(str.replace("mod.ts", ""))
dropped.substring(dropped.lastIndexOf("@") + 1, dropped.length)
}

def findImportMapPaths(config: Config, includeDenoConfig: Boolean): Set[Path] = {
val objectMapper = new ObjectMapper
FileUtils
.getFileTree(Paths.get(config.srcDir), config, List(".json"))
.filter(_.endsWith(TypescriptTranspiler.DENO_CONFIG))
.flatMap { file =>
val packageJson = objectMapper.readTree(Files.readAllBytes(file))
val importMap = Option(packageJson.path("importMap").asText()).map(file.resolveSibling)

if (includeDenoConfig) Iterable(file) ++ importMap
else importMap
}
.filter(Files.exists(_))
.toSet
}

def dependencies(freshJsonPath: Path): Map[String, String] =
cachedDependencies.getOrElseUpdate(
freshJsonPath, {
val deps = Try {
val content = FileUtils.readLinesInFile(freshJsonPath).mkString("\n")
val objectMapper = new ObjectMapper
val json = objectMapper.readTree(content)

var depToVersion = Map.empty[String, String]
val dependencyIt = Option(json.get("imports"))
.map(_.fields().asScala)
.getOrElse(Iterator.empty)
dependencyIt.foreach {
case entry if entry.getKey.startsWith("@") => // ignored
case entry =>
depToVersion = depToVersion.updated(cleanKey(entry.getKey), extractVersion(entry.getValue.asText()))
}
depToVersion
}.toOption

if (deps.isDefined) {
logger.debug(s"Loaded dependencies from '$freshJsonPath'.")
deps.get
} else {
logger.debug(
s"No project dependencies found in ${freshJsonPath.getFileName} at '${freshJsonPath.getParent}'."
)
Map.empty
}
}
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import io.shiftleft.js2cpg.io.{ExternalCommand, FileUtils}
import io.shiftleft.js2cpg.parser.PackageJsonParser
import io.shiftleft.js2cpg.parser.TsConfigJsonParser
import io.shiftleft.js2cpg.preprocessing.TypescriptTranspiler.DEFAULT_MODULE
import io.shiftleft.js2cpg.preprocessing.TypescriptTranspiler.DENO_CONFIG
import org.slf4j.LoggerFactory
import org.apache.commons.io.{FileUtils => CommonsFileUtils}

import java.nio.file.{Path, Paths}
import scala.util.{Failure, Success, Try}

Expand All @@ -22,6 +22,8 @@ object TypescriptTranspiler {
private val tscTypingWarnings =
List("error TS", ".d.ts", "The file is in the program because", "Entry point of type library")

val DENO_CONFIG: String = "deno.json"

}

class TypescriptTranspiler(override val config: Config, override val projectPath: Path, subDir: Option[Path] = None)
Expand All @@ -36,10 +38,13 @@ class TypescriptTranspiler(override val config: Config, override val projectPath
private def hasTsFiles: Boolean =
FileUtils.getFileTree(projectPath, config, List(TS_SUFFIX)).nonEmpty

private def isFreshProject: Boolean = (File(projectPath) / DENO_CONFIG).exists

private def isTsProject: Boolean =
(File(projectPath) / "tsconfig.json").exists || isFreshProject

override def shouldRun(): Boolean =
config.tsTranspiling &&
(File(projectPath) / "tsconfig.json").exists &&
hasTsFiles
config.tsTranspiling && isTsProject && hasTsFiles

private def moveIgnoredDirs(from: File, to: File): Unit = {
val ignores = if (config.ignoreTests) {
Expand Down Expand Up @@ -110,6 +115,13 @@ class TypescriptTranspiler(override val config: Config, override val projectPath
override protected def transpile(tmpTranspileDir: Path): Boolean = {
if (installTsPlugins()) {
File.usingTemporaryDirectory() { tmpForIgnoredDirs =>
if (isFreshProject) {
// Fresh projects do not need a separate tsconfig, but tsc needs at least an empty one
(File(projectPath) / "tsconfig.json")
.touch()
.write("{}")
.deleteOnExit(swallowIOExceptions = true)
}
// Sadly, tsc does not allow to exclude folders when being run from cli.
// Hence, we have to move ignored folders to a temporary folder ...
moveIgnoredDirs(File(projectPath), tmpForIgnoredDirs)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package io.shiftleft.js2cpg.cpg.passes

import better.files.File
import io.shiftleft.codepropertygraph.Cpg
import io.shiftleft.codepropertygraph.generated.PropertyNames
import better.files.File
import io.shiftleft.js2cpg.core.{Config, Report}
import io.shiftleft.js2cpg.parser.PackageJsonParser
import io.shiftleft.js2cpg.preprocessing.TypescriptTranspiler
import overflowdb.traversal._

class DependenciesPassTest extends AbstractPassTest {
Expand Down Expand Up @@ -61,7 +62,7 @@ class DependenciesPassTest extends AbstractPassTest {

"generate dependency nodes correctly (simple lock dependencies)" in DependencyFixture(
code = "",
packageJsonContent = """
jsonContent = """
|{
| "dependencies": {
| "dep1": {
Expand All @@ -73,23 +74,68 @@ class DependenciesPassTest extends AbstractPassTest {
| }
|}
|""".stripMargin,
packageJsonName = PackageJsonParser.JSON_LOCK_FILENAME
jsonFilename = PackageJsonParser.JSON_LOCK_FILENAME
) { cpg =>
def deps = getDependencies(cpg)
deps.size shouldBe 2
deps.has(PropertyNames.NAME, "dep1").has(PropertyNames.VERSION, "0.1").size shouldBe 1
deps.has(PropertyNames.NAME, "dep2").has(PropertyNames.VERSION, "0.2").size shouldBe 1
}

"generate dependency nodes correctly (simple fresh dependencies)" in DependencyFixture(
code = "",
jsons = Iterable(
(
"import_map.json",
"""
|{
| "imports": {
| "@/": "./",
| "$fresh/": "https://deno.land/x/[email protected]/",
| "$std/": "https://deno.land/[email protected]/",
| "gfm": "https://deno.land/x/[email protected]/mod.ts",
| "preact": "https://esm.sh/[email protected]",
| "preact/": "https://esm.sh/[email protected]/",
| "preact/signals": "https://esm.sh/*@preact/[email protected]",
| "preact/signals-core": "https://esm.sh/*@preact/[email protected]",
| "preact-render-to-string": "https://esm.sh/*[email protected]/",
| "twind": "https://esm.sh/[email protected]",
| "twind/": "https://esm.sh/[email protected]/",
| "redis": "https://deno.land/x/[email protected]/mod.ts",
| "puppeteer": "https://deno.land/x/[email protected]/mod.ts",
| "envalid": "https://deno.land/x/[email protected]/mod.ts"
| }
|}
|""".stripMargin
),
(TypescriptTranspiler.DENO_CONFIG, """{"importMap": "./import_map.json"}""")
)
) { cpg =>
def deps = getDependencies(cpg)

deps.size shouldBe 11
deps.has(PropertyNames.NAME, "fresh").has(PropertyNames.VERSION, "1.1.0").size shouldBe 1
deps.has(PropertyNames.NAME, "std").has(PropertyNames.VERSION, "0.152.0").size shouldBe 1
deps.has(PropertyNames.NAME, "gfm").has(PropertyNames.VERSION, "0.1.22").size shouldBe 1
deps.has(PropertyNames.NAME, "preact").has(PropertyNames.VERSION, "10.10.6").size shouldBe 1
deps.has(PropertyNames.NAME, "preact/signals").has(PropertyNames.VERSION, "1.0.3").size shouldBe 1
deps.has(PropertyNames.NAME, "preact/signals-core").has(PropertyNames.VERSION, "1.0.1").size shouldBe 1
deps.has(PropertyNames.NAME, "preact-render-to-string").has(PropertyNames.VERSION, "5.2.3").size shouldBe 1
deps.has(PropertyNames.NAME, "twind").has(PropertyNames.VERSION, "0.16.17").size shouldBe 1
deps.has(PropertyNames.NAME, "redis").has(PropertyNames.VERSION, "v0.26.0").size shouldBe 1
deps.has(PropertyNames.NAME, "puppeteer").has(PropertyNames.VERSION, "16.2.0").size shouldBe 1
deps.has(PropertyNames.NAME, "envalid").has(PropertyNames.VERSION, "0.1.2").size shouldBe 1
}

"generate dependency nodes correctly (simple dependency)" in DependencyFixture(
code = "",
packageJsonContent = """
|{
| "dependencies": {
| "dep1": "0.1"
| }
|}
|""".stripMargin
jsonContent = """
|{
| "dependencies": {
| "dep1": "0.1"
| }
|}
|""".stripMargin
) { cpg =>
def deps = getDependencies(cpg)
deps.size shouldBe 1
Expand All @@ -98,7 +144,7 @@ class DependenciesPassTest extends AbstractPassTest {

"generate dependency nodes correctly (different types of dependencies)" in DependencyFixture(
code = "",
packageJsonContent = """
jsonContent = """
{
"dependencies": {
"dep1": "0.1"
Expand Down Expand Up @@ -126,25 +172,31 @@ class DependenciesPassTest extends AbstractPassTest {
}

private object DependencyFixture extends Fixture {
def apply(
code: String,
packageJsonContent: String,
packageJsonName: String = PackageJsonParser.PACKAGE_JSON_FILENAME
)(f: Cpg => Unit): Unit = {
def apply(code: String, jsons: Iterable[(String, String)])(f: Cpg => Unit): Unit = {
File.usingTemporaryDirectory("js2cpgTest") { dir =>
val file = dir / "file.js"
val json = dir / packageJsonName
file.write(code)
json.write(packageJsonContent)

val cpg = Cpg.emptyCpg
val packageJson: String = jsons.head._1
for ((jsonFilename, jsonContent) <- jsons) {
val json = dir / jsonFilename
json.write(jsonContent)
}

val filenames = List((file.path, file.parent.path))
val cpg = Cpg.emptyCpg
new AstCreationPass(dir, filenames, cpg, new Report()).createAndApply()
new DependenciesPass(cpg, Config(srcDir = dir.toString, packageJsonLocation = packageJsonName)).createAndApply()
new DependenciesPass(cpg, Config(srcDir = dir.toString, packageJsonLocation = packageJson)).createAndApply()

f(cpg)
}
}

def apply(code: String, jsonContent: String, jsonFilename: String = PackageJsonParser.PACKAGE_JSON_FILENAME)(
f: Cpg => Unit
): Unit = {
DependencyFixture(code, Iterable((jsonFilename, jsonContent)))(f)
}
}

}

0 comments on commit 829ef11

Please sign in to comment.