mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
c1a689f4b3f06598d94f6e43e0e6efd8cb32167d
161
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c1a689f4b3 |
measure(contributor-scan): 7.8 portioning matches the cap on findings, not on completion
Final end-to-end on the 500-region PHP subject: findings identical at 125 either way, completion slightly lower under portioning (131 of 406 vs the hard-cap-plus-reserve 153), wall time higher (2,982 vs 2,338 s). Told straight: portioning does not beat the cap on throughput here; it matches it on what it finds, and what it buys is kill-safety -- a killed batch keeps its streamed answers instead of losing them. Reaching the same 125 findings is the evidence that streaming loses nothing the cap kept. Completion did not rise because the requeue re-asks a killed portion remainder, and on this subject those are the expensive output-encoding questions that keep hitting the ceiling in smaller pieces, spending budget without completing. Telling a transient kill from a too-expensive family is the next lever, not more portioning. Neither approach is release-ready anyway: 32-38% completion against a 95% gate on development data that qualifies nothing. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
2e96ccb8ea |
test(push): freeze_source mints its own path so it can never clobber a live mount
The helper took a destination from the caller and rm -rf it. Re-running it into the same path while a container had that path mounted deleted taint.sc out from under a live scan, and every request after printed no such cpg query -- the live-mount hazard the helper exists to prevent, in a new form. It now mints base/frozen-<commit>-<pid>-<n> itself and prints it, so reuse is impossible by construction and the caller captures the printed path. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
a084dce780 |
test(push): a verified source freeze for long measurement runs
Every long run mounts src into a container, and mounting the working tree is a live mount: edits made during a multi-hour run are picked up mid-run, and a measurement whose code changed underneath it is not a measurement of anything. A five-hour PHP transfer replay was lost that way today. The export has to be checked too. git archive piped into tar once produced a tree missing the whole ontology -- every tracked file after groovy alphabetically -- and reported success, because tar status in a pipeline was never read. Every scan on that tree died at its first fact load and looked like a slow run for fourteen minutes. So: archive to a file, extract, count against git, and refuse an export without the ontology. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
d4ce173592 |
measure(m3): the declared PHP target is detected, and the replay budget is sized
classes/class.memberorder.php:936:getMemberOrderByCode, injection. A value concatenated into SQL with no prepare, which is the declared M3 target for PHP, found for the first time. Nothing about detection changed. The population became affordable: tier-zero pruning scoped to its partition, batches budgeted by the sink work they imply, a per-query timeout no longer cancelling the scan, and a reserve kept for arbitration. Before those this population completed zero questions at every budget tried. At 500 regions with a 3,600 s deadline: 271 of 406 questions completed, 213 findings, 2,950 s, and eight findings in the target file including the target itself. The earlier bounded attempt failed for a reason no budget increase could fix -- 25 regions of 4,461 is 0.6 percent of the population and the target was never asked about -- so this sizes the replay before spending hours on it. One scan at this budget is 2,950 s, a push transaction is two scans plus discovery and preparation, so the three declared comparisons are roughly five and a half hours. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
f09d788568 |
test(census): record which regions a budget actually reached
A census that reports what it found cannot say whether a defect it missed was examined and cleared or never looked at, and that is the difference between a negative and a silence. It is also how a bounded run budget gets sized without guessing: task 13.3 needs to know at what population the declared PHP target enters scope, and asking the record is cheaper than binary-searching with forty-minute scans. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
8874145874 |
measure(contributor-scan): confirm 7.7 costs the small population nothing
PMPro at 25 regions, which completed before this work and still does: 27 questions completed and 19 findings before and after, injection 1 and output encoding 18 in both. The batching changes what a large population can afford and nothing about what a small one answers. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
7ad3decf9a |
measure(contributor-scan): 7.7 makes a large PHP population affordable, 0 findings to 125
On the 500-region population where every earlier run produced nothing: 153 of 406 questions completed and 125 findings, inside the deadline. 380 requests carrying 5,807 sink visits split into 11 batches, each answering in 80 to 197 s, none near the 300 s ceiling. Every family that can speak on this subject now does, injection included, which is the one holding a true positive here. Four defects, each hidden by the one before it, and three of them one rule. Batches sized by request count, when cost is per matched sink. Packing heaviest-first, which starved injection of all 114 of its questions. Any per-query timeout cancelling the whole scan, abandoning batches with 33 minutes left. No budget kept for arbitration, which removing that cancellation exposed: rows are not findings. The reserve had to be checked per BATCH. Checked between query kinds it changed nothing at all, because one kind is a single loop iteration that runs every one of its batches inside it. What it does not claim: 253 of 406 questions still went unanswered and one batch still records a timeout; the raw finding count is dominated by output encoding, the family already measured to be imprecise on PHP; and the reserve share of a fifth is a first value, not a tuned one. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
9910dd18b9 |
measure(php): the query cost is the matched sink count, and the blocker is identified
Open since September and re-measured nine times. The cost of a taint batch is the number of CALL NODES a family sink names match in the repository. Not the request count, not the declared sink count, not any request parameter. reachableByFlows is asked per matched sink, and PHP soutput-encoding sinks are echo and print, which a WordPress plugin writes thousands of times. Three controlled sweeps on one graph, built once. Batch size exonerated: 400 requests answer in 170 s, and cost per request FALLS with size because roughly 70 s is fixed JVM start and graph load. Request parameters exonerated: at 200 requests the full production shape, call depth 3 with parameter sources, costs 221 s against a 150 s baseline. The family is the variable: at the same 100 requests, SQL injection 147 s with six declared sinks, path 194 s with sixteen, output encoding 704 s with seven. And the driver named its own expensive step once it was asked to: 2,435 evidence requests in 45.5 s, then 315 arbitration requests unanswered at 1,200 s, then a split abandoned for want of time. It survived since September because every reproduction outside the driver carried the SQL sinks -- the family the declared target belongs to, and the one family that is cheap on this subject. Nine hypotheses were eliminated by re-measuring the cheap case. Splitting never rescued it because a failed batch is split by REQUEST COUNT and each part carries the same everywhere-sinks. contributor-scan 7.7 carries the fix: budget a batch by the sink nodes it implies, using the count the evidence pass already computes, and report a family whose sinks are everywhere as expensive rather than as failed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
617fab157c |
measure(php): batch size is not the cost, and the curve is sublinear
Four census runs on this subject spent whatever query ceiling they were given: 363 s of 300, 1,255 s
of 1,200, 1,874 s of 1,800, at question counts of 2,526 and 406. Cost that tracks the budget rather
than the work is not per-question cost, and tier-zero pruning cutting the count six-fold changed
nothing. A census runs one batch, so no census could answer whether the batch itself scales.
One graph, built once, five batch sizes, every one answered:
25 requests 71.4 s 2.86 s each
50 requests 76.0 s 1.52 s each
100 requests 155.4 s 1.55 s each
200 requests 166.7 s 0.83 s each
400 requests 169.7 s 0.42 s each
Sublinear, and roughly 70 s of it is fixed JVM start and graph load. Four hundred dataflow requests
answer in 170 seconds on the graph production cannot finish 406 on.
So the difference is in the request PARAMETERS. The probe sent callDepth=0 and no parameter sources;
production sends callDepth=3 with parameter sources on every entry point, plus a hook table, a
dispatch list and context evidence. Task 13.2 recorded in September that every reproduction outside
the push path answered in under a minute and the difference was not known. Size is now removed as a
candidate and a four-variant probe is running to isolate the rest.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
|
||
|
|
3b6e642433 |
fix(scan): prune a tier-zero question unless ITS partition is the incomplete one
The mechanism already existed. A question whose evidence vector says no sink of its family is in reach cannot produce a flow, and tier-zero pruning defers it. That was switched off for the whole scan whenever any of twelve degradation reasons appeared, and a multi-language repository always carries two of them, so the measured plugin arbitrated every question it could ask. Third instance this week of one blast radius, and the invariant written into the design on Monday named it. A prune blocker that names a partition now withholds pruning from that partition questions and from nobody else. cross_partition_semantics_unresolved leaves the blocker set entirely, for the reason it left the integrity gaps: a PHP family sink is PHP, so a second language cannot be why its vector saw none. Measured on Paid Memberships Pro. At 25 regions arbitration falls from 127 questions to 34 with 93 recorded deferred as tier_zero, and 19 findings before and after, identical family by family. At 500 it falls from 2,526 to 406 with 2,120 deferred. It is not sufficient, and that is the useful half. The 500-region run still fails in the same way at the same cost, because 406 questions at roughly one and a half seconds each still exceed the 300-second per-batch ceiling. This lever moved the volume; the ceiling is a separate limit. The census now records what a scan skipped and why, per family, so a cheaper scan and a narrower one stop looking identical: a family whose every question was pruned reports no sink of this family in reach rather than region offered, no question asked. 1,420 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
b0500ecfa0 |
measure(contributor-scan): first ontology coverage run, and it found a family we do not have
NodeGoat models 18 of 175 distinct call names, covering 12 percent of 1,185 calls. That share is not a score: most of what a program calls is not security-relevant, and a high number would mean the tables were too loose. It is a baseline to move deliberately. The top product candidate is res.render, 23 calls with five in a method that reads untrusted input. Task 14.3 reasoned its way to that same gap by hand and declined to model it as a flow sink; the instrument found it in one run, which is the validation that matters. It also surfaced Math.random in the user data-access module, and weak randomness is a weakness class this ontology has no family for at all -- so no family census could have found it, because the family list is itself the blind spot. That is the kind of gap only a coverage instrument finds. It had to be taught what is not an API. The first run ranked _tmp_3.push and __ecma.Array.factory first, which is how the frontend lowers a destructuring and an array literal, and a Cypress suite and a Gruntfile held four of the next five. Both are separated now, because noise at the top of a worklist is how a worklist stops being read. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
0beae46ba9 |
feat(contributor-scan): ask a repository what it calls that the ontology does not model
Five of seven families established nothing in the family census, and every one failed for an ontology reason with the flow engine ready and nothing to aim at. Each was found by investigating one family at a time over days. vocabulary.sc asks it once: census the calls, subtract everything any fact table names, rank the remainder by how often it is called and whether it sits in a method that reads untrusted input. It needs no dataflow, because the structural pass already walks every call. The harness reads its token set from the SPECS rather than from the TOML, so it measures what the analyser actually consults: a fact the loader drops is not part of the ontology however neatly it is written. Ontology sizes today are 152 tokens for JavaScript, 167 for Python, 84 for PHP. Both carry the constraint the maintainer set: the output is a candidate list, never an ontology. A name becomes a fact only when someone resolves it to a library API, states the weakness class and records provenance, because adding a name because a subject called it is how a general-purpose tool becomes a fixture of its own benchmark. Also records what the bounded PHP transfer attempt found. At 25 regions the first case now runs to completion and writes a full artifact in 1,706 s, where at 100 it had exhausted a 2,400 s deadline with nothing. But 25 of 4,461 regions is 0.6 percent of the population and the declared target was not among them, so the question was never asked. Task 13.3 therefore depends on 7.3, skipping arbitration where the evidence says there is nothing to find: raising the budget is already measured to fail, and making most of the budget unnecessary has not been tried. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
d105d2f84a |
fix(scan): carry the partition in both census passes, and PHP completes 120 of 127
The scoping fix looked applied and was not. The same census gap is raised in TWO passes, arbitration and evidence, and only the first carried the partition it came from, so an unscoped copy kept demoting every question in the repository. One key in one dict. What found it was the record, not the code: the census now prints each outcome reason rather than only its status, and three separate causes had been looking identical as a bare count of unresolved. Measured on Paid Memberships Pro at 25 regions: questions completed goes from 0 to 120 of 127, with its 19 findings unchanged. The seven still unresolved are the JavaScript partition own questions, which is correct, because the four missing files are its files and its answers are the ones that could be wrong. Still open, and recorded rather than implied: the push policy _blocking_degradations exempts declared exclusions but does not yet respect a partition, and a post-hoc diagnostic copy carries no language, so a JavaScript gap would still block admission of a PHP finding. 1,419 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
c93669c34f |
fix(scan): a language boundary is a declared limit, not a graph-integrity failure
Membership of GRAPH_INTEGRITY_GAPS should turn on whether a gap can make a reported answer WRONG, not on whether it cost coverage. An unparsed file may hold the sanitizer that would have cleared a flow this scan reports, so it belongs there. A flow this tool cannot follow from one language into another can hide a finding and never invent one, because a PHP sink is sanitised in PHP. cross_partition_semantics_unresolved therefore moves to DECLARED_EXCLUSION_GAPS. It touches exactly two places, both of which already honour the distinction: the outcome demotion in scan.py, and _blocking_degradations in the push policy, whose docstring already says a declared exclusion is not one of them. The reason stays published in the scope unresolved boundaries either way, so the limit is still reported; the class decides only whether every answer in the repository is demoted. This corrects the task note written earlier today, which said not to reclassify on the grounds that it would claim cross-language flows are out of scope rather than unresolved. That was a misreading of the class. PMPro still completes none of its questions, and the measurement now names why: the JavaScript partition expected 36 files and holds 32, so partition_file_census_incomplete fires and is still an integrity gap. The four absent files are js/blocks/blocks.build.js, js/jsapi.js, js/select2.min.js and webpack.config.js -- a build output, a source file, a minified third-party library and a build config. A bundled select2 costs 4,425 PHP regions their admissibility. Same over-broad blast radius, third instance of one principle in this thread, and the fix direction is recorded. The census harness now records each degradation whole rather than only its reason, because a partition census that reports files missing carries WHICH files and without them the gap is a label rather than a lead. 1,417 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
73e9c1fed6 |
measure(m3): two languages in a repository defeat completion, and so defeat admission
No PHP question is ever marked completed, and PHP was never the variable. partitions.py raises cross_partition_semantics_unresolved whenever more than one language partition exists, that reason is a member of GRAPH_INTEGRITY_GAPS, and a graph-integrity gap demotes every completed outcome in the scan. Admission requires a completed outcome. The evidence is one column wide. NodeGoat is JavaScript alone and completes 299 of 299. VAmPI is Python alone and completes 35 of 35. PMPro is PHP with 36 regions of bundled JavaScript and completes 0 of 127 and 0 of 520 while establishing 19 and 43 findings. NodeGoat own degradation, vendor_semantics_unresolved, is a DECLARED exclusion rather than an integrity gap, which is exactly why it does not demote. This lands on the declared v0.1 target. A WordPress plugin is PHP plus its admin JavaScript, so every one of them is multi-language, so on the target this tool was built for it can detect defects and never admit one. Neither subject that currently passes could reveal it, which is why a census across a third subject was worth running. Recording the boundary is right: a flow from PHP into JavaScript is semantics the partitioned graph cannot follow. Demoting every question is not, including one wholly inside a single PHP file. Task 12.3 already fixed this bug class one level down by making a gap question-owned so it could not block another question comparison; the same principle applies to a partition boundary. Reclassifying the reason as a declared exclusion is explicitly the wrong fix, because that would claim cross-language flows are out of scope when they are unresolved. Task 13.4 rewritten with the diagnosis, and the milestone amendment now ranks it with M2 rather than behind it: a narrow JavaScript-only release can proceed without it, the WordPress target cannot proceed at all. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
6bd6e3531e |
test(m3): let the transfer replay take a region budget
The replay hardcoded 500 regions, which on a 673-file PHP plugin asks 2,526 questions and fits no ceiling this project sets. The budget is now an argument, defaulting to 500 so every earlier measurement keeps its shape. It bounds the RUN, never the repository: the artifact census still reports what was left unexamined, and a bounded population establishes transfer rather than coverage. Task 13.3. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
1f51c82b51 |
fix(census): stop hiding findings behind a question-outcome count, and correct the PHP record
PHP is not silent. Paid Memberships Pro establishes findings, including a true positive, and this project's own instrument is why it looked otherwise. The census harness ordered its verdict on completion before findings, so a family with eighteen findings and no completed question reported "asked but no question completed". Findings are now checked first: a question-outcome count is bookkeeping and cannot hide the thing being censused. NodeGoat and VAmPI were never affected, because they complete every question and their verdicts reached the findings check. The scan summary now also carries the total findings and the outcome status counts, so the two numbers can be seen disagreeing instead of one silently winning. The second mistake was mine rather than the harness's: only the 500-region runs had been looked at, which are the ones that time out. What PHP actually detects on PMPro. At 25 regions, 19 findings in seven minutes with no timeout; at 100 regions, 43 findings in twelve minutes with no timeout. Among them adminpages/ discountcodes.php:749, where sanitize_text_field($_REQUEST["s"]) reaches $wpdb->get_results($sqlQuery, OBJECT) through `$sqlQuery .= "WHERE code LIKE '%$s%' "` with no prepare. That is the class of CVE-2023-23488, which php.toml's own comment names as the reason those WordPress sanitizers are kept out of the sanitizer list. The prediction and the measurement agree. The precision cost beside it is measured too: output encoding reports eighteen findings at 25 regions and the three sampled are all properly escaped, two with esc_attr and one coerced with intval. Those escapers are absent from a sanitizer list that is flat across families, because none of them stops SQL injection. Keeping the injection finding costs this family its precision. The scale limit is roughly one to two seconds per question. 25 and 100 regions complete cleanly; 500 regions asks 2,526 questions, needs more than the 1,800-second ceiling and is killed. Four things are measured NOT to be the problem: the malformed-CPG bug (fixed in 4.0.625, verified on the real trigger file), the dataflow overlay (42 s on the full 637-file graph), the engine version (4.0.625 and 4.0.631 indistinguishable), and the bundled Stripe, Braintree and Twocheckout SDKs under includes/lib that the vendored list does not cover -- that was my hypothesis for the cost and the overlay measures 42 s with them and 37 s without, so it is refuted. What remains is bookkeeping, not detection: questions_completed is zero in every PHP run including the ones that find things, with cross_partition_semantics_unresolved and partition_file_census_incomplete present throughout. The admission policy reads those outcomes. 1,417 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
99abe12062 |
measure(php): quantify what remains once the malformed-CPG bug is excluded
The graph bug is fixed and was never what these runs were hitting. joernio/joern#6281, this project's own report, is a PHP `global` inside a closure giving a node two AST parents, which made the dataflow overlay throw and cost the whole graph. Filed against 4.0.623, fixed by #6270 in v4.0.625, which the image has pinned since before any measurement recorded under task 13.2. Verified today on the real trigger rather than a transcription of the repro, which does not reproduce: PMPro's includes/fields.php builds and applies the default overlays cleanly on 4.0.625 and on 4.0.631, files=2 methods=158, no ReachingDefPass assertion. What remains is runtime. A taint batch of 2,526 questions over the 673-file checkout does not complete in thirty minutes. Raising the query ceiling six-fold, from 300 to 1,800 seconds, changed nothing but how long the failure took: query time went from 363 s to 1,874 s, and zero questions completed in both. The eight-way split retry did not rescue it either, finishing its parts in about three minutes against a deadline with 3,300 seconds still on it, which is worth its own look. The engine pin stays at 4.0.625. An image on 4.0.631, today's release, was built and measured and is indistinguishable on this subject: 627.7 s against 619.1 s, 363.4 s against 367.3 s of query time, zero completed in both. There is no demonstrated benefit to weigh against re-verifying every JavaScript and Python measurement on a new engine. Should that change, the bump is three values rather than a port: the upstream AstGenRunner.scala is byte-identical in 4.0.631 and its patch anchor still appears exactly once, so only the jar path, the jar checksum and the source URL tag move. Both are recorded. Also closes task 14.5. Neither prototype pollution nor deserialization has a subject that can answer, each for its own reason. Deserialization's mechanism exists on exactly one available subject and that subject cannot be queried at all. Prototype pollution has no available subject containing the mechanism, so its ability to answer is established on a control instead. 1,417 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
3bd844055c |
fix(config): report every permissive setting in a file, not the first one
NodeGoat's server.js configures a permissive session at line 78 and disables template
auto-escaping at line 135, both inside the same function. The arbiter returned one verdict per
question, so the escaping defect that task 14.3 added was detected on every run and reported on
none: which of two real defects a contributor saw depended on where in the file it sat.
Configuration now returns one verdict per setting whose value the model could read. The pipeline
already emitted one finding per entailed answer, so this arbiter was the only place still folding
them to one.
Three boundaries are drawn rather than assumed.
- A setting whose value is COMPUTED stays one verdict per region. That band is a question for the
judge rather than a finding, and returning every undecided setting would multiply model calls
for claims the graph did not establish.
- A call carrying two permissive literals is one finding. cors({origin: true, credentials: true})
is one misconfiguration, not two.
- Config verdicts now carry a path:line:function location. Without one they shared the region's
identity and the deduplicator kept a single finding, so the plural answer collapsed straight
back to one. That is the same gap dominance carried until its findings stopped arriving with no
line at all.
Dominance is left answering once per region, and that is settled rather than pending: its claim is
that a region contradicts itself, so the region is the unit and a second verdict would restate the
same inconsistency. The pipeline now says so where it used to call this a known gap.
Measured: NodeGoat's config_secrets goes from one finding to two, server.js:78 for the session and
server.js:135 for the disabled escaping, with the applied marked.setOptions fix between them
staying silent. VAmPI is unchanged and no other family moved.
1,417 tests pass, seventeen skipped; thirteen Joern-gated query tests pass in the image.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
|
||
|
|
e6bdac46e2 |
fix(access-control): decide the sibling scope, and see identity a destructuring spelled away
Two of NodeGoat's three corroborated claims were correct code. `const { userId } = req.session`
lowers to a temporary and then a field read, so nothing in the graph spells session.userId by the
time the value reaches dao.getByUserId(userId, ...), and the constraint the handler really applies
was invisible.
The arbiter now walks back three hops over the method's own assignments from the names the
operation is GIVEN, and req.session joins the identity sources. The token is only safe because of
that requirement: what discharges an object-level obligation is that the value passed to the
operation comes from the authenticated context, not that a session is mentioned nearby. A handler
that reads the session for logging and keys its query on a path parameter is the bug this family
exists for, and it stays unguarded.
Two exclusions the measurement forced, each caught by a page whose whole purpose is the leak it
demonstrates.
- A callback is not a constraint. Taking identifiers from the whole argument subtree credited
NodeGoat's memo listing with a discharge, because memosDAO.getAllMemos((err, docs) => ... userId
...) reads every memo in the collection and only RENDERS the session's user. Method references
are excluded from the argument names.
- An assignment is not a dominating guard. The session read dominates the query, so that clause
credited it instead and the memo listing went quiet again. An assignment is a read of the
authenticated context, not a check applied to it, so operator calls are excluded there -- the
same exclusion the sink matcher needed. The leak stays reported.
On NodeGoat displayContributions and displayProfile go silent, the allocations insecure direct
object reference, both benefits pages and the memo listing stay, and the login and signup handlers
become honestly unguarded rather than accidentally cleared by a callback's text. Six corroborated
where there were seven, and a better six. VAmPI is unchanged at two entailed findings.
The sibling scope is decided by measurement and kept at the file. On NodeGoat the region's own
file entails two handlers, both public routes and both false, and no true defect at all. Every
handler in the application entails six: four true, and the same two public routes. Both scopes
carry the same false positives, because neither can tell a public route from a protected one --
a semantic question that is already the corroborated band's residual. With the carried rung capped
the scope changes no verdict, only the witness, so widening it would buy a repository-wide walk
per question and no precision. A public-route discriminator would change that; the file scope is
not what holds this family back.
1,415 tests pass, sixteen skipped; thirteen Joern-gated query tests pass in the image.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
|
||
|
|
c0427e6ceb |
feat(access-control): model the collection read, by argument shape rather than by name
NodeGoat's documented insecure direct object reference reads with allocationsCol.find(searchCriteria()), and `find` was absent from an operation table holding findOne, findById, findAll and findByPk. The obligation was missing at the commonest read there is, so that defect raised nothing at any point in the pipeline. It was absent for a good reason, and the reason is the argument, not the name. Measured on wp-statistics, a WordPress plugin that ships jQuery: its assets hold 88 calls named find, of which 70 pass a jQuery selector, 13 pass a predicate to Array.prototype.find and 5 pass a selector built at run time by a template literal or a concatenation. None is a collection read. So the obligation is claimed only where the first argument has the shape of a query document. The operation fact states which argument with query_document_arg, and the arbiter that can see argument shapes decides it: no arguments at all, an object literal, or a builder call are claimed; a method reference, a literal, an operator call and a bare identifier are not. Each exclusion is one of the measured classes above rather than a guess. Zero of those 88 calls became an obligated operation, and the zero is measured rather than empty: the same run's census reports 60 files and 4,552 methods, and the files holding those calls are in it. On NodeGoat all three find calls are collection reads and all three are claimed, which raises the obligation at four more handlers -- the allocations handler that takes userId from req.params on an authenticated route, the benefits pages whose isAdmin guard is commented out in the route table, and the memo listing. Seven of fifteen handlers now reach a verdict, all corroborated, none reported without a judge, and the census is unchanged on both subjects. A bare identifier is deliberately not claimed: col.find(query) and arr.find(pred) are the same shape and neither repository measured holds one, so this is a stated false negative rather than an unmeasured false-positive risk. 1,415 tests pass, sixteen skipped; twelve Joern-gated query tests pass in the image. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
102a21fc4e |
feat(access-control): carry the obligation across the route-and-DAO split
The census measured this family completing 35 questions on NodeGoat with no obligation raised anywhere: every modelled data operation lives in app/data/*-dao.js, none in a route handler file, and operations were scoped to the region's own file. The family had nothing to be right or wrong about. The obligation now follows the one named call the handler writes, out of the region's file and no further. How it is followed is the point. jssrc2cpg does not resolve allocationsDAO.getByUserIdAndThreshold(...) to the DAO body: the callee is an external stub with no code. That stub's fullName records where the member was written, so the implementation is reached by the graph's own provenance rather than by a repository-wide name guess, which is the false positive the file scope was closed against. A control asserts it: a third module defining a member of the same name that nothing imports is not attributed. A row is attributed to the region's DECLARED function, which is what lets a question about displayAllocations match a graph that holds a <lambda>. A carried row is also checked for a guard at the CALL, because that is where a handler establishes who is asking. The method holding a carried operation joins the question's context scope, so the module holding the query cannot change under a claim that depends on it without the change being attributed. The witness names both sites and the finding is located at the call, which is the line a contributor changed. A carried obligation corroborates, it does not entail, and that was measured rather than assumed. Entailing it reported handleSignup, which looks a user up by name to check the name is free during registration and is public by design, because two authenticated siblings in the same file pass req.session.userId into the same module. The entailed rung's justification is that one FILE contradicts itself, same file and same trust context; carried, the compared operations live in a data-access module that handlers of different trust contexts share. The corroborated band already asks the right residual: does this operation require a check, or is the route legitimately public? Measured on NodeGoat: 15 handlers asked, obligations raised at zero before and six now, three reaching a verdict, all corroborated, none reported without a judge. VAmPI unchanged at two entailed findings, because it keeps handler and query in one file and carries nothing. What this does not reach is recorded, not implied. NodeGoat's documented insecure direct object reference still raises nothing, because its module reads with allocationsCol.find(...) and find is not a modelled operation: Array.prototype.find and jQuery's .find are everywhere, so it needs an argument-shape discriminator and its own measurement (task 14.8). Two of the three corroborated claims are correct code whose identity read a destructuring spelled away, and the cross-file contradiction that would entail the real defect needs a sibling-scope decision (task 14.7). 1,415 tests pass, fifteen skipped; eleven Joern-gated query tests pass in the image. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
f31ef862a7 |
fix(output-encoding): ask about the server half, and say what the family cannot decide
The census recorded this family completing 44 questions on a Node application and finding
nothing, which is what a client-only vocabulary must do there: all six declared sinks were DOM
sinks. NodeGoat's cross-site scripting is server-side.
Decision: the family asks about a value reaching a rendering call, and states in its own
declaration that it cannot decide template-mediated escaping. Whether res.render is safe depends
on the engine's autoescape setting and on the template's markup, neither of which is at the
call, so res.render is not a sink and the family's limits field says so. A new CWE-79 sink
covers res.write, res.end, res.send and the response.* spellings; res.json stays out because an
application/json body is not a document.
Template auto-escaping is claimed where its evidence is, as a configuration defect:
swig.setDefaults, nunjucks.configure and marked.setOptions are security settings now.
It establishes a finding: app/routes/research.js:25, model_entailed,
`req.query.url + req.query.symbol -> res.write(body)`. The handler fetches a URL the caller
chooses and writes the response body into a text/html response, so the document served from
this origin is the attacker's. The record states how coarse the step is: the path from url to
body runs through the client's callback parameter, which the graph treats as reachable from the
call's arguments, so the conclusion rests on the call's semantics rather than on what the graph
established. Five of seven families now establish findings.
Four defects were found on the way, three of them false positives this change would otherwise
have shipped.
- The permissive-value set was shared across every setting, while a boolean's polarity belongs
to the setting: true leaves the door open in cors({origin: true}) and closes it in
swig.setDefaults({autoescape: true}). Adding the template engines would have reported the fix
as the defect on the line that offers it. ConfigSpec now carries per-setting values, and
Jinja2's Environment moved out of Python's shared list for the same latent reason.
- config.sc matched a dotted setting on its trailing segment alone, so marked.setOptions would
have claimed an editor's setOptions. dominance.sc already required the qualifier.
- taint.sc asked its flow query of every argument including the receiver at index 0. On a
response sink the receiver is the response object, so res.end() -- which takes no data -- was
reported exactly like the res.write(body) two lines above it. Flows now use the real
arguments, as arity already did.
- The arbiter sorted a file's settings by line number as TEXT, so line 126 preceded line 78 and
which setting a file reported depended on the spelling of its line numbers.
Template escaping disabled at server.js:135 is detected, measured model_entailed against the
real query and the real arbiter, and still not reported: the arbiter returns one verdict per
question and that file has an earlier permissive setting. Task 14.6 carries it rather than
hiding it.
1,415 tests pass, thirteen skipped; nine Joern-gated query tests pass in the image.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
|
||
|
|
57f5a3f0dc |
fix(semantic-facts): give untrusted destination the clients a Node server calls
The family census recorded this family completing 44 questions on NodeGoat, finding the open redirect, and having no way to ask about the repository's documented SSRF: needle.get on a query-string URL at app/routes/research.js:16. A thin vocabulary is a silent false negative. One data change, no query change. The declared set now covers axios, got, needle, request, superagent, unirest, undici and fetch as bare module tokens, plus http.get, http.request, https.get and https.request dotted. Bare is the documented call form of each module and covers its verb methods too, since a token matches word-bounded against the callee and axios is bounded inside axios.get; axios.get and axios.post are dropped as subsumed rather than unwanted. http and https stay dotted because their namespace also holds createServer, and a bare token would have claimed every server a repository starts. The request is now reported at app/routes/research.js:16, model_entailed, witness `req.query.url + req.query.symbol -> needle.get(url, ...)`. This family goes from one finding to two on NodeGoat, every other family is unchanged, and 299 of 299 questions still complete. Measured on that graph, needle matches exactly one call and the eleven other clients match none in first-party or vendored code, so NodeGoat shows the gap closing and cannot show those clients' noise. A control asserts both halves. A handler calling axios.get with a literal and needle.get with req.query.url reports the tainted client alone, and it reads the shipped vocabulary rather than a copy, so removing the client fails it. The CWE-601 redirect entry is untouched: a Location-header redirect is value-shaped and stays outside what this family asks. 1,411 tests pass, eleven skipped; seven Joern-gated query tests pass in the image. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
4e2619e248 |
feat(pre-push-safety-net): census the declared families and fix a sink matched in a comment
Seven vulnerability families are declared and two had been proven. The census asks each of them, on whole checkouts of NodeGoat and VAmPI, whether it establishes anything at all, and reports a family that answered and found nothing separately from one that was never asked. Four establish findings: injection, access_control, config_secrets and untrusted_destination. Output encoding, path, prototype and deserialization establish nothing on either subject. All 299 NodeGoat and 35 VAmPI questions completed, so the silences are answers. Three of those silences are diagnosed gaps, not clean code. NodeGoat's documented SSRF calls needle.get, which the sink vocabulary does not hold. Its XSS is server-rendered, and output encoding declares only DOM sinks, so no question it can ask could have found it. Its two access-control defects raise no obligation at all: measured with the real dominance query, every modelled data operation lives in app/data/*-dao.js and none in a route handler file, while the family scopes operations to the region's own file. Tasks 14.2 to 14.5 carry each gap; none is fixed here. The census also found a false positive and it is fixed. NodeGoat's only prototype report was the word `set` inside `// set these up in case we have an error case`: the matched node is the <operator>.assignment holding the entire 1,000-character signup handler, and sinkMatches searched a node's whole text. It now confines the text clause to the callee and excludes operator nodes. Fourth instance of this bug class after fgets/gets, resolve/resolveUrl and user/users. Measured on NodeGoat's non-vendor calls, injection sinks fall 28 to 3, prototype 13 to 2 and untrusted destination 26 to 7, with no true positive lost; req.query had been an injection sink because `query` is word-bounded inside it. Witnesses now name the sink call rather than the assignment that contained it. 1,411 tests pass, ten skipped; the six Joern-gated query tests pass in the image, including a new one that fails on the shape this fixed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
770bae0e9e |
test(m3): record the PHP blocker as open, six hypotheses eliminated
Paused at the maintainer's direction to return to vulnerability work, with the state recorded rather than left implied. Measured and eliminated: batch size, since 17,700 requests answer in 44 s with a 2.6 MB response; dataflow cost, 50 execution requests in 24 s; call depth 3 with parameter sources, 24.7 s for 20 requests; argument length, a 16 KB hook table against a 128 KB single-argument cap; hook callback resolution, one second of difference with 233 hooks; and build or sharding, 79 s with no shards. Preparation measures 273.5 s, which is real but a small share of a 3,600-second case. Every reproduction of the failing 22,125-request batch outside the push path answers in under a minute. The difference from the production invocation is not identified, and the exception itself has still never been read: the backend logged a stack trace's tail, which is frames, and the corrected extractor reports its head, which on this engine is JVM startup noise. Four instrument defects were found and fixed along the way, all of the same shape: a failure detail that discarded the cause, stages that reported no cost, logging that was never configured so the new stage lines could not emit, and a harness that buffers stderr until a case ends. Node and Python transfer stand. PHP stays unestablished. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
537483657d |
test(m3): PHP still not established; blocker is the dataflow execution pass
Retried the PMPro transfer with batch splitting, a 4500-second deadline and a 900-second query ceiling. Stopped after 26 minutes with no case completed and no verdict, so PHP transfer remains unestablished. A probe narrowed the cause and corrected my earlier reading of it. The evidence pass answers all 4,425 PMPro PHP regions in ONE batch in 29.7 seconds with a 681 KB response, exit 0, and its cost is fixed rather than per request: one request took 40.2 s and 4,425 took 29.7 s. Neither request count nor response size is anywhere near a limit, so batch size and serialization are not the cause as I first supposed from a single extrapolated data point. What fails is the execution pass, which runs reachableByFlows per candidate. The probe's own scope is recorded with it: it sent evidenceOnly=true and therefore measured the structural pass, not the failing one. Those numbers would otherwise read as evidence that PMPro's taint work is cheap, which they are not. Splitting stays, because it is correct on its own terms and recovers a batch that fails whole. It is worth noting it also makes an unrecoverable batch slower to abandon, since each of the eight parts burns its own share of the query ceiling. M3 remains two of three: Node established in M1b, Python established, PHP blocked on dataflow cost for a 1,274-file repository. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
d64abc2d43 |
test(m3): PHP transfer not established; the taint batch cannot answer PMPro
All three PMPro comparisons exhausted a 3000-second deadline with a 1200-second query ceiling and produced no artifact, so there is no target evidence at all. The engine reports `cpg batch taint failed` twice per case on the 1,274-file tree: first an exception whose trace runs through ujson array transformation, then a timeout on the retry. Budgets tried: 900 s, 1200 s and 3000 s deadlines against 300 s and 1200 s query ceilings. The pins, source receipts and declared target are all correct, so the question was asked properly and the engine could not answer it. This is a runtime blocker on a large repository, not evidence about whether the design transfers to PHP. Two harness defects were found on the way, and both had made an earlier run look like a transfer failure rather than an instrument problem. The manifest labels a case by what it is, vulnerable or fixed or benign, while the id says what the comparison does, introduce or repair or benign; filtering on one while keying expectations by the other ran a single case and reported an all-false verdict. And the per-query ceiling is taken as the minimum with the remaining transaction time, so raising only the deadline could never have helped, and twice did not. M3 is not met. Its exit asks for Node, PHP and Python together: Python is established, Node was established in M1b, PHP is blocked. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
bb72fcd413 |
test(m3): Python transfer detects all three declared targets (M3 13.1)
Generalized the replay instrument so the case family is an argument rather than a hard-coded target, since a harness that only knows one language cannot say whether the design transfers. Targets, pins and expectations come from the committed inputs, so Python is asked exactly the way Node was, through the same core, ranker, comparison and admission. VAmPI-SQLI models/user_model.py:73:get_user detected, answered VAmPI-BOLA api_views/users.py:187:update_password detected, answered VAmPI-BOLA-BOOKS api_views/books.py:51:get_by_title detected, answered 35 of 35 questions complete on every case, nothing reported new, zero admitted defects. Two results stand out. The SQL injection is the evidence the milestone spec names as previously unresolved. And two of the three targets are access control, the family that could not complete a question on any repository before group 12, so this is the first time broken object-level authorization has been asked on Python at all, and it answered on both endpoints. VAmPI has the same revision on both sides, so the regression mode establishes detection and completion, never novelty. It asserts that directly: with nothing changed, a target reported new would be a comparison defect, and none was. That tests the novelty logic from the opposite side to the NodeGoat case. Identical revisions cannot show novelty, and VAmPI is inspected teaching data that cannot qualify production Python support. No capability enabled, no latency claim. 1,407 tests passed, nine skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
fc5dc453b6 |
test(ranker): every question now completes on the real case (M1b 12.4)
Measured after scoping a registration-only region to its file. M1a, before group 12 0 of 436 head questions completed after per-family context 298 of 436 after region attribution 299 of 299, and 299 of 299 on the base The count falls from 436 to 299 because the mis-attributed function-scoped regions in a route-registration module are gone, replaced by file-scoped questions the graph can answer. Nothing is unresolved on either revision. Detections are unchanged: the vulnerable revision is still new with operation_absent_from_comparable_base, the fixed twin analyzed and quiet, the unchanged revision not a regression. What remains is recorded limits rather than failures: 210 bounded-reachability boundaries, each owned by the question that reported it, and one vendor exclusion. Neither blocks a comparison. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
ca2c3a8b5f |
test(contributor-scan): record the group 12 end state (M1b 12.1-12.3)
Final verification of the three security capability fixes on the pinned revisions. head questions completed 205 -> 298 of 436 base questions completed 436 of 436 access control completed 0 -> 52 context_projection_unavailable 116 -> 0 context_scope_empty 138 -> 0 Every remaining unresolved question now reports context_function_unresolved and names the function its file does not define, so nothing is unresolved for a reason the tool could not explain. That is a region-attribution question for the ranker, not a limit of what the engine can establish. Detections are unchanged: the vulnerable revision is still new with operation_absent_from_comparable_base and every verdict entry holds. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
3e4ab8d219 |
test(contributor-scan): measure the per-family context effect (M1b 12.2)
Rerunning the pinned revisions with each family producing its own change context. head questions completed 205 -> 298 of 436 context_projection_unavailable 116 -> 0 access_control completed 0 -> 52 Access control is the headline: the family could not complete a question on any repository before, and now completes every question it is asked. That is a vulnerability class the tool could not reason about at all. Detections are unchanged. The vulnerable revision is still new with operation_absent_from_comparable_base, the fixed twin is analyzed and quiet, the unchanged revision is not a regression. The remaining 138 unresolved questions are all context_scope_empty and sit in three files, none of them access control. They are function-scoped questions whose named function the query cannot locate in the graph, so the scope it describes is honestly empty. That is the query-to-operation identity problem M1a recorded at the witness end, showing up at the scope end, and it is task 12.3 rather than something to patch by widening a question to its whole file. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
a91551e547 |
feat(contributor-scan): give each family its own change context (M1b 12.2)
Access control and configuration could not complete their change context on any repository, which left 231 of 436 questions unresolved on the measured Node case and made two of the three shipped families unable to answer. The gap was larger than a missing query block. The evidence pass dispatches taint only, because only taint has an evidence vector to rank by, so these families were never asked for context at all. A new context pass asks each family with the flag set. Each query describes its own scope, which the owner research required and which matters: copying taint's projection would describe a reachability walk these queries never perform. Dominance exports the file's methods labelled operation, guard or sibling, because its claim is that a file contradicts itself. Configuration exports the file's methods labelled setting or sibling, because it asks what a file declares. Both report an empty scope explicitly rather than implying one. A family whose context query answers nothing keeps no context and stays unresolved, so none can complete on another family's context. A control asserts that directly. Confirmed on a real NodeGoat graph through the production backend: both families produce a context summary and seven scoped methods where they previously produced nothing. All seven are siblings for that file, which is the honest answer since it declares no access-control operation and no matched setting. 1,405 tests passed, nine skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
ed0f12b593 |
revert: JavaScript one-start build did not deliver in the push path (M2 11.6)
Reverts
|
||
|
|
f04a839181 |
perf(cpg): build JavaScript in one engine start, gated on the census (M2 11.6)
JavaScript was the only language paying two engine starts: the declared frontend and then a separate overlay pass. joern-parse applies the overlays itself. On the pinned Node tree that is 11.0 s against 21.0 s. The declared frontend stays as the fallback, because it is what the versioned retention adaptation and the explicit exclusion policy run through, and the census decides per build: a graph that does not hold every readable declared file is rebuilt through the frontend rather than trusted. Exclusions keep the frontend path outright. Measured under the production retention environment, both paths produce the same 44 files, the same 579 methods, the same file names, the same five overlays, the Gruntfile and all eighteen first-party test files, with no file present in one graph and missing from the other. My first measurement did not establish that. It ran without the retention environment, both paths dropped to 25 files, and the apparent equivalence was two runs discarding the same things. The existing control named for that decision failed against the change built on it, which is exactly what it is for; the evidence was wrong, not the control. That control now asserts the intent directly: the faster path is tried first, and a short census rebuilds through the frontend. 1,401 tests passed, nine skipped. A build saving is not the latency gate; the profile rerun decides that. Rollout stays NO-GO. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
ffbd864195 |
test(runtime): measure the seven-class profile with the session; NO-GO (M2 11.5)
The unchanged seven-class profile, three samples each, with the engine session enabled. Every changed-code workload still times out at the 30-second deadline, and identical-tip moved the wrong way: p50 rose from 18.5 s to 24.2 s. The cause is in the artifacts rather than inferred. Thirteen transactions report session_unavailable:session_startup_timeout, so under the real deadline the session usually never starts and everything falls back to the disposable path. Startup is 9 to 13 seconds, about a third of the whole budget, and it is spent before any analysis happens. Where a session did start, on cached-graph transactions whose remaining work is small, it cost more than it saved. That is the honest shape of the result: a transaction-owned session amortizes engine startup across the invocations inside one transaction, and a warm changed-code push does not have enough of them. It helps a long development-budget transaction, 326 s to 243 s measured earlier, and cannot help a 30-second one. Correctness is unchanged throughout: the same three pinned revisions through the production rules with the session enabled reproduce the M1 verdict exactly. Group 11 as designed cannot reach the gate, so the dominant remaining cost is named and the decision is raised rather than worked around. Task 11.6 records the candidates, since two of them change the product's shape: a session shared across transactions is a resident local service, which the accepted milestone spec does not imply, and analysing less is a scope change rather than a speed change. The 30-second and 2-second gates stay unchanged. Rollout stays NO-GO. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
7fc6713e78 |
test(cpg): confirm the session stays alive and halves query time (M2 11.4)
The leak was the whole cause. With the session closed by the transaction that owns it, a session-enabled replay of the same pinned revisions under the same budget keeps its session for the whole run: no session_query_failed appears on either side, where the previous run reported one per side and fell back to the disposable path for everything. Measured on the same pins, instrument and budget, changing only the transport: vulnerable revision 326.4 s -> 243.3 s head query time 63.6 s -> 32.0 s base query time 85.1 s -> 51.2 s dominance and config ~14 s each -> ~4 s each Detections are unchanged: witness present, fixed twin analyzed and quiet, unchanged revision not a regression, kill criterion not triggered. Speed bought by losing a detection would not be speed. This is a cold transaction on one teaching repository under a 900-second development budget. It is not the hook latency gate, which is warm changed-code p95 against the frozen seven-class profile, and that is task 11.5. Rollout stays NO-GO. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
d93dee9f1a |
test(cpg): prove transport equivalence for every shipped query kind (M2 11.2)
A session is only usable if it returns exactly what the disposable invocation returns. On a real NodeGoat graph, taint, dominance and configuration all produce byte-identical payloads through both transports, with parameters built by each arbiter's own request function through the driver's family lookup rather than a hand-written shape. The failure shapes that would make a session dangerous rather than merely slow are correct too. An empty answer stays present and distinguishable from an unavailable one, which is the distinction that once turned a failed scan into a clean bill of health. An unknown query and an absent graph are both unavailable. Two graphs in one session each answer about the graph the request names. Amortized over five taint requests the session took 22.5 s against 72.0 s disposable, all five answered and identical to each other. Two instrument defects are recorded rather than hidden. The first run created a fresh backend per comparison, so every session served one request and paid full startup, measuring the session's worst case and making it look slower; that is why the amortized comparison exists. The switched-graph check crashed on a scratch directory it never created, so it produced no verdict at all. These timings are orientation, not the latency gate. Task 11.5 measures against the frozen seven-class profile, and the transport stays off by default until then. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
7e3f12bc00 |
feat(cpg): add a transaction-owned engine session and route it opt-in (M2 11.1, 11.3)
JVM startup, not analysis, is what a push scan spends its time on: each disposable script invocation costs 13 to 23 seconds while the work takes under a second. The session owns one Joern server per transaction on loopback with an OS-chosen port and no published port. Three properties are load-bearing, and the probe that settled the design demonstrated why each is needed. An HTTP status is not an answer. The server returns success even for a compile error, with the diagnostic in its body, so every request redirects the evaluated code's console output into a fresh private file behind a nonce, and the nonce is verified before the answer is accepted. The engine's body is retained for exactly the case where the receipt is missing, without which that failure explains nothing. A poisoned session answers nothing afterwards, and cancellation kills the process group rather than the launcher, which this project has already run a machine out of memory by neglecting. What the probe established about the engine, after three iterations that each failed loudly: runScript is absent from the server REPL; a shipped @main script cannot be nested in a block expression, so definitions go to the top level unwrapped; and the REPL shadows Console with io.joern.console.Console, which has no withOut, so capture must qualify scala.Console. With that fixed, both mechanisms answer identically to the disposable invocation, 0.8 s against 9.8 s. The backend routes batch and single queries through the session only when session_transport is enabled, which it is not by default. Any failure falls back to the disposable invocation rather than reporting a zero, and each script is defined under its own name because every script declares exec. Equivalence for taint, dominance and configuration is task 11.2 and gates any measured run. 1,391 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 |
||
|
|
cec1c4a428 |
feat(pre-push-safety-net): render the experimental evaluation explanation (M1b 10.10)
The M1b exit run established the comparison but could not show a developer-readable consequence or repair direction, because both belong to an evaluated capability declaration and the installed registry is deliberately empty. The experimental evaluation output now renders, for each change-attributed finding, its family, exact location, witness, change attribution, provenance, engine method and operation, together with a consequence and repair direction produced from a supplied declaration through the same grounded-template path the admitted case uses. No prose is invented: when no declaration matches, or a template is not grounded in the operation and context, or the declaration does not cover the operation symbol, the explanation is absent and the reason is stated. Declarations reach this rendering only. The runner never passes them to admission, so nothing in that file can enable a capability or emit an alert however it is written, and a control asserts that even a declaration claiming PASS admits nothing. The flag requires explicit base/head replay and the recorded-veto flag. The evaluation instrument generates its own unreviewed declaration bound to the run's analysis semantics, asserting that it is neither enabled nor PASS, so the demonstration cannot drift into a qualified claim. 1,377 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 |
||
|
|
57d6ddcbba |
test(pre-push-safety-net): record the M1b exit run (task 10.9)
All three pinned NodeGoat revisions replayed through the production rules with exact pins and reopened source receipts. The vulnerable revision reports all three sites new with operation_absent_from_comparable_base, and admission now rejects on capability_unavailable alone: dependency_unresolved is gone, which verifies 10.7 on real data since those gaps belonged to unrelated questions. The fixed twin is analyzed and quiet with three base-only findings. The unchanged revision reports all three sites unchanged with base locations resolved to the same lines. Every finding carries its question, the engine-reported method and its witness. The head census is the complete 44 files and all 436 base questions complete. Zero admitted defects on every revision. All five vetoes M1a recorded are repaired, plus two the repairs exposed. What remains is recorded rather than repaired: 231 head questions still report an incomplete change context from families whose queries export none, which did not block this case and whose families stay experimental, and the vendor exclusion and correspondence ambiguity remain reported as the limits they are. M1b is not yet verified. The milestone also asks for a developer-readable finding carrying a supported security consequence and a concrete repair direction, and both are owned by an evaluated capability declaration that is deliberately empty. Writing that text without a declaration is what admission exists to prevent, so task 10.10 renders it through an explicitly experimental declaration with eligibility still disabled. Runtime, independent qualification and rollout all remain NO-GO. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
b8b46f1c19 |
test(pre-push-safety-net): record the M1b production comparison on NodeGoat
Rerunning the three pinned revisions after tasks 10.1-10.6 shows the production comparison classifying all of them with no lifting, and agreeing exactly with the recorded view. That agreement is the point: the vetoes M1a recorded were repaired rather than bypassed. The vulnerable revision is new with operation_absent_from_comparable_base, and the three eval witnesses at contributions.js lines 32 to 34 are attributed to the change that introduced them. The fixed twin is analyzed and quiet with three base-only findings. The unchanged revision is unchanged. In M1a every one of the 436 questions was demoted on both sides and production reported witness_identity_unresolved on every finding. All three artifacts still report finding_status none with zero admitted defects. Admission rejects on capability_unavailable, the deliberate eligibility gate, and on dependency_unresolved, which the rerun exposed as coarse plumbing: the runner passes every coverage reason as every candidate's dependency gaps. That is now task 10.7, ahead of M4, since it would block admission forever once capabilities qualify. It is not a comparison failure and not an M1b exit blocker. Three comparisons on one inspected teaching repository under a development budget. No capability enabled, no alert emitted, no latency claim. Runtime, independent qualification and rollout all remain NO-GO. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
c1cc332bc1 |
feat(pre-push-safety-net): stop declared exclusion vetoing first-party answers (M1b 10.6)
vendor_semantics_unresolved was grouped with the graph integrity failures, so any declared exclusion demoted every otherwise complete answer in the repository to graph_incomplete. Every real repository excludes its dependencies, so this fired on all of them, including on a complete first-party census with successful query receipts. Graph boundaries are now split by what they mean. An integrity failure says the graph could not be read or is incomplete, so an answer drawn from it is demoted: unparsed files, an empty or sharded graph, an incomplete or unavailable census, an unresolved symlink, an unsupported frontend, unreadable source, an ambiguous frontend path and unvalidated TypeScript property support all keep that behavior. A declared exclusion is a deliberate scope choice. It stays physical, stays out of targets and graphs, and stays reported in coverage, but it is not evidence that a first-party answer is wrong. Whether a particular answer needed the excluded semantics is carried by that question's own unresolved call destinations, recorded per question in 10.5, rather than assumed for every answer in the repository. Controls exercise the real partition logic rather than an injected reason: a vendored directory leaves the answer completed with the exclusion still reported, an unresolved symlink still demotes, and the two together still demote. 1,365 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 |
||
|
|
c73efd2bde |
feat(pre-push-safety-net): export the enumerated operation inventory (M1b 10.4)
A taint answer reports traced flows, so an absent row means either that the operation is not present or that no traced path reached it. Change comparison needs that distinction to say a change introduced an operation, and reachability is deliberately bounded, so the two can never be separated from flow rows alone. That is why the veto guarding the confusion is permanently true on any real Express application, and why it could be neither discharged nor safely removed. The taint query now exports one row per operation it enumerated in scope, plus a completion marker. The marker is what makes an empty inventory meaningful: without it a scope that was never enumerated and a scope holding no such operation are indistinguishable. Comparison separates three outcomes on a changed line: the operation is present in the base without a traced flow, which is a reachability statement and not novelty; the operation is absent from an enumerated base, which establishes that the change introduced it; or the base scope was never enumerated, which stays explicitly unresolved. A malformed inventory row is not an empty inventory. The arbiter now skips any row carrying a kind, so an inventory row cannot be normalized into a flow with an empty source that names no attacker input. Verified against the real engine on an authored two-revision JavaScript control: the operation is enumerated on both sides, the base carries it with zero traced flows, the zero-operation marker appears for an empty scope, and answers and findings are unchanged. No new sink inference, alias resolver or second source IR. 1,354 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 |
||
|
|
86980d8aa0 |
feat(pre-push-safety-net): measure M1b step 1 and record the inventory prerequisite
Rerunning the three NodeGoat revisions after tasks 10.1-10.3 shows the recorded view now classifies the case: the vulnerable revision reaches new with operation_absent_from_comparable_base, and the unchanged revision reaches unchanged with its base location resolved to the same lines. Both previously stopped at operation_correspondence_unresolved and witness_identity_unresolved. The rerun also caught a defect in the repair itself: the new novelty reason was missing from the admission allowlist, so admission rejected the candidate as change_unsupported. Fixed with a control. The production path is still blocked by the last two vetoes, and the measurement shows neither can be scoped or relaxed as the plan described. A taint answer reports traced flows, so an absent row means either that the operation does not exist in the base or that no traced flow reached it. dynamic_external_or_depth_context_unresolved exists to stop the second being read as the first, and removing it would turn a wrong absence into a wrong new finding. It is also computed over every in-scope method, so on any real Express application it is always true and can never be discharged, which is why the tool reports nothing on real projects. The resolution is new evidence rather than a relaxed rule: export the operations the query actually enumerated, separately from the flows it traced, so absence is established from the inventory while a traced-flow claim still requires complete reachability. That is now task 10.4, ahead of the two vetoes it unblocks, and it adds no detector, alias resolver or second source IR. 1,348 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 |
||
|
|
3224884d8f |
feat(pre-push-safety-net): record admission vetoes and meet M1a on NodeGoat
Add group 9 to the approved task plan as the bounded owner amendment release milestone M1a requires, implement the task-local recorded-veto evaluation flag, and run the three pinned NodeGoat revisions under it. The flag re-runs the production comparison and admission over a lifted copy of the same evidence and records each veto instead of applying it. Recognized completeness vetoes are lifted and logged; failures never are, because silence from a failure is not a quiet fixed twin. It is off by default, accepted only on explicit base/head replay, refused in Git stdin mode, labels every payload it produces, and admits no defect even when a lifted candidate otherwise would. M1a outcome, inside its one-week box and with expectations frozen on disk first: the three entailed <lambda>2 witnesses are present at contributions.js 32-34; the fixed twin was genuinely analyzed and quiet, answering its target question with zero rows and no deadline reason while the same findings appear as base-only; the unchanged revision was analyzed and recorded unchanged. The kill criterion did not trigger. Five vetoes fired and were recorded. M1a also corrected the diagnosis: with all five lifted the detection still stops at operation_correspondence_unresolved, because a newly introduced operation sits on a changed line and has no base anchor. The unchanged revision resolves its base location on the same lines and classifies as unchanged, which isolates correspondence as the mechanism. That fifth veto was not in the earlier four-veto diagnosis and is now M1b's central repair. Diagnosis only. No defect admitted, no capability enabled, no gate changed, no latency claim; the 900-second development budget cannot measure the hook gate. Rollout and task 8.3 remain NO-GO. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo |
||
|
|
1ad472c68d | test(release): measure repaired reuse and local Joern lifecycle | ||
|
|
7c1c2d4475 | fix(contributor-scan): retain declared Gruntfile and report census gaps honestly | ||
|
|
88216c5072 | test(pre-push-safety-net): validate packaged outcome and retain rollout no-go | ||
|
|
b506516d87 | feat(pre-push-safety-net): enforce evidence eligibility and record blocked qualification |