mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
docs(authorization-obligations): record review rounds 2 and 3 and the design amendments
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Opus 5
parent
e5103fc645
commit
7d0ca74bf3
@@ -131,7 +131,7 @@ src/openultrasast/
|
||||
│ ├── policy.py # DeclaredPolicy schema + load_declared_policy(path) (closed; rejects unknown fields); version hash
|
||||
│ ├── dominance.py # Dominance protocol; OrderDominance default (decorators, router middleware, statement order, hop order)
|
||||
│ └── check.py # check_obligations(...) -> ObligationResult(findings, discharges, sibling_sets, degradations); findings_to_static
|
||||
├── data/facts/obligations.toml # closed operation/discharger facts per language (python, javascript, typescript)
|
||||
├── ruleset/obligations/{python,javascript,typescript}.toml # closed operation/discharger facts, beside (not inside) the flow facts dir that load_facts globs
|
||||
├── cli.py # MAP: obligation check after variant search; manifest `obligations`; `mechanisms export` also writes obligation shapes
|
||||
├── config.py # [obligations] enabled=true, min_siblings=3, policy_path=".openultrasast/obligations.toml"
|
||||
├── reports.py # obligation lines in markdown, `obligation_*` SARIF properties
|
||||
@@ -358,7 +358,7 @@ class ObligationResult:
|
||||
degradations: tuple[dict[str, object], ...]
|
||||
|
||||
def check_obligations(*, irs, entries, facts, flow_facts, policy, paths, dominance, store_shapes, min_siblings) -> ObligationResult: ...
|
||||
def findings_to_static(result) -> list[StaticFinding]: ... # id "obligation:<kind>:<path>:<line>", evidence suspicion, tags
|
||||
def findings_to_static(result) -> list[StaticFinding]: ... # id "obligation:<kind>:<path>:<line>:<missing>", evidence suspicion, tags
|
||||
```
|
||||
|
||||
Rules: reached = a `PathRecord` ends at the operation, or (no paths) the operation's function is an entry point; label precedence declared > consistency > function_local; a declared public route suppresses `path_guard` obligations for that route; an operation with a matching store shape gains `known_fix`; a discharge that binds `request_input` where `authenticated_context` is required is reported with `provenance = "request_input"`.
|
||||
@@ -369,7 +369,7 @@ When a hunter client is available (the same resolution as `pairs --hunter`), the
|
||||
|
||||
### Reports, manifest, rank
|
||||
|
||||
Markdown per finding: `- Obligation: <kind> on <resource>`, `- Missing discharger: <kind> (<provenance>)`, `- Evidence: <label> — <siblings | clause>`, `- Known fix: <guard shape> learned from <pairs>`, `- Intent: <adjudication>`; a `## Obligations` section lists sibling sets and the policy version. SARIF properties `obligation_kind`, `obligation_missing`, `obligation_label`, `obligation_evidence`, `obligation_known_fix`, `obligation_intent`. Manifest `obligations = {sibling_sets, under_populated, operations, findings_by_label, policy_version | null, degradations}`. Rank: `ranking_priority = base(sensitivity) x weight(label)` with `declared > consistency > function_local`, capped strictly below the lowest sandbox-proven finding in the run.
|
||||
Markdown per finding: `- Obligation: <kind> on <resource>`, `- Missing discharger: <kind> (<provenance>)`, `- Evidence: <label> — <siblings | clause>`, `- Known fix: <guard shape> learned from <pairs>`, `- Intent: <adjudication>`; a `## Obligations` section lists sibling sets and the policy version. SARIF properties `obligation_kind`, `obligation_missing`, `obligation_label`, `obligation_evidence`, `obligation_known_fix`, `obligation_intent`. Manifest `obligations = {sibling_sets, under_populated, operations, findings_by_label, policy_version | null, degradations, sets}` (`sets` lists `{module, resource, handlers}` so the report can name them). Rank: `ranking_priority = base(sensitivity) x weight(label)` with `declared > consistency > function_local`, capped strictly below the lowest sandbox-proven finding in the run. Proof rungs live on `CandidateVerdict`, never on `StaticFinding`, so the cap takes the proven finding ids from the run's `TRIGGERABLE` verdicts: `rank_obligations(findings, proven_ids=...)` runs once at MAP and again after REGRESS when the sandbox proved anything.
|
||||
|
||||
### Corpus and leave-one-out
|
||||
|
||||
|
||||
@@ -117,5 +117,22 @@
|
||||
## Implementation Notes
|
||||
|
||||
- Requirements approved 2026-09-05, design approved 2026-09-05 (maintainer, in session).
|
||||
- Task 1.1 (2026-09-05, RED first: 3 failing tests in `tests/test_obligation_facts.py`, `ModuleNotFoundError: semantic.obligations`; then green): `semantic/obligations/facts.py` with closed `OPERATION_KINDS`, `DISCHARGER_KINDS`, `PROVENANCE_KINDS`, `OperationFact`/`DischargerFact`/`ObligationFacts.for_language`, `load_obligation_facts(dir)` rejecting unknown kinds and fields by name; facts in `ruleset/obligations/{python,javascript,typescript}.toml` (design deviation: a sibling directory of the flow facts rather than `data/facts/`, because `load_facts` globs every `*.toml` in its own directory). Data decision: protected reads and writes are discharged only by an identity constraint or an ownership check; a path guard proves the caller is logged in, not that this caller may touch this record, so `path_guard` discharges privileged actions only.
|
||||
- Task 1.2 (2026-09-05, RED first: 5 failing tests in `tests/test_obligation_shapes.py`; then green): `ObligationShape` (family `obligation`, text-free key with a family prefix), `derive_obligation`, `explain_skip`. Design refinements forced by the corpus: Real-Vuln twins are either the identical flag-toggled function or an unrelated trap function, so the discharger lesson is any *discharged* operation of the target's kind — fixed side first, then the vulnerable function's own safe branch (the vampi pair teaches from `filter_by(user=user, ...)` with `user` bound from `token_validator(...)['sub']`); `FileIR` call sites carry positional arguments only, so keyword constraints are read from the call's own source line (derivation only, nothing textual enters the shape); the CST walker names a chained `filter_by(...).update(x)` after its first segment, so when the labeled kind matches no operation any obligated operation in the labeled function stands in and the shape keeps the labeled kind; an identity constraint bound from request input or a constant discharges nothing (that is the `identity_from_request_body` bug itself).
|
||||
- Facts tightening while starting 2.1 (2026-09-05, after group 1 went to review): bare `get`/`find`/`filter`/`all` in the read facts matched `request.args.get(...)` and produced a phantom resource-less operation; the entries are receiver-qualified (`query.get`, `objects.get`, `query.filter`, `objects.all`) in all three languages. Group-1 tests unchanged and green; disclosed to the group-1 reviewer.
|
||||
- Review round 1 (2026-09-05) APPROVED 1.2; REJECTED 1.1 (no test for the discharger-kind rejection arm of Req 1.4) and 1.3 (the sink-shape fallback in `pair_lessons` was untested and the threatbyte note was wrong). Remediated RED-first: `test_loader_rejects_...` gains the discharger-kind arm; `test_absence_row_without_an_obligation_lesson_still_teaches_its_sink_shape` pins the fallback; the note above now states the measured behaviour. Suggestions applied: permissive values and request-source patterns are discharger-fact data (`permissive_values`, `request_sources`) instead of code-level lists; the skip reason names the dischargers the operation accepts (`no_discharger_added:identity_constraint|ownership_check`); a trap-twin fixture exercises the vulnerable-side lesson; the dead `source_kind_of_name` re-export is gone; `_skip_detail` passes the loaded flow facts; the package exports the shape API; design File Structure Plan amended to `ruleset/obligations/<language>.toml`.
|
||||
- Task 2.1 (2026-09-05, RED first: 3 failing tests in `tests/test_obligation_operations.py`, `ModuleNotFoundError: obligations.operations`; then green): `operations.py` with `Operation`, `Discharge`, `find_operations(ir, facts, text=)`, `find_discharges(ir, facts, flow_facts, text=, entries=)`, `covers`. Design deviations: both functions take the file text (decorators are not calls in `FileIR`, keyword constraints live on the call line); chained call sites (`x.filter_by(...).first()` recorded twice) are deduped per function, line and fact; `covers` is the kind-and-provenance fit only (ordering is the Dominance protocol's job). Resource token: the receiver's first segment (`Book` -> `book`), the fact's `resource_arg`, or a table name in a SQL literal; receivers like `self`, `db`, `cursor`, `request` name nothing.
|
||||
- Task 2.2 (2026-09-05, RED first: 3 failing tests in `tests/test_obligation_siblings.py`; then green): `siblings.py` groups entry-point handlers by (module path without suffix, resource token); `discharging` counts only valid witnesses (an identity constraint must bind the authenticated context); `consistency_anomalies(sets, min_siblings)` reports handlers lacking a kind at least `min_siblings` siblings practice, with the siblings and, for path guards, the route access classifications as evidence; sets below the minimum are returned as under-populated. Consistency is about practice, so a path-guard anomaly is raised even though a protected read's own obligation is identity or ownership (Req 3.4).
|
||||
- Task 2.3 (2026-09-05, RED first: 2 failing tests in `tests/test_obligation_policy.py`; then green): `policy.py` `load_declared_policy(path) -> DeclaredPolicy | None`, closed top-level, resource and route fields, `access` in public|authenticated|role, sensitivity in the closed set, `identity_source` restricted to the discharger facts' identity sources or `token`; unknown fields and values rejected by name; `version_hash` over the comment-stripped text; `route_access` matches exact paths or a trailing `*` glob.
|
||||
- Task 2.4 (2026-09-05, RED first: 3 failing tests in `tests/test_obligation_dominance.py`; then green): `dominance.py` runtime-checkable `Dominance` protocol and `OrderDominance(texts)`: decorator and router witnesses dominate the handler; an identity constraint or a non-permissive value dominates the operation it sits on; a guard call before the operation dominates by contract; another statement witness dominates only with an early exit (`return`, `raise`, `abort(`, `throw`, ...) between it and the operation; along a path record a witness on an earlier hop dominates.
|
||||
- Task 2.5 (2026-09-05, RED first: 4 failing tests in `tests/test_obligation_check.py`, `ModuleNotFoundError: obligations.check`; then green): `check.py` `check_obligations(irs, entries, facts, flow_facts, policy, paths, dominance, store_shapes, min_siblings, extra_witnesses=)` -> `ObligationResult`; reached = a path record's last hop is the operation's node, or (no paths) the operation's function is an entry point, with one `obligations_function_local` degradation; an operation is discharged when any of the dischargers it accepts dominates (OR semantics); the missing kind is the one the siblings practice when an anomaly names it, else the first accepted kind; consistency anomalies on kinds outside the operation's own requirement (a path guard) are reported too, except that a route declared public waives only the guard; label precedence: a declared clause covers the obligation it speaks about (a protected resource covers identity and ownership, a route declared authenticated or role covers the path guard) > consistency > `function_local`; `known_fix` from a matching store shape; the route path is read from the handler's route decorator literal; `findings_to_static` emits `obligation:<kind>:<path>:<line>:<missing>` at `suspicion` with tags `obligation:*`, `discharger:*`, `obligation_evidence:*`, `resource:*`, `mechanism:*`.
|
||||
- Task 3.1 (2026-09-05, RED first: 3 of 4 tests in `tests/test_obligation_scan.py` failing — no `obligation:` finding, no manifest block, sandbox test's finding absent; then green): `[obligations] enabled=true, min_siblings=3, policy_path=".openultrasast/obligations.toml"`; `_check_obligations` runs in MAP after variant search (standard/deep only) with `OrderDominance` over the parsed targets, the declared policy loaded from the target (a `policy_invalid` degradation continues the scan), obligation shapes from the target's mechanism store, and `paths=()` until reachability lands; findings are appended and ranked; the manifest carries `obligations = {sibling_sets, under_populated, operations, findings_by_label, policy_version}`; checker degradations join the run's; obligation findings never enter the sandbox candidate set (they are neither overlay promotions nor variant findings). Test note: the scan reads `openultrasast.toml` from the working directory, so the tests pass `--config <target>/openultrasast.toml`.
|
||||
- Task 3.2 (2026-09-05, RED first: 3 failing tests in `tests/test_obligation_reports.py` — writers rejected `obligations=`, `rank_obligations` missing; then green): the markdown writer prints `Obligation`, `Missing discharger` (with provenance), `Evidence` (label and siblings or clause), `Known fix` (guard learned from pairs) and `Intent` per finding plus a `## Obligations` section; SARIF gains `obligation_kind/resource/missing/provenance/label/evidence/known_fix/intent`; `rank.rank_obligations` (top-level `rank.py`, per the design's file plan) weighs sensitivity x label (declared 0.9 > consistency 0.6 > function_local 0.3) and caps strictly below the lowest sandbox-proven finding; other findings keep their priority; `_search_variants`-style, `_check_obligations` returns the per-finding mapping the writers render. `rank.py` imports `StaticFinding` under `TYPE_CHECKING` only (findings imports rank).
|
||||
- Task 3.3 (2026-09-05, RED first: 3 failing tests in `tests/test_obligation_intent.py`, `ModuleNotFoundError: obligations.intent`; then green): `intent.py` `adjudicate_intent(result, client=, model=, texts=)` asks the hunter one closed question per route-bearing finding (route, handler, operation, missing discharger, siblings; the handler's own lines only) and parses a JSON answer into `intent` public | protected | unknown with a one-sentence rationale; malformed or off-vocabulary answers are `unknown`; without a client or model one `intent_adjudication_unavailable` degradation is recorded (only when a route-bearing finding exists to adjudicate; a run without obligation findings records nothing) and findings are identical; labels, evidence and evidence level never change. `ObligationFinding.intent_rationale` added (additive, disclosed to the groups 1–2 reviewer). The CLI resolves the client through `tool_hunter.resolve_hunter_client()` and `[models].hunter`; the report mapping carries intent and rationale.
|
||||
- Task 1.3 (2026-09-05, RED first: 4 failing tests in `tests/test_obligation_export.py` — `obligation_mechanisms` missing, absence pairs skipped as `no_labeled_sink_call_site`; then green): `seed.pair_lessons` derives obligation shapes for rows whose mechanism is in the closed `ABSENCE_MECHANISMS` set (or carries the future `obligation` label) and falls back to a sink shape when an absence row teaches no obligation (a weak literal at a sink still teaches); skip reasons come from `explain_skip` (`no_obligated_operation`, `no_undischarged_operation`, `no_discharger_added`); `append_from_pair` accepts both shape families (`guard` carries the discharger kind); `variant_search.search_tree` ignores obligation rows; `obligation_mechanisms()` selects them. Measured on vendored vibe-py: `vampi-books-get-by-title` and `vampi-users-update-password` seed `protected_read`/`identity_constraint`/`authenticated_context`; the two threatbyte pairs teach no obligation (their twins are login handlers) and fall back to the sink shape they taught before (`execute`, guard `none`), so the flow store is unchanged; the three permissive-default pairs skip `no_obligated_operation` (no security-setting call in the facts matches `main`/`init`/`session_middleware`, a facts-growth item, not a code gap). Flow shapes unchanged (10 with the extra).
|
||||
- Review round 2 (2026-09-05) APPROVED 2.1, 2.3, 2.4; REJECTED 2.2 (sibling key was the file basename, so `api/v1/books.py` and `admin/legacy/books.py` merged; a `min_discharging=1` default let one sibling set the norm against Req 3.2; the tasks.md two-sibling observable was untested) and 2.5 (with path records and neither anomaly nor clause the checker still emitted `consistency_violation` with empty evidence; the `obligations_function_local` degradation named no file; two weak assertions). Remediated RED-first (2026-09-05; 5 new or tightened tests failed for the stated reasons, then green): `sibling_sets` keys on the module path without suffix (`_module_of`), pinned by `test_same_named_files_in_different_modules_are_not_siblings`; `consistency_anomalies(sets, min_siblings)` has one threshold: at least `min_siblings` siblings must discharge the kind (Req 3.2) and a set with fewer members is under-populated (Req 3.3); the shared fixture gains `constrained_too` (identity from `current_user.id`) and `constrained` becomes `@login_required`, so the leaky handler's anomaly names both constraining siblings and `min_siblings=3` stays silent with two; `_finding` returns `None` in path-aware mode without a clause or anomaly (design gating 3, `test_with_path_records_an_unjustified_obligation_yields_no_finding`), `function_local` is the degraded mode's label only; the degradation carries `language` and `files`; `dominance.calls` is asserted `== 0` without a valid witness and `> 0` with the hop witness; the static id assertion is exact. Suggestions applied: `valid_witness` lives in `operations.py` and is used by `covers`, `siblings` and `check`; decorator witnesses match the parsed decorator name only; the duplicated `maybe_guard` block is gone; `[[route]]` unknown-field test added; `orm-write` and `security-setting` facts are receiver-qualified (`session.add`, `query.update`, `app.run`, `app.listen`) and `abort` is no longer a path-guard call; the package exports `check_obligations`, `ObligationFinding`, `ObligationResult`, `findings_to_static`. Both matrices green (569 passed / 4 skipped; extra-free 554 / 19), ruff, format and mypy clean, gate stdout byte-identical to HEAD.
|
||||
- Task 4.1 (2026-09-05, RED first: `test_handler_context_mode_keeps_decorators_and_registration` (`KeyError: extract_handler_context`), `test_obligation_labeled_row_loads_and_is_detected_by_an_obligation_finding` (vocabulary lacked the three ids), `test_catalog_rejects_an_obligation_kind_outside_the_closed_set` (no rejection); then green): `harvest.py` mode `handler_context` = `extract_handler_context(source, function, language)`: the function with its decorators (via `extract_function`) followed by every statement outside it that names the function as an identifier in the masked text (comments and strings never count; `def`/`function`/`sub` declarations excluded), one line per registration statement (a multi-line `urlpatterns = [...]` contributes only the line naming the handler; recorded limitation); `validate_recipe` requires `function` for the mode; `extract_pair(_with_lines)` dispatch; README lists the mode. `mechanisms.toml` version 2 adds `unconstrained_protected_read`, `unconstrained_protected_write`, `unguarded_privileged_action` with definitions naming the operation kind. `ExpectedFinding.obligation: str | None = None` (additive, default keeps every manifest and catalog loading unchanged); `pairs._parse_expected` accepts `obligation` only from the closed `OPERATION_KINDS` and rejects others by name. Scorer: `_overlay_scan(..., obligations=)` computes obligation statics through `_obligation_scan` (function-local `check_obligations`, no policy, no store, `ObligationsConfig().min_siblings`) only when a row carries the label, so rows without it run byte-identical code; the one additive rule `_obligation_matches_expected` counts a finding tagged `obligation:<kind>` inside the labeled function; obligation findings on the fixed side are leaks under `fix_policy = silent`; `detection_kinds` gains `obligation`; the inventory fallback is not taken when obligation statics exist. Both matrices green (572 passed / 4 skipped), ruff and mypy clean, gate stdout byte-identical to HEAD (no gate row carries the label).
|
||||
- Review round 3 (2026-09-05) APPROVED 2.2; REJECTED 2.5, 3.1, 3.2, 3.3. Remediated RED-first (6 tests failed for the stated reasons, then green): (2.5) the declared-public waiver now covers the operation's own requirement, not only sibling anomalies (Req 4.4): the checker drops `path_guard` from `requires` when the route is declared public and falls through to the next accepted kind, pinned by `test_a_declared_public_route_never_reports_its_missing_path_guard_but_keeps_the_resource_obligation` on a `privileged_action` handler; the tautological static-id assertion is now exact for two findings; a malformed store row is an `obligations_store_row_invalid` degradation naming the record ids instead of a silent skip (design Error Handling). (3.1) the manifest block carries `degradations` and `sets` (`{module, resource, handlers}`) as the design requires; the scan test asserts the block's `obligations_function_local` entry names the language and file. (3.2) the `## Obligations` section prints `Sibling sets evaluated: N (under-populated: M)`, one line per sibling set and the policy version, fed by a new `obligations_summary=` parameter carrying the manifest payload; the proven-finding cap is keyed on verdict ids (`rank_obligations(findings, proven_ids=)`) because proof rungs live on `CandidateVerdict` and never on `StaticFinding`, and the scan re-ranks after REGRESS when the sandbox proved anything. (3.3) the intent prompt is passed through `redaction.redact_secrets` before it reaches the client (design Security Considerations); a test pins that the `leaky` prompt carries its own decorator and body and none of the other three handlers, and that a runtime-assembled key literal in the handler reaches the model redacted. Design amended: the static id contract carries the `:<missing>` suffix, the manifest lists `sets`, and the rank cap states the verdict-id keying. Both matrices green (578 passed / 4 skipped; extra-free 563 / 19), ruff and mypy clean, gate stdout byte-identical to HEAD.
|
||||
- Task 4.2 (2026-09-05, RED first: 2 failing tests in `tests/test_loo.py` — `LooOutcome` had no `obligations`, obligation rows produced no hits; then green): `score_pair_with_store` runs `check_obligations` on both materialized sides for rows carrying an obligation label, with the held-out store's obligation shapes as `store_shapes`; a row is detected only when a finding inside the labeled function carries both the labeled `obligation:<kind>` tag and a `mechanism:` tag naming a store record, so the checker's own facts-only finding is never counted as the corpus teaching; obligation findings on the fixed side are leaks; `LooOutcome.obligations` and `LooResult.per_obligation_kind` join the existing groupings in the payload. A three-pair toy slice measures 2/2 on `protected_read` (each pair found by the other) and 0/1 on `security_setting` (nobody else teaches the kind).
|
||||
- Path-aware evaluation (5.1) is wired through the `Dominance` protocol and an optional `PathRecord` input; until `reachability-flow-model` lands, every run is function-local by construction and tests use a stub `PathRecord`. No task here waits on the upstream specs.
|
||||
- Sibling grouping keys on (router or module, resource token). If the maintainer prefers data-model-only grouping, only task 2.2 and its tests change.
|
||||
|
||||
Reference in New Issue
Block a user