mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 20:49:28 +00:00
Regex inventory stays the proposer. MAP attaches promote/demote/unadjudicated/coverage from intra-file taint (tree-sitter when a grammar parses, Python ast as last resort). Deep sandboxes promotions only, ordered by the existing heuristic ranker plus optional OpenRouter mechanism recall. The SAST pair slice scores overlay outcomes; the stage-1 detection gate and local pair gate stay inventory-only.
350 lines
16 KiB
Markdown
350 lines
16 KiB
Markdown
# Design Document
|
||
|
||
## Overview
|
||
|
||
**Purpose.** This feature makes the three-stage scan *mean* three jobs: pattern inventory **proposes** sinks; a flow overlay **adjudicates** those proposals on real files; the sandbox **proves** only what was promoted. Operators scanning a working tree they are writing get honest `unadjudicated` instead of fake cleanliness.
|
||
|
||
**Users.** Developers get `quick` as cheap inventory on WIP code. AppSec gets `standard` dispositions (promote / demote / unadjudicated / coverage). DevOps gets `deep` budget spent on promoted reachable sinks.
|
||
|
||
**Impact.** Today `quick_scan_findings` is the de facto decision. After this spec, inventory is necessary but not sufficient. MAP writes overlay artifacts. REGRESS candidate selection reads promotions, not demotions.
|
||
|
||
### Goals
|
||
|
||
- Map every current scan module onto propose, adjudicate, budget, or prove — rank/embed is not a verdict.
|
||
- Treat messy unlabeled trees as the default; labeled corpora as calibration.
|
||
- Fail closed: cannot parse or cannot follow flow → `unadjudicated`, not demote.
|
||
- **Prioritize the semantic engine now**: tree-sitter IR for in-scope languages; Joern optional CPG sidecar; stdlib Python parse only as last resort.
|
||
- Tunable embeddings + existing ranker order **prove** candidates.
|
||
- SAST pair slice scores the overlay; smoke gate stays inventory-only.
|
||
|
||
### Non-Goals
|
||
|
||
- Replacing pattern rules or `LANGUAGE_MANIFESTS`.
|
||
- LLM-as-adjudicator or LLM-as-parser.
|
||
- Requiring Joern or an embedding API for `quick`.
|
||
- Using retrieval similarity to drop promotions.
|
||
- Auto-authoring source/sink/sanitizer facts from `ousast improve`.
|
||
- Buffer-size / memory-safety semantics (Juliet CWE-121 goodG2B stays a known leak until a later spec).
|
||
|
||
## Boundary Commitments
|
||
|
||
### This Spec Owns
|
||
|
||
- Overlay dispositions and flow-fact records.
|
||
- Source / sink / sanitizer **facts as data** (not CWE severity).
|
||
- Tree-sitter probe/runner (CLI-first, like Docker) and last-resort Python stdlib parse.
|
||
- Optional Joern probe/runner (CLI sidecar).
|
||
- Intra-file taint on the tree-sitter IR.
|
||
- Prove-budget: heuristic rank + OpenRouter embeddings over the mechanism log.
|
||
- Calibration scoring of overlay on the sast pair slice; unlabeled-scan honesty.
|
||
|
||
### Out of Boundary
|
||
|
||
- Pattern text, rule `enabled|shadow|disabled`, CWE TSV, project score formula (`three-stage-scan` / ruleset / policy).
|
||
- Complexity-map identity keys, Docker sandbox flags, hunter suspicion contract (`three-stage-scan`).
|
||
- Pair catalog schema except scoring **which** findings count (overlay vs inventory).
|
||
- External analyzer execution (SARIF ingest remains a proposal input).
|
||
- MCP exposing `deep`.
|
||
|
||
### Allowed Dependencies
|
||
|
||
- Preprocess, mapping, rank (`heuristic_rank` 0.5/0.2/0.3), `quick_scan_findings`, complexity map, `ousast index` / json-local vectorstore, tool hunter, sandbox runner, pair eval, reports.
|
||
- Host binaries when present: `tree-sitter`, `joern` / `joern-parse` (probed, never required for inventory).
|
||
- Optional embedding model from existing `[embeddings]` config.
|
||
- Clearwing as an **oracle** for tree-sitter callgraph + taint-pattern tables + mechanism retrieval — not a dependency.
|
||
|
||
### Revalidation Triggers
|
||
|
||
- Adding a fourth disposition that implies “safe.”
|
||
- Demoting when flow is incomplete.
|
||
- Stamping overlay above `static_corroboration`.
|
||
- Selecting sandbox candidates from demoted or unadjudicated sets.
|
||
- Demoting because retrieval score is low.
|
||
- Folding overlay Youden into `python -m openultrasast.gate`.
|
||
- Making tree-sitter or Joern a hard dependency of `quick`.
|
||
|
||
## Architecture
|
||
|
||
### Existing Architecture Analysis
|
||
|
||
Concrete mapping of **what exists today** onto the three roles:
|
||
|
||
| Role | Stage / mode | Module today | What it actually does | After this spec |
|
||
|------|----------------|--------------|------------------------|-----------------|
|
||
| Propose | STATIC / `quick` | `findings.quick_scan_findings` | Pattern rules decide the report | **Proposal list only** |
|
||
| Propose | STATIC | `mapping.ingest_sarif` | Operator SARIF | Still proposals |
|
||
| Propose | STATIC | `mapping.analyze_entry_points` | Routes / CLI | **Sources** for overlay |
|
||
| Budget | STATIC | `rank.heuristic_rank` 0.5/0.2/0.3 | File budget | Same formula; later **prove order** |
|
||
| Budget | `ousast index` | `vectorstore` json-local | Hunter retrieval | **Prove retrieval** on promotions |
|
||
| Adjudicate? | MAP | `complexity.map` | Hotspot band | Attention only |
|
||
| Adjudicate | MAP | **new** tree-sitter IR + taint | — | **Primary semantic engine** |
|
||
| Adjudicate | MAP | **new** Joern CLI sidecar | — | Optional CPG facts; never required |
|
||
| Adjudicate fallback | MAP | stdlib `ast` | Routes only today | Python last resort if tree-sitter missing |
|
||
| Suspicion | MAP | `tool_hunter` | LLM + grep | May query overlay; never adjudicate |
|
||
| Prove | REGRESS / `deep` | `regress` + sandbox | Recipes on ranked findings | Recipes on **promoted then budgeted** findings |
|
||
| Calibrate | CI | `gate.py` / `pairs` | Cheat-sheet vs Youden | Inventory vs overlay, unchanged split |
|
||
|
||
**Selected pattern.** Propose → adjudicate → **budget** → prove. Clearwing’s tree-sitter callgraph + taint tables + mechanism memory are the oracle, not a library import. Embeddings never demote.
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph propose["PROPOSE inventory"]
|
||
PRE[preprocess]
|
||
EP[entry points as sources]
|
||
RX[pattern inventory]
|
||
SARIF[optional SARIF]
|
||
end
|
||
subgraph adjudicate["ADJUDICATE overlay"]
|
||
TS[tree-sitter IR]
|
||
JOERN[optional Joern CPG]
|
||
FALLBACK[Python stdlib parse last resort]
|
||
FACTS[source sink sanitizer facts]
|
||
TAINT[intra-file taint]
|
||
DISP[promote demote unadjudicated coverage]
|
||
end
|
||
subgraph budget["BUDGET not a verdict"]
|
||
RANK[heuristic and optional model ranker]
|
||
VS[json-local embeddings]
|
||
end
|
||
subgraph prove["PROVE sandbox"]
|
||
CAND[promoted reachable then budgeted]
|
||
BOX[existing Docker runner]
|
||
VERD[triggerable or not]
|
||
end
|
||
PRE --> EP
|
||
PRE --> RX
|
||
RX --> DISP
|
||
SARIF --> DISP
|
||
EP --> TAINT
|
||
TS --> TAINT
|
||
JOERN --> TAINT
|
||
FALLBACK --> TAINT
|
||
FACTS --> TAINT
|
||
TAINT --> DISP
|
||
DISP --> RANK
|
||
DISP --> VS
|
||
RANK --> CAND
|
||
VS --> CAND
|
||
CAND --> BOX
|
||
BOX --> VERD
|
||
```
|
||
|
||
### Technology Stack
|
||
|
||
| Layer | Choice | Role |
|
||
|-------|--------|------|
|
||
| CLI / modes | existing `quick` `standard` `deep` | Role gating |
|
||
| Propose | pattern rules + SARIF | Inventory |
|
||
| Semantic IR | `tree-sitter` CLI or API extra; probe like Docker | Primary adjudicator parse |
|
||
| Semantic CPG | Joern CLI when present | Optional inter-file facts |
|
||
| Fallback parse | stdlib `ast` | Python only, last resort |
|
||
| Facts | TOML next to the ruleset | Sources sinks sanitizers |
|
||
| Budget | existing ranker + json-local vectorstore | Prove order, tunable |
|
||
| Prove | existing Docker CLI sandbox | Unchanged runner |
|
||
|
||
`quick` stays zero-dep. `standard`/`deep` degrade to unadjudicated when parsers are missing.
|
||
|
||
## File Structure Plan
|
||
|
||
```
|
||
src/openultrasast/semantic/
|
||
__init__.py
|
||
facts.py # source sink sanitizer facts
|
||
engines.py # probe tree-sitter, joern, ast fallback
|
||
ir.py # FileIR from the first engine that works
|
||
taint.py # worklist; incomplete -> Unadjudicated
|
||
overlay.py # dispositions
|
||
prove_budget.py # rank + optional retrieval over promotions
|
||
src/openultrasast/ruleset/semantic/
|
||
python.toml javascript.toml c.toml java.toml
|
||
```
|
||
|
||
### Modified Files
|
||
|
||
- `stages.py` / `cli.py` — overlay in MAP; prove-budget before regress.
|
||
- `findings.py` — inventory unchanged.
|
||
- `regress/candidate.py` — `promote` only, then budget order.
|
||
- `rank.py` / `index.py` / `vectorstore.py` — reuse; do not invent a second store.
|
||
- `reports.py` — disposition columns.
|
||
- `pairs.py` — sast slice uses overlay.
|
||
- `mcp.py` — still no `deep`.
|
||
|
||
## System Flows
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> Proposed
|
||
Proposed --> Unadjudicated: parse fail or flow incomplete or language unsupported
|
||
Proposed --> Promoted: source reaches sink without sanitizer
|
||
Proposed --> Demoted: constant or sanitizer dominates
|
||
Proposed --> Coverage: flow to uninventoried sink
|
||
Promoted --> Proven: sandbox triggerable
|
||
Promoted --> Unproven: sandbox skip or unavailable
|
||
Unadjudicated --> [*]
|
||
Demoted --> [*]
|
||
Coverage --> Promoted: same flow rule
|
||
```
|
||
|
||
**Key decisions**
|
||
|
||
- Demote only with a positive reason (constant or sanitizer on that path).
|
||
- Unadjudicated is the default for messy real files.
|
||
- Coverage findings enter the same state machine as proposals.
|
||
|
||
### Unlabeled working-tree flow
|
||
|
||
No expected.toml. Success is: every inventory item has a disposition; unadjudicated reasons are explicit; `deep` does not execute demoted sinks; `--fail-on worth-fixing` ignores unadjudicated.
|
||
|
||
## Requirements Traceability
|
||
|
||
| Requirement | Summary | Components | Flows |
|
||
|-------------|---------|------------|-------|
|
||
| 1.1–1.5 | Role split by mode | Stage plan, overlay, prove budget, regress | propose → adjudicate → budget → prove |
|
||
| 8.1–8.6 | Semantic engines | engines.py, ir.py, taint.py | tree-sitter then Joern then ast |
|
||
| 9.1–9.5 | Prove budget | prove_budget.py, rank, vectorstore | retrieval never demotes |
|
||
| 2.1–2.5 | Messy trees, unadjudicated | IR fail, taint incomplete | unlabeled scan |
|
||
| 3.1–3.5 | Promote demote coverage | taint, overlay | state diagram |
|
||
| 4.1–4.5 | Calibration vs product | gate.py, pairs.py | sast overlay score |
|
||
| 5.1–5.5 | Evidence honesty | overlay evidence, hunter | ladder |
|
||
| 6.1–6.5 | Prove promoted only | candidate filter, sandbox | deep |
|
||
| 7.1–7.5 | Artifacts and fail-on | reports, overlay.json | operator |
|
||
|
||
## Components and Interfaces
|
||
|
||
| Component | Domain | Intent | Req Coverage | Dependencies | Contracts |
|
||
|-----------|--------|--------|--------------|--------------|-----------|
|
||
| InventoryProposer | Propose | Existing pattern + SARIF list | 1.1 | findings, mapping | State |
|
||
| FactStore | Adjudicate | Sources sinks sanitizers as data | 3, 5.5 | ruleset/semantic | State |
|
||
| FileIR | Adjudicate | Parse one file or fail | 2.1 | ast | Service |
|
||
| TaintEngine | Adjudicate | Intra-file paths or incomplete | 2.2, 3 | FileIR, FactStore | Service |
|
||
| Overlay | Adjudicate | Dispositions on proposals | 1.2, 3, 5 | TaintEngine | State |
|
||
| ProveFilter | Prove | Candidates from promote | 1.3, 6 | Overlay, regress | Service |
|
||
| OverlayPairEval | Calibrate | SAST slice uses dispositions | 4 | Overlay, pairs | Batch |
|
||
|
||
### Adjudicate layer
|
||
|
||
#### OverlayRecord
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | One disposition per proposal or coverage item |
|
||
| Requirements | 3.5, 7.1 |
|
||
|
||
**State contract**
|
||
|
||
```
|
||
proposal_id: str
|
||
path: str
|
||
line: int | None
|
||
disposition: promote | demote | unadjudicated | coverage
|
||
reason: str
|
||
cwe: str
|
||
sources: list[str]
|
||
sinks: list[str]
|
||
sanitizers: list[str]
|
||
evidence_level: static_corroboration | suspicion
|
||
```
|
||
|
||
**Invariants**
|
||
|
||
- `demote` requires a recorded constant or sanitizer.
|
||
- `unadjudicated` requires a reason in {`parse_failed`, `language_unsupported`, `flow_incomplete`, `facts_unavailable`}.
|
||
- `promote` and `coverage` do not exceed `static_corroboration`.
|
||
|
||
#### TaintEngine service
|
||
|
||
```
|
||
analyze_file(path, text, language, facts) -> FileFlow | Unadjudicated
|
||
adjudicate(proposals, flows, facts) -> list[OverlayRecord]
|
||
```
|
||
|
||
- Engine order: tree-sitter grammar → Joern file export if probed and useful → Python stdlib parse → unadjudicated.
|
||
- Joern may add paths; it must not demote a tree-sitter promote, and a Joern miss must not demote.
|
||
- Postconditions: every inventory proposal appears exactly once; extras are `coverage` only.
|
||
|
||
### Prove layer
|
||
|
||
#### ProveFilter
|
||
|
||
Inbound: overlay + reachability from mapping. Outbound: existing `regress` candidates.
|
||
|
||
- Drop `demote`.
|
||
- Do not sandbox `unadjudicated` as a negative proof.
|
||
- Keep `promote` that are reachable or inferred-file-surface.
|
||
- Order survivors with heuristic rank, then **mechanism recall**: hard filter (language, CWE, tags) then OpenRouter dense cosine on the mechanism text. Low similarity never drops a promotion.
|
||
- Write mechanisms only after prove/triggerable. JSONL log is authoritative; json-local `mechanisms` namespace is a rebuildable embedding cache via `OpenRouterEmbeddingClient`.
|
||
|
||
## Data Models
|
||
|
||
**Facts (data, versioned with the ruleset)**
|
||
|
||
- `source`: identifier (e.g. request parameter, header, stdin, argv)
|
||
- `sink`: call shape + CWE (aligned to existing rule ids when possible)
|
||
- `sanitizer`: call shape that stops taint (parameterized execute, `printf` with literal format, `safe_load`, list-form subprocess)
|
||
|
||
**FileFlow**
|
||
|
||
- assignments and call arguments in one function
|
||
- one-hop name copies (`bar = param`, `sql = f"...{bar}"`)
|
||
- no types, no inter-file, no may-alias heap in v1
|
||
|
||
**Mechanism (Clearwing reference architecture)**
|
||
|
||
Identity is the *pattern*, not a file slice:
|
||
|
||
| Field | Role |
|
||
|-------|------|
|
||
| id | stable uuid |
|
||
| summary | one sentence, no path/line |
|
||
| cwe, language | filters |
|
||
| tags, keywords | hard filters, not a ranker |
|
||
| what_made_it_exploitable | why a near-miss became a bug |
|
||
| source_finding_id, source_repo | provenance only |
|
||
|
||
Persistence: append-only JSONL (portable). Recall is **not** Clearwing’s TF-IDF ladder.
|
||
|
||
1. **Filter** — language, CWE, tags (exact overlap). This is a hard prefilter, not a ranker.
|
||
2. **Rank** — OpenRouter `/embeddings` (`OpenRouterEmbeddingClient`, existing `[embeddings].model`) over `summary + what_made_it_exploitable + tags`. Cosine against the promotion’s overlay text. Cache vectors in json-local `mechanisms` namespace, keyed by mechanism id + model id.
|
||
3. **Degrade** — no key / no model / embed failure → heuristic ranker only, degradation recorded. Never bag-of-words cosine.
|
||
|
||
Query for a promotion: embed the overlay rationale (sources, sink, CWE). Result is a **boost to prove order**, never a demote.
|
||
|
||
Repo-code chunks (`ousast index` 80-line slices) stay for hunter/verifier retrieval. They are **not** the prove-budget store. Textbook fixtures must not fill the mechanism log unless they were actually proven in the sandbox.
|
||
|
||
## Error Handling
|
||
|
||
| Condition | Disposition | Operator sees |
|
||
|-----------|-------------|---------------|
|
||
| Syntax error / parse fail | unadjudicated parse_failed | proposal kept, marked unresolved |
|
||
| No tree-sitter grammar and not Python | unadjudicated language_unsupported | same |
|
||
| Joern missing | ignore sidecar | standard still overlays via tree-sitter |
|
||
| Embedding model missing | heuristic prove order | deep still runs |
|
||
| Callee in another file | unadjudicated flow_incomplete | same |
|
||
| Fact file missing | unadjudicated facts_unavailable; `quick` still emits inventory | degradation in manifest |
|
||
| Sandbox missing | promoted remains unproven | `sandbox_unavailable` |
|
||
|
||
## Testing Strategy
|
||
|
||
- Unit: parse fail → unadjudicated; constant `exec("ls")` → demote; `eval(request.args)` → promote; `sql = f"…{x}"; execute(sql)` → coverage or promote.
|
||
- Pair: `--slice sast` uses overlay; `juliet-c-cwe134-printf` stays PASS; `owasp-python-deser` stays PASS; Juliet `goodG2B` exec/strcpy must **demote or unadjudicated**, not inventory-leak counted as pair fail for overlay.
|
||
- Unlabeled: tmp tree with a syntax-error file next to a clean `eval(request.args)` — error file unadjudicated, clean file promoted; no expected.toml required.
|
||
- Gate: `python -m openultrasast.gate` still inventory-only.
|
||
- Deep: candidate list excludes demoted.
|
||
|
||
## Security
|
||
|
||
- Overlay does not execute target code.
|
||
- Facts cannot override CWE severity.
|
||
- Hunter still cannot stamp corroboration.
|
||
|
||
## Performance
|
||
|
||
- Overlay is per-file, MAP stage, same file budget as rank.
|
||
- Skip files already unreadable at preprocess.
|
||
|
||
## Migration
|
||
|
||
- Default `standard` runs overlay when facts load; if facts missing, degrade as Requirement 7.5.
|
||
- Existing `quick` CI unchanged.
|
||
- Pair gate local 100% remains inventory-or-overlay-equivalent on fixture counterparts (those files are still “nice”; unlabeled tests are additional).
|