Files
Open_Ultra_SAST/.kiro/specs/authorization-obligations/research.md
T
Marius CiepluchandClaude Fable 5.1 27626d3a07 docs(authorization-obligations): approve requirements; design and research
Requirements approved by the maintainer in session. Design: closed
obligation facts (operations and dischargers), obligation shapes as a
second family in the mechanism store, sibling-set consistency and an
optional declared policy as the policy sources, a Dominance protocol
with an order-based default so the checker works function-local before
reachability and guard dominance land, findings at suspicion carrying
consistency_violation / declared_policy_violation / function_local as
data, reports, manifest and ranking fields, handler_context harvest
mode, leave-one-out per obligation kind, and an obligations lever with
pending policy clauses under the existing gate. All 44 acceptance
criteria traced.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-05 16:42:58 +02:00

89 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Research & Design Decisions
## Summary
- **Feature**: `authorization-obligations`
- **Discovery Scope**: Extension of the semantic layer (facts, shapes, store, MAP proposers), the entry-point mapper, reports, the pair corpus, leave-one-out and the improve lever; complex integration because it consumes two unimplemented upstream specs.
- **Key Findings**:
- The absence pairs are real and structured. `vampi-books-get-by-title` has identical code on both sides except a constraint `user=user` bound from `token_validator(request.headers.get('Authorization'))['sub']`; the vulnerable branch queries `Book` by title alone. The difference is the provenance of a constraint, not a flow. 15 of 29 `agent-vfc` rows and 6 of the 19 non-teaching `vibe-py` pairs carry absence labels (`missing_auth_guard`, `permissive_default`, `identity_from_request_body`, `secret_in_client`, `validation_strength`).
- The entry-point mapper already classifies every route as `public`, `authenticated` or `role-restricted` from its decorators (`mapping._python_route_access`), records the decorators as `access_evidence`, and lists `conditions` (`if` tests that look like checks). That is the sibling detector's upstream for path guards and needs no change.
- `FileIR` binds carry `value_text`, `names` and `call_name`; `variants.source_kind_of_name` already traces a name through binds to a fact source, a parameter or a container read. Provenance of a constraint's value (authenticated context versus request input versus constant) is the same trace with one more source class.
- `MechanismStore` rows carry a free `shape` dict and deterministic ids from `Shape.key()`; a second shape family fits without schema change. `MechanismEdit`, `apply_mechanism_edits`, `StoreSnapshot`, the tombstone and `evaluate_mechanism_profiles` are reusable as-is for a second lever family.
- `reachability-flow-model` (unapproved) defines `PathRecord(entry, hops, sink, sink_line, sink_node, incomplete_at, ...)` and `compose(graph, summaries, entry_nodes, max_depth)`. Obligations need paths from an entry point to an *operation*, which is a sink-shaped target; the same `compose` works when obligated operations are passed as the sink set. `guard-dominance-regime` has no spec directory yet; only the roadmap line exists. Dominance must therefore be consumed through a narrow interface this design defines and the two upstream specs later satisfy.
- The evidence ladder is closed: `OverlayRecord` allows `static_corroboration` and `suspicion`; proof rungs are forbidden outside the sandbox. Obligation findings are `StaticFinding`s at `suspicion` with the obligation label carried as data, so no ladder change is needed and Req 6.1 holds by construction.
- The pair scorer counts promote and coverage-with-source records as detections; an obligation finding has no source or sink. Detection for obligation-labeled rows must be an additive rule keyed on the obligation label (allowed by the corpus spec's "one additive label field").
## Research Log
### What the absence pairs actually differ by
- **Context**: Decide whether absence bugs can be a shape family at all.
- **Sources Consulted**: `benchmarks/pairs/vibe-py` excerpts for `missing_auth_guard` and `permissive_default` rows; `benchmarks/pairs/agent-vfc/recipes.toml` mechanisms; `mechanisms.toml` definitions.
- **Findings**: three recurring differences: (a) a constraint added to a data access whose value comes from the authenticated context; (b) a guard call or decorator added before the operation (early return on a failed token check, `login_required`); (c) a literal changed from permissive to restrictive (`CORS(app)` → origins list, `DEBUG=True` → `False`, RLS enabled). A fourth, rarer: identity read from the body replaced by identity from the token (same as (a) seen from the other side).
- **Implications**: discharger kinds `identity_constraint`, `path_guard`, `ownership_check`, `non_permissive_value`, `validated_input`; the value provenance is the discriminating feature for (a) and (d).
### Where the policy comes from
- **Context**: Absence is relative to intent; the tool must not invent intent.
- **Sources Consulted**: `mapping.analyze_entry_points` (routes, access level, evidence), Real-Vuln repositories (many handlers per resource), Clearwing's sibling-comparison practice.
- **Findings**: in every Real-Vuln web app the handlers on one resource share a router and a model; the vulnerable handler is the odd one out. Route access classification already exists. A declared policy is the only way to say "this route is meant to be public"; the hunter can only guess it.
- **Implications**: consistency first (no declaration needed, measurable now), declared policy second (stronger rung), hunter last (records intent, never raises).
### Dominance before guard-dominance-regime exists
- **Context**: Req 5 needs "a discharger dominates the path"; the spec that owns dominance is not written.
- **Findings**: within one handler, the existing `FunctionIR` order of binds and calls gives statement order; a guard call followed by an early return before the operation line is dominance in the common case; decorators dominate the whole handler; middleware attached to the router dominates every route under it. Across calls, `PathRecord.hops` gives the sequence of nodes; a discharger on any hop before the operation node dominates the path when the hop's function returns early on failure.
- **Implications**: define `Dominance` as a narrow protocol (`dominates(witness, operation) -> bool`) with a built-in `OrderDominance` implementation (statement order in the handler, decorators, router middleware, hop order) that `guard-dominance-regime` may replace with nesting-aware dominance. The degraded mode (`function_local`) is `OrderDominance` without path records.
### Store and lever reuse
- **Context**: Avoid a second store and a second lever machinery.
- **Findings**: `Mechanism.shape` is a dict validated where used; `corpus_mechanism_id` hashes `shape.key()`; `validate_mechanism` checks guard and source kinds against `variants` constants. A second family needs its own closed kinds check and a `family` field in the shape dict; the tombstone, snapshot and profile clause need nothing.
- **Implications**: `ObligationShape.to_dict()` carries `family = "obligation"`; the lever validator dispatches on `family`; `search_tree` ignores obligation rows (they are not sink shapes) and the obligation checker ignores sink rows.
## Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|--------|-------------|-----------|---------------------|-------|
| Absence as a taint sink ("sink with a missing sanitizer") | Treat protected access as a sink needing an identity "sanitizer" | Reuses taint | Wrong semantics: the constraint is not a sanitizer of a flow; unconstrained access has no tainted argument | Rejected |
| Model-authored authorization rules | LLM writes per-project rules | Flexible | Violates no-model-authored-patterns; unverifiable | Rejected |
| Obligations with dischargers, policy from consistency plus declaration, path-aware | Closed facts, learned shapes, sibling anomaly, optional declared policy, dominance protocol | Zero-dep, measurable leave-one-out, honest rungs, grows with corpus | Coarse resource grouping; dominance approximated until the guard spec lands | **Selected** |
| Framework-specific authorization linters | Per-framework hard-coded checks | High precision | Regex growth, no learning, no policy | Rejected |
## Design Decisions
### Decision: Obligation findings are `suspicion` with a data label, not new ladder rungs
- **Context**: Req 6.1 names three labels; the ladder is closed and owned elsewhere.
- **Selected Approach**: `StaticFinding.evidence_level = "suspicion"`, tags `obligation:<kind>`, `discharger:<kind>`, `obligation_evidence:<label>`; reports render the label. Dispositions and `ALLOWED_EVIDENCE` unchanged.
- **Trade-offs**: SARIF consumers see `suspicion` plus properties; honest and non-invasive.
### Decision: Dominance is a protocol with an order-based default
- **Context**: `guard-dominance-regime` is unwritten; this spec cannot wait for it to be useful.
- **Selected Approach**: `Dominance` protocol; `OrderDominance` default (decorator, router middleware, statement order before the operation, hop order along a path). The guard spec replaces it without touching callers.
- **Trade-offs**: order-based dominance misses guards inside conditionals; recorded as the known coarseness and measured on the fixed twins.
### Decision: Policy is never written by the tool
- **Selected Approach**: `declared policy` is a hand-maintained file with a closed schema; the lever may only *propose* a clause, journaled as pending until a human accepts; scans never read pending clauses.
### Decision: Resource identity for sibling grouping
- **Selected Approach**: a handler's resource is the trailing segment of its route path with parameters stripped, joined with the operation's target token (model, table, collection name from the operation fact match); router or module is the second key. Coarse but deterministic and text-free.
## Risks & Mitigations
- Sibling grouping over-merges unrelated handlers → minimum sibling count, both keys required, siblings listed in the finding for review.
- Consistency leaks on legitimately public handlers → declared public routes suppress path-guard violations; leak rate on fixed twins reported per slice; the lever's holdout gate rejects shapes that leak.
- Order dominance misses conditional guards → `function_local` and `consistency_violation` labels stay below proof; guard spec replaces the default.
- Corpus excerpts lack registration context → new `handler_context` harvest mode; pointer pairs fetch whole files anyway.
- Two unimplemented upstream specs → this design degrades to function-local mode without path records, so tasks 1–6 are implementable before reachability lands; only path-aware evaluation waits.
## References
- `.kiro/specs/reachability-flow-model/design.md` (PathRecord, compose)
- `.kiro/specs/corpus-seeded-mechanisms/design.md` (store, lever, LOO)
- `src/openultrasast/mapping.py` (`EntryPointRecord`, `_python_route_access`)
- `src/openultrasast/semantic/variants.py` (`source_kind_of_name`, `classify_guard`)