mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-19 04:59:28 +00:00
perf(model): one Joern invocation per query kind, not one per region
JVM startup dominated a scan: a call per region per family put a ten-line file at 275s over three regions, and would have put a thousand regions at roughly fifty hours. The queries themselves are milliseconds once the CPG is loaded, so the cost was almost entirely process starts. scan_repository() is now collect -> batch -> arbitrate. The three .sc scripts accept a `requests` JSON map and answer all of them in one run, keeping the single-request form so the committed measurements stay reproducible byte-for-byte. Arbiters are untouched: each is handed a CpgResult whose `run` returns its own prefetched rows. The batcher builds parameters by calling request_params() extracted from taint.py, dominance.py and config_value.py rather than reconstructing them. A second copy that drifts from the arbiter's is the exact shape of the five family-assumption bugs the predecessor spec spent a session finding; there is now one definition per family and the batcher borrows it. Regions are also deduplicated by (path, function), collapsing the nameless entry-point rows that were producing `model:injection:?:10`. Measured on the same ten-line file: 275s -> 106s, three duplicate findings -> one `model:injection:ping:10`, rung unchanged at model_entailed. Gates byte-identical. Server mode is recorded as task 2.3, deliberately sequenced after the repository measurement: whether the remaining ~100s floor is worth a process model, a weakened subprocess seam and a Scala-evaluating HTTP endpoint depends on a build-time-to-query-time ratio only 2.2 produces. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Opus 5
parent
e67f3ee2f0
commit
c35426e186
@@ -116,6 +116,43 @@ class JoernBackend:
|
||||
return None
|
||||
return CpgResult(cpg_path=cpg_path, run=lambda query, params: self.query(cpg_path, query, params))
|
||||
|
||||
def query_batch(self, cpg_path: Path, query: str, requests: Mapping[str, Mapping[str, object]]) -> dict[str, list[object]]:
|
||||
"""Run ONE script invocation carrying many requests, keyed back to their ids.
|
||||
|
||||
JVM startup, not CPG construction, is what dominates a repository scan: every ``joern --script`` call
|
||||
starts a JVM of roughly thirty seconds, and issuing one per region per family put a ten-line Python
|
||||
file at four minutes and a thousand regions at about fifty hours. The CPG is already built and loaded
|
||||
by then; the work itself is milliseconds. So the batch carries the whole scan's questions in one
|
||||
parameter and the script loops over them.
|
||||
|
||||
Fails closed like every other path here: an engine that could not answer returns ``{}``, never an
|
||||
empty result per request, because "no rows" and "could not ask" must stay distinguishable.
|
||||
"""
|
||||
script = self.queries_dir / f"{query}.sc"
|
||||
if not script.is_file():
|
||||
logger.warning("no such cpg query: %s", script)
|
||||
return {}
|
||||
payload = json.dumps({rid: {k: _render(v) for k, v in req.items()} for rid, req in requests.items()})
|
||||
command = [
|
||||
shutil.which("joern") or "joern",
|
||||
"--script",
|
||||
str(script),
|
||||
"--param",
|
||||
f"cpgFile={cpg_path}",
|
||||
"--param",
|
||||
f"requests={payload}",
|
||||
]
|
||||
completed = self._run(command, timeout=self.query_timeout, cwd=cpg_path.parent)
|
||||
if completed is None or completed.returncode != 0:
|
||||
detail = (completed.stderr or "")[-400:] if completed is not None else "timeout"
|
||||
logger.warning("cpg batch %s failed: %s", query, detail)
|
||||
return {}
|
||||
parsed = extract_payload(completed.stdout or "")
|
||||
if not isinstance(parsed, Mapping):
|
||||
return {}
|
||||
# A request the engine did not answer is absent, not empty: the caller must be able to tell them apart.
|
||||
return {str(rid): list(rows) for rid, rows in parsed.items() if isinstance(rows, list)}
|
||||
|
||||
def query(self, cpg_path: Path, query: str, params: Mapping[str, object]) -> object | None:
|
||||
"""Run a shipped CPGQL script against a built CPG and return its fenced JSON payload."""
|
||||
script = self.queries_dir / f"{query}.sc"
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
// Parameters: cpgFile, settings, function (optional).
|
||||
// Output: fenced JSON array of {setting, line, method, args, literalArgs}.
|
||||
|
||||
@main def exec(cpgFile: String, settings: String, function: String = "") = {
|
||||
// BATCHED, like taint.sc. Single-request form kept for the committed measurements.
|
||||
|
||||
@main def exec(cpgFile: String, settings: String = "", function: String = "", requests: String = "") = {
|
||||
importCpg(cpgFile)
|
||||
|
||||
def split(raw: String): List[String] = raw.split(",").map(_.trim).filter(_.nonEmpty).toList
|
||||
val names = split(settings)
|
||||
|
||||
def rowsFor(settingsS: String, functionS: String): List[ujson.Obj] = {
|
||||
val names = split(settingsS)
|
||||
val function = functionS
|
||||
|
||||
def matches(name: String): Boolean = names.exists(n => name == n || name == n.split("\\.").last)
|
||||
|
||||
@@ -38,7 +43,19 @@
|
||||
)
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
println("---OUSAST-CPG-BEGIN---")
|
||||
println(ujson.write(ujson.Arr(rows: _*)))
|
||||
if (requests.nonEmpty) {
|
||||
val parsed = ujson.read(requests).obj
|
||||
val answers = parsed.map { case (id, req) =>
|
||||
def field(name: String): String = req.obj.get(name).map(_.str).getOrElse("")
|
||||
id -> ujson.Arr(rowsFor(field("settings"), field("function")): _*)
|
||||
}
|
||||
println(ujson.write(ujson.Obj.from(answers)))
|
||||
} else {
|
||||
println(ujson.write(ujson.Arr(rowsFor(settings, function): _*)))
|
||||
}
|
||||
println("---OUSAST-CPG-END---")
|
||||
}
|
||||
|
||||
@@ -22,13 +22,26 @@
|
||||
//
|
||||
// Output: a fenced JSON array of {operation, opLine, opMethod, dominatingGuards}.
|
||||
|
||||
@main def exec(cpgFile: String, operations: String, dischargers: String, function: String = "") = {
|
||||
// BATCHED, like taint.sc: `requests` is {id: {operations, dischargers, function}} and the output is
|
||||
// {id: [row, ...]}. One JVM start answers a whole scan. The single-request form is kept so the committed
|
||||
// measurements remain reproducible against the same call shape.
|
||||
|
||||
@main def exec(
|
||||
cpgFile: String,
|
||||
operations: String = "",
|
||||
dischargers: String = "",
|
||||
function: String = "",
|
||||
requests: String = ""
|
||||
) = {
|
||||
importCpg(cpgFile)
|
||||
|
||||
def split(raw: String): List[String] = raw.split(",").map(_.trim).filter(_.nonEmpty).toList
|
||||
|
||||
val opNames = split(operations)
|
||||
val guardTokens = split(dischargers)
|
||||
def rowsFor(operationsS: String, dischargersS: String, functionS: String): List[ujson.Obj] = {
|
||||
|
||||
val opNames = split(operationsS)
|
||||
val guardTokens = split(dischargersS)
|
||||
val function = functionS
|
||||
|
||||
// Word-boundary matching, never substring. A discharger token like `user` matched as a substring hits
|
||||
// `users` inside "SELECT * FROM users WHERE id = ?", marking a textbook IDOR as guarded -- a silent false
|
||||
@@ -69,7 +82,19 @@
|
||||
)
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
println("---OUSAST-CPG-BEGIN---")
|
||||
println(ujson.write(ujson.Arr(rows: _*)))
|
||||
if (requests.nonEmpty) {
|
||||
val parsed = ujson.read(requests).obj
|
||||
val answers = parsed.map { case (id, req) =>
|
||||
def field(name: String): String = req.obj.get(name).map(_.str).getOrElse("")
|
||||
id -> ujson.Arr(rowsFor(field("operations"), field("dischargers"), field("function")): _*)
|
||||
}
|
||||
println(ujson.write(ujson.Obj.from(answers)))
|
||||
} else {
|
||||
println(ujson.write(ujson.Arr(rowsFor(operations, dischargers, function): _*)))
|
||||
}
|
||||
println("---OUSAST-CPG-END---")
|
||||
}
|
||||
|
||||
@@ -22,18 +22,29 @@
|
||||
// Output: a fenced JSON array of {sink, sinkLine, sinkMethod, source, sanitized, length}. The fence exists
|
||||
// because Joern prints a banner, pass logs and a prompt around whatever a script emits.
|
||||
|
||||
// BATCHED. `requests` is a JSON object of {id: {sources, sinks, sanitizers, function, parameterSources}} and
|
||||
// the output is {id: [row, ...]}. One invocation answers a whole scan's questions, because the JVM start
|
||||
// (~30s) dwarfs the queries themselves once the CPG is loaded -- one call per region per family put a
|
||||
// ten-line file at four minutes and a thousand regions at roughly fifty hours.
|
||||
//
|
||||
// The single-request form is kept below it: the pair harnesses and every measurement committed so far use it,
|
||||
// and changing their call shape would silently invalidate comparisons against those artifacts.
|
||||
|
||||
@main def exec(
|
||||
cpgFile: String,
|
||||
sources: String,
|
||||
sinks: String,
|
||||
sources: String = "",
|
||||
sinks: String = "",
|
||||
sanitizers: String = "",
|
||||
function: String = "",
|
||||
parameterSources: String = "false"
|
||||
parameterSources: String = "false",
|
||||
requests: String = ""
|
||||
) = {
|
||||
importCpg(cpgFile)
|
||||
|
||||
def split(raw: String): List[String] = raw.split(",").map(_.trim).filter(_.nonEmpty).toList
|
||||
|
||||
def rowsFor(sourcesS: String, sinksS: String, sanitizersS: String, functionS: String, paramSrc: String): List[ujson.Obj] = {
|
||||
|
||||
// Word-boundary matching, never substring. `resolveUrl` contains `resolve`, so a bare-substring sanitizer
|
||||
// test marked the VULNERABLE flow sanitized and the verdict fell back to a captured `res` that names no
|
||||
// attacker input. Same bug class as the dominance query's guard matching; fixed in both.
|
||||
@@ -45,9 +56,11 @@
|
||||
.find()
|
||||
)
|
||||
|
||||
val sourcePatterns = split(sources)
|
||||
val sinkNames = split(sinks)
|
||||
val sanitizerNames = split(sanitizers)
|
||||
val sourcePatterns = split(sourcesS)
|
||||
val sinkNames = split(sinksS)
|
||||
val sanitizerNames = split(sanitizersS)
|
||||
val function = functionS
|
||||
val parameterSources = paramSrc
|
||||
|
||||
// A source is any call whose code contains one of the patterns. Matching on `code` rather than a method
|
||||
// name keeps `request.args["x"]` (an indexAccess over a fieldAccess) and `req.body.name` both reachable
|
||||
@@ -126,7 +139,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
println("---OUSAST-CPG-BEGIN---")
|
||||
println(ujson.write(ujson.Arr(rows: _*)))
|
||||
if (requests.nonEmpty) {
|
||||
val parsed = ujson.read(requests).obj
|
||||
val answers = parsed.map { case (id, req) =>
|
||||
def field(name: String): String = req.obj.get(name).map(_.str).getOrElse("")
|
||||
val paramSrc = req.obj.get("parameterSources").map(_.str).getOrElse("false")
|
||||
id -> ujson.Arr(rowsFor(field("sources"), field("sinks"), field("sanitizers"), field("function"), paramSrc): _*)
|
||||
}
|
||||
println(ujson.write(ujson.Obj.from(answers)))
|
||||
} else {
|
||||
println(ujson.write(ujson.Arr(rowsFor(sources, sinks, sanitizers, function, parameterSources): _*)))
|
||||
}
|
||||
println("---OUSAST-CPG-END---")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user