"""model-grounded-detection task 1.1: the security vocabularies survive the modules that carried them. The guard vocabulary lived in `semantic/variants.py`, whose optimisation loop this feature deletes. The source/sink/sanitizer facts and the obligation facts live in `ruleset/` data and are retained, so the specs *derive* from them rather than copying them — a copy would be a second place to update. What must be ported verbatim is the guard vocabulary, because nothing else holds it. """ from __future__ import annotations import ast from pathlib import Path SPECS = Path("src/openultrasast/model/specs.py") def test_the_guard_vocabulary_is_preserved_verbatim() -> None: from openultrasast.model.specs import GUARD_KINDS assert GUARD_KINDS == ("null_test", "bounds_test", "allowlist_test", "auth_check", "parameterized_call", "type_change", "none") def test_the_guard_classifier_still_recognises_each_kind() -> None: """The patterns classify the *fix*, first match wins — the order is part of the vocabulary.""" from openultrasast.model.specs import classify_guard_text assert classify_guard_text('cur.execute("SELECT * FROM t WHERE id = %s", (uid,))') == "parameterized_call" assert classify_guard_text("if not current_user.is_authenticated: abort(403)") == "auth_check" assert classify_guard_text('if name not in ALLOWED: abort(400)') == "allowlist_test" assert classify_guard_text("if len(buf) > 64: return") == "bounds_test" assert classify_guard_text("if value is None: return") == "null_test" assert classify_guard_text("page = int(raw)") == "type_change" assert classify_guard_text("total = total + 1") == "none" def test_the_seeded_families_and_the_named_gaps_are_both_exact() -> None: """What the retained fact data can seed, and what it provably cannot — Req 9.2, a gap is *named*. Task 4.2 closed the gap this test was written to hold open: `path`, `output_encoding`, `untrusted_destination` and `prototype` now have sink facts routing to them by CWE. The set is still asserted exactly, so a family can neither lose its seed nor gain one without this record moving. """ from openultrasast.model.specs import dominance_specs, taint_specs from openultrasast.model.taxonomy import load_families taxonomy = load_families() seeded = set() for language in ("python", "javascript", "java", "c"): seeded |= {spec.family for spec in taint_specs(language=language).values()} seeded |= {spec.family for spec in dominance_specs(language=language).values()} deferred = {"unknown", "memory"} # the declined bucket and the deferred execution tier assert seeded == { "injection", "deserialization", "config_secrets", "access_control", "path", "output_encoding", "untrusted_destination", "prototype", "memory", } gaps = {family.id for family in taxonomy.families} - seeded - deferred assert gaps == set(), f"a family lost its seed: {sorted(gaps)}" def test_the_injection_taint_spec_carries_sources_sinks_and_sanitizers() -> None: from openultrasast.model.specs import taint_specs spec = taint_specs(language="python")["injection"] assert any("request.args" in source for source in spec.sources) assert any(sink in ("eval", "exec", "os.system") for sink in spec.sinks) assert spec.sanitizers, "a family with no sanitizer model can never distinguish a fixed twin" def test_the_access_control_dominance_spec_carries_operations_and_dischargers() -> None: from openultrasast.model.specs import dominance_specs spec = dominance_specs(language="python")["access_control"] assert any("filter_by" in operation for operation in spec.operations) assert spec.dischargers, "an absence family with no discharger model can only ever report suspicion" def test_specs_import_nothing_from_the_removed_packages() -> None: """Req 1.2: the vocabularies must not depend on the modules this feature deletes.""" tree = ast.parse(SPECS.read_text()) imported = set() for node in ast.walk(tree): if isinstance(node, ast.ImportFrom) and node.module: imported.add(node.module) elif isinstance(node, ast.Import): imported.update(alias.name for alias in node.names) forbidden = ("learning", "improve", "variants", "mechanisms", "evolve") offending = [name for name in imported if any(token in name for token in forbidden)] assert not offending, f"model/specs.py imports removed code: {offending}" def test_the_specs_module_exposes_no_writer() -> None: """Req 7.4, the verifier boundary: the models are read-only data no LLM or optimiser can rewrite.""" tree = ast.parse(SPECS.read_text()) writers = [ node.name for node in tree.body if isinstance(node, ast.FunctionDef) and any(node.name.startswith(verb) for verb in ("write", "save", "dump", "store", "append", "update", "set_")) ] assert not writers, f"specs must be read-only, found writers: {writers}" assert "open(" not in SPECS.read_text().replace("# ", ""), "specs must not open a file for writing" def test_a_sink_is_never_its_own_sanitizer() -> None: """A parameterized fact names a safe sink *shape*, not a cleansing call on the path. Two shipped facts have this shape: `parameterized_execute` ("execute with bound parameters is safe") and C's `literal_format` ("printf with a constant format string is safe"). Flattened into the sanitizer list each makes its sink its own sanitizer, so every flow through it reports as already-clean and nothing is ever entailed. Both were caught on the first live Joern run — the second only because the first was fixed as a class rather than as an instance. """ from openultrasast.model.specs import taint_specs for language in ("python", "javascript", "java", "c"): for spec in taint_specs(language=language).values(): overlap = set(spec.sinks) & set(spec.sanitizers) assert not overlap, f"{language}/{spec.family}: sink is its own sanitizer: {sorted(overlap)}" def test_safe_shape_sinks_are_kept_for_the_shape_test() -> None: from openultrasast.model.specs import taint_specs spec = taint_specs(language="python")["injection"] assert "execute" in spec.safe_shape_sinks, "the safe-shape names must survive for a later shape test" assert "execute" not in spec.sanitizers c_memory = taint_specs(language="c").get("memory") assert c_memory is not None and "printf" in c_memory.safe_shape_sinks def test_dischargers_carry_identity_and_constraint_tokens_but_not_the_negative_fields() -> None: """A discharger fact spreads across six fields and two of them mean the opposite. `identity_sources` (current_user, request.user) and `constraint_params` (owner_id, user_id) are how an identity constraint is actually written, and omitting them left the absence arbiter reporting "no guard" on a handler that plainly had one. `request_sources` and `permissive_values` are excluded because they describe the vulnerable shape, not the fix -- including them would let the arbiter read a bug as its fix. """ from openultrasast.model.specs import dominance_specs from openultrasast.semantic.obligations import load_obligation_facts spec = dominance_specs(language="python")["access_control"] assert "current_user" in spec.dischargers and "owner_id" in spec.dischargers assert "login_required" in spec.dischargers # decorators still present facts = load_obligation_facts().for_language("python") negative = {p for fact in facts.dischargers for p in fact.request_sources} | { v for fact in facts.dischargers for v in fact.permissive_values } assert not (negative & set(spec.dischargers)), "the negative fields must never read as a discharge"