Files
Open_Ultra_SAST/src/openultrasast/cpg/queries/dominance.sc
T
Marius CiepluchandClaude Opus 5 c35426e186 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
2026-09-09 07:57:35 +02:00

101 lines
4.9 KiB
Python

// Guard dominance for the absence families (model-grounded-detection Req 6.2).
//
// An access-control bug is the ABSENCE of a guard: there is no flow to follow and no crash to reproduce.
// What the model can establish is a consistency violation -- an obligated operation that no discharging guard
// governs, in a file where its siblings are governed by one.
//
// "Governed" is not one relation, because a guard discharges an obligation in three different shapes, and
// asking pure CFG dominance of the operation gets two of them wrong:
//
// 1. identity constraint the guard is INSIDE the operation's own arguments
// Note.query.filter_by(owner_id = current_user.id)
// 2. identity branch the method branches on identity, and the denial is controlled by that condition
// note = ...filter_by(id=nid); if note.owner_id != current_user.id: abort(403)
// (measured: abort(403).controlledBy == [note.owner_id != current_user.id])
// 3. dominating guard a call naming a discharger that dominates the operation -- a check before it
//
// Shape 2 is why `controlledBy` is used rather than `dominatedBy`: the ownership check runs AFTER the fetch,
// so it never dominates the operation, but it does control whether the value escapes.
//
// Parameters: cpgFile, operations, dischargers, function (optional; reported, never used to filter, because
// the sibling comparison needs every operation in the CPG).
//
// Output: a fenced JSON array of {operation, opLine, opMethod, dominatingGuards}.
// 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
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
// negative on the one family this arbiter exists for.
def mentionsGuard(code: String): Boolean =
guardTokens.exists(g => java.util.regex.Pattern.compile("(?<![A-Za-z0-9_])" + java.util.regex.Pattern.quote(g) + "(?![A-Za-z0-9_])").matcher(code).find())
// Match by CALL NAME, never by code containment: the frontend desugars `Note.query.filter_by(x).first()`
// into a chain of temporaries, and matching on code yields four or five rows for one operation -- some of
// them the bare `tmp1.filter_by` receiver, which carries none of the guards the real call does. A `leaky`
// verdict computed over those extra rows would flag a guarded operation. A dotted spec (`query.get`)
// matches on its trailing segment, which is how the call is named in the graph.
def matchesOperation(name: String): Boolean =
opNames.exists(n => name == n || name == n.split("\\.").last)
val opCalls = cpg.call.filter(c => matchesOperation(c.name)).l
val rows = opCalls.map { op =>
val method = op.method
// 1. identity constraint: a discharger inside the operation's own arguments.
val inArguments = op.argument.code.l.filter(mentionsGuard)
// 2. identity branch: a control structure in this method whose condition names a discharger. The denial
// it guards need not dominate the operation -- it controls whether the fetched value escapes.
val inCondition = method.controlStructure.condition.code.l.filter(mentionsGuard)
// 3. dominating guard: a call naming a discharger that actually dominates the operation.
val dominating = op.dominatedBy.isCall.code.l.filter(mentionsGuard)
val guards = (inArguments ++ inCondition ++ dominating).distinct.map(_.take(120))
ujson.Obj(
"operation" -> op.code.take(200),
"opLine" -> op.lineNumber.getOrElse(-1).toString,
"opMethod" -> method.name,
"dominatingGuards" -> ujson.Arr(guards.map(ujson.Str(_)): _*)
)
}
rows
}
println("---OUSAST-CPG-BEGIN---")
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---")
}