mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
Both were found by running the real engine, and neither was visible offline.
1. A sink was its own sanitizer. The shipped facts mark some sanitizers with a
*shape* qualifier -- parameterized ("execute(sql, params) binds rather than
interpolates") and literal_format_arg ("printf with a constant format string
is safe"). These describe a safe FORM OF THE SINK CALL, not a call that
cleans a value on the path. Flattening them into the sanitizer list made
`execute` both the injection sink and its own sanitizer, so every SQL flow
would have reported as already-clean and nothing could ever be entailed.
Fixed as a class, not an instance: any fact carrying a shape qualifier is
excluded from `sanitizers` and kept in `safe_shape_sinks` for the shape test
task 4.2 will model properly. Fixing only the Python `parameterized` case
would have left C's entire printf family -- which the generalised test then
caught immediately.
2. labeled_family lost its deterministic derivation in group 1. The deleted
classifier resolved a pair's family by declared label, else obligation, else
mechanism, else CWE, and only asked an LLM when every link failed -- which
for a labeled corpus was never. Group 1 reduced this to the declared label
alone, which silently emptied every family whose pairs are labeled by CWE:
the injection slice went to zero pairs. The chain is restored, still with no
model call, and the family census again matches the committed entailment
ceiling exactly (injection 50, memory 176, access_control 8, ...).
Also: the Joern seam now runs in the CPG's own scratch directory, because
`joern --script` writes a `workspace/` beside the working directory and would
otherwise leave one inside the repository being analysed.
The taint query itself was corrected against the live engine: the sink is the
call's ARGUMENTS (data flows into arguments, not into the call node) and
reachableByFlows is called on the sink with the source. With that, the
three-hop chain our flat IR could not resolve is found:
request.args["cmd"] -> cmd -> "echo " + cmd -> full -> os.system(full)
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
129 lines
6.5 KiB
Python
129 lines
6.5 KiB
Python
"""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*.
|
|
|
|
`path`, `output_encoding`, `untrusted_destination` and `prototype` have no sink in `ruleset/semantic`
|
|
whose CWE routes to them, which is the same hole the entailment ceiling diagnosed ("closed literal sink
|
|
table: misses per-family sinks"). Task 4.2 authors those sinks and must update this test when it does —
|
|
the set is asserted exactly so a gap can neither appear nor close silently.
|
|
"""
|
|
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", "memory"}
|
|
gaps = {family.id for family in taxonomy.families} - seeded - deferred
|
|
assert gaps == {"path", "output_encoding", "untrusted_destination", "prototype"}, (
|
|
"a family lost or gained a seed without this record being updated"
|
|
)
|
|
|
|
|
|
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
|