feat(prover): access-control prover, delivery A: path records, Stapler and Jetty entries, Java operation rows, the prover and its scoring arm

Codex delivery under spec access-control-prover. `semantic/obligations/paths.py` builds entry-to-function path
records from the cached caller index; `mapping.py` names Stapler web methods and Jetty handlers as entries;
`check_obligations` gains the opt-in premise `assume_entries_protected` with the label `unguarded_path`;
`hunt/prover.py` proves a change with no model call and states its coverage; `benchmarks/valid/prover_arm.py`
scores it with the valid-benchmark metrics. Two fixes after the first smoke run: a product file git holds empty
is read, not unread (four changes failed on `__init__.py` and empty assets), and absent-input reasons are
counted by name.

Smoke on the 23 fix changes of the training pool: 7 pushes caught (30 percent, Wilson 16 to 51), 28 of 93
locations, precision 7 of 8 flagged, no instrument failure; three changes over the 120 second bound. Twelve of
the 17 Jenkins fixes reach the fixed method but check no operation because the method is the operation;
the spec now carries handler-named operations for delivery B.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-14 16:50:04 +02:00
co-authored by Claude Fable 5.1
parent e514551cb6
commit beb272a5ec
15 changed files with 1352 additions and 17 deletions
+183
View File
@@ -0,0 +1,183 @@
"""Prover integration on instrumented local Git fixtures."""
import subprocess
import pytest
from openultrasast.hunt.producers import Context
from openultrasast.hunt.prover import prove_change
def repository(tmp_path, text, extra=None, lines=None):
def git(*args):
return subprocess.run(
["git", "-C", str(tmp_path), "-c", "core.hooksPath=/dev/null", *args], check=True, capture_output=True, text=True
).stdout.strip()
git("init", "-q")
git("config", "user.name", "Fixture")
git("config", "user.email", "[email protected]")
(tmp_path / "Fixture.java").write_text("class Plain {}\n")
git("add", "Fixture.java")
git("commit", "-qm", "base")
base = git("rev-parse", "HEAD")
(tmp_path / "Fixture.java").write_text(text)
for path, value in (extra or {}).items():
(tmp_path / path).parent.mkdir(parents=True, exist_ok=True)
(tmp_path / path).write_text(value)
git("add", path)
git("add", "Fixture.java")
git("commit", "-qm", "head")
change = dict(
id="fixture",
repo="fixture/app",
kind="ordinary",
base=base,
head=git("rev-parse", "HEAD"),
files=[{"path": "Fixture.java", "head_lines": lines or [[1, len(text.splitlines())]]}],
)
return tmp_path, change
@pytest.mark.parametrize("guard,findings", [("", 1), ("Jenkins.get().checkPermission(Jenkins.ADMINISTER);", 0)])
def test_stapler_verdict(tmp_path, guard, findings):
root, change = repository(
tmp_path,
f"""class X extends Descriptor<Thing> {{
public Object doTestConnection() {{
{guard}
return CredentialsProvider.lookupCredentials(Thing.class);
}}
}}""",
)
ctx = Context(root, change)
result = prove_change(root, change, ctx)
assert len(result.findings) == findings
assert result.coverage.operations_checked == 1
assert result.coverage.operations_discharged == 1 - findings
assert result.coverage.reached == 1
assert len(ctx.read("Fixture.java").encode()) > 0
if findings:
(finding,) = result.findings
assert finding["family"] == "access_control" and finding["function"] == "doTestConnection"
assert finding["id"] == "obligation:protected_read:Fixture.java:4:path_guard"
assert finding["entry"]["kind"] == "route" and finding["hops"] == [["Fixture.java::doTestConnection", None]]
assert finding["operation"]["calls"] == ["lookupCredentials"] and finding["missing"] == "path_guard"
assert finding["evidence"]
def test_unrecognised_is_coverage_only(tmp_path):
root, change = repository(tmp_path, "class Plain { public Object run() { return lookupCredentials(); } }")
result = prove_change(root, change)
assert not result.findings and result.coverage.frameworks_recognised == {}
assert result.coverage.unchecked_by_reason["framework_unrecognised"] == 1
@pytest.mark.parametrize("guard_position,expected", [("before", 0), ("after", 1), ("none", 1)])
def test_callee_and_caller_guard_order(tmp_path, guard_position, expected):
guard = "Jenkins.get().checkPermission(Jenkins.ADMINISTER);"
root, change = repository(
tmp_path,
f"""class X extends Descriptor<Thing> {{
public Object doTestConnection() {{
{guard if guard_position == "before" else ""}
Object result = Helper.readSecret();
{guard if guard_position == "after" else ""}
return result;
}}
}}""",
{"Helper.java": "class Helper { public static Object readSecret() { return lookupCredentials(); } }"},
[[2, 7]],
)
result = prove_change(root, change)
assert len(result.findings) == expected and result.coverage.operations_checked == 1
if expected:
assert result.findings[0]["path"] == "Helper.java"
assert len(result.findings[0]["hops"]) == 2
def test_touched_helper_walks_to_entry(tmp_path):
root, change = repository(
tmp_path,
"""class X extends Descriptor<Thing> {
public Object doTestConnection() { return readSecret(); }
public Object readSecret() { return lookupCredentials(); }
}""",
lines=[[3, 3]],
)
result = prove_change(root, change)
assert len(result.findings) == 1 and len(result.findings[0]["hops"]) == 2
def test_all_paths_are_checked(tmp_path):
root, change = repository(
tmp_path,
"""class X extends Descriptor<Thing> {
public Object doFirst() {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
return readSecret();
}
public Object doSecond() { return readSecret(); }
public Object readSecret() { return lookupCredentials(); }
}""",
lines=[[7, 7]],
)
result = prove_change(root, change)
assert len(result.findings) == 1 and result.coverage.operations_discharged == 0
def test_generated_output_and_parse_failure_counted(tmp_path):
root, change = repository(
tmp_path,
"class X { public Object broken( { return lookupCredentials(); }",
{"dist/bundle.java": "class X { public void doTest() {} }"},
)
change["files"].append({"path": "dist/bundle.java", "head_lines": [[1, 1]]})
result = prove_change(root, change)
assert not result.findings and result.coverage.unchecked_by_reason["parse_failed"] >= 1
assert result.coverage.unchecked_by_reason["excluded_dir"] == 1
@pytest.mark.parametrize("exits,expected", [(True, 0), (False, 1)])
def test_caller_permission_predicate_requires_exit(tmp_path, exits, expected):
root, change = repository(
tmp_path,
f"""class X extends Descriptor<Thing> {{
public Object doTestConnection() {{
boolean allowed = Jenkins.get().hasPermission(Jenkins.ADMINISTER);
{"if (!allowed) {" if exits else ""}
{"return null;" if exits else ""}
{"}" if exits else ""}
return Helper.readSecret();
}}
}}""",
{"Helper.java": "class Helper { public static Object readSecret() { return lookupCredentials(); } }"},
)
result = prove_change(root, change)
assert len(result.findings) == expected and result.coverage.reached == 1
assert result.coverage.operations_discharged == 1 - expected
def test_declared_public_route_suppresses_premise(tmp_path):
root, change = repository(
tmp_path,
"""class X extends Descriptor<Thing> {
public Object doTestConnection() { return lookupCredentials(); }
}""",
{".openultrasast/obligations.toml": 'version = 1\n[[route]]\npath = "doTestConnection"\naccess = "public"\n'},
)
result = prove_change(root, change)
assert not result.findings and result.coverage.operations_checked == 1
assert result.coverage.operations_discharged == 0
def test_nonpublic_unreached_function_in_partly_covered_repo(tmp_path):
root, change = repository(
tmp_path,
"class Plain { public Object run() { return lookupCredentials(); } }",
{"Exposed.java": "class X extends Descriptor<Thing> { public void doTest() {} }"},
)
result = prove_change(root, change)
assert result.coverage.frameworks_recognised == {"stapler": 1}
assert result.coverage.unchecked_by_reason["framework_unrecognised"] == 1
assert not result.findings
+69
View File
@@ -0,0 +1,69 @@
"""Java framework entries are tied to their declaring class and public method."""
import pytest
from openultrasast.mapping import analyze_entry_points
from openultrasast.preprocess import build_file_target
def entries(tmp_path, text):
path = tmp_path / "Fixture.java"
path.write_text(text)
return [e for e in analyze_entry_points(tmp_path, [build_file_target(tmp_path, path)]) if e.function_name]
def test_stapler_class_scope_visibility_and_ranges(tmp_path):
rows = entries(
tmp_path,
"""class Exposed extends Descriptor<Thing> {
public String doTestConnection() {
return lookupCredentials();
}
private void doHidden() {}
public void ordinary() {}
}
class Plain {
public void doUnexposed() {}
public void doCheck(@QueryParameter String value) {}
}
""",
)
assert [(e.function_name, e.line, e.end_line) for e in rows] == [("doTestConnection", 2, 4), ("doCheck", 10, 10)]
assert all(e.access_level == "unknown" and e.trust_boundary == "http_request" and e.access_evidence for e in rows)
@pytest.mark.parametrize("base", ["Action", "RootAction", "Plugin", "AbstractDescribableImpl<Thing>", "Descriptor<Thing>"])
def test_exposed_types(tmp_path, base):
assert len(entries(tmp_path, f"class X extends {base} {{ public void doCheckX() {{}} }}")) == 1
def test_unprotected_root_and_stapler_parameters(tmp_path):
rows = entries(
tmp_path,
"""class Open implements UnprotectedRootAction {
public void doTest() {}
}
class Plain {
public void doCheck(StaplerRequest2 request, StaplerResponse response) {}
public void doLowercase() {}
}""",
)
assert [e.access_level for e in rows] == ["public", "unknown"]
assert rows[0].access_declared
@pytest.mark.parametrize(
"text,name",
[
("class X extends AbstractHandler { public void handle(String target) {} }", "handle"),
("class X extends HttpServlet { protected void doGet(Request r) {} }", "doGet"),
('@Path("/api") class X { @GET public String read() { return "ok"; } }', "read"),
],
)
def test_jetty_servlet_and_jaxrs(tmp_path, text, name):
(row,) = entries(tmp_path, text)
assert row.function_name == name and row.kind == "route" and row.access_evidence
def test_unrecognised_and_comments(tmp_path):
assert entries(tmp_path, "/* class X extends Descriptor { public void doFake() {} } */\nclass Plain { public void doCheck() {} }") == []
+31
View File
@@ -206,3 +206,34 @@ def test_a_malformed_store_row_is_a_degradation_not_a_crash() -> None:
(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)
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 == ()
+24
View File
@@ -91,3 +91,27 @@ def test_java_php_facts_resolve_operations_and_dischargers(language, text, opera
assert ir.parse_ok
assert operation in {op.fact_id for op in find_operations(ir, facts, text=text)}
assert discharger in {d.fact_id for d in find_discharges(ir, facts, load_facts().for_language(language), text=text)}
def test_delivery_a_java_operations_and_dischargers_resolve():
from openultrasast.semantic.obligations.facts import load_obligation_facts
facts = load_obligation_facts().for_language("java")
operations = {call: fact for fact in facts.operations for call in fact.calls}
dischargers = {call: fact for fact in facts.dischargers for call in fact.calls}
for call in (
"lookupCredentials",
"lookupCredentialsInItem",
"getCredentialsFromId",
"testConnection",
"verifyCredentials",
"listPropertiesOfSecrets",
"openConnection",
"connect",
"publish",
"getItemByFullName",
"updateByXml",
):
assert "path_guard" in operations[call].requires
assert dischargers["checkPermission"].kind == dischargers["checkAnyPermission"].kind == "path_guard"
assert dischargers["hasPermission"].kind == "ownership_check"
+65
View File
@@ -0,0 +1,65 @@
"""Bounded paths preserve file identity and call-site lines."""
import pytest
from openultrasast.semantic.obligations.paths import PathRecord, compose_paths
@pytest.mark.parametrize("depth,reached", [(0, False), (1, False), (2, True), (3, True)])
def test_depth_and_call_sites(depth, reached):
graph = {"c.java::target": [("b.java::helper", 8)], "b.java::helper": [("a.java::doTest", 4)]}
paths, unreached = compose_paths(
callers=lambda p, f: graph.get(f"{p}::{f}", []), entries={"a.java::doTest"}, targets=[("c.java", "target")], max_depth=depth
)
assert bool(paths) == reached
assert unreached == ([] if reached else ["c.java::target"])
if reached:
assert paths == [PathRecord((("a.java::doTest", 4), ("b.java::helper", 8), ("c.java::target", None)), "a.java::doTest", True)]
def test_entry_dedupe_cycles_fan_out_and_file_identity():
graph = {"c.java::save": [("b.java::helper", 8), ("a.java::doTest", 4), ("a.java::doTest", 4)], "b.java::helper": [("c.java::save", 5)]}
kwargs = dict(
callers=lambda p, f: graph.get(f"{p}::{f}", []),
entries={"a.java::doTest"},
targets=[("c.java", "save"), ("other.java", "save"), ("a.java", "doTest"), ("c.java", "save")],
)
paths, unreached = compose_paths(**kwargs)
assert len(paths) == 2 and unreached == ["other.java::save"]
assert any(p.hops == (("a.java::doTest", None),) for p in paths)
assert compose_paths(**kwargs, fan_out=1)[1] == ["c.java::save", "other.java::save"]
@pytest.mark.parametrize("key,value", [("max_depth", -1), ("fan_out", 0)])
def test_invalid_bounds(key, value):
with pytest.raises(ValueError):
compose_paths(callers=lambda p, f: [], entries=set(), targets=[], **{key: value})
def test_records_are_consumed_by_the_checker():
from types import SimpleNamespace
from openultrasast.semantic.facts import load_facts
from openultrasast.semantic.ir import parse_file
from openultrasast.semantic.obligations.check import check_obligations
from openultrasast.semantic.obligations.dominance import OrderDominance
from openultrasast.semantic.obligations.facts import load_obligation_facts
text = "def entry(title):\n return Book.query.filter_by(book_title=title).first()\n"
paths, _ = compose_paths(callers=lambda p, f: [], entries={"app.py::entry"}, targets=[("app.py", "entry")])
result = check_obligations(
irs={"app.py": (parse_file("app.py", text, "python"), text)},
entries=[
SimpleNamespace(path="app.py", function_name="entry", access_level="unknown", kind="route", access_evidence=["fixture entry"])
],
facts=load_obligation_facts(),
flow_facts=load_facts(),
policy=None,
paths=paths,
dominance=OrderDominance({"app.py": text}),
store_shapes=(),
min_siblings=3,
assume_entries_protected=True,
)
(finding,) = result.findings
assert finding.operation.function == "entry" and finding.label == "unguarded_path"
+137
View File
@@ -0,0 +1,137 @@
"""Offline replay, resume, scoring and instrument refusal for the prover arm."""
import hashlib
import json
import socket
import subprocess
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from test_hunt_prover import repository # noqa: E402
from benchmarks.unseen import score # noqa: E402
from benchmarks.valid import prover_arm # noqa: E402
@pytest.fixture
def pool(tmp_path, monkeypatch):
def forbidden(*args, **kwargs):
pytest.fail("network access attempted")
monkeypatch.setattr(socket.socket, "connect", forbidden)
root = tmp_path / "repo"
root.mkdir()
text = """class X extends Descriptor<Thing> {
public Object doTestConnection() {
return lookupCredentials();
}
}"""
root, positive = repository(root, text)
positive.update(
id="positive",
kind="vulnerability",
family="access_control",
path="Fixture.java",
function="doTestConnection",
function_lines=[2, 4],
targets=[{"path": "Fixture.java", "function": "doTestConnection", "function_lines": [2, 4], "family": "access_control"}],
)
(root / "Fixture.java").write_text(text.replace(" return", " Jenkins.get().checkPermission(Jenkins.ADMINISTER);\n return"))
subprocess.run(["git", "-C", str(root), "add", "Fixture.java"], check=True)
subprocess.run(["git", "-C", str(root), "-c", "core.hooksPath=/dev/null", "commit", "-qm", "safe"], check=True)
head = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], check=True, capture_output=True, text=True).stdout.strip()
ordinary = {
**positive,
"id": "ordinary",
"kind": "ordinary",
"base": positive["head"],
"head": head,
"targets": [],
"files": [{"path": "Fixture.java", "head_lines": [[3, 3]]}],
}
changes = [positive, ordinary]
manifest = tmp_path / "frozen.json"
manifest.write_text(
json.dumps({"changes": changes, "sha256": hashlib.sha256(json.dumps(changes, sort_keys=True).encode()).hexdigest()})
)
repos = tmp_path / "repos.json"
repos.write_text(json.dumps({"fixture/app": str(root)}))
return root, manifest, repos, tmp_path / "out"
def test_run_resume_summary_and_locations(pool, monkeypatch, capsys):
root, manifest, repos, out = pool
assert prover_arm.main(["run", str(manifest), "--repos", str(repos), "--out", str(out)]) == 0
rows = score.load_records(out)
assert len(rows) == 2 and all(r["done"] and r["seconds"] > 0 for r in rows)
assert score.catches(rows[0]) == ["alerts"] and not score.false_alarm(rows[1])
summary = json.loads((out / "summary.json").read_text())
assert summary["recall"]["count"] == summary["recall"]["n"] == 1
assert summary["location_recall"]["count"] == summary["location_recall"]["n"] == 1
assert summary["false_alarms_per_100_pushes"]["rate"] == 0
assert summary["precision"]["rate"] == 1
assert summary["instrument_failures"] == 0
assert "fixture/app" in summary["per_repository"] and "stapler" in summary["per_framework"]
assert summary["changes_over_120_seconds"] == [] and summary["uncovered_repositories"] == []
assert summary["seconds_per_change"]["n"] == 2
assert "bytes read" in capsys.readouterr().out
old = {p.name: p.read_bytes() for p in out.glob("*.json") if p.name not in {"inputs.json", "summary.json"}}
def forbidden(*args, **kwargs):
pytest.fail("completed change re-run")
monkeypatch.setattr(prover_arm, "prove_change", forbidden)
assert prover_arm.run(manifest, repos, out) == 0
assert old == {p.name: p.read_bytes() for p in out.glob("*.json") if p.name not in {"inputs.json", "summary.json"}}
assert (
len(
subprocess.run(
["git", "-C", str(root), "worktree", "list", "--porcelain"], check=True, capture_output=True, text=True
).stdout.split("worktree ")
)
== 2
)
def test_zero_byte_record_refuses(pool):
_, manifest, repos, out = pool
assert prover_arm.run(manifest, repos, out) == 0
path = out / (hashlib.sha256(b"positive").hexdigest() + ".json")
record = json.loads(path.read_text())
record["instrument"]["changed_bytes"]["Fixture.java"] = 0
path.write_text(json.dumps(record))
with pytest.raises(ValueError, match="zero-byte"):
prover_arm.summarize(out)
def test_checkout_error_is_type_only(pool):
_, manifest, repos, out = pool
repos.write_text("{}")
assert prover_arm.run(manifest, repos, out) == 2
rows = score.load_records(out)
assert all(row["status"] == "instrument_failure" and row["instrument"]["failure"] == "KeyError" for row in rows)
def test_resume_rejects_new_depth(pool):
_, manifest, repos, out = pool
assert prover_arm.run(manifest, repos, out) == 0
with pytest.raises(ValueError, match="inputs"):
prover_arm.run(manifest, repos, out, max_depth=2)
def test_unchanged_file_empty_read_refuses(pool, monkeypatch):
_, manifest, repos, out = pool
real = prover_arm.Context.read
def empty(ctx, path):
if path == "Fixture.java":
return ""
return real(ctx, path)
monkeypatch.setattr(prover_arm.Context, "read", empty)
assert prover_arm.run(manifest, repos, out) == 2
with pytest.raises(ValueError, match="zero-byte"):
prover_arm.summarize(out)