perf(taint): ask the caller check only of a parameter that starts a flow into the field

Asked of every parameter up front, 6ebbc89's caller check ran a repository-
wide flow query per parameter of every method in a file and cut PMPro's
completed injection questions from 272 to 67 in the same budget -- so its
48 -> 26 findings could not be read as precision. The flow into the field
is asked once as before; only a parameter at the head of an unsanitized
flow is checked against its callers. finding_dump now records completed
regions so runs compare only where both answered.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-23 22:49:44 +02:00
co-authored by Claude Opus 5.5
parent 6ebbc89d64
commit e0c260ca5f
2 changed files with 19 additions and 5 deletions
+7 -1
View File
@@ -81,11 +81,17 @@ def main() -> int:
"seconds": round(time.monotonic() - started, 1), "seconds": round(time.monotonic() - started, 1),
"questions": len(outcomes), "questions": len(outcomes),
"completed": sum(1 for o in outcomes if o.status == "completed"), "completed": sum(1 for o in outcomes if o.status == "completed"),
# Which questions answered, so two runs can be compared only where BOTH answered: a finding that
# vanishes because its question went unasked is lost coverage, not gained precision.
"completed_regions": sorted({f"{o.identity.path}:{o.identity.function or ''}" for o in outcomes if o.status == "completed"}),
"degradations": sorted({str(d.get("reason")) for d in result.degradations}), "degradations": sorted({str(d.get("reason")) for d in result.degradations}),
"findings": findings, "findings": findings,
} }
args.out.write_text(json.dumps(record, indent=2) + "\n") args.out.write_text(json.dumps(record, indent=2) + "\n")
print(json.dumps({k: v for k, v in record.items() if k != "findings"} | {"findings": len(findings)}), flush=True) print(
json.dumps({k: v for k, v in record.items() if k not in ("findings", "completed_regions")} | {"findings": len(findings)}),
flush=True,
)
if not outcomes: if not outcomes:
raise SystemExit("no question was asked: the dump measured nothing") raise SystemExit("no question was asked: the dump measured nothing")
return 0 return 0
+12 -4
View File
@@ -306,10 +306,12 @@
val methods = methodsIn(fileName) val methods = methodsIn(fileName)
val framework: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] = val framework: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] =
methods.flatMap(_.ast.isCall.filter(c => sourcePatterns.exists(p => c.code.contains(p))).l) methods.flatMap(_.ast.isCall.filter(c => sourcePatterns.exists(p => c.code.contains(p))).l)
// Every parameter is a CANDIDATE start; whether its callers feed it is asked only of a parameter that
// actually begins an unsanitized flow into the field being read (`fieldIsTainted`). Asked of every
// parameter up front, the caller check ran a repository-wide flow query per parameter of every
// method in the file and cut PMPro's completed questions from 272 to 67 in the same budget.
val params: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] = val params: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] =
if (parameterSources == "true") if (parameterSources == "true") methods.flatMap(_.parameter.l.filter(_.name != "this")) else Nil
methods.flatMap(m => m.parameter.l.filter(p => p.name != "this" && fedByCaller(m, p)))
else Nil
framework ++ params framework ++ params
} }
) )
@@ -331,7 +333,13 @@
// queries that way: the fixed side of its pair went from 1 finding to 13 without this, // queries that way: the fixed side of its pair went from 1 finding to 13 without this,
// which is the pair no longer separating at all. // which is the pair no longer separating at all.
val flows = args(1).start.reachableByFlows(seeds.iterator).l val flows = args(1).start.reachableByFlows(seeds.iterator).l
flows.exists(f => !f.elements.map(_.code).l.exists(code => mentionsToken(code, sanitizerNames))) flows.exists { f =>
!f.elements.map(_.code).l.exists(code => mentionsToken(code, sanitizerNames)) && (f.elements.headOption match {
case Some(param: io.shiftleft.codepropertygraph.generated.nodes.MethodParameterIn) =>
fedByCaller(param.method, param)
case _ => true
})
}
case _ => false case _ => false
}) })
} }