mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
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]>
86 lines
3.1 KiB
Python
86 lines
3.1 KiB
Python
"""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))
|