From 4f5053bae1e5c06933090204714f758ee17a3afe Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:17:42 +0200 Subject: [PATCH] feat(cpg): let the field half be switched off, so its cost can be attributed A cost you cannot switch off is a cost you cannot attribute. The hook half already disables itself when `hookCallbacks` is empty; the field half had no equivalent, which made "what do the two-stage joins cost" unanswerable except by editing the query -- measuring through a modified instrument, which this project has been burned by twice already. `fieldSources` defaults to ON when the field is absent, so nothing about the shipped behaviour depends on a caller remembering to set it. 850 tests pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG --- src/openultrasast/cpg/queries/taint.sc | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/openultrasast/cpg/queries/taint.sc b/src/openultrasast/cpg/queries/taint.sc index dc9aeca..855875e 100644 --- a/src/openultrasast/cpg/queries/taint.sc +++ b/src/openultrasast/cpg/queries/taint.sc @@ -48,6 +48,7 @@ callDepth: String = "0", boundedSinks: String = "", parameterSources: String = "false", + fieldSources: String = "true", hookCallbacks: String = "", dispatchApply: String = "", requests: String = "", @@ -72,6 +73,7 @@ dispatchApplyS: String, functionS: String, paramSrc: String, + fieldSrc: String, fileS: String, depthS: String, boundedS: String @@ -289,10 +291,15 @@ else (2 to parts.length).exists(n => fieldIsTainted(fileName, parts.take(n).mkString("->"))) } - def fieldSourceNodes = { - val reads = cpg.call.nameExact(FIELD_ACCESS).filter(c => inScope(c.method)).l - if (reads.isEmpty) Iterator.empty else reads.filter(r => taintedPrefixes(r.code.trim, fileS)).iterator - } + // Off by request only, and defaulting to ON when the field is absent, so nothing about the shipped + // behaviour depends on a caller remembering to set it. It exists so the join can be measured against + // itself: a cost you cannot switch off is a cost you cannot attribute. + def fieldSourceNodes = + if (fieldSrc != "true") Iterator.empty + else { + val reads = cpg.call.nameExact(FIELD_ACCESS).filter(c => inScope(c.method)).l + if (reads.isEmpty) Iterator.empty else reads.filter(r => taintedPrefixes(r.code.trim, fileS)).iterator + } // ---- the hook half of the two-stage join (task 5.11) ---------------------------------------------- // @@ -568,6 +575,7 @@ 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") + val fieldSrc = req.obj.get("fieldSources").map(_.str).getOrElse("true") id -> ujson.Arr( rowsFor( field("sources"), @@ -580,6 +588,7 @@ if (field("dispatchApply").nonEmpty) field("dispatchApply") else dispatchApply, field("function"), paramSrc, + fieldSrc, field("file"), field("callDepth"), field("boundedSinks") @@ -595,7 +604,7 @@ println(ujson.write(ujson.Obj.from(answers.toSeq :+ ("__census__" -> census)))) } else { println( - ujson.write(ujson.Arr(rowsFor(sources, sinks, sanitizers, hookCallbacks, dispatchApply, function, parameterSources, file, callDepth, boundedSinks): _*)) + ujson.write(ujson.Arr(rowsFor(sources, sinks, sanitizers, hookCallbacks, dispatchApply, function, parameterSources, fieldSources, file, callDepth, boundedSinks): _*)) ) } println("---OUSAST-CPG-END---")