mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 20:49:28 +00:00
Ran the humanizer rules over 79 prose files in the active specs (brief, design, requirements, research and narrative docs). Removed em dashes, significance inflation, forced rule-of-three, bold-label list items and filler. Skipped tasks.md checklists and the retired specs (harnessx-*, openrouter-sast-harness, unseen-repo-evaluation). Every number, path, command, hash and identifier was preserved: checked each file against HEAD for drift in numbers, inline code and code fences, zero drift across all 79 files. Co-Authored-By: Claude Opus 4.8 <[email protected]>
140 lines
8.8 KiB
Markdown
140 lines
8.8 KiB
Markdown
# Brief: contributor-scan, make the model layer a tool someone runs
|
||
|
||
**Status:** brief only, unapproved. Successor to `model-grounded-detection`, which is complete (16/16 tasks)
|
||
and is not reopened by this.
|
||
|
||
## The gap this exists to close
|
||
|
||
`model-grounded-detection` built and measured an arbiter. It works: 41.3% of the corpus reaches
|
||
`model_entailed` against the flat IR's 2.2%, and the wired pipeline scores 86.7% pair-correct at 6.7% leak
|
||
against the LLM hunter's 76.7% / 12.3%, at a fifth of the model calls and ~100× cheaper.
|
||
|
||
**And none of it is reachable by a user.** `ousast scan` still runs
|
||
`preprocess → map → overlay → obligations → regress → report` and touches the model layer nowhere;
|
||
`model/pipeline`, `judge`, `config_value`, `report`, `calibrate` and `execution` have zero production callers
|
||
outside `model/`. Every number quoted above came from a measurement harness in a scratch directory.
|
||
|
||
That is a scope gap in the predecessor spec, not an oversight in its execution: it had no task saying *make
|
||
this reachable*, and its Req 11.2 (the gates stay byte-identical) is satisfied precisely **because** nothing
|
||
is wired. The spec is complete and the feature is unshipped, both at once.
|
||
|
||
## What the goal actually is
|
||
|
||
> "a real world tool that helps developers when they want to contribute"
|
||
|
||
That reframes the work. A contributor does not have a labelled function and a known weakness class; they
|
||
have a repository they are about to touch and a question about what is risky in it. Everything measured so
|
||
far is function-level pairs: a labelled function, a declared family, a known target. None of it exercises the
|
||
questions a whole repository asks.
|
||
|
||
## What is missing, in the order it bites
|
||
|
||
1. **Scan integration.** Wire `model/pipeline` into `ousast scan` so a finding with a rung reaches a report.
|
||
Without this nothing else matters.
|
||
2. **Whole-repo scanning.** Region selection from entry points instead of a labelled function; ranking across
|
||
thousands of candidates rather than one; a budget policy (the enumerator caps at 8 candidates per region,
|
||
which is meaningless at repo scale); and the performance envelope: a CPG build on a 100k-line tree, not a
|
||
40-line excerpt. **This has never been run.**
|
||
3. **A known-vulnerable repository as the target.** The 176 real-world CVE pairs already in the corpus are
|
||
single-function excerpts and do not exercise any of the above. Running against a real checkout (curl,
|
||
libpng, a WordPress plugin) is the only way to find what breaks at scale.
|
||
4. **The memory decision.** 173 of those 176 pairs are `memory` family, routed to a deferred execution tier
|
||
that is a stub. Either adopt Clearwing for them or scope C memory-safety out explicitly. Right now it is
|
||
neither, and a sixth of the corpus sits idle.
|
||
5. **Retire `evidence_level`.** It duplicates `rung` in intent; noted as a follow-up in group 3 and never
|
||
tracked.
|
||
|
||
## Already built and unrecorded
|
||
|
||
These were delivered during `model-grounded-detection` without being in any spec. They belong here so the
|
||
record is not scattered:
|
||
|
||
- The pipeline itself (`model/pipeline.py`): Req 8 existed, task 3.2 built `judge()`, and nothing ever
|
||
said to connect an enumerator to it. Built, tested, measured, unspecified.
|
||
- Ops: a single image carrying tool and engine, a compose file with read-only target and tmpfs scratch,
|
||
bash/zsh/PowerShell wrappers that make the container invisible, and a pyinfra deploy for host installs.
|
||
The Docker path is written and syntax-validated but **the image has never been built**.
|
||
- Corpus: the `owasp` slice (40 validated xss pairs; `output_encoding` went from 1 pair to 41) and
|
||
authored sink/sanitizer facts for Java and JavaScript.
|
||
|
||
## Known limits to carry forward
|
||
|
||
- `permissive_values` is a flat literal list with no keyword association, so it holds both `True` and `False`.
|
||
`debug=True` is dangerous where `httponly=True` is safe and the model cannot tell them apart.
|
||
- Interprocedural resolution is the most repeated miss cause across every experiment: OWASP's helper classes,
|
||
`angular-http-server`'s `resolveUrl`. Joern supports it; our queries may not be configured for it.
|
||
- Five bugs in `model-grounded-detection` shared one shape: a taint-specific assumption applied to every
|
||
family, each found by a measurement rather than a check. Any new family-specific path should be audited for
|
||
siblings before it is measured, not after.
|
||
|
||
## What the evidence says so far (appended 2026-09-09, after group 2)
|
||
|
||
This is a research project whose findings get taught, so a negative result is a deliverable. But the standing
|
||
test the work has to pass is blunter than that:
|
||
|
||
> "it must find the bugs, otherwise it's SAST noise. most devs know codeql / semgrep have dataflow
|
||
> capabilities but never use it"
|
||
|
||
That sentence settles what the differentiator is, and it is **not the dataflow engine**. CodeQL and Semgrep
|
||
already have one and developers do not turn it on. What they will not do is write queries, tune a ruleset, or
|
||
read two hundred rows to find the one that matters. So the product is the **rung** -- a claim the graph
|
||
decided, reported without configuration -- and everything below `model_entailed` is on trial.
|
||
|
||
### The scoreboard, honestly
|
||
|
||
| checkout | known in-scope | found | rung | false positives |
|
||
|---|---|---|---|---|
|
||
| vampi (python, 520 lines) | 3 | **3** | all `model_entailed` | 2, both endpoints the OpenAPI spec declares public |
|
||
| libpng (c, 89k lines) | 0 (its CVE is out of family) | — | — | 30 over the session, now 1 |
|
||
|
||
vampi is the result worth teaching: SQL injection across two modules, a broken object-level authorization,
|
||
and a second BOLA **the engine found before it was in the recipe**. libpng is the result worth being honest
|
||
about: 198 rows, zero of them valid, and the reason is structural rather than a tuning problem -- the library
|
||
never calls a sink the `c/memory` family models.
|
||
|
||
### What the session actually demonstrated
|
||
|
||
Nine defects, each found by a measurement and none by review. Three of them were one bug class in three
|
||
matchers (`resolve` in `resolveUrl`, `user` in `users`, `gets` in `fgets`); two were capability present but
|
||
unreachable (the model layer itself, then `query_batch`); two were a paid budget stopping unpaid work and a
|
||
question scoped to a function when the bug crosses modules.
|
||
|
||
The teachable finding is not any one of them. It is that **a corpus of function-level pairs cannot see any of
|
||
them**, and every one appeared within minutes of pointing the tool at a real repository. The gates stayed
|
||
byte-identical through all nine fixes, which is the same statement from the other side.
|
||
|
||
### What this changes about priorities
|
||
|
||
* **2.11 (the suspicion band) is the critical path**, not a cleanup task. libpng produced 163 rows that
|
||
assert an API is present rather than misused. That is precisely the noise the quote above is about, and no
|
||
amount of arbiter accuracy survives shipping it.
|
||
* **A family that cannot decide its class must say so** (2.16). A silent C scan must not read as a clean bill
|
||
of health; that is worse than noise because it is quiet.
|
||
* **Negative results get recorded as measurements, not as failures.** `c/memory` scoring zero on libpng is a
|
||
finding about the abstraction, and the artifact says which abstraction would reach it (size arithmetic,
|
||
i.e. a constraint problem) rather than leaving it as a gap to be tuned at.
|
||
|
||
### The final objective, and what it needs
|
||
|
||
> "that + an LLM way of diving in without being forced to rethink the code"
|
||
|
||
Two halves, and they are complementary. The **rung** says *this was decided*; the second half says the
|
||
developer must not have to re-derive *why*. The material for that already exists and is called the
|
||
**witness**: `username -> db.session.execute(text(user_query)) in models/user_model.py (line 73, 8 steps)` is
|
||
the reasoning, already written down, already attached to the finding.
|
||
|
||
So the objective is largely a plumbing question, and the session's recurring defect shape applies to it.
|
||
Checked rather than assumed: `openultrasast.get_finding` returns the whole record including the witness, but
|
||
`openultrasast.findings` was dropping the rung -- so an LLM listing libpng's 198 rows could not tell the one
|
||
the graph decided from the 197 an enumerator proposed without fetching all 198 individually. Fixed: the list
|
||
carries the rung, leads with what was decided, and returns a per-rung tally.
|
||
|
||
What still stands between here and the objective:
|
||
|
||
* **The witness is one line.** It names the source, the sink, the file and the step count, but not the steps.
|
||
For "dive in without rethinking", the path elements are the thing worth having, and the query already walks
|
||
them (`flow.elements`) before discarding all but the first.
|
||
* **The suspicion band drowns the signal** (2.11). An LLM given 163 "this API is present" rows will reason
|
||
about them, at length, and that is worse than a human ignoring them.
|
||
* **A family that cannot decide must say so** (2.16), or the LLM will confidently explain a silence.
|