mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 09:48:18 +00:00
feat(access-control-prover): 10.6 three-way judgments in the prover
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"""End-to-end three-way judgment mode for ``prove_change`` (task 10.6).
|
||||
|
||||
The fixtures mirror ``tests/test_hunt_prover.py``: a real git repository whose head
|
||||
commit introduces one Stapler-shaped Java class. Judgment mode is opt-in; the
|
||||
legacy boolean flow must stay byte-identical without the keyword (Req 4.6).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from openultrasast.hunt.producers import Context
|
||||
from openultrasast.hunt.prover import prove_change
|
||||
from openultrasast.semantic.obligations.trace import parse_abstract_state
|
||||
|
||||
|
||||
def repository(tmp_path: Any, java_text: str) -> tuple[Any, dict[str, Any]]:
|
||||
"""Build a one-commit repository whose head commit introduces ``java_text``."""
|
||||
work = tmp_path / "repo"
|
||||
work.mkdir()
|
||||
run = lambda *args: subprocess.run( # noqa: E731 - fixture-local shorthand
|
||||
args, cwd=work, check=True, capture_output=True, text=True
|
||||
)
|
||||
run("git", "init", "-q")
|
||||
run("git", "config", "user.email", "[email protected]")
|
||||
run("git", "config", "user.name", "fixture")
|
||||
(work / "Fixture.java").write_text("class Plain {}\n", encoding="utf-8")
|
||||
run("git", "add", "Fixture.java")
|
||||
run("git", "commit", "-qm", "base")
|
||||
head_lines = {
|
||||
"Fixture.java": [
|
||||
index for index, line in enumerate(java_text.splitlines(), start=1) if line.strip() and "class Fixture" not in line
|
||||
]
|
||||
}
|
||||
(work / "Fixture.java").write_text(java_text, encoding="utf-8")
|
||||
run("git", "add", "Fixture.java")
|
||||
run("git", "commit", "-qm", "head")
|
||||
head = run("git", "rev-parse", "HEAD").stdout.strip()
|
||||
base = run("git", "rev-parse", "HEAD~1").stdout.strip()
|
||||
change = dict(
|
||||
id="change-1",
|
||||
repo="fixture/repo",
|
||||
kind="vulnerability",
|
||||
base=base,
|
||||
head=head,
|
||||
files=[{"path": "Fixture.java", "head_lines": [[1, len(java_text.splitlines())]]}],
|
||||
head_lines=head_lines,
|
||||
)
|
||||
return work, change
|
||||
|
||||
|
||||
UNGUARDED = """class Fixture extends Descriptor<Thing> {
|
||||
public Object doTestConnection() {
|
||||
return CredentialsProvider.lookupCredentials(Thing.class);
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
GUARDED = """class Fixture extends Descriptor<Thing> {
|
||||
public Object doTestConnection() {
|
||||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||||
return CredentialsProvider.lookupCredentials(Thing.class);
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
CROSS_FUNCTION = """class Fixture extends Descriptor<Thing> {
|
||||
public Object doTestConnection() {
|
||||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||||
return this.helper();
|
||||
}
|
||||
|
||||
private Object helper() {
|
||||
return CredentialsProvider.lookupCredentials(Thing.class);
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _prove(work: Any, change: dict[str, Any], **kwargs: Any) -> Any:
|
||||
context = Context(root=work, change=change)
|
||||
return prove_change(work, change, context, **kwargs)
|
||||
|
||||
|
||||
def test_unguarded_fixture_flags_and_counts_unguarded(tmp_path: Any) -> None:
|
||||
work, change = repository(tmp_path, UNGUARDED)
|
||||
result = _prove(work, change, judgment_mode=True)
|
||||
assert len(result.findings) == 3
|
||||
finding = result.findings[0]
|
||||
assert finding["missing"] in {"path_guard", "ownership_check", "identity_constraint"}
|
||||
assert finding["family"] == "access_control"
|
||||
abstract = parse_abstract_state(finding["trace"]["abstract_state"])
|
||||
assert abstract.path_judgment.judgment.value == "unguarded"
|
||||
assert abstract.path_judgment.missing == ("identity_constraint", "ownership_check", "path_guard")
|
||||
assert result.coverage.pair_judgments.unguarded == 1
|
||||
|
||||
|
||||
def test_guarded_fixture_is_indeterminate_without_resource_binding(tmp_path: Any) -> None:
|
||||
work, change = repository(tmp_path, GUARDED)
|
||||
result = _prove(work, change, judgment_mode=True)
|
||||
assert result.findings == ()
|
||||
assert result.coverage.pair_judgments.indeterminate == 1
|
||||
assert result.coverage.pair_judgments.discharged == 0
|
||||
abstract_states = [trace["abstract_state"] for trace in result.traces if "abstract_state" in trace]
|
||||
assert len(abstract_states) == 1
|
||||
assert parse_abstract_state(abstract_states[0]).path_judgment.judgment.value == "indeterminate"
|
||||
|
||||
|
||||
def test_cross_function_guard_is_indeterminate_in_strict_mode(tmp_path: Any) -> None:
|
||||
work, change = repository(tmp_path, CROSS_FUNCTION)
|
||||
result = _prove(work, change, judgment_mode=True)
|
||||
assert result.findings == ()
|
||||
assert result.coverage.pair_judgments.indeterminate == 1
|
||||
assert result.coverage.pair_judgments.discharged == 0
|
||||
|
||||
|
||||
def test_legacy_mode_is_unchanged_without_the_keyword(tmp_path: Any) -> None:
|
||||
work, change = repository(tmp_path, GUARDED)
|
||||
result = _prove(work, change)
|
||||
assert result.findings == ()
|
||||
assert all("abstract_state" not in trace for trace in result.traces)
|
||||
assert result.coverage.pair_judgments.unguarded == 0
|
||||
assert result.coverage.pair_judgments.discharged == 0
|
||||
assert result.coverage.pair_judgments.indeterminate == 0
|
||||
|
||||
|
||||
def test_traces_carry_judgment_and_missing_kinds(tmp_path: Any) -> None:
|
||||
work, change = repository(tmp_path, UNGUARDED)
|
||||
result = _prove(work, change, judgment_mode=True)
|
||||
assert result.traces
|
||||
judgment_values = {
|
||||
parse_abstract_state(trace["abstract_state"]).path_judgment.judgment.value for trace in result.traces if "abstract_state" in trace
|
||||
}
|
||||
assert judgment_values == {"unguarded"}
|
||||
Reference in New Issue
Block a user