mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
Add openultrasast[semantic] with official grammar wheels and one CST walker into FileIR. quick stays extra-free. Host CLI is not overlay-ready. Default CI stays extra-free; a semantic job runs pytest -m semantic.
273 lines
12 KiB
Markdown
273 lines
12 KiB
Markdown
# Design Document
|
||
|
||
## Overview
|
||
|
||
**Purpose.** This feature turns on a real structured parse for overlay adjudication by installing an optional extra. AppSec scans of JavaScript, C/C++, Java, and Python then get `FileIR` from official tree-sitter grammars instead of `language_unsupported`.
|
||
|
||
**Users.** Default/CI installs keep `quick` and extra-free pytest. Operators who install `openultrasast[semantic]` get overlay promote/demote on the in-scope languages.
|
||
|
||
**Impact.** `parse_file` prefers extra grammars when they load. The dead CLI parse-success path is removed. Taint, facts, dispositions, sandbox, and the detection gate do not change.
|
||
|
||
### Goals
|
||
|
||
- Optional extra with official grammar wheels; core `dependencies` stay empty.
|
||
- One CST walker into existing `FileIR` (`Bind`, `CallSite`, `FunctionIR`).
|
||
- Fail closed: missing extra, missing grammar, or error tree → `unadjudicated`.
|
||
- Dual test matrix: extra-free always; extra-on JS plus C or Java.
|
||
|
||
### Non-Goals
|
||
|
||
- Joern queries, inter-file taint, CWE-121 buffer semantics, pair-catalog harvest, LLM parse.
|
||
- New overlay dispositions or evidence rungs.
|
||
- Requiring the extra for `quick` or for importing `openultrasast`.
|
||
|
||
## Boundary Commitments
|
||
|
||
### This Spec Owns
|
||
|
||
- Packaging extra `semantic` and lazy grammar load.
|
||
- CST → `FileIR` projection and `parse_file` dispatch order.
|
||
- Probe semantics: extra/grammar vs host CLI.
|
||
- pytest `semantic` marker and CI extra-on job.
|
||
|
||
### Out of Boundary
|
||
|
||
- Overlay dispositions, fact TOML, taint worklist (`propose-adjudicate-prove`).
|
||
- Stage plan, sandbox, hunter suspicion (`three-stage-scan`).
|
||
- Pair catalog slices (`real-world-vfc-slice`).
|
||
- CWE policy, pattern rules, MCP `deep`.
|
||
|
||
### Allowed Dependencies
|
||
|
||
- Existing `FileIR` types and `parse_file` callers (`taint.py`, `overlay.py`).
|
||
- `harness_ext` capability-guard pattern (find_spec, no import at module load).
|
||
- External: PyPI `tree-sitter` and grammar packages listed in Technology Stack (extra only).
|
||
|
||
### Revalidation Triggers
|
||
|
||
- Changing `FileIR` / `Bind` / `CallSite` fields.
|
||
- Importing `tree_sitter` at module load of `cli` or `semantic/__init__`.
|
||
- Adding core (non-extra) parser dependencies.
|
||
- Using host `tree-sitter` CLI output as overlay IR.
|
||
|
||
## Architecture
|
||
|
||
### Existing Architecture Analysis
|
||
|
||
Overlay already calls `parse_file` → taint → dispositions. Python `ast` works. `parse_tree_sitter_cli` always returns `None`. `tree_sitter_available()` is true if a CLI binary exists, which does not imply a grammar.
|
||
|
||
### Architecture Pattern and Boundary Map
|
||
|
||
Selected: **hybrid extra guard + CST walker + existing dispatch.** Same packaging seam as HarnessX. `parse_file` remains the only overlay parse entry.
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
overlay[overlay adjudicate]
|
||
parse[parse_file]
|
||
extra[has_semantic_extra]
|
||
gram[grammar_for language]
|
||
cst[cst walker]
|
||
ast[python ast]
|
||
fail[FileIR language_unsupported or parse_failed]
|
||
taint[taint on FileIR]
|
||
overlay --> parse
|
||
parse --> extra
|
||
extra -->|yes| gram
|
||
gram -->|loaded| cst
|
||
cst -->|has_error| fail
|
||
cst -->|ok| taint
|
||
extra -->|no| ast
|
||
gram -->|missing| ast
|
||
ast -->|python ok| taint
|
||
ast -->|not python| fail
|
||
```
|
||
|
||
**Key decisions**
|
||
|
||
- Extra grammars are the product parse. Host CLI is not a success path.
|
||
- Taint does not import tree-sitter.
|
||
- Dependency direction: extra guard → cst walker → `parse_file` → taint → overlay. No reverse imports.
|
||
|
||
### Technology Stack
|
||
|
||
| Layer | Choice / Version | Role in Feature | Notes |
|
||
|-------|------------------|-----------------|-------|
|
||
| Packaging | extra `semantic` | Optional wheels | Core `dependencies` stay `[]` |
|
||
| Parser runtime | `tree-sitter>=0.23,<0.27` | `Language` + `Parser` | Constructor API, not `build_library` |
|
||
| Grammars | `tree-sitter-python`, `tree-sitter-javascript`, `tree-sitter-c`, `tree-sitter-cpp`, `tree-sitter-java` | Per-language CST | `cpp` for `.cpp`; `c` for `.c` |
|
||
| Tests | pytest marker `semantic` | Skip extra-on tests | Default CI extra-free |
|
||
| Types | mypy override `tree_sitter*` | Extra absent in typecheck env | Same as `harnessx.*` |
|
||
|
||
License of adopted wheels: MIT. Pins are verified at implement time against the selected minor.
|
||
|
||
## File Structure Plan
|
||
|
||
```
|
||
src/openultrasast/semantic/
|
||
extra.py # has_semantic_extra, grammar_for; no tree_sitter import at module load
|
||
cst.py # Node walk → FileIR; node-kind map
|
||
ir.py # parse_file dispatch; keep python ast; remove CLI success path
|
||
engines.py # stop treating host CLI as overlay-ready
|
||
pyproject.toml
|
||
tests/test_semantic_extra.py
|
||
tests/test_semantic_cst.py
|
||
.github/workflows/ci.yml
|
||
```
|
||
|
||
### Modified Files
|
||
|
||
- `pyproject.toml` — extra `semantic`; pytest marker; mypy overrides.
|
||
- `src/openultrasast/semantic/ir.py` — dispatch; delete `parse_tree_sitter_cli` as success path.
|
||
- `src/openultrasast/semantic/engines.py` — `tree_sitter_available` must not mean CLI-only; extra/grammar helpers live in `extra.py`.
|
||
- `src/openultrasast/semantic/__init__.py` — export `has_semantic_extra` only if needed; do not import cst at package load.
|
||
- `tests/test_semantic_overlay.py` — extra-free JS stays `language_unsupported` without monkeypatching a fake IR as “tree-sitter success”.
|
||
- `.github/workflows/ci.yml` — second job with `--extra semantic` running `@pytest.mark.semantic`.
|
||
- `dagger/ci.py` — keep extra-free unless the same extra-on command is cheap to add; default is GHA second job only.
|
||
|
||
Do not modify `taint.py`, `overlay.py`, `facts.py`, `gate.py`, `pairs.py`.
|
||
|
||
## System Flows
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
start[parse_file]
|
||
g{grammar_for language}
|
||
err{root has_error}
|
||
py{language python}
|
||
start --> g
|
||
g -->|yes| err
|
||
err -->|yes| failed[parse_failed]
|
||
err -->|no| ir[FileIR engine tree-sitter]
|
||
g -->|no| py
|
||
py -->|yes| ast[python-ast FileIR]
|
||
py -->|no| uns[language_unsupported]
|
||
```
|
||
|
||
Gating: grammar load failure is missing extra/grammar, not a scan abort. `has_error` is `parse_failed`.
|
||
|
||
## Requirements Traceability
|
||
|
||
| Requirement | Summary | Components | Flows |
|
||
|-------------|---------|------------|-------|
|
||
| 1.1–1.3 | Extra-free quick and import | extra.py, pyproject | no grammar load on quick |
|
||
| 2.1–2.4 | Structured parse; JS promote; C/Java demote; same dispositions | extra.py, cst.py, ir.py, existing taint/overlay | parse_file → taint |
|
||
| 3.1–3.5 | Fail closed; Python ast last resort; missing extra does not fail standard | ir.py, extra.py | unsupported / parse_failed |
|
||
| 4.1–4.3 | One walker; no JS/C visitors; CLI not success | cst.py, ir.py | grammar_for only |
|
||
| 5.1–5.5 | No LLM; Joern not required; corroboration; prove/gate unchanged | none new | existing overlay |
|
||
| 6.1–6.3 | Dual tests | markers, CI job | skip vs extra-on job |
|
||
|
||
## Components and Interfaces
|
||
|
||
| Component | Domain | Intent | Req Coverage | Key Dependencies | Contracts |
|
||
|-----------|--------|--------|--------------|------------------|-----------|
|
||
| SemanticExtra | Packaging | Probe extra and load grammar | 1.1–1.3, 3.1–3.2, 4.3 | PyPI wheels P0 | Service |
|
||
| CstWalker | Parse | CST to FileIR | 2.1–2.3, 3.4, 4.1 | SemanticExtra P0, FileIR P0 | Service |
|
||
| ParseDispatch | Parse | parse_file order | 2.1, 3.3–3.5, 4.3 | CstWalker P0, python ast P0 | Service |
|
||
| ExtraTestMatrix | Validation | skip vs extra-on CI | 6.1–6.3 | pytest P0 | Batch |
|
||
|
||
### Parse layer
|
||
|
||
#### SemanticExtra
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | Capability guard and per-language grammar load |
|
||
| Requirements | 1.2, 1.3, 3.1, 3.2, 4.3 |
|
||
|
||
**Contracts**: Service
|
||
|
||
```python
|
||
def has_semantic_extra() -> bool:
|
||
"""True iff tree_sitter and at least one in-scope grammar module are importable. No eager import of wheels at semantic package load."""
|
||
|
||
def grammar_for(language: str) -> object | None:
|
||
"""Return a tree_sitter.Language for python, javascript, c, cpp, java, or None. Lazy importlib.import_module. Never raises into the scan."""
|
||
```
|
||
|
||
- Preconditions: `language` is a preprocess language id.
|
||
- Postconditions: `None` means overlay must not claim tree-sitter IR.
|
||
- Invariants: host CLI path does not make this return non-None.
|
||
|
||
#### CstWalker
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | One walk from a tree-sitter tree to FileIR |
|
||
| Requirements | 2.1–2.3, 3.4, 4.1 |
|
||
|
||
**Contracts**: Service
|
||
|
||
```python
|
||
def file_ir_from_tree(*, path: str, text: str, language: str, tree: object) -> FileIR:
|
||
"""Project named nodes into Bind, CallSite, FunctionIR. If root.has_error, parse_ok False reason parse_failed. engine is tree-sitter when parse_ok."""
|
||
```
|
||
|
||
Node-kind map (names are grammar types; walker is shared):
|
||
|
||
| Kind | python | javascript | c / cpp | java |
|
||
|------|--------|------------|---------|------|
|
||
| function | function_definition | function_declaration, method_definition, arrow_function | function_definition | method_declaration, constructor_declaration |
|
||
| assign | assignment, augmented_assignment | assignment_expression | assignment_expression | assignment_expression |
|
||
| call | call | call_expression | call_expression | method_invocation |
|
||
| name | identifier | identifier | identifier | identifier |
|
||
| string | string | string | string_literal | string_literal |
|
||
|
||
**Implementation Notes**
|
||
|
||
- Integration: only `parse_file` calls this after `grammar_for`.
|
||
- Validation: JS `eval(req.query.x)` and C `printf("%s\\n", data)` fixtures with extra on.
|
||
- Risks: node-type aliases (JS `assignment_pattern`); table is the extension point, not a new visitor module per language.
|
||
|
||
#### ParseDispatch
|
||
|
||
| Field | Detail |
|
||
|-------|--------|
|
||
| Intent | Existing `parse_file` with extra-first order |
|
||
| Requirements | 2.1, 3.3–3.5, 4.2, 4.3 |
|
||
|
||
Order: grammar_for → cst; else python ast if `language == python`; else `language_unsupported`. Do not call host CLI for overlay IR.
|
||
|
||
## Data Models
|
||
|
||
`FileIR`, `Bind`, `CallSite`, `FunctionIR` stay as in `ir.py`. No new disposition. `engine` values: `tree-sitter` | `python-ast` | `none`.
|
||
|
||
## Error Handling
|
||
|
||
| Condition | Overlay | Scan |
|
||
|-----------|---------|------|
|
||
| Extra not installed | language_unsupported for non-Python | standard succeeds |
|
||
| Grammar missing for language | language_unsupported or Python ast | standard succeeds |
|
||
| root.has_error | parse_failed | standard succeeds |
|
||
| Wheel import error | treat as extra absent | do not raise |
|
||
|
||
## Testing Strategy
|
||
|
||
- Unit extra-free: import `openultrasast.cli` does not import `tree_sitter`; JS `parse_file` → `language_unsupported`; Python ast still promotes `eval(request.args)`; `quick` scan unchanged (1.1–1.3, 3.1, 3.3, 6.1).
|
||
- Unit extra-on (`pytest.mark.semantic`): JS `eval(req.query.x)` overlay `promote` (2.2); C `printf("%s\\n", data)` overlay `demote` with dominating fact (2.3); syntax-broken JS → `parse_failed` (3.4); `engine == tree-sitter` on success (2.1).
|
||
- Skip: if `not has_semantic_extra()`, marked tests skip, extra-free suite green (6.3).
|
||
- CI: default job extra-free; second job installs extra and runs `-m semantic` (6.2).
|
||
- Do not assert detection-gate Youden (5.5).
|
||
|
||
## Security Considerations
|
||
|
||
Parser wheels run in-process on operator source. Same trust as reading the tree today. Extra does not execute target code. Overlay evidence stays `static_corroboration` or below (5.3).
|
||
|
||
## Performance
|
||
|
||
Parse is per-file MAP, same file budget as rank. No whole-program index.
|
||
|
||
## Migration Strategy
|
||
|
||
Default install behavior unchanged. Operators who want JS/C/Java overlay run `uv sync --extra semantic`. No artifact format migration.
|
||
|
||
## Supporting References
|
||
|
||
Grammar load sketch (not runtime-imported in core):
|
||
|
||
```python
|
||
from tree_sitter import Language, Parser
|
||
import tree_sitter_javascript as tsjs
|
||
parser = Parser(Language(tsjs.language()))
|
||
tree = parser.parse(text.encode())
|
||
```
|