mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 20:49:28 +00:00
1f91c0b089c4a7e20fe2510ffb5e453f7bcf8481
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6b85a60ff1 |
perf(push): find the Python-side scale walls all at once, and remove them
The loop the maintainer called out: four scale defects in the PHP transfer replay, found one per two-hour Joern run, all Python bookkeeping between engine calls. benchmarks/push/python_overhead.py ends that: the real replay() on the real PMPro pins, with a JoernBackend whose graphs are stubs and whose queries answer instantly with rows recorded from the real engine. Engine time is zero, so everything measured is ours, in about a minute, under the sampling profiler. Its first pass named two more walls: - Region discovery re-parsed the ruleset for every region: 89,127 TOML parses, 275 s of a 339 s discovery, twice per push. The fact, obligation and taxonomy loaders now keep the parsed (immutable) result keyed by each file's name, size and mtime -- an edit in place or an added file reloads. Discovery 339 s -> 7 s; output digest identical on PMPro. - affected_context decoded every span's and every line anchor's path for every context row -- a changed file has an anchor per unchanged line -- 78% of a stubbed push. Decoded once per call, anchors indexed by line and bisected, original order kept. A randomised control compares it against the row-by-row matching over 200 contexts (>100 attachments). Result: the whole stubbed PMPro push, 27,542 answered requests and 2,688 outcomes, runs in 58.5 s of Python. What is left is the engine. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
e7e2941e37 |
fix(scan): attach change relationships in linear time
The third wall under the 13.3 replay, found by the streamed case log: the evidence and context passes finished at 14:53, then nothing -- no engine call, no log line -- for 82 minutes, until the case was killed 60 s past its 7,200 s deadline. `affected_context` tested membership with `relation not in relationships` on a list: 7,998,000 comparisons for 4,000 relationships (profiled, 83% of the time), and a push over a WordPress plugin attaches tens of thousands. The deadline is checked once per question, so a single late question could run for minutes, which is also why the process overran its cancellation allowance. Membership now goes through a set beside the ordered list, and each distinct context path is resolved once instead of once per row. Measured: 4,000 relationships 7.4 s -> 0.44 s; 400,000 in 16 s. The control counts comparisons rather than timing them, and fails on the old code with 7,998,000. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
3c195c9369 |
fix(scan): itemise change context only in the files a change touched
The PHP transfer replay (pre-push 13.3) spent its whole 7,200 s case in the head side's evidence pass. A probe on one PMPro graph named the cause: not compute -- context adds ~20% engine time -- but OUTPUT. Each evidence request listed every method its scope reached, up to 1,845 rows; 200 requests into the bundled Braintree SDK returned 52,831 rows, 9.4 MB, against 211 rows without context, over 22,125 requests in a 3 GB container. `affected_context` only ever attaches a location in a changed file, and every other row was checked (real file under the root, usable extent) and discarded. The scan now sends the changed files; the engine itemises those and folds the rest into one `context_elsewhere` row carrying exactly what that check reads. With no safe file list the engine itemises everything, as before. Equivalence: unit controls over valid, missing-file, no-extent and outside-root cases, with and without a changed-file row; a real-Joern control; and on PMPro itself, 2,440 relationships and zero gaps decided identically from both forms, 52,831 rows down to 3,241 with the most referenced file as the change. Probe: benchmarks/push/context_cost_probe.py. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
573b764bbb |
fix(ranker): scope a region to the file that defines its handler (M1b 12.4)
A route-registration module registers handlers that other modules define. app/routes/index.js requires seven handler modules and defines none, yet regions there were scoped to handleContributionsUpdate and its siblings. The graph rightly held no such method, so every question in those regions was unresolvable: 138 across six families on that one repository. The entry record still names the handler. The authorization question is which middleware guards which handler, and that is cross-file by nature. It now also records whether the file defines what it names, and a region built from an entry that does not is scoped to the file instead of to a method the file lacks. The handler's own module still gets its function-scoped region, so nothing is lost. My first attempt changed the mapper to stop naming cross-file handlers at all. An existing control caught it by asserting that a multi-line registration names the handler its middleware guards, which is exactly the attribution the authorization family needs. The evidence was against me and the fix belonged one layer up, in region construction. 1,407 tests passed, nine skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
b95a371b7e |
feat(contributor-scan): name the function a question cannot locate (M1b 12.3)
After per-family context, every remaining unresolved question reported context_scope_empty, all in three files. The cause is not a missing producer: app/routes/index.js requires seven handler modules and defines none of them, so a question naming handleContributionsUpdate there names a function that file genuinely does not hold, and the empty scope is the correct answer. Reporting that as "scope empty" hid which of two different things had happened. A file the frontend produced no methods for is one problem; a question naming a function defined in another module is another, and it is the common one. Taint and configuration now separate them, reporting context_function_unresolved with the name in the first case and context_scope_empty only when the file has no methods at all. The driver no longer stacks its generic gap on top of a query that already explained itself, and the new gap is question-owned so it cannot block another question's comparison. Completion is deliberately unchanged. The tempting fix is to widen such a question to its whole file, which would raise the number by answering a question nobody asked; a question whose function the graph does not hold stays unresolved, with the name now recoverable from the question that asked it. Group 12 is complete: admission no longer rejects genuine findings on a mis-extracted operation symbol, both families that could never complete their change context now can, and the remaining unresolved questions say precisely why. 1,406 tests passed, nine skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
b202aabd8b |
feat(pre-push-safety-net): bind the reachability boundary to what it limits (M1b 10.5)
dynamic_external_or_depth_context_unresolved was applied as though it invalidated everything about a question. It does not. Bounded reachability can hide further flows; it cannot invalidate a flow that was traced, and it cannot remove a call from a structurally scoped enumeration. At scan level the boundary is now recorded against its owning question without demoting that question's answer. An unavailable context projection, an empty context scope and an unreadable method extent are unknown ownership and still demote. In comparison it no longer blocks the head's own claim, since the finding rests on a path that was actually traced, and no longer blocks an absence read off the enumerated inventory. It still blocks every claim that rests on a flow not having been traced, including the anchored source-connection branch, which now reports base_reachability_bounded rather than silently passing. The query declares the basis of its enumeration. A file- or nesting-scoped question walks calls structurally, so an unresolved destination cannot remove one from scope. Following the call graph makes the reachable set the scope, so the same unresolved destination can shrink what was enumerated; that inventory cannot establish absence while reachability is bounded, and says so. One prior control asserted a question's own reachability gap blocks its own claim. It is updated with its reason and split, so an own projection gap is still proven to block while an own reachability gap no longer does. 1,362 tests passed, nine skipped. Rollout stays NO-GO. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
8ff1d9a638 |
feat(pre-push-safety-net): scope comparison completeness and ambiguity (M1b 10.2, 10.3)
Two vetoes M1a recorded were global where the evidence is local. Comparison required every selected base question to complete, so one unsupported family anywhere prevented an otherwise complete target comparison. Completeness is now demanded over the required scope: the counterpart question plus the dependencies the change context actually recorded for it. Every other gap stays in aggregate coverage and is still reported, never discharged. An incomplete counterpart, an incomplete recorded dependency, an unknown-ownership boundary and a truncated base scan all still yield unknown. Correspondence ambiguity was recorded file-wide and inherited by every question, so repeated unchanged lines in one file invalidated every question in the repository and blocked the whole transaction comparison. It is now decided per operation by the correspondence classifier: an operation needing an ambiguous anchor stays uncomparable, while a uniquely mapped or newly introduced operation in the same file is unaffected. The mapping is still withheld and still reported. On the NodeGoat case the ambiguity is on the target's own file, so file-level scoping would not have been enough; only per-operation resolution is. 1,347 tests passed, nine skipped. No capability enabled; rollout stays NO-GO. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
ebad83009e | feat(pre-push-safety-net): connect change context to ranked questions | ||
|
|
60d2a1a92c |
feat(model): an endpoint its contract declares open carries no obligation
contributor-scan 2.8. VAmPI's last two false positives were register_user
and login_user, both declared open by its OpenAPI spec: the arbiter was
reporting a missing guard on endpoints whose contract says there is
nothing to miss.
**The fix keys on provenance, not on the value, and that distinction is
the whole task.** The mapper says "public" in two very different
situations. An OpenAPI operation with no `security` block is DECLARED
open, because the spec is a complete contract. A Flask handler with no
`@login_required` is "public" only because nothing was found -- which is
precisely the bug this family exists to report. Suppressing on the value
alone would have silenced the detector on its primary case, so
EntryPointRecord now carries `access_declared` and only a declaration
removes the obligation.
WordPress supplied the second source of declared access on the same day --
wp_ajax_nopriv_ for a logged-out handler, wp_ajax_ for a logged-in one,
permission_callback => __return_true for an open REST route. A rule resting
on one framework's convention would be a special case; two make it a shape.
vampi entailed 6 -> 4
access-control precision 2 of 4 -> 2 of 2
known vulnerabilities 3 of 3 still entailed
access_control offered to 6 of 14 handlers, exactly the six the spec
declares authenticated
**And the guard that decides whether it is a real improvement:** the
access-control pair corpus, which nobody tuned this against, is unmoved --
pair_correct 3 and leak 1 across all nine, pair_correct 2 and leak 0.0%
across the seven from projects unrelated to VAmPI, identical to task 2.6.
The precision was not bought by silencing the family. Pair excerpts carry
no route manifest and no hook, so nothing in them is declared open and the
rule correctly does not fire.
Gates byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
|
||
|
|
61811512a8 |
feat(facts): PHP semantic facts, validated against a real PHP CPG
contributor-scan 5.1, the first step toward the v0.1 WordPress target. Written against a PHP CPG rather than against PHP source, because the two do not look alike. Verified with joern-parse 4.0.623 / php2cpg on a probe: $_GET["name"] -> call `<operator>.indexAccess`, CODE $_GET["name"] mysqli_query($conn, $sql) -> call NAMED mysqli_query echo $_GET["msg"] -> call NAMED echo So sources match on code and sinks match on name, and every entry the probe did not exercise is marked unverified in the file -- `include` and `require` among them, which is PHP's most characteristic path bug and the first thing to check on a real checkout. Five families derive (injection, output_encoding, path, deserialization, untrusted_destination) with no sink that is its own sanitizer -- asserted by a test, because that bug class has now appeared three times in three matchers. `php` is added to the region model's languages, without which the facts would have reached nothing. The arbiter decides 4 of 4 on the probe: the SQL concat entails, its `mysqli_real_escape_string` twin drops to corroborated, `echo` XSS entails and `system` command injection entails. **A whole scan finds 2 of those 3, and the gap is the finding.** PHP files yield FILE-LEVEL regions because no PHP entry points are mapped, and the taint arbiter returns one verdict per region per family -- so a file-level region reports at most one injection finding however many it holds. `system` won on flow length and the SQL injection went unreported. That is the mirror image of task 2.9: that one is many regions collapsing to one site, this is many sites collapsing to one verdict, and it bites hardest exactly where PHP needs it not to, since a WordPress file is large and has no decorator for the mapper to find. Recorded as task 5.4 with WordPress's registration model (add_action, add_filter, admin-ajax, REST routes), which the mapper already handles in principle for other frameworks. A 13-line probe is not a WordPress result. What is established is that the seam works end to end for PHP; everything about scale and coverage is unmeasured, and there are still zero PHP pairs in a corpus of 263 python, 351 c_cpp, 47 javascript and 49 java. Gates byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG |
||
|
|
6dc25d77e4 |
feat(model): let the project say what it ships, and rank that first
contributor-scan 2.10. Every one of libpng's twenty-five entry-point regions was a `main()` -- twenty-three under contrib/, plus example.c and pngtest.c -- and those regions carried the fifty interprocedural taint requests that dominated a 300s query phase. A library has no entry points, so entry-point-driven analysis had nothing to anchor to except its own example programs. The task said to derive this rather than hardcode `contrib/`, and a project already declares it. Makefile.am names installed targets in `lib_LTLIBRARIES` and `bin_PROGRAMS`, each target's `<target>_SOURCES` names its files, and `check_PROGRAMS`/`noinst_PROGRAMS` are automake's own word for "not installed" -- exactly the distinction wanted. Taking every `_SOURCES` would have called libpng's whole test suite shipped code, since contrib/libtests declares its sources the same way the library does. interprocedural regions 25 -> 1 taint query 300.1s -> 125.0s total query 311.9s -> 138.5s regions 121 -> 121, unchanged by design The surviving interprocedural region is contrib/tools/pngfix.c:main, which libpng genuinely installs. Two rules keep it honest. **Silence is not exclusion**: a project with no build file this can read gets None back and nothing changes, so vampi's numbers are untouched. **Not shipped is not unscanned**: those regions are still arbitrated function-locally, because a contributor editing a test must still get an answer -- what they lose is the expensive treatment and first claim on the budget. Entailed findings went 0 -> 3, and all three are false positives at contrib/gregbook/wpng.c:374, `strcpy(outname+len, ".png")`. It is safe: outname is char[256] and line 325 guards with `if ((len = strlen(inname)) > 250)`. They were not introduced so much as uncovered -- a different callDepth meant a different sink set, and the 26 fgets false positives were masking them. The structural cause is recorded as 2.12: c/injection and c/memory carry ZERO sanitizers, so no C flow can ever be discharged and every `argv -> strcpy` entails however carefully it is bounded. The discharge here is not a call but a guard that dominates the sink, which is precisely what dominance.sc already does for access control and is wired to that one family. Also adds Group 7, five tasks for stopping the same false positive twice. Two of today's three false-positive classes were one bug in three matchers -- `resolve` in `resolveUrl`, `user` in `users`, `gets` in `fgets` -- each fixed locally and each next one surviving because nobody carried the fix across. A model may group counter-examples by cause and draft an edit; only a split-respecting measurement may adopt one, because the predecessor's improve lever admitted shapes taught by the holdout pairs they then recovered and turned a -0.059 Youden into +0.059. Gates byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG |
||
|
|
0fcd5feade |
feat(model): a region for the code that is in no function
contributor-scan 2.5. VAmPI's `host='0.0.0.0'` is now reported exactly once, at app.py:17:<module>. Before task 2.2 it was reported eight times in eight files that do not contain it; after 2.2 it was reported nowhere. Two causes, one shape: nothing could ask about module-level code. **The main-guard marker is not a function.** `__main__` is what the mapper emits for `if __name__ == '__main__':`. Sent to the engine as a function name it matched nothing, because a CPG calls a file's top-level code `<module>` -- verified against the VAmPI CPG rather than assumed. The region model translates the marker, which is the one place that turns what the mapper meant into what the scanner asks. **A function-scoped file had no region for its module body.** Settings, route registration and globals live in no function, and every region's query filters by function name. A file-level region beside the handlers would report each handler's findings a second time, which is why the earlier rule dropped it as "a second full-file pass". A module-scope region PARTITIONS the file instead of overlapping it. That only became cheap after batching: before it, a region was a JVM. Two things this needed to be correct rather than merely present: - A module method spans its whole file, so the taint query's line-range containment made `<module>` the parent of every function in it. Under module scope, membership is direct -- otherwise the module region reports every handler's sinks again, which is the duplication the old rule was avoiding. - A module body is not a handler. The marker-derived region inherited the handler families and entailed a missing authorization check on the main guard: a finding with no possible remediation. Caught by the first repository run that had the region, and now covered by a test. bind address 8 wrong files -> 0 -> 1, at app.py:17:<module> regions 22 -> 25 entailed 8 -> 9 query time 37.4s -> 41.3s peak RSS 827MB -> 823MB cost $0.0067 -> $0.0068 Three more regions for four seconds and no more model calls: the budget is a total and was already spent, so regions_unasked rose 17 -> 20 -- those regions got the free pass and are reported as unasked, not as clean. `<module>` is Joern's Python name for a module body; JavaScript uses `:program`, so this is verified for Python only. Gates byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG |
||
|
|
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 |
||
|
|
8663c1a5b3 |
feat(contributor-scan): 1.1 regions from entry points, 1.2 the repository driver
Pair scoring could rely on a labelled function; a contributor cannot supply one -- which code is risky is the question they are asking. Regions now come from entry points, with a file-level fallback that says it is one, and each carries only the families its shape and language admit. Ranking uses what EntryPointRecord already decided (access_level, trust_boundary) rather than a second opinion computed from the same evidence. A family is offered only where its arbiter could answer: access_control needs a handler, so a library file with no entry point is not asked about obligations it has no siblings to be inconsistent with. The driver makes the two costs that were free at pair scale explicit. ONE CPG per repository, reused across regions -- a build is 5-30s on a forty-line excerpt and per-region builds would make this unusable. And a budget that is a TOTAL for the run rather than the enumerator's per-region cap of eight, spent highest-rank-first, with whatever it does not reach counted into regions_unjudged. A tool that silently analyses a tenth of a repository and reports a clean result is worse than one that says what it skipped. Two flaws the tests caught, both mine and both about ownership: the budget counted calls by reading a client's own usage log, so it silently never tripped for a client that keeps none -- the driver now counts its own spend through a wrapper; and the driver trusted its caller to have sorted, when the order the budget is spent in belongs to whoever holds the budget. The third was worse and only surfaced because a test failed for the right reason: the driver passed candidates=() and never enumerated, so a repository scan would have reported only what the arbiters establish unaided and never populated the suspicion band -- which supplied 15 of 26 findings in the pair measurement. It enumerates now. Sibling audit (Req 8.5) done before writing: family routing lives in exactly one production place, pipeline._arbitrate, dispatching on spec type. regions_for returns family names and the driver resolves specs, so that stays true. 750 passing, mypy clean over 96 files, gates byte-identical, zero orphans. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG |