mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 09:48:18 +00:00
feat(access-control-prover): 10.1 abstract dominance relations without prover fallback
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
"""Strict abstract-dominance relations over the CFG (Task 10.1, Reqs 10.1-10.7)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.semantic.cfg import FunctionCfg
|
||||
from openultrasast.semantic.cst import parse_function_nodes
|
||||
from openultrasast.semantic.extra import grammar_for
|
||||
from openultrasast.semantic.obligations.abstract_domain import EvidenceRelation
|
||||
from openultrasast.semantic.obligations.cfg_dominance import CfgDominance, DominanceEvidence
|
||||
from openultrasast.semantic.obligations.operations import Discharge, Operation
|
||||
|
||||
PATH = "src/C.java"
|
||||
|
||||
JAVA_GUARD = """class C {
|
||||
void handle(Request request) {
|
||||
if (!request.user.isAdmin()) { throw new IllegalStateException(); }
|
||||
request.store.deleteAll();
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
JAVA_VALUE_ONLY = """class C {
|
||||
void handle(Request request) {
|
||||
boolean admin = request.user.isAdmin();
|
||||
request.store.deleteAll();
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
JAVA_AFTER = """class C {
|
||||
void handle(Request request) {
|
||||
request.store.deleteAll();
|
||||
if (!request.user.isAdmin()) { throw new IllegalStateException(); }
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
JAVA_SOME_PATHS = """class C {
|
||||
void handle(Request request) {
|
||||
if (request.flag) {
|
||||
if (!request.user.isAdmin()) { throw new IllegalStateException(); }
|
||||
}
|
||||
request.store.deleteAll();
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _witness(line: int | None, *, kind: str = "path_guard", scope: str = "statement") -> Discharge:
|
||||
return Discharge(
|
||||
path=PATH,
|
||||
line=line,
|
||||
function="handle",
|
||||
kind=kind,
|
||||
fact_id="f:guard",
|
||||
provenance="call",
|
||||
scope=scope,
|
||||
detail="",
|
||||
)
|
||||
|
||||
|
||||
def _operation(line: int) -> Operation:
|
||||
return Operation(
|
||||
path=PATH,
|
||||
line=line,
|
||||
function="handle",
|
||||
kind="protected_read",
|
||||
fact_id="f:op",
|
||||
resource="store",
|
||||
requires=("path_guard",),
|
||||
)
|
||||
|
||||
|
||||
def _dominance(text: str) -> CfgDominance:
|
||||
if grammar_for("java") is None:
|
||||
pytest.skip("no Java grammar installed")
|
||||
nodes = parse_function_nodes(PATH, text, "java")
|
||||
assert nodes, "the parser read no function"
|
||||
cfg = FunctionCfg.from_cst(nodes[0], "java")
|
||||
return CfgDominance({PATH: text}, {(PATH, "handle"): cfg})
|
||||
|
||||
|
||||
def _lines(text: str) -> dict[str, int]:
|
||||
file_lines = text.splitlines()
|
||||
return {
|
||||
"guard": next(number for number, line_text in enumerate(file_lines, start=1) if "isAdmin" in line_text),
|
||||
"operation": next(number for number, line_text in enumerate(file_lines, start=1) if "deleteAll" in line_text),
|
||||
}
|
||||
|
||||
|
||||
def test_throwing_guard_on_all_paths_is_must() -> None:
|
||||
dominance = _dominance(JAVA_GUARD)
|
||||
lines = _lines(JAVA_GUARD)
|
||||
evidence = dominance.evidence(_witness(lines["guard"]), _operation(lines["operation"]))
|
||||
assert evidence.relation is EvidenceRelation.MUST
|
||||
assert evidence.guard_effect == "throws"
|
||||
|
||||
|
||||
def test_value_only_guard_on_all_paths_is_no() -> None:
|
||||
dominance = _dominance(JAVA_VALUE_ONLY)
|
||||
lines = _lines(JAVA_VALUE_ONLY)
|
||||
evidence = dominance.evidence(_witness(lines["guard"], kind="ownership_check"), _operation(lines["operation"]))
|
||||
assert evidence.relation is EvidenceRelation.NO
|
||||
assert evidence.guard_effect == "value_only"
|
||||
|
||||
|
||||
def test_guard_after_operation_is_no() -> None:
|
||||
dominance = _dominance(JAVA_AFTER)
|
||||
lines = _lines(JAVA_AFTER)
|
||||
evidence = dominance.evidence(_witness(lines["guard"]), _operation(lines["operation"]))
|
||||
assert evidence.relation is EvidenceRelation.NO
|
||||
|
||||
|
||||
def test_branch_conditional_guard_is_may() -> None:
|
||||
dominance = _dominance(JAVA_SOME_PATHS)
|
||||
lines = _lines(JAVA_SOME_PATHS)
|
||||
evidence = dominance.evidence(_witness(lines["guard"]), _operation(lines["operation"]))
|
||||
assert evidence.relation is EvidenceRelation.MAY
|
||||
|
||||
|
||||
def test_missing_graph_is_unknown_never_order_fallback() -> None:
|
||||
dominance = CfgDominance({PATH: JAVA_GUARD}, {})
|
||||
lines = _lines(JAVA_GUARD)
|
||||
evidence = dominance.evidence(_witness(lines["guard"]), _operation(lines["operation"]))
|
||||
assert evidence.relation is EvidenceRelation.UNKNOWN
|
||||
assert evidence.reason == "cfg_unavailable"
|
||||
|
||||
|
||||
def test_line_without_graph_node_is_unknown() -> None:
|
||||
dominance = _dominance(JAVA_GUARD)
|
||||
lines = _lines(JAVA_GUARD)
|
||||
evidence = dominance.evidence(_witness(1), _operation(lines["operation"]))
|
||||
assert evidence.relation is EvidenceRelation.UNKNOWN
|
||||
assert evidence.reason == "line_without_node"
|
||||
|
||||
|
||||
def test_cross_function_witness_is_unknown_never_order() -> None:
|
||||
dominance = _dominance(JAVA_GUARD)
|
||||
lines = _lines(JAVA_GUARD)
|
||||
cross_function_witness = Discharge(
|
||||
path=PATH,
|
||||
line=lines["guard"],
|
||||
function="other",
|
||||
kind="path_guard",
|
||||
fact_id="f:guard",
|
||||
provenance="call",
|
||||
scope="hop",
|
||||
detail="",
|
||||
)
|
||||
evidence = dominance.evidence(cross_function_witness, _operation(lines["operation"]))
|
||||
assert evidence.relation is EvidenceRelation.UNKNOWN
|
||||
assert evidence.reason == "dominance:cross_function"
|
||||
|
||||
|
||||
def test_scoped_witness_dominates_whole_handler() -> None:
|
||||
dominance = _dominance(JAVA_GUARD)
|
||||
lines = _lines(JAVA_GUARD)
|
||||
scoped = _witness(None, scope="router")
|
||||
evidence = dominance.evidence(scoped, _operation(lines["operation"]))
|
||||
assert evidence.relation is EvidenceRelation.MUST
|
||||
|
||||
|
||||
def test_in_operation_witness_same_line_is_must() -> None:
|
||||
dominance = _dominance(JAVA_GUARD)
|
||||
lines = _lines(JAVA_GUARD)
|
||||
constraint = _witness(lines["operation"], kind="identity_constraint")
|
||||
evidence = dominance.evidence(constraint, _operation(lines["operation"]))
|
||||
assert evidence.relation is EvidenceRelation.MUST
|
||||
|
||||
|
||||
def test_legacy_boolean_surface_unchanged() -> None:
|
||||
dominance = CfgDominance({PATH: JAVA_GUARD}, {})
|
||||
lines = _lines(JAVA_GUARD)
|
||||
assert dominance.dominates(_witness(lines["guard"]), _operation(lines["operation"]), ()) is True
|
||||
assert isinstance(dominance.relation(_witness(lines["guard"]), _operation(lines["operation"])), str)
|
||||
|
||||
|
||||
def test_dominance_evidence_is_frozen() -> None:
|
||||
evidence = DominanceEvidence(relation=EvidenceRelation.MUST, guard_effect="throws", reason=None)
|
||||
with pytest.raises(Exception): # noqa: B017 - frozen dataclass raises on assignment
|
||||
evidence.relation = EvidenceRelation.NO # type: ignore[misc]
|
||||
|
||||
|
||||
def test_evidence_is_deterministic() -> None:
|
||||
dominance = _dominance(JAVA_GUARD)
|
||||
lines = _lines(JAVA_GUARD)
|
||||
first = dominance.evidence(_witness(lines["guard"]), _operation(lines["operation"]))
|
||||
second = dominance.evidence(_witness(lines["guard"]), _operation(lines["operation"]))
|
||||
assert first == second
|
||||
Reference in New Issue
Block a user