mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 20:49:28 +00:00
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
89 lines
10 KiB
Markdown
89 lines
10 KiB
Markdown
# 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`)
|