mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
244 lines
12 KiB
Markdown
244 lines
12 KiB
Markdown
# Design Document: contributor-scan
|
||
|
||
## Integration amendment — 2026-09-12 (draft, unapproved)
|
||
|
||
The maintainer has clarified the product as an actionable pre-push safety net across supported
|
||
ecosystems. The proposed successor `../pre-push-safety-net/design.md` consumes this driver/backend
|
||
without replacing its ranker: the ranker remains the scope mechanism, and vendor code stays out
|
||
of both targets and graphs. Existing approvals below apply to the prior design, not this amendment.
|
||
|
||
Proposed shared seams needing requirements/design review before implementation:
|
||
|
||
- Accept an absolute deadline across graph build, overlays, retries and queries, not only region/call caps.
|
||
- Accept an optional validated reusable graph lease while preserving ordinary fresh-build scans;
|
||
backend remains authoritative for artifact identity, census, compatibility and cleanup.
|
||
- Return exact selected/deferred question IDs and context boundaries from ranker decisions;
|
||
callers must not reconstruct exclusions by slicing an earlier order.
|
||
- Record engine, fact, query, layout and ranking provenance. Align Docker/Compose/ops pins with
|
||
the evaluated baseline before packaging parity claims.
|
||
- Resolve task 5.6's family-specific sanitizer semantics before admitting affected capabilities to
|
||
a low-noise hook; ranker improvements do not repair detector semantics.
|
||
|
||
The push spec owns immutable Git inputs, comparison/admission policy, persistence and presentation.
|
||
This spec owns reusable engine contracts and family correctness. No hook imports belong inside
|
||
the detector, no second heuristic scope selector is introduced, and no task is marked complete by
|
||
this documentation amendment. Revalidate normal scans and repository benchmarks when seams change.
|
||
|
||
## Overview
|
||
|
||
`model-grounded-detection` built an arbiter and measured it at 90.0% pair-correct / 6.7% leak on vibe-py. It
|
||
has **zero production callers**. This feature makes it reachable, and makes it work on a repository rather
|
||
than on a labelled function.
|
||
|
||
The shape is deliberately conservative: the model layer joins the existing scan as **one more stage inside
|
||
MAP**, beside the obligations checker it most resembles, and adds findings rather than replacing any. The
|
||
pattern and overlay paths keep working exactly as they do today, the gates stay byte-identical, and a machine
|
||
without a CPG engine scans exactly as it does now. Nothing in this design is allowed to make the tool worse
|
||
for someone who does not have Joern.
|
||
|
||
### Goals
|
||
|
||
- A finding with a rung reaches a user through `ousast scan`.
|
||
- Regions come from entry points, not from a label a contributor cannot supply.
|
||
- Ranking and budget hold at repository scale.
|
||
- The performance envelope is measured on a real checkout, not asserted.
|
||
|
||
### Non-Goals
|
||
|
||
- New arbiters or families — the three forms exist and are measured.
|
||
- Any optimiser, prompt search, or K-run averaging. The predecessor deleted that machinery; this does not
|
||
reintroduce it by another name.
|
||
- Building a sandbox. Req 10 of the predecessor stands: adopt, never build.
|
||
- Changing the gates, the taxonomy, or the fact-table schema.
|
||
|
||
## Boundary Commitments
|
||
|
||
### This Spec Owns
|
||
|
||
- `model/regions.py` — entry points and file fallbacks to regions, with the family routing for each.
|
||
- `model/scan.py` — the repository-level driver: rank, budget, dispatch to `model/pipeline`, collect.
|
||
- The `Stage.MAP` wiring in `cli.py` and the scan's model-layer payload and report section.
|
||
- `benchmarks/repos/` — pinned known-vulnerable checkouts and their baselines.
|
||
- The decisions in Req 7 and their record.
|
||
|
||
### Out of Boundary
|
||
|
||
- `model/pipeline`, `taint`, `dominance`, `config_value`, `ladder`, `specs`, `candidates` — built, measured,
|
||
used unchanged. If this feature needs to change one, that is a signal the change belongs in its own commit
|
||
with its own measurement.
|
||
- `mapping.analyze_entry_points`, the overlay, the obligations checker, the gates — untouched.
|
||
|
||
### Allowed Dependencies
|
||
|
||
- Core: standard library. `dependencies = []` unchanged.
|
||
- Optional and capability-detected, each degrading to a recorded reason: the CPG engine (Joern), the LLM
|
||
endpoint, any execution tier.
|
||
|
||
### Revalidation Triggers
|
||
|
||
- Any gate output moving (Req 1.4).
|
||
- A scan on a machine without Joern behaving differently from today (Req 1.3).
|
||
- The repository baseline losing a previously-entailed finding (Req 10.3).
|
||
|
||
## Architecture
|
||
|
||
### Existing Architecture Analysis
|
||
|
||
`ousast scan` runs `policy_load → ruleset_load → preprocess → entry_point_mapping → rank → calibrate →
|
||
STATIC → MAP → REGRESS → report`. Inside MAP it already builds a complexity map, runs the overlay, and — the
|
||
closest analogue to what we are adding — runs `_check_obligations(root, targets, entries, facts, settings,
|
||
runtime, hunter_model)`, which takes entry points, produces findings, and merges them by re-ranking.
|
||
|
||
The model layer slots in exactly there, with the same shape and the same failure discipline. `entry_points`
|
||
is already computed at line ~249 and carries what region selection needs: `path`, `line`, `end_line`,
|
||
`function_name`, `access_level`, `trust_boundary`.
|
||
|
||
### Boundary Map
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
EP[entry_point_mapping<br/>existing, untouched] --> REG[model/regions<br/>regions + families]
|
||
REG --> RANK[model/scan<br/>rank, budget]
|
||
RANK --> CPG[cpg.build once per repo]
|
||
CPG --> PIPE[model/pipeline.scan_region<br/>existing, unchanged]
|
||
PIPE --> F[findings with rungs]
|
||
F --> MERGE[merge beside pattern/overlay/obligation findings]
|
||
CPG -. absent .-> SKIP[stage skipped, cpg_unavailable recorded]
|
||
RANK -. budget spent .-> REPORT[unjudged count reported]
|
||
```
|
||
|
||
### Key Decisions
|
||
|
||
| Decision | Choice | Why |
|
||
|---|---|---|
|
||
| Where it runs | a stage inside `Stage.MAP` | it needs entry points, which MAP has; it mirrors the obligations checker exactly |
|
||
| CPG lifetime | **one build per scan**, reused across regions | a build is 5–30s on an excerpt and the dominant cost at repo scale; per-region builds would make this unusable |
|
||
| Region source | entry points, file fallback | Req 2; a contributor has no labelled function |
|
||
| Family routing | by region shape and language, from the specs that exist | Req 2.3; asking every family of every region is the unbounded version |
|
||
| Budget | one total per scan, spent highest-rank-first | Req 3.2; the enumerator's per-region cap of 8 is meaningless across thousands of regions |
|
||
| Ordering | rung first, then rank | Req 3.1; what the model established outranks what the LLM proposed |
|
||
| Merge | additive | Req 1.2; removing an existing finding is a product change nobody asked for |
|
||
|
||
## File Structure Plan
|
||
|
||
### New
|
||
|
||
```
|
||
src/openultrasast/model/
|
||
├── regions.py # EntryPointRecord|FileTarget -> Region + the families that region admits
|
||
└── scan.py # repo driver: rank regions, hold the budget, drive pipeline, collect findings
|
||
|
||
benchmarks/repos/
|
||
├── README.md # what a pinned checkout is for and how a baseline is regenerated
|
||
└── <repo>.toml # pinned url + commit + the known CVE and where it lives
|
||
```
|
||
|
||
### Changed
|
||
|
||
- `cli.py` — a `model` stage inside MAP, mirroring `_check_obligations`; its payload into the manifest and a
|
||
report section.
|
||
- `model/report.py` — a per-rung summary block for a scan (it currently reports per-slice for corpus runs).
|
||
- `config.py` — a `ModelConfig`-style block: enabled, budget, max regions. Zero-dep, defaults conservative.
|
||
|
||
### Unchanged and load-bearing
|
||
|
||
`model/pipeline`, `taint`, `dominance`, `config_value`, `specs`, `candidates`, `ladder`; `mapping`; the three
|
||
gates; `redaction`.
|
||
|
||
## Components and Interfaces
|
||
|
||
### model/regions.py
|
||
|
||
```python
|
||
@dataclass(frozen=True)
|
||
class ScanRegion:
|
||
"""Where to look, and what to look for there."""
|
||
path: str
|
||
function: str | None # None for a file-level fallback region
|
||
families: tuple[str, ...] # only the families this region's shape and language admit
|
||
rank: float # from the entry point's access level and trust boundary
|
||
source: str # "entry_point" | "file_fallback"
|
||
|
||
def regions_for(entries, targets, *, taxonomy, language_of) -> tuple[ScanRegion, ...]
|
||
```
|
||
|
||
Ranking uses what `EntryPointRecord` already carries — an externally-reachable entry point at a trust
|
||
boundary outranks an internal one — rather than inventing a new signal.
|
||
|
||
### model/scan.py
|
||
|
||
```python
|
||
@dataclass(frozen=True)
|
||
class ScanBudget:
|
||
"""A total for the run, not a per-region cap."""
|
||
max_model_calls: int = 200
|
||
max_regions: int = 500
|
||
|
||
@dataclass(frozen=True)
|
||
class ModelScanResult:
|
||
findings: tuple[ModelFinding, ...]
|
||
by_rung: Mapping[str, int]
|
||
regions_scanned: int
|
||
regions_unjudged: int # Req 3.3: reported, never silently truncated
|
||
model_calls: int
|
||
cost_usd: float
|
||
degradations: tuple[Mapping[str, object], ...]
|
||
|
||
def scan_repository(root, regions, *, backend, specs, client, model, budget) -> ModelScanResult
|
||
```
|
||
|
||
One CPG for the run; regions consumed in rank order; the budget decremented per model call and checked before
|
||
each; everything past exhaustion counted into `regions_unjudged`.
|
||
|
||
## Error Handling
|
||
|
||
| Condition | Behaviour | Recorded |
|
||
|---|---|---|
|
||
| No CPG engine | stage skipped, scan otherwise identical to today | `cpg_unavailable` |
|
||
| CPG build fails on the repo | stage skipped, other stages proceed | `cpg_build_failed` + stage |
|
||
| A region's query times out | that region yields nothing, the scan continues | `cpg_query_timeout` |
|
||
| Budget exhausted | stop asking; count the rest | `budget_exhausted` + `regions_unjudged` |
|
||
| No LLM endpoint | entailment still reported; suspicion band unasked | `learning_endpoint_unavailable` |
|
||
|
||
## Requirements Traceability
|
||
|
||
| Criteria | Delivered by |
|
||
|---|---|
|
||
| 1.1, 1.2 | the `model` stage in MAP; findings merged additively |
|
||
| 1.3 | capability probe; stage skipped with `cpg_unavailable` |
|
||
| 1.4 | the committed gate baseline, re-run before and after |
|
||
| 1.5 | `ModelScanResult.by_rung` into the manifest and report |
|
||
| 2.1, 2.2 | `regions_for` over `entry_points`, file fallback recorded in `ScanRegion.source` |
|
||
| 2.3 | `ScanRegion.families` from region shape and language |
|
||
| 2.4 | no `function` label on the scan path; `ScanRegion.function` may be None |
|
||
| 3.1 | ordering by rung, then rank |
|
||
| 3.2, 3.4 | `ScanBudget` total, regions consumed in rank order |
|
||
| 3.3 | `regions_unjudged` + `budget_exhausted` |
|
||
| 3.5 | `cost_usd`, elapsed in the manifest |
|
||
| 4.1–4.5 | `benchmarks/repos/` pinned checkouts; a measurement recording build time, wall-clock, peak memory, rungs, cost, and whether the known CVE was found |
|
||
| 5.1, 5.2 | rung + witness + path:line on every finding; the report states what `suspicion` means |
|
||
| 5.3 | findings to the writable output, never the analysed tree |
|
||
| 5.4 | `model/report.py` scan summary |
|
||
| 6.1–6.5 | the image built and verified; wrappers; read-only mount; network off; pinned + checksummed engine |
|
||
| 7.1, 7.2, 7.3 | the two decisions recorded in this spec's notes with their reasons |
|
||
| 8.1, 8.2 | `model/report.py` per-slice + overfitting gap, already built |
|
||
| 8.3, 8.4 | redaction on every prompt; capability detection throughout |
|
||
| 8.5 | the sibling-audit step in task 1.1 |
|
||
| 9.1–9.7 | the corpus tasks, each gated on a read-check before vendoring |
|
||
| 10.1–10.4 | `benchmarks/repos/` baselines, delta reporting, engine version recorded |
|
||
|
||
## Migration and Phases
|
||
|
||
1. **Wire it (Req 1, 2).** Regions, the MAP stage, findings with rungs reaching a report. Gate-protected.
|
||
Nothing else matters until a user can see a rung.
|
||
2. **Scale it (Req 3, 4).** Budget and ranking, then the first run against a pinned known-vulnerable
|
||
checkout — the measurement that has never been taken and can reshape the rest.
|
||
3. **Ship it (Req 5, 6).** Contributor output; build and verify the image that is currently only defined.
|
||
4. **Close the deferred (Req 7).** The memory decision; retire `evidence_level`.
|
||
5. **Grow the corpus (Req 9).** PHP/WordPress first — the families the model arbitrates best. C CVEs only
|
||
after 7.1.
|
||
6. **Regression baselines (Req 10).**
|
||
|
||
Phase 2 is the one that can invalidate the rest: if a CPG build on a real repository is infeasible, the
|
||
region/budget design changes shape and it is better to learn that before phases 3–6 are built on it.
|