mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(prover): delivery B: handler-named operations, Symfony, Livewire, Remix and Tornado entries, enforcement witnesses, memoised path walk
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]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
7f1b820d5a
commit
17e07b36c4
@@ -181,3 +181,112 @@ def test_nonpublic_unreached_function_in_partly_covered_repo(tmp_path):
|
||||
assert result.coverage.frameworks_recognised == {"stapler": 1}
|
||||
assert result.coverage.unchecked_by_reason["framework_unrecognised"] == 1
|
||||
assert not result.findings
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard,expected", [("", 1), ("checkPermission(ADMINISTER);", 0)])
|
||||
def test_handler_without_listed_call(tmp_path, guard, expected):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
f"""class X extends Descriptor<Thing> {{
|
||||
public Object doCheckValue() {{
|
||||
{guard}
|
||||
return 1;
|
||||
}}
|
||||
}}""",
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert len(result.findings) == expected and result.coverage.operations_checked == 1
|
||||
if expected:
|
||||
assert result.findings[0]["line"] == 2
|
||||
assert result.findings[0]["operation"]["id"] == "stapler-handler"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("middleware,expected", [("->middleware(['auth'])", 0), ("->middleware(['throttle:10'])", 1)])
|
||||
def test_laravel_enforcement_in_prover(tmp_path, middleware, expected):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
"class Plain {}",
|
||||
{
|
||||
"Editor.php": "<?php class Editor extends Controller { public function store() { Product::save(); } }",
|
||||
"routes/web.php": f"<?php Route::post('/items', [Editor::class, 'store']){middleware};",
|
||||
},
|
||||
)
|
||||
change["files"] = [{"path": "Editor.php", "head_lines": [[1, 1]]}]
|
||||
result = prove_change(root, change)
|
||||
assert len(result.findings) == expected and result.coverage.operations_checked == 1
|
||||
assert result.coverage.frameworks_recognised == {"laravel": 1}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arrow", [False, True])
|
||||
def test_remix_session_does_not_discharge_ownership(tmp_path, arrow):
|
||||
declaration = "export const action = async () =>" if arrow else "export async function action()"
|
||||
text = f"""{declaration} {{
|
||||
const user = await requireUser(request);
|
||||
return deactivateRecord(requestId);
|
||||
}}"""
|
||||
root, change = repository(tmp_path, "class Plain {}", {"app/routes/items.ts": text})
|
||||
change["files"] = [{"path": "app/routes/items.ts", "head_lines": [[1, 4]]}]
|
||||
result = prove_change(root, change)
|
||||
assert result.coverage.operations_checked == 1 and result.coverage.operations_discharged == 0
|
||||
assert len(result.findings) == 1 and result.findings[0]["missing"] == "ownership_check"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("decorator,expected", [("", 1), ("@tornado.web.authenticated", 0)])
|
||||
def test_tornado_enforcement_in_prover(tmp_path, decorator, expected):
|
||||
text = f"""import tornado.web
|
||||
class Handler(tornado.web.RequestHandler):
|
||||
{decorator}
|
||||
def get(self):
|
||||
return 1
|
||||
"""
|
||||
root, change = repository(tmp_path, "class Plain {}", {"app.py": text})
|
||||
change["files"] = [{"path": "app.py", "head_lines": [[1, 5]]}]
|
||||
result = prove_change(root, change)
|
||||
assert len(result.findings) == expected and result.coverage.operations_checked == 1
|
||||
|
||||
|
||||
def test_symfony_yaml_is_read_by_prover(tmp_path):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
"class Plain {}",
|
||||
{
|
||||
"Controller.php": (
|
||||
"<?php class Editor extends AbstractController { #[Route('/admin')] public function store() { Product::save(); } }"
|
||||
),
|
||||
"config/packages/security.yaml": "security:\n access_control:\n - { path: ^/admin, roles: ROLE_ADMIN }\n",
|
||||
},
|
||||
)
|
||||
change["files"] = [{"path": "Controller.php", "head_lines": [[1, 1]]}]
|
||||
ctx = Context(root, change)
|
||||
result = prove_change(root, change, ctx)
|
||||
assert not result.findings and result.coverage.operations_discharged == 1
|
||||
assert len(ctx.texts["config/packages/security.yaml"].encode()) > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exits,expected", [(True, 0), (False, 1)])
|
||||
def test_handler_permission_predicate_requires_exit(tmp_path, exits, expected):
|
||||
root, change = repository(
|
||||
tmp_path,
|
||||
f"""class X extends Descriptor<Thing> {{
|
||||
public Object doCheckValue() {{
|
||||
boolean allowed = hasPermission(ADMINISTER);
|
||||
{"if (!allowed) {" if exits else ""}
|
||||
{"return null;" if exits else ""}
|
||||
{"}" if exits else ""}
|
||||
return 1;
|
||||
}}
|
||||
}}""",
|
||||
)
|
||||
result = prove_change(root, change)
|
||||
assert len(result.findings) == expected and result.coverage.operations_discharged == 1 - expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("public,expected_missing", [(False, ["path_guard", "ownership_check"]), (True, ["ownership_check"])])
|
||||
def test_remix_missing_kinds_and_public_policy(tmp_path, public, expected_missing):
|
||||
extra = {"app/routes/items.ts": "export function action() { return deactivateRecord(requestId); }"}
|
||||
if public:
|
||||
extra[".openultrasast/obligations.toml"] = 'version = 1\n[[route]]\npath = "action"\naccess = "public"\n'
|
||||
root, change = repository(tmp_path, "class Plain {}", extra)
|
||||
change["files"] = [{"path": "app/routes/items.ts", "head_lines": [[1, 1]]}]
|
||||
result = prove_change(root, change)
|
||||
assert [finding["missing"] for finding in result.findings] == expected_missing
|
||||
|
||||
@@ -67,3 +67,175 @@ def test_jetty_servlet_and_jaxrs(tmp_path, text, name):
|
||||
|
||||
def test_unrecognised_and_comments(tmp_path):
|
||||
assert entries(tmp_path, "/* class X extends Descriptor { public void doFake() {} } */\nclass Plain { public void doCheck() {} }") == []
|
||||
|
||||
|
||||
def framework_entries(tmp_path, files):
|
||||
targets = []
|
||||
for name, text in files.items():
|
||||
path = tmp_path / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text)
|
||||
targets.append(build_file_target(tmp_path, path))
|
||||
return [
|
||||
e for e in analyze_entry_points(tmp_path, targets, texts=files, include_manifests=False) if e.kind == "route" and e.function_name
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("annotation", ['#[Route("/items")]', '/** @Route("/items") */'])
|
||||
def test_symfony_actions(tmp_path, annotation):
|
||||
rows = framework_entries(
|
||||
tmp_path,
|
||||
{
|
||||
"Controller.php": f"""<?php
|
||||
class Items extends AbstractController {{
|
||||
{annotation}
|
||||
public function show() {{ return 1; }}
|
||||
private function hidden() {{}}
|
||||
}}
|
||||
class Plain {{ public function helper() {{}} }}
|
||||
"""
|
||||
},
|
||||
)
|
||||
assert [e.function_name for e in rows] == ["show"]
|
||||
assert "Symfony" in " ".join(rows[0].access_evidence)
|
||||
assert "/items" in " ".join(rows[0].access_evidence)
|
||||
|
||||
|
||||
def test_livewire_public_methods_and_bindings(tmp_path):
|
||||
rows = framework_entries(
|
||||
tmp_path,
|
||||
{
|
||||
"Component.php": """<?php
|
||||
use Livewire\\Component;
|
||||
class Editor extends Component {
|
||||
public function store() {}
|
||||
public function _helper() {}
|
||||
protected function hidden() {}
|
||||
}
|
||||
class Other { public function store() {} }
|
||||
""",
|
||||
"view.blade.php": '<button wire:click="store">Save</button>',
|
||||
},
|
||||
)
|
||||
assert [(e.path, e.function_name) for e in rows] == [("Component.php", "store")]
|
||||
assert "Livewire" in " ".join(rows[0].access_evidence)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("middleware,access", [("", "public"), ("->middleware(['auth', 'can:edit'])", "unknown")])
|
||||
def test_laravel_cross_file_registration(tmp_path, middleware, access):
|
||||
rows = framework_entries(
|
||||
tmp_path,
|
||||
{
|
||||
"app/Http/Controllers/Editor.php": """<?php
|
||||
class Editor extends Controller {
|
||||
public function store() {}
|
||||
public function helper() {}
|
||||
}
|
||||
class Other extends Controller { public function store() {} }
|
||||
""",
|
||||
"routes/web.php": f"<?php Route::post('/items', [Editor::class, 'store']){middleware};",
|
||||
},
|
||||
)
|
||||
assert [(e.function_name, e.access_level) for e in rows] == [("store", access)]
|
||||
assert rows[0].path == "app/Http/Controllers/Editor.php"
|
||||
assert "Laravel" in " ".join(rows[0].access_evidence)
|
||||
|
||||
|
||||
def test_symfony_route_config(tmp_path):
|
||||
rows = framework_entries(
|
||||
tmp_path,
|
||||
{
|
||||
"Controller.php": "<?php class Editor extends AbstractController { public function show() {} public function helper() {} }",
|
||||
"config/routes.yaml": "items:\n path: /items\n controller: App\\Controller\\Editor::show\n",
|
||||
},
|
||||
)
|
||||
assert [e.function_name for e in rows] == ["show"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"declaration",
|
||||
[
|
||||
"export async function action() { return 1; }",
|
||||
"export const action = async () => { return 1; }",
|
||||
"export const action: ActionFunction = async () => { return 1; }",
|
||||
],
|
||||
)
|
||||
def test_remix_exports(tmp_path, declaration):
|
||||
rows = framework_entries(tmp_path, {"apps/web/app/routes/items.ts": declaration + "\nexport function loader() { return 1; }"})
|
||||
assert [e.function_name for e in rows] == ["action", "loader"]
|
||||
assert all("Remix" in " ".join(e.access_evidence) and e.access_level == "unknown" for e in rows)
|
||||
assert framework_entries(tmp_path, {"app/helpers/items.ts": declaration}) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("base", ["tornado.web.RequestHandler", "tornado.websocket.WebSocketHandler", "RequestHandler"])
|
||||
def test_tornado_methods(tmp_path, base):
|
||||
rows = framework_entries(
|
||||
tmp_path,
|
||||
{
|
||||
"app.py": f"""from tornado.web import RequestHandler
|
||||
class Handler({base}):
|
||||
def get(self):
|
||||
return 1
|
||||
def open(self):
|
||||
pass
|
||||
def helper(self):
|
||||
pass
|
||||
class Plain:
|
||||
def get(self):
|
||||
pass
|
||||
"""
|
||||
},
|
||||
)
|
||||
assert [e.function_name for e in rows] == ["get", "open"]
|
||||
assert all("Tornado" in " ".join(e.access_evidence) for e in rows)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", ["", "\n" * 320])
|
||||
def test_symfony_named_paths_and_class_prefix(tmp_path, prefix):
|
||||
rows = framework_entries(
|
||||
tmp_path,
|
||||
{
|
||||
"Controller.php": prefix
|
||||
+ """<?php
|
||||
/** @Route(path="/admin") */
|
||||
class Editor extends AbstractController {
|
||||
/** @Route(path="/items") */
|
||||
public function show() {}
|
||||
}
|
||||
"""
|
||||
},
|
||||
)
|
||||
assert [row.name for row in rows] == ["/admin/items"]
|
||||
assert rows[0].line == 5 + prefix.count("\n")
|
||||
|
||||
|
||||
def test_tornado_inherited_and_explicit_super_handler(tmp_path):
|
||||
rows = framework_entries(
|
||||
tmp_path,
|
||||
{
|
||||
"app.py": """import tornado.websocket
|
||||
import terminado
|
||||
class Base(tornado.websocket.WebSocketHandler):
|
||||
pass
|
||||
class Child(Base):
|
||||
def open(self):
|
||||
pass
|
||||
class Terminal(terminado.TermSocket):
|
||||
def open(self):
|
||||
tornado.websocket.WebSocketHandler.open(self)
|
||||
"""
|
||||
},
|
||||
)
|
||||
assert [row.function_name for row in rows] == ["open", "open"]
|
||||
|
||||
|
||||
def test_remix_tsx_with_jsx(tmp_path):
|
||||
rows = framework_entries(
|
||||
tmp_path,
|
||||
{
|
||||
"app/routes/items.tsx": """export const action = async () => { return 1; };
|
||||
export default function Items() { return <div>Items</div>; }
|
||||
"""
|
||||
},
|
||||
)
|
||||
assert [row.function_name for row in rows] == ["action"]
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Framework enforcement is scoped to the registered handler, never a nearby method."""
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.mapping import analyze_entry_points
|
||||
from openultrasast.preprocess import build_file_target
|
||||
from openultrasast.semantic.obligations.dominance import OrderDominance
|
||||
from openultrasast.semantic.obligations.operations import Operation
|
||||
|
||||
|
||||
def mapped(tmp_path, files):
|
||||
targets = []
|
||||
for name, text in files.items():
|
||||
path = tmp_path / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text)
|
||||
if path.suffix in {".php", ".py"}:
|
||||
targets.append(build_file_target(tmp_path, path))
|
||||
return [
|
||||
e for e in analyze_entry_points(tmp_path, targets, texts=files, include_manifests=False) if e.kind == "route" and e.function_name
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard", ["auth", "can:edit", "role:admin", "permission:edit"])
|
||||
def test_laravel_route_middleware(tmp_path, guard):
|
||||
from openultrasast.semantic.obligations.enforcement import witnesses
|
||||
|
||||
files = {
|
||||
"Editor.php": "<?php class Editor extends Controller { public function store() {} public function other() {} }",
|
||||
"routes/web.php": f"<?php Route::post('/items', [Editor::class, 'store'])->middleware(['{guard}']);",
|
||||
}
|
||||
rows = witnesses(root=tmp_path, texts=files, entries=mapped(tmp_path, files), language="php")
|
||||
(witness,) = rows
|
||||
assert (witness.path, witness.function, witness.line, witness.scope) == ("Editor.php", "store", None, "router")
|
||||
assert "routes/web.php" in witness.detail
|
||||
assert OrderDominance(files).dominates(
|
||||
witness, Operation("Editor.php", 1, "store", "protected_write", "x", None, ("path_guard",)), None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard", ["$this->middleware('auth');", "$this->authorizeResource(Item::class, 'item');"])
|
||||
def test_laravel_constructor(tmp_path, guard):
|
||||
from openultrasast.semantic.obligations.enforcement import witnesses
|
||||
|
||||
files = {
|
||||
"Editor.php": f"""<?php class Editor extends Controller {{
|
||||
public function __construct() {{ {guard} }}
|
||||
public function store() {{}}
|
||||
}}
|
||||
Route::post('/items', [Editor::class, 'store']);
|
||||
"""
|
||||
}
|
||||
(witness,) = witnesses(root=tmp_path, texts=files, entries=mapped(tmp_path, files), language="php")
|
||||
assert witness.scope == "router" and witness.function == "store"
|
||||
assert OrderDominance(files).dominates(
|
||||
witness, Operation("Editor.php", 3, "store", "protected_write", "x", None, ("path_guard",)), None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard", ['#[IsGranted("ROLE_ADMIN")]', "#[Security(\"is_granted('ROLE_ADMIN')\")]"])
|
||||
def test_symfony_attributes(tmp_path, guard):
|
||||
from openultrasast.semantic.obligations.enforcement import witnesses
|
||||
|
||||
files = {
|
||||
"Editor.php": f"""<?php class Editor extends AbstractController {{
|
||||
#[Route('/items')]
|
||||
{guard}
|
||||
public function store() {{}}
|
||||
#[Route('/other')]
|
||||
public function other() {{}}
|
||||
}}"""
|
||||
}
|
||||
(witness,) = witnesses(root=tmp_path, texts=files, entries=mapped(tmp_path, files), language="php")
|
||||
assert witness.scope == "decorator" and witness.function == "store"
|
||||
assert OrderDominance(files).dominates(
|
||||
witness, Operation("Editor.php", 4, "store", "protected_write", "x", None, ("path_guard",)), None
|
||||
)
|
||||
|
||||
|
||||
def test_symfony_security_yaml(tmp_path):
|
||||
from openultrasast.semantic.obligations.enforcement import witnesses
|
||||
|
||||
files = {
|
||||
"Editor.php": "<?php class Editor extends AbstractController { #[Route('/admin')] public function store() {} }",
|
||||
"config/packages/security.yaml": "security:\n access_control:\n - { path: ^/admin, roles: ROLE_ADMIN }\n",
|
||||
}
|
||||
(witness,) = witnesses(root=tmp_path, texts=files, entries=mapped(tmp_path, files), language="php")
|
||||
assert witness.scope == "router" and "security.yaml" in witness.detail
|
||||
assert OrderDominance(files).dominates(
|
||||
witness, Operation("Editor.php", 1, "store", "protected_write", "x", None, ("path_guard",)), None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard", ["authenticated", "tornado.web.authenticated"])
|
||||
def test_tornado_decorator(tmp_path, guard):
|
||||
from openultrasast.semantic.obligations.enforcement import witnesses
|
||||
|
||||
files = {
|
||||
"app.py": f"""import tornado.web
|
||||
class Handler(tornado.web.RequestHandler):
|
||||
@{guard}
|
||||
def get(self):
|
||||
pass
|
||||
def post(self):
|
||||
pass
|
||||
"""
|
||||
}
|
||||
(witness,) = witnesses(root=tmp_path, texts=files, entries=mapped(tmp_path, files), language="python")
|
||||
assert witness.scope == "decorator" and witness.function == "get"
|
||||
assert OrderDominance(files).dominates(witness, Operation("app.py", 4, "get", "protected_read", "x", None, ("path_guard",)), None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("restriction", ["->only(['store'])", "->except(['other'])"])
|
||||
def test_constructor_restrictions_do_not_guard_other_handlers(tmp_path, restriction):
|
||||
from openultrasast.semantic.obligations.enforcement import witnesses
|
||||
|
||||
files = {
|
||||
"Editor.php": f"""<?php class Editor extends Controller {{
|
||||
public function __construct() {{ $this->middleware('auth'){restriction}; }}
|
||||
public function store() {{}}
|
||||
public function other() {{}}
|
||||
}}
|
||||
Route::post('/items', [Editor::class, 'store']);
|
||||
Route::get('/other', [Editor::class, 'other']);
|
||||
"""
|
||||
}
|
||||
rows = witnesses(root=tmp_path, texts=files, entries=mapped(tmp_path, files), language="php")
|
||||
assert [row.function for row in rows] == ["store"]
|
||||
|
||||
|
||||
def test_symfony_first_public_rule_does_not_become_a_guard(tmp_path):
|
||||
from openultrasast.semantic.obligations.enforcement import witnesses
|
||||
|
||||
files = {
|
||||
"Editor.php": "<?php class Editor extends AbstractController { #[Route('/admin')] public function store() {} }",
|
||||
"config/packages/security.yaml": (
|
||||
"security:\n access_control:\n - { path: ^/admin, roles: PUBLIC_ACCESS }\n - { path: ^/admin, roles: ROLE_ADMIN }\n"
|
||||
),
|
||||
}
|
||||
assert witnesses(root=tmp_path, texts=files, entries=mapped(tmp_path, files), language="php") == []
|
||||
|
||||
|
||||
def test_unrelated_guards_and_comments_are_not_witnesses(tmp_path):
|
||||
from openultrasast.semantic.obligations.enforcement import witnesses
|
||||
|
||||
files = {
|
||||
"Editor.php": """<?php class Editor extends Controller {
|
||||
public function store() {}
|
||||
}
|
||||
// Route::post('/items', [Editor::class, 'store'])->middleware('auth');
|
||||
Route::post('/items', [Editor::class, 'store'])->middleware('throttle:10');
|
||||
""",
|
||||
"app.py": "class Handler(RequestHandler):\n @not_authenticated\n def get(self):\n pass\n",
|
||||
}
|
||||
entries = mapped(tmp_path, files)
|
||||
assert witnesses(root=tmp_path, texts=files, entries=entries, language="php") == []
|
||||
assert witnesses(root=tmp_path, texts=files, entries=entries, language="python") == []
|
||||
@@ -115,3 +115,30 @@ def test_delivery_a_java_operations_and_dischargers_resolve():
|
||||
assert "path_guard" in operations[call].requires
|
||||
assert dischargers["checkPermission"].kind == dischargers["checkAnyPermission"].kind == "path_guard"
|
||||
assert dischargers["hasPermission"].kind == "ownership_check"
|
||||
|
||||
|
||||
def test_handler_rows_and_validation(tmp_path):
|
||||
from openultrasast.semantic.obligations.facts import ObligationFactsError, load_obligation_facts
|
||||
|
||||
text = """version = "1"
|
||||
language = "python"
|
||||
[[operation]]
|
||||
id = "handler"
|
||||
kind = "privileged_action"
|
||||
handlers = ["^open$"]
|
||||
framework = "tornado"
|
||||
requires = ["path_guard"]
|
||||
sensitivity = "high"
|
||||
"""
|
||||
path = tmp_path / "python.toml"
|
||||
path.write_text(text)
|
||||
(row,) = load_obligation_facts(tmp_path).operations
|
||||
assert row.handlers == ("^open$",) and row.framework == "tornado" and row.calls == ()
|
||||
for invalid in [
|
||||
text.replace('framework = "tornado"', ""),
|
||||
text.replace('"^open$"', '"["'),
|
||||
text.replace('framework = "tornado"', "framework = 12"),
|
||||
]:
|
||||
path.write_text(invalid)
|
||||
with pytest.raises(ObligationFactsError):
|
||||
load_obligation_facts(tmp_path)
|
||||
|
||||
@@ -111,3 +111,33 @@ def test_discharge_witness_covers_operation_only_when_kind_and_provenance_fit()
|
||||
assert covering["constrained"] == ["identity_constraint"] and covering["constrained_too"] == ["identity_constraint"]
|
||||
assert covering["leaky"] == [] # request-bound identity does not cover
|
||||
assert covering["guarded"] == [] # a path guard does not cover a protected read (only privileged actions)
|
||||
|
||||
|
||||
def test_handler_named_operations_are_framework_scoped():
|
||||
from openultrasast.semantic.obligations import load_obligation_facts
|
||||
from openultrasast.semantic.obligations.operations import find_operations
|
||||
|
||||
text = "class Handler:\n def open(self):\n pass\n def helper(self):\n pass\n"
|
||||
ir = parse_file("app.py", text, "python")
|
||||
facts = load_obligation_facts().for_language("python")
|
||||
assert find_operations(ir, facts, text=text) == ()
|
||||
assert find_operations(ir, facts, text=text, handlers_of={"open": "other"}) == ()
|
||||
(op,) = find_operations(ir, facts, text=text, handlers_of={"open": "tornado", "helper": "tornado"})
|
||||
assert (op.function, op.line, op.requires) == ("open", 2, ("path_guard",))
|
||||
|
||||
|
||||
def test_handler_rows_in_java_and_typescript():
|
||||
from openultrasast.semantic.obligations import load_obligation_facts
|
||||
from openultrasast.semantic.obligations.operations import find_operations
|
||||
|
||||
facts = load_obligation_facts()
|
||||
cases = [
|
||||
("java", "stapler", "class Editor { public void doFillNamesItems() {} }", "doFillNamesItems", ("path_guard",)),
|
||||
("typescript", "remix", "export function action() { return 1; }", "action", ("path_guard", "ownership_check")),
|
||||
]
|
||||
for language, framework, text, name, requires in cases:
|
||||
ir = parse_file(f"app.{language}", text, language)
|
||||
assert ir.parse_ok
|
||||
assert find_operations(ir, facts.for_language(language), text=text) == ()
|
||||
(operation,) = find_operations(ir, facts.for_language(language), text=text, handlers_of={name: framework})
|
||||
assert operation.line == 1 and operation.requires == requires and operation.function == name
|
||||
|
||||
@@ -63,3 +63,21 @@ def test_records_are_consumed_by_the_checker():
|
||||
)
|
||||
(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 == []
|
||||
|
||||
Reference in New Issue
Block a user