24 KiB
Research & Design Decisions
Summary
- Feature:
access-control-prover - Discovery Scope: Extension of
authorization-obligations(checker, facts, dominance) and the hunter's per-change context; gap analysis against the code on 2026-10-14 (commit9feb8f1). - Key Findings:
- The checker already takes
pathsandentriesand already decides reached-ness and along-path dominance (semantic/obligations/check.py::_reached,dominance.py::_dominates_along); nothing supplies paths today, so every operation outside an entry handler is invisible to it. - The entry-point mapper (
mapping.py) recognises Flask, Django, Express, Laravel routes, WordPress hooks, Next and Remix route modules partially, and nothing for Java: no StaplerdoXxxweb method, no Servlet or Jetty handler, no JAX-RS resource. Seventeen of the pool's 23 fixes are Stapler web methods. - The hunter's
Context(hunt/producers.py) caches product files, texts, file IR, a call index and the repository's entry points per change;source.callers_ofgives one hop of callers from the index. A bounded upward walk from a function to an entry point is a loop over that, no new index. - Enforcement outside the function fits the existing
Dischargescopesrouteranddecorator, whichOrderDominance.dominatesaccepts without a line, through the checker'sextra_witnessesparameter. No schema change to the facts loader is needed for them; the facts files gain operation rows only. benchmarks/valid/metrics.pyscores replay-shaped records withbenchmarks.unseen.score; the hunter arm (benchmarks/valid/hunter_arm.py) writes that shape. A prover arm writing the same shape is scored by the same code, so its recall and false alarms per 100 stand beside the hunter arms.- The pool's fixes by shape: 17 Jenkins web methods (
doTestConnection,doFill...Items,doCheck...,doValidate...,doChangeSubmit,doNotifyCommit,doPython,persistValue,create) missingcheckPermission; one Jetty service without authentication (CWE-306); one Tornado terminal websocket open without authentication (CWE-306); one Laravel Livewirestorewithout authorization (CWE-285); one Symfony controller action (CWE-284); one Remix action deactivating a schedule by id without an ownership check (CWE-639); one Flarum JavaScript extension (CWE-863, out of reach for this prover: client-side permission gating, counted as uncovered).
- The checker already takes
Research Log
Which checker parameters exist and what they expect
- Context: avoid a second checker.
- Sources Consulted:
src/openultrasast/semantic/obligations/check.py(check_obligations,_reached,_relevant),dominance.py(OrderDominance,_dominates_along,_hop_index), the approved design ofauthorization-obligations(Dominance, Checker, EntryPoints sections). - Findings:
pathsis a sequence of objects with ahopsattribute; each hop is"path::function"or a(node, line)pair; a path reaches an operation when its last hop equalsf"{op.path}::{op.function}". With no paths, only operations inside entry handlers (entries) are reached. A witness of scopehopon an earlier hop dominates along the path. Witness scopesdecoratorandrouterdominate without a line. - Implications: the prover produces
PathRecord(hops=(...))objects ending at the operation's function and passes them aspaths; it produces router and decorator witnesses asextra_witnesses; it needs no change to the checker's logic. One gap: the checker buildsoperationsfrom the IRs it is given, so the prover passes the IR of every file on a path, not only the touched file.
Where entry points come from
- Sources Consulted:
src/openultrasast/mapping.py(analyze_entry_points,_entry, the per-framework recognisers around lines 260-930),EntryPointRecordfields. - Findings: recognisers are code, one block per framework, each calling
_entry(target, start, end, handler, name, kind, access, trust_boundary, evidence, conditions, ...). No Java recogniser exists.frameworks.tomlis a lexicon for priors and stratification, not for entry recognition. - Implications: add recognisers in
mapping.pyfollowing the existing block pattern (Stapler, Jetty, Symfony attributes, Livewire, Tornado, Remix exports where missing). Each names its evidence.
How a rule arm is scored
- Sources Consulted:
benchmarks/valid/metrics.py(summarize,scoring_records),benchmarks/unseen/score.py(catches,location_match,false_alarm,coverage),benchmarks/valid/hunter_arm.py(run, record writing),src/openultrasast/hunt/metric.py::search_outcome. - Findings: a record needs
id,kind,repo,family, the change'spath/function_lines(copied from the pool),findingswithpath,line,family, andcoveragefields the scorer reads; a push is caught when a finding of the change's family lands within the tolerance of a verified location. - Implications: the prover arm copies the hunter arm's record envelope and instrument fields (bytes read per
changed file, head verified), writes per-change files, and calls
benchmarks.valid.metrics.summarize.
Budget
- Findings: the second screen's feature table took 53 minutes for 483 changes with every producer, 7 seconds per change on average and 117 seconds for the slowest seatunnel change; the caller index is the dominant cost and is already cached per change. A depth-3 upward walk adds a few index lookups per function.
- Implications: the 120-second bound of Requirement 6 is realistic; the runner measures it per change.
Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
| Extend the checker's inputs (chosen) | Supply paths, entries and witnesses to check_obligations from the hunter's context |
One checker, one dominance rule, facts stay closed data | Order dominance is textual inside a function; nesting-aware dominance (guard-dominance-regime) never existed |
Honest for "check present before operation"; the Jenkins shape |
| New prover module with its own graph | Build a call graph and CFG per change | Precise dominance | Weeks of work, duplicates the checker, no CFG for Java or PHP in the tree | Rejected for this increment |
| Joern engine | Dataflow and call graph from the engine | Precise | 225 s per batch, pilot produced no findings, no PHP in the budget | Rejected as the gate; usable later as a proof backend |
Design Decisions
Decision: paths from the caller index, not a call graph
- Context: Requirement 3.
- Alternatives Considered: a per-change call graph over all product files; the engine's call graph.
- Selected Approach: an upward walk from the function through
source.callers_ofon the cached index, bounded by depth and fan-out, stopping at the first entry point of each chain. - Rationale: the index exists, is cached per change, and the pool's shapes are shallow (the operation is in the web method or one helper below it).
- Trade-offs: name-based caller resolution conflates same-named functions across files; chains are kept per
file but a caller of
savein another class may be a false hop. Accepted and visible in the evidence. - Follow-up: measure how many positives are reached at depth 1, 2 and 3 on the pool.
Decision: framework enforcement as router and decorator witnesses
- Context: Requirement 2.
- Selected Approach: a small recogniser per framework reads route registrations and attributes and emits
Dischargewitnesses with scoperouterordecoratorfor the handlers they cover; passed throughextra_witnesses. - Rationale: the dominance rule already accepts those scopes; the facts files stay about calls.
- Trade-offs: coverage is as wide as the recognisers; an unrecognised middleware name means a finding on a guarded route. Every recogniser lists its names in the facts header and the coverage statement counts routes with unrecognised middleware.
Decision: an opt-in premise in the checker, not a synthesized policy
- Context: Codex's first attempt stopped on a real conflict: with path records and neither a declared clause nor a sibling norm the checker returns no finding by design (gating 3), so the prover's core verdict could not be emitted without a change.
- Alternatives Considered: synthesize a
DeclaredPolicyin memory marking every recognised entryauthenticated(dishonest: the finding would read "route declared authenticated" when nothing was declared); a second checker in the prover (duplicates the rules); an opt-in keyword and a distinct label (chosen). - Selected Approach:
assume_entries_protected=Falsekeyword, labelunguarded_path, evidence naming the premise. Default behaviour and tests unchanged. - Trade-offs: the label makes the premise visible in every finding and in the static tags; consumers that
grouped by label need the new value (revalidation trigger for
authorization-obligations).
Decision: no model call inside the prover
- Context: Requirement 4.4; the maintainer's direction after the engine pilot: the model's job is the trust boundary question the static side cannot answer, asked after a static result exists.
- Selected Approach: the prover is deterministic; the intent adjudication hook of the checker stays off in the arm; a later increment may wire it as a separate, measured arm.
Risks & Mitigations
- Jenkins web methods whose protected operation is an outbound connection test with stored secrets
(
doTestConnection) call no listed operation name — mitigate by adding operation rows for credential lookups and HTTP client calls used in those handlers, checked in the clones, and by counting the handlers the facts do not cover. - Textual order dominance inside a function misses a check in an
elsebranch — accepted for this increment; recorded as a degradation when a witness exists but does not dominate. - Same-name conflation in callers — chains kept per file; evidence names the hop files.
- Over-wide entry recognition (every
doXxx) raises false alarms on helper methods — the Stapler recogniser requires a public method on an exposed type or a Stapler-typed parameter; the pool's ordinary pushes measure the rate.
References
- Jenkins AccessControlled: https://javadoc.jenkins.io/hudson/security/AccessControlled.html
- Stapler web methods: https://www.jenkins.io/doc/developer/security/form-validation/ and https://www.jenkins.io/doc/developer/security/secrets-and-credentials/
- Laravel authorization and route middleware: https://laravel.com/docs/authorization, https://laravel.com/docs/middleware
- Symfony security: https://symfony.com/doc/current/security.html
- Remix route module API: https://remix.run/docs/en/main/route/action
- Tornado authentication: https://www.tornadoweb.org/en/stable/guide/security.html
Design amendment synthesis -- 2026-10-29
Context: gates 1-3 (measurements 2026-10-28-access-control-{prover,judge,ablation}) found one cross-cutting
lever -- interprocedural resolution -- behind both the 8 recall misses (B: operations never linked; C: coarse
guards discharge) and the kept false alarms (guards present but unmodeled, name-collision resolution, base-class
projection). Requirements amendment [2026-10-29] (AC 1.9, 2.9, 2.10, 3.7-3.10, 5.6, 7.3); this section
records the code survey behind the design amendment (same date tag).
Research Log: resolution seams (verified against current code)
- Caller index keys on bare function name only:
src/openultrasast/source.py:447callers_of(regex overCodeLineindexes);CallSite(number, name)atsource.py:22carries no receiver. The prover adapterhunt/prover.py:161-172re-qualifies hits tofile::enclosing_fnby IR span containment. - Tree-sitter IR drops receiver text:
semantic/ir.py:21-29CallSite(callee is trailing name only;semantic/cst.py:220_callee_namestrips$obj->method/x.y.z); receiver must be captured incst.py:203_calland added toir.CallSite(python-ast pathir.py:100+likewise). - Target enumeration is one-hop trailing-name join:
hunt/prover.py:174-185(call_namesfrom touched functions' IR calls; any(path,name)with that name and operations). Second receiver-blind seam. - Guards on hops:
Hop.guards_before_callpopulated only atsemantic/obligations/trace.py:224-231(serialization); the verdict path usessemantic/obligations/check.py:312-323_on_caller_hop, which today re-judges onlypath_guard/ownership_check(router/decorator => True; other kinds => False). Dominance order ruledominance.py:52-60; CFG deferralcfg_dominance.py:63-65. - Discharge has no resource identity:
semantic/obligations/operations.py:35-44(detailis identifier-only). The only resource token today isOperation.resource(operations.py:31,_resource_tokenat 229-243: receiver first segment, SQL table, orresource_arg). Suppression loopcheck.py:117-122; relevancecheck.py:326-327; scope matching is(path, function)equality (operations.py:124,dominance.py:38). - Enforcement witnesses:
semantic/obligations/enforcement.py:18covers Tornado decorators, SymfonyIsGranted/Security/access_control, Laravel middleware +authorizeResource; called per language athunt/prover.py:223-227. In-function recognizer_call_witnesses(operations.py:163-183) matches discharger rows by trailing name --denyAccessUnlessGranted/isGrantedalready match existing rows (ruleset/obligations/php.toml:68-78);voteOnAttributeneeds no row (interface method, trailing-name match). Jenkins guard rows exist (java.toml:68-76). - Mapping:
_jetty_entries(mapping.py:562) anchors at method declarations only; call-site registration precedent exists (_python_registered_routesmapping.py:945-1015, WordPress_PHP_REGISTRATIONmapping.py:657-680withregistered_byfield onEntryPointRecord,mapping.py:53-74); java dispatch atmapping.py:185-198; framework tag tableentry_frameworkmapping.py:472-488. - prover_arm refusal seams: per-file zero-byte loop
benchmarks/valid/prover_arm.py:86-113; stale-anchor validation belongs in_provebetween lines 114 and 125 (beforerecord.update(findings=...)), mirrored insummarize()(184-187); type-name-only catch at_prove129-133. - Checker boundary:
authorization-obligationsowns check.py/facts.py/operations.py/paths.py/dominance.py. The design's third extension (resource field +resource_boundkeyword + all-kind_on_caller_hop) touchesoperations.py(dataclass field) andcheck.py(keyword + relevance compare), default-off. Othercheck_obligationscallers (cli.py, hunt/producers.py, pairs.py) keep unchanged defaults.
Decisions
- New module
semantic/obligations/resolution.py+ additiveCallSite.receiver(captured incst.py) rather than widening the source.py index: the index is shared with hunt search; a per-change class table keeps resolution local, cacheable on the Context, and testable in isolation. Bare-name fallback +receiver_unresolvedcoverage counter keeps dynamic languages (TS/PHP dynamic receivers) working as today. - Resource binding as checker keyword (
resource_bound) with emission-side tokens rather than prover-side filtering: the suppression decision is per (witness, operation) pair insidecheck_obligations; filtering outside would need a restructured call. Default-off keepsauthorization-obligationstests untouched. - Programmatic registration follows the existing call-site registration pattern (python routes, WordPress)
with class-literal resolution through
resolution.class_table-- one owner for class resolution, reused by the mapping recogniser and the callers adapter.
Risks
- Dynamic receivers (TS
this.handler.get, PHP$container->get(...)) stay name-keyed; the counter makes the fallback visible instead of silent. If the pool shows largereceiver_unresolvedrates, the fact-proposal lane (deferred injection spec) is the designated place to grow type facts, not this spec. down_depthwidens target enumeration; the fan-out bound and the 120 s budget cap it. Per-repository seconds-per-change in the runner summary will show any cost.
Abstract judgment and admission amendment -- 2026-10-29
Context
The design review after the capability-level lift found that the proposed amendment still allowed two degraded instruments to participate in binary decisions: unresolved receivers fell back to a bare-name call key, and unavailable CFGs fell back to source order. It also found that the fact-initialization task used known development misses and then remeasured the same population, which is valid regression work but cannot qualify an admission arm.
Synthesis
- Generalization: call certainty, path completeness, resource compatibility, authority compatibility and
dominance are instances of one evidence-relation problem. They share a finite domain
(
MUST | MAY | NO | UNKNOWN) and one incompleteness record instead of separate booleans and counters. - Build vs adopt: the repository already owns the semantic IR, structural CFG, obligation checker and path records. A small abstract-domain layer and monotone function summaries extend those contracts; adopting another graph or abstract-interpretation engine would duplicate the current architecture and violate steering.
- Simplification: the prover keeps one checker and one concrete trace. Function summaries accelerate and stabilize interprocedural discovery, but final evidence remains a materialised path. No second verdict engine is introduced.
Decisions
Decision: three-way judgment
- Selected approach: every path and aggregate pair is
unguarded,discharged, orindeterminate. - Invariant: less precise evidence may preserve a verdict or move it to
indeterminate; it may not switch directly between the two final verdicts. - Algebra:
MUSTandNOare incomparable, both belowMAY, which is belowUNKNOWN. Alternative evidence uses the least-upper-bound table; dimensions of one witness use conjunction; witnesses for one obligation use existential aggregation. These operations are distinct soMUST + NOon alternative paths becomesMAY, while one definite governing witness remains sufficient for the obligation it covers. - Consequence: prover mode does not use textual order as a semantic fallback. Legacy checker callers retain it.
Decision: stable symbols and finite candidate sets
- Selected approach: identify repository declarations by module, enclosing type, method and available signature. Partial resolution returns a finite candidate set plus incompleteness; unbounded resolution returns unresolved.
- Rejected approach: global bare-name fallback, because it conflates unrelated methods and can both invent paths and borrow unrelated guards.
Decision: resource and authority are separate abstract values
- Selected approach: match witness kind, scope, resource and authority independently, joining each relation into the pair judgment. Request-global guards are valid only when the operation fact explicitly allows that scope.
- Rejected approach:
Noneas a resource wildcard. It cannot distinguish intentionally global authentication from missing resource evidence.
Decision: summaries discover; concrete paths prove
- Selected approach: monotone summaries over strongly connected call components collect operations, witnesses, call target sets and incompleteness. Concrete paths are materialised from exact edges for trace evidence.
- Trade-off: summary and path code add one engine boundary, but they remove recursion and depth handling from ad-hoc traversal and make every cutoff explicit.
Decision: development attribution before admission
- Selected approach: compare old/new analyzer crossed with old/new facts on the current development population, then freeze analyzer, facts, acquisition record, configuration and a separate untouched population for admission.
- Consequence: the 2026-10-28 numbers remain the immutable baseline. A better rerun on the same repositories is regression evidence, not a G1 admission claim.
Risks and mitigations
- Indeterminate inflation: strict uncertainty may initially lower apparent coverage. Report the stage funnel and reason distribution; improve the dominant reason rather than weakening the lattice.
- Summary cost: a repository-wide symbol graph may threaten the pre-push budget. Reuse Context caches, use finite summary sets, and fail the budget gate when any measured change exceeds 120 seconds.
- Authority vocabulary growth: permission implication can become target-specific code. Keep implications in promoted facts and include the fact identifier in every match.
- Admission scarcity: the current pool is already development data. Source and review a separate population
through
valid-detection-benchmark; do not relabel the current pool as untouched.
Admission outcome (2026-10-30)
The first admission run on the untouched pilot population returned NO-GO (0/12 pairs,
in-family 0/3; 9/12 repositories with zero framework recognition; 0 instrument failures;
determinism 4/4). See benchmarks/measurements/2026-10-30-access-control-admission/. The
next lever is entry-recognition and call-resolution breadth for the valid pool's
ecosystems, gated by the same admission workflow. The pilot pool remains unspent: no
acquisition observed its labels; only admission scoring read them. Standing rules:
steering/detection-quality.md.
Detection-core fact-authority ownership amendment (2026-11-01)
Context
Detection-core task 9.4 reached a real boundary gap while migrating the installed entry pack. The generic core already validates fact packs, structural matches, lineages, custody, and relation application, but no owner supplied the production access-control contract, reviewed target authority, structural matcher, or persisted match artifacts. Putting those objects into core would make a vulnerability-neutral package own access-control and target vocabulary.
The instrument was verified before diagnosis: the installed entry file was readable at 963 bytes, and the focused legacy/v2 fact suites passed. The blocker was therefore ownership and missing authority, not an unread input or broken validator.
Synthesis and decision
- Dependency direction:
access-control-proverimports detection-core contracts and supplies family-owned objects. Detection-core never imports access-control or target code. - Authority: a checked-in immutable bundle references the production family contract, one reviewed fact pack per language, recognition projection, policies, lineages, matcher identity, and canonical evidence. A separately configured bootstrap policy authenticates the bundle's reviewer and store; the bundle cannot trust itself.
- Evidence custody: the downstream adapter persists canonical structural observations and match bytes before calling core. Core receives only typed objects, immutable content references, and read-only custody/resolver ports.
- Compatibility: existing regex rows remain a byte-stable recognition projection during migration. They are explicitly non-authoritative; exact typed observations are required for complete fact evidence.
- Task routing: access-control-prover tasks 14.1–14.6 define the family contract, authenticate authority, migrate reviewed data, persist evidence, match structure, and publish the conformance handoff. Detection-core task 9.4 then runs the target-neutral handoff-acceptance gate. Access-control-prover task 14.7 subsequently migrates the production caller and proves zero bypasses before detection-core task 10.2 cuts over.
Risks and mitigations
- Self-authorized trust: keep bootstrap policy outside the bundle, re-derive every digest, require producer-reviewer separation, and enforce forbidden identity pairs.
- Pattern authority inflation: preserve recognition behavior separately and require the complete typed predicate set before core application.
- Circular match references: persist the completed canonical match first, then pass its reference to core; the reference is never part of match-digest construction.
- Admission contamination: bind fact and matcher lineages to current custody and the evaluation population so label access refuses the same run.