diff --git a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Kotlin2Cpg.scala b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Kotlin2Cpg.scala index 1ffd6196bf57..7b9a0a4dbb94 100644 --- a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Kotlin2Cpg.scala +++ b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Kotlin2Cpg.scala @@ -1,14 +1,15 @@ package io.joern.kotlin2cpg import better.files.File + import java.nio.file.{Files, Paths} import org.jetbrains.kotlin.psi.KtFile import org.slf4j.LoggerFactory + import scala.util.Try import scala.jdk.CollectionConverters.{CollectionHasAsScala, EnumerationHasAsScala} - import io.joern.kotlin2cpg.files.SourceFilesPicker -import io.joern.kotlin2cpg.passes.{AstCreationPass, ConfigPass} +import io.joern.kotlin2cpg.passes.{AstCreationPass, ConfigPass, DependenciesFromMavenCoordinatesPass} import io.joern.kotlin2cpg.compiler.{CompilerAPI, ErrorLoggingMessageCollector} import io.joern.kotlin2cpg.types.{ContentSourcesPicker, DefaultTypeInfoProvider} import io.joern.kotlin2cpg.utils.PathUtils @@ -20,7 +21,7 @@ import io.joern.kotlin2cpg.interop.JavasrcInterop import io.joern.kotlin2cpg.jar4import.UsesService import io.shiftleft.codepropertygraph.Cpg import io.shiftleft.codepropertygraph.generated.Languages -import io.shiftleft.semanticcpg.language._ +import io.shiftleft.semanticcpg.language.* import io.shiftleft.utils.IOUtils object Kotlin2Cpg { @@ -85,6 +86,11 @@ class Kotlin2Cpg extends X2CpgFrontend[Config] with UsesService { Seq() } + val mavenCoordinates = if (config.generateNodesForDependencies) { + logger.info(s"Fetching maven coordinates.") + fetchMavenCoordinates(sourceDir, config) + } else Seq() + val jarsAtConfigClassPath = findJarsIn(config.classpath) if (config.classpath.nonEmpty) { if (jarsAtConfigClassPath.nonEmpty) { @@ -143,6 +149,9 @@ class Kotlin2Cpg extends X2CpgFrontend[Config] with UsesService { val configCreator = new ConfigPass(configFiles, cpg) configCreator.createAndApply() + val dependenciesFromMavenCoordinatesPass = new DependenciesFromMavenCoordinatesPass(mavenCoordinates, cpg) + dependenciesFromMavenCoordinatesPass.createAndApply() + val hasAtLeastOneMethodNode = cpg.method.take(1).nonEmpty if (!hasAtLeastOneMethodNode) { logger.warn("Resulting CPG does not contain any METHOD nodes.") @@ -181,6 +190,25 @@ class Kotlin2Cpg extends X2CpgFrontend[Config] with UsesService { } } + private def fetchMavenCoordinates(sourceDir: String, config: Config): Seq[String] = { + val gradleParams = Map( + GradleConfigKeys.ProjectName -> config.gradleProjectName, + GradleConfigKeys.ConfigurationName -> config.gradleConfigurationName + ).collect { case (key, Some(value)) => (key, value) } + + val resolverParams = DependencyResolverParams(Map.empty, gradleParams) + DependencyResolver.getCoordinates(Paths.get(sourceDir), resolverParams) match { + case Some(coordinates) => + logger.info(s"Found ${coordinates.size} maven coordinates.") + coordinates.toSeq + case None => + logger.warn(s"Could not fetch coordinates for project at path $sourceDir") + println("Could not fetch coordinates when explicitly asked to. Exiting.") + System.exit(1) + Seq() + } + } + private def findJarsIn(dirs: Set[String]) = { val jarExtension = ".jar" dirs.foldLeft(Seq[String]())((acc, classpathEntry) => { diff --git a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Main.scala b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Main.scala index a55f49c88698..abd1af27e92a 100644 --- a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Main.scala +++ b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/Main.scala @@ -13,7 +13,8 @@ final case class Config( gradleProjectName: Option[String] = None, gradleConfigurationName: Option[String] = None, jar4importServiceUrl: Option[String] = None, - includeJavaSourceFiles: Boolean = false + includeJavaSourceFiles: Boolean = false, + generateNodesForDependencies: Boolean = false ) extends X2CpgConfig[Config] { def withClasspath(classpath: Set[String]): Config = { @@ -43,6 +44,10 @@ final case class Config( def withIncludeJavaSourceFiles(value: Boolean): Config = { this.copy(includeJavaSourceFiles = value).withInheritedFields(this) } + + def withGenerateNodesForDependencies(value: Boolean): Config = { + this.copy(generateNodesForDependencies = value).withInheritedFields(this) + } } private object Frontend { @@ -75,7 +80,10 @@ private object Frontend { .action((value, c) => c.withGradleConfigurationName(value)), opt[Unit]("include-java-sources") .text("Include Java sources in the resulting CPG") - .action((_, c) => c.withIncludeJavaSourceFiles(true)) + .action((_, c) => c.withIncludeJavaSourceFiles(true)), + opt[Unit]("generate-nodes-for-dependencies") + .text("Generate nodes for the dependencies of the target project") + .action((_, c) => c.withGenerateNodesForDependencies(true)) ) } } diff --git a/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/passes/DependenciesFromMavenCoordinatesPass.scala b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/passes/DependenciesFromMavenCoordinatesPass.scala new file mode 100644 index 000000000000..7ffe4e2f9a9b --- /dev/null +++ b/joern-cli/frontends/kotlin2cpg/src/main/scala/io/joern/kotlin2cpg/passes/DependenciesFromMavenCoordinatesPass.scala @@ -0,0 +1,41 @@ +package io.joern.kotlin2cpg.passes + +import io.shiftleft.codepropertygraph.Cpg +import io.shiftleft.passes.CpgPass +import io.shiftleft.semanticcpg.language._ +import io.shiftleft.codepropertygraph.generated.nodes.{NewDependency} +import org.slf4j.{Logger, LoggerFactory} + +import scala.util.matching.Regex + +// This pass takes a list of strings representing maven coordinates in order to add DEPENDENCY nodes to the graph. +/* +example of a sequence of coordinates that are valid for the pass: +``` +org.jetbrains.kotlin:kotlin-stdlib:1.7.22 +org.jetbrains.kotlin:kotlin-stdlib-common:1.7.22 +org.jetbrains:annotations:13.0 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.22 +org.apache.logging.log4j:log4j-api:2.15.0 +org.springframework.boot:spring-boot-starter:3.0.5 +org.springframework.boot:spring-boot:3.0.5 +org.springframework:spring-core:6.0.7 +org.springframework:spring-jcl:6.0.7 +org.springframework:spring-context:6.0.7 +``` + */ +class DependenciesFromMavenCoordinatesPass(coordinates: Seq[String], cpg: Cpg) extends CpgPass(cpg) { + override def run(dstGraph: DiffGraphBuilder): Unit = { + + coordinates.foreach { coordinate => + val keyValPattern: Regex = "^([^:]+):([^:]+):([^:]+)$".r + for (patternMatch <- keyValPattern.findAllMatchIn(coordinate)) { + val groupId = patternMatch.group(1) + val name = patternMatch.group(2) + val version = patternMatch.group(3) + val node = NewDependency().name(name).version(version).dependencyGroupId(groupId) + dstGraph.addNode(node) + } + } + } +}