Off every active line (PATHFINDER-2026-10-13/03-unified-proposal.md, cuts). Deleted: src/openultrasast/regress/ and the deep-mode REGRESS branch (deep now records a regress_retired degradation and equals standard; proof is the search-with-proof arm's job), the sandbox docker runner and probe that only regress used, --fail-on worth-fixing, RegressConfig, the two ledger helpers only regress called; skills.py and index.py with the hunter pool's skill routing; model/audit.py; cpg/queries/vocabulary.sc with benchmarks/push/ontology_gap.py. Docs and the scan skill describe the retirement. Net: 3,895 lines deleted, 86 added. Targeted tests: 180 passed, 28 skipped; 3,331 tests collect. Co-Authored-By: Claude Fable 5.1 <[email protected]>
7.1 KiB
Architecture
This page covers four things:
- how a scan works;
- how a finding earns its place;
- how severity and the project score are decided;
- how scan calibration works.
State as of 2026-10-02; module paths are under src/openultrasast/.
The scan pipeline
The CLI is thin. HarnessRuntime owns the lifecycle. It writes an event trace for every stage
(trace/events.jsonl). MODE_STAGES in stages.py lists the stages each mode requests:
quickruns STATIC;standardadds MAP;deeprequests the REGRESS stage, which was retired on 2026-10-13: the scan records aregress_retireddegradation and otherwise equalsstandard. Execution proof now comes from the search-with-proof arm.
policy_load ─▶ ruleset_load ─▶ policy_check (fail loud on an ungoverned CWE)
─▶ static_mapping (SARIF ingest) ─▶ preprocess ─▶ entry_point_mapping
─▶ rank ─▶ calibrate (demote previously rejected scopes)
─▶ findings quick, deep: quick_findings (language-scoped pattern rules)
│ standard: hunter_pool (the same rules, scheduled per ranked target)
─▶ verify ─▶ record_calibration ─▶ fusion (standard only, deterministic) ─▶ score
─▶ MAP (standard, deep)
│ map complexity map and hotspots
│ overlay semantic overlay over the findings (tree-sitter with the `semantic` extra)
│ obligations absence bugs: an obligated operation with no governing guard
│ model one Joern CPG per repository; taint, dominance and configuration arbiters
│ tool hunter only with `[models] hunter`
─▶ REGRESS (deep) retired 2026-10-13, recorded as `regress_retired`
─▶ report.md, report.sarif, manifest.json
The model layer (model/, cpg/) is the core of standard. It builds one code property
graph (CPG: a graph of the program's syntax, control flow and data flow) per scan with Joern.
It runs joern-parse, then batched joern --script queries from cpg/queries/*.sc. Each
question goes to the arbiter (a checker that decides one kind of question) for its spec type:
- taint (untrusted input reaching a dangerous call): source to sink with no discharging sanitizer;
- dominance: does a guard govern the obligated operation;
- configuration: constant abstraction over a value.
Taint facts ship for C, Java, JavaScript, PHP and Python (ruleset/semantic/*.toml). Without
Joern, the layer records cpg_unavailable and the rest of the scan is unchanged. The Docker
image ships Joern and php-cli, which the PHP frontend needs. Detection
techniques explains the representations and techniques behind each
stage.
The model layer reports on a rung ladder:
execution_confirmed a test or sandbox reproduced it
model_entailed the graph decided: a flow with no discharge, an obligation with no
guard, a permissive value
model_corroborated the graph found the shape; a judge agreed on the residual question
suspicion proposed, nothing corroborated it
The LLM answers only the residual question: the part the graph cannot settle
(model/pipeline.py, residual_question). It is asked only about a flow the graph already
found. It can confirm or contradict that flow, but never create one. When the graph resolved
nothing, the LLM sees the enumerator's candidates (at most MAX_JUDGED_CANDIDATES, 8).
Anything it affirms is reported at suspicion. Without a provider key, neither question is
asked. Only the graph's entailed findings are reported then.
REGRESS was retired on 2026-10-13. Deep mode records a regress_retired degradation and otherwise equals standard. Execution proof is the search-with-proof arm's job (search/verify.py, bubblewrap sandbox, differential runs against owned canaries).
Evidence ladder: how a false positive is eliminated
Evidence level is a state machine, not a label a model may assign. The verifier
(verification.py) runs on independent context and enforces it:
suspicion ─▶ static_corroboration ─▶ crash_reproduced ─▶ root_cause_explained
─▶ exploit_demonstrated ─▶ patch_validated
- below
static_corroboration:NEEDS_EVIDENCE; static_corroborationwith reachability unknown:REJECTED, with a tie-breaker demanding call-graph, route, CLI, parser or dynamic evidence;static_corroborationwith function-level reachability:ACCEPTED.
A rejected finding becomes a scoped false-positive learning (calibration.py). It has a
scope and a reason from a fixed taxonomy: unreachable_path, missing_attacker_control,
sanitizer_disproved, static_rule_mismatch, incorrect_model_assumption, duplicate,
insufficient_impact, unsupported, contradicted, unverified. A rejection in auth/
demotes only auth/..., never the whole class
(tests/test_calibration.py::test_scoped_false_positive_demotion_does_not_suppress_class_globally).
How a false negative is surfaced
An expected vulnerability that no finding matched becomes a BenchmarkCalibrationRecord in
calibration_records.json. Each record names a next_improvement_candidate: a static rule,
SARIF source, entry-point mapping, retrieval package, hunter prompt, dynamic reproducer or
skill route. Once a miss is closed, the detection gate keeps it closed in CI.
Central CWE policy and the project score
Policy (policy/verycode.py): a vendored CWE_Score.tsv (verycode-policies) is the single
source of truth. Each CWE carries:
- a flaw category;
- a severity (0-5);
static/dynamicscope flags.
A finding's severity comes only from policy, keyed on CWE. CWEs that are not static resolve
to 0 and are report-only. assert_rules_resolve runs as the policy_check stage. An enabled
rule that names an ungoverned CWE aborts the scan. Verycode has no CWE-120, so the C/C++
buffer rules carry CWE-121.
Score (scoring/project_score.py): each finding's penalty is
SEV_WEIGHT[severity] x REACH_MULT[reachability]. The score is 100 * e^(-total/k) with
k = 60.
| Severity | Weight | Reachability | Multiplier | |
|---|---|---|---|---|
| 5 | 50 | reachable |
1.0 | |
| 4 | 25 | inferred-file-surface |
0.6 | |
| 3 | 10 | unknown |
0.4 | |
| 2 / 1 / 0 | 2 / 1 / 0 |
A confirmed false positive lowers a finding's reachability multiplier. The rule is not deleted. The score gate works like this:
- a severity-5 reachable finding always fails it;
score < min_score(default 80) fails it only with[score] blocking = true.
The score stage writes score.json and merges it into manifest.json.
Scoped calibration (every scan)
scan ─▶ verify ─▶ non-accepted findings ─▶ scoped learnings
(.openultrasast/calibration/false_positive_learnings.json)
next scan ─▶ rank ─▶ calibrate (demote rejected scopes) ─▶ findings
record_calibrationruns afterverify. It merges this run's non-accepted outcomes into the ledger, de-duplicated by finding ID.calibrateruns afterrank. It demotes the priority of every scope that produced a rejection and writesapplied_calibrations.json.
Demotion is scoped and reversible (tests/test_pipeline_calibration.py).