mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(skills): Phase 14 — Trail of Bits skill index + router
Replace the hardcoded skill-name stub with a real, routable skill index. - skills.py (zero-dep): SkillDescriptor inventory (~22 ToB skills with routing metadata — languages, tags, vulnerability classes, stages — plus a short operating checklist each), aligned with the spec's routing examples. route_skills scores by vulnerability-class/tag/stage signal with a language filter+bonus, and never selects on a pure language match. route_skills_for_target derives vuln classes from tags; select_skill_context packs checklists into a char budget; skill_chunks chunks the guidance into the `skills` retrieval namespace. - hunter.select_skill_snippets now delegates to the router (existing C-memory and crypto+auth routing expectations preserved). - hunter_harness._describe feeds the budget-capped routed skill context into the HarnessX hunter prompt (the genuine consumer of skill_budget_chars). - 8.4 zero-dep guard extended to cover skills.py. tests/test_skills.py (9 tests: C parser → memory/fuzzing, crypto → constant-time/ vector, language-mismatch + language-only exclusion, stage filtering, budget cap, namespace chunking). openrouter-sast-harness Phase 14 checked off. 188 passed, 4 skipped; ruff + mypy clean; detection gate PASS. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9c917dd6aa
commit
3fd0057e5f
@@ -0,0 +1,85 @@
|
||||
"""Trail of Bits skill index + router (Phase 14)."""
|
||||
|
||||
from openultrasast.preprocess import FileTarget
|
||||
from openultrasast.skills import (
|
||||
SKILL_INVENTORY,
|
||||
route_skill_ids,
|
||||
route_skills,
|
||||
route_skills_for_target,
|
||||
select_skill_context,
|
||||
skill_chunks,
|
||||
)
|
||||
|
||||
|
||||
def _target(language: str, tags: list[str]) -> FileTarget:
|
||||
return FileTarget(path="t", absolute_path="/repo/t", language=language, loc=10, tags=tags, has_fuzz_entry_point=False)
|
||||
|
||||
|
||||
# ---- routing by language / tags / vulnerability class -----------------------
|
||||
|
||||
|
||||
def test_c_parser_target_routes_to_memory_and_fuzzing_skills() -> None:
|
||||
ids = route_skill_ids(_target("c", ["memory_unsafe", "parser"]))
|
||||
assert {"address-sanitizer", "harness-writing"} <= set(ids) # memory + fuzzing
|
||||
assert "libfuzzer" in ids
|
||||
|
||||
|
||||
def test_crypto_target_routes_to_constant_time_and_vector_skills() -> None:
|
||||
ids = route_skill_ids(_target("python", ["crypto"]))
|
||||
assert "constant-time-analysis" in ids
|
||||
assert "vector-forge" in ids
|
||||
|
||||
|
||||
def test_language_mismatch_excludes_other_language_skills() -> None:
|
||||
# A C target must not pull Python-only (atheris) or Rust-only (cargo-fuzz) skills.
|
||||
ids = route_skill_ids(_target("c", ["memory_unsafe", "parser", "fuzzable"]))
|
||||
assert "atheris" not in ids
|
||||
assert "cargo-fuzz" not in ids
|
||||
|
||||
|
||||
def test_language_only_match_does_not_select() -> None:
|
||||
# python + crypto must not surface atheris (python but no crypto signal).
|
||||
ids = route_skills_for_target(_target("python", ["crypto"]), max_snippets=10)
|
||||
assert "atheris" not in [skill.id for skill in ids]
|
||||
|
||||
|
||||
# ---- routing by stage -------------------------------------------------------
|
||||
|
||||
|
||||
def test_stage_filter_restricts_to_stage_skills() -> None:
|
||||
# fuzzing-dictionary is verify-only; at the map stage it must not appear, while a
|
||||
# map-stage skill (semgrep) does for an injection signal.
|
||||
verify = route_skills(tags=("parser",), vulnerability_classes=("parsing",), stage="verify", max_snippets=10)
|
||||
mapped = route_skills(tags=("parser",), vulnerability_classes=("parsing",), stage="map", max_snippets=10)
|
||||
assert "fuzzing-dictionary" in {s.id for s in verify}
|
||||
assert "fuzzing-dictionary" not in {s.id for s in mapped}
|
||||
|
||||
|
||||
# ---- prompt budget caps -----------------------------------------------------
|
||||
|
||||
|
||||
def test_select_skill_context_respects_budget() -> None:
|
||||
target = _target("c", ["memory_unsafe", "parser"])
|
||||
full = select_skill_context(target, budget_chars=10_000)
|
||||
capped = select_skill_context(target, budget_chars=80)
|
||||
assert len(capped) <= 80
|
||||
assert len(full) > len(capped)
|
||||
|
||||
|
||||
def test_zero_budget_yields_no_context() -> None:
|
||||
assert select_skill_context(_target("c", ["memory_unsafe"]), budget_chars=0) == ""
|
||||
|
||||
|
||||
# ---- retrieval-namespace chunking + inventory integrity ---------------------
|
||||
|
||||
|
||||
def test_skill_chunks_land_in_the_skills_namespace() -> None:
|
||||
chunks = skill_chunks()
|
||||
assert chunks
|
||||
assert all(chunk.namespace == "skills" for chunk in chunks)
|
||||
assert any(chunk.metadata.get("skill_id") == "address-sanitizer" for chunk in chunks)
|
||||
|
||||
|
||||
def test_inventory_ids_are_unique() -> None:
|
||||
ids = [skill.id for skill in SKILL_INVENTORY]
|
||||
assert len(ids) == len(set(ids))
|
||||
Reference in New Issue
Block a user