diff --git a/.kiro/specs/pre-push-safety-net/brief.md b/.kiro/specs/pre-push-safety-net/brief.md index 24dcc54..a328cc1 100644 --- a/.kiro/specs/pre-push-safety-net/brief.md +++ b/.kiro/specs/pre-push-safety-net/brief.md @@ -1,6 +1,6 @@ # Brief: pre-push-safety-net -Status: requirements, design and tasks approved on 2026-09-12; groups 1 and 2 plus tasks 3.1–3.3 verified complete (10/27 subtasks); task 3.4 blocked on frontend test retention; groups 4–8 pending. Language: English. +Status: requirements, design and tasks approved on 2026-09-12; groups 1–3 verified complete (11/27 subtasks), including the approved contributor-scan frontend-retention prerequisite; groups 4–8 pending. Language: English. ## Problem diff --git a/.kiro/specs/pre-push-safety-net/implementation-group-3.md b/.kiro/specs/pre-push-safety-net/implementation-group-3.md index 4977e16..9997cef 100644 --- a/.kiro/specs/pre-push-safety-net/implementation-group-3.md +++ b/.kiro/specs/pre-push-safety-net/implementation-group-3.md @@ -15,10 +15,10 @@ coverage limits. ## Verification state -Tasks 3.1–3.3 are independently approved and verified (10/27 tasks overall). -Task 3.4 is blocked on a confirmed frontend retention prerequisite; group 3 is -not complete. The proposed foundation repair is in -`../contributor-scan/frontend-retention-prerequisite.md`. +Group 3 is independently approved and VERIFIED (TASK scope): tasks 3.1–3.4 are +complete, bringing the pre-push spec to 11/27 tasks. The approved contributor-scan +frontend-retention prerequisite 2.18 is also verified. Groups 4–8 remain pending; +this is not feature GO, capability admission or a hook latency claim. ## Baseline @@ -130,7 +130,7 @@ line enforced security, that the defect is new, or that a hook meets its latency Ten of 27 tasks are now complete; task 3.4 remains for this group. -## Task 3.4 blocked verification +## Historical task 3.4 blocker Claim type TASK; status NOT_VERIFIED. The implementer returned BLOCKED, and an independent fresh-context Kiro debug investigation returned STOP_FOR_HUMAN with @@ -169,3 +169,44 @@ uncommitted pending the foundation repair, review and fresh runtime verification The packaged failure preceded a comment-only clarification in `cpg/backend.py` removing the disproven attribution to legacy frontend autodetection. No executable backend behavior changed after that measurement. + +## Task 3.4 final verification + +Status VERIFIED, claim type TASK; independent review APPROVED after two concrete +error-path repairs. The final suite passed 1,151 tests with nine skipped in +100.55 seconds. Ruff, formatting (230 files), mypy (109 source files), compileall, +diff and all three regression gates passed. + +The approved foundation repair is committed as `51fabef`. Physically filtered +frontend inputs retain original source paths and bytes; the existing layout facts +mark JavaScript/TypeScript test conventions unshipped. One existing ranker decision +and region/deadline budget cover supported partitions. Browser-path source remains +available, while runtime origin stays unspecified without declarations. Vendor, +cross-language, TypeScript-property and C-bounds limits remain explicit. + +Exact source names now travel in taint, dominance and configuration batch censuses. +PHP aggregation preserves them only when every shard provides named evidence. +Synthetic `` nodes cannot mask missing real files. Unreadable unknown +extensions produce `source_unreadable` coverage instead of raising PermissionError. +Legacy callbacks that omit census entirely retain their existing compatibility +behavior; production queries emit the named census, and unsupported/partial +sharded evidence cannot establish a complete negative. + +`benchmarks/measurements/2026-09-13-partition-scope-smoke.json` records the final +image and ten matching source/query/fact hashes. The real mixed graph retains +seven JavaScript files (296 bytes), including tests and browser source, plus two +PHP files (97 bytes), with vendor nodes absent. One question is selected and three +deferred; unsupported Go is reported. No false empty/unavailable census remains. +PHP and JavaScript change-context controls both complete with unchanged-helper +witnesses, preserving dirty checkout/index state and cleaning snapshot scratch. + +A real Python graph supplies named census from all three production query kinds. +Those unchanged Scala sources were verified before the separate PHP transport and +discovery repairs; the artifact records that narrower provenance explicitly. A +real filesystem permission probe verifies the repaired error path, using a clearly +labeled synthetic graph callback. + +The mixed smoke took 197.76 seconds under a 900-second lab allowance; PHP/JavaScript +context checks took 117.3/95.9 seconds. These manually declared controls do not +establish automatic scope discovery, production hook latency or alert quality. +Group 4 now owns novelty comparison, actionability admission and compact output. diff --git a/.kiro/specs/pre-push-safety-net/research.md b/.kiro/specs/pre-push-safety-net/research.md index 6ae5397..9f1bafd 100644 --- a/.kiro/specs/pre-push-safety-net/research.md +++ b/.kiro/specs/pre-push-safety-net/research.md @@ -6,7 +6,7 @@ - User direction: a scalable pre-push safety net as AI accelerates development; WordPress is one case. - Clarifications: advisory must be actionable, not SAST noise; the ranker is the scope mechanism; third-party vendor code must remain outside targets and graphs. -- Requirements, design and tasks are now approved, and groups 1 and 2 plus tasks 3.1–3.3 are verified complete; task 3.4 is blocked on JavaScript frontend test retention; see +- Requirements, design and tasks are now approved, and groups 1–3 are verified complete after the approved frontend-retention repair; see `tasks.md` for verified completion. The discovery log below preserves the earlier drafting state and measurements. No hook latency claim is made. @@ -167,3 +167,14 @@ Direct frontend invocation reproduced the omission. Two unconditional upstream test filters require an owning foundation repair; see `../contributor-scan/frontend-retention-prerequisite.md`. The runtime failure is not overridden by passing mocks, static checks or a populated server graph. + +### Group 3 completed after frontend and coverage repairs + +The approved contributor-scan 2.18 adaptation preserves test directories and +filename suffixes through both pinned JavaScript frontend filters. Task 3.4 then +passed independent review with exact graph-path census, including PHP aggregation. +Review also repaired unreadable discovery inputs throwing rather than reporting +incomplete coverage. A synthetic graph file must never compensate for an omitted +real source, and a wrapper must not discard evidence supplied by a frontend. +See `implementation-group-3.md` for final evidence. Eleven of 27 tasks are complete; +change attribution, actionability admission and compact output are next. diff --git a/.kiro/specs/pre-push-safety-net/spec.json b/.kiro/specs/pre-push-safety-net/spec.json index fe8bb3d..feaad60 100644 --- a/.kiro/specs/pre-push-safety-net/spec.json +++ b/.kiro/specs/pre-push-safety-net/spec.json @@ -1,7 +1,7 @@ { "feature_name": "pre-push-safety-net", "created_at": "2026-09-12T15:35:00Z", - "updated_at": "2026-09-13T09:17:01.788332+00:00", + "updated_at": "2026-09-13T09:50:01.821464+00:00", "language": "en", "phase": "implementation", "approvals": { @@ -19,7 +19,7 @@ } }, "ready_for_implementation": true, - "implementation_approval_note": "Groups 1 and 2 verified complete. Maintainer authorized continuing group 3; remaining approved tasks execute in dependency order with independent review and fresh verification.", + "implementation_approval_note": "Groups 1\u20133 verified complete. Contributor-scan frontend-retention prerequisite 2.18 approved and verified. Remaining approved tasks require independent review and fresh verification.", "depends_on": [ "contributor-scan" ], @@ -77,23 +77,17 @@ "2.3", "3.1", "3.2", - "3.3" + "3.3", + "3.4" ], - "remaining_tasks": 17, + "remaining_tasks": 16, "verification_record": "implementation-group-3.md", "verification_records": [ "implementation-group-1.md", "implementation-group-2.md", "implementation-group-3.md" ], - "next_group": 3, - "blocked_tasks": [ - { - "task": "3.4", - "reason": "Pinned JavaScript frontend silently excludes first-party tests; missing contributor-scan prerequisite.", - "record": "../contributor-scan/frontend-retention-prerequisite.md", - "next_action": "implement_approved_contributor_scan_2.18" - } - ] + "next_group": 4, + "blocked_tasks": [] } } diff --git a/.kiro/specs/pre-push-safety-net/tasks.md b/.kiro/specs/pre-push-safety-net/tasks.md index 1e6ad07..469b02d 100644 --- a/.kiro/specs/pre-push-safety-net/tasks.md +++ b/.kiro/specs/pre-push-safety-net/tasks.md @@ -1,7 +1,7 @@ # Implementation Plan: pre-push-safety-net -Status: task plan approved by the maintainer's "approve." on 2026-09-12. Groups 1 and 2 (tasks 1.1–2.3) -are complete after independent review and fresh verification. Group 3 has 3.1–3.3 verified; 3.4 is blocked on frontend test retention. Groups 4–8 remain pending; +Status: task plan approved by the maintainer's "approve." on 2026-09-12. Groups 1–3 (tasks 1.1–3.4) +are complete after independent review and fresh verification (11/27 subtasks). Groups 4–8 remain pending; the hook and capability admission are not yet implemented. See `implementation-group-1.md` and `implementation-group-2.md`. ## Execution contract @@ -78,7 +78,7 @@ the hook and capability admission are not yet implemented. See `implementation-g - _Requirements: 1.4, 2.1, 2.4, 3.4_ - _Verified 2026-09-12:_ independent review APPROVED; fresh final suite 1,069 passed, 9 skipped; focused 148 passed. Global Ruff/format, touched mypy and regression gates passed. PHP/JavaScript fixtures retain removed spans and unchanged-operation anchors across declaration renames; uncertain moves, repeated lines, binary and unavailable inputs remain unresolved. Forty independent real-Git comparisons checked exact spans/anchors. Network-disabled image smoke preserved committed PHP/JavaScript bytes and dirty non-HEAD state, with cleanup verified; see `implementation-group-2.md`. Lexical anchors do not establish semantic impact or novelty. -- [ ] 3. Integrate bounded execution and ranker scope with the existing engine +- [x] 3. Integrate bounded execution and ranker scope with the existing engine - [x] 3.1 (P) Enforce one remaining-time budget in the backend - Thread the absolute deadline through readability probes, frontend builds, overlays, census, query batches, fallbacks and retries; no phase gets a fresh full budget. - Bound process-group termination and lease/scratch cleanup by the cancellation allowance, replacing any longer fixed cleanup waits on the push path. @@ -103,13 +103,14 @@ the hook and capability admission are not yet implemented. See `implementation-g - _Depends: 2.3, 3.2_ - _Requirements: 2.1, 2.2, 2.4, 8.1, 8.2_ - _Verified 2026-09-13:_ independent review APPROVED after an unknown-call boundary repair; fresh suite 1,128 passed, nine skipped; global Ruff/format/mypy passed. Real PHP/JavaScript Git guard-removal snapshots reached unchanged helper-file witnesses with local dirty state preserved. Production negative regression reproduced then fixed unknown(req.body) being pruned as tier zero: modeled arguments no longer summarize unknown consumers. Existing ranking and detector semantics unchanged; unsupported projections remain explicit. Evidence: `benchmarks/measurements/2026-09-13-change-context-smoke.json`. Lexical attribution is not semantic novelty or alert admission. -- [ ] 3.4 Apply vendor exclusion and explicit language partitions - - _Blocked:_ Joern 4.0.625/ASTGen 3.50.1 omit first-party JavaScript tests despite readable filtered inputs. Independent debug returned STOP_FOR_HUMAN; review the owning `contributor-scan/frontend-retention-prerequisite.md` before resuming. Implementation remains uncommitted and unapproved; runtime evidence: `benchmarks/measurements/2026-09-13-partition-retention-blocker.json`. +- [x] 3.4 Apply vendor exclusion and explicit language partitions + - _Prerequisite resolved 2026-09-13:_ contributor-scan 2.18 independently approved and verified at `51fabef`; both JavaScript test filters repaired by explicit versioned input policy. Task 3.4 subsequently passed its own independent review. - Reuse declared layout exclusions before target enumeration and graph build; retain unshipped first-party tests and separate browser/server or language capability coverage. - Build supported frontend partitions explicitly; preserve unknown cross-language and vendor semantics without restoring excluded sources. - Done when PHP and Node graphs/targets contain no declared vendor code, first-party callers remain visible, and unsupported partitions appear as coverage limits rather than clean results. - _Boundary: Ranker Scope Contract, Cache and Engine Boundary integration_ - _Requirements: 2.2, 2.3, 7.3, 8.2, 8.4_ + - _Verified 2026-09-13:_ independent review APPROVED; 1,151 passed, nine skipped; Ruff/format/mypy pass. Real PHP/JavaScript graphs retain tests/browser source, omit vendor nodes and share one ranker limit. Named census survives PHP aggregation; missing source names and unreadable discovery inputs remain explicit. Python exercises all three batch census schemas. Evidence: `benchmarks/measurements/2026-09-13-partition-scope-smoke.json`. No hook capability or latency claim. - [ ] 4. Implement change attribution and actionable output - [ ] 4.1 Compare targeted base/head evidence for defect novelty diff --git a/.kiro/steering/roadmap.md b/.kiro/steering/roadmap.md index 8365b00..1b7e8c7 100644 --- a/.kiro/steering/roadmap.md +++ b/.kiro/steering/roadmap.md @@ -27,7 +27,7 @@ engine are out of scope. - Equal-score tie-breaking alone cannot bring PMPro or WP Statistics under 50. The existing ranker is useful, but its measured ordering is not enabled by the ordinary CLI caller. - Docker/Compose/ops now default to 4.0.625; task 1.1 passed local PHP/JavaScript image smoke and independent review. No host deployment or hook latency claim follows from that runtime evidence. -- `contributor-scan` has 24/40 checked tasks; historical checkboxes are not rewritten by this review. +- `contributor-scan` has 25/41 checked tasks, including the approved and verified frontend-retention prerequisite 2.18. `flow-aware-ranking` is still an unapproved brief despite implementation history. ## Existing Spec Updates @@ -46,15 +46,16 @@ engine are out of scope. - [ ] pre-push-safety-net — Immutable pushed tips, existing-ranker integration, compatible artifact reuse, total deadline, change attribution, actionability admission and compact hook behavior. Dependencies: contributor-scan. Requirements/design approved 2026-09-12; tasks generated - (8 groups, 27 executable subtasks), independently reviewed and approved; groups 1 and 2 and tasks 3.1–3.3 are complete (10/27 tasks); 3.4 is blocked. + (8 groups, 27 executable subtasks), independently reviewed and approved; groups 1–3 are complete (11/27 tasks), including the resolved frontend-retention prerequisite. ### Next concrete step -Groups 1 and 2 and tasks 3.1–3.3 have passed independent review: runtime, inputs, -scoring, snapshots, shared deadline, exact ranker scope and change-context evidence. -Task 3.4 is blocked: real Joern graphs omit first-party JavaScript tests even though -filtered inputs contain them. Review `contributor-scan/frontend-retention-prerequisite.md`, -repair that foundation contract, then rerun the partition census and independent review. +Groups 1–3 have passed independent review: runtime, inputs, scoring, snapshots, +shared deadline, exact ranker scope, change-context evidence and vendor-free language +partitions. The approved contributor-scan prerequisite 2.18 (`51fabef`) repairs both +JavaScript test filters; real graphs retain first-party tests and exclude vendor code. +Task 3.4 also repairs unreadable-input handling and preserves named PHP census evidence. +Group 4 is next: novelty comparison, actionability admission and compact advisory output. See `pre-push-safety-net/implementation-group-3.md` for evidence and limits. The first complete experiment is 5.2, before persistent caching and hook installation work. Keep PMPro as the primary core-optimization example, then freeze the core and diff --git a/.kiro/steering/safety-net.md b/.kiro/steering/safety-net.md index 3c68a29..84f5795 100644 --- a/.kiro/steering/safety-net.md +++ b/.kiro/steering/safety-net.md @@ -79,8 +79,10 @@ updates as a shortcut to an unmeasured latency promise. The product direction above is user-requested. The amended requirements, design and initial evaluation profile in `pre-push-safety-net` were approved for task generation on 2026-09-12 ("ok, next kiro phase"). The maintainer then approved the task plan and starting group 1 -("approve.") and then group 2 ("ok, next kiro phase, approved"). Groups 1 and 2 are now verified -complete, as are group 3 tasks 3.1–3.3 (10/27 total). Task 3.4 is blocked on a -confirmed frontend test-retention gap; review the proposed contributor-scan -frontend-retention prerequisite before resuming it. Implementation is authorized under that plan; task completion still requires -independent review and fresh evidence. Hook performance and capability admission remain unproven. +("approve.") and then group 2 ("ok, next kiro phase, approved"). Groups 1–3 are now verified +complete (11/27 total). The maintainer approved the frontend-retention prerequisite; +contributor-scan 2.18 and pre-push task 3.4 have both passed independent review and +fresh verification. Group 4 is next: novelty comparison, actionability admission and +compact advisory output. Implementation is authorized under the approved plan; +task completion still requires independent review and fresh evidence. Hook performance +and capability admission remain unproven. diff --git a/benchmarks/measurements/2026-09-08-module-audit.json b/benchmarks/measurements/2026-09-08-module-audit.json index be47a4a..ce994bc 100644 --- a/benchmarks/measurements/2026-09-08-module-audit.json +++ b/benchmarks/measurements/2026-09-08-module-audit.json @@ -1,10 +1,10 @@ { "counts": { - "load_bearing": 98, + "load_bearing": 99, "standalone_capability": 9 }, - "modules": 107, - "note": "Refreshed 2026-09-12 after pre-push-safety-net group 2: immutable comparison, materialization and lexical change context are spec-owned; ranker/runner integration remains pending.", + "modules": 108, + "note": "Refreshed after pre-push-safety-net task 3.4: first-party frontend partitions share the authoritative ranker and execution budget.", "orphaned": [], "rows": [ { @@ -129,6 +129,7 @@ "model.config_value", "model.dominance", "model.judge", + "model.partitions", "model.pipeline", "model.scan", "model.taint" @@ -149,6 +150,7 @@ "model.config_value", "model.dominance", "model.judge", + "model.partitions", "model.pipeline", "model.scan", "model.taint" @@ -338,12 +340,14 @@ "classification": "load_bearing", "importers": [ "cli", + "cpg.backend", "model.candidates", "model.config_value", "model.dominance", "model.execution", "model.judge", "model.layout", + "model.partitions", "model.pipeline", "model.regions", "model.report", @@ -390,6 +394,10 @@ { "classification": "load_bearing", "importers": [ + "cpg.backend", + "model.partitions", + "model.regions", + "model.scan", "push.contracts", "push.snapshot" ], @@ -450,12 +458,21 @@ { "classification": "load_bearing", "importers": [ + "model.partitions", "model.scan", "preprocess" ], "module": "model.layout", "reason": "reachable from gate, map_gate, pair_gate or the CLI" }, + { + "classification": "load_bearing", + "importers": [ + "model.scan" + ], + "module": "model.partitions", + "reason": "reachable from gate, map_gate, pair_gate or the CLI" + }, { "classification": "load_bearing", "importers": [ @@ -596,6 +613,7 @@ "map_gate", "mapping", "model.candidates", + "model.partitions", "model.scan", "pairs", "rank", diff --git a/benchmarks/measurements/2026-09-13-partition-scope-smoke.json b/benchmarks/measurements/2026-09-13-partition-scope-smoke.json new file mode 100644 index 0000000..d32dc0b --- /dev/null +++ b/benchmarks/measurements/2026-09-13-partition-scope-smoke.json @@ -0,0 +1,1026 @@ +{ + "recorded_at": "2026-09-13T09:50:01.821464+00:00", + "task": "pre-push-safety-net 3.4", + "verification": "VERIFIED", + "claim_type": "TASK", + "independent_review": "APPROVED", + "image_id": "sha256:e38f06ef1bebaeaa72fa31fd95675aca2935de3b45cebe8147f92caeb98fc5a1", + "build_exit_code": 0, + "mixed_smoke_exit_code": 0, + "context_smoke_exit_code": 0, + "package": { + "hashes": { + "cpg/backend.py": "a18835af13d85238758b77cef7137b49154050a96b7f858b7e615d0ef9a90fe5", + "model/scan.py": "ad6f134c8fae43aa102b61d99f273b467415193d0b27fb634cae2586cb3c43ae", + "model/partitions.py": "82dae4769afab97f301fc28adea77d3ae83696e29dd9d83e883e66b1090d3796", + "model/layout.py": "50a1a163459f8802037faae750bc1f99a0ab81912b571b40303675590794d27c", + "preprocess.py": "0d0b1f22fd2024be3e50dc1a9ff947eed7f8f60f67644277e344b83697aaefc8", + "cli.py": "79608d4a74bcceb5bd41b149cf806d25c4f71d8b3201d710217395941169bdc2", + "cpg/queries/taint.sc": "dbc8e7c60463dc4b68db8e15537d2fafb7d429597a0c9789f4b95b0cfb0e14bb", + "cpg/queries/dominance.sc": "0623d84ee35d6dc06f577ebfc0bcfaa591dafe0287515b16c7d6b5c9a29ac290", + "cpg/queries/config.sc": "f0cd5519a6a246d4159d1141353c1a85a82b610c8c717f43132bc52d24818b0b", + "ruleset/semantic/javascript.toml": "574dd6b6313b3ca589c57285a6adae78339bb8168e199617d6e994f36b2d0c64" + }, + "manifest": { + "revision": "ousast-frontend-retention-v1", + "builder_sha256": "21be57123922fa83ab3a4a3c684f7ddd8bf4ffc79dfb8379226ea72d2b1cac02", + "policy": "OUSAST_INCLUDE_TESTS=1", + "sources": { + "astgen.tar.gz": [ + "https://codeload.github.com/joernio/astgen-monorepo/tar.gz/refs/tags/javascript-astgen/v3.50.1", + "8d9728dca8eab694a0f07bcd7a1c9a88368cb7bd354fc19c2ee8ca8611ff869a" + ], + "AstGenRunner.scala": [ + "https://raw.githubusercontent.com/joernio/joern/v4.0.625/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/utils/AstGenRunner.scala", + "9963f949360d7e17baed1bd8875b717b2855745cf7260eef8a87d17533a54268" + ], + "bun.zip": [ + "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64.zip", + "36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913" + ] + }, + "original_jar_sha256": "49fe488a79cc52b4e1d2e056143ec414f59e0007ed25f054c5fa7ae4aa34dff3", + "adapted_jar_sha256": "7b3a91a695abcd98652e83a4dfdbfd7c3be63b085e1bdfac0aae3139a714f1be", + "astgen_sha256": "eb1b01b2244e7dea4388716240bc0c1c1b66ff6de8cd1dfda30d3855627083e4", + "compiled_entries": [ + "io/joern/jssrc2cpg/utils/AstGenRunner$$anon$1.class", + "io/joern/jssrc2cpg/utils/AstGenRunner$.class", + "io/joern/jssrc2cpg/utils/AstGenRunner$astGenMetaData$.class", + "io/joern/jssrc2cpg/utils/AstGenRunner.class", + "io/joern/jssrc2cpg/utils/AstGenRunner.tasty" + ] + } + }, + "packaged_sources_match_workspace": true, + "script_hashes": { + "ops/smoke_partitions.py": "b616b88802c1bce12d4d68f2ae669becbc2db869ecb6ab85b65c6d8fd81f08ff", + "ops/smoke_context.py": "fb23c84638fbcca79017f69654b32e34b01ccda34f23ea7cc0e7d84191c9e2b9", + "ops/smoke_partition_census.py": "fe257ebbee3301bf1b4e7bb8af9ed95fd3850ef6cb16d255b6a7d7620a7173d1" + }, + "mixed_runtime": { + "purpose": "Real mixed PHP/JavaScript graph census and one global ranker budget; no admission or latency claim", + "shared_budget_seconds": 900, + "include_tests": true, + "inputs": { + "src/api.php": { + "bytes": 49, + "sha256": "47ee82519e90f5ce2c845035f168bc4eaa4b0cc6e46cfc68626b8144e20b8f72" + }, + "server/api.js": { + "bytes": 51, + "sha256": "a9f1b897b9361d0d30250f82aebec987cfd99e66308c9ea889d5b6211e3cad5e" + }, + "browser/client.js": { + "bytes": 41, + "sha256": "42ef299a3c9a53f1ca77f9e4b027c8d6e854f69b7df765bdb833079198513fc4" + }, + "tests/probe.php": { + "bytes": 48, + "sha256": "93c25992f2b709d4f222d88573a004dfb4e65239ce9a3afc2301f629b148c03d" + }, + "tests/probe.js": { + "bytes": 41, + "sha256": "182f7cb37514b7fc6afbba045a9d1b3ca5dc297c35bb98b9f34c18cfd02864f5" + }, + "vendor/library.php": { + "bytes": 55, + "sha256": "dd2aa2514d7c2dffdf4cfd238d7b795c2cd7bb6be086054b0b6d2278bd2c9599" + }, + "node_modules/library.js": { + "bytes": 46, + "sha256": "5e42f2831e2e32d8c19df33ba98fbda879495b74bd6d7e0b6ff45a077674f398" + }, + "unsupported.go": { + "bytes": 36, + "sha256": "ecf7a397251f1fea2e5992bdc3b0581274c20eb83f0ac403f6b7babe7d7b6b5b" + }, + "checks/probe.test.js": { + "bytes": 41, + "sha256": "182f7cb37514b7fc6afbba045a9d1b3ca5dc297c35bb98b9f34c18cfd02864f5" + }, + "checks/probe.spec.js": { + "bytes": 41, + "sha256": "a2677f51273a325b802a4a628ceb54f0bcea533a672c8997c0439d6ea206de37" + }, + "checks/probe.mock.js": { + "bytes": 41, + "sha256": "bc14cd012a0d2c34a3605ce21a009b7fd335bdf248f81aec3462b506929eabd5" + }, + "checks/probe.e2e.js": { + "bytes": 40, + "sha256": "14884e5a7d6c909f30addd8385bb82f6ea3751bb126192052dabe0eedfb42400" + } + }, + "graph_witnesses": [ + { + "frontend": "javascript", + "input_root": "/tmp/ousast-input-v8ria8_s", + "graph_witness": { + "files": [ + "browser/client.js", + "checks/probe.e2e.js", + "checks/probe.mock.js", + "checks/probe.spec.js", + "checks/probe.test.js", + "server/api.js", + "tests/probe.js" + ], + "methods": [ + "browser_marker", + ":program", + "js_e2e_marker", + ":program", + "js_mock_marker", + ":program", + "js_spec_marker", + ":program", + "js_test_marker", + ":program", + "js_handler", + ":program", + "js_test_marker", + ":program", + ".assignment", + "eval", + ".fieldAccess" + ] + } + }, + { + "frontend": "php", + "input_root": "/tmp/ousast-input-71hph722", + "graph_witness": { + "files": [ + "src/api.php", + "tests/probe.php", + "" + ], + "methods": [ + "", + "php_handler", + "", + "php_test_marker", + "eval" + ] + } + } + ], + "result": { + "findings": [ + { + "site": "src/api.php:2:php_handler", + "family": "injection", + "rung": "model_entailed", + "witness": "$req -> eval($req) in src/api.php (line 2, 2 steps)", + "contradiction": "", + "reached_from": 1 + } + ], + "by_rung": { + "suspicion": 0, + "model_corroborated": 0, + "model_entailed": 1, + "execution_confirmed": 0 + }, + "regions_scanned": 1, + "regions_unjudged": 3, + "regions_unasked": 0, + "model_calls": 0, + "cost_usd": 0.0, + "seconds": 197.76, + "build_seconds": 124.7, + "query_seconds": 40.89, + "query_seconds_by_kind": { + "evidence": 32.07, + "taint": 40.89 + }, + "degradations": [ + { + "stage": "model", + "reason": "cross_partition_semantics_unresolved" + }, + { + "stage": "model", + "reason": "frontend_unsupported" + }, + { + "stage": "model", + "reason": "vendor_semantics_unresolved" + }, + { + "stage": "model", + "reason": "regions_truncated", + "examined": 1, + "total": 4 + } + ], + "tiers": [ + [ + "src/api.php", + "php_handler", + "injection", + 4 + ] + ], + "tier_counts": { + "4": 1 + }, + "requests_pruned": 0, + "partitions": [ + { + "language": "go", + "frontend": null, + "paths": [ + "unsupported.go" + ], + "source_bytes": 0, + "status": "unsupported", + "boundaries": [ + "frontend_unsupported" + ], + "runtime": "unspecified", + "excluded_paths": [ + "node_modules/", + "vendor/" + ], + "unshipped_paths": [] + }, + { + "language": "javascript", + "frontend": "javascript", + "paths": [ + "browser/client.js", + "checks/probe.e2e.js", + "checks/probe.mock.js", + "checks/probe.spec.js", + "checks/probe.test.js", + "server/api.js", + "tests/probe.js" + ], + "source_bytes": 296, + "status": "built", + "boundaries": [ + "runtime_unspecified" + ], + "runtime": "unspecified", + "excluded_paths": [ + "node_modules/", + "vendor/" + ], + "unshipped_paths": [ + "checks/probe.e2e.js", + "checks/probe.mock.js", + "checks/probe.spec.js", + "checks/probe.test.js", + "tests/probe.js" + ] + }, + { + "language": "php", + "frontend": "php", + "paths": [ + "src/api.php", + "tests/probe.php" + ], + "source_bytes": 97, + "status": "built", + "boundaries": [], + "runtime": "unspecified", + "excluded_paths": [ + "node_modules/", + "vendor/" + ], + "unshipped_paths": [ + "tests/probe.php" + ] + } + ], + "change_context": null, + "scope": { + "ranker_version": "existing-static-evidence-v1", + "ranking_mode": "static", + "population_complete": true, + "selected": [ + { + "identity": { + "unit": "partition-smoke", + "language": "php", + "path": "src/api.php", + "function": "php_handler", + "family": "injection" + }, + "priority": 0.0, + "evidence": [ + "static_rank=10.0", + "normalized_evidence={\"access_declared_public\":true,\"bound_names\":[\"mysqli_prepare\",\"mysqli_stmt_bind_param\",\"pg_execute\",\"pg_prepare\"],\"carried\":false,\"carried_weak\":false,\"entry\":true,\"family_in_repo\":true,\"shipped\":true,\"sinks\":[{\"arg0_literal\":false,\"arity\":1,\"cleansed_on_call\":false,\"code\":\"eval($req)\",\"line\":2,\"method\":\"php_handler\"}],\"source_local\":false,\"source_near\":false}", + "tier=4", + "score=2.5" + ], + "tier": 4, + "score": 2.5 + } + ], + "deferred": [ + { + "identity": { + "unit": "partition-smoke", + "language": "javascript", + "path": "server/api.js", + "function": "js_handler", + "family": "injection" + }, + "reason": "region_budget", + "evidence": [ + "static_rank=9.0", + "evidence_unknown" + ] + }, + { + "identity": { + "unit": "partition-smoke", + "language": "php", + "path": "tests/probe.php", + "function": "php_test_marker", + "family": "injection" + }, + "reason": "region_budget", + "evidence": [ + "static_rank=2.0", + "evidence_unknown" + ] + }, + { + "identity": { + "unit": "partition-smoke", + "language": "javascript", + "path": "tests/probe.js", + "function": "js_test_marker", + "family": "injection" + }, + "reason": "region_budget", + "evidence": [ + "static_rank=1.0", + "evidence_unknown" + ] + } + ], + "unresolved_boundaries": [ + "evidence_unknown:022bab405f3a4daa5ee1cbca6b54c24d052b3060f915b850bc1b592d2233999c", + "evidence_unknown:d867a0ae5d785b179a30fcef9efcdb8304281d53694a51c2da4180ef5069a53d", + "evidence_unknown:66aa438d134c37f7107503949179ce763fecaa08f0a1d00646ee8a5f446d88f7", + "cross_partition_semantics_unresolved", + "frontend_unsupported", + "vendor_semantics_unresolved" + ] + }, + "question_outcomes": [ + { + "identity": { + "unit": "partition-smoke", + "language": "php", + "path": "src/api.php", + "function": "php_handler", + "family": "injection" + }, + "status": "unresolved", + "reason": "graph_incomplete", + "raw_rows_json": "[{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":2,\"paths\":1,\"sanitized\":false,\"sink\":\"eval($req)\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"src/api.php\",\"sinkLine\":\"2\",\"sinkMethod\":\"php_handler\",\"source\":\"$req\",\"sourceKind\":\"parameter\"}]" + } + ], + "family_coverage": [ + { + "unit": "partition-smoke", + "language": "javascript", + "family": "injection", + "selected": 0, + "completed": 0, + "unanswered": 0, + "deferred": 2, + "unsupported": 0 + }, + { + "unit": "partition-smoke", + "language": "php", + "family": "injection", + "selected": 1, + "completed": 0, + "unanswered": 1, + "deferred": 1, + "unsupported": 0 + } + ] + } + }, + "context_runtime": { + "purpose": "Committed removed-guard context reaches real ranked questions; not semantic novelty or admission proof", + "shared_budget_seconds": 900, + "languages": { + "php": { + "base_revision": "a6b5ebf9bc3dbee298a268678f0cb48976d2c7ae", + "head_revision": "639e3754323993d0d9938882c1dcf85ed19d30cd", + "source_bytes": 124, + "source_sha256": "e41333ddd3e341d08603a5cc6a3a9faedb442e6ef3b14627cd00516cdf12cedb", + "helper_bytes": 48, + "helper_sha256": "4eb14937fd2b027cc11bd5a79c5de38f572d9a5af8ee36a46d4a0e499817ae76", + "input_context": { + "base_revision": "a6b5ebf9bc3dbee298a268678f0cb48976d2c7ae", + "head_revision": "639e3754323993d0d9938882c1dcf85ed19d30cd", + "changed_paths": [ + "6170692e706870" + ], + "deleted_paths": [], + "spans": [ + { + "path": "6170692e706870", + "start_line": 4, + "end_line": 4, + "side": "base" + } + ], + "renames": [], + "declaration_paths": [], + "relationships": [], + "unresolved_boundaries": [], + "path_encoding": "filesystem-bytes-hex", + "line_correspondences": [ + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 1, + "base_end_line": 1, + "head_start_line": 1 + }, + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 2, + "base_end_line": 2, + "head_start_line": 2 + }, + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 3, + "base_end_line": 3, + "head_start_line": 3 + }, + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 5, + "base_end_line": 5, + "head_start_line": 4 + }, + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 6, + "base_end_line": 6, + "head_start_line": 5 + } + ] + }, + "result": { + "findings": [ + { + "site": "helper.php:2:helper", + "family": "injection", + "rung": "model_entailed", + "witness": "$req -> eval($value) in helper.php (line 2, 4 steps)", + "contradiction": "", + "reached_from": 1 + } + ], + "by_rung": { + "suspicion": 0, + "model_corroborated": 0, + "model_entailed": 1, + "execution_confirmed": 0 + }, + "regions_scanned": 1, + "regions_unjudged": 0, + "regions_unasked": 0, + "model_calls": 0, + "cost_usd": 0.0, + "seconds": 117.3, + "build_seconds": 51.92, + "query_seconds": 33.47, + "query_seconds_by_kind": { + "evidence": 31.85, + "taint": 33.47 + }, + "degradations": [], + "tiers": [ + [ + "api.php", + "handler", + "injection", + 4 + ] + ], + "tier_counts": { + "4": 1 + }, + "requests_pruned": 0, + "partitions": [ + { + "language": "php", + "frontend": "php", + "paths": [ + "api.php", + "helper.php" + ], + "source_bytes": 124, + "status": "built", + "boundaries": [], + "runtime": "unspecified", + "excluded_paths": [], + "unshipped_paths": [] + } + ], + "change_context": { + "base_revision": "a6b5ebf9bc3dbee298a268678f0cb48976d2c7ae", + "head_revision": "639e3754323993d0d9938882c1dcf85ed19d30cd", + "changed_paths": [ + "6170692e706870" + ], + "deleted_paths": [], + "spans": [ + { + "path": "6170692e706870", + "start_line": 4, + "end_line": 4, + "side": "base" + } + ], + "renames": [], + "declaration_paths": [], + "relationships": [ + { + "source": { + "unit": "context-smoke", + "language": "php", + "path": "api.php", + "function": "handler", + "family": "injection" + }, + "target": { + "unit": "context-smoke", + "language": "php", + "path": "api.php", + "function": "handler", + "family": "injection" + }, + "kind": "entry", + "evidence": [ + "base_span:api.php:4-4", + "unchanged_line:3;lexical_only", + "unchanged_line:4;lexical_only", + "unchanged_line:5;lexical_only" + ] + } + ], + "unresolved_boundaries": [], + "path_encoding": "filesystem-bytes-hex", + "line_correspondences": [ + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 1, + "base_end_line": 1, + "head_start_line": 1 + }, + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 2, + "base_end_line": 2, + "head_start_line": 2 + }, + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 3, + "base_end_line": 3, + "head_start_line": 3 + }, + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 5, + "base_end_line": 5, + "head_start_line": 4 + }, + { + "base_path": "6170692e706870", + "head_path": "6170692e706870", + "base_start_line": 6, + "base_end_line": 6, + "head_start_line": 5 + } + ] + }, + "scope": { + "ranker_version": "existing-static-evidence-v1", + "ranking_mode": "evidence", + "population_complete": true, + "selected": [ + { + "identity": { + "unit": "context-smoke", + "language": "php", + "path": "api.php", + "function": "handler", + "family": "injection" + }, + "priority": 0.0, + "evidence": [ + "static_rank=1.0", + "normalized_evidence={\"access_declared_public\":true,\"bound_names\":[\"mysqli_prepare\",\"mysqli_stmt_bind_param\",\"pg_execute\",\"pg_prepare\"],\"carried\":false,\"carried_weak\":false,\"entry\":true,\"family_in_repo\":true,\"shipped\":true,\"sinks\":[{\"arg0_literal\":false,\"arity\":1,\"cleansed_on_call\":false,\"code\":\"eval($value)\",\"line\":2,\"method\":\"helper\"}],\"source_local\":false,\"source_near\":false}", + "tier=4", + "score=2.5", + "change_sites={\"base_revision\":\"a6b5ebf9bc3dbee298a268678f0cb48976d2c7ae\",\"changed_paths\":[\"6170692e706870\"],\"declaration_paths\":[],\"deleted_paths\":[],\"head_revision\":\"639e3754323993d0d9938882c1dcf85ed19d30cd\",\"path_encoding\":\"filesystem-bytes-hex\"}", + "affected_relationship={\"evidence\":[\"base_span:api.php:4-4\",\"unchanged_line:3;lexical_only\",\"unchanged_line:4;lexical_only\",\"unchanged_line:5;lexical_only\"],\"kind\":\"entry\",\"source\":{\"family\":\"injection\",\"function\":\"handler\",\"language\":\"php\",\"path\":\"api.php\",\"unit\":\"context-smoke\"},\"target\":{\"family\":\"injection\",\"function\":\"handler\",\"language\":\"php\",\"path\":\"api.php\",\"unit\":\"context-smoke\"}}" + ], + "tier": 4, + "score": 2.5 + } + ], + "deferred": [], + "unresolved_boundaries": [] + }, + "question_outcomes": [ + { + "identity": { + "unit": "context-smoke", + "language": "php", + "path": "api.php", + "function": "handler", + "family": "injection" + }, + "status": "completed", + "reason": "query_answered_and_arbitrated", + "raw_rows_json": "[{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":4,\"paths\":1,\"sanitized\":false,\"sink\":\"eval($value)\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"helper.php\",\"sinkLine\":\"2\",\"sinkMethod\":\"helper\",\"source\":\"$req\",\"sourceKind\":\"parameter\"}]" + } + ], + "family_coverage": [ + { + "unit": "context-smoke", + "language": "php", + "family": "injection", + "selected": 1, + "completed": 1, + "unanswered": 0, + "deferred": 0, + "unsupported": 0 + } + ] + } + }, + "javascript": { + "base_revision": "14e9c2e283b5582031844e8aa119226d28e4b90a", + "head_revision": "0dab75fe2b6096f66b50eae27db92490f36e7443", + "source_bytes": 133, + "source_sha256": "ee6b40b0c84f84cda86b2f5d7d118f39504bc7237450d30e5c189f6b3517c965", + "helper_bytes": 47, + "helper_sha256": "173055ccb4d1b0cb76378bcb674ac9944bad02f848bf20139923775b57e7634d", + "input_context": { + "base_revision": "14e9c2e283b5582031844e8aa119226d28e4b90a", + "head_revision": "0dab75fe2b6096f66b50eae27db92490f36e7443", + "changed_paths": [ + "6170692e6a73" + ], + "deleted_paths": [], + "spans": [ + { + "path": "6170692e6a73", + "start_line": 3, + "end_line": 3, + "side": "base" + } + ], + "renames": [], + "declaration_paths": [], + "relationships": [], + "unresolved_boundaries": [], + "path_encoding": "filesystem-bytes-hex", + "line_correspondences": [ + { + "base_path": "6170692e6a73", + "head_path": "6170692e6a73", + "base_start_line": 1, + "base_end_line": 1, + "head_start_line": 1 + }, + { + "base_path": "6170692e6a73", + "head_path": "6170692e6a73", + "base_start_line": 2, + "base_end_line": 2, + "head_start_line": 2 + }, + { + "base_path": "6170692e6a73", + "head_path": "6170692e6a73", + "base_start_line": 4, + "base_end_line": 4, + "head_start_line": 3 + }, + { + "base_path": "6170692e6a73", + "head_path": "6170692e6a73", + "base_start_line": 5, + "base_end_line": 5, + "head_start_line": 4 + } + ] + }, + "result": { + "findings": [ + { + "site": "helper.js:1:helper", + "family": "injection", + "rung": "model_entailed", + "witness": "req -> eval(value) in helper.js (line 1, 4 steps)", + "contradiction": "", + "reached_from": 1 + } + ], + "by_rung": { + "suspicion": 0, + "model_corroborated": 0, + "model_entailed": 1, + "execution_confirmed": 0 + }, + "regions_scanned": 1, + "regions_unjudged": 0, + "regions_unasked": 0, + "model_calls": 0, + "cost_usd": 0.0, + "seconds": 95.9, + "build_seconds": 29.66, + "query_seconds": 28.09, + "query_seconds_by_kind": { + "evidence": 38.12, + "taint": 28.09 + }, + "degradations": [], + "tiers": [ + [ + "api.js", + "handler", + "injection", + 4 + ] + ], + "tier_counts": { + "4": 1 + }, + "requests_pruned": 0, + "partitions": [ + { + "language": "javascript", + "frontend": "javascript", + "paths": [ + "api.js", + "helper.js" + ], + "source_bytes": 133, + "status": "built", + "boundaries": [ + "runtime_unspecified" + ], + "runtime": "unspecified", + "excluded_paths": [], + "unshipped_paths": [] + } + ], + "change_context": { + "base_revision": "14e9c2e283b5582031844e8aa119226d28e4b90a", + "head_revision": "0dab75fe2b6096f66b50eae27db92490f36e7443", + "changed_paths": [ + "6170692e6a73" + ], + "deleted_paths": [], + "spans": [ + { + "path": "6170692e6a73", + "start_line": 3, + "end_line": 3, + "side": "base" + } + ], + "renames": [], + "declaration_paths": [], + "relationships": [ + { + "source": { + "unit": "context-smoke", + "language": "javascript", + "path": "api.js", + "function": "handler", + "family": "injection" + }, + "target": { + "unit": "context-smoke", + "language": "javascript", + "path": "api.js", + "function": "handler", + "family": "injection" + }, + "kind": "entry", + "evidence": [ + "base_span:api.js:3-3", + "unchanged_line:2;lexical_only", + "unchanged_line:3;lexical_only", + "unchanged_line:4;lexical_only" + ] + } + ], + "unresolved_boundaries": [], + "path_encoding": "filesystem-bytes-hex", + "line_correspondences": [ + { + "base_path": "6170692e6a73", + "head_path": "6170692e6a73", + "base_start_line": 1, + "base_end_line": 1, + "head_start_line": 1 + }, + { + "base_path": "6170692e6a73", + "head_path": "6170692e6a73", + "base_start_line": 2, + "base_end_line": 2, + "head_start_line": 2 + }, + { + "base_path": "6170692e6a73", + "head_path": "6170692e6a73", + "base_start_line": 4, + "base_end_line": 4, + "head_start_line": 3 + }, + { + "base_path": "6170692e6a73", + "head_path": "6170692e6a73", + "base_start_line": 5, + "base_end_line": 5, + "head_start_line": 4 + } + ] + }, + "scope": { + "ranker_version": "existing-static-evidence-v1", + "ranking_mode": "evidence", + "population_complete": true, + "selected": [ + { + "identity": { + "unit": "context-smoke", + "language": "javascript", + "path": "api.js", + "function": "handler", + "family": "injection" + }, + "priority": 0.0, + "evidence": [ + "static_rank=1.0", + "normalized_evidence={\"access_declared_public\":true,\"bound_names\":[\"execute\",\"query\"],\"carried\":false,\"carried_weak\":false,\"entry\":true,\"family_in_repo\":true,\"shipped\":true,\"sinks\":[{\"arg0_literal\":false,\"arity\":1,\"cleansed_on_call\":false,\"code\":\"eval(value)\",\"line\":1,\"method\":\"helper\"}],\"source_local\":false,\"source_near\":false}", + "tier=4", + "score=2.5", + "change_sites={\"base_revision\":\"14e9c2e283b5582031844e8aa119226d28e4b90a\",\"changed_paths\":[\"6170692e6a73\"],\"declaration_paths\":[],\"deleted_paths\":[],\"head_revision\":\"0dab75fe2b6096f66b50eae27db92490f36e7443\",\"path_encoding\":\"filesystem-bytes-hex\"}", + "affected_relationship={\"evidence\":[\"base_span:api.js:3-3\",\"unchanged_line:2;lexical_only\",\"unchanged_line:3;lexical_only\",\"unchanged_line:4;lexical_only\"],\"kind\":\"entry\",\"source\":{\"family\":\"injection\",\"function\":\"handler\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-smoke\"},\"target\":{\"family\":\"injection\",\"function\":\"handler\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-smoke\"}}" + ], + "tier": 4, + "score": 2.5 + } + ], + "deferred": [], + "unresolved_boundaries": [] + }, + "question_outcomes": [ + { + "identity": { + "unit": "context-smoke", + "language": "javascript", + "path": "api.js", + "function": "handler", + "family": "injection" + }, + "status": "completed", + "reason": "query_answered_and_arbitrated", + "raw_rows_json": "[{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":4,\"paths\":1,\"sanitized\":false,\"sink\":\"eval(value)\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"helper.js\",\"sinkLine\":\"1\",\"sinkMethod\":\"helper\",\"source\":\"req\",\"sourceKind\":\"parameter\"}]" + } + ], + "family_coverage": [ + { + "unit": "context-smoke", + "language": "javascript", + "family": "injection", + "selected": 1, + "completed": 1, + "unanswered": 0, + "deferred": 0, + "unsupported": 0 + } + ] + } + } + } + }, + "python_census_runtime": { + "purpose": "Real Python batch schema and query compilation; no security property admission claim", + "source_bytes": 37, + "source_sha256": "a1d637400a93a180d8fc1fc3a174ff5205cbc83fb0877ce68fcd28f25b9e5d88", + "shared_budget_seconds": 600, + "censuses": { + "taint": [ + { + "methods": "3", + "files": "2", + "file_names": [ + "probe.py", + "N/A" + ] + } + ], + "dominance": [ + { + "methods": "3", + "files": "2", + "file_names": [ + "probe.py", + "N/A" + ] + } + ], + "config": [ + { + "methods": "3", + "files": "2", + "file_names": [ + "probe.py", + "N/A" + ] + } + ] + } + }, + "python_census_provenance": { + "image_id": "sha256:73d312065b9b47a2a50f94deb76b038150acd5db1dfb3ba2525a3e4a70106c31", + "query_hashes": { + "cpg/queries/taint.sc": "dbc8e7c60463dc4b68db8e15537d2fafb7d429597a0c9789f4b95b0cfb0e14bb", + "cpg/queries/dominance.sc": "0623d84ee35d6dc06f577ebfc0bcfaa591dafe0287515b16c7d6b5c9a29ac290", + "cpg/queries/config.sc": "f0cd5519a6a246d4159d1141353c1a85a82b610c8c717f43132bc52d24818b0b" + }, + "query_sources_match_final": true, + "scope": "Python direct query path; unchanged Scala census scripts, before separate PHP aggregation and discovery-error repairs." + }, + "permission_regression": { + "purpose": "Real filesystem permission/error-path check; graph callback is a synthetic control, not engine validation", + "source_bytes_read": [ + 42 + ], + "unreadable_open_refused": true, + "outcome": "unresolved", + "boundaries": [ + "population_not_asserted_complete", + "evidence_unknown:cb309657f3abedd2dc5989df224b8121797e6bfa57dd2e24b1e5f4a33b156f18", + "source_unreadable" + ] + }, + "review_repairs": { + "unreadable_discovery_red": "F [100%]\n=================================== FAILURES ===================================\n____ test_unreadable_unknown_extension_reports_incomplete_without_crashing _____\n\ntmp_path = PosixPath('/tmp/pytest-of-mc/pytest-1535/test_unreadable_unknown_extens0')\nmonkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7eae6eb96510>\n\n def test_unreadable_unknown_extension_reports_incomplete_without_crashing(tmp_path, monkeypatch):\n from openultrasast.model import partitions\n \n (tmp_path / \"app.js\").write_bytes(b\"function handler(value) { return value; }\\n\")\n (tmp_path / \"README.md\").write_bytes(b\"unreadable declaration candidate\\n\")\n original_open = partitions.os.open\n \n def unreadable_open(path, flags, *args, **kwargs):\n if str(path) == \"README.md\":\n raise PermissionError(\"controlled unreadable discovery input\")\n return original_open(path, flags, *args, **kwargs)\n \n monkeypatch.setattr(partitions.os, \"open\", unreadable_open)\n \n class Backend:\n def build(self, root, **kwargs):\n assert (root / \"app.js\").read_bytes()\n return CpgResult(Path(\"graph\"), lambda query, params: [])\n \n region = ScanRegion(\"app.js\", \"handler\", \"javascript\", (\"injection\",), 1.0, \"entry_point\")\n> result = scan_repository(tmp_path, [region], backend=Backend(), budget=ScanBudget(tiering=False))\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\ntests/test_model_partitions.py:242: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nsrc/openultrasast/model/scan.py:172: in scan_repository\n result = _scan_repository_impl(\nsrc/openultrasast/model/scan.py:234: in _scan_repository_impl\n cpg = build_partitions(\nsrc/openultrasast/model/partitions.py:184: in build_partitions\n fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory_fd)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\npath = 'README.md', flags = 133120, args = (), kwargs = {'dir_fd': 13}\n\n def unreadable_open(path, flags, *args, **kwargs):\n if str(path) == \"README.md\":\n> raise PermissionError(\"controlled unreadable discovery input\")\nE PermissionError: controlled unreadable discovery input\n\ntests/test_model_partitions.py:231: PermissionError\n=========================== short test summary info ============================\nFAILED tests/test_model_partitions.py::test_unreadable_unknown_extension_reports_incomplete_without_crashing\n1 failed, 13 deselected in 0.43s\n", + "census_transport_red": "FF... [100%]\n=================================== FAILURES ===================================\n_____ test_shard_aggregation_preserves_complete_named_census[shard_names0] _____\n\nmonkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x76f7e2cbe990>\nshard_names = ('app.php',)\n\n @pytest.mark.parametrize(\"shard_names\", [(\"app.php\",), (\"app.php\", \"helper.php\")])\n def test_shard_aggregation_preserves_complete_named_census(monkeypatch, shard_names):\n from openultrasast.cpg.backend import JoernBackend\n \n def batch(self, path, query, requests):\n return {\"__census__\": [{\"methods\": \"2\", \"files\": \"2\", \"file_names\": [\"\", path.name]}]}\n \n monkeypatch.setattr(JoernBackend, \"query_batch\", batch)\n result = JoernBackend().query_batch_across([Path(name) for name in shard_names], \"taint\", {})\n assert result is not None\n> assert set(result[\"__census__\"][0][\"file_names\"]) == {\"\", *shard_names}\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nE KeyError: 'file_names'\n\ntests/test_cpg_backend.py:969: KeyError\n_____ test_shard_aggregation_preserves_complete_named_census[shard_names1] _____\n\nmonkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x76f7e2c938d0>\nshard_names = ('app.php', 'helper.php')\n\n @pytest.mark.parametrize(\"shard_names\", [(\"app.php\",), (\"app.php\", \"helper.php\")])\n def test_shard_aggregation_preserves_complete_named_census(monkeypatch, shard_names):\n from openultrasast.cpg.backend import JoernBackend\n \n def batch(self, path, query, requests):\n return {\"__census__\": [{\"methods\": \"2\", \"files\": \"2\", \"file_names\": [\"\", path.name]}]}\n \n monkeypatch.setattr(JoernBackend, \"query_batch\", batch)\n result = JoernBackend().query_batch_across([Path(name) for name in shard_names], \"taint\", {})\n assert result is not None\n> assert set(result[\"__census__\"][0][\"file_names\"]) == {\"\", *shard_names}\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nE KeyError: 'file_names'\n\ntests/test_cpg_backend.py:969: KeyError\n=========================== short test summary info ============================\nFAILED tests/test_cpg_backend.py::test_shard_aggregation_preserves_complete_named_census[shard_names0]\nFAILED tests/test_cpg_backend.py::test_shard_aggregation_preserves_complete_named_census[shard_names1]\n2 failed, 3 passed, 41 deselected in 0.33s\n", + "prior_transport_symptom": [ + { + "stage": "model", + "reason": "cpg_empty" + }, + { + "stage": "model", + "reason": "cpg_empty", + "files": 0 + }, + { + "stage": "model", + "reason": "partition_file_census_unavailable" + } + ] + }, + "pytest": { + "passed": 1151, + "skipped": 9, + "seconds": 100.55, + "exit_code": 0 + }, + "static_checks": "Ruff,format230,mypy109,diffcheck pass", + "regression_gates": "detection44/47 FP0/44; split-sink map PASS; localpairs3/3 labeled28/28 fixleaks0", + "limits": [ + "Declared entry regions; not automatic mapper discovery or admission proof.", + "Vendor and cross-partition semantics remain conservatively unresolved.", + "Runtime origin unspecified without declarations; TypeScript property and C bounds claims not admitted.", + "Lab budget and timings do not establish pre-push latency; no persistent graph reuse implemented." + ] +} diff --git a/ops/README.md b/ops/README.md index be10aba..1fc2e65 100644 --- a/ops/README.md +++ b/ops/README.md @@ -181,8 +181,19 @@ unsupported Go partition is reported explicitly. One ranker decision and one reg limit cover both supported frontends. The shared 900-second budget is a lab allowance; this check establishes neither production hook latency nor framework admission. -Current status (2026-09-13): this is a failing regression on Joern 4.0.625. The -projected JavaScript test file is readable but absent from the resulting graph. -See `benchmarks/measurements/2026-09-13-partition-retention-blocker.json` and the -proposed contributor-scan frontend-retention prerequisite. An exit 1 is expected -until that engine contract is repaired; do not treat it as a passing partition smoke. +Stock Joern 4.0.625 fails the retention check: the projected test file is readable +but absent from the graph. The approved source adaptation fixes both test filters; +see `frontend-retention/README.md` for build pins and inclusion policy, and +`benchmarks/measurements/2026-09-13-frontend-retention-smoke.json` for passing +foundation verification. The earlier failing evidence remains in +`benchmarks/measurements/2026-09-13-partition-retention-blocker.json`. + +Setting `OUSAST_SMOKE_INCLUDE_TESTS=0` in the container explicitly disables the +backend inclusion policy; that negative control must fail the retention assertion. +The expanded task 3.4 smoke also verifies first-party browser-path retention and +unshipped test-path records. Runtime origin remains unspecified without declarations. + +`ops/smoke_partition_census.py`, using the same container invocation, builds a small +readable Python graph and exercises the production taint, dominance and configuration +batch queries. Each must return the actual source filename in its census. This +checks query compilation and census transport, not security-property coverage. diff --git a/ops/smoke_context.py b/ops/smoke_context.py index 52165cc..b9382e9 100644 --- a/ops/smoke_context.py +++ b/ops/smoke_context.py @@ -95,6 +95,10 @@ def main() -> None: enriched = result.change_context assert enriched is not None and enriched.relationships, "changed context did not reach any question" assert result.scope is not None and result.scope.selected + invalid_census = {"cpg_empty", "files_unparsed", "partition_file_census_unavailable", "partition_file_census_incomplete"} + assert not invalid_census.intersection(result.scope.unresolved_boundaries), result.scope + assert not any(d.get("reason") in invalid_census for d in result.degradations), result.degradations + assert result.question_outcomes and all(outcome.status == "completed" for outcome in result.question_outcomes), result selected = {question.identity for question in result.scope.selected} assert any(relation.target in selected for relation in enriched.relationships) assert result.findings, "controlled unchanged sink has no witness" diff --git a/ops/smoke_partition_census.py b/ops/smoke_partition_census.py new file mode 100644 index 0000000..f536606 --- /dev/null +++ b/ops/smoke_partition_census.py @@ -0,0 +1,54 @@ +"""Compile all production batch censuses on a readable real Python graph.""" + +from __future__ import annotations + +import hashlib +import json +import tempfile +import time +from pathlib import Path + +from openultrasast.cpg.backend import JoernBackend +from openultrasast.model.contracts import ExecutionBudget + + +def main() -> None: + budget = ExecutionBudget(time.monotonic() + 600, 2.0) + results = {} + with tempfile.TemporaryDirectory(prefix="ousast-census-smoke-") as temporary: + root = Path(temporary) + source = root / "probe.py" + source.write_text("def handler(value):\n return value\n") + data = source.read_bytes() + assert data, "source input unreadable" + backend = JoernBackend(heap_mb=1024, execution_budget=budget) + graph = backend.build(root, language="python") + assert graph is not None and graph.run_batch is not None, "real Python graph unavailable" + try: + for kind in ("taint", "dominance", "config"): + answer = graph.run_batch(kind, {"probe": {"file": "probe.py", "function": "handler"}}) + assert isinstance(answer, dict) and "probe" in answer, f"{kind} batch unanswered" + census = answer.get("__census__") + assert isinstance(census, list) and census, f"{kind} census missing" + names = census[0].get("file_names") + assert isinstance(names, list) and any(str(p).endswith("probe.py") for p in names), census + assert int(census[0]["methods"]) > 0, census + results[kind] = census + finally: + graph.cleanup() + print( + json.dumps( + { + "purpose": "Real Python batch schema and query compilation; no security property admission claim", + "source_bytes": len(data), + "source_sha256": hashlib.sha256(data).hexdigest(), + "shared_budget_seconds": 600, + "censuses": results, + }, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/ops/smoke_partitions.py b/ops/smoke_partitions.py index f457957..da53714 100644 --- a/ops/smoke_partitions.py +++ b/ops/smoke_partitions.py @@ -39,6 +39,7 @@ def main() -> None: sources = { "src/api.php": " None: assert any(str(filename).endswith(path) for filename in files), witness assert method in methods, witness assert "php_handler" not in methods, witness + assert any(str(path).endswith("browser/client.js") for path in files), witness + assert "browser_marker" in methods, witness else: raise AssertionError(f"unexpected frontend: {language}") records.append({"frontend": language, "input_root": str(root), "graph_witness": witness}) @@ -116,12 +119,20 @@ def main() -> None: ) assert {record["frontend"] for record in records} == {"php", "javascript"}, records assert result.scope is not None + invalid_census = {"cpg_empty", "files_unparsed", "partition_file_census_unavailable", "partition_file_census_incomplete"} + assert not invalid_census.intersection(result.scope.unresolved_boundaries), result.scope + assert not any(d.get("reason") in invalid_census for d in result.degradations), result.degradations assert [q.identity.function for q in result.scope.selected] == ["php_handler"], result.scope assert {q.identity.function for q in result.scope.deferred} == {"js_handler", "php_test_marker", "js_test_marker"}, result.scope assert len(result.question_outcomes) == 1, result.question_outcomes assert result.question_outcomes[0].identity == result.scope.selected[0].identity assert all("vendor" not in q.identity.path and "node_modules" not in q.identity.path for q in result.scope.selected), result.scope assert any(p.language == "go" and p.status == "unsupported" for p in result.partitions), result.partitions + javascript = next(p for p in result.partitions if p.language == "javascript") + assert set(javascript.unshipped_paths) == {"tests/probe.js", *suffix_tests}, javascript + assert javascript.runtime == "unspecified", "runtime role needs declared evidence" + php = next(p for p in result.partitions if p.language == "php") + assert php.unshipped_paths == ("tests/probe.php",), php assert all(not Path(record["input_root"]).exists() for record in records), "partition scratch leaked" report = { "purpose": "Real mixed PHP/JavaScript graph census and one global ranker budget; no admission or latency claim", diff --git a/src/openultrasast/cli.py b/src/openultrasast/cli.py index 6cf374e..8b9f2a6 100644 --- a/src/openultrasast/cli.py +++ b/src/openultrasast/cli.py @@ -4,7 +4,7 @@ import argparse import json import tempfile from collections.abc import Mapping, Sequence -from dataclasses import dataclass, replace +from dataclasses import asdict, dataclass, replace from pathlib import Path from time import perf_counter from typing import TYPE_CHECKING, Any @@ -903,6 +903,7 @@ def _model_payload( "scope": scope.to_payload() if scope is not None else None, "question_outcomes": [q.to_payload() for q in outcomes], "family_coverage": [c.to_payload() for c in getattr(result, "family_coverage", ())], + "partitions": [asdict(p) for p in getattr(result, "partitions", ())], "by_rung": dict(getattr(result, "by_rung", {}) or {}), "regions_scanned": getattr(result, "regions_scanned", 0), "regions_unjudged": getattr(result, "regions_unjudged", 0), diff --git a/src/openultrasast/cpg/backend.py b/src/openultrasast/cpg/backend.py index 6b8088f..1504164 100644 --- a/src/openultrasast/cpg/backend.py +++ b/src/openultrasast/cpg/backend.py @@ -697,9 +697,12 @@ class JoernBackend: methods = 0 files = 0 answered = 0 + file_names: set[str] = set() + named_census = True for shard in shards: one = self.query_batch(shard, query, requests) if one is None: + named_census = False if self.execution_budget is not None: self._note("shard_query_incomplete") return None @@ -709,11 +712,21 @@ class JoernBackend: if census and isinstance(census[0], Mapping): methods += int(str(census[0].get("methods", "0")) or 0) files += int(str(census[0].get("files", "0")) or 0) + names = census[0].get("file_names") + if isinstance(names, list) and all(isinstance(name, str) for name in names): + file_names.update(names) + else: + named_census = False + else: + named_census = False for rid, rows in one.items(): merged.setdefault(rid, []).extend(rows) if not answered: return None - merged["__census__"] = [{"methods": str(methods), "files": str(files), "shards": str(len(shards))}] + total: dict[str, object] = {"methods": str(methods), "files": str(files), "shards": str(len(shards))} + if named_census: + total["file_names"] = sorted(file_names) + merged["__census__"] = [total] return merged def _apply_overlays(self, cpg_path: Path, scratch: Path) -> bool: diff --git a/src/openultrasast/cpg/queries/config.sc b/src/openultrasast/cpg/queries/config.sc index 44a1a7b..dd022a7 100644 --- a/src/openultrasast/cpg/queries/config.sc +++ b/src/openultrasast/cpg/queries/config.sc @@ -78,7 +78,7 @@ // per-file warnings -- so "no rows" and "no graph" are indistinguishable to the driver without this. // It rides the batch rather than costing its own invocation, because JVM startup is what this whole // batching design exists to avoid. - val census = ujson.Arr(ujson.Obj("methods" -> cpg.method.size.toString, "files" -> cpg.file.size.toString)) + val census = ujson.Arr(ujson.Obj("methods" -> cpg.method.size.toString, "files" -> cpg.file.size.toString, "file_names" -> ujson.Arr.from(cpg.file.name.l))) println(ujson.write(ujson.Obj.from(answers.toSeq :+ ("__census__" -> census)))) } else { println(ujson.write(ujson.Arr(rowsFor(settings, function, file): _*))) diff --git a/src/openultrasast/cpg/queries/dominance.sc b/src/openultrasast/cpg/queries/dominance.sc index 250dfe1..7c66bbc 100644 --- a/src/openultrasast/cpg/queries/dominance.sc +++ b/src/openultrasast/cpg/queries/dominance.sc @@ -190,7 +190,7 @@ // per-file warnings -- so "no rows" and "no graph" are indistinguishable to the driver without this. // It rides the batch rather than costing its own invocation, because JVM startup is what this whole // batching design exists to avoid. - val census = ujson.Arr(ujson.Obj("methods" -> cpg.method.size.toString, "files" -> cpg.file.size.toString)) + val census = ujson.Arr(ujson.Obj("methods" -> cpg.method.size.toString, "files" -> cpg.file.size.toString, "file_names" -> ujson.Arr.from(cpg.file.name.l))) println(ujson.write(ujson.Obj.from(answers.toSeq :+ ("__census__" -> census)))) } else { println(ujson.write(ujson.Arr(rowsFor(operations, dischargers, function, file, operationRequires, dischargersByKind): _*))) diff --git a/src/openultrasast/cpg/queries/taint.sc b/src/openultrasast/cpg/queries/taint.sc index 8e58c53..705a67a 100644 --- a/src/openultrasast/cpg/queries/taint.sc +++ b/src/openultrasast/cpg/queries/taint.sc @@ -852,7 +852,7 @@ // per-file warnings -- so "no rows" and "no graph" are indistinguishable to the driver without this. // It rides the batch rather than costing its own invocation, because JVM startup is what this whole // batching design exists to avoid. - val census = ujson.Arr(ujson.Obj("methods" -> cpg.method.size.toString, "files" -> cpg.file.size.toString)) + val census = ujson.Arr(ujson.Obj("methods" -> cpg.method.size.toString, "files" -> cpg.file.size.toString, "file_names" -> ujson.Arr.from(cpg.file.name.l))) println(ujson.write(ujson.Obj.from(answers.toSeq :+ ("__census__" -> census)))) } else { println( diff --git a/src/openultrasast/model/layout.py b/src/openultrasast/model/layout.py index 9a7b89c..c67488b 100644 --- a/src/openultrasast/model/layout.py +++ b/src/openultrasast/model/layout.py @@ -26,7 +26,12 @@ def _matches(relative: str, patterns: Iterable[str]) -> bool: parts = relative.split("/") for pattern in patterns: if pattern.endswith("/"): - if pattern.rstrip("/") in parts[:-1] or pattern.rstrip("/") in parts: + directory = pattern.rstrip("/") + if any( + fnmatch.fnmatch("/".join(parts[start:end]), directory) + for start in range(len(parts)) + for end in range(start + 1, len(parts) + 1) + ): return True elif fnmatch.fnmatch(relative, pattern) or fnmatch.fnmatch(parts[-1], pattern): return True diff --git a/src/openultrasast/model/partitions.py b/src/openultrasast/model/partitions.py new file mode 100644 index 0000000..418bfdd --- /dev/null +++ b/src/openultrasast/model/partitions.py @@ -0,0 +1,279 @@ +"""Physical first-party frontend inputs and routing; selection remains in the scan ranker.""" + +from __future__ import annotations + +import os +import stat +import tempfile +import time +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from ..cpg.backend import _DeadlineExpired, _remove_owned_tree +from ..preprocess import LANGUAGE_BY_EXTENSION +from .contracts import ExecutionBudget +from .layout import is_test_path, is_vendored, layout_facts + +FRONTENDS = { + "php": "php", + "javascript": "javascript", + "typescript": "typescript", + "python": "python", + "java": "java", + "c": "c", + "cpp": "c_cpp", +} + + +@dataclass(frozen=True) +class PartitionCoverage: + language: str + frontend: str | None + paths: tuple[str, ...] + source_bytes: int + status: str + boundaries: tuple[str, ...] = () + runtime: str = "unspecified" + excluded_paths: tuple[str, ...] = () + unshipped_paths: tuple[str, ...] = () + + +class PartitionGraph: + """Routes existing requests without reordering or allocating another scan budget.""" + + def __init__(self, budget: ExecutionBudget | None = None) -> None: + self.budget = budget + self.diagnostics: list[str] = [] + self.graphs: dict[str, Any] = {} + self.roots: dict[str, Path] = {} + self.path_languages: dict[str, str] = {} + self.partitions: tuple[PartitionCoverage, ...] = () + self.boundaries: tuple[str, ...] = () + self.unparsed: tuple[str, ...] = () + self.cpg_path = Path("partitioned-cpg") + + def cleanup(self) -> None: + try: + for graph in self.graphs.values(): + cleanup = getattr(graph, "cleanup", None) + if callable(cleanup): + cleanup() + finally: + for root in self.roots.values(): + deadline = self.budget.deadline_monotonic + self.budget.cancellation_allowance_seconds if self.budget else float("inf") + try: + _remove_owned_tree(root, deadline) + except (OSError, _DeadlineExpired): + self.diagnostics.append("scratch_cleanup_incomplete") + + def execution_diagnostics(self) -> tuple[str, ...]: + reasons = list(self.diagnostics) + for graph in self.graphs.values(): + read = getattr(graph, "execution_diagnostics", None) + if callable(read): + reasons.extend(read()) + return tuple(reasons) + + def _normalize(self, value: Any) -> Any: + if isinstance(value, str): + for root in self.roots.values(): + value = value.replace(str(root) + "/", "") + return value + if isinstance(value, list): + return [self._normalize(v) for v in value] + if isinstance(value, dict): + return {k: self._normalize(v) for k, v in value.items()} + return value + + def run(self, kind: str, params: Mapping[str, object]) -> list[object] | None: + return self.run_batch(kind, {"one": params}).get("one") + + def run_batch(self, kind: str, requests: Mapping[str, Mapping[str, object]]) -> dict[str, list[object]]: + answers: dict[str, list[object]] = {} + languages = dict.fromkeys(self.path_languages.get(str(params.get("file", "")), "") for params in requests.values()) + for language in languages: + graph = self.graphs.get(language) + if graph is None: + continue + if self.budget is not None and time.monotonic() >= self.budget.deadline_monotonic: + break + selected = {rid: params for rid, params in requests.items() if self.path_languages.get(str(params.get("file", ""))) == language} + if not selected: + continue + batch = getattr(graph, "run_batch", None) + raw = batch(kind, selected) if callable(batch) else {rid: graph.run(kind, params) for rid, params in selected.items()} + if not isinstance(raw, Mapping): + continue + for rid, rows in raw.items(): + if rid in selected and isinstance(rows, list): + answers[rid] = self._normalize(rows) + elif rid == "__census__" and isinstance(rows, list): + expected = set(next(p.paths for p in self.partitions if p.language == language)) + normalized = self._normalize(rows) + names = [r.get("file_names") for r in normalized if isinstance(r, Mapping)] + named = bool(names) and all(isinstance(n, list) and all(isinstance(p, str) for p in n) for n in names) + observed = {p.removeprefix("./") for n in names if isinstance(n, list) for p in n if isinstance(p, str)} + if not named or not expected <= observed: + self.diagnostics.append("partition_file_census_incomplete" if named else "partition_file_census_unavailable") + rows = [ + { + "methods": 0, + "files": 0, + "shards": max((int(str(r.get("shards", 1))) for r in rows if isinstance(r, Mapping)), default=1), + } + ] + # Preserve any individual empty or sharded graph's degradation. + previous = answers.get(rid, []) + if not previous or any( + isinstance(r, Mapping) and (int(str(r.get("methods", 0))) == 0 or int(str(r.get("shards", 1))) > 1) for r in rows + ): + answers[rid] = rows + return answers + + +def build_partitions( + root: Path, build: Callable[[Path, str], Any], budget: ExecutionBudget | None, declared: Mapping[str, tuple[str, ...]] | None = None +) -> PartitionGraph: + graph = PartitionGraph(budget) + paths: dict[str, list[Path]] = {} + boundaries: set[str] = set() + excluded: set[str] = set() + facts = layout_facts() + root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + for directory, dirs, files, directory_fd in os.fwalk( + ".", follow_symlinks=False, dir_fd=root_fd, onerror=lambda error: boundaries.add("source_unreadable") + ): + if budget is not None and time.monotonic() >= budget.deadline_monotonic: + boundaries.add("deadline_exhausted") + break + base = root / directory + for name in list(dirs): + if budget is not None and time.monotonic() >= budget.deadline_monotonic: + boundaries.add("deadline_exhausted") + dirs.clear() + break + path = base / name + relative = path.relative_to(root).as_posix() + if name in {".git", ".hg", ".svn", ".openultrasast", ".venv", "venv", "__pycache__"}: + excluded.add(relative + "/") + dirs.remove(name) + continue + if path.is_symlink() or is_vendored(relative + "/", facts): + excluded.add(relative + "/") + boundaries.add("symlink_context_unresolved" if path.is_symlink() else "vendor_semantics_unresolved") + dirs.remove(name) + for name in files: + if budget is not None and time.monotonic() >= budget.deadline_monotonic: + boundaries.add("deadline_exhausted") + break + path = base / name + relative = path.relative_to(root).as_posix() + if is_vendored(relative, facts): + excluded.add(relative) + boundaries.add("vendor_semantics_unresolved") + continue + if path.is_symlink() or not path.is_file(): + excluded.add(relative) + boundaries.add("symlink_context_unresolved") + continue + language = LANGUAGE_BY_EXTENSION.get(path.suffix.lower(), "unknown") + if language == "unknown": + try: + fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory_fd) + with os.fdopen(fd, "rb") as handle: + first = handle.readline(4096).lower() + except OSError: + boundaries.add("source_unreadable") + continue + if first.startswith(b"#!"): + language = "python" if b"python" in first else "javascript" if b"node" in first else "unknown" + languages = (language,) if language != "unknown" else (declared or {}).get(relative, ()) + for item in languages: + paths.setdefault(item, []).append(path) + if len(languages) == 1: + graph.path_languages[relative] = languages[0] + elif languages: + boundaries.add("ambiguous_frontend_path") + if len(paths) > 1: + boundaries.add("cross_partition_semantics_unresolved") + records: list[PartitionCoverage] = [] + for language, sources in sorted(paths.items()): + relative_paths = tuple(sorted(p.relative_to(root).as_posix() for p in sources)) + frontend = FRONTENDS.get(language) + if frontend is None: + records.append(PartitionCoverage(language, None, relative_paths, 0, "unsupported", ("frontend_unsupported",))) + boundaries.add("frontend_unsupported") + continue + scratch = Path(tempfile.mkdtemp(prefix="ousast-input-")) + graph.roots[language] = scratch + size = 0 + complete = True + for source in sources: + if budget is not None and time.monotonic() >= budget.deadline_monotonic: + complete = False + break + target = scratch / source.relative_to(root) + target.parent.mkdir(parents=True, exist_ok=True) + # A no-follow open prevents file symlink substitution between census and read. + try: + fd = _open_source(root_fd, source.relative_to(root).parts) + except OSError: + boundaries.add("source_unreadable") + complete = False + break + with os.fdopen(fd, "rb") as handle, target.open("wb") as output: + while chunk := handle.read(1024 * 1024): + if budget is not None and time.monotonic() >= budget.deadline_monotonic: + complete = False + break + output.write(chunk) + size += len(chunk) + if not complete: + break + built = build(scratch, frontend) if complete else None + if built is not None: + graph.graphs[language] = built + graph.unparsed += tuple(graph._normalize(list(getattr(built, "unparsed", ())))) + record_limits: tuple[str, ...] = ("runtime_unspecified",) if language in {"javascript", "typescript"} else () + if language == "typescript": + record_limits += ("typescript_property_support_unvalidated",) + boundaries.add("typescript_property_support_unvalidated") + if language in {"c", "cpp"}: + record_limits += ("c_bounds_arithmetic_unmodeled",) + records.append( + PartitionCoverage(language, frontend, relative_paths, size, "built" if built is not None else "build_failed", record_limits) + ) + from dataclasses import replace + + graph.partitions = tuple( + replace( + record, excluded_paths=tuple(sorted(excluded)), unshipped_paths=tuple(p for p in record.paths if is_test_path(p, facts)) + ) + for record in records + ) + graph.boundaries = tuple(sorted(boundaries)) + return graph + except BaseException: + graph.cleanup() + raise + finally: + os.close(root_fd) + + +def _open_source(root_fd: int, parts: tuple[str, ...]) -> int: + descriptor = os.dup(root_fd) + try: + for component in parts[:-1]: + child = os.open(component, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=descriptor) + os.close(descriptor) + descriptor = child + result = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=descriptor) + if not stat.S_ISREG(os.fstat(result).st_mode): + os.close(result) + raise OSError("source is not a regular file") + return result + finally: + os.close(descriptor) diff --git a/src/openultrasast/model/scan.py b/src/openultrasast/model/scan.py index 42f9355..dbc9ba3 100644 --- a/src/openultrasast/model/scan.py +++ b/src/openultrasast/model/scan.py @@ -43,7 +43,8 @@ from .contracts import ( from .dominance import request_params as dominance_params from .evidence import TIER_EXCLUDE, Evidence, evidence_from_rows from .ladder import Rung -from .layout import with_layout +from .layout import is_vendored, layout_facts, with_layout +from .partitions import PartitionCoverage, build_partitions from .pipeline import ModelFinding, scan_region from .regions import MODULE_SCOPE, ScanRegion, affected_context from .specs import ConfigSpec, DominanceSpec, TaintSpec, config_specs, dominance_specs, taint_specs @@ -126,6 +127,7 @@ class ModelScanResult: # could have been found was skipped. Reported so the request count is legible. requests_pruned: int = 0 + partitions: tuple[PartitionCoverage, ...] = () change_context: ChangeContext | None = None scope: ScopeDecision | None = None question_outcomes: tuple[QuestionOutcome, ...] = () @@ -158,6 +160,8 @@ def scan_repository( if ranking_mode is not None: limits = replace(limits, order_by_evidence=ranking_mode == "evidence") mode = "evidence" if limits.order_by_evidence else "static" + layout = layout_facts() + regions = tuple(r for r in regions if not is_vendored(r.path, layout)) # Reject duplicate canonical questions before any engine work or resource ownership. identities = [_identity(unit, r, f) for r in regions for f in r.families] if len(set(identities)) != len(identities): @@ -223,13 +227,29 @@ def _scan_repository_impl( for region in regions: counts[region.language] = counts.get(region.language, 0) + 1 dominant = max(counts, key=lambda name: (counts[name], name)) if counts else "" - cpg = _build(backend, root, dominant, execution_budget=execution_budget) + declarations: dict[str, set[str]] = {} + for region in regions: + declarations.setdefault(region.path, set()).add(region.language) + if root.is_dir(): + cpg = build_partitions( + root, + lambda source, language: _build(backend, source, language, execution_budget=execution_budget), + execution_budget, + {path: tuple(sorted(languages)) for path, languages in declarations.items()}, + ) + degradations.extend({"stage": "model", "reason": reason} for reason in cpg.boundaries) + else: + cpg = _build(backend, root, dominant, execution_budget=execution_budget) build_seconds = round(time.monotonic() - build_started, 2) - if cpg is None: + if cpg is None or (hasattr(cpg, "graphs") and not cpg.graphs): + if cpg is not None: + owned.append(cpg) scope, _ = _scope_work( regions, {}, limits, unit, ranking_mode, population_complete, failure="cpg_build_failed", change_context=change_context ) + scope = replace(scope, unresolved_boundaries=tuple(dict.fromkeys((*scope.unresolved_boundaries, *getattr(cpg, "boundaries", ()))))) return ModelScanResult( + partitions=getattr(cpg, "partitions", ()), change_context=change_context, scope=scope, family_coverage=_family_coverage(scope, ()), @@ -322,7 +342,22 @@ def _scan_repository_impl( # Stable question IDs are the IDs sent to every execution request below. scope_limits = ( replace(limits, prune_tier0=False) - if any(d.get("reason") in {"files_unparsed", "cpg_empty", "cpg_sharded"} for d in degradations) + if any( + d.get("reason") + in { + "files_unparsed", + "cpg_empty", + "cpg_sharded", + "cross_partition_semantics_unresolved", + "vendor_semantics_unresolved", + "symlink_context_unresolved", + "frontend_unsupported", + "source_unreadable", + "ambiguous_frontend_path", + "typescript_property_support_unvalidated", + } + for d in degradations + ) else limits ) scope, work = _scope_work( @@ -457,16 +492,32 @@ def _scan_repository_impl( if unasked: degradations.append({"stage": "model", "reason": "budget_exhausted", "regions_unasked": len(unasked)}) - graph_gaps = tuple(str(d["reason"]) for d in degradations if d.get("reason") in {"files_unparsed", "cpg_empty", "cpg_sharded"}) + graph_gaps = tuple( + str(d["reason"]) + for d in degradations + if d.get("reason") + in { + "files_unparsed", + "cpg_empty", + "cpg_sharded", + "cross_partition_semantics_unresolved", + "vendor_semantics_unresolved", + "symlink_context_unresolved", + "frontend_unsupported", + "source_unreadable", + "ambiguous_frontend_path", + "typescript_property_support_unvalidated", + } + ) for question in scope.selected: rid = question.identity.question_id if rid not in outcomes: - answered = rid in rows_by_id + has_answer = rid in rows_by_id outcomes[rid] = QuestionOutcome( question.identity, - "not_arbitrated" if answered else "unanswered", + "not_arbitrated" if has_answer else "unanswered", "deadline_exhausted" if not _within_deadline(execution_budget) else "query_unanswered", - _rows_json(rows_by_id[rid]) if answered else None, + _rows_json(rows_by_id[rid]) if has_answer else None, ) if question.identity in context_gaps and outcomes[rid].status == "completed": outcomes[rid] = replace(outcomes[rid], status="unresolved", reason="change_context_incomplete") @@ -480,6 +531,7 @@ def _scan_repository_impl( # The graph has answered everything it is going to. Its scratch tree holds the CPG, the request files and # joern's own working copy -- tens of megabytes per scan -- and nothing else reclaims it. return ModelScanResult( + partitions=getattr(cpg, "partitions", ()), findings=findings, by_rung=_tally(findings), regions_scanned=scanned, diff --git a/src/openultrasast/preprocess.py b/src/openultrasast/preprocess.py index 04a7f62..fe0ca1e 100644 --- a/src/openultrasast/preprocess.py +++ b/src/openultrasast/preprocess.py @@ -97,7 +97,13 @@ def enumerate_source_files(root: Path) -> list[Path]: patterns = _load_ignore_patterns(root) paths: list[Path] = [] for path in root.rglob("*"): - if not path.is_file() or _has_ignored_dir(root, path) or _is_ignored(root, path, patterns) or _is_vendored(root, path): + if ( + path.is_symlink() + or not path.is_file() + or _has_ignored_dir(root, path) + or _is_ignored(root, path, patterns) + or _is_vendored(root, path) + ): continue if detect_language(path) != "unknown": paths.append(path) diff --git a/src/openultrasast/ruleset/semantic/javascript.toml b/src/openultrasast/ruleset/semantic/javascript.toml index 056fc7f..cf571ce 100644 --- a/src/openultrasast/ruleset/semantic/javascript.toml +++ b/src/openultrasast/ruleset/semantic/javascript.toml @@ -92,3 +92,18 @@ calls = ["path.normalize", "path.resolve"] # CWE-79. The standard encoders: the DOM/global URI encoders and the common escaping helpers. id = "html.escape" calls = ["encodeURIComponent", "encodeURI", "escapeHtml", "escape", "sanitizeHtml", "DOMPurify.sanitize"] + + +[[layout]] +# First-party test conventions are retained as graph context, ordered as unshipped. +# These declarations cover JS module/JSX and TS/TSX filenames; they do not assert +# TypeScript property support or a browser/server runtime classification. +id = "javascript_first_party_tests" +vendored = ["node_modules/"] +tests = [ + "tests/", "test/", "spec/", "__tests__/", "__mocks__/", "e2e/", + "*.test.js", "*.test.mjs", "*.test.cjs", "*.test.jsx", "*.test.ts", "*.test.mts", "*.test.cts", "*.test.tsx", + "*.spec.js", "*.spec.mjs", "*.spec.cjs", "*.spec.jsx", "*.spec.ts", "*.spec.mts", "*.spec.cts", "*.spec.tsx", + "*.mock.js", "*.mock.mjs", "*.mock.cjs", "*.mock.jsx", "*.mock.ts", "*.mock.mts", "*.mock.cts", "*.mock.tsx", + "*.e2e.js", "*.e2e.mjs", "*.e2e.cjs", "*.e2e.jsx", "*.e2e.ts", "*.e2e.mts", "*.e2e.cts", "*.e2e.tsx", +] diff --git a/tests/test_cpg_backend.py b/tests/test_cpg_backend.py index 639f85c..02b091e 100644 --- a/tests/test_cpg_backend.py +++ b/tests/test_cpg_backend.py @@ -956,6 +956,36 @@ def test_a_source_file_the_size_of_a_data_table_is_excluded_and_named(tmp_path: assert [Path(name).name for name in result.unparsed] == ["profiles.php"], "and it is reported, not silently dropped" +@pytest.mark.parametrize("shard_names", [("app.php",), ("app.php", "helper.php")]) +def test_shard_aggregation_preserves_complete_named_census(monkeypatch, shard_names): + from openultrasast.cpg.backend import JoernBackend + + def batch(self, path, query, requests): + return {"__census__": [{"methods": "2", "files": "2", "file_names": ["", path.name]}]} + + monkeypatch.setattr(JoernBackend, "query_batch", batch) + result = JoernBackend().query_batch_across([Path(name) for name in shard_names], "taint", {}) + assert result is not None + assert set(result["__census__"][0]["file_names"]) == {"", *shard_names} + assert result["__census__"][0]["shards"] == str(len(shard_names)) + + +@pytest.mark.parametrize("missing", ["names", "census", "answer"]) +def test_shard_aggregation_does_not_invent_complete_named_census(monkeypatch, missing): + from openultrasast.cpg.backend import JoernBackend + + def batch(self, path, query, requests): + if path.name == "missing": + return None if missing == "answer" else {} if missing == "census" else {"__census__": [{"methods": "2", "files": "2"}]} + return {"__census__": [{"methods": "2", "files": "2", "file_names": ["app.php", ""]}]} + + monkeypatch.setattr(JoernBackend, "query_batch", batch) + result = JoernBackend().query_batch_across([Path("app.php"), Path("missing")], "taint", {}) + assert result is not None + assert "file_names" not in result["__census__"][0] + assert result["__census__"][0]["shards"] == "2" + + def test_a_clean_build_whose_census_cannot_be_taken_is_a_failed_build(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """`cpg query census failed: timeout` was logged as a warning during a build whose every later query then timed out too. A graph that cannot answer the census inside the query timeout cannot answer a taint diff --git a/tests/test_cpg_budget.py b/tests/test_cpg_budget.py index 5ae98ac..ed5b06a 100644 --- a/tests/test_cpg_budget.py +++ b/tests/test_cpg_budget.py @@ -261,6 +261,7 @@ def test_cleanup_does_not_follow_substituted_directory_links(tmp_path: Path) -> def test_driver_propagates_budget_and_rejects_legacy_without_launch(tmp_path: Path) -> None: + (tmp_path / "input.py").write_bytes(b"print(1)\n") from openultrasast.cpg.backend import CpgResult from openultrasast.model.scan import scan_repository @@ -296,6 +297,7 @@ def test_driver_propagates_budget_and_rejects_legacy_without_launch(tmp_path: Pa def test_budgeted_driver_does_not_retry_internal_typeerror(tmp_path: Path) -> None: + (tmp_path / "input.py").write_bytes(b"print(1)\n") from openultrasast.model.scan import scan_repository class Backend: @@ -337,6 +339,7 @@ def test_cancelled_graph_session_does_not_poison_next_build(tmp_path: Path, monk def test_driver_cleans_graph_on_unexpected_exception(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + (tmp_path / "input.py").write_bytes(b"print(1)\n") from openultrasast.cpg.backend import CpgResult from openultrasast.model import scan diff --git a/tests/test_layout_facts.py b/tests/test_layout_facts.py index 71d4d6c..24005e8 100644 --- a/tests/test_layout_facts.py +++ b/tests/test_layout_facts.py @@ -65,11 +65,13 @@ def test_test_regions_are_not_product_and_order_last() -> None: ) -def test_the_scan_hands_the_vendored_trees_to_the_build(tmp_path: Path) -> None: +def test_the_scan_excludes_vendored_trees_before_the_build(tmp_path: Path) -> None: from openultrasast.model.regions import ScanRegion from openultrasast.model.scan import ScanBudget, scan_repository (tmp_path / "vendor").mkdir() + (tmp_path / "a.php").write_bytes(b" None: from openultrasast.cpg.backend import CpgResult seen["exclude"] = tuple(exclude) + seen["files"] = tuple(p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file()) return CpgResult(cpg_path=Path("cpg.bin"), run=lambda q, p: [], run_batch=lambda q, r: {rid: [] for rid in r}) region = ScanRegion(path="a.php", function="f", language="php", families=("injection",), rank=0.5, source="x") scan_repository(tmp_path, [region], backend=_Backend(), budget=ScanBudget(max_model_calls=0)) - assert seen["exclude"] == ("vendor",) + assert seen["exclude"] == () + assert seen["files"] == ("a.php",) diff --git a/tests/test_model_partitions.py b/tests/test_model_partitions.py new file mode 100644 index 0000000..9e07800 --- /dev/null +++ b/tests/test_model_partitions.py @@ -0,0 +1,337 @@ +from pathlib import Path + +from openultrasast.cpg.backend import CpgResult +from openultrasast.model.regions import ScanRegion +from openultrasast.model.scan import ScanBudget, scan_repository + + +def test_polyglot_graph_inputs_and_single_scope(tmp_path: Path) -> None: + sources = { + "api.php": "", str(root / "a.php")]}], + }, + ) + + graph = build_partitions(tmp_path, build, None) + try: + result = graph.run_batch("taint", {"q": {"file": "a.php"}}) + assert result["__census__"][0]["files"] == 0 + assert "partition_file_census_incomplete" in graph.execution_diagnostics() + finally: + graph.cleanup() + + +def test_named_census_accepts_exact_paths_for_every_query_kind(tmp_path): + from openultrasast.model.partitions import build_partitions + + (tmp_path / "app.php").write_text("", str(root / "app.php")]}], + }, + ) + + graph = build_partitions(tmp_path, build, None) + try: + for kind in ("taint", "dominance", "config"): + assert graph.run_batch(kind, {"q": {"file": "app.php"}})["__census__"][0]["files"] == 2 + assert not graph.execution_diagnostics() + finally: + graph.cleanup() + + +def test_unnamed_sharded_census_remains_explicitly_incomplete(tmp_path): + from openultrasast.model.partitions import build_partitions + + (tmp_path / "app.php").write_text(" None: assert result.findings == () -def test_a_region_cap_that_hides_most_of_a_repository_is_reported() -> None: +def test_a_region_cap_that_hides_most_of_a_repository_is_reported(tmp_path: Path) -> None: """Paid Memberships Pro yields 4,463 regions from 637 files. A default budget of 500 examines 11% of it, and a report that does not say so invites its silence to be read as a clean bill of health for the rest. """ @@ -414,14 +414,16 @@ def test_a_region_cap_that_hides_most_of_a_repository_is_reported() -> None: ScanRegion(path=f"f{n}.py", function="handler", language="python", families=("injection",), rank=0.5, source="entry_point") for n in range(30) ] - result = scan_repository(Path("."), regions, backend=_Backend(), budget=ScanBudget(max_model_calls=0, max_regions=10)) + for region in regions: + (tmp_path / region.path).write_bytes(b"print(1)\n") + result = scan_repository(tmp_path, regions, backend=_Backend(), budget=ScanBudget(max_model_calls=0, max_regions=10)) truncated = [d for d in result.degradations if d.get("reason") == "regions_truncated"] assert truncated, "the cap hid two thirds of the regions and said nothing" assert truncated[0]["examined"] == 10 and truncated[0]["total"] == 30 -def test_tier_zero_pairs_are_not_asked_and_nothing_else_is_skipped() -> None: +def test_tier_zero_pairs_are_not_asked_and_nothing_else_is_skipped(tmp_path: Path) -> None: """Phase 1 of flow-aware-ranking. A pair with no sink of its family in reach cannot yield a flow, so its taint request is never issued -- and every other pair's request still is.""" from openultrasast.model.regions import ScanRegion @@ -466,7 +468,8 @@ def test_tier_zero_pairs_are_not_asked_and_nothing_else_is_skipped() -> None: region = ScanRegion( path="a.py", function="run", language="python", families=("injection", "path", "deserialization"), rank=0.9, source="entry_point" ) - result = scan_repository(Path("."), [region], backend=_Backend(), budget=ScanBudget(max_model_calls=0)) + (tmp_path / "a.py").write_bytes(b"print(1)\n") + result = scan_repository(tmp_path, [region], backend=_Backend(), budget=ScanBudget(max_model_calls=0)) taint = next(i for i in issued if i["kind"] == "taint") assert taint["count"] == 1, "only the pair with a sink in reach is asked" @@ -474,7 +477,7 @@ def test_tier_zero_pairs_are_not_asked_and_nothing_else_is_skipped() -> None: assert not any(d.get("reason") == "query_failed" for d in result.degradations) -def test_the_budget_is_spent_by_evidence_when_asked() -> None: +def test_the_budget_is_spent_by_evidence_when_asked(tmp_path: Path) -> None: """Phase 2 of flow-aware-ranking. With `order_by_evidence`, the evidence pass runs over every region and the region cap is cut from the (tier, score) order, so a low-ranked region with an open sink and a local source is examined before a top-ranked region with no sink in reach. Off, the static order holds.""" @@ -523,15 +526,17 @@ def test_the_budget_is_spent_by_evidence_when_asked() -> None: top = ScanRegion(path="top.py", function="handle", language="python", families=("injection",), rank=1.0, source="entry_point") low = ScanRegion(path="low.py", function="run", language="python", families=("injection",), rank=0.3, source="entry_point") + (tmp_path / "top.py").write_bytes(b"print(1)\n") + (tmp_path / "low.py").write_bytes(b"print(2)\n") static = _Backend() - scan_repository(Path("."), [top, low], backend=static, budget=ScanBudget(max_model_calls=0, max_regions=1)) + scan_repository(tmp_path, [top, low], backend=static, budget=ScanBudget(max_model_calls=0, max_regions=1)) assert static.evidence_sizes == [1] and static.taint_files == [], ( "static order: the budget holds `top`, whose pair is tier 0 and pruned" ) ordered = _Backend() result = scan_repository( - Path("."), [top, low], backend=ordered, budget=ScanBudget(max_model_calls=0, max_regions=1, order_by_evidence=True) + tmp_path, [top, low], backend=ordered, budget=ScanBudget(max_model_calls=0, max_regions=1, order_by_evidence=True) ) assert ordered.evidence_sizes == [2], "the evidence pass covers every region, not only the budget" assert ordered.taint_files == [["low.py"]], "and the budget is spent on the region the evidence points at" diff --git a/tests/test_model_scope.py b/tests/test_model_scope.py index 6c89234..da2ecd6 100644 --- a/tests/test_model_scope.py +++ b/tests/test_model_scope.py @@ -23,7 +23,7 @@ class Backend: def build(self, root, **kwargs): # Prove these tests provide real readable source to the driver boundary. - assert any(p.read_bytes() for p in root.iterdir() if p.is_file()) + assert any(p.read_bytes() for p in root.rglob("*") if p.is_file()) if self.fail: return None return SimpleNamespace(cpg_path=Path("test.cpg"), run_batch=self.batch, cleanup=lambda: None) @@ -162,7 +162,7 @@ def test_later_timeout_retains_raw_answer_without_claiming_arbitration(tmp_path) def batch(self, kind, requests): rows = super().batch(kind, requests) if kind == "taint" and not any(p.get("evidenceOnly") for p in requests.values()): - time.sleep(0.08) + time.sleep(0.3) return {rid: [{"kind": "retained", "value": 42}] for rid in requests} return rows @@ -171,7 +171,7 @@ def test_later_timeout_retains_raw_answer_without_claiming_arbitration(tmp_path) [region("a.py", families=("injection", "access_control"))], backend=Slow(), ranking_mode="static", - execution_budget=ExecutionBudget(time.monotonic() + 0.05, 0.2), + execution_budget=ExecutionBudget(time.monotonic() + 0.2, 0.2), ) outcomes = {q.identity.family: q for q in result.question_outcomes} assert outcomes["injection"].status == "not_arbitrated"