mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
Java parsed already, but the producer failed every Java row because no obligation facts existed for the language and that failure took roles and def_use down with it. PHP had no grammar. Now: tree-sitter-php in the semantic extra with PHP node kinds in the CST reader; obligation facts for Jenkins/Stapler, Spring Security and Servlet code (java.toml) and for Laravel, WordPress and Symfony (php.toml), each header naming the training clones and references the names were checked against; a language with an IR but no obligation facts keeps roles and def_use and records obligations as unchecked (None), which the engineered features treat as missing, never as zero. Proven on the committed pool: 6 of 6 Java rows and 33 of 33 PHP rows of one fix change each now carry semantic features. Implemented by Codex; gated here with ruff, mypy and the full suite. Co-Authored-By: Claude Fable 5.1 <[email protected]>
140 lines
5.3 KiB
Python
140 lines
5.3 KiB
Python
"""Extra-on CST walker and overlay. Skips when wheels are absent."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from openultrasast.findings import quick_scan_findings
|
|
from openultrasast.preprocess import preprocess_repository
|
|
from openultrasast.rank import rank_targets
|
|
from openultrasast.semantic import adjudicate, has_semantic_extra
|
|
from openultrasast.semantic.ir import parse_file
|
|
|
|
pytestmark = pytest.mark.semantic
|
|
|
|
|
|
def _require_extra() -> None:
|
|
if not has_semantic_extra():
|
|
pytest.skip("requires openultrasast[semantic]")
|
|
|
|
|
|
def test_javascript_eval_query_is_tree_sitter_call() -> None:
|
|
_require_extra()
|
|
ir = parse_file("app.js", "eval(req.query.x)\n", "javascript")
|
|
assert ir.parse_ok is True
|
|
assert ir.engine == "tree-sitter"
|
|
calls = [call for function in ir.functions for call in function.calls]
|
|
eval_calls = [call for call in calls if call.name == "eval"]
|
|
assert eval_calls
|
|
assert any("req.query" in arg for arg in eval_calls[0].arg_texts)
|
|
|
|
|
|
def test_broken_javascript_is_parse_failed() -> None:
|
|
_require_extra()
|
|
ir = parse_file("app.js", "eval(req.query.x\n", "javascript")
|
|
assert ir.parse_ok is False
|
|
assert ir.reason == "parse_failed"
|
|
|
|
|
|
def test_javascript_eval_promotes(tmp_path: Path) -> None:
|
|
_require_extra()
|
|
root = tmp_path / "repo"
|
|
root.mkdir()
|
|
(root / "app.js").write_text("eval(req.query.x)\n")
|
|
_, targets = preprocess_repository(root)
|
|
findings = quick_scan_findings(root, targets, rank_targets(targets))
|
|
records = adjudicate(root=root, targets=targets, findings=findings)
|
|
eval_records = [record for record in records if "js-eval" in record.proposal_id]
|
|
assert eval_records
|
|
assert eval_records[0].disposition == "promote"
|
|
assert eval_records[0].evidence_level == "static_corroboration"
|
|
assert eval_records[0].engine == "tree-sitter"
|
|
|
|
|
|
def test_c_constant_system_demotes(tmp_path: Path) -> None:
|
|
_require_extra()
|
|
root = tmp_path / "repo"
|
|
root.mkdir()
|
|
(root / "run.c").write_text('void f(void) { system("ls"); }\n')
|
|
_, targets = preprocess_repository(root)
|
|
findings = quick_scan_findings(root, targets, rank_targets(targets))
|
|
records = adjudicate(root=root, targets=targets, findings=findings)
|
|
hits = [record for record in records if record.path == "run.c"]
|
|
assert hits
|
|
assert all(record.disposition != "unadjudicated" or record.reason != "language_unsupported" for record in hits)
|
|
demoted = [record for record in hits if record.disposition == "demote"]
|
|
assert demoted
|
|
assert demoted[0].dominating_fact == "constant"
|
|
assert demoted[0].evidence_level == "static_corroboration"
|
|
|
|
|
|
def test_php_functions_binds_calls_and_parameters(tmp_path: Path) -> None:
|
|
_require_extra()
|
|
from tree_sitter import Parser
|
|
|
|
from openultrasast.semantic.cst import _KIND, _walk
|
|
from openultrasast.semantic.extra import grammar_for
|
|
|
|
text = """<p>Text mode</p>
|
|
<?php
|
|
class Example {
|
|
public function run(?Thing $x, &$y, ...$rest) {
|
|
$value = $this->g($y);
|
|
$value .= "hello $x";
|
|
$allowed = Gate::authorize('read', $x);
|
|
$this?->g($y);
|
|
}
|
|
}
|
|
function free_fn($x) { return consume($x); }
|
|
$closure = function ($z) { return consume($z); };
|
|
$arrow = fn($a) => consume($a);
|
|
"""
|
|
path = tmp_path / "sample.php"
|
|
path.write_text(text)
|
|
data = path.read_bytes()
|
|
assert len(data) == len(text.encode()) > 0
|
|
tree = Parser(grammar_for("php")).parse(data)
|
|
assert not tree.root_node.has_error
|
|
seen = {node.type for node in _walk(tree.root_node)}
|
|
# Exercise every registered kind against the installed grammar, including fn.
|
|
assert set().union(*_KIND["php"].values()) <= seen
|
|
ir = parse_file(path.name, data.decode(), "php")
|
|
assert ir.parse_ok and ir.engine == "tree-sitter"
|
|
assert [(fn.name, fn.start_line, fn.end_line, fn.params) for fn in ir.functions] == [
|
|
("<module>", 1, 14, ()),
|
|
("run", 4, 9, ("$x", "$y", "$rest")),
|
|
("free_fn", 11, 11, ("$x",)),
|
|
("<anon>", 12, 12, ("$z",)),
|
|
("<anon>", 13, 13, ("$a",)),
|
|
]
|
|
module, method, free, closure, arrow = ir.functions
|
|
assert module.calls == ()
|
|
assert [b.name for b in module.binds] == ["$closure", "$arrow"]
|
|
assert [(b.name, b.line, b.names, b.call_name) for b in method.binds] == [
|
|
("$value", 5, ("$this", "$y"), "g"),
|
|
("$value", 6, ("$value", "$x"), None),
|
|
("$allowed", 7, ("$x",), "authorize"),
|
|
]
|
|
assert [(c.name, c.line, c.arg_texts, c.arg_names) for c in method.calls] == [
|
|
("g", 5, ("$y",), (("$y",),)),
|
|
("authorize", 7, ("'read'", "$x"), ((), ("$x",))),
|
|
("g", 8, ("$y",), (("$y",),)),
|
|
]
|
|
assert method.calls[1].arg_is_constant == (True, False)
|
|
for fn in (free, closure, arrow):
|
|
assert [call.name for call in fn.calls] == ["consume"]
|
|
assert fn.calls[0].arg_names == (fn.params,)
|
|
|
|
|
|
def test_php_interpolation_and_named_arguments() -> None:
|
|
_require_extra()
|
|
ir = parse_file("sample.php", '<?php function f($x) { $s = "hello $x"; consume(value: "hello", other: "$x"); }', "php")
|
|
assert ir.parse_ok
|
|
fn = ir.functions[1]
|
|
assert fn.binds[0].names == ("$x",) and not fn.binds[0].is_constant
|
|
assert fn.calls[0].arg_texts == ('"hello"', '"$x"')
|
|
assert fn.calls[0].arg_names == ((), ("$x",))
|
|
assert fn.calls[0].arg_is_constant == (True, False)
|