mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
112 lines
5.2 KiB
Python
112 lines
5.2 KiB
Python
"""authorization-obligations task 2.2: sibling handler sets and consistency anomalies (offline)."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
||
|
|
from test_obligation_operations import APP, ENTRIES # noqa: E402
|
||
|
|
|
||
|
|
from openultrasast.semantic.facts import load_facts # noqa: E402
|
||
|
|
from openultrasast.semantic.ir import parse_file # noqa: E402
|
||
|
|
|
||
|
|
|
||
|
|
def _inputs():
|
||
|
|
from openultrasast.semantic.obligations import load_obligation_facts
|
||
|
|
from openultrasast.semantic.obligations.operations import find_discharges, find_operations
|
||
|
|
|
||
|
|
ir = parse_file("app.py", APP, "python")
|
||
|
|
facts = load_obligation_facts().for_language("python")
|
||
|
|
ops = find_operations(ir, facts, text=APP)
|
||
|
|
wit = find_discharges(ir, facts, load_facts().for_language("python"), text=APP, entries=ENTRIES)
|
||
|
|
return ops, wit
|
||
|
|
|
||
|
|
|
||
|
|
def test_sibling_sets_group_by_router_and_resource_and_record_who_discharges() -> None:
|
||
|
|
from openultrasast.semantic.obligations.siblings import sibling_sets
|
||
|
|
|
||
|
|
ops, wit = _inputs()
|
||
|
|
sets = sibling_sets(ENTRIES, ops, wit)
|
||
|
|
assert len(sets) == 1
|
||
|
|
(books,) = sets
|
||
|
|
assert books.key == ("app", "book")
|
||
|
|
assert set(books.handlers) == {"app.py::guarded", "app.py::constrained", "app.py::constrained_too", "app.py::leaky"}
|
||
|
|
assert books.discharging["identity_constraint"] == (
|
||
|
|
"app.py::constrained",
|
||
|
|
"app.py::constrained_too",
|
||
|
|
) # leaky's request-bound one does not count
|
||
|
|
assert books.discharging["path_guard"] == ("app.py::constrained", "app.py::guarded")
|
||
|
|
assert books.operation_kinds == ("protected_read",)
|
||
|
|
|
||
|
|
|
||
|
|
def test_consistency_anomalies_respect_the_minimum_and_name_the_siblings() -> None:
|
||
|
|
from openultrasast.semantic.obligations.siblings import consistency_anomalies, sibling_sets
|
||
|
|
|
||
|
|
ops, wit = _inputs()
|
||
|
|
sets = sibling_sets(ENTRIES, ops, wit)
|
||
|
|
anomalies, under = consistency_anomalies(sets, min_siblings=2)
|
||
|
|
by_handler = {(a.handler, a.missing): a for a in anomalies}
|
||
|
|
# tasks.md observable: two siblings constrain by owner -> one anomaly per unconstrained handler naming both siblings
|
||
|
|
assert by_handler[("app.py::leaky", "identity_constraint")].siblings == ("app.py::constrained", "app.py::constrained_too")
|
||
|
|
assert by_handler[("app.py::guarded", "identity_constraint")].siblings == ("app.py::constrained", "app.py::constrained_too")
|
||
|
|
assert by_handler[("app.py::leaky", "identity_constraint")].operation_kind == "protected_read"
|
||
|
|
assert ("app.py::constrained", "identity_constraint") not in by_handler
|
||
|
|
assert under == ()
|
||
|
|
# Req 3.2: the configured minimum applies to how many siblings discharge, not to one sibling setting the norm
|
||
|
|
anomalies_three, _ = consistency_anomalies(sets, min_siblings=3)
|
||
|
|
assert not [a for a in anomalies_three if a.missing == "identity_constraint"] # only two discharge
|
||
|
|
anomalies_strict, under_strict = consistency_anomalies(sets, min_siblings=5)
|
||
|
|
assert anomalies_strict == () and under_strict == (("app", "book"),) # Req 3.3: under-populated, never reported
|
||
|
|
|
||
|
|
|
||
|
|
def test_public_route_among_authenticated_siblings_is_a_missing_path_guard_with_access_evidence() -> None:
|
||
|
|
from openultrasast.semantic.obligations.siblings import consistency_anomalies, sibling_sets
|
||
|
|
|
||
|
|
ops, wit = _inputs()
|
||
|
|
sets = sibling_sets(ENTRIES, ops, wit)
|
||
|
|
anomalies, _ = consistency_anomalies(sets, min_siblings=2)
|
||
|
|
guard_anomalies = [a for a in anomalies if a.missing == "path_guard"]
|
||
|
|
assert {a.handler for a in guard_anomalies} == {"app.py::constrained_too", "app.py::leaky"}
|
||
|
|
assert all(a.access_level == "public" and "authenticated" in " ".join(a.evidence) for a in guard_anomalies)
|
||
|
|
assert all(a.siblings == ("app.py::constrained", "app.py::guarded") for a in guard_anomalies)
|
||
|
|
|
||
|
|
|
||
|
|
def test_same_named_files_in_different_modules_are_not_siblings() -> None:
|
||
|
|
"""Round-2 finding: the key must be the module path, not the file's basename."""
|
||
|
|
from openultrasast.mapping import EntryPointRecord
|
||
|
|
from openultrasast.semantic.obligations.operations import Operation
|
||
|
|
from openultrasast.semantic.obligations.siblings import sibling_sets
|
||
|
|
|
||
|
|
def entry(path: str, function: str) -> EntryPointRecord:
|
||
|
|
return EntryPointRecord(
|
||
|
|
path=path,
|
||
|
|
line=1,
|
||
|
|
end_line=2,
|
||
|
|
function_name=function,
|
||
|
|
name=function,
|
||
|
|
kind="route",
|
||
|
|
access_level="public",
|
||
|
|
trust_boundary="http_request",
|
||
|
|
access_evidence=[],
|
||
|
|
conditions=[],
|
||
|
|
provenance="t",
|
||
|
|
rationale="",
|
||
|
|
) # type: ignore[arg-type]
|
||
|
|
|
||
|
|
def op(path: str, function: str) -> Operation:
|
||
|
|
return Operation(
|
||
|
|
path=path,
|
||
|
|
line=2,
|
||
|
|
function=function,
|
||
|
|
kind="protected_read",
|
||
|
|
fact_id="orm-read",
|
||
|
|
resource="book",
|
||
|
|
requires=("identity_constraint",),
|
||
|
|
)
|
||
|
|
|
||
|
|
entries = [entry("api/v1/books.py", "list_a"), entry("admin/legacy/books.py", "list_b")]
|
||
|
|
sets = sibling_sets(entries, [op("api/v1/books.py", "list_a"), op("admin/legacy/books.py", "list_b")], [])
|
||
|
|
assert {group.key for group in sets} == {("api/v1/books", "book"), ("admin/legacy/books", "book")}
|
||
|
|
assert all(len(group.handlers) == 1 for group in sets)
|