chore(p0): retire regress, the skill router, the module audit tool and vocabulary.sc

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]>
This commit is contained in:
Marius Ciepluch
2026-10-13 10:50:01 +02:00
co-authored by Claude Fable 5.1
parent 94e96c6885
commit f5a904b7f2
50 changed files with 86 additions and 3895 deletions
+4 -71
View File
@@ -34,7 +34,6 @@ from .calibration import (
write_ranking_calibrations,
)
from .complexity import map as complexity_map
from .complexity.ledger import persist_verdicts
from .config import ModelLayerConfig, ObligationsConfig, ResolvedConfig, RetiredConfigError, load_config, load_dotenv
from .findings import StaticFinding, quick_scan_findings, uncovered_languages, write_findings
from .fusion import FusionDecision, fuse_findings
@@ -60,7 +59,6 @@ from .preprocess import FileTarget, preprocess_repository, write_preprocess_arti
from .profiling import start as start_profiling
from .provenance import fingerprint
from .rank import rank_obligations, rank_targets, write_rankings
from .regress import TRIGGERABLE, CandidateVerdict, run_regression, write_verdicts
from .reports import scan_exit_code, write_manifest, write_markdown_report, write_sarif_report
from .repos import (
DEFAULT_REPO_DIR,
@@ -74,12 +72,10 @@ from .repos import select as select_repo
from .repos import verify as verify_repo
from .ruleset import DEFAULT_RULESET_DIR, load_ruleset
from .run import ScanRun, create_scan_run
from .sandbox import resolve_sandbox_probe, resolve_sandbox_runner
from .scoring import build_score_artifact
from .semantic import (
OverlayRecord,
adjudicate,
filter_promoted_hotspots,
finding_from_coverage,
load_facts,
write_overlay,
@@ -224,7 +220,7 @@ def _main(argv: list[str] | None) -> int:
scan.add_argument("path", type=Path)
scan.add_argument("--mode", choices=("quick", "standard", "deep"), default="quick")
scan.add_argument("--config", type=Path, default=Path("openultrasast.toml"))
scan.add_argument("--fail-on", choices=("never", "findings", "verified", "worth-fixing"), default="never")
scan.add_argument("--fail-on", choices=("never", "findings", "verified"), default="never")
benchmark = subparsers.add_parser("benchmark", help="run a benchmark manifest and write scoreboard artifacts")
benchmark.add_argument("manifest", type=Path)
@@ -756,52 +752,9 @@ def _run_scan(path: Path, config_path: Path, mode: str, fail_on: str) -> ScanOut
"hotspot_count": len(built_map.hotspots),
"heuristic_only": built_map.heuristic_only,
}
verdicts_path = run.root / "verdicts.json"
wrote_verdicts = False
verdict_records: tuple[CandidateVerdict, ...] = ()
if Stage.REGRESS in plan.requested:
probe = resolve_sandbox_probe()
if not probe.available():
plan = record_skip(plan, Stage.REGRESS, "sandbox_unavailable")
runtime.state["degradations"].append(skip_as_degradation(Stage.REGRESS, "sandbox_unavailable"))
else:
sandbox = resolve_sandbox_runner()
hotspots = built_map.hotspots if built_map is not None else ()
hotspots = filter_promoted_hotspots(hotspots, overlay_records, findings)
promoted = [
finding
for finding in findings
if finding.finding_id in {record.proposal_id for record in overlay_records if record.disposition == "promote"}
]
cwe_by_rule_id = {rule.rule_id: rule.cwe for rule in ruleset}
rule_cwe_by_finding = {finding.finding_id: cwe_by_rule_id.get(finding.finding_id.split(":", 1)[0], "") for finding in promoted}
languages_by_path = {target.path: target.language for target in targets}
records = runtime.run_stage(
"regress",
lambda: run_regression(
hotspots,
promoted,
max_candidates=config.regress.max_candidates,
policy=policy,
rule_cwe=rule_cwe_by_finding,
languages_by_path=languages_by_path,
repo_root=run.target,
sandbox=sandbox,
sandbox_limits=config.sandbox,
images=dict(config.regress.images),
),
)
write_verdicts(records, verdicts_path)
persist_verdicts(run.target / CALIBRATION_DIR / "complexity_ledger.json", records)
# Req 6.4: proof rungs live on verdicts, not on StaticFinding, so obligations re-rank once the sandbox has spoken.
proven_ids = {fid for record in records if record.verdict == TRIGGERABLE for fid in record.inventory_finding_ids}
if proven_ids and any(finding.finding_id.startswith("obligation:") for finding in findings):
findings = rank_obligations(findings, proven_ids=proven_ids)
write_findings(findings, findings_path)
verdict_records = records
wrote_verdicts = True
plan = record_completed(plan, Stage.REGRESS)
worth_fixing_payload = _worth_fixing_payload(verdict_records) if wrote_verdicts else None
runtime.state["degradations"].append(skip_as_degradation(Stage.REGRESS, "regress_retired"))
plan = record_skip(plan, Stage.REGRESS, "regress_retired")
# Req 5.6: what each family could see, for the report. Absent when the model layer did not run --
# and a scan with no model layer states no limits because it made no claims.
coverage_rows = model_payload.get("coverage") if isinstance(model_payload, dict) else None
@@ -814,7 +767,6 @@ def _run_scan(path: Path, config_path: Path, mode: str, fail_on: str) -> ScanOut
verifications,
redact=config.hardening.redact_secrets,
complexity_map=built_map,
verdicts=verdict_records if wrote_verdicts else None,
overlay=overlay_records if wrote_overlay else None,
obligations=obligations_cited or None,
obligations_summary=obligations_payload or None,
@@ -851,7 +803,6 @@ def _run_scan(path: Path, config_path: Path, mode: str, fail_on: str) -> ScanOut
fusion=fusion_path if fusion_decisions else None,
complexity_map=complexity_map_path if complexity_payload is not None else None,
overlay=overlay_path if wrote_overlay else None,
verdicts=verdicts_path if wrote_verdicts else None,
),
path=manifest_path,
score=score_artifact.to_dict(),
@@ -859,7 +810,6 @@ def _run_scan(path: Path, config_path: Path, mode: str, fail_on: str) -> ScanOut
fusion=[_fusion_summary(decision) for decision in fusion_decisions] or None,
stages=stages_payload(plan),
complexity=complexity_payload,
worth_fixing=worth_fixing_payload,
provenance=provenance.to_dict(),
obligations=obligations_payload,
model=model_payload,
@@ -873,7 +823,7 @@ def _run_scan(path: Path, config_path: Path, mode: str, fail_on: str) -> ScanOut
ranked_target_count=len(rankings),
finding_count=len(findings),
calibrations_applied=len(applied_calibrations),
exit_code=scan_exit_code(findings, verifications, fail_on, worth_fixing_verdicts=verdict_records),
exit_code=scan_exit_code(findings, verifications, fail_on),
notices=tuple(
f"language not covered: {item['language']} ({item['count']} files)"
for item in runtime.state["degradations"]
@@ -966,23 +916,6 @@ def _load_static_hints(sarif_paths: tuple[str, ...]) -> list[object]:
return hints
def _worth_fixing_payload(verdicts: tuple[CandidateVerdict, ...]) -> dict[str, object]:
worth = [record for record in verdicts if record.worth_fixing]
return {
"count": len(worth),
"verdicts": [
{
"path": record.path,
"function_name": record.function_name,
"verdict": record.verdict,
"reason": record.reason,
"worth_fixing": True,
}
for record in worth
],
}
def _artifact_paths(**paths: Path | None) -> dict[str, Path]:
return {name: path for name, path in paths.items() if path is not None}
-6
View File
@@ -6,10 +6,7 @@ from .ledger import (
apply_overlay,
hotspot_key,
load_ledger,
must_keep_as_candidate,
persist_verdicts,
record_verdict,
select_forced_candidates,
write_ledger,
)
from .map import ComplexityMap, Hotspot, build_complexity_map, write_complexity_map
@@ -27,10 +24,7 @@ __all__ = [
"collect_signals",
"hotspot_key",
"load_ledger",
"must_keep_as_candidate",
"persist_verdicts",
"record_verdict",
"select_forced_candidates",
"write_complexity_map",
"write_ledger",
]
-68
View File
@@ -8,8 +8,6 @@ from dataclasses import asdict, dataclass, replace
from pathlib import Path
from typing import TYPE_CHECKING
from ..findings import StaticFinding
if TYPE_CHECKING:
from .map import Hotspot
@@ -19,8 +17,6 @@ VERDICT_DELTAS = {
"triggerable": TRIGGERABLE_DELTA,
"not_triggerable": NOT_TRIGGERABLE_DELTA,
}
FORCED_CANDIDATE_SEVERITY = 5
FORCED_CANDIDATE_REACHABILITY = frozenset({"reachable", "inferred-file-surface"})
@dataclass(frozen=True)
@@ -57,27 +53,6 @@ def write_ledger(path: Path, ledger: Mapping[str, LedgerEntry | Mapping[str, obj
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
def persist_verdicts(path: Path, verdicts: Sequence[object]) -> dict[str, LedgerEntry]:
"""Record triggerable / not_triggerable outcomes into the loop-owned complexity ledger."""
ledger = load_ledger(path)
changed = False
for item in verdicts:
verdict = str(getattr(item, "verdict", ""))
if verdict not in VERDICT_DELTAS:
continue
raw_function = getattr(item, "function_name", None)
ledger = record_verdict(
ledger,
path=str(getattr(item, "path", "")),
function_name=raw_function if isinstance(raw_function, str) else None,
verdict=verdict,
)
changed = True
if changed:
write_ledger(path, ledger)
return ledger
def record_verdict(
ledger: Mapping[str, LedgerEntry | Mapping[str, object]],
*,
@@ -130,49 +105,6 @@ def apply_overlay(
return tuple(sorted(adjusted, key=lambda item: (-item.score, item.path, item.function_name or "")))
def must_keep_as_candidate(
finding: StaticFinding,
policy_severity: int | object,
reachability: str | None = None,
) -> bool:
status = finding.reachability_status if reachability is None else reachability
return _as_severity(policy_severity) >= FORCED_CANDIDATE_SEVERITY and status in FORCED_CANDIDATE_REACHABILITY
def select_forced_candidates(
findings: Sequence[StaticFinding],
policy: Mapping[str, object],
*,
rule_cwe: Mapping[str, str] | None = None,
) -> tuple[StaticFinding, ...]:
cwe_by_id = rule_cwe or {}
return tuple(
finding
for finding in findings
if must_keep_as_candidate(finding, _policy_severity_for(finding, policy, cwe_by_id), finding.reachability_status)
)
def _policy_severity_for(finding: StaticFinding, policy: Mapping[str, object], cwe_by_id: Mapping[str, str]) -> int:
if finding.finding_id in policy:
return _as_severity(policy[finding.finding_id])
cwe = cwe_by_id.get(finding.finding_id)
if cwe is not None and cwe in policy:
return _as_severity(policy[cwe])
return 0
def _as_severity(value: object) -> int:
if isinstance(value, bool):
return 0
if isinstance(value, int):
return value
severity = getattr(value, "severity", None)
if isinstance(severity, bool) or not isinstance(severity, int):
return 0
return severity
def _coerce_entry(value: object) -> LedgerEntry | None:
if isinstance(value, LedgerEntry):
return value
+2 -29
View File
@@ -25,8 +25,8 @@ _UNREAD_DYNAMIC = "no dynamic or deep tier exists; deep mode will define its own
_UNREAD_EVIDENCE = "evidence tiers come from the program model's ladder (model/ladder.py), not from configuration"
_UNREAD_VARIANTS = "the structural variant search it configured was deleted with semantic/variants.py"
_UNREAD_SANDBOX = (
"the sandbox always runs with `--network none` and a read-only workspace (sandbox/runner.py); that is an "
"invariant the runner enforces, not a knob"
"the search sandbox enforces isolation and read-only source (search/_sandbox.py); that is an "
"invariant, not a knob"
)
_UNREAD_TOP_K = "no stage read it; `[complexity] max_hunter_hotspots` is the one complexity key the scan reads"
_UNREAD_EMBEDDINGS = (
@@ -156,13 +156,6 @@ class ComplexityConfig:
max_hunter_hotspots: int = 8
@dataclass(frozen=True)
class RegressConfig:
# Stage-3 candidate cap and optional per-language sandbox image pins. Isolation limits stay on SandboxConfig.
max_candidates: int = 5
images: tuple[tuple[str, str], ...] = ()
@dataclass(frozen=True)
class ModelLayerConfig:
# contributor-scan: the model layer in MAP. Named for the LAYER, not the model: `ModelConfig` is already
@@ -215,7 +208,6 @@ class ResolvedConfig:
fusion: FusionConfig = FusionConfig()
hardening: HardeningConfig = HardeningConfig()
complexity: ComplexityConfig = ComplexityConfig()
regress: RegressConfig = RegressConfig()
obligations: ObligationsConfig = ObligationsConfig()
model: ModelLayerConfig = ModelLayerConfig()
push: PushConfig = PushConfig()
@@ -240,7 +232,6 @@ def load_config(config_path: Path | None = None, *, dotenv: bool = True) -> Reso
fusion=_load_fusion(data.get("fusion", {})),
hardening=_load_hardening(data.get("hardening", {})),
complexity=_load_complexity(data.get("complexity", {})),
regress=_load_regress(data.get("regress", {})),
obligations=_load_obligations(data.get("obligations", {})),
model=_load_model(data.get("model", {})),
push=PushConfig.from_payload(data.get("push", {})),
@@ -344,24 +335,6 @@ def _load_complexity(value: object) -> ComplexityConfig:
)
def _load_regress(value: object) -> RegressConfig:
data = _section(value)
return RegressConfig(
max_candidates=_int_value(data.get("max_candidates"), 5),
images=_string_map(data.get("images")),
)
def _string_map(value: object) -> tuple[tuple[str, str], ...]:
data = _section(value)
images: list[tuple[str, str]] = []
for key, item in data.items():
pinned = _string(item)
if pinned is not None:
images.append((str(key), pinned))
return tuple(sorted(images))
def _float_value(value: object, default: float) -> float:
if isinstance(value, bool):
return default
-101
View File
@@ -1,101 +0,0 @@
// What this repository calls that the ontology does not model (contributor-scan 7.1).
//
// Five of seven families established nothing in the 2026-09 family census, and every one of them failed for
// an ontology reason with the flow engine ready and nothing to aim at: no HTTP client for a server-side
// request forgery, no collection read for an object-reference defect, no WordPress spelling for a
// deserialization. Each was found by investigating one family at a time. This answers the question directly.
//
// It needs no dataflow. The structural pass already walks every call, which is why a census that took three
// investigations can be one query.
//
// What it reports is a CANDIDATE LIST, never an ontology. A name here becomes a fact only when someone
// resolves it to a library API, states the weakness class and records provenance. Adding a name because a
// subject called it is how a general-purpose tool becomes a fixture of its own benchmark.
//
// Parameters: cpgFile, modelled (comma-separated tokens any fact table names), sources (comma-separated
// source patterns, used only to say whether a call sits in a method that reads untrusted input).
// Output: fenced JSON array of {name, spelling, count, modelled, nearSource, file, line}.
@main def exec(cpgFile: String, modelled: String = "", sources: String = "") = {
importCpg(cpgFile)
def split(raw: String): List[String] = raw.split(",").map(_.trim).filter(_.nonEmpty).toList
val modelledTokens = split(modelled)
val sourcePatterns = split(sources)
def boundedPattern(t: String) =
java.util.regex.Pattern.compile("(?<![A-Za-z0-9_$])" + java.util.regex.Pattern.quote(t) + "(?![A-Za-z0-9_$])")
val patterns = modelledTokens.map(t => (t, boundedPattern(t)))
// The callee, which is the only part of a call that names what is being called. Matching a whole node's
// text is the defect this project has now fixed six times.
def calleeText(code: String): String = {
val open = code.indexOf('(')
val head = if (open < 0) code else code.substring(0, open)
head.trim.takeRight(120)
}
// Names the FRONTEND synthesised are not names the code calls. jssrc2cpg lowers a destructuring to
// `_tmp_3.push` and an array literal to `__ecma.Array.factory`, and on the first measured subject those
// were the two most frequent candidates in the list -- noise at the top of a worklist is how a worklist
// stops being read.
def synthesised(name: String): Boolean =
name.startsWith("<") || name.startsWith("__ecma") || name.matches("^_tmp_?[0-9]*\\..*") || name.startsWith("_tmp")
val calls = cpg.call.filterNot(c => c.name.startsWith("<operator") || synthesised(c.name)).l
// Memoised per method: does this method read something the ontology calls untrusted input? A gap that sits
// beside a source is worth more attention than one that does not, and that is a ranking hint rather than a
// claim about any flow.
val sourceMemo = scala.collection.mutable.Map.empty[String, Boolean]
def nearSource(c: io.shiftleft.codepropertygraph.generated.nodes.Call): Boolean =
if (sourcePatterns.isEmpty) false
// CPS-IT/mailqueue Extension::KEY has a null .method; it cannot be near a method source.
else Option(c.method).exists { m =>
sourceMemo.getOrElseUpdate(
m.fullName,
m.ast.isCall.exists(x => sourcePatterns.exists(p => boundedPattern(p).matcher(x.code).find()))
)
}
def isModelled(c: io.shiftleft.codepropertygraph.generated.nodes.Call): Boolean = {
val callee = calleeText(c.code)
patterns.exists { case (token, pattern) => c.name == token || pattern.matcher(callee).find() }
}
case class Row(name: String, spelling: String, modelledHere: Boolean, near: Boolean, file: String, line: Int)
val rows = calls.map { c =>
Row(
c.name,
calleeText(c.code),
isModelled(c),
nearSource(c),
c.file.name.l.headOption.getOrElse(""),
c.lineNumber.getOrElse(-1)
)
}
val grouped = rows.groupBy(_.name).toList.map { case (name, items) =>
val first = items.head
ujson.Obj(
"name" -> name,
// The commonest qualified spelling, because `get` and `needle.get` are different leads.
"spelling" -> items.groupBy(_.spelling).toList.maxBy(_._2.size)._1,
"count" -> items.size,
"modelled" -> items.exists(_.modelledHere),
"nearSource" -> items.count(_.near),
"file" -> first.file,
"spellings" -> items.map(_.spelling).distinct.size,
"line" -> first.line
)
}
// Unmodelled first and by frequency, because that ordering IS the worklist.
val ordered = grouped.sortBy(o => (o("modelled").bool, -o("count").num.toInt))
println("---OUSAST-CPG-BEGIN---")
println(ujson.write(ujson.Arr(ordered: _*)))
println("---OUSAST-CPG-END---")
}
+3 -15
View File
@@ -21,7 +21,6 @@ class HunterBudget:
max_targets: int
max_findings_per_target: int
retrieval_budget_chars: int
skill_budget_chars: int
@dataclass(frozen=True)
@@ -30,7 +29,6 @@ class HunterTask:
ranking: RankingScore
tier: HunterTier
budget: HunterBudget
selected_skills: list[str]
retrieval_context: str
@@ -40,7 +38,6 @@ class HunterTrajectory:
target_path: str
tier: HunterTier
priority: float
selected_skills: list[str]
retrieval_context_chars: int
findings: list[str]
status: str
@@ -53,9 +50,9 @@ class HunterPoolResult:
DEFAULT_BUDGETS = {
"A": HunterBudget(tier="A", max_targets=20, max_findings_per_target=20, retrieval_budget_chars=6000, skill_budget_chars=2500),
"B": HunterBudget(tier="B", max_targets=40, max_findings_per_target=10, retrieval_budget_chars=4000, skill_budget_chars=1500),
"C": HunterBudget(tier="C", max_targets=80, max_findings_per_target=5, retrieval_budget_chars=2000, skill_budget_chars=800),
"A": HunterBudget(tier="A", max_targets=20, max_findings_per_target=20, retrieval_budget_chars=6000),
"B": HunterBudget(tier="B", max_targets=40, max_findings_per_target=10, retrieval_budget_chars=4000),
"C": HunterBudget(tier="C", max_targets=80, max_findings_per_target=5, retrieval_budget_chars=2000),
}
@@ -93,7 +90,6 @@ def schedule_hunter_tasks(
ranking=ranking,
tier=tier,
budget=budget,
selected_skills=select_skill_snippets(target),
retrieval_context=retrieval_context_by_path.get(target.path, "")[: budget.retrieval_budget_chars],
)
)
@@ -134,13 +130,6 @@ def build_retrieval_context_by_path(root: Path, targets: list[FileTarget], *, ma
return contexts
def select_skill_snippets(target: FileTarget, *, max_snippets: int = 4) -> list[str]:
"""Route the target to its most relevant Trail of Bits skills (ids), via the skill index."""
from .skills import route_skill_ids
return route_skill_ids(target, max_snippets=max_snippets)
def _run_hunter_task(
root: Path,
scan_id: str,
@@ -154,7 +143,6 @@ def _run_hunter_task(
target_path=task.target.path,
tier=task.tier,
priority=task.ranking.priority,
selected_skills=task.selected_skills,
retrieval_context_chars=len(task.retrieval_context),
findings=[finding.finding_id for finding in findings],
status="completed",
-74
View File
@@ -1,74 +0,0 @@
"""Text chunking for the skill router (`skills.py`).
The vector index that once lived beside the chunker (`VectorIndex`, `build_vector_index`, `query_vector_index`,
`build_retrieval_package`) and the `ousast index` subcommand that fed it were removed on 2026-10-02: nothing ever
read the `chunks.json` they wrote. The decision engine's embeddings live in `learn/embeddings.py`.
"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass
VALID_NAMESPACES = {"repo_code", "docs", "static_findings", "mechanisms", "skills", "traces"}
@dataclass(frozen=True)
class CodeChunk:
chunk_id: str
namespace: str
path: str
language: str
start_line: int
end_line: int
text: str
metadata: dict[str, str | int | bool]
def chunk_text_namespace(
*,
namespace: str,
path: str,
text: str,
metadata: dict[str, str | int | bool] | None = None,
max_lines: int = 80,
) -> list[CodeChunk]:
if namespace not in VALID_NAMESPACES:
raise ValueError(f"unsupported namespace: {namespace}")
if max_lines < 1:
raise ValueError("max_lines must be at least 1")
lines = text.splitlines()
chunks: list[CodeChunk] = []
for start in range(0, len(lines), max_lines):
chunk_lines = lines[start : start + max_lines]
if not any(line.strip() for line in chunk_lines):
continue
start_line = start + 1
end_line = start + len(chunk_lines)
chunk_text = "\n".join(chunk_lines)
chunk_metadata: dict[str, str | int | bool] = {
"path": path,
"namespace": namespace,
"start_line": start_line,
"end_line": end_line,
}
if metadata:
chunk_metadata.update({key: value for key, value in metadata.items() if isinstance(value, str | int | bool)})
chunks.append(
CodeChunk(
chunk_id=_chunk_id(f"{namespace}:{path}", start_line, end_line, chunk_text),
namespace=namespace,
path=path,
language=str(chunk_metadata.get("language", "text")),
start_line=start_line,
end_line=end_line,
text=chunk_text,
metadata=chunk_metadata,
)
)
return chunks
def _chunk_id(path: str, start_line: int, end_line: int, text: str) -> str:
digest = hashlib.sha256(f"{path}:{start_line}:{end_line}:{text}".encode()).hexdigest()[:16]
return f"{path}:{start_line}-{end_line}:{digest}"
-260
View File
@@ -1,260 +0,0 @@
"""Module audit: what is load-bearing, what stands alone, and what is orphaned (Req 3).
The tool outgrew itself — 99 modules and 832 tests at the point this feature began, a large share of them
serving an architecture its own measurements retired. This classifier exists so outgrowth is visible and
checkable rather than a thing someone notices two specs later.
A module is:
load_bearing reachable by imports from a gate, the CLI, or a module run with ``python -m``
standalone_capability reachable the same way, but its own entry point and users, not on the scan path
orphaned not reachable from any of those -- it should not be in the tree
Reachability decides the class. ``SPEC_OWNED`` and ``STANDALONE`` only supply the reason text: being named
by a spec does not keep a module nobody imports alive.
Pure stdlib, no imports of the package it audits: it reads the import graph with ``ast`` so a broken module
is still classifiable.
"""
from __future__ import annotations
import ast
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from pathlib import Path
PACKAGE = "openultrasast"
# Entry points a module can be reachable from. The three gates are the contract; the CLI is the product.
ROOTS = ("gate", "map_gate", "pair_gate", "cli", "__main__")
# Reason text for reachable modules, by the requirement that owns them. Not a classification: see audit().
SPEC_OWNED = {
"push.cache": "pre-push-safety-net task 6.2: bounded artifact publication and leased eviction; runner integration in 6.4",
"push.report": "pre-push-safety-net Req 5.1/5.2/5.5/7.3: compact output and complete artifacts; runner integration pending",
"push.policy": "pre-push-safety-net Req 2.4/3.1/3.4/4.1: targeted same-semantics base comparison; replay integration pending",
"push.snapshot": "pre-push-safety-net Req 1.1/1.3/1.4/7.1: local immutable ref resolution; runner integration pending",
"contracts": "pre-push-safety-net Req 7.3: strict shared manifest serialization, independent of push",
"model.contracts": "pre-push-safety-net Req 4.1/7.3: generic deadline, change and exact scope contracts; integration pending",
"cpg.artifact": "pre-push-safety-net Req 4.3/7.3: backend graph identity and live lease contract; validation integration pending",
"push": "pre-push-safety-net Req 5.5/7.3: experimental push contract package; no hook installed",
"push.contracts": "pre-push-safety-net Req 5.5/7.3: immutable comparisons and independent result statuses; runner pending",
"model.specs": "model-grounded-detection Req 7.2-7.4: the security vocabularies the CPG queries are parameterised by",
"model.endpoint": "model-grounded-detection Req 8: the bounded model client the hunter, the plane and the CLI share",
"model.audit": "model-grounded-detection Req 3: this classifier; run by the maintainer, not by the scan",
"model.ladder": "model-grounded-detection Req 5: the evidence ladder every finding carries",
"model.taint": "model-grounded-detection Req 6.1: taint reachability over the CPG, the flow-family arbiter",
"model.dominance": "model-grounded-detection Req 6.2: guard dominance, the arbiter for bugs that never crash",
"model.config_value": "model-grounded-detection Req 6.3: constant abstraction for the configuration families",
"model.report": "model-grounded-detection Req 9.3/11.1: per-slice reporting with the overfitting gap",
"model.pipeline": "model-grounded-detection Req 8: the enumerator proposes, the LLM answers, the model disposes",
"model.regions": "contributor-scan Req 2: regions from entry points, with the families each admits",
"model.scan": "contributor-scan Req 3: the repository driver -- one CPG, one budget, ranked spend",
"cpg": "model-grounded-detection Req 4: the CPG seam package",
"cpg.backend": "model-grounded-detection Req 4.1/4.5: the single Joern subprocess boundary",
"cpg.capability": "model-grounded-detection Req 4.2/4.3: the engine probe that degrades to suspicion",
"plane": "ai-service-plane Req 1-4: the service plane package; runner and reconciler integration in tasks 3-4",
"plane.budget": "ai-service-plane Req 2.3/2.4: metered chat client with usd and calls ceilings; task integration in tasks 3, 6",
"plane.manifests": "ai-service-plane Req 1.1-1.4: ax-shaped Task/Workspace/Model and the Run kind, validated before anything runs",
"plane.tasks": "ai-service-plane Req 4.1: the task entrypoint package the runner imports by name; runner integration pending (task 3)",
"plane.tasks.repo_facts": "ai-service-plane Req 5.1/5.2: source-only functions and callers per file; runner integration pending",
"plane.tasks.verify": "ai-service-plane Req 5.3/2.1-2.3/7.2: the batched hunt with known callers, metered and resumable per file hunt",
"plane.tasks.agree": "ai-service-plane Req 5.3/2.1: agreement across two verify passes with per-candidate cost, turns and site match",
"plane.reconciler": "ai-service-plane Req 3.1-3.5, 7.3: ax-backed Run execution, status attribution and doctor; `ousast plane`",
"plane.generate": "ai-service-plane task 7: an increment's Workspaces, per-case Tasks and Run from a recorded set",
"plane.memory": "harnessx-removal Req 6.1/6.4: the memory store (file, S3), ingest and fact reuse; `ousast plane remember`",
"plane.tasks.remember": "harnessx-removal Req 6.1: a case's delivered artifacts as memory rows, model-free",
"plane.tasks.alerts": "harnessx-removal Req 6.3: quick-mode alerts on a case's vulnerable and fixed pins, model-free",
"plane.engine_alerts": "harnessx-removal Req 6.3: engine alerts where quick mode has no rules (PHP), on the host",
"plane.tasks.loop": "harnessx-removal Req 6.3: the improvement loop's snapshot, measure, propose and improve steps",
"plane.tasks.features": "learned-decision-engine Req 1.1-1.3/7.2: one allow-listed feature record per candidate, model-free",
"plane.tasks.roles": (
"learned-decision-engine Req 8.1: model role classification per file chunk (model_sinks.py moved in); "
"its Run template comes with the harvest (task 5)"
),
"learn.program": (
"learned-decision-engine Req 3.1/3.3: the AI classifier program (signature, Retrieve -> Classify(k), response "
"cache); `ousast learn compile` (task 6.5) and the decide task (task 8) are its callers"
),
"plane.workspaces": "ai-service-plane task 4: Workspace manifests per case pin of a population; `ousast plane workspaces`",
"plane.runner": "ai-service-plane Req 4.1-4.4: the ax-task-runner entrypoint (PID 1 of the task image); delivers the output directory",
}
# Reachable modules that stand alone: their own CLI subcommand or external protocol, off the scan path.
STANDALONE = {
"search.demo_replay": "benchmarks standalone: no-model stored demo replay; python -m benchmarks.search.demo_replay",
"search.coordinator": (
"search-with-proof Req 1/2/4: injected search executor and offline board checkpoint inspector; agent worker pending"
),
"push_scoring": "pre-push-safety-net task 1.3: frozen-profile diagnostic outcome scorer; python -m openultrasast.push_scoring",
"push_inputs": "pre-push-safety-net task 1.2: offline input provenance validator; python -m openultrasast.push_inputs",
"mcp": "narrow MCP server over stdio; its own `ousast mcp` entry point and OpenCode integration",
"skills": "skill router; consumed by mcp and by the OpenCode integration, not by the scan path",
"fusion": "two-panel adjudication engine; reached from the scan's report stage and used standalone",
}
@dataclass(frozen=True)
class ModuleAudit:
module: str
classification: str # load_bearing | standalone_capability | orphaned
reason: str
importers: tuple[str, ...]
def module_name(path: Path, root: Path) -> str:
relative = path.relative_to(root).with_suffix("")
parts = [part for part in relative.parts if part != "__init__"]
return ".".join(parts)
def import_graph(root: Path) -> dict[str, set[str]]:
"""``module -> the modules it imports`` for every module under ``root``, resolved to package-relative names."""
graph: dict[str, set[str]] = {}
for path in sorted(root.rglob("*.py")):
if "__pycache__" in path.parts:
continue
name = module_name(path, root)
# For a package's `__init__`, the containing package *is* this module, so `from .x import y` resolves to
# `<name>.x`; for a plain module it resolves against the parent. Getting this backwards made every module
# a package re-exports look unimported.
is_package = path.name == "__init__.py"
imported: set[str] = set()
try:
tree = ast.parse(path.read_text())
except (OSError, SyntaxError):
graph[name] = imported
continue
package_parts = name.split(".") if is_package else name.split(".")[:-1]
package_parts = [part for part in package_parts if part]
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
if node.level:
base = package_parts[: len(package_parts) - node.level + 1]
target = ".".join([*base, node.module]) if node.module else ".".join(base)
else:
target = node.module or ""
if target.startswith(f"{PACKAGE}."):
target = target[len(PACKAGE) + 1 :]
elif target == PACKAGE:
target = ""
else:
continue
if target:
imported.add(target)
# `from .plane import engine_alerts` imports the submodule `plane.engine_alerts`, not only the
# package; names that are not modules are dropped later because they are not in the graph.
prefix = f"{target}." if target else ""
imported.update(f"{prefix}{alias.name}" for alias in node.names if alias.name != "*")
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name.startswith(f"{PACKAGE}."):
imported.add(alias.name[len(PACKAGE) + 1 :])
graph[name] = imported
return graph
def main_modules(root: Path) -> set[str]:
"""Modules with a ``__main__`` block: run by name (``python -m``, the plane runner), so they are entry points."""
found: set[str] = set()
for path in sorted(root.rglob("*.py")):
if "__pycache__" in path.parts:
continue
try:
tree = ast.parse(path.read_text())
except (OSError, SyntaxError):
continue
for node in tree.body:
if (
isinstance(node, ast.If)
and isinstance(node.test, ast.Compare)
and isinstance(node.test.left, ast.Name)
and node.test.left.id == "__name__"
and any(isinstance(c, ast.Constant) and c.value == "__main__" for c in node.test.comparators)
):
found.add(module_name(path, root))
return found
def _reachable(graph: Mapping[str, set[str]], roots: Iterable[str]) -> set[str]:
seen: set[str] = set()
stack = [root for root in roots if root in graph]
while stack:
current = stack.pop()
if current in seen:
continue
seen.add(current)
for target in graph.get(current, ()):
# an import of `a.b` also keeps the package `a` alive
for candidate in (target, target.rsplit(".", 1)[0]):
if candidate in graph and candidate not in seen:
stack.append(candidate)
return seen
def audit(root: Path) -> tuple[ModuleAudit, ...]:
"""Classify every module under ``root``."""
graph = import_graph(root)
importers: dict[str, set[str]] = {name: set() for name in graph}
for name, targets in graph.items():
for target in targets:
for candidate in (target, target.rsplit(".", 1)[0]):
if candidate in importers and candidate != name:
importers[candidate].add(name)
run_by_name = main_modules(root)
live = _reachable(graph, (*ROOTS, *sorted(run_by_name)))
results: list[ModuleAudit] = []
for name in sorted(graph):
if not name: # the package __init__ itself
continue
who = tuple(sorted(importers[name]))
listed = SPEC_OWNED.get(name) or STANDALONE.get(name)
# Reachability decides; the lists only supply reason text. A listed module nothing reaches is still
# orphaned -- the list is what hid five dead modules behind "load_bearing" until 2026-10-02.
if name not in live:
reason = "no import path from an entry point or a python -m module"
if who:
reason += f" (imported only by unreachable {', '.join(who)})"
if listed:
reason += f"; listed as: {listed}"
results.append(ModuleAudit(name, "orphaned", reason, who))
elif name in STANDALONE:
results.append(ModuleAudit(name, "standalone_capability", STANDALONE[name], who))
elif name in SPEC_OWNED:
results.append(ModuleAudit(name, "load_bearing", SPEC_OWNED[name], who))
elif name in ROOTS:
results.append(ModuleAudit(name, "load_bearing", "entry point", who))
elif name in run_by_name:
results.append(ModuleAudit(name, "load_bearing", f"python -m openultrasast.{name}", who))
else:
results.append(ModuleAudit(name, "load_bearing", f"reachable from {', '.join(ROOTS[:4])}", who))
return tuple(results)
def to_dict(results: Iterable[ModuleAudit]) -> dict[str, object]:
rows = list(results)
counts: dict[str, int] = {}
for row in rows:
counts[row.classification] = counts.get(row.classification, 0) + 1
return {
"modules": len(rows),
"counts": dict(sorted(counts.items())),
"orphaned": [row.module for row in rows if row.classification == "orphaned"],
"standalone": {row.module: row.reason for row in rows if row.classification == "standalone_capability"},
"rows": [
{"module": row.module, "classification": row.classification, "reason": row.reason, "importers": list(row.importers)}
for row in rows
],
}
__all__ = ["ModuleAudit", "SPEC_OWNED", "STANDALONE", "audit", "import_graph", "main_modules", "module_name", "to_dict"]
if __name__ == "__main__": # python -m openultrasast.model.audit [SRC] -- the manifest's regeneration command
import json
import sys
source = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parents[1]
print(json.dumps(to_dict(audit(source)), indent=2))
-36
View File
@@ -1,36 +0,0 @@
"""Isolated regression snippets and verdicts."""
from .candidate import CandidateVerdict, RegressionRunner, run_regression, select_candidates, write_verdicts
from .recipes import recipe_for
from .safety import UnsafeSnippetError, check_snippet_safety
from .verdict import (
ALREADY_COVERED,
INCONCLUSIVE,
MISSING_RECIPE,
NOT_TRIGGERABLE,
SAFETY_REJECTED,
TRIGGERABLE,
RegressionVerdict,
is_worth_fixing,
verdict_from_result,
)
__all__ = [
"ALREADY_COVERED",
"INCONCLUSIVE",
"MISSING_RECIPE",
"NOT_TRIGGERABLE",
"SAFETY_REJECTED",
"TRIGGERABLE",
"CandidateVerdict",
"RegressionRunner",
"RegressionVerdict",
"UnsafeSnippetError",
"check_snippet_safety",
"is_worth_fixing",
"recipe_for",
"run_regression",
"select_candidates",
"verdict_from_result",
"write_verdicts",
]
-251
View File
@@ -1,251 +0,0 @@
"""Candidate selection and safety-gated regression execution."""
from __future__ import annotations
import json
from collections.abc import Callable, Mapping, Sequence
from dataclasses import asdict, dataclass, replace
from pathlib import Path
from ..complexity.ledger import hotspot_key, select_forced_candidates
from ..complexity.map import Hotspot
from ..config import SandboxConfig
from ..findings import StaticFinding
from ..preprocess import LANGUAGE_BY_EXTENSION
from ..sandbox import SandboxJob, SandboxResult, SandboxRunner
from ..sandbox.runner import WORKSPACE_MOUNT
from .recipes import recipe_for
from .safety import UnsafeSnippetError, check_snippet_safety
from .verdict import (
INCONCLUSIVE,
MISSING_RECIPE,
NOT_TRIGGERABLE,
SAFETY_REJECTED,
TRIGGERABLE,
RegressionVerdict,
is_worth_fixing,
verdict_from_result,
)
DEFAULT_IMAGES = {
"python": "python:3.12-alpine",
"javascript": "node:22-alpine",
"c": "gcc:13",
}
__all__ = [
"INCONCLUSIVE",
"MISSING_RECIPE",
"NOT_TRIGGERABLE",
"SAFETY_REJECTED",
"TRIGGERABLE",
"CandidateVerdict",
"RegressionRunner",
"RegressionVerdict",
"run_regression",
"select_candidates",
"snippet_for",
"write_verdicts",
]
@dataclass(frozen=True)
class CandidateVerdict:
path: str
function_name: str | None
language: str
verdict: str
reason: str
inventory_finding_ids: tuple[str, ...]
worth_fixing: bool = False
def select_candidates(
hotspots: Sequence[Hotspot],
findings: Sequence[StaticFinding],
*,
max_candidates: int,
policy_severity_by_id: Mapping[str, object] | None = None,
rule_cwe: Mapping[str, str] | None = None,
) -> tuple[Hotspot, ...]:
"""Select min(max_candidates, ranked hotspots) ∪ sev-5 reachable inventory.
The cap applies to the ranked hotspot slice. Forced policy-severity-5
reachable (or inferred-file-surface) inventory stays in the set even when a
ledger overlay demoted its hotspot out of that slice.
"""
ranked = tuple(sorted(hotspots, key=lambda item: (-item.score, item.path, item.function_name or "")))
selected = list(ranked[: max(max_candidates, 0)])
seen = {hotspot_key(item.path, item.function_name) for item in selected}
extras: list[Hotspot] = []
for finding in select_forced_candidates(findings, policy_severity_by_id or {}, rule_cwe=rule_cwe):
hotspot = _hotspot_for_finding(finding, ranked) or _hotspot_from_forced_finding(finding)
key = hotspot_key(hotspot.path, hotspot.function_name)
if key in seen:
continue
seen.add(key)
extras.append(hotspot)
return tuple(selected + extras)
def _hotspot_for_finding(finding: StaticFinding, hotspots: Sequence[Hotspot]) -> Hotspot | None:
for hotspot in hotspots:
if finding.finding_id in hotspot.inventory_finding_ids:
return hotspot
key = hotspot_key(finding.path, finding.function_name)
for hotspot in hotspots:
if hotspot_key(hotspot.path, hotspot.function_name) == key:
return hotspot
return None
def hotspot_from_finding(finding: StaticFinding, *, rationale: str) -> Hotspot:
"""A zero-score hotspot wrapping one finding: forced sev-5 inventory or a variant of a known mechanism."""
return Hotspot(
path=finding.path,
function_name=finding.function_name,
score=0.0,
band="low",
signals={},
rationale=rationale,
test_hint=None,
inventory_finding_ids=(finding.finding_id,),
)
def _hotspot_from_forced_finding(finding: StaticFinding) -> Hotspot:
return hotspot_from_finding(finding, rationale="forced reachable severity-5 inventory")
class RegressionRunner:
"""Run a snippet in the sandbox only after the structural safety check passes."""
def __init__(self, sandbox: SandboxRunner) -> None:
self._sandbox = sandbox
def run_recipe(
self,
language: str,
snippet: str,
image: str,
sandbox_limits: SandboxConfig,
*,
repo_root: Path,
oracle: Callable[[SandboxResult], bool] | None = None,
) -> RegressionVerdict:
job = recipe_for(language, snippet, image, sandbox_limits, repo_root=repo_root)
if job is None:
return verdict_from_result(recipe_missing=True)
return self.run_snippet(snippet, job, oracle=oracle)
def run_snippet(
self, snippet: str, job: SandboxJob | None, *, oracle: Callable[[SandboxResult], bool] | None = None
) -> RegressionVerdict:
if job is None:
return verdict_from_result(recipe_missing=True)
try:
check_snippet_safety(snippet)
except UnsafeSnippetError:
return verdict_from_result(safety_rejected=True)
return verdict_from_result(self._sandbox.run(job), oracle=oracle)
def snippet_for(language: str, path: str, function_name: str | None) -> str:
"""Render a scratch snippet that loads the candidate source inside the sandbox."""
relative = path.replace("\\", "/")
workspace_path = f"{WORKSPACE_MOUNT}/{relative}"
identity = function_name or relative
if language == "python":
return (
f"# candidate {identity}\n"
"from pathlib import Path\n"
f"source = Path({workspace_path!r}).read_text()\n"
f"compile(source, {relative!r}, 'exec')\n"
)
if language == "javascript":
return (
f"// candidate {identity}\n"
"const fs = require('fs');\n"
f"const source = fs.readFileSync({workspace_path!r}, 'utf8');\n"
"if (!source.length) { process.exit(1); }\n"
)
if language == "c":
return f"/* candidate {identity} */\nint main(void) {{ return 0; }}\n"
return ""
def run_regression(
hotspots: Sequence[Hotspot],
findings: Sequence[StaticFinding],
*,
max_candidates: int,
policy: Mapping[str, object],
rule_cwe: Mapping[str, str],
languages_by_path: Mapping[str, str],
repo_root: Path,
sandbox: SandboxRunner,
sandbox_limits: SandboxConfig,
images: Mapping[str, str],
) -> tuple[CandidateVerdict, ...]:
"""Select candidates under the cap, run recipes in the sandbox, and return verdicts."""
selected = select_candidates(
hotspots,
findings,
max_candidates=max_candidates,
policy_severity_by_id=policy,
rule_cwe=rule_cwe,
)
runner = RegressionRunner(sandbox)
findings_by_id = {finding.finding_id: finding for finding in findings}
records: list[CandidateVerdict] = []
for hotspot in selected:
language = _language_for(hotspot.path, languages_by_path)
snippet = _proposed_snippet(hotspot, findings) or snippet_for(language, hotspot.path, hotspot.function_name)
image = images.get(language) or DEFAULT_IMAGES.get(language, "ousast-missing-image")
mapped = runner.run_recipe(language, snippet, image, sandbox_limits, repo_root=repo_root)
reachability = _reachability_for(hotspot, findings_by_id)
mapped = replace(mapped, worth_fixing=is_worth_fixing(mapped.verdict, reachability))
records.append(
CandidateVerdict(
path=hotspot.path,
function_name=hotspot.function_name,
language=language,
verdict=mapped.verdict,
reason=mapped.reason,
inventory_finding_ids=hotspot.inventory_finding_ids,
worth_fixing=mapped.worth_fixing,
)
)
return tuple(records)
def write_verdicts(verdicts: Sequence[CandidateVerdict], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
payload = {"verdicts": [asdict(item) for item in verdicts]}
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
def _proposed_snippet(hotspot: Hotspot, findings: Sequence[StaticFinding]) -> str | None:
for finding in findings:
snippet = finding.proposed_snippet
if not snippet:
continue
if finding.path == hotspot.path or finding.finding_id in hotspot.inventory_finding_ids:
return snippet
return None
def _language_for(path: str, languages_by_path: Mapping[str, str]) -> str:
if path in languages_by_path:
return languages_by_path[path]
return LANGUAGE_BY_EXTENSION.get(Path(path).suffix.lower(), "unknown")
def _reachability_for(hotspot: Hotspot, findings_by_id: Mapping[str, StaticFinding]) -> str:
statuses = [
findings_by_id[finding_id].reachability_status for finding_id in hotspot.inventory_finding_ids if finding_id in findings_by_id
]
for status in statuses:
if is_worth_fixing(TRIGGERABLE, status):
return status
return statuses[0] if statuses else "unknown"
-71
View File
@@ -1,71 +0,0 @@
"""Language command recipes for isolated regression snippets."""
from __future__ import annotations
from pathlib import Path
from ..config import SandboxConfig
from ..sandbox.runner import SCRATCH_MOUNT, WORKSPACE_MOUNT, SandboxJob
_PYTHON_FILE = "case.py"
_JS_FILE = "case.js"
_C_FILE = "case.c"
_C_BIN = "case"
def recipe_for(
language: str,
snippet: str,
image: str,
sandbox_limits: SandboxConfig,
*,
repo_root: Path,
) -> SandboxJob | None:
"""Return a sandbox job for `language`, or None when no recipe exists."""
key = language.strip().lower()
if key == "python":
return _job(
image,
sandbox_limits,
repo_root,
command=("env", f"PYTHONPATH={WORKSPACE_MOUNT}", "python", f"{SCRATCH_MOUNT}/{_PYTHON_FILE}"),
scratch_files={_PYTHON_FILE: snippet},
)
if key == "javascript":
return _job(
image,
sandbox_limits,
repo_root,
command=("env", f"NODE_PATH={WORKSPACE_MOUNT}", "node", f"{SCRATCH_MOUNT}/{_JS_FILE}"),
scratch_files={_JS_FILE: snippet},
)
if key == "c":
source = f"{SCRATCH_MOUNT}/{_C_FILE}"
binary = f"{SCRATCH_MOUNT}/{_C_BIN}"
return _job(
image,
sandbox_limits,
repo_root,
command=("sh", "-c", f"cc {source} -o {binary} && {binary}"),
scratch_files={_C_FILE: snippet},
)
return None
def _job(
image: str,
sandbox_limits: SandboxConfig,
repo_root: Path,
*,
command: tuple[str, ...],
scratch_files: dict[str, str],
) -> SandboxJob:
return SandboxJob(
image=image,
command=command,
repo_root=repo_root,
scratch_files=scratch_files,
timeout_seconds=sandbox_limits.timeout_seconds,
memory_mb=sandbox_limits.memory_mb,
pids_limit=sandbox_limits.pids_limit,
)
-81
View File
@@ -1,81 +0,0 @@
"""Structural deny-list for untrusted regression snippets.
Snippets are data for `/scratch` inside the sandbox. Mentions of sockets, curl,
host networking, or writes under the source mount (`/workspace`) are rejected
before any `SandboxRunner.run` call.
"""
from __future__ import annotations
import re
from ..sandbox.runner import WORKSPACE_MOUNT
_WORKSPACE = re.escape(WORKSPACE_MOUNT)
class UnsafeSnippetError(ValueError):
"""Raised when a snippet fails the structural sandbox safety check."""
_HOST_NETWORK = re.compile(
r"--net(?:work)?(?:\s*=\s*|\s+|['\"]\s*,\s*['\"])host\b",
re.IGNORECASE,
)
_SOCKET_WORD = re.compile(r"\bsocket\b", re.IGNORECASE)
_CURL_WORD = re.compile(r"\bcurl\b", re.IGNORECASE)
_OPEN_WORKSPACE_WRITE = re.compile(
rf"open\s*\(\s*[fFrRbBuU]*['\"]{_WORKSPACE}[^'\"]*['\"]\s*,\s*(?:mode\s*=\s*)?[fFrRbBuU]*['\"][^'\"]*[wax+]",
re.IGNORECASE | re.DOTALL,
)
_PATH_WORKSPACE_WRITE = re.compile(
rf"Path\s*\(\s*['\"]{_WORKSPACE}[^'\"]*['\"]\s*\)\s*\.\s*"
r"(?:write_text|write_bytes|touch|mkdir|unlink|rmdir|replace|chmod|rename|open)\b",
re.IGNORECASE | re.DOTALL,
)
_FS_WORKSPACE_WRITE = re.compile(
r"(?:writeFile(?:Sync)?|appendFile(?:Sync)?|mkdir(?:Sync)?|rm(?:dir)?(?:Sync)?|"
rf"unlink(?:Sync)?|rename(?:Sync)?)\s*\(\s*['\"]{_WORKSPACE}",
re.IGNORECASE,
)
_FOPEN_WORKSPACE_WRITE = re.compile(
rf"fopen\s*\(\s*['\"]{_WORKSPACE}[^'\"]*['\"]\s*,\s*['\"][^'\"]*[wax+]",
re.IGNORECASE | re.DOTALL,
)
_SHUTIL_OS_WORKSPACE_WRITE = re.compile(
r"(?:shutil\.(?:copy(?:2)?|copyfile|copytree|move|rmtree)|"
r"os\.(?:remove|unlink|replace|rename|mkdir|makedirs|rmdir|chmod))"
rf"\s*\([^;]{{0,240}}{_WORKSPACE}",
re.IGNORECASE | re.DOTALL,
)
_REDIRECT_WORKSPACE = re.compile(rf"(?:>>|>)\s*['\"]?{_WORKSPACE}\b", re.IGNORECASE)
_WORKSPACE_WRITES = (
_OPEN_WORKSPACE_WRITE,
_PATH_WORKSPACE_WRITE,
_FS_WORKSPACE_WRITE,
_FOPEN_WORKSPACE_WRITE,
_SHUTIL_OS_WORKSPACE_WRITE,
_REDIRECT_WORKSPACE,
)
def check_snippet_safety(snippet: str) -> None:
"""Return None when `snippet` is structurally safe; raise otherwise."""
lowered = snippet.lower()
if "docker.sock" in lowered:
raise UnsafeSnippetError("snippet mentions the Docker socket")
if _SOCKET_WORD.search(snippet) is not None:
raise UnsafeSnippetError("snippet mentions socket")
if _CURL_WORD.search(snippet) is not None:
raise UnsafeSnippetError("snippet mentions curl")
if _HOST_NETWORK.search(snippet) is not None:
raise UnsafeSnippetError("snippet requests host network")
if _writes_under_workspace(snippet):
raise UnsafeSnippetError(f"snippet writes under {WORKSPACE_MOUNT}")
def _writes_under_workspace(snippet: str) -> bool:
if WORKSPACE_MOUNT not in snippet.lower():
return False
return any(pattern.search(snippet) is not None for pattern in _WORKSPACE_WRITES)
-110
View File
@@ -1,110 +0,0 @@
"""Map sandbox outcomes to verdicts and the worth-fixing gate."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from ..sandbox import SandboxResult
TRIGGERABLE = "triggerable"
NOT_TRIGGERABLE = "not_triggerable"
ALREADY_COVERED = "already_covered"
INCONCLUSIVE = "inconclusive"
MISSING_RECIPE = "missing_recipe"
SAFETY_REJECTED = "safety_rejected"
SANDBOX_UNAVAILABLE = "sandbox_unavailable"
TIMEOUT = "timeout"
COVERED_TEST_PASSED = "covered_test_passed"
ASSERTION = "assertion"
SANITIZER_ABORT = "sanitizer_abort"
CRASH = "crash"
NONZERO_EXIT = "nonzero_exit"
EXIT_ZERO = "exit_zero"
ORACLE_FIRED = "oracle_fired"
ORACLE_QUIET = "oracle_quiet"
WORTH_FIXING_REACHABILITY = frozenset({"reachable", "inferred-file-surface"})
_SANITIZER_MARKERS = (
"AddressSanitizer",
"UndefinedBehaviorSanitizer",
"ThreadSanitizer",
"MemorySanitizer",
"LeakSanitizer",
)
_ASSERTION_MARKERS = (
"AssertionError",
"Assertion failed",
"assertion failed",
"assert failed",
)
_CRASH_MARKERS = (
"Fatal Python error",
"Segmentation fault",
"SIGSEGV",
"SIGABRT",
"Aborted",
)
@dataclass(frozen=True)
class RegressionVerdict:
verdict: str
reason: str = ""
worth_fixing: bool = False
def is_worth_fixing(verdict: str, reachability: str) -> bool:
"""True iff the candidate is triggerable on a reachable or inferred-file-surface finding."""
return verdict == TRIGGERABLE and reachability in WORTH_FIXING_REACHABILITY
def verdict_from_result(
result: SandboxResult | None = None,
*,
recipe_missing: bool = False,
sandbox_missing: bool = False,
safety_rejected: bool = False,
covering_test: bool = False,
oracle: Callable[[SandboxResult], bool] | None = None,
) -> RegressionVerdict:
"""Map a sandbox observation onto one of the four regression verdicts.
``oracle`` is how a family says what "the vulnerability fired" means for it: when one is given it
decides triggerability instead of the exit code, because a crash and a demonstrated injection are
not the same evidence. An inconclusive run stays inconclusive either way.
"""
if recipe_missing:
return RegressionVerdict(verdict=INCONCLUSIVE, reason=MISSING_RECIPE)
if sandbox_missing:
return RegressionVerdict(verdict=INCONCLUSIVE, reason=SANDBOX_UNAVAILABLE)
if safety_rejected:
return RegressionVerdict(verdict=INCONCLUSIVE, reason=SAFETY_REJECTED)
if result is None:
return RegressionVerdict(verdict=INCONCLUSIVE, reason=SANDBOX_UNAVAILABLE)
if result.timed_out:
return RegressionVerdict(verdict=INCONCLUSIVE, reason=TIMEOUT)
if covering_test and result.exit_code == 0:
return RegressionVerdict(verdict=ALREADY_COVERED, reason=COVERED_TEST_PASSED)
if oracle is not None:
return (
RegressionVerdict(verdict=TRIGGERABLE, reason=ORACLE_FIRED)
if oracle(result)
else RegressionVerdict(verdict=NOT_TRIGGERABLE, reason=ORACLE_QUIET)
)
if result.exit_code != 0:
return RegressionVerdict(verdict=TRIGGERABLE, reason=_nonzero_reason(result))
return RegressionVerdict(verdict=NOT_TRIGGERABLE, reason=EXIT_ZERO)
def _nonzero_reason(result: SandboxResult) -> str:
text = f"{result.stdout}\n{result.stderr}".lower()
if any(marker.lower() in text for marker in _SANITIZER_MARKERS):
return SANITIZER_ABORT
if any(marker.lower() in text for marker in _ASSERTION_MARKERS):
return ASSERTION
if any(marker.lower() in text for marker in _CRASH_MARKERS):
return CRASH
return NONZERO_EXIT
+1 -29
View File
@@ -17,7 +17,6 @@ def write_markdown_report(
*,
redact: bool = True,
complexity_map: object | None = None,
verdicts: Sequence[object] | None = None,
overlay: Sequence[object] | None = None,
mechanisms: Mapping[str, Mapping[str, object]] | None = None,
obligations: Mapping[str, Mapping[str, object]] | None = None,
@@ -103,8 +102,6 @@ def write_markdown_report(
_append_overlay(lines, overlay)
if complexity_map is not None:
_append_complexity_map(lines, complexity_map)
if verdicts is not None:
_append_worth_fixing(lines, verdicts)
report = "\n".join(lines).rstrip() + "\n"
if redact:
from .redaction import redact_secrets
@@ -149,25 +146,6 @@ def _append_complexity_map(lines: list[str], complexity_map: object) -> None:
lines.extend(["", rationale, ""] if rationale else [""])
def _append_worth_fixing(lines: list[str], verdicts: Sequence[object]) -> None:
worth = [item for item in verdicts if getattr(item, "worth_fixing", False)]
lines.extend(["## Worth fixing", "", f"Count: {len(worth)}", ""])
if not worth:
lines.extend(["No worth-fixing verdicts were produced.", ""])
return
for item in worth:
identity = _report_identity(getattr(item, "path", ""), getattr(item, "function_name", None))
lines.extend(
[
f"### `{identity}`",
"",
f"- Verdict: `{getattr(item, 'verdict', '')}`",
f"- Reason: `{getattr(item, 'reason', '')}`",
"",
]
)
def _report_identity(path: object, function_name: object) -> str:
path_text = str(path or "")
if function_name:
@@ -227,7 +205,6 @@ def write_manifest(
fusion: list[dict[str, object]] | None = None,
stages: dict[str, object] | None = None,
complexity: dict[str, object] | None = None,
worth_fixing: dict[str, object] | None = None,
provenance: dict[str, object] | None = None,
variants: dict[str, object] | None = None,
obligations: dict[str, object] | None = None,
@@ -265,8 +242,6 @@ def write_manifest(
payload["stages"] = stages
if complexity is not None:
payload["complexity"] = complexity
if worth_fixing is not None:
payload["worth_fixing"] = worth_fixing
if provenance is not None:
payload["provenance"] = provenance
if variants is not None:
@@ -282,7 +257,6 @@ def scan_exit_code(
findings: list[StaticFinding],
verifications: list[VerificationResult],
policy: str,
worth_fixing_verdicts: Sequence[object] | None = None,
) -> int:
if policy == "never":
return 0
@@ -290,8 +264,6 @@ def scan_exit_code(
return 1 if findings else 0
if policy == "verified":
return 1 if any(result.verified for result in verifications) else 0
if policy == "worth-fixing":
return 1 if any(getattr(verdict, "worth_fixing", False) for verdict in worth_fixing_verdicts or ()) else 0
raise ValueError(f"unknown fail policy: {policy}")
@@ -463,7 +435,7 @@ _COVERAGE_DEGRADATIONS: dict[str, str] = {
"policy_invalid": "The `{stage}` policy could not be loaded, so that stage decided nothing. {detail}",
"max_findings_exceeded": "Only the first {requested} of {actual} findings are reported (`max_findings`).",
"hunter_model_unavailable": "The `{stage}` stage needs a model and none is available, so it was skipped.",
"sandbox_unavailable": "Docker is not available, so no finding was reproduced in the sandbox (REGRESS skipped).",
"regress_retired": "the sandboxed snippet stage was retired on 2026-10-13; execution proof comes from the search-with-proof arm",
"language_not_covered": (
"language not covered: {language} ({count} files). No quick rules{engine} exist for it, so **silence about "
"these files is not a clean result**."
+3 -23
View File
@@ -1,25 +1,5 @@
"""Isolated Docker CLI sandbox."""
"""Shared sandbox job and result types."""
from .probe import FakeSandboxProbe, SandboxProbe, resolve_sandbox_probe
from .runner import (
DockerCliRunner,
FakeSandboxRunner,
SandboxJob,
SandboxResult,
SandboxRunner,
build_docker_argv,
resolve_sandbox_runner,
)
from .runner import SandboxJob, SandboxResult
__all__ = [
"DockerCliRunner",
"FakeSandboxProbe",
"FakeSandboxRunner",
"SandboxJob",
"SandboxProbe",
"SandboxResult",
"SandboxRunner",
"build_docker_argv",
"resolve_sandbox_probe",
"resolve_sandbox_runner",
]
__all__ = ["SandboxJob", "SandboxResult"]
-56
View File
@@ -1,56 +0,0 @@
"""Docker CLI presence and usability probe."""
from __future__ import annotations
import os
import subprocess
from collections.abc import Callable
from subprocess import CompletedProcess
PROBE_TIMEOUT_SECONDS = 5
PROBE_ENV = "OPENULTRASAST_SANDBOX_PROBE"
_DOCKER_INFO = ("docker", "info")
_PROBE_OFF = frozenset({"0", "off", "false", "unavailable", "no"})
_PROBE_ON = frozenset({"1", "on", "true", "available", "yes"})
class SandboxProbe:
"""Report whether `docker info` succeeds without raising into the scan."""
def __init__(self, *, runner: Callable[..., CompletedProcess[str]] | None = None) -> None:
self._runner = runner
def available(self) -> bool:
run = self._runner if self._runner is not None else subprocess.run
try:
result = run(
list(_DOCKER_INFO),
capture_output=True,
text=True,
timeout=PROBE_TIMEOUT_SECONDS,
check=False,
)
return result.returncode == 0
except Exception:
return False
class FakeSandboxProbe:
"""Test double that reports a programmed availability without touching Docker."""
def __init__(self, available: bool) -> None:
self._available = available
def available(self) -> bool:
return self._available
def resolve_sandbox_probe() -> SandboxProbe | FakeSandboxProbe:
"""Return a real probe, or a fake when OPENULTRASAST_SANDBOX_PROBE is set."""
flag = os.environ.get(PROBE_ENV, "").strip().lower()
if flag in _PROBE_OFF:
return FakeSandboxProbe(False)
if flag in _PROBE_ON:
return FakeSandboxProbe(True)
return SandboxProbe()
+1 -175
View File
@@ -1,28 +1,9 @@
"""Docker CLI sandbox runner that never mounts the host Docker socket."""
"""Job and result types shared by the search sandbox and verifier."""
from __future__ import annotations
import os
import subprocess
import tempfile
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from pathlib import Path
from subprocess import CompletedProcess
from typing import Protocol
NONROOT_USER = "65534:65534"
WORKSPACE_MOUNT = "/workspace"
SCRATCH_MOUNT = "/scratch"
SCRATCH_SEED_MOUNT = "/scratch-in"
RUNNER_ENV = "OPENULTRASAST_SANDBOX_RUNNER"
_COPY_SEED_THEN_EXEC = 'cp -r /scratch-in/. /scratch/ && exec "$@"'
_SEED_ARGV0 = "ousast-sandbox"
_FAKE_RUNNER = frozenset({"fake"})
class SandboxRunner(Protocol):
def run(self, spec: SandboxJob) -> SandboxResult: ...
@dataclass(frozen=True)
@@ -43,158 +24,3 @@ class SandboxResult:
stderr: str
timed_out: bool
class FakeSandboxRunner:
"""Record jobs and return a programmed result without a Docker daemon."""
def __init__(self, result: SandboxResult | None = None) -> None:
self.jobs: list[SandboxJob] = []
self.result = result if result is not None else SandboxResult(exit_code=0, stdout="", stderr="", timed_out=False)
def run(self, spec: SandboxJob) -> SandboxResult:
self.jobs.append(spec)
return self.result
def resolve_sandbox_runner() -> SandboxRunner:
"""Return the Docker CLI runner, or a fake when OPENULTRASAST_SANDBOX_RUNNER=fake."""
flag = os.environ.get(RUNNER_ENV, "").strip().lower()
if flag in _FAKE_RUNNER:
return FakeSandboxRunner()
return DockerCliRunner()
class DockerCliRunner:
"""Run a job with Docker CLI flags that match the threat model."""
def __init__(self, *, runner: Callable[..., CompletedProcess[str]] | None = None) -> None:
self._runner = runner
def run(self, spec: SandboxJob) -> SandboxResult:
if spec.scratch_files:
with tempfile.TemporaryDirectory(prefix="ousast-scratch-") as tmp:
seed = Path(tmp)
_write_scratch_files(seed, spec.scratch_files)
return self._invoke(_argv_with_scratch_seed(spec, seed), spec)
return self._invoke(build_docker_argv(spec), spec)
def _invoke(self, argv: list[str], spec: SandboxJob) -> SandboxResult:
_reject_unsafe_argv(argv)
run = self._runner if self._runner is not None else subprocess.run
try:
completed = run(
argv,
capture_output=True,
text=True,
timeout=spec.timeout_seconds,
check=False,
)
except subprocess.TimeoutExpired as exc:
return SandboxResult(
exit_code=-1,
stdout=_as_text(exc.stdout),
stderr=_as_text(exc.stderr),
timed_out=True,
)
return SandboxResult(
exit_code=completed.returncode,
stdout=completed.stdout or "",
stderr=completed.stderr or "",
timed_out=False,
)
def build_docker_argv(job: SandboxJob) -> list[str]:
argv = [*_isolation_argv(job), job.image, *job.command]
_reject_unsafe_argv(argv)
return argv
def _isolation_argv(job: SandboxJob) -> list[str]:
repo = str(job.repo_root.resolve())
return [
"docker",
"run",
"--rm",
"--init",
"--network",
"none",
"--read-only",
"--tmpfs",
f"{SCRATCH_MOUNT}:rw,exec",
"--memory",
f"{job.memory_mb}m",
"--pids-limit",
str(job.pids_limit),
"--cap-drop",
"ALL",
"--security-opt",
"no-new-privileges",
"--user",
NONROOT_USER,
"--mount",
f"type=bind,src={repo},dst={WORKSPACE_MOUNT},ro",
]
def _argv_with_scratch_seed(job: SandboxJob, seed: Path) -> list[str]:
argv = [
*_isolation_argv(job),
"--mount",
f"type=bind,src={seed.resolve()},dst={SCRATCH_SEED_MOUNT},ro",
job.image,
"/bin/sh",
"-c",
_COPY_SEED_THEN_EXEC,
_SEED_ARGV0,
*job.command,
]
_reject_unsafe_argv(argv)
return argv
def _write_scratch_files(root: Path, files: Mapping[str, str]) -> None:
resolved_root = root.resolve()
for relative, content in files.items():
candidate = Path(relative)
if candidate.is_absolute():
raise ValueError(f"scratch path {relative!r} must be relative to {SCRATCH_MOUNT}")
destination = (resolved_root / candidate).resolve()
try:
common = os.path.commonpath([str(resolved_root), str(destination)])
except ValueError as exc:
raise ValueError(f"scratch path {relative!r} escapes {SCRATCH_MOUNT}") from exc
if common != str(resolved_root):
raise ValueError(f"scratch path {relative!r} escapes {SCRATCH_MOUNT}")
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(content)
_chmod_seed_tree(resolved_root)
def _chmod_seed_tree(root: Path) -> None:
# Container user 65534 must be able to traverse and read the seed bind.
for dirpath, _dirnames, filenames in os.walk(root):
Path(dirpath).chmod(0o755)
for name in filenames:
(Path(dirpath) / name).chmod(0o644)
def _reject_unsafe_argv(argv: list[str]) -> None:
joined = " ".join(argv)
if "/var/run/docker.sock" in joined:
raise ValueError("sandbox argv must not mount the host Docker socket")
if "--privileged" in argv:
raise ValueError("sandbox argv must not be privileged")
for index, item in enumerate(argv):
if item in {"--network", "--net"} and index + 1 < len(argv) and argv[index + 1] == "host":
raise ValueError("sandbox argv must not use host network")
if item in {"--network=host", "--net=host"}:
raise ValueError("sandbox argv must not use host network")
def _as_text(value: str | bytes | None) -> str:
if value is None:
return ""
if isinstance(value, bytes):
return value.decode(errors="replace")
return value
-372
View File
@@ -1,372 +0,0 @@
"""Trail of Bits skill index + router (Phase 14 of the retired openrouter-sast-harness plan).
Security skills are scoped expertise, not global prompt bloat. Each skill is a small
descriptor with routing metadata (languages, tags, vulnerability classes, stages) and
a short operating checklist. The router selects the most relevant skills for a target
and stage; ``select_skill_context`` packs their checklists into a char budget; and
``skill_chunks`` chunks the guidance into the ``skills`` retrieval namespace. Nothing
here imports a model or the optional extra — it is pure routing data.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from .index import CodeChunk, chunk_text_namespace
from .preprocess import FileTarget
# Pipeline stages a skill can apply to.
STAGES = ("map", "hunt", "verify", "fix")
# FileTarget tags → vulnerability classes the router reasons over.
TAG_TO_VULN = {
"memory_unsafe": "memory-safety",
"parser": "parsing",
"fuzzable": "parsing",
"crypto": "crypto",
"deserialization": "deserialization",
"auth_boundary": "access-control",
"network_entry": "access-control",
"privileged": "access-control",
}
@dataclass(frozen=True)
class SkillDescriptor:
id: str
title: str
summary: str
checklist: str
languages: tuple[str, ...] = () # empty => language-agnostic
tags: tuple[str, ...] = ()
vulnerability_classes: tuple[str, ...] = ()
stages: tuple[str, ...] = () # empty => any stage
def descriptor(self) -> dict[str, object]:
return {
"id": self.id,
"title": self.title,
"summary": self.summary,
"languages": list(self.languages),
"tags": list(self.tags),
"vulnerability_classes": list(self.vulnerability_classes),
"stages": list(self.stages),
}
# Curated inventory aligned with the spec's Trail of Bits skill routing examples.
SKILL_INVENTORY: tuple[SkillDescriptor, ...] = (
SkillDescriptor(
"c-review",
"C/C++ security review",
"Manual review discipline for memory-unsafe code.",
"Audit buffer indexing, object lifetime, integer overflow before length math, and unchecked copies.",
languages=("c", "cpp", "c++"),
tags=("memory_unsafe",),
vulnerability_classes=("memory-safety",),
stages=("map", "hunt"),
),
SkillDescriptor(
"address-sanitizer",
"AddressSanitizer",
"Detect memory-safety violations at runtime.",
"Build with -fsanitize=address; reproduce the overflow/UAF; capture the ASan trace as crash evidence.",
languages=("c", "cpp", "c++"),
tags=("memory_unsafe",),
vulnerability_classes=("memory-safety",),
stages=("hunt", "verify"),
),
SkillDescriptor(
"libfuzzer",
"libFuzzer harnessing",
"In-process coverage-guided fuzzing of C/C++ entries.",
"Write a LLVMFuzzerTestOneInput harness over the parser entry; minimize crashing inputs.",
languages=("c", "cpp", "c++"),
tags=("memory_unsafe", "fuzzable", "parser"),
vulnerability_classes=("memory-safety", "parsing"),
stages=("hunt", "verify"),
),
SkillDescriptor(
"harness-writing",
"Fuzz harness writing",
"Build deterministic harnesses around attacker-controlled entries.",
"Identify the smallest attacker-controlled entry; build a deterministic harness; seed a corpus.",
languages=("c", "cpp", "c++"),
tags=("memory_unsafe", "fuzzable", "parser"),
vulnerability_classes=("memory-safety", "parsing"),
stages=("hunt", "verify"),
),
SkillDescriptor(
"fuzzing-dictionary",
"Fuzzing dictionary",
"Reach deep parser states with a format dictionary.",
"Extract format tokens/magic bytes into a fuzzing dictionary to reach deep parser states.",
tags=("parser", "fuzzable"),
vulnerability_classes=("parsing",),
stages=("verify",),
),
SkillDescriptor(
"fuzzing-obstacles",
"Fuzzing obstacles",
"Remove coverage barriers that block the fuzzer.",
"Remove checksums/magic gates that block coverage; document each bypass.",
tags=("parser", "fuzzable"),
vulnerability_classes=("parsing",),
stages=("verify",),
),
SkillDescriptor(
"atheris",
"Atheris (Python fuzzing)",
"Coverage-guided fuzzing for Python parsers.",
"Fuzz the Python parser entry with Atheris; assert on uncaught exceptions and resource blowups.",
languages=("python",),
tags=("parser", "fuzzable"),
vulnerability_classes=("parsing",),
stages=("hunt", "verify"),
),
SkillDescriptor(
"property-based-testing",
"Property-based testing",
"Encode parser invariants as properties.",
"Encode parser invariants as Hypothesis properties; shrink counterexamples.",
languages=("python",),
tags=("parser",),
vulnerability_classes=("parsing",),
stages=("verify",),
),
SkillDescriptor(
"constant-time-analysis",
"Constant-time analysis",
"Find secret-dependent timing in crypto code.",
"Find secret-dependent branches/indexing/early-exit comparisons; require constant-time primitives.",
tags=("crypto",),
vulnerability_classes=("crypto",),
stages=("hunt", "verify"),
),
SkillDescriptor(
"wycheproof",
"Wycheproof vectors",
"Stress crypto usage with known-edge test vectors.",
"Run Wycheproof test vectors against the crypto usage; flag edge-case acceptance.",
tags=("crypto",),
vulnerability_classes=("crypto",),
stages=("verify",),
),
SkillDescriptor(
"vector-forge",
"Crypto vector forging",
"Forge boundary vectors to probe crypto APIs.",
"Forge boundary test vectors (zero/identity/malleable signatures) to probe the crypto API.",
tags=("crypto",),
vulnerability_classes=("crypto",),
stages=("verify",),
),
SkillDescriptor(
"zeroize-audit",
"Secret zeroization audit",
"Verify secrets are wiped after use.",
"Verify secrets are zeroized on drop; check for lingering copies.",
languages=("rust",),
tags=("crypto",),
vulnerability_classes=("crypto",),
stages=("verify",),
),
SkillDescriptor(
"cargo-fuzz",
"cargo-fuzz",
"Coverage-guided fuzzing for Rust entries.",
"Add a cargo-fuzz target over the entry; run under ASan.",
languages=("rust",),
tags=("fuzzable", "parser"),
vulnerability_classes=("memory-safety", "parsing"),
stages=("hunt", "verify"),
),
SkillDescriptor(
"sarif-parsing",
"SARIF parsing",
"Normalize external tool output into the map.",
"Normalize SARIF locations/rules/fingerprints into FileTarget static hints.",
vulnerability_classes=(),
stages=("map",),
),
SkillDescriptor(
"semgrep",
"Semgrep mapping",
"Pattern sinks and iterate variants.",
"Pattern the sink shape; iterate variants; export normalized findings.",
vulnerability_classes=("injection", "deserialization"),
stages=("map",),
),
SkillDescriptor(
"semgrep-rule-creator",
"Semgrep rule creator",
"Author a Semgrep rule for a sink.",
"Author a Semgrep rule for the sink; validate against the fixture.",
tags=("deserialization",),
vulnerability_classes=("injection", "deserialization"),
stages=("map",),
),
SkillDescriptor(
"semgrep-rule-variant-creator",
"Semgrep variant creator",
"Widen sink coverage with variants.",
"Generate sink variants (aliases, wrappers) to widen coverage without false positives.",
vulnerability_classes=("injection",),
stages=("map",),
),
SkillDescriptor(
"codeql",
"CodeQL dataflow",
"Model source→sink taint paths.",
"Model source→sink taint with a CodeQL query; inspect the dataflow path.",
vulnerability_classes=("injection",),
stages=("map",),
),
SkillDescriptor(
"differential-review",
"Differential review",
"Scope the changed attack surface in a diff.",
"Scope the changed attack surface and new trust-boundary crossings in the diff.",
stages=("map", "fix"),
),
SkillDescriptor(
"graph-evolution",
"Graph evolution",
"Bound a patch's blast radius.",
"Trace how the call/dataflow graph changed; bound the patch blast radius.",
stages=("fix",),
),
SkillDescriptor(
"sharp-edges",
"Sharp edges review",
"Map misuse-prone APIs at the boundary.",
"Map misuse-prone APIs, insecure defaults, and confusing configuration at the boundary.",
tags=("auth_boundary", "network_entry"),
vulnerability_classes=("access-control", "api-misuse"),
stages=("map", "hunt"),
),
SkillDescriptor(
"insecure-defaults",
"Insecure defaults review",
"Flag unsafe defaults exposed at entries.",
"Flag unsafe default flags/permissions/timeouts exposed at the entry.",
tags=("auth_boundary", "network_entry"),
vulnerability_classes=("access-control", "api-misuse"),
stages=("map", "hunt"),
),
)
_BY_ID = {skill.id: skill for skill in SKILL_INVENTORY}
@dataclass(frozen=True)
class SkillMatch:
skill: SkillDescriptor
score: int
reasons: list[str] = field(default_factory=list)
def route_skills(
*,
language: str | None = None,
tags: tuple[str, ...] = (),
vulnerability_classes: tuple[str, ...] = (),
stage: str | None = None,
max_snippets: int = 4,
) -> list[SkillDescriptor]:
"""Select the most relevant skills for the given routing signals.
A skill is excluded when its language list is non-empty and ``language`` is not in
it, or its stage list is non-empty and ``stage`` is not in it. Eligibility requires
at least one topical signal (tag/vulnerability-class/stage) — a pure language match
never selects a skill. Ranked by score, then id, for determinism.
"""
tag_set, vuln_set = set(tags), set(vulnerability_classes)
matches: list[SkillMatch] = []
for skill in SKILL_INVENTORY:
if skill.languages and language is not None and language not in skill.languages:
continue
if skill.stages and stage is not None and stage not in skill.stages:
continue
signal = 2 * len(vuln_set & set(skill.vulnerability_classes)) + len(tag_set & set(skill.tags))
if stage is not None and stage in skill.stages:
signal += 1
if signal == 0:
continue # no topical match — do not select on language alone
rank = signal + (3 if skill.languages and language in skill.languages else 0)
matches.append(SkillMatch(skill=skill, score=rank))
matches.sort(key=lambda match: (-match.score, match.skill.id))
return [match.skill for match in matches[:max_snippets]]
def _vuln_classes_for_tags(tags: tuple[str, ...]) -> tuple[str, ...]:
return tuple(sorted({TAG_TO_VULN[tag] for tag in tags if tag in TAG_TO_VULN}))
def route_skills_for_target(target: FileTarget, *, stage: str | None = None, max_snippets: int = 4) -> list[SkillDescriptor]:
"""Route skills for a scan target (language + tags + derived vulnerability classes)."""
tags = tuple(target.tags)
return route_skills(
language=target.language,
tags=tags,
vulnerability_classes=_vuln_classes_for_tags(tags),
stage=stage,
max_snippets=max_snippets,
)
def route_skill_ids(target: FileTarget, *, stage: str | None = None, max_snippets: int = 4) -> list[str]:
return [skill.id for skill in route_skills_for_target(target, stage=stage, max_snippets=max_snippets)]
def select_skill_context(target: FileTarget, *, stage: str | None = None, budget_chars: int, max_snippets: int = 4) -> str:
"""Pack routed skill checklists into a prompt context capped at ``budget_chars``."""
if budget_chars <= 0:
return ""
parts: list[str] = []
used = 0
for skill in route_skills_for_target(target, stage=stage, max_snippets=max_snippets):
snippet = f"### {skill.title}\n{skill.checklist}"
if not parts and len(snippet) > budget_chars:
return snippet[:budget_chars] # first snippet alone exceeds the cap → truncate
if used + len(snippet) + (1 if parts else 0) > budget_chars:
break
used += len(snippet) + (1 if parts else 0)
parts.append(snippet)
return "\n".join(parts)
def skill_chunks(skills: tuple[SkillDescriptor, ...] | None = None, *, max_lines: int = 40) -> list[CodeChunk]:
"""Chunk skill operating guidance into the ``skills`` retrieval namespace."""
chunks: list[CodeChunk] = []
for skill in skills or SKILL_INVENTORY:
text = f"# {skill.title}\n{skill.summary}\n\nChecklist:\n{skill.checklist}"
chunks.extend(
chunk_text_namespace(
namespace="skills",
path=f"skills/{skill.id}.md",
text=text,
metadata={
"skill_id": skill.id,
"languages": ",".join(skill.languages),
"tags": ",".join(skill.tags),
"vulnerability_classes": ",".join(skill.vulnerability_classes),
"stages": ",".join(skill.stages),
},
max_lines=max_lines,
)
)
return chunks
__all__ = [
"SKILL_INVENTORY",
"STAGES",
"TAG_TO_VULN",
"SkillDescriptor",
"route_skill_ids",
"route_skills",
"route_skills_for_target",
"select_skill_context",
"skill_chunks",
]