2026-09-06 07:24:13 +02:00
|
|
|
"""authorization-obligations task 2.5: the checker, function-local and path-aware (offline)."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
|
from test_obligation_operations import APP, ENTRIES, _entry # noqa: E402
|
|
|
|
|
|
|
|
|
|
from openultrasast.semantic.facts import load_facts # noqa: E402
|
|
|
|
|
from openultrasast.semantic.ir import parse_file # noqa: E402
|
|
|
|
|
|
|
|
|
|
POLICY = (
|
|
|
|
|
'version = 1\nidentity_source = "request.user"\n\n[[resource]]\nname = "book"\nsensitivity = "high"\nidentity_field = "user_id"\n\n'
|
|
|
|
|
'[[route]]\npath = "/books/any/*"\naccess = "public"\n'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class _Path:
|
|
|
|
|
hops: tuple[tuple[str, int], ...]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _check(
|
|
|
|
|
*, policy_text: str | None = None, paths=(), dominance=None, min_siblings: int = 2, store_shapes=(), tmp_path: Path | None = None
|
|
|
|
|
):
|
|
|
|
|
from openultrasast.semantic.obligations import load_obligation_facts
|
|
|
|
|
from openultrasast.semantic.obligations.check import check_obligations
|
|
|
|
|
from openultrasast.semantic.obligations.dominance import OrderDominance
|
|
|
|
|
from openultrasast.semantic.obligations.policy import load_declared_policy
|
|
|
|
|
|
|
|
|
|
policy = None
|
|
|
|
|
if policy_text is not None and tmp_path is not None:
|
|
|
|
|
(tmp_path / "obligations.toml").write_text(policy_text)
|
|
|
|
|
policy = load_declared_policy(tmp_path / "obligations.toml")
|
|
|
|
|
ir = parse_file("app.py", APP, "python")
|
|
|
|
|
return check_obligations(
|
|
|
|
|
irs={"app.py": (ir, APP)},
|
|
|
|
|
entries=ENTRIES,
|
|
|
|
|
facts=load_obligation_facts(),
|
|
|
|
|
flow_facts=load_facts(),
|
|
|
|
|
policy=policy,
|
|
|
|
|
paths=paths,
|
|
|
|
|
dominance=dominance or OrderDominance(texts={"app.py": APP}),
|
|
|
|
|
store_shapes=store_shapes,
|
|
|
|
|
min_siblings=min_siblings,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_function_local_run_reports_the_consistency_violation_with_siblings_and_a_degradation() -> None:
|
|
|
|
|
result = _check()
|
|
|
|
|
identity = [f for f in result.findings if f.missing == "identity_constraint"]
|
|
|
|
|
assert {f.operation.function for f in identity} == {"leaky", "guarded"}
|
|
|
|
|
leaky = next(f for f in identity if f.operation.function == "leaky")
|
|
|
|
|
assert leaky.label == "consistency_violation" and leaky.provenance == "request_input"
|
|
|
|
|
assert "app.py::constrained" in leaky.evidence and "app.py::constrained_too" in leaky.evidence # names both siblings
|
|
|
|
|
assert leaky.operation.resource == "book" and leaky.operation.kind == "protected_read"
|
|
|
|
|
assert not [f for f in result.findings if f.operation.function == "constrained" and f.missing == "identity_constraint"]
|
|
|
|
|
(degradation,) = [d for d in result.degradations if d["reason"] == "obligations_function_local"]
|
|
|
|
|
assert degradation["language"] == "python" and degradation["files"] == ["app.py"] # Req 5.3: names the file
|
|
|
|
|
assert result.sibling_sets and result.discharges
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_declared_policy_outranks_consistency_and_public_routes_drop_only_the_guard(tmp_path: Path) -> None:
|
|
|
|
|
result = _check(policy_text=POLICY, tmp_path=tmp_path)
|
|
|
|
|
leaky = [f for f in result.findings if f.operation.function == "leaky"]
|
|
|
|
|
identity = next(f for f in leaky if f.missing == "identity_constraint")
|
|
|
|
|
assert identity.label == "declared_policy_violation" and any("resource book" in e for e in identity.evidence)
|
|
|
|
|
assert not [f for f in leaky if f.missing == "path_guard"] # /books/any/* is declared public
|
|
|
|
|
constrained_guard = [f for f in result.findings if f.operation.function == "constrained_too" and f.missing == "path_guard"]
|
|
|
|
|
assert constrained_guard and constrained_guard[0].label == "consistency_violation" # not declared public: the guard anomaly stands
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_path_records_gate_reachability_and_a_dominating_hop_discharges() -> None:
|
|
|
|
|
from openultrasast.semantic.obligations.operations import Discharge
|
|
|
|
|
|
|
|
|
|
class _HopAware:
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.calls = 0
|
|
|
|
|
|
|
|
|
|
def dominates(self, witness: Discharge, operation, path) -> bool: # type: ignore[no-untyped-def]
|
|
|
|
|
self.calls += 1
|
|
|
|
|
return witness.scope == "hop"
|
|
|
|
|
|
|
|
|
|
reached = (_Path(hops=(("api.py::gateway", 3), ("app.py::leaky", 25))),)
|
|
|
|
|
result = _check(paths=reached)
|
|
|
|
|
assert {f.operation.function for f in result.findings} == {"leaky"} # only the operation a path reaches
|
|
|
|
|
assert not any(d["reason"] == "obligations_function_local" for d in result.degradations)
|
|
|
|
|
dominance = _HopAware()
|
|
|
|
|
hop_guard = Discharge(
|
|
|
|
|
path="api.py", line=1, function="gateway", kind="identity_constraint", fact_id="x", provenance="authenticated_context", scope="hop"
|
|
|
|
|
)
|
|
|
|
|
covered = _check(paths=reached, dominance=dominance, store_shapes=(), min_siblings=2)
|
|
|
|
|
assert dominance.calls == 0 # leaky's only witness is request-bound, so there is nothing valid to ask dominance about
|
|
|
|
|
from openultrasast.semantic.obligations import load_obligation_facts
|
|
|
|
|
from openultrasast.semantic.obligations.check import check_obligations
|
|
|
|
|
|
|
|
|
|
ir = parse_file("app.py", APP, "python")
|
|
|
|
|
with_hop = check_obligations(
|
|
|
|
|
irs={"app.py": (ir, APP)},
|
|
|
|
|
entries=ENTRIES,
|
|
|
|
|
facts=load_obligation_facts(),
|
|
|
|
|
flow_facts=load_facts(),
|
|
|
|
|
policy=None,
|
|
|
|
|
paths=reached,
|
|
|
|
|
dominance=dominance,
|
|
|
|
|
store_shapes=(),
|
|
|
|
|
min_siblings=2,
|
|
|
|
|
extra_witnesses=(hop_guard,),
|
|
|
|
|
)
|
|
|
|
|
assert dominance.calls > 0 # the hop witness is valid, so the checker consulted the injected Dominance implementation
|
|
|
|
|
assert not [f for f in with_hop.findings if f.operation.function == "leaky" and f.missing == "identity_constraint"]
|
|
|
|
|
assert covered.findings # without the hop witness the finding stands
|
|
|
|
|
|
|
|
|
|
|
2026-09-08 12:15:54 +02:00
|
|
|
def test_findings_become_suspicion_static_findings_with_labels() -> None:
|
|
|
|
|
"""The finding-id contract, the suspicion rung and the tags, without the deleted mechanism store.
|
2026-09-06 07:24:13 +02:00
|
|
|
|
2026-09-08 12:15:54 +02:00
|
|
|
The original also asserted `known_fix` — a hint the store supplied. The store is gone, so the hint is gone
|
|
|
|
|
with it; everything the checker establishes on its own is still pinned here.
|
|
|
|
|
"""
|
|
|
|
|
from openultrasast.semantic.obligations.check import findings_to_static
|
|
|
|
|
|
|
|
|
|
result = _check()
|
2026-09-06 07:24:13 +02:00
|
|
|
leaky = next(f for f in result.findings if f.operation.function == "leaky" and f.missing == "identity_constraint")
|
2026-09-08 12:15:54 +02:00
|
|
|
guarded = next(f for f in result.findings if f.operation.function == "guarded" and f.missing == "identity_constraint")
|
2026-09-06 07:24:13 +02:00
|
|
|
statics = findings_to_static(result)
|
|
|
|
|
ids = {s.finding_id for s in statics}
|
|
|
|
|
assert f"obligation:protected_read:app.py:{leaky.operation.line}:identity_constraint" in ids # exact id contract
|
|
|
|
|
assert f"obligation:protected_read:app.py:{guarded.operation.line}:identity_constraint" in ids
|
|
|
|
|
static = next(s for s in statics if s.line == leaky.operation.line and "discharger:identity_constraint" in s.tags)
|
|
|
|
|
assert static.evidence_level == "suspicion"
|
2026-09-08 12:15:54 +02:00
|
|
|
assert {"obligation:protected_read", "discharger:identity_constraint", "obligation_evidence:consistency_violation"} <= set(static.tags)
|
2026-09-06 07:24:13 +02:00
|
|
|
assert "app.py::constrained" in static.rationale
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_with_path_records_an_unjustified_obligation_yields_no_finding() -> None:
|
|
|
|
|
"""Round-2 finding: `function_local` is the degraded mode's label only; with paths and no sibling or clause, nothing."""
|
|
|
|
|
reached = (_Path(hops=(("api.py::gateway", 3), ("app.py::leaky", 25))),)
|
|
|
|
|
result = _check(paths=reached, min_siblings=5) # every set under-populated: no consistency evidence, no policy
|
|
|
|
|
assert result.findings == () and result.under_populated == (("app", "book"),)
|
|
|
|
|
assert not any(d["reason"] == "obligations_function_local" for d in result.degradations)
|
|
|
|
|
assert all(f.label != "consistency_violation" or f.evidence for f in _check(paths=reached, min_siblings=2).findings)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ADMIN = (
|
|
|
|
|
"from flask import request\n\n\n"
|
|
|
|
|
"@app.route('/admin/reset/<name>')\ndef reset(name):\n"
|
|
|
|
|
" set_password(name, 'temporary')\n"
|
|
|
|
|
" return Book.query.filter_by(book_title=name).first()\n"
|
|
|
|
|
)
|
|
|
|
|
ADMIN_ENTRIES = [_entry("reset", 4, 6, "public", [])]
|
|
|
|
|
ADMIN_POLICY = (
|
|
|
|
|
'version = 1\nidentity_source = "request.user"\n\n[[resource]]\nname = "book"\nsensitivity = "high"\n\n'
|
|
|
|
|
'[[route]]\npath = "/admin/*"\naccess = "public"\n'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _check_admin(tmp_path: Path, policy_text: str | None):
|
|
|
|
|
from openultrasast.semantic.obligations import load_obligation_facts
|
|
|
|
|
from openultrasast.semantic.obligations.check import check_obligations
|
|
|
|
|
from openultrasast.semantic.obligations.dominance import OrderDominance
|
|
|
|
|
from openultrasast.semantic.obligations.policy import load_declared_policy
|
|
|
|
|
|
|
|
|
|
policy = None
|
|
|
|
|
if policy_text is not None:
|
|
|
|
|
(tmp_path / "obligations.toml").write_text(policy_text)
|
|
|
|
|
policy = load_declared_policy(tmp_path / "obligations.toml")
|
|
|
|
|
ir = parse_file("app.py", ADMIN, "python")
|
|
|
|
|
return check_obligations(
|
|
|
|
|
irs={"app.py": (ir, ADMIN)},
|
|
|
|
|
entries=ADMIN_ENTRIES,
|
|
|
|
|
facts=load_obligation_facts(),
|
|
|
|
|
flow_facts=load_facts(),
|
|
|
|
|
policy=policy,
|
|
|
|
|
paths=(),
|
|
|
|
|
dominance=OrderDominance(texts={"app.py": ADMIN}),
|
|
|
|
|
store_shapes=(),
|
|
|
|
|
min_siblings=2,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_declared_public_route_never_reports_its_missing_path_guard_but_keeps_the_resource_obligation(tmp_path: Path) -> None:
|
|
|
|
|
"""Round-3 finding (Req 4.4): the waiver must also cover the operation's own requirement, not only sibling anomalies."""
|
|
|
|
|
without = _check_admin(tmp_path, None)
|
|
|
|
|
assert [(f.operation.kind, f.missing) for f in without.findings if f.operation.kind == "privileged_action"] == [
|
|
|
|
|
("privileged_action", "path_guard")
|
|
|
|
|
]
|
|
|
|
|
declared = _check_admin(tmp_path, ADMIN_POLICY)
|
|
|
|
|
assert not [f for f in declared.findings if f.missing == "path_guard"] # /admin/* is declared public: no guard obligation
|
|
|
|
|
identity = [f for f in declared.findings if f.operation.kind == "protected_read" and f.missing == "identity_constraint"]
|
|
|
|
|
assert identity and identity[0].label == "declared_policy_violation" # the declared resource still owes its identity constraint
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_malformed_store_row_is_a_degradation_not_a_crash() -> None:
|
|
|
|
|
"""Design Error Handling: store row with malformed obligation shape -> `obligations_store_row_invalid`."""
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class _Row:
|
|
|
|
|
id: str
|
|
|
|
|
shape: object
|
|
|
|
|
|
|
|
|
|
result = _check(store_shapes=(_Row(id="corpus:bad", shape="garbage"), _Row(id="corpus:worse", shape={"family": "obligation"})))
|
|
|
|
|
(degradation,) = [d for d in result.degradations if d["reason"] == "obligations_store_row_invalid"]
|
|
|
|
|
assert degradation["records"] == ["corpus:bad", "corpus:worse"]
|
|
|
|
|
assert all(f.known_fix is None for f in result.findings)
|
2026-10-14 16:50:04 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_recognised_entry_premise_is_opt_in_and_never_applies_to_public():
|
|
|
|
|
from dataclasses import replace
|
|
|
|
|
|
|
|
|
|
from openultrasast.semantic.obligations import load_obligation_facts
|
|
|
|
|
from openultrasast.semantic.obligations.check import check_obligations, findings_to_static
|
|
|
|
|
from openultrasast.semantic.obligations.dominance import OrderDominance
|
|
|
|
|
|
|
|
|
|
text = "def exposed(title):\n return Book.query.filter_by(book_title=title).first()\n"
|
|
|
|
|
entry = replace(_entry("exposed", 1, 2, "authenticated", ["framework entry"]), access_level="unknown")
|
|
|
|
|
kwargs = dict(
|
|
|
|
|
irs={"app.py": (parse_file("app.py", text, "python"), text)},
|
|
|
|
|
entries=[entry],
|
|
|
|
|
facts=load_obligation_facts(),
|
|
|
|
|
flow_facts=load_facts(),
|
|
|
|
|
policy=None,
|
|
|
|
|
paths=[_Path((("app.py::exposed", 1),))],
|
|
|
|
|
dominance=OrderDominance({"app.py": text}),
|
|
|
|
|
store_shapes=(),
|
|
|
|
|
min_siblings=3,
|
|
|
|
|
)
|
|
|
|
|
assert check_obligations(**kwargs).findings == ()
|
|
|
|
|
result = check_obligations(**kwargs, assume_entries_protected=True)
|
|
|
|
|
(finding,) = result.findings
|
|
|
|
|
assert finding.label == "unguarded_path"
|
|
|
|
|
assert "no declared policy clause and no sibling norm" in "; ".join(finding.evidence)
|
|
|
|
|
assert "framework entry" in "; ".join(finding.evidence)
|
|
|
|
|
assert "unguarded_path" in findings_to_static(result)[0].rationale
|
|
|
|
|
kwargs["entries"] = [replace(entry, access_level="public")]
|
|
|
|
|
assert check_obligations(**kwargs, assume_entries_protected=True).findings == ()
|