Files
Open_Ultra_SAST/.kiro/specs/tree-sitter-overlay-extra/design.md
T

273 lines
12 KiB
Markdown
Raw Normal View History

# 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())
```