A deterministic check that a protected operation is reachable from an entry point without a check dominating the path, per framework, built on the existing obligations checker, caller index and entry mapper; scored as a rule arm on the access-control training pool. Follows the maintainer's finding that the hunter's features describe shape and cannot decide unsafety. Co-Authored-By: Claude Fable 5.1 <[email protected]>
13 KiB
Requirements Document
Project Description (Input)
An access-control prover: a deterministic check that an operation needing authorization is reachable from an entry point without a check dominating the path, modelled per framework, built from the existing obligation facts, caller index and entry-point mapper, scored as a rule on the access-control training pool (recall on 23 fix changes, false alarms per 100 ordinary pushes) and feeding the proof arm.
Introduction
The maintainer's finding of 2026-10-14: the hunter's features (diff shape, function size, callers, entry-point counts, name roles, textual obligation order) describe the shape of code around a change and cannot decide that code is unsafe. Unsafety for access control is a property: an operation that needs authorization is reachable by a caller that no check has constrained. The access-control training pool (23 fix changes, 93 verified locations, 460 ordinary pushes, 23 application repositories) holds exactly that shape: a permission check added before a protected operation in a web-exposed method. Seventeen of the 23 fixes are Jenkins plugin web methods.
This spec delivers the prover for that one family, as a deterministic rule with no model call, measured on the
pool with the standard metrics, and emitting findings that carry the unguarded path so the proof arm can act on
them. It is the increment that follows the annotation ablation in hunter-learning-loop.
Boundary Context
- In scope: entry-point recognition for the pool's frameworks; reachability paths from entry points to touched functions through the existing caller index; framework enforcement points that discharge an obligation outside the function (route middleware, controller attributes, declared public routes); the verdict per change; a runner that scores the verdict as an arm on the pool; a coverage statement per repository.
- Out of scope: taint flow, the Joern engine, any model call inside the prover, new families beyond access
control, a project-owned declared policy file (exists in
authorization-obligationsand is consumed as is), the unseen pilot measurement (G1 exit, run by the maintainer after this arm exists), the pre-push hook wiring (pre-push-safety-net). - Adjacent expectations:
authorization-obligationsowns the facts schema, the checker and the dominance protocol; this spec extends facts and supplies paths and witnesses through the checker's existing parameters.hunter-learning-loopowns the feature table and the hunter arms; the prover is scored beside them with the same metric functions.search-with-proofconsumes the finding's path and operation to build a demonstration.
Requirements
Requirement 1: Entry points of the pool's frameworks
Objective: As a maintainer, I want the entry-point mapper to name the web-exposed methods of the frameworks the pool uses, so that reachability starts where an attacker starts.
Acceptance Criteria
- When a Java file declares a public method whose name starts with
doin a class that extends or implements a Jenkins or Stapler exposed type (Descriptor, Action, RootAction, Plugin, UnprotectedRootAction, AbstractDescribableImpl, or a parameter typed StaplerRequest, StaplerResponse or annotated QueryParameter), the mapper shall record an entry point of kindroutewith trust boundaryhttp_requestand the method's line range, with accesspublicwhen the class implements UnprotectedRootAction andunknownotherwise. - When a PHP file declares a Symfony controller action (a method carrying a
Routeattribute or annotation, or a class extending AbstractController with public methods named by a routes file), the mapper shall record it as arouteentry with the attribute's path as evidence. - When a PHP Livewire component or Laravel controller method is registered by a route file, a
Route::call, awire:binding or a public method on a class extending Livewire Component, the mapper shall record it as arouteentry, and shall carry a declared access ofpubliconly when the route file names no middleware. - When a TypeScript or JavaScript file under a Remix or React Router routes directory exports
actionorloader, the mapper shall record each export as arouteentry with trust boundaryhttp_request. - When a Python class extends a Tornado RequestHandler or WebSocketHandler, the mapper shall record its
get,post,put,delete,patchandopenmethods asrouteentries. - When a Java class extends a Servlet, a Jetty handler or a JAX-RS resource, the mapper shall record the
request methods as
routeentries (existing Servlet and JAX-RS rows are kept; Jetty handlers are added). - The mapper shall record every recogniser's evidence strings so a finding can name why a method is an entry.
- If a file's framework is not recognised, then the mapper shall record nothing for it and the coverage statement (Requirement 7) shall count the file as unchecked; silence is never a pass.
Requirement 2: Enforcement points outside the function
Objective: As a maintainer, I want checks that frameworks apply outside the handler to count as discharges, so that a guarded route is not reported because its handler body shows no check.
Acceptance Criteria
- When a Laravel route registration attaches
auth,can:,roleorpermissionmiddleware, or a controller callsauthorizeResourceormiddlewarein its constructor, the prover shall supply a discharge witness of scoperouterfor every handler the registration covers. - When a Symfony action carries
IsGranted,Securityor adenyAccessUnlessGrantedcall, or security.yaml'saccess_controlcovers its path with a role, the prover shall supply adecoratororrouterwitness. - When a Jenkins web method, or a method it calls on its own path, calls
checkPermission,checkAnyPermissionor guards onhasPermissionwith an exit, the existing statement witnesses shall apply;RequirePOST,POSTandInterceptorannotations shall not count as authorization. - When a Remix action or loader calls a session requirement (
requireUser,requireUserId,requireAuthentication,authenticateApiRequest,authorize) before the operation, the prover shall treat that call as a path guard, and when the operation reads a record by an identifier from the request without an ownership constraint bound to the authenticated principal the prover shall report the missingownership_check(the pool's CWE-639 case). - When a Tornado handler is decorated with
authenticatedortornado.web.authenticated, or the class overridesget_current_userand checks it before the operation, the prover shall treat that as a path guard. - Where a declared policy marks a route public, the prover shall suppress
path_guardfindings for it and keepownership_checkfindings, as the checker already does. - The prover shall write every enforcement fact as closed data in the obligation facts files, with the header naming the clones and references checked, and shall never add a name that does not occur in the checked code or the framework's documentation.
Requirement 3: Reachability paths through the caller index
Objective: As a maintainer, I want a path from an entry point to each touched function, so that an obligation inside a helper is judged by the checks on the way to it.
Acceptance Criteria
- When a change is checked out at head, the prover shall build, for every touched function and every function that holds a protected operation it calls, the caller chains that end at an entry point, through the per-change caller index, up to a configurable depth with default 3 and a configurable fan-out with default 25 callers per step.
- The prover shall emit each chain as a path record whose hops are
path::functionstrings from the entry to the operation's function, in the shapecheck_obligationsandOrderDominancealready consume. - If a touched function is itself an entry point, then the prover shall emit the one-hop path.
- If no chain reaches an entry point within the depth, then the prover shall record the function as
unreachedin the coverage statement and shall produce no finding for it. - The prover shall deduplicate chains, shall stop at the first entry point on a chain, and shall bound the work so a change of the pool's largest repositories finishes within the budget of Requirement 6.
- When two functions share a name in different files, the prover shall keep the chains separate by file.
Requirement 4: The verdict
Objective: As a developer about to push, I want a finding only when the tool can show an unguarded path to a protected operation, so that every alarm names what to add and where.
Acceptance Criteria
- When a protected operation is reached by a path record and no witness of a required discharger kind
dominates it along that path or inside its function, the prover shall emit one finding per operation and
missing kind with family
access_control, the operation's path and line, the entry point, the hops, the missing kind and the evidence strings. - When every required discharger kind is dominated, the prover shall emit no finding and shall count the
operation as
dischargedin the coverage statement. - The prover shall never emit a finding for a function whose file failed to parse, whose language has no obligation facts, or whose framework is unrecognised; each such case shall be counted under its reason.
- The prover shall make no model call.
- The prover shall report findings in the hunt defect shape (
path,line,family,evidence,function), so the existing scoring, the proof arm and the pre-push admission consume them unchanged. - When a touched function holds no protected operation and reaches none, the prover shall emit nothing for it.
Requirement 5: Scoring as a rule arm on the pool
Objective: As a maintainer, I want the prover scored on the training pool with the standard metrics, so that its number stands beside the hunter arms and decides whether it becomes a G1 arm.
Acceptance Criteria
- The runner shall run the prover over every change of a pool slice on the training clones, one checkout per
change, and shall write one record per change in the replay record shape that
benchmarks.valid.metricsscores (id, kind, repo, status, findings, bytes read, seconds). - The runner shall report recall on fix changes and on verified locations with Wilson intervals, false alarms per 100 ordinary pushes, precision, instrument failures, and seconds per change, and shall refuse to summarise when any record read zero bytes of a tree git holds.
- The runner shall report a per-framework and per-repository coverage table: entry points found, touched functions reached, operations checked, functions unchecked by reason.
- The runner shall checkpoint records per change and resume by skipping finished ones.
- The record of a run shall carry counts, digests and framework names, never repository names of reserved populations, as the reserved-repository guard requires.
Requirement 6: Budget
Objective: As a developer, I want the prover to finish inside the pre-push window, so that it can run on every push.
Acceptance Criteria
- The prover shall finish a change of the pool within 120 seconds on the development host, measured by the runner, and the runner shall list every change over that bound.
- The prover shall reuse the per-change context caches (product files, texts, call index, file IR, entry points) and shall not walk or read the product tree more than once per change.
Requirement 7: Coverage statement
Objective: As a developer, I want the tool to say what it did not check, so that no finding count reads as a clean bill.
Acceptance Criteria
- The prover shall return, with its findings, the counts of files by language, files whose framework was
recognised, entry points found, touched functions reached and unreached, operations checked and discharged,
and functions unchecked by reason (
parse_failed,language_unsupported,framework_unrecognised,unreached,no_facts). - When the coverage statement shows zero recognised frameworks for a repository, the runner shall mark the
repository
uncoveredin its summary, and recall for that repository shall be reported separately from the covered recall.
Requirement 8: Proof-arm feed
Objective: As the proof arm, I want each finding to carry what a demonstration needs, so that a finding can be proved or dismissed without re-deriving it.
Acceptance Criteria
- Each finding shall carry the entry point record (kind, path, line, evidence), the hop list, the operation (id, kind, calls matched, line) and the missing discharger kind.
- The finding id shall be
obligation:<operation kind>:<path>:<line>:<missing>as the checker already emits.