OFFAT-AI: code-graph white-box and AI roadmap#
Status: proposal + first implementation (whitebox/offat_wb/cpg/), October 2026.
1. Where AI sits in OFFAT today#
| Stage | What the model does | Limitation |
|---|---|---|
whitebox hunt |
nothing (regex + semgrep wrapper) | per-line matches, no data flow; noisy |
whitebox trace |
nothing (graft callers by name, "looks like a handler" regex) |
name collisions, no proof the data reaches the sink |
triage (offat_triage) |
batched screen + tiered verify of each finding | sees an 800-char snippet; cannot look anything up |
graybox analyze |
same triager, reachable-only | reachability = graft names or "same file as a route" |
platform test-gen |
proposes extra payloads per endpoint (700 tokens) | no idea which sink sits behind the param |
The model is asked to judge exploitability without the one artifact that decides it: the path from attacker input to the sink and what happens along it.
2. What this change adds#
A code-graph taint engine replaces semgrep as the white-box core.
cpg/graph.py: a networkxMultiDiGraphcode property graph. Nodes are functions, params, versioned locals,selffields, call sites, args, returns and sources. Edges areflow(with per-classsanitizes),call/retlabelled by call site, andcalls. Exportable as JSON or GraphML (Gephi, yEd, Neo4j import).cpg/py_frontend.py: stdlibast. Covers Flask, FastAPI, Django/DRF, Tornado, aiohttp and Lambda,src/layouts, nested functions, and deferred calls (queue.enqueue(fn, x),fn.delay(x),Thread(target=fn), executors,BackgroundTasks).cpg/js_frontend.py: tree-sitter JS/TS. Covers Express routers including handler factories and wrappers,require/import/export, TS destructuredreq, and callbacks.cpg/taint.py: an interprocedural BFS with a call-site stack, so calls and returns stay matched (f(tainted)does not leak intof("const")), plus per-class sanitizers and reaching definitions (x = int(x)kills taint). BOLA candidates are suppressed when the lookup is scoped to the caller or an ownership check follows. Mass assignment (API3) fires only on whole-body sources.- Pattern hits are fused, not discarded: a regex hit on a proven line is dropped as a duplicate. A regex injection hit with no source is kept but demoted, and gains graph reachability.
- SARIF now carries
codeFlows, so GitHub code scanning renders the path.
Three AI features built on the graph:
- Graph-grounded triage: the batch prompt receives the proven hop-by-hop path, routes and authz context instead of a snippet.
- Graph-tool agent (
--agent N,cpg/agent.py): a tool-use loop with read-only, repo-confined tools (get_function,callers,callees,read_lines,flows_into). It tries to refute first, then submits a verdict and a PoC request. Repo content is framed as untrusted data, since comments are a prompt-injection channel. It usesclaude-opus-5-5with server-side refusal fallback. - Spec inference (
--infer-specs,cpg/infer.py): the taint frontier is the set of unknown callees that receive attacker data. The model labels each one sink, sanitizer or propagator once per API, and the result is written toinferred-specs.jsonand fed back via--specs. This targets the main source of misses (in-house wrappers) and of noise (lookups such asauthenticatedUsers.get(token)that return server-owned data).
Also fixed: the default screen model claude-haiku-5 is not a valid model ID,
so batched screening always failed over to per-finding calls. It is now
claude-haiku-5-5.
3. Benchmarks (no AI, graph engine only)#
| Target | Size | Time | Entry points | Flows | Notes |
|---|---|---|---|---|---|
fixture (whitebox/tests/fixtures/taint_app) |
Flask+FastAPI+Express | <1s | 29 | 16 | 16/16 planted vulns found, 0/12 safe twins flagged |
| OWASP Juice Shop (TS) | 4,260 fns | 6.3s | 91 | 56 (17 in intentionally vulnerable codefixes/) |
login + search SQLi, SSRF, open redirect, 3× $where/Mongo NoSQLi, eval + pug SSTI, basket IDOR, YAML deser; every finding mapped to its route |
| we45 Vulnerable-Flask-App | 969 fns | 1.1s | 11 | 4 | 4/5 taint-style vulns, 0 FP; miss: SSTI via str(exception) |
| dvpwa (aiohttp) | 978 fns | 1.3s | 8 | 1 | the SQLi |
| pygoat (Django) | 293 fns | 0.5s | 131 | 25 | SQLi, cmdi, eval, pickle/yaml, SSRF, path traversal, XSS |
Known gaps: taint through exceptions, key-sensitive containers (dict fields),
second-order (DB write -> read), and vm sandbox contexts.
4. Roadmap#
P0: close the loop between white-box and DAST#
- Graph finding -> targeted test case. Each graph finding already has a
route, a param and a sink class (plus a PoC from the agent). Emit these into
the Bundle as test cases so the Rust engine confirms them against an
authorized deployment, and mark them
confirmed-dynamic. Today test-gen guesses vectors from parameter names. - Sink-aware payload generation. Give
platform/testgen._augment_with_aithe sink code at the end of the path, not just the endpoint (SQL dialect, shell vs argv, template engine,$wherevs operator smuggling). - Agent verification for DAST findings. For a dynamic hit, start the agent from the route's handler node in the graph so it can explain why the response looked vulnerable, or refute it.
P1: precision and coverage where AI is the right tool#
- Authorization model extraction. Collect per-route decorators/middleware
from the graph (
login_required,isAuthorized(),Depends(current_user)) into an auth matrix. Let the model flag outliers ("every/api/Cards/*route hasisAuthorizedexcept this one") and feed BOLA/BFLA test-gen with--identities. - Business-logic hypotheses. Hand the model graph slices of state-changing handlers (checkout, coupon, wallet, transfer) and ask for multi-step abuse cases (negative quantities, replayed coupons, race windows), then emit PRG chains.
- Second-order sources. Treat data read back from the DB, and, for security tools and API clients, responses from the scanned server, as sources. That would have caught the report XSS in §5.
- Key-sensitive containers. Track
d["field"]separately and use the agent to refute container over-taint (e.g.post_test_processor.py:189). - Shared spec registry. Cache AI-inferred specs per package and version and share them across runs and projects, with human review.
- PR / diff mode. Slice the graph to changed functions plus everything they reach. Comment with SARIF code flows and the agent's verdict.
P2: platform and safety of the AI layer#
- Expose the graph to agents over MCP. The repo already ships Claude Code
skills and a plugin. An
offat-graphMCP server (callers,slice,paths_to(sink)) would let interactive agents use the same graph. - Sanitizer validation by execution. The model writes a harness, a sandbox runs the project's custom validator against a payload corpus, and the interpreter, not the model, decides whether it holds.
- Cost controls. Prompt-cache the system prompt and tools across agent turns, use the Message Batches API for nightly runs, and set a per-run token budget.
- Eval harness. Juice Shop, pygoat, VFA, dvpwa and OWASP Benchmark with expected findings: track graph precision/recall and AI verdict accuracy per release, including prompt-injection canaries planted in code comments.
5. Self-scan of OFFAT#
Running the engine on this repository proves flows from POST /api/v1/scan
through the RQ job queue into the classic tester (the job function is reached via
task_queue.enqueue(scan_api, ...), which the deferred-call modelling resolves).
Security-relevant results have been reported privately to the maintainers.
Functional bugs surfaced on the way:
src/offat/api/jobs.py:16callsgenerate_and_run_tests()without the requiredhttp_methodsargument (sinceacd733b), so every REST-API scan job errors out.- A non-URL
openapistring is passed asspec=andBaseParsercalls.geton astr, so inline specs via the API cannot work. report/generator.py:47appends each result once per key (duplicated rows).post_test_processor.py:184:HEADER_REGEX_FILTERmatches the body, not headers.