mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
Task 2.2. Handlers on the same resource under the same module are siblings; when at least min_siblings of them discharge an obligation and one does not, that one is the anomaly and the discharging siblings are its evidence (Req 3.2). A set with fewer members than the minimum is under-populated and never reported (Req 3.3). A public route among authenticated siblings is a missing path guard with the mapper's access classification as evidence (Req 3.4). The sibling key is the module path without its suffix, so two same-named files in different directories are never merged. Witness validity (an identity constraint counts only when it binds the authenticated context) is one helper shared by operations, siblings and the checker. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
99 lines
4.7 KiB
Python
99 lines
4.7 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"
|
|
)
|
|
|
|
|
|
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"),
|
|
]
|
|
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)
|