mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +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]>
144 lines
7.3 KiB
Python
144 lines
7.3 KiB
Python
"""authorization-obligations task 2.1: operations and discharge witnesses in a parsed file (offline, stdlib ast)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from openultrasast.mapping import EntryPointRecord
|
|
from openultrasast.semantic.facts import load_facts
|
|
from openultrasast.semantic.ir import parse_file
|
|
|
|
APP = (
|
|
"from flask import request\n\n\n"
|
|
"@app.route('/books/<title>')\n@login_required\ndef guarded(title):\n"
|
|
" return Book.query.filter_by(book_title=title).first()\n\n\n"
|
|
"@app.route('/books/mine/<title>')\n@login_required\ndef constrained(title):\n"
|
|
" user_id = request.user.id\n"
|
|
" return Book.query.filter_by(user_id=user_id, book_title=title).first()\n\n\n"
|
|
"@app.route('/books/shared/<title>')\ndef constrained_too(title):\n"
|
|
" owner = current_user.id\n"
|
|
" return Book.query.filter_by(owner=owner, book_title=title).first()\n\n\n"
|
|
"@app.route('/books/any/<title>')\ndef leaky(title):\n"
|
|
" owner = request.args.get('owner')\n"
|
|
" return Book.query.filter_by(user_id=owner, book_title=title).first()\n\n\n"
|
|
"@app.route('/health')\ndef health():\n return 'ok'\n\n\n"
|
|
# Registered from a separate OpenAPI document, guarded by a call rather than a decorator, and five bindings
|
|
# between the token and the constraint: the shape the vibe-py absence rows really have. Without it this fixture
|
|
# is strictly easier than the corpus it claims to cover (learning-harness Req 10.4).
|
|
"def from_spec(book_title):\n"
|
|
" resp = token_validator(request.headers.get('Authorization'))\n"
|
|
" if 'error' in resp:\n"
|
|
" return None\n"
|
|
" claims = resp\n"
|
|
" subject = claims['sub']\n"
|
|
" user = User.query.filter_by(username=subject).first()\n"
|
|
" owner = user.id\n"
|
|
" return Book.query.filter_by(user_id=owner, book_title=book_title).first()\n"
|
|
)
|
|
|
|
|
|
def _entry(function: str, line: int, end: int, access: str, evidence: list[str]) -> EntryPointRecord:
|
|
return EntryPointRecord(
|
|
path="app.py",
|
|
line=line,
|
|
end_line=end,
|
|
function_name=function,
|
|
name=function,
|
|
kind="route",
|
|
access_level=access, # type: ignore[arg-type]
|
|
trust_boundary="http_request",
|
|
access_evidence=evidence,
|
|
conditions=[],
|
|
provenance="test",
|
|
rationale="",
|
|
)
|
|
|
|
|
|
ENTRIES = [
|
|
_entry("guarded", 6, 7, "authenticated", ["@login_required"]),
|
|
_entry("constrained", 12, 14, "authenticated", ["@login_required"]),
|
|
_entry("constrained_too", 18, 20, "public", []),
|
|
_entry("leaky", 24, 26, "public", []),
|
|
_entry("health", 30, 31, "public", []),
|
|
]
|
|
|
|
|
|
def test_find_operations_names_kind_fact_function_and_resource() -> None:
|
|
from openultrasast.semantic.obligations import load_obligation_facts
|
|
from openultrasast.semantic.obligations.operations import find_operations
|
|
|
|
ir = parse_file("app.py", APP, "python")
|
|
ops = find_operations(ir, load_obligation_facts().for_language("python"), text=APP)
|
|
assert [(op.function, op.kind, op.resource) for op in ops] == [
|
|
("guarded", "protected_read", "book"),
|
|
("constrained", "protected_read", "book"),
|
|
("constrained_too", "protected_read", "book"),
|
|
("leaky", "protected_read", "book"),
|
|
# `from_spec` reads the user row to resolve the token subject and then the book: two operations, both real
|
|
("from_spec", "protected_read", "user"),
|
|
("from_spec", "protected_read", "book"),
|
|
]
|
|
assert all(op.path == "app.py" and op.fact_id == "orm-read" and op.line > 0 for op in ops)
|
|
assert not [op for op in ops if op.function == "health"] # a constant return is no operation
|
|
|
|
|
|
def test_find_discharges_reports_witness_kind_provenance_and_scope() -> None:
|
|
from openultrasast.semantic.obligations import load_obligation_facts
|
|
from openultrasast.semantic.obligations.operations import find_discharges
|
|
|
|
ir = parse_file("app.py", APP, "python")
|
|
witnesses = find_discharges(
|
|
ir, load_obligation_facts().for_language("python"), load_facts().for_language("python"), text=APP, entries=ENTRIES
|
|
)
|
|
by_function = {}
|
|
for w in witnesses:
|
|
by_function.setdefault(w.function, []).append((w.kind, w.provenance, w.scope))
|
|
assert ("path_guard", "authenticated_context", "decorator") in by_function["guarded"]
|
|
assert ("identity_constraint", "authenticated_context", "statement") in by_function["constrained"]
|
|
assert ("identity_constraint", "authenticated_context", "statement") in by_function["constrained_too"]
|
|
assert ("identity_constraint", "request_input", "statement") in by_function["leaky"] # present, but discharges nothing
|
|
assert "health" not in by_function
|
|
assert all(w.path == "app.py" and w.fact_id for w in witnesses)
|
|
|
|
|
|
def test_discharge_witness_covers_operation_only_when_kind_and_provenance_fit() -> None:
|
|
from openultrasast.semantic.obligations import load_obligation_facts
|
|
from openultrasast.semantic.obligations.operations import covers, find_discharges, find_operations
|
|
|
|
ir = parse_file("app.py", APP, "python")
|
|
facts = load_obligation_facts().for_language("python")
|
|
ops = {op.function: op for op in find_operations(ir, facts, text=APP)}
|
|
witnesses = find_discharges(ir, facts, load_facts().for_language("python"), text=APP, entries=ENTRIES)
|
|
covering = {op.function: [w.kind for w in witnesses if covers(w, op)] for op in ops.values()}
|
|
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
|