mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 12:39:28 +00:00
contributor-scan 5.4. Both halves, and the second found a frontend blocker.
**One verdict per region was right for a pair and wrong for a file.** A
pair is one labelled function with one bug, so one answer is correct. A
region spanning a PHP file is not: the probe's SQL injection on line 5 and
command injection on line 13 produced ONE injection verdict, `system` won
it on flow length, and the SQL injection went unreported by construction
rather than by any failure of the analysis.
`taint.verdicts()` now returns one verdict per distinct sink site,
strongest first, and its first element is exactly what `verdict()`
returns -- so the pair path and every committed measurement resting on it
are unchanged, and the gates are byte-identical. Both entry points share
`_usable_flows` so neither can drift from the other.
php probe 2 of 3 -> 3 of 3
vampi 6 entailed, unchanged, now with lines on access-control
findings from task 2.7 (users.py:187, books.py:51 -- the two
documented BOLAs at their exact lines)
libpng 1 -> 2 entailed; the second was hidden by the collapse.
Neither is validated and both are in contrib/.
**WordPress registers handlers rather than decorating them, and the hook
name is the declaration.** `wp_ajax_nopriv_*` is WordPress saying
logged-out callers reach this, so it is public; `wp_ajax_*` fires only for
logged-in ones; `permission_callback => __return_true` declares a REST
route open. That is the declared-provenance signal task 2.8 needs,
arriving from a second framework. Plus every top-level PHP function as its
own region, without which a file is one region however many functions it
holds: 1 file-level region became 10 function-scoped ones.
**And a blocker, recorded as 5.5.** php2cpg emits a CPG containing only
ONE PHP file, the last alphabetically. Reproduced twice on independent
samples; both files parse cleanly under `php -l` and under PHP-Parser
directly, and joern-parse reports success either way. php2cpg logs "Found
warning in PHP-Parser JSON output" naming the survivor, and that warning is
PHP-Parser's own `====> File X:` banner appearing in what php2cpg expects
to be pure JSON.
Not isolated yet: this machine has no PHP interpreter, so php2cpg was
driven through a container shim. The banner comes from PHP-Parser rather
than the shim, which makes a shim-only cause unlikely but not excluded, and
rerunning with the native php-cli in the shipped image is the first step.
It blocks WordPress outright -- a plugin is many files, and a scan that
silently covers one of them and reports success is the quiet failure Req
5.6 was approved for.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
479 lines
33 KiB
Markdown
479 lines
33 KiB
Markdown
# Implementation Plan: contributor-scan
|
||
|
||
## Guidance for the implementing agent
|
||
|
||
- **The predecessor's lesson, in one line.** Five bugs in `model-grounded-detection` shared one shape: a
|
||
taint-specific assumption applied to every family, each caught by a *measurement* rather than a check.
|
||
Before you finish any change to a routing rule, a matching rule, or a per-family question, **grep for its
|
||
siblings and fix them in the same commit.** The most expensive instance cost a 19-minute run that returned
|
||
an identical number, and only the identity revealed the bug.
|
||
- **Run the whole CI list before pushing**, not the parts you remember: `ruff format --check .`,
|
||
`ruff check .`, `mypy src/openultrasast`, `pytest`, the three gates, `compileall src tests`, and
|
||
`pytest -m semantic`. CI was red for most of a session because `ruff check` passing was taken as evidence
|
||
that `ruff format --check` would.
|
||
- **The gate baseline is the safety net.** `benchmarks/measurements/2026-09-06-gate-baseline.txt`. Run all
|
||
three gates before and after every task. The model layer is additive: if a gate moves, you changed
|
||
something that was never meant to change.
|
||
- **RED first**, one commit per task, selective `git add`.
|
||
- **Zero-dependency core.** Copy the capability-probe pattern in `semantic/extra.py` and `cpg/capability.py`.
|
||
- **Do not modify `model/pipeline`, `taint`, `dominance`, `config_value`, `specs` or `candidates`.** They are
|
||
built and measured. If a change looks necessary, it belongs in its own commit with its own measurement.
|
||
|
||
## Group 1 — Wire it (gate-protected)
|
||
|
||
- [x] 1.1 Regions from entry points
|
||
- `model/regions.py`: `ScanRegion(path, function, families, rank, source)` and
|
||
`regions_for(entries, targets, *, taxonomy, language_of)`. Rank from what `EntryPointRecord` already
|
||
carries (`access_level`, `trust_boundary`) — do not invent a new signal. File-level fallback when a target
|
||
has no entry point, with `source="file_fallback"`. Families from region shape and language, via the
|
||
existing `taint_specs`/`dominance_specs`/`config_specs`.
|
||
- **Sibling audit (Req 8.5):** family routing now exists in three places — `ceiling.py`'s harness,
|
||
`pipeline._arbitrate`, and here. Check all three agree before finishing.
|
||
- Observable: a region is produced per entry point; a target with no entry point yields a file region marked
|
||
as such; no region carries a family whose spec is absent for its language.
|
||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 8.5_
|
||
|
||
- [x] 1.2 The repository driver
|
||
- `model/scan.py`: `ScanBudget`, `ModelScanResult`, `scan_repository(...)`. **One CPG per run**, reused
|
||
across regions. Regions in rank order; budget decremented per model call and checked before each; the
|
||
remainder counted into `regions_unjudged`. Findings ordered by rung, then rank.
|
||
- Observable: one `backend.build` call for many regions; a budget of N yields at most N model calls and a
|
||
non-zero `regions_unjudged`; ordering puts `model_entailed` above `suspicion`; a failed CPG build yields
|
||
an empty result with `cpg_build_failed`, never an exception.
|
||
- Redaction (Req 8.3) is inherited from `model/pipeline`, which redacts every prompt; a test asserts the
|
||
driver adds no unredacted path of its own.
|
||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 8.3_
|
||
- _Depends: 1.1_
|
||
|
||
- [x] 1.3 The MAP stage
|
||
- Wire `scan_repository` into `cli.py` inside `Stage.MAP`, mirroring `_check_obligations`: takes
|
||
`entry_points` and `targets`, returns findings merged **additively** beside the pattern, overlay and
|
||
obligation findings. A `[model]` config block (enabled, budget, max regions), zero-dep, conservative
|
||
defaults. Payload into the manifest with `by_rung`, cost and elapsed.
|
||
- Observable: a scan on a repo with Joern present reports findings carrying rungs; the same scan with
|
||
`OPENULTRASAST_JOERN_PROBE=off` is identical to today's output plus a `cpg_unavailable` degradation;
|
||
**the three gates are byte-identical**.
|
||
- The `[model]` block is capability-detected like every other optional plane (Req 8.4): core stays
|
||
`dependencies = []`, and the engine, endpoint and execution tier each degrade to a recorded reason.
|
||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 8.4_
|
||
- _Depends: 1.2_
|
||
|
||
- [x] 1.4 Batch the queries — one Joern invocation per scan, not one per region and family
|
||
- **Why this is in group 1 rather than group 2.** Wiring 1.3 exposed that the cost model in the design was
|
||
half right. One CPG per scan was the right call, but every *query* is its own `joern --script` JVM launch
|
||
of roughly 30 seconds, and the driver issues one per region per family. A single ten-line Python file
|
||
admits six families and therefore takes six launches and over three minutes. At 100 regions that is ~5
|
||
hours; at 1000, ~50. Task 2.2's go/no-go would not have been a measurement, only arithmetic.
|
||
- Extend `cpg/backend.py` with a batched call: one invocation carrying many (region, family, spec) requests
|
||
and returning rows keyed back to each. The `.sc` scripts already take parameter lists; they take a list of
|
||
requests instead. `model/scan.py` collects the work first, issues one call, then arbitrates from the rows.
|
||
- **Keep the seam.** The subprocess boundary stays the only Joern touch-point (predecessor Req 4.5). Server
|
||
mode would keep a JVM warm but adds a process model and weakens that promise; batching needs neither.
|
||
- **The arbiters do not change.** `taint`, `dominance`, `config_value` and `pipeline` take rows and are
|
||
already measured; if this task needs to edit one, that is a signal the batching is leaking into them.
|
||
- Observable: a scan over N regions issues **one** `joern --script` invocation, not N×families; the same
|
||
fixture produces the same findings and rungs as the unbatched path, asserted against a recorded baseline;
|
||
the ten-line scan drops from minutes to seconds.
|
||
- _Requirements: 3.5, 4.2, 4.3_
|
||
- _Depends: 1.3_
|
||
|
||
## Group 2 — Scale it (the phase that can reshape the rest)
|
||
|
||
- [x] 2.1 A pinned known-vulnerable checkout
|
||
- `benchmarks/repos/<name>.toml`: url, commit, the known CVE, the file and function it lives in, licence.
|
||
Start with one small enough to iterate on. Fetch is explicit and offline-by-default, like the pointer
|
||
pairs.
|
||
- Observable: the recipe resolves to a checkout; CI never fetches it.
|
||
- _Requirements: 4.1_
|
||
- _Depends: 1.3_
|
||
|
||
- [x] 2.2 The first repository measurement
|
||
- Run the full scan against that checkout and commit the artifact: CPG build time, query time and total
|
||
scan wall-clock as three separate numbers, peak memory, findings by rung, cost, `regions_unjudged`,
|
||
and **whether the known CVE was found and at which rung**. The build/query split is separated because
|
||
2.3 is decided on that ratio and cannot be decided on a single wall-clock figure. If the repository cannot be processed in its envelope, record the stage it failed at.
|
||
- This is the go/no-go for the shape of the rest: if one CPG per repository is infeasible, the region and
|
||
budget design changes and phases 3–6 should not be built on it first.
|
||
- Observable: the artifact exists with every field; the reading states go or no-go and why.
|
||
- _Requirements: 4.2, 4.3, 4.4, 4.5_
|
||
- _Depends: 2.1_
|
||
|
||
- [x] 2.3 Decide on Joern server mode, on 2.2's numbers
|
||
- Joern offers `--server` with `--server-host`, `--server-port` and basic auth: one warm JVM with the CPG
|
||
loaded once, and queries in milliseconds after that. It would remove the three-to-four JVM starts
|
||
(~100s) that remain per scan now that 1.4 has batched the queries.
|
||
- **Deliberately sequenced after the measurement, because deciding it now would be optimising before
|
||
measuring.** Batching already moved query cost from O(regions) to O(1) per scan; whether the remaining
|
||
floor matters is a ratio 2.2 produces and nothing before it does. If a real CPG build takes ten minutes,
|
||
a 100s query floor is noise and server mode buys nothing. If the build is thirty seconds, the floor is
|
||
3x the build and server mode is the obvious next move.
|
||
- Costs to weigh against that ratio: process lifecycle (start, health, shutdown, orphan reaping -- stray
|
||
JVMs had to be killed by hand twice while building the batching); the seam weakening from "the subprocess
|
||
is the only Joern touch-point" to "subprocess or HTTP client", which is the promise that has kept the
|
||
backend replaceable; and that a CPGQL endpoint evaluates Scala, so it is arbitrary code execution on the
|
||
analysis host -- acceptable inside the shipped container, which runs `network_mode: none`, and much less
|
||
so as a port listening on a contributor's laptop.
|
||
- Observable: a recorded decision that cites 2.2's build-time-to-query-time split. A preference does not
|
||
close this task; a ratio does.
|
||
- **Decided: no.** On libpng (89k lines of C) the query phase splits taint 243.3s / config 10.7s over two
|
||
invocations, and fixed overhead measured on that CPG is ~13s per invocation. So ~26s of 250s is JVM
|
||
startup and ~224s is CPGQL evaluation: a warm server removes 6% of a 446s scan and none of the part that
|
||
costs. Recorded in `benchmarks/measurements/2026-09-09-repo-libpng-envelope.json`.
|
||
- The small repository pointed the other way -- vampi's 34.3s over three kinds is roughly three startups
|
||
and almost no evaluation -- and the ratio inverts with size. That inversion is precisely why this task
|
||
was sequenced after a real repository instead of being decided on a 520-line file.
|
||
- _Requirements: 4.2_
|
||
- _Depends: 2.2_
|
||
|
||
- [x] 2.4 A region that spans the flow, not just a function
|
||
- **2.2's finding, and the one that reshapes the phase.** VAmPI-SQLI is a request parameter reaching
|
||
`get_by_username(username)`, passed to `User.get_user(username)` in another module, interpolated into a
|
||
query and executed. Source and sink are in different functions, regions are per-function, and a
|
||
repository scan does not treat parameters as untrusted -- so neither region sees both ends and the
|
||
engine is silent on the most ordinary shape a real injection bug has.
|
||
- The CPG already has the call graph; `reachableByFlows` is not function-bounded. What is bounded is the
|
||
QUESTION the driver asks. So the work is in the region model and the query scope, not in the arbiter:
|
||
a region should be able to name an entry point and admit sinks reachable from it, with the file scope
|
||
from 2.2 kept so the answer is still attributable.
|
||
- Watch the cost: an unbounded interprocedural question over a large repository is how this gets slow
|
||
again. Measure it on vampi first, then on the envelope checkout.
|
||
- Observable: VAmPI-SQLI entailed or corroborated at `models/user_model.py:get_user`, with the scan's
|
||
wall-clock and the entailed-finding count recorded against 2.2's numbers.
|
||
- _Requirements: 4.4, 8.1_
|
||
- _Depends: 2.2_
|
||
|
||
- [x] 2.5 Module-level regions the engine can actually ask about
|
||
- A setting in `if __name__ == '__main__':` belongs to a region the mapper calls `app.py:__main__`, but
|
||
Joern names that method `<module>`. The function filter matches nothing, so module-level configuration
|
||
is invisible. Before 2.2's file scoping it was worse, not better: the literal was reported eight times
|
||
in eight files that do not contain it.
|
||
- Observable: `app.py:17`'s `host='0.0.0.0'` reported exactly once, at that location.
|
||
- _Requirements: 4.4_
|
||
- _Depends: 2.2_
|
||
|
||
- [x] 2.6 Access control that does not entail every public endpoint
|
||
- 7 of 9 VAmPI handlers were entailed, including `register_user` and `get_all_users`, which are meant to
|
||
be unauthenticated. The dominance rule -- unguarded, with guarded siblings -- was calibrated on pairs
|
||
where a guarded twin always existed. At repository scale every public endpoint has guarded siblings.
|
||
- This is a precision question and needs a precision measurement: on a real repository, what fraction of
|
||
entailed access-control findings are endpoints that genuinely require authorisation? Do not tune it
|
||
against VAmPI alone; that is how the +16% overfitting gap in Req 8 happened.
|
||
- Observable: a recorded precision figure before and after, on more than one checkout.
|
||
- _Requirements: 8.1, 8.2_
|
||
- _Depends: 2.2_
|
||
|
||
- [x] 2.7 A dominance witness that names its line
|
||
- Access-control sites read `api_views/users.py:?:update_password`. Taint and config witnesses carry
|
||
`(line N)`; dominance does not, so its findings have no line and a contributor cannot open them.
|
||
- Observable: every entailed finding in a repository scan has an integer line.
|
||
- _Requirements: 5.1_
|
||
- _Depends: 2.2_
|
||
|
||
- [ ] 2.8 An endpoint that declares itself public carries no obligation
|
||
- **2.6's remainder.** Both false positives left on VAmPI are `register_user` and `login_user`, which its
|
||
OpenAPI spec declares public with no `security` block. The arbiter cannot see that declaration, so it
|
||
reports a missing guard on an endpoint whose contract says there is nothing to miss.
|
||
- The mapper's `access_level` is NOT a safe substitute on its own. For an OpenAPI route it comes from the
|
||
declared `security` block and is trustworthy; for a decorator-based framework "public" means only that
|
||
no `@login_required` was found, which is precisely the bug this family exists to report. Suppressing on
|
||
that would silence the primary detection case for Flask and Express.
|
||
- So the work is to carry the PROVENANCE of the access level, not just its value: a declared contract
|
||
removes the obligation, an absent decorator does not. `EntryPointRecord.provenance` currently only
|
||
echoes the level, so this needs a real signal rather than a string match on evidence text.
|
||
- Observable: `register_user` and `login_user` no longer entailed on vampi, with the pair-corpus leak rate
|
||
unchanged at 0% on the non-VAmPI pairs -- the check that this suppressed a declaration and not a family.
|
||
- _Requirements: 8.1, 8.2_
|
||
- _Depends: 2.6_
|
||
|
||
- [ ] 2.9 One defect is one finding, however many regions reach it
|
||
- libpng's scan produced 26 entailed findings that are all the identical site,
|
||
`contrib/gregbook/wpng.c:335:main`. Many regions reach one shared sink and each emits its own finding;
|
||
nothing dedupes by site. A contributor would be shown the same defect twenty-six times.
|
||
- Invisible at vampi scale, where regions rarely share a sink, and it appeared the moment task 2.4 let a
|
||
region's question follow the call graph.
|
||
- Dedupe on the reported site, keeping the strongest rung and recording how many regions reached it --
|
||
"reached from 26 entry points" is useful information, twenty-six rows are not.
|
||
- Observable: libpng's entailed count reflects distinct sites; the artifact records both numbers.
|
||
- _Requirements: 5.1, 8.1_
|
||
- _Depends: 2.4_
|
||
|
||
- [x] 2.11 The suspicion band has to be worth reading
|
||
- libpng's scan produced 198 rows: 26 entailed (all one false positive, since fixed), 113 `c-unsafe-*`
|
||
pattern matches that assert an API is PRESENT rather than misused, 50 overlay-coverage annotations that
|
||
are not findings at all, and 9 model suspicions. Zero validated defects.
|
||
- The rung is honest -- these say "needs review" and mean it -- but a contributor handed 163 rows about a
|
||
library that uses `memcpy` correctly throughout will not read the one that matters. 82 of the 113 are in
|
||
`contrib/`, so task 2.10 removes most of it; what remains needs a policy, not a filter invented here.
|
||
- Observable: a stated rule for what reaches a contributor's report at `suspicion`, and libpng's row count
|
||
under it, measured.
|
||
- _Requirements: 5.1, 5.2_
|
||
- _Depends: 2.10_
|
||
|
||
- [x] 2.10 Do not spend the scan on example code
|
||
- 22 of libpng's 30 highest-ranked regions are under `contrib/` -- the sample programs shipped with the
|
||
library rather than the library. The budget and the 243s taint query go there first.
|
||
- The rank already comes from the entry-point mapper; what is missing is that a path can be evidence too.
|
||
Vendored, generated, example and test trees are not what a contributor is changing. This is also the
|
||
cheapest available reduction of the 243s.
|
||
- Do not hardcode `contrib/`: derive it, record what was excluded and why, and measure the scan with and
|
||
without so the exclusion is a number rather than a preference.
|
||
- Observable: a recorded before/after on libpng -- regions, query seconds, and entailed sites.
|
||
- _Requirements: 4.2, 8.1_
|
||
- _Depends: 2.2_
|
||
|
||
- [x] 2.12 A C flow that a bounds check governs is not a finding
|
||
- `c/injection` and `c/memory` carry ZERO sanitizers, so no C flow can ever be discharged. Every
|
||
`argv -> strcpy` entails however carefully it is bounded, which is why libpng reports
|
||
`strcpy(outname+len, ".png")` at wpng.c:374 -- guarded four dozen lines earlier by
|
||
`if ((len = strlen(inname)) > 250)` against a `char[256]`.
|
||
- A sanitizer list will not fix this: the discharge is not a call, it is a GUARD that dominates the sink.
|
||
That abstraction already exists and already works -- it is what `dominance.sc` does for access control --
|
||
and it is wired to one family only. The work is to let a taint family name a guard-shaped discharge and
|
||
route it through the arbiter that can decide one.
|
||
- Observable: wpng.c:374 not entailed, with the pair corpus and both repositories measured either side. Do
|
||
not do this by adding `strlen` to a sanitizer list; that would discharge the unguarded case too.
|
||
- _Requirements: 8.1, 8.2_
|
||
- _Depends: 2.9_
|
||
|
||
- [ ] 2.13 A destination allocated to size is bounded too
|
||
- 2.12's remainder, and libpng's last entailed finding: `strcpy(output + len, ".icc")` at
|
||
contrib/examples/iccfrompng.c:124, where `output = malloc(len + 5)` and a five-byte copy lands at offset
|
||
`len`. It fits exactly. There is no comparison anywhere, so the bound rule cannot see it -- the
|
||
destination is ALLOCATED to size rather than bounds-checked.
|
||
- Deliberately not done inside 2.12. Both discharge shapes were found on the same repository, and
|
||
extending a rule until one checkout is clean is the overfitting Req 8 exists to prevent. This wants a
|
||
second C checkout with a genuine CWE-121 first, so the change can be measured for what it silences as
|
||
well as what it clears.
|
||
- Observable: iccfrompng.c:124 not entailed, AND a real overflow on another checkout still entailed.
|
||
Without the second half this task should not be closed.
|
||
- _Requirements: 8.1, 8.2_
|
||
- _Depends: 2.12, 5.3_
|
||
|
||
- [ ] 2.14 Control dependence is not populated, and two arbiters want it
|
||
- Measured, not assumed: **0 of 13,184 calls in the libpng CPG and 0 of 1,152 in the vampi CPG have a
|
||
controlling control structure.** `controlledBy` returns nothing in either, so the PDG's control half is
|
||
simply absent from the graphs we build. The data half is present and load-bearing -- `reachableByFlows`
|
||
is the whole taint arbiter, and `joern-parse` reports `dataflowOss` applied.
|
||
- Everything guard-shaped we do therefore uses CFG dominance (`dominatedBy`) or syntactic conditions
|
||
(`method.controlStructure.condition`) instead. That is why 2.12's first attempt failed: libpng's bound
|
||
reaches its sink through an `error` flag, which is a control-dependence fact and nothing else.
|
||
- Find out whether a CDG overlay can be enabled on `joern-parse` or in the script, what it costs on the
|
||
envelope checkout, and whether it makes the flag-mediated guard decidable. If it cannot be enabled,
|
||
record that -- it bounds what guard reasoning this engine can ever do.
|
||
- Observable: a recorded answer with the build-time and query-time cost, and wpng.c:374 decided or a
|
||
stated reason it cannot be.
|
||
- _Requirements: 4.2, 8.1_
|
||
- _Depends: 2.12_
|
||
|
||
- [ ] 2.15 A C checkout whose CVE goes through a sink we model
|
||
- libpng cannot demonstrate a C true positive, and pinning an older vulnerable commit would not change
|
||
that. **Every one of the twelve files using `strcpy`/`strcat`/`sprintf`/`gets` is under `contrib/`; the
|
||
library itself uses none of them.** Its overflows are array and pointer arithmetic and `memcpy` sizing,
|
||
and five buffer-overflow fixes checked (`png_set_quantize`, `png_image_finish_read`,
|
||
`png_init_read_transformations`, `png_do_quantize`, `png_write_image_8bit`) touch no modelled sink at
|
||
all.
|
||
- So there are two ways forward and they are different sizes.
|
||
- **Cheap: pin a C project whose CVE genuinely runs through a string function.** That gives the memory
|
||
family its first true positive and tells us whether the family works at all. It does not make libpng
|
||
decidable.
|
||
- **Expensive, and a different engine: model how C actually overflows.** A `memcpy` whose size is
|
||
computed, an index that is not bounded. That is *size arithmetic*, not sink matching -- "can this
|
||
expression exceed this allocation" rather than "does tainted data reach this call". It is a constraint
|
||
problem, which is what SMT is for and what this project deliberately did not adopt: the architecture
|
||
decision was abstract interpretation as arbiter, taken because a model layer can be built from code
|
||
where execution cannot be assumed. That decision was right for the flow families and it does not reach
|
||
this one.
|
||
- The maintainer has attacked libpng this way directly -- SMT to lay out memory, then guiding a fuzzer to
|
||
produce the PNGs that reach it -- which is evidence about the shape of the problem, not a suggestion to
|
||
reimplement it. It says the honest routes to C memory safety are constraint solving or execution, and
|
||
the ladder already has a rung for the second (`execution_confirmed`, built and unreachable per the
|
||
module audit).
|
||
- Observable: a pinned checkout with an in-scope CVE found at a rung, OR a recorded decision that
|
||
`c/memory` is scoped to string-function misuse and makes no claim about arithmetic overflows. Do not
|
||
leave it implying it covers a class it cannot see.
|
||
- _Requirements: 4.1, 4.4_
|
||
- _Depends: 2.12_
|
||
|
||
- [ ] 2.16 Say what `c/memory` actually covers, or retire it
|
||
- Its score so far is zero true positives and every finding a false one: 26 from a substring match, 3 from
|
||
a bound it could not see, 1 from an allocation sized to fit. Each was a real defect in the engine and
|
||
each is now fixed, but the family has never once been right, and 2.15 explains why -- it models string
|
||
functions in a codebase class that overflows through arithmetic.
|
||
- This spec's predecessor threw away parts that did not carry their weight, and the same question applies
|
||
here. The answer may well be "keep it, scoped and labelled" -- `strcpy` misuse is real in plenty of C --
|
||
but it should be a recorded decision with a number behind it, not an assumption.
|
||
- Observable: either a measured true positive on the 2.15 checkout, or the family's declared scope
|
||
narrowed in the facts and the docs so no reader takes a silent C scan for a clean bill of health.
|
||
- _Requirements: 3.1, 8.2_
|
||
- _Depends: 2.15_
|
||
|
||
- [ ] 2.17 The model's own suspicion band is a pattern match with an opinion attached
|
||
- **Req 5.5's other half.** Task 2.11 made pattern matches proportionate and left this untouched, so vampi
|
||
still emits 61 sections for a 520-line application -- 38 of them model suspicions, roughly one per
|
||
fourteen lines of code.
|
||
- The epistemic case for treating them as reasoned claims does not survive inspection. A model suspicion is
|
||
what the enumerator proposed at a site the graph could NOT decide, affirmed by a judge with no coverage
|
||
there. That is a pattern match with an opinion attached, and it is weaker evidence than the obligations
|
||
checker's output, which comes from a structural analysis rather than a guess.
|
||
- So group them by family the way 2.11 groups pattern rules, and keep obligation findings and everything
|
||
at `model_corroborated` or above in full. Expect vampi to fall from 61 sections to roughly 23.
|
||
- Measure both repositories, and measure what it costs: if a real finding only ever appeared at model
|
||
suspicion, this hides it, and that is the number that decides whether the rule is right.
|
||
- _Requirements: 5.1, 5.2, 5.5_
|
||
- _Depends: 2.11_
|
||
|
||
## Group 3 — Ship it
|
||
|
||
- [ ] 3.1 Contributor output
|
||
- `model/report.py`: a scan summary — counts per rung, the top findings with witness and `path:line`, and a
|
||
plain statement of what `suspicion` means (the model could not confirm it). Findings to the writable
|
||
output only.
|
||
- Observable: a scan produces a summary readable without opening JSON; every finding shows rung, witness
|
||
and location; nothing is written into the analysed tree.
|
||
- Keeps the disciplines already built into `model/report.py` (Req 8.1, 8.2): per-slice rows with the
|
||
real-world slice deciding, a refused bare aggregate, and the overfitting gap published beside any headline
|
||
— absent rather than zero when a slice is missing.
|
||
- _Requirements: 5.1, 5.2, 5.3, 5.4, 8.1, 8.2_
|
||
- _Depends: 1.3_
|
||
|
||
- [ ] 3.2 Build and verify the image
|
||
- `docker compose build`, then run a real scan through it and through the shell wrapper. The Dockerfile and
|
||
compose exist and are syntax-valid; **the image has never been built**, so nothing about it is proven.
|
||
- Observable: the image builds; `TARGET=<repo> docker compose run --rm ousast scan /target` produces the
|
||
same findings as a native run; the target mount is read-only; the network is off by default.
|
||
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
|
||
- _Depends: 3.1_
|
||
|
||
## Group 4 — Close the deferred decisions
|
||
|
||
- [ ] 4.1 The memory-family decision
|
||
- Either reach `memory` through an adopted execution tier, or record explicitly that C memory-safety is out
|
||
of scope. 173 of the 176 real-world CVE pairs sit idle behind a stub; the decision gates Req 9.2.
|
||
- Observable: the decision and its reason are recorded where a later reader finds them; if out of scope,
|
||
the corpus says so per slice.
|
||
- _Requirements: 7.1, 7.3_
|
||
|
||
- [ ] 4.2 Retire `evidence_level`
|
||
- It duplicates `rung` in intent. Retire it, or record why both are kept. Note the legacy value
|
||
`static_corroboration` comes from the flat-IR overlay, which Req 4.4 of the predecessor demotes to the
|
||
suspicion band — so a straight rename would overstate it.
|
||
- Observable: one vocabulary, or a recorded reason for two; SARIF and JSON consumers considered.
|
||
- _Requirements: 7.2, 7.3_
|
||
|
||
## Group 5 — Grow the corpus
|
||
|
||
**What group 2 established about each of these**, so none is started on a wrong assumption:
|
||
|
||
* **PHP/WordPress**: Joern ships `php2cpg`, so the engine can parse it. The blocker is ours --
|
||
`taint_specs(language="php")` returns **zero families**, as do ruby and go. WordPress would parse and
|
||
produce nothing, which is the quiet failure that reads as a clean bill of health. The work is fact tables
|
||
first, corpus second.
|
||
* **A known-vulnerable old libpng**: measured and it will not work, for a reason the version cannot fix.
|
||
Every file using `strcpy`/`strcat`/`sprintf`/`gets` is under `contrib/`; the library uses none of them and
|
||
overflows through arithmetic instead. See 2.15 and 2.16.
|
||
* **Vibe-code repos**: the best fit available. Python and JavaScript both have full fact tables, the pair
|
||
corpus already carries 70 vibe-py and 17 vfc-js pairs, and vampi -- the one repository where the engine
|
||
finds 3 of 3 -- is exactly this shape. A whole-repository vibe-code checkout is the cheapest next
|
||
measurement that could produce a true positive.
|
||
|
||
- [ ] 5.1 PHP/WordPress slice
|
||
- **Fact tables landed 2026-09-09** (`benchmarks/measurements/2026-09-09-php-facts.json`). Five families
|
||
derive, and the arbiter decides a real PHP CPG 4 of 4 on a probe: SQL concat entails, its escaped twin
|
||
drops to corroborated, `echo` XSS and `system` command injection entail. `php` added to the region
|
||
model's languages. What remains is below.
|
||
- Plugin and core weaknesses. `php2cpg` needs a PHP interpreter, which the image provides. **Validate on a
|
||
sample that the model can read the pairs before vendoring** — the OWASP harvest found 238 and vendored 40
|
||
for exactly this reason.
|
||
- Observable: a sample measurement precedes vendoring; only categories the model can arbitrate are
|
||
vendored; the slice reports as its own row; every pair states a licence.
|
||
- _Requirements: 9.1, 9.4, 9.5, 9.6, 9.7_
|
||
- _Depends: 3.2_
|
||
|
||
- [ ] 5.2 More vibe-code pairs, and at least one as a whole repository
|
||
- The pair corpus has 70 vibe-py and 17 vfc-js pairs, all excerpts. Group 2 showed that excerpts cannot
|
||
see the defects a repository shows: nine engine bugs this session, none of them visible to the corpus,
|
||
the gates byte-identical through every fix.
|
||
- So the higher-value half of this task is a **pinned vibe-code repository**, not more pairs. It is the
|
||
cheapest measurement that could produce a second true-positive checkout, because Python and JavaScript
|
||
are the two languages whose fact tables are complete.
|
||
- Observable: a `benchmarks/repos/` recipe for a vibe-code checkout with in-scope known vulnerabilities,
|
||
and a measurement saying which were found at which rung.
|
||
- _Requirements: 9.1, 9.2, 4.1_
|
||
- The slice where the stated audience works, currently 35 pairs. Same read-check discipline.
|
||
- _Requirements: 9.3, 9.4, 9.5, 9.6, 9.7_
|
||
|
||
- [ ] 5.3 Real-world C library CVEs
|
||
- libpng and the image libraries, OpenSSH, libpam, kernel modules. **Gated on 4.1**: until the memory
|
||
decision closes these would join 173 idle pairs.
|
||
- _Requirements: 9.2, 9.4, 9.5, 9.6_
|
||
- _Depends: 4.1_
|
||
|
||
- [x] 5.4 WordPress entry points, and the one-verdict-per-region limit
|
||
- Two things stand between the PHP facts and a WordPress result, and the second was found by running the
|
||
first.
|
||
- **Entry points.** WordPress registers handlers with `add_action`, `add_filter`, admin-ajax and REST
|
||
routes. Nothing maps them, so every PHP file becomes one file-level region. The mapper already handles
|
||
registration-based entry points for other frameworks (predecessor task 2.6), so this is a new recogniser
|
||
rather than a new mechanism.
|
||
- **One verdict per region per family.** The taint arbiter returns a single verdict for a region and
|
||
family, so a file-level region reports at most one injection finding however many the file holds: on the
|
||
probe, `system` won on flow length and the SQL injection went unreported. This is the mirror of 2.9 --
|
||
that is many regions collapsing to one site, this is many sites collapsing to one verdict -- and it bites
|
||
hardest where PHP needs it not to.
|
||
- Observable: the probe reports 3 of 3, and a WordPress checkout yields function-scoped regions rather than
|
||
one region per file.
|
||
- _Requirements: 2.1, 8.1, 9.1_
|
||
- _Depends: 5.1_
|
||
|
||
- [ ] 5.5 php2cpg emits a CPG containing one file
|
||
- **Found by 5.4 and it blocks the WordPress target.** A CPG built over two PHP files contains only one,
|
||
and the survivor is the last alphabetically. Reproduced twice on independent samples; both files parse
|
||
cleanly under `php -l` and under PHP-Parser directly, and `joern-parse` reports success either way.
|
||
- php2cpg logs `Found warning in PHP-Parser JSON output` naming the surviving file, and that warning is
|
||
PHP-Parser's own `====> File X:` banner appearing in what php2cpg expects to be pure JSON. That is the
|
||
thread to pull.
|
||
- Not yet isolated: this machine has no PHP interpreter, so php2cpg was driven through a container shim.
|
||
The banner comes from PHP-Parser rather than from the shim, which makes a shim-only cause unlikely but
|
||
not excluded. **Rerun with the native php-cli in the shipped image first.**
|
||
- A plugin is many files. A scan that silently covers one of them and reports success is exactly the quiet
|
||
failure Req 5.6 was approved for.
|
||
- Observable: a CPG over N PHP files contains N files, or a recorded reason it cannot with the version and
|
||
the reproduction.
|
||
- _Requirements: 4.3, 5.6, 9.1_
|
||
- _Depends: 5.4_
|
||
|
||
## Group 6 — Regression baselines
|
||
|
||
- [ ] 6.1 Repository baseline and delta
|
||
- A committed baseline per pinned checkout; a delta report on fact-table or query changes showing findings
|
||
gained, lost and rung movements. An unexplained loss of a previously-entailed finding is a regression to
|
||
investigate, not a number to accept. The baseline records the **engine version**, because a CPG engine
|
||
change moves every verdict.
|
||
- _Requirements: 10.1, 10.2, 10.3, 10.4_
|
||
- _Depends: 2.2_
|
||
|
||
## Task-plan review notes
|
||
|
||
- Group 1 must land and pass review before group 2: measuring an unwired pipeline is what produced a
|
||
session's worth of harness-only numbers.
|
||
- Task 1.4 was added after 1.3 was wired, when a ten-line scan took three minutes. It belongs in group 1
|
||
because group 1's promise is a working scan, and a scan that cannot finish on a real repository is not one.
|
||
Finding it here rather than in 2.2 is the argument for wiring before measuring.
|
||
- **Task 2.2 is a go/no-go.** If one CPG per repository is infeasible, stop and redesign the region/budget
|
||
model rather than building groups 3–6 on an assumption.
|
||
- Group 5 is deliberately last. The corpus is not the binding constraint today — reachability is — and 198 of
|
||
238 OWASP pairs were left unvendored precisely because volume without readability makes every number worse.
|
||
|
||
---
|
||
|
||
## Lifted out of this spec (2026-09-09)
|
||
|
||
Two groups drafted here -- a counter-example ledger, and per-repository evolutionary prompting so developers
|
||
can commit dismissals and evolve the harness -- were appended directly to this file without passing
|
||
requirements or design. That was a workflow error and they are a separate feature, not a refinement of scan
|
||
integration. They now live in `.kiro/specs/finding-feedback-loop/`, at brief, unapproved.
|
||
|
||
Three requirements amendments this group argues for are drafted at the end of `requirements.md`, also
|
||
unapproved: what a report may repeat, a family stating a class it cannot decide, and Req 4.1 requiring a
|
||
checkout that can actually demonstrate detection.
|