Skip to content

Commit 630e1c5

Browse files
authored
Carry over QueryDatabase from query-database repo (#1069)
1 parent 9d31d73 commit 630e1c5

2 files changed

Lines changed: 180 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package io.shiftleft.console
2+
3+
import io.shiftleft.codepropertygraph.Cpg
4+
import io.shiftleft.codepropertygraph.generated.nodes
5+
import org.reflections8.Reflections
6+
import org.reflections8.util.{ClasspathHelper, ConfigurationBuilder}
7+
import org.slf4j.{Logger, LoggerFactory}
8+
import overflowdb.traversal.Traversal
9+
10+
import scala.annotation.{StaticAnnotation, unused}
11+
import scala.jdk.CollectionConverters._
12+
import scala.reflect.runtime.universe._
13+
import scala.reflect.runtime.{universe => ru}
14+
15+
trait QueryBundle
16+
class q() extends StaticAnnotation
17+
18+
case class Query(name: String,
19+
author: String,
20+
title: String,
21+
description: String,
22+
score: Double,
23+
f: Cpg => Traversal[nodes.StoredNode])
24+
25+
class QueryDatabase(defaultArgumentProvider: DefaultArgumentProvider = new DefaultArgumentProvider,
26+
namespace: String = "io.joern.scanners") {
27+
28+
private val logger: Logger = LoggerFactory.getLogger(classOf[QueryDatabase])
29+
30+
private val runtimeMirror: ru.Mirror =
31+
ru.runtimeMirror(getClass.getClassLoader)
32+
33+
/**
34+
* Determine all bundles on the class path
35+
* */
36+
def allBundles: List[Class[_ <: QueryBundle]] =
37+
new Reflections(
38+
new ConfigurationBuilder().setUrls(
39+
ClasspathHelper.forPackage(namespace,
40+
ClasspathHelper.contextClassLoader(),
41+
ClasspathHelper.staticClassLoader()))
42+
).getSubTypesOf(classOf[QueryBundle]).asScala.toList
43+
44+
/**
45+
* Determine queries across all bundles
46+
* */
47+
def allQueries: List[Query] = {
48+
allBundles.flatMap { bundle =>
49+
queriesInBundle(bundle)
50+
}
51+
}
52+
53+
/**
54+
* Return all queries inside `bundle`.
55+
* */
56+
def queriesInBundle[T <: QueryBundle](bundle: Class[T]): List[Query] = {
57+
queryCreatorsInBundle(bundle).map {
58+
case (method, args) =>
59+
method.apply(args: _*).asInstanceOf[Query]
60+
}
61+
}
62+
63+
/**
64+
* Obtain all (methodMirror, args) pairs from bundle, making it possible to override
65+
* default args before creating the query.
66+
* */
67+
def queryCreatorsInBundle[T <: QueryBundle](bundle: Class[T]): List[(ru.MethodMirror, List[Any])] = {
68+
methodsForBundle(bundle).map(m => (m, bundle)).flatMap {
69+
case (method, bundle) =>
70+
val args = defaultArgs(method.symbol, classToType(bundle))
71+
if (args.isDefined) {
72+
List((method, args.get))
73+
} else {
74+
logger.warn(s"Cannot determine default arguments for query: $method")
75+
List()
76+
}
77+
78+
}
79+
}
80+
81+
private def classToType[T](x: Class[T]) = {
82+
runtimeMirror.classSymbol(x).toType
83+
}
84+
85+
private def methodsForBundle[T <: QueryBundle](bundle: Class[T]) = {
86+
val bundleType = classToType(bundle)
87+
val methods = bundleType.members
88+
.collect { case m if m.isMethod => m.asMethod }
89+
.filter { m =>
90+
m.annotations.map(_.tree.tpe.typeSymbol.name.toString).contains("q")
91+
}
92+
93+
val im = runtimeMirror.reflect(
94+
runtimeMirror
95+
.reflectModule(bundleType.typeSymbol.asClass.module.asModule)
96+
.instance)
97+
methods.map { m =>
98+
im.reflectMethod(m)
99+
}.toList
100+
}
101+
102+
private def defaultArgs(method: MethodSymbol, bundleType: Type): Option[List[Any]] = {
103+
val runtimeMirror = ru.runtimeMirror(getClass.getClassLoader)
104+
val im = runtimeMirror.reflect(
105+
runtimeMirror
106+
.reflectModule(bundleType.typeSymbol.asClass.module.asModule)
107+
.instance)
108+
val args = (for (ps <- method.paramLists; p <- ps) yield p).zipWithIndex
109+
.map {
110+
case (x, i) => defaultArgumentProvider.defaultArgument(method, im, x, i)
111+
}
112+
if (args.contains(None)) {
113+
None
114+
} else {
115+
Some(args.map(_.get))
116+
}
117+
}
118+
119+
}
120+
121+
/**
122+
* Joern and Ocular require different implicits to be present, and when
123+
* we encounter these implicits as parameters in a query that we invoke
124+
* via reflection, we need to obtain these implicits from somewhere.
125+
*
126+
* We achieve this by implementing a `DefaultArgumentProvider` for Ocular,
127+
* and one for Joern.
128+
* */
129+
class DefaultArgumentProvider {
130+
131+
def defaultArgument(method: MethodSymbol, im: InstanceMirror, @unused x: Symbol, i: Int): Option[Any] = {
132+
val typeSignature = im.symbol.typeSignature
133+
val defaultMethodName = s"${method.name}$$default$$${i + 1}"
134+
val m = typeSignature.member(TermName(defaultMethodName))
135+
if (m.isMethod) {
136+
Some(im.reflectMethod(m.asMethod).apply())
137+
} else {
138+
None
139+
}
140+
}
141+
142+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.shiftleft.console
2+
3+
import io.shiftleft.semanticcpg.language._
4+
import org.scalatest.matchers.should
5+
import org.scalatest.wordspec.AnyWordSpec
6+
7+
object TestBundle extends QueryBundle {
8+
@q def foo(n: Int = 4): Query = Query(
9+
name = "a-name",
10+
author = "an-author",
11+
title = "a-title",
12+
description = s"a-description $n",
13+
score = 2.0, { cpg =>
14+
cpg.method
15+
}
16+
)
17+
}
18+
19+
class QueryDatabaseTests extends AnyWordSpec with should.Matchers {
20+
"QueryDatabase" should {
21+
"contain Metrics bundle" in {
22+
new QueryDatabase(namespace = "io.shiftleft.console").allBundles.count { bundle =>
23+
bundle.getName.endsWith("TestBundle$")
24+
} shouldBe 1
25+
}
26+
27+
"contain `foo` query" in {
28+
val qdb = new QueryDatabase(namespace = "io.shiftleft.console")
29+
val testBundles = qdb.allBundles.filter { bundle =>
30+
bundle.getName.endsWith("TestBundle$")
31+
}
32+
testBundles.size shouldBe 1
33+
val testBundle = testBundles.head
34+
val queries = qdb.queriesInBundle(testBundle)
35+
queries.count(_.title == "a-title") shouldBe 1
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)