Files
Open_Ultra_SAST/benchmarks/measurements/2026-10-12-banks-fixed-search-mvp/NOTES.md
T
Marius CiepluchandClaude Opus 4.8 8f7fc4ae83 measure(g2): banks fixed-search MVP -- "no wrong targets, only wrong searching" demonstrated
Same CPG, only the search changes. Baseline (shipped path spec) resolves 0
prompt->write_text flows on banks; injecting the two annotations an LLM steer
would supply (parameter_sources=True + write_text/read_text path sinks) makes the
engine resolve prompt -> path-join -> prompt_file.write_text(prompt.raw). First
measured instance of "#2 guided by #1" closing on an unseen target; fix needed
exactly the param-source + file-write-sink annotations.

Files the instrument bug the first MVP hit: single-query cpg.run("taint",
request_params(...)) is dead -- request_params emits 5 keys (guards/fixedOrigin/
originAnchors/quotedSanitizers/fieldParameterSources) absent from taint.sc's exec
signature; both single-query sub-routes reject them (silent None). Batch path
(run_batch->requestsFile JSON) unaffected; push/scan route uses batch.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 14:02:41 +02:00

4.6 KiB

Banks fixed-search MVP — "no wrong targets, only wrong searching"

Date: 2026-10-12 · Host: clean (local docker, replay-task image) · Repo HEAD: 7ab5c2a Target: masci/banks @ 5e7093c9b3c2bbe2e0bc281f5a3602bbdabf00b9 (the "library" that gave the static engine ZERO path findings in the engine-on pilot).

Thesis under test

The governing principle (maintainer, 2026-10-12): we have a search-with-proof architecture, so there are no wrong targets, only wrong searching. Banks produced zero path-family findings in the engine-on pilot not because it is a wrong target but because the SEARCH was not annotated for it:

  1. the source — prompt.name, a function parameter — is not recognised (entry:false, so _parameters_are_input returns False and parameters are not treated as untrusted); and
  2. the sink — Path.write_text / Path.read_text at a joined path — is not in the shipped path-family sink list.

Inject exactly those two annotations (what a per-target LLM steer would supply) and check whether the engine now traces prompt → path-join → write_text where the shipped search produced nothing.

Instrument (verified before believing the number)

Per AGENTS.md "verify the instrument": the first MVP attempt (single-query cpg.run("taint", …) path) returned 0/0, but that was an instrument failure, not a result — every query died on cpg query taint failed (see the filed bug taint-sc-single-query-signature-skew.md). This record uses the batch path (cpg.run_batch("taint", {requestsFile JSON})), the same invocation the real engine-on scans use. Confirmed it ran: CPG built, run_batch available, and the fixed search returned concrete sink strings (prompt_file.write_text(prompt.raw)) — the opposite of the v1 failure's empty rows. Harness: banks_mvp2.py. Raw output: results.out.

Setup

Same target, same CPG; only the search changes.

  • Functions probed: from_prompt_path, set, _load, _scan in src/banks/registries/directory.py.
  • BASELINE = shipped path taint spec, parameter_sources=False.
  • FIXED = shipped spec + write_text/read_text added to path sinks, parameter_sources=True.

Result — flow-row counts

Function Baseline (shipped) Fixed search (annotation ON)
from_prompt_path 1 row (sink=None) 4 rows — incl. sink=prompt_file.write_text(prompt.raw) source=prompt
set 1 row (sink=None) 1 row (sink=None)
_load 1 row (sink=None) 2 rows (sink=None)
_scan 2 rows (sink=None) 4 rows — incl. sink=self._index_path.write_text(...)
Total 5 11

What the counts actually mean (honest read)

The raw totals (5 vs 11) overstate the effect. All 5 baseline rows have sink=None, source=None — they are census/evidence rows, not resolved source→sink path flows. That is consistent with the engine-on pilot producing zero path-family findings on banks. The result that carries the thesis is the resolved-flow delta:

  • Baseline: 0 resolved prompt → write_text flows.
  • Fixed search: the engine resolves prompt → path-join → prompt_file.write_text(prompt.raw) in from_prompt_path (source=prompt), plus a second write_text sink in _scan. These appear only with the two injected annotations.

Verdict

On the exact target that gave the static engine zero, the engine traces the real flow once the search is corrected. It needed exactly two annotations: the parameter-as-source and the missing file-write sink. This demonstrates "no wrong targets, only wrong searching" on a concrete case, and names the precise inputs the per-target LLM steer (DSPy-optimised, framework-seeded) must learn to supply.

Caveats (not papered over)

  1. One from_prompt_path row shows source=path rather than prompt — the parameter-source annotation fires broadly, not yet pinned to the untrusted parameter. A precision knob for the steering loop, not a blocker.
  2. This shows the engine traces the flow; it is not the runtime differential proof. Proving attacker-reachability remains the async BLOCK lane (search-with-proof verifier).
  3. The single-query taint path is broken (filed separately); this record uses the batch path deliberately.