mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
Codex delivery under spec access-control-prover plus one fix of mine. Operation rows may now name a handler by name pattern and framework (Jenkins form validation and list filling, Tornado request methods, Remix actions); the mapper recognises Symfony, Livewire and Laravel controllers, Remix exports and Tornado handlers; `obligations/enforcement.py` turns Laravel route middleware, Symfony attributes and security.yaml rules and Tornado's authenticated decorator into router and decorator witnesses. The path walk looks each node's callers up once per change and does not walk a tree with no recognised entry: one seatunnel change fell from 670 s to 35 s. One name Codex could not source (Tornado get_current_user) is left out and said so in the facts header. Smoke on the 23 fix changes: 15 pushes caught (65 percent, Wilson 45 to 81), 55 of 93 locations, precision 15 of 15 flagged, no instrument failure, no change over 120 s. Delivery A caught 7. Co-Authored-By: Claude Fable 5.1 <[email protected]>
84 lines
3.9 KiB
Python
84 lines
3.9 KiB
Python
"""Bounded paths preserve file identity and call-site lines."""
|
|
|
|
import pytest
|
|
|
|
from openultrasast.semantic.obligations.paths import PathRecord, compose_paths
|
|
|
|
|
|
@pytest.mark.parametrize("depth,reached", [(0, False), (1, False), (2, True), (3, True)])
|
|
def test_depth_and_call_sites(depth, reached):
|
|
graph = {"c.java::target": [("b.java::helper", 8)], "b.java::helper": [("a.java::doTest", 4)]}
|
|
paths, unreached = compose_paths(
|
|
callers=lambda p, f: graph.get(f"{p}::{f}", []), entries={"a.java::doTest"}, targets=[("c.java", "target")], max_depth=depth
|
|
)
|
|
assert bool(paths) == reached
|
|
assert unreached == ([] if reached else ["c.java::target"])
|
|
if reached:
|
|
assert paths == [PathRecord((("a.java::doTest", 4), ("b.java::helper", 8), ("c.java::target", None)), "a.java::doTest", True)]
|
|
|
|
|
|
def test_entry_dedupe_cycles_fan_out_and_file_identity():
|
|
graph = {"c.java::save": [("b.java::helper", 8), ("a.java::doTest", 4), ("a.java::doTest", 4)], "b.java::helper": [("c.java::save", 5)]}
|
|
kwargs = dict(
|
|
callers=lambda p, f: graph.get(f"{p}::{f}", []),
|
|
entries={"a.java::doTest"},
|
|
targets=[("c.java", "save"), ("other.java", "save"), ("a.java", "doTest"), ("c.java", "save")],
|
|
)
|
|
paths, unreached = compose_paths(**kwargs)
|
|
assert len(paths) == 2 and unreached == ["other.java::save"]
|
|
assert any(p.hops == (("a.java::doTest", None),) for p in paths)
|
|
assert compose_paths(**kwargs, fan_out=1)[1] == ["c.java::save", "other.java::save"]
|
|
|
|
|
|
@pytest.mark.parametrize("key,value", [("max_depth", -1), ("fan_out", 0)])
|
|
def test_invalid_bounds(key, value):
|
|
with pytest.raises(ValueError):
|
|
compose_paths(callers=lambda p, f: [], entries=set(), targets=[], **{key: value})
|
|
|
|
|
|
def test_records_are_consumed_by_the_checker():
|
|
from types import SimpleNamespace
|
|
|
|
from openultrasast.semantic.facts import load_facts
|
|
from openultrasast.semantic.ir import parse_file
|
|
from openultrasast.semantic.obligations.check import check_obligations
|
|
from openultrasast.semantic.obligations.dominance import OrderDominance
|
|
from openultrasast.semantic.obligations.facts import load_obligation_facts
|
|
|
|
text = "def entry(title):\n return Book.query.filter_by(book_title=title).first()\n"
|
|
paths, _ = compose_paths(callers=lambda p, f: [], entries={"app.py::entry"}, targets=[("app.py", "entry")])
|
|
result = check_obligations(
|
|
irs={"app.py": (parse_file("app.py", text, "python"), text)},
|
|
entries=[
|
|
SimpleNamespace(path="app.py", function_name="entry", access_level="unknown", kind="route", access_evidence=["fixture entry"])
|
|
],
|
|
facts=load_obligation_facts(),
|
|
flow_facts=load_facts(),
|
|
policy=None,
|
|
paths=paths,
|
|
dominance=OrderDominance({"app.py": text}),
|
|
store_shapes=(),
|
|
min_siblings=3,
|
|
assume_entries_protected=True,
|
|
)
|
|
(finding,) = result.findings
|
|
assert finding.operation.function == "entry" and finding.label == "unguarded_path"
|
|
|
|
|
|
def test_callers_are_looked_up_once_per_node_and_never_without_entries():
|
|
from openultrasast.semantic.obligations.paths import compose_paths
|
|
|
|
calls: list[str] = []
|
|
graph = {"a.py::leaf": [("a.py::mid", 3)], "a.py::mid": [("web.py::view", 9), ("a.py::other", 4)], "a.py::other": [("a.py::mid", 5)]}
|
|
|
|
def callers(path, function):
|
|
calls.append(f"{path}::{function}")
|
|
return graph.get(f"{path}::{function}", [])
|
|
|
|
paths, unreached = compose_paths(callers=callers, entries={"web.py::view"}, targets=[("a.py", "leaf"), ("a.py", "mid")], max_depth=3)
|
|
assert [p.hops[0][0] for p in paths] == ["web.py::view"] * len(paths) and len(paths) >= 2 and unreached == []
|
|
assert len(calls) == len(set(calls)) # once per node across both targets
|
|
calls.clear()
|
|
paths, unreached = compose_paths(callers=callers, entries=set(), targets=[("a.py", "leaf")])
|
|
assert paths == [] and unreached == ["a.py::leaf"] and calls == []
|