mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(pre-push-safety-net): connect change context to ranked questions
This commit is contained in:
@@ -90,3 +90,39 @@ This manually declared entry-region smoke does not validate mapper discovery,
|
||||
PMPro/NodeGoat replay, capability admission or hook latency.
|
||||
|
||||
Nine of 27 tasks are now complete; tasks 3.3–3.4 remain for this group.
|
||||
|
||||
## Task 3.3 evidence
|
||||
|
||||
Independent review APPROVED after one required repair; verification status VERIFIED,
|
||||
claim type TASK. The fresh reviewer suite passed 1,128 tests, nine skipped, in
|
||||
105.95 seconds. Global Ruff, formatting (224 files), mypy (108 source files) and
|
||||
diff checks passed. The context suite contains 23 focused cases.
|
||||
|
||||
The generic driver accepts `change_context` and retains its enriched form. Existing
|
||||
taint evidence optionally projects full first-party method paths/ranges for entry,
|
||||
callee, same-file field, known hook and sanitizer context. Decoded snapshot changes
|
||||
and lexical anchors attach to exact questions without changing ranking weights or
|
||||
introducing another scope selector. Missing base, locations, empty projections,
|
||||
dynamic/external/depth dependencies and unsupported configuration/deleted-dependency
|
||||
projections remain explicit gaps. Missing context prevents tier-zero pruning and
|
||||
complete-negative outcomes; raw answers remain available.
|
||||
|
||||
Review found that broad source/sink/sanitizer text matching could mistakenly exempt
|
||||
an unknown consumer such as `unknown(req.body)` from missing-context reporting.
|
||||
The production-query regression reproduced exit 1 with that question incorrectly
|
||||
pruned as tier zero. The task-local repair restricts the exemption to directly
|
||||
invoked fact-modeled operations. Existing detector matchers are unchanged. After
|
||||
repair, consumers of a source, nested sink and sanitizer name all remain unresolved;
|
||||
a direct modeled sink completes, and the direct sanitizer retains tier zero without
|
||||
a false context gap.
|
||||
|
||||
`benchmarks/measurements/2026-09-13-change-context-smoke.json` retains the final image
|
||||
and matching backend/driver/regions/snapshot/query hashes, plus both runtime checks.
|
||||
Real Git histories remove a caller guard while keeping helper code unchanged. PHP
|
||||
reads 124 source bytes and reaches `helper.php:2:helper` in 63.48 seconds; JavaScript
|
||||
reads 133 bytes and reaches `helper.js:1:helper` through an ES-module import in
|
||||
39.63 seconds. Both preserve the dirty checkout/index and remove snapshot scratch.
|
||||
These are controlled change-to-question/witness checks, not proof that the removed
|
||||
line enforced security, that the defect is new, or that a hook meets its latency bar.
|
||||
|
||||
Ten of 27 tasks are now complete; task 3.4 remains for this group.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"feature_name": "pre-push-safety-net",
|
||||
"created_at": "2026-09-12T15:35:00Z",
|
||||
"updated_at": "2026-09-13T08:16:14.274231+00:00",
|
||||
"updated_at": "2026-09-13T08:41:28.781759+00:00",
|
||||
"language": "en",
|
||||
"phase": "implementation",
|
||||
"approvals": {
|
||||
@@ -71,9 +71,10 @@
|
||||
"2.2",
|
||||
"2.3",
|
||||
"3.1",
|
||||
"3.2"
|
||||
"3.2",
|
||||
"3.3"
|
||||
],
|
||||
"remaining_tasks": 18,
|
||||
"remaining_tasks": 17,
|
||||
"verification_record": "implementation-group-3.md",
|
||||
"verification_records": [
|
||||
"implementation-group-1.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 is in progress (3.1–3.2 verified); groups 4–8 remain pending;
|
||||
are complete after independent review and fresh verification. Group 3 is in progress (3.1–3.3 verified); 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
|
||||
@@ -95,13 +95,14 @@ the hook and capability admission are not yet implemented. See `implementation-g
|
||||
- _Boundary: Ranker Scope Contract, generic driver integration_
|
||||
- _Requirements: 2.1, 2.2, 6.4, 7.3, 8.1, 8.2_
|
||||
- _Verified 2026-09-13:_ independent review APPROVED; 1,105 passed, nine skipped; global Ruff/format/mypy passed. Twenty-four independent order/budget/missing-family combinations retained all question identities. Packaged PHP/JavaScript smoke selected the same controlled risky operation at tier 4/score 2.5, with exact execution/outcome IDs and reconciled family coverage. Missing evidence, partial graphs and interrupted arbitration remain explicit; CLI consumes authoritative scope. Evidence: `benchmarks/measurements/2026-09-13-ranker-scope-smoke.json`. No new ranking weights or hook admission claim.
|
||||
- [ ] 3.3 Connect change context to supported first-party relationships
|
||||
- [x] 3.3 Connect change context to supported first-party relationships
|
||||
- Use existing entry/callee, field, hook and guard relationships to supply affected context to ranked questions, including unchanged sinks after source/guard changes.
|
||||
- Preserve current abstract semantics; absent dynamic or external relationships remain unresolved rather than guessed safe. A required new feature or fact is reported to its owning spec.
|
||||
- Done when controlled cross-file and removed-guard cases reach the relevant question/witness, while an unresolved boundary cannot produce a complete negative.
|
||||
- _Boundary: Ranker Scope Contract, Snapshot Adapter integration_
|
||||
- _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
|
||||
- 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.
|
||||
|
||||
@@ -0,0 +1,942 @@
|
||||
{
|
||||
"recorded_at": "2026-09-13T08:40:22.821019+00:00",
|
||||
"task": "3.3",
|
||||
"image_id": "sha256:a30d41f7e0cde5d3c9b634889fef07f8c3aa089be176362f73645c3bda1e5953",
|
||||
"build_exit_code": 0,
|
||||
"cross_file_smoke_exit_code": 0,
|
||||
"negative_regression_exit_code": 0,
|
||||
"packaged_source_hashes": {
|
||||
"openultrasast.cpg.backend": "2e327d26958efd592932bbe5a84aa122d969c0e49a1c13469a4d956b621a9a34",
|
||||
"openultrasast.model.scan": "c803fa05a9f190b66df194616ed327043a592883c9f746a902768c89b253d36f",
|
||||
"openultrasast.model.regions": "1ecafa9080ac537383d91d02147f204eadc72c5241618f357ed30c5a5ff79cc1",
|
||||
"openultrasast.push.snapshot": "c6891f6de9221f63e534372c3009190d227172ca0b006bfb68bb6cffd7bf38f3",
|
||||
"taint.sc": "d09b5608870571c57b5ec82b9e886943ad6df0fd2510a7c56cec4bcaa352d116"
|
||||
},
|
||||
"packaged_sources_match_workspace": true,
|
||||
"supporting_script_hashes": {
|
||||
"ops/smoke_context.py": "4d515258c01e44e2a30ee71f8de8de2df06c88dac2e0ded6f2904f7682e347d9",
|
||||
"ops/smoke_context_boundaries.py": "35cc2cabbfbdfe843c3f82c776efe43dfd0db8418eda4ce6e045bd3269d4cb6e"
|
||||
},
|
||||
"cross_file_runtime": {
|
||||
"purpose": "Committed removed-guard context reaches real ranked questions; not semantic novelty or admission proof",
|
||||
"shared_budget_seconds": 900,
|
||||
"languages": {
|
||||
"php": {
|
||||
"base_revision": "3d894f361e33b1801a770addb7e068f69dab7ee4",
|
||||
"head_revision": "c6dee5e55aef86153a52e27835d51f8675eda0c8",
|
||||
"source_bytes": 124,
|
||||
"source_sha256": "e41333ddd3e341d08603a5cc6a3a9faedb442e6ef3b14627cd00516cdf12cedb",
|
||||
"helper_bytes": 48,
|
||||
"helper_sha256": "4eb14937fd2b027cc11bd5a79c5de38f572d9a5af8ee36a46d4a0e499817ae76",
|
||||
"input_context": {
|
||||
"base_revision": "3d894f361e33b1801a770addb7e068f69dab7ee4",
|
||||
"head_revision": "c6dee5e55aef86153a52e27835d51f8675eda0c8",
|
||||
"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": 63.48,
|
||||
"build_seconds": 28.21,
|
||||
"query_seconds": 18.0,
|
||||
"query_seconds_by_kind": {
|
||||
"evidence": 17.21,
|
||||
"taint": 18.0
|
||||
},
|
||||
"degradations": [],
|
||||
"tiers": [
|
||||
[
|
||||
"api.php",
|
||||
"handler",
|
||||
"injection",
|
||||
4
|
||||
]
|
||||
],
|
||||
"tier_counts": {
|
||||
"4": 1
|
||||
},
|
||||
"requests_pruned": 0,
|
||||
"change_context": {
|
||||
"base_revision": "3d894f361e33b1801a770addb7e068f69dab7ee4",
|
||||
"head_revision": "c6dee5e55aef86153a52e27835d51f8675eda0c8",
|
||||
"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\":\"3d894f361e33b1801a770addb7e068f69dab7ee4\",\"changed_paths\":[\"6170692e706870\"],\"declaration_paths\":[],\"deleted_paths\":[],\"head_revision\":\"c6dee5e55aef86153a52e27835d51f8675eda0c8\",\"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": "2f8c5857f56009b1bf3cc7e56b3579fd87a90239",
|
||||
"head_revision": "cbec42fd18a83f3a29943f7084760437fbe971f2",
|
||||
"source_bytes": 133,
|
||||
"source_sha256": "ee6b40b0c84f84cda86b2f5d7d118f39504bc7237450d30e5c189f6b3517c965",
|
||||
"helper_bytes": 47,
|
||||
"helper_sha256": "173055ccb4d1b0cb76378bcb674ac9944bad02f848bf20139923775b57e7634d",
|
||||
"input_context": {
|
||||
"base_revision": "2f8c5857f56009b1bf3cc7e56b3579fd87a90239",
|
||||
"head_revision": "cbec42fd18a83f3a29943f7084760437fbe971f2",
|
||||
"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": 39.63,
|
||||
"build_seconds": 5.76,
|
||||
"query_seconds": 17.2,
|
||||
"query_seconds_by_kind": {
|
||||
"evidence": 16.65,
|
||||
"taint": 17.2
|
||||
},
|
||||
"degradations": [],
|
||||
"tiers": [
|
||||
[
|
||||
"api.js",
|
||||
"handler",
|
||||
"injection",
|
||||
4
|
||||
]
|
||||
],
|
||||
"tier_counts": {
|
||||
"4": 1
|
||||
},
|
||||
"requests_pruned": 0,
|
||||
"change_context": {
|
||||
"base_revision": "2f8c5857f56009b1bf3cc7e56b3579fd87a90239",
|
||||
"head_revision": "cbec42fd18a83f3a29943f7084760437fbe971f2",
|
||||
"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\":\"2f8c5857f56009b1bf3cc7e56b3579fd87a90239\",\"changed_paths\":[\"6170692e6a73\"],\"declaration_paths\":[],\"deleted_paths\":[],\"head_revision\":\"cbec42fd18a83f3a29943f7084760437fbe971f2\",\"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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"negative_runtime": {
|
||||
"source_bytes": 255,
|
||||
"source_sha256": "2460518ef54c6ae3a31007801a28fc75b0c374cdb579bfa31d3e425e48b6a9b4",
|
||||
"result": {
|
||||
"findings": [
|
||||
{
|
||||
"site": "api.js:2:unknownSink",
|
||||
"family": "injection",
|
||||
"rung": "model_entailed",
|
||||
"witness": "eval(req.body) -> unknown(eval(req.body)) in api.js (line 2, 1 steps)",
|
||||
"contradiction": "",
|
||||
"reached_from": 1
|
||||
},
|
||||
{
|
||||
"site": "api.js:4:knownSink",
|
||||
"family": "injection",
|
||||
"rung": "model_entailed",
|
||||
"witness": "req.body -> eval(req.body) in api.js (line 4, 1 steps)",
|
||||
"contradiction": "",
|
||||
"reached_from": 1
|
||||
}
|
||||
],
|
||||
"by_rung": {
|
||||
"suspicion": 0,
|
||||
"model_corroborated": 0,
|
||||
"model_entailed": 2,
|
||||
"execution_confirmed": 0
|
||||
},
|
||||
"regions_scanned": 4,
|
||||
"regions_unjudged": 1,
|
||||
"regions_unasked": 0,
|
||||
"model_calls": 0,
|
||||
"cost_usd": 0.0,
|
||||
"seconds": 58.88,
|
||||
"build_seconds": 8.53,
|
||||
"query_seconds": 24.91,
|
||||
"query_seconds_by_kind": {
|
||||
"evidence": 25.38,
|
||||
"taint": 24.91
|
||||
},
|
||||
"degradations": [],
|
||||
"tiers": [
|
||||
[
|
||||
"api.js",
|
||||
"knownSanitizer",
|
||||
"injection",
|
||||
0
|
||||
],
|
||||
[
|
||||
"api.js",
|
||||
"knownSink",
|
||||
"injection",
|
||||
4
|
||||
],
|
||||
[
|
||||
"api.js",
|
||||
"unknownSanitizer",
|
||||
"injection",
|
||||
0
|
||||
],
|
||||
[
|
||||
"api.js",
|
||||
"unknownSink",
|
||||
"injection",
|
||||
4
|
||||
],
|
||||
[
|
||||
"api.js",
|
||||
"unknownSource",
|
||||
"injection",
|
||||
0
|
||||
]
|
||||
],
|
||||
"tier_counts": {
|
||||
"0": 3,
|
||||
"4": 2
|
||||
},
|
||||
"requests_pruned": 1,
|
||||
"change_context": {
|
||||
"base_revision": "base",
|
||||
"head_revision": "head",
|
||||
"changed_paths": [
|
||||
"api.js"
|
||||
],
|
||||
"deleted_paths": [],
|
||||
"spans": [
|
||||
{
|
||||
"path": "api.js",
|
||||
"start_line": 1,
|
||||
"end_line": 5,
|
||||
"side": "head"
|
||||
}
|
||||
],
|
||||
"renames": [],
|
||||
"declaration_paths": [],
|
||||
"relationships": [
|
||||
{
|
||||
"source": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSink",
|
||||
"family": "injection"
|
||||
},
|
||||
"target": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSink",
|
||||
"family": "injection"
|
||||
},
|
||||
"kind": "entry",
|
||||
"evidence": [
|
||||
"head_span:api.js:1-5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "knownSink",
|
||||
"family": "injection"
|
||||
},
|
||||
"target": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "knownSink",
|
||||
"family": "injection"
|
||||
},
|
||||
"kind": "entry",
|
||||
"evidence": [
|
||||
"head_span:api.js:1-5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "knownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"target": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "knownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"kind": "entry",
|
||||
"evidence": [
|
||||
"head_span:api.js:1-5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "knownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"target": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "knownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"kind": "guard",
|
||||
"evidence": [
|
||||
"head_span:api.js:1-5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"target": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"kind": "entry",
|
||||
"evidence": [
|
||||
"head_span:api.js:1-5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"target": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"kind": "guard",
|
||||
"evidence": [
|
||||
"head_span:api.js:1-5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSource",
|
||||
"family": "injection"
|
||||
},
|
||||
"target": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSource",
|
||||
"family": "injection"
|
||||
},
|
||||
"kind": "entry",
|
||||
"evidence": [
|
||||
"head_span:api.js:1-5"
|
||||
]
|
||||
}
|
||||
],
|
||||
"unresolved_boundaries": [
|
||||
"dynamic_external_or_depth_context_unresolved:contributor-scan:71905af15166a20401cb2e53ffd500f3307c2a76da41017a64bc9ef47afce2e7",
|
||||
"dynamic_external_or_depth_context_unresolved:contributor-scan:cb4c6cecef36f957c9646d31def81d962d9ff2d1d1455275a0a6e9b20b8ed0ea",
|
||||
"dynamic_external_or_depth_context_unresolved:contributor-scan:460011e339d204cda534bca278ebc1936596121cec7924cc88a9c179c4d2c681"
|
||||
],
|
||||
"path_encoding": "text",
|
||||
"line_correspondences": []
|
||||
},
|
||||
"scope": {
|
||||
"ranker_version": "existing-static-evidence-v1",
|
||||
"ranking_mode": "evidence",
|
||||
"population_complete": true,
|
||||
"selected": [
|
||||
{
|
||||
"identity": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSink",
|
||||
"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\":\"unknown(eval(req.body))\",\"line\":2,\"method\":\"unknownSink\"},{\"arg0_literal\":false,\"arity\":1,\"cleansed_on_call\":false,\"code\":\"eval(req.body)\",\"line\":2,\"method\":\"unknownSink\"}],\"source_local\":true,\"source_near\":true}",
|
||||
"tier=4",
|
||||
"score=6.5",
|
||||
"change_sites={\"base_revision\":\"base\",\"changed_paths\":[\"api.js\"],\"declaration_paths\":[],\"deleted_paths\":[],\"head_revision\":\"head\",\"path_encoding\":\"text\"}",
|
||||
"affected_relationship={\"evidence\":[\"head_span:api.js:1-5\"],\"kind\":\"entry\",\"source\":{\"family\":\"injection\",\"function\":\"unknownSink\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"},\"target\":{\"family\":\"injection\",\"function\":\"unknownSink\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"}}",
|
||||
"change_context_incomplete"
|
||||
],
|
||||
"tier": 4,
|
||||
"score": 6.5
|
||||
},
|
||||
{
|
||||
"identity": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "knownSink",
|
||||
"family": "injection"
|
||||
},
|
||||
"priority": -1.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(req.body)\",\"line\":4,\"method\":\"knownSink\"}],\"source_local\":true,\"source_near\":true}",
|
||||
"tier=4",
|
||||
"score=5.5",
|
||||
"change_sites={\"base_revision\":\"base\",\"changed_paths\":[\"api.js\"],\"declaration_paths\":[],\"deleted_paths\":[],\"head_revision\":\"head\",\"path_encoding\":\"text\"}",
|
||||
"affected_relationship={\"evidence\":[\"head_span:api.js:1-5\"],\"kind\":\"entry\",\"source\":{\"family\":\"injection\",\"function\":\"knownSink\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"},\"target\":{\"family\":\"injection\",\"function\":\"knownSink\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"}}"
|
||||
],
|
||||
"tier": 4,
|
||||
"score": 5.5
|
||||
},
|
||||
{
|
||||
"identity": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"priority": -2.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\":[],\"source_local\":false,\"source_near\":false}",
|
||||
"tier=0",
|
||||
"score=0.0",
|
||||
"change_sites={\"base_revision\":\"base\",\"changed_paths\":[\"api.js\"],\"declaration_paths\":[],\"deleted_paths\":[],\"head_revision\":\"head\",\"path_encoding\":\"text\"}",
|
||||
"affected_relationship={\"evidence\":[\"head_span:api.js:1-5\"],\"kind\":\"entry\",\"source\":{\"family\":\"injection\",\"function\":\"unknownSanitizer\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"},\"target\":{\"family\":\"injection\",\"function\":\"unknownSanitizer\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"}}",
|
||||
"affected_relationship={\"evidence\":[\"head_span:api.js:1-5\"],\"kind\":\"guard\",\"source\":{\"family\":\"injection\",\"function\":\"unknownSanitizer\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"},\"target\":{\"family\":\"injection\",\"function\":\"unknownSanitizer\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"}}",
|
||||
"change_context_incomplete"
|
||||
],
|
||||
"tier": 0,
|
||||
"score": 0.0
|
||||
},
|
||||
{
|
||||
"identity": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSource",
|
||||
"family": "injection"
|
||||
},
|
||||
"priority": -3.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\":[],\"source_local\":true,\"source_near\":true}",
|
||||
"tier=0",
|
||||
"score=0.0",
|
||||
"change_sites={\"base_revision\":\"base\",\"changed_paths\":[\"api.js\"],\"declaration_paths\":[],\"deleted_paths\":[],\"head_revision\":\"head\",\"path_encoding\":\"text\"}",
|
||||
"affected_relationship={\"evidence\":[\"head_span:api.js:1-5\"],\"kind\":\"entry\",\"source\":{\"family\":\"injection\",\"function\":\"unknownSource\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"},\"target\":{\"family\":\"injection\",\"function\":\"unknownSource\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"}}",
|
||||
"change_context_incomplete"
|
||||
],
|
||||
"tier": 0,
|
||||
"score": 0.0
|
||||
}
|
||||
],
|
||||
"deferred": [
|
||||
{
|
||||
"identity": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "knownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"reason": "tier_zero",
|
||||
"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\":[],\"source_local\":true,\"source_near\":true}",
|
||||
"tier=0",
|
||||
"score=0.0",
|
||||
"change_sites={\"base_revision\":\"base\",\"changed_paths\":[\"api.js\"],\"declaration_paths\":[],\"deleted_paths\":[],\"head_revision\":\"head\",\"path_encoding\":\"text\"}",
|
||||
"affected_relationship={\"evidence\":[\"head_span:api.js:1-5\"],\"kind\":\"entry\",\"source\":{\"family\":\"injection\",\"function\":\"knownSanitizer\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"},\"target\":{\"family\":\"injection\",\"function\":\"knownSanitizer\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"}}",
|
||||
"affected_relationship={\"evidence\":[\"head_span:api.js:1-5\"],\"kind\":\"guard\",\"source\":{\"family\":\"injection\",\"function\":\"knownSanitizer\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"},\"target\":{\"family\":\"injection\",\"function\":\"knownSanitizer\",\"language\":\"javascript\",\"path\":\"api.js\",\"unit\":\"context-boundaries\"}}"
|
||||
]
|
||||
}
|
||||
],
|
||||
"unresolved_boundaries": [
|
||||
"dynamic_external_or_depth_context_unresolved:contributor-scan:71905af15166a20401cb2e53ffd500f3307c2a76da41017a64bc9ef47afce2e7",
|
||||
"dynamic_external_or_depth_context_unresolved:contributor-scan:cb4c6cecef36f957c9646d31def81d962d9ff2d1d1455275a0a6e9b20b8ed0ea",
|
||||
"dynamic_external_or_depth_context_unresolved:contributor-scan:460011e339d204cda534bca278ebc1936596121cec7924cc88a9c179c4d2c681"
|
||||
]
|
||||
},
|
||||
"question_outcomes": [
|
||||
{
|
||||
"identity": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSink",
|
||||
"family": "injection"
|
||||
},
|
||||
"status": "unresolved",
|
||||
"reason": "change_context_incomplete",
|
||||
"raw_rows_json": "[{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":1,\"paths\":2,\"sanitized\":false,\"sink\":\"unknown(eval(req.body))\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"api.js\",\"sinkLine\":\"2\",\"sinkMethod\":\"unknownSink\",\"source\":\"eval(req.body)\",\"sourceKind\":\"framework\"},{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":2,\"paths\":2,\"sanitized\":false,\"sink\":\"unknown(eval(req.body))\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"api.js\",\"sinkLine\":\"2\",\"sinkMethod\":\"unknownSink\",\"source\":\"req.body\",\"sourceKind\":\"framework\"},{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":3,\"paths\":2,\"sanitized\":false,\"sink\":\"unknown(eval(req.body))\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"api.js\",\"sinkLine\":\"2\",\"sinkMethod\":\"unknownSink\",\"source\":\"req\",\"sourceKind\":\"parameter\"},{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":1,\"paths\":1,\"sanitized\":false,\"sink\":\"eval(req.body)\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"api.js\",\"sinkLine\":\"2\",\"sinkMethod\":\"unknownSink\",\"source\":\"req.body\",\"sourceKind\":\"framework\"},{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":2,\"paths\":1,\"sanitized\":false,\"sink\":\"eval(req.body)\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"api.js\",\"sinkLine\":\"2\",\"sinkMethod\":\"unknownSink\",\"source\":\"req\",\"sourceKind\":\"parameter\"}]"
|
||||
},
|
||||
{
|
||||
"identity": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "knownSink",
|
||||
"family": "injection"
|
||||
},
|
||||
"status": "completed",
|
||||
"reason": "query_answered_and_arbitrated",
|
||||
"raw_rows_json": "[{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":1,\"paths\":1,\"sanitized\":false,\"sink\":\"eval(req.body)\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"api.js\",\"sinkLine\":\"4\",\"sinkMethod\":\"knownSink\",\"source\":\"req.body\",\"sourceKind\":\"framework\"},{\"bound\":\"\",\"bounded\":false,\"inLabeledScope\":true,\"length\":2,\"paths\":1,\"sanitized\":false,\"sink\":\"eval(req.body)\",\"sinkArg0Literal\":false,\"sinkArity\":1,\"sinkFile\":\"api.js\",\"sinkLine\":\"4\",\"sinkMethod\":\"knownSink\",\"source\":\"req\",\"sourceKind\":\"parameter\"}]"
|
||||
},
|
||||
{
|
||||
"identity": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSanitizer",
|
||||
"family": "injection"
|
||||
},
|
||||
"status": "unresolved",
|
||||
"reason": "change_context_incomplete",
|
||||
"raw_rows_json": "[]"
|
||||
},
|
||||
{
|
||||
"identity": {
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"path": "api.js",
|
||||
"function": "unknownSource",
|
||||
"family": "injection"
|
||||
},
|
||||
"status": "unresolved",
|
||||
"reason": "change_context_incomplete",
|
||||
"raw_rows_json": "[]"
|
||||
}
|
||||
],
|
||||
"family_coverage": [
|
||||
{
|
||||
"unit": "context-boundaries",
|
||||
"language": "javascript",
|
||||
"family": "injection",
|
||||
"selected": 4,
|
||||
"completed": 1,
|
||||
"unanswered": 3,
|
||||
"deferred": 1,
|
||||
"unsupported": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"review_repair": {
|
||||
"red_exit_code": 1,
|
||||
"red_symptom": "unknownSource was excluded as tier_zero with no unresolved boundaries",
|
||||
"repair": "Only directly invoked fact-modeled calls can exempt an external call; argument/subtree mentions cannot.",
|
||||
"additional_parent_assertions": "Direct knownSanitizer remains tier_zero with no question context gap; verified against captured GREEN output."
|
||||
},
|
||||
"limits": [
|
||||
"Controlled declared entry regions, not full mapper/workload replay.",
|
||||
"Lexical change-to-question relationships do not prove semantic guard removal or novelty.",
|
||||
"Current field/hook abstraction limits preserved; missing dominance/config/deleted dependencies remain explicit.",
|
||||
"No hook latency, capability admission or broad Joern reliability claim."
|
||||
]
|
||||
}
|
||||
@@ -97,6 +97,18 @@ that the lower static-ranked risky function is selected, that the harmless funct
|
||||
recorded as deferred, and that selected IDs match completed query outcomes and per-family
|
||||
coverage. It uses one 900-second lab budget; it is not a mapper or hook-quality evaluation.
|
||||
|
||||
`ops/smoke_context.py` uses the same invocation with its own filename. It creates local
|
||||
PHP/JavaScript Git histories with a removed caller guard and an unchanged helper-file sink, then feeds
|
||||
immutable snapshot change evidence into the real driver. It checks affected-question
|
||||
relationships, a controlled witness and preservation of the dirty checkout. Its lexical
|
||||
change attribution does not prove that the removed guard enforced security or that the
|
||||
finding is new; comparison and admission are separate stages.
|
||||
|
||||
`ops/smoke_context_boundaries.py` is the production-query negative regression using
|
||||
the same invocation. Unknown consumers of a framework source, nested sink, or sanitizer
|
||||
name must retain unresolved context rather than become tier-zero exclusions. Direct
|
||||
modeled sink and sanitizer calls remain positive controls for the boundary projection.
|
||||
|
||||
Why this exists: Joern was first installed here by hand with `curl` and `unzip`, which is fine once and
|
||||
unreproducible thereafter. The engine decides whether a finding is a `suspicion` or a `model_entailed`, and
|
||||
every ceiling number in `benchmarks/measurements` is only comparable against a fixed version — so the version
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Real Git-to-engine change-context smoke; lexical attribution is not novelty proof."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
|
||||
from openultrasast.cpg.backend import JoernBackend
|
||||
from openultrasast.model.contracts import ExecutionBudget
|
||||
from openultrasast.model.regions import ScanRegion
|
||||
from openultrasast.model.scan import ScanBudget, scan_repository
|
||||
from openultrasast.push.snapshot import SnapshotAdapter
|
||||
|
||||
|
||||
def main() -> None:
|
||||
budget = ExecutionBudget(time.monotonic() + 900, 2.0)
|
||||
report: dict[str, object] = {
|
||||
"purpose": "Committed removed-guard context reaches real ranked questions; not semantic novelty or admission proof",
|
||||
"shared_budget_seconds": 900,
|
||||
"languages": {},
|
||||
}
|
||||
languages = report["languages"]
|
||||
assert isinstance(languages, dict)
|
||||
with tempfile.TemporaryDirectory(prefix="ousast-context-smoke-") as temporary:
|
||||
for language, filename, before, removed, helper_name, helper in (
|
||||
(
|
||||
"php",
|
||||
"api.php",
|
||||
b"<?php\nrequire_once 'helper.php';\nfunction handler($req) {\n if (!authorized($req)) return;\n helper($req);\n}\n",
|
||||
b" if (!authorized($req)) return;\n",
|
||||
"helper.php",
|
||||
b"<?php\nfunction helper($value) { eval($value); }\n",
|
||||
),
|
||||
(
|
||||
"javascript",
|
||||
"api.js",
|
||||
b"import { helper } from './helper.js';\nexport function handler(req) {\n"
|
||||
b" if (!authorized(req)) return;\n helper(req);\n}\n",
|
||||
b" if (!authorized(req)) return;\n",
|
||||
"helper.js",
|
||||
b"export function helper(value) { eval(value); }\n",
|
||||
),
|
||||
):
|
||||
repo = Path(temporary) / language
|
||||
repo.mkdir()
|
||||
|
||||
def git(*args: str, root: Path = repo) -> bytes:
|
||||
return subprocess.run(["git", "-C", str(root), *args], check=True, capture_output=True, timeout=10).stdout
|
||||
|
||||
def commit(message: str, path: str = filename) -> str:
|
||||
git("add", path)
|
||||
git("commit", "-m", message)
|
||||
return git("rev-parse", "HEAD").decode().strip()
|
||||
|
||||
git("init")
|
||||
git("config", "user.name", "Context smoke")
|
||||
git("config", "user.email", "[email protected]")
|
||||
git("config", "core.hooksPath", os.devnull)
|
||||
source = repo / filename
|
||||
source.write_bytes(before)
|
||||
(repo / helper_name).write_bytes(helper)
|
||||
git("add", helper_name)
|
||||
base = commit("base")
|
||||
after = before.replace(removed, b"")
|
||||
source.write_bytes(after)
|
||||
head = commit("removed guard")
|
||||
source.write_bytes(b"uncommitted content\n")
|
||||
index = (repo / ".git/index").read_bytes()
|
||||
status = git("status", "--porcelain=v1", "-z")
|
||||
adapter = SnapshotAdapter(repo)
|
||||
(comparison,) = adapter.resolve_updates(f"refs/heads/topic {head} refs/heads/topic {base}\n").comparisons
|
||||
context = adapter.compare(comparison, declaration_paths=(), budget=budget)
|
||||
assert any(span.side == "base" for span in context.spans), context
|
||||
with adapter.materialize(head, budget=budget) as snapshot:
|
||||
data = (snapshot.root / filename).read_bytes()
|
||||
assert data == after and data
|
||||
assert (snapshot.root / helper_name).read_bytes() == helper
|
||||
result = scan_repository(
|
||||
snapshot.root,
|
||||
(ScanRegion(filename, "handler", language, ("injection",), 1.0, "entry_point"),),
|
||||
backend=JoernBackend(heap_mb=1024),
|
||||
budget=ScanBudget(max_regions=1),
|
||||
execution_budget=budget,
|
||||
ranking_mode="evidence",
|
||||
unit="context-smoke",
|
||||
population_complete=True,
|
||||
change_context=context,
|
||||
)
|
||||
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
|
||||
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"
|
||||
assert any(Path(finding.site.split(":", 1)[0]).name == helper_name for finding in result.findings), result.findings
|
||||
languages[language] = {
|
||||
"base_revision": base,
|
||||
"head_revision": head,
|
||||
"source_bytes": len(data) + len(helper),
|
||||
"source_sha256": hashlib.sha256(data).hexdigest(),
|
||||
"helper_bytes": len(helper),
|
||||
"helper_sha256": hashlib.sha256(helper).hexdigest(),
|
||||
"input_context": context.to_payload(),
|
||||
"result": asdict(result),
|
||||
}
|
||||
assert not snapshot.root.exists()
|
||||
assert source.read_bytes() == b"uncommitted content\n"
|
||||
assert (repo / ".git/index").read_bytes() == index
|
||||
assert git("status", "--porcelain=v1", "-z") == status
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Production-query regression: a known argument does not summarize an unknown callee."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
|
||||
from openultrasast.cpg.backend import JoernBackend
|
||||
from openultrasast.model.contracts import ChangeContext, ChangedSpan, ExecutionBudget
|
||||
from openultrasast.model.regions import ScanRegion
|
||||
from openultrasast.model.scan import ScanBudget, scan_repository
|
||||
|
||||
|
||||
def main() -> None:
|
||||
content = (
|
||||
b"function unknownSource(req) { unknown(req.body); }\n"
|
||||
b"function unknownSink(req) { unknown(eval(req.body)); }\n"
|
||||
b'function unknownSanitizer(req) { unknown("escape"); }\n'
|
||||
b"function knownSink(req) { eval(req.body); }\n"
|
||||
b"function knownSanitizer(req) { escape(req.body); }\n"
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="ousast-context-boundary-") as temporary:
|
||||
root = Path(temporary)
|
||||
source = root / "api.js"
|
||||
source.write_bytes(content)
|
||||
assert source.read_bytes() == content
|
||||
names = ("unknownSource", "unknownSink", "unknownSanitizer", "knownSink", "knownSanitizer")
|
||||
result = scan_repository(
|
||||
root,
|
||||
tuple(ScanRegion("api.js", name, "javascript", ("injection",), 1.0, "entry_point") for name in names),
|
||||
backend=JoernBackend(heap_mb=1024),
|
||||
budget=ScanBudget(max_regions=5),
|
||||
execution_budget=ExecutionBudget(time.monotonic() + 600, 2.0),
|
||||
ranking_mode="evidence",
|
||||
unit="context-boundaries",
|
||||
population_complete=True,
|
||||
change_context=ChangeContext("base", "head", ("api.js",), (), (ChangedSpan("api.js", 1, 5, "head"),), (), (), (), ()),
|
||||
)
|
||||
assert result.scope is not None
|
||||
outcomes = {item.identity.function: item for item in result.question_outcomes}
|
||||
for name in names[:3]:
|
||||
assert name in outcomes, (name, "unknown call was excluded", result.scope)
|
||||
assert outcomes[name].status == "unresolved", (name, outcomes[name])
|
||||
question_id = outcomes[name].identity.question_id
|
||||
assert any(
|
||||
"dynamic_external_or_depth_context_unresolved" in gap and question_id in gap for gap in result.scope.unresolved_boundaries
|
||||
)
|
||||
assert outcomes["knownSink"].status == "completed", outcomes["knownSink"]
|
||||
known_sanitizer = next(item for item in result.scope.deferred if item.identity.function == "knownSanitizer")
|
||||
assert known_sanitizer.reason == "tier_zero", known_sanitizer
|
||||
assert not any(known_sanitizer.identity.question_id in gap for gap in result.scope.unresolved_boundaries)
|
||||
print(
|
||||
json.dumps(
|
||||
{"source_bytes": len(content), "source_sha256": hashlib.sha256(content).hexdigest(), "result": asdict(result)}, indent=2
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -95,7 +95,8 @@
|
||||
evidenceS: String,
|
||||
fileS: String,
|
||||
depthS: String,
|
||||
boundedS: String
|
||||
boundedS: String,
|
||||
contextS: String = ""
|
||||
): List[ujson.Obj] = {
|
||||
|
||||
// Word-boundary matching, never substring. `resolveUrl` contains `resolve`, so a bare-substring sanitizer
|
||||
@@ -692,7 +693,51 @@
|
||||
"cleansedOnCall" -> sink.argument.ast.l.exists(node => sanitizesHere(node))
|
||||
)
|
||||
}
|
||||
return summary :: perSink
|
||||
// Export locations already used by this query, for change attribution only. No new
|
||||
// source/sink/guard inference and no change to the evidence vector or its weights.
|
||||
val contextRows = if (contextS != "true") Nil else {
|
||||
def location(m: io.shiftleft.codepropertygraph.generated.nodes.Method, kind: String): ujson.Obj =
|
||||
ujson.Obj("kind" -> "context_method", "path" -> m.filename, "function" -> m.name,
|
||||
"startLine" -> m.lineNumber.getOrElse(-1), "endLine" -> m.lineNumberEnd.getOrElse(-1),
|
||||
"relationship" -> kind)
|
||||
val scoped = cpg.method.filterNot(_.isExternal).filter(m => inScope(m)).l
|
||||
val methods = scoped.map(m => location(m, if (labeledMethods.exists(_.id == m.id)) "entry" else "callee"))
|
||||
// Field carrying is deliberately same-file in the existing abstraction. Retain
|
||||
// its file context conservatively, never claim every method carries the value.
|
||||
val fieldFiles = if (fieldCarriedKind.isEmpty) Set.empty[String] else
|
||||
cpg.call.nameExact(FIELD_ACCESS).filter(c => inScope(c.method)).l.map(_.method.filename).toSet
|
||||
val fields = fieldFiles.toList.flatMap(f => methodsIn(f).filterNot(_.isExternal).map(m => location(m, "field")))
|
||||
val callbacks = if (hookApply.isEmpty) Nil else cpg.call.filter(c => hookApply.contains(c.name))
|
||||
.filter(c => inScope(c.method)).l.flatMap { call =>
|
||||
val hook = call.argument.l.headOption.map(a => unquote(a.code)).getOrElse("")
|
||||
hookTable.getOrElse(hook, Nil).flatMap(name => cpg.method.nameExact(name).filterNot(_.isExternal).l)
|
||||
}.distinct
|
||||
val hooks = callbacks.map(m => location(m, "hook"))
|
||||
// Export sanitizer locations as guard context, not proof of discharge. Removed
|
||||
// guards are retained through base spans and unchanged-line correspondence.
|
||||
val guards = scoped.filter(_.ast.l.exists(sanitizesHere)).map(m => location(m, "guard"))
|
||||
// Reachability remains bounded. External/dynamic calls and calls outside the
|
||||
// current reachability set cannot establish a complete negative for a change.
|
||||
// A modeled argument does not summarize its consumer: unknown(req.body),
|
||||
// unknown(eval(...)) and unknown("escape") all retain an external boundary.
|
||||
// Only the directly invoked fact-named operation can use this exemption.
|
||||
def directlyModeled(c: io.shiftleft.codepropertygraph.generated.nodes.Call): Boolean =
|
||||
(sinkNames ++ sanitizerNames ++ sourcePatterns).exists { raw =>
|
||||
val name = raw.stripSuffix("(")
|
||||
c.name == name || c.code.startsWith(name + "(")
|
||||
}
|
||||
val missing = scoped.exists(m => m.ast.isCall.l.exists { c =>
|
||||
!c.name.startsWith("<operator>") && !directlyModeled(c) && {
|
||||
val destinations = c.callee.l
|
||||
destinations.isEmpty || destinations.exists(d => d.isExternal || !inScope(d))
|
||||
}
|
||||
})
|
||||
val boundaries = if (scoped.isEmpty) List(ujson.Obj("kind" -> "context_boundary",
|
||||
"reason" -> "context_scope_empty:contributor-scan")) else if (missing) List(ujson.Obj("kind" -> "context_boundary",
|
||||
"reason" -> "dynamic_external_or_depth_context_unresolved:contributor-scan")) else Nil
|
||||
ujson.Obj("kind" -> "context_summary") :: (methods ++ fields ++ hooks ++ guards ++ boundaries)
|
||||
}
|
||||
return summary :: (perSink ++ contextRows)
|
||||
}
|
||||
|
||||
val rows = sinkCalls.l.flatMap { sink =>
|
||||
@@ -797,7 +842,8 @@
|
||||
evidence,
|
||||
field("file"),
|
||||
field("callDepth"),
|
||||
field("boundedSinks")
|
||||
field("boundedSinks"),
|
||||
field("contextEvidence")
|
||||
): _*
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,9 +18,12 @@ Two restraints are deliberate:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
|
||||
from .contracts import AffectedRelationship, ChangeContext, ExecutionBudget, QuestionIdentity
|
||||
from .specs import config_specs, dominance_specs, taint_specs
|
||||
|
||||
# From `mapping.AccessLevel`. A publicly reachable handler at a trust boundary is where a missing check costs
|
||||
@@ -174,3 +177,80 @@ def _families(language: str, *, has_handler: bool) -> tuple[str, ...]:
|
||||
if has_handler:
|
||||
families |= set(dominance_specs(language=language))
|
||||
return tuple(sorted(families))
|
||||
|
||||
|
||||
def affected_context(
|
||||
context: ChangeContext,
|
||||
questions: Sequence[QuestionIdentity],
|
||||
rows_by_question: Mapping[QuestionIdentity, Sequence[Mapping[str, object]]],
|
||||
root: Path,
|
||||
execution_budget: ExecutionBudget | None = None,
|
||||
) -> tuple[ChangeContext, set[QuestionIdentity]]:
|
||||
"""Attach existing graph locations to changes; never select or reprioritize a question.
|
||||
|
||||
Base spans are file-level hints unless an unchanged line anchors them in the head.
|
||||
A method relationship is context, not proof of taint, guard removal or novelty.
|
||||
"""
|
||||
|
||||
relationships = list(context.relationships)
|
||||
gaps = list(context.unresolved_boundaries)
|
||||
affected_gaps: set[QuestionIdentity] = set()
|
||||
changed = {context.decode_path(p) for p in context.changed_paths}
|
||||
if context.deleted_paths:
|
||||
gaps.append("deleted_dependency_projection_unavailable:contributor-scan")
|
||||
if changed.intersection(context.decode_path(p) for p in context.declaration_paths):
|
||||
gaps.append("configuration_dependency_projection_unavailable:contributor-scan")
|
||||
if context.base_revision is None:
|
||||
gaps.append("comparison_base_unavailable")
|
||||
inherited_gap = bool(gaps)
|
||||
renamed = {context.decode_path(r.base_path): context.decode_path(r.head_path) for r in context.renames}
|
||||
for identity in questions:
|
||||
if execution_budget is not None and time.monotonic() >= execution_budget.deadline_monotonic:
|
||||
gaps.append("change_context_deadline_exhausted")
|
||||
affected_gaps.update(questions)
|
||||
break
|
||||
rows = rows_by_question.get(identity, ())
|
||||
local_gaps = []
|
||||
if not any(r.get("kind") == "context_summary" for r in rows):
|
||||
local_gaps.append("context_projection_unavailable:contributor-scan")
|
||||
elif not any(r.get("kind") == "context_method" for r in rows):
|
||||
local_gaps.append("context_scope_empty:contributor-scan")
|
||||
for row in rows:
|
||||
if row.get("kind") == "context_boundary":
|
||||
local_gaps.append(str(row.get("reason", "context_unresolved")))
|
||||
if row.get("kind") != "context_method":
|
||||
continue
|
||||
path = Path(str(row.get("path", "")))
|
||||
try:
|
||||
absolute = path if path.is_absolute() else root / path
|
||||
path_text = absolute.resolve().relative_to(root.resolve()).as_posix()
|
||||
if not absolute.is_file():
|
||||
raise ValueError("missing context source")
|
||||
start, end = int(str(row.get("startLine", 0))), int(str(row.get("endLine", 0)))
|
||||
if start < 1 or end < start:
|
||||
raise ValueError("missing method extent")
|
||||
except (ValueError, TypeError, OSError):
|
||||
local_gaps.append("context_location_unavailable")
|
||||
continue
|
||||
evidence = []
|
||||
for span in context.spans:
|
||||
old_path = context.decode_path(span.path)
|
||||
match_path = renamed.get(old_path, old_path) if span.side == "base" else old_path
|
||||
if match_path != path_text:
|
||||
continue
|
||||
if span.side == "base" or (span.start_line <= end and span.end_line >= start):
|
||||
evidence.append(f"{span.side}_span:{old_path}:{span.start_line}-{span.end_line}")
|
||||
for anchor in context.line_correspondences:
|
||||
if context.decode_path(anchor.head_path) == path_text and start <= anchor.head_start_line <= end:
|
||||
evidence.append(f"unchanged_line:{anchor.head_start_line};lexical_only")
|
||||
if not evidence or path_text not in changed | set(renamed.values()):
|
||||
continue
|
||||
function = str(row.get("function", "")) or None
|
||||
source = QuestionIdentity(identity.unit, identity.language, path_text, function, identity.family)
|
||||
relation = AffectedRelationship(source, identity, str(row.get("relationship", "method")), tuple(evidence))
|
||||
if relation not in relationships:
|
||||
relationships.append(relation)
|
||||
if local_gaps or inherited_gap:
|
||||
affected_gaps.add(identity)
|
||||
gaps.extend(reason + ":" + identity.question_id for reason in local_gaps)
|
||||
return replace(context, relationships=tuple(relationships), unresolved_boundaries=tuple(dict.fromkeys(gaps))), affected_gaps
|
||||
|
||||
@@ -30,13 +30,22 @@ from typing import Any
|
||||
from ..cpg.backend import CpgResult
|
||||
from .candidates import enumerate_candidates
|
||||
from .config_value import request_params as config_params
|
||||
from .contracts import DeferredQuestion, ExecutionBudget, FamilyCoverage, QuestionIdentity, QuestionOutcome, RankedQuestion, ScopeDecision
|
||||
from .contracts import (
|
||||
ChangeContext,
|
||||
DeferredQuestion,
|
||||
ExecutionBudget,
|
||||
FamilyCoverage,
|
||||
QuestionIdentity,
|
||||
QuestionOutcome,
|
||||
RankedQuestion,
|
||||
ScopeDecision,
|
||||
)
|
||||
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 .pipeline import ModelFinding, scan_region
|
||||
from .regions import MODULE_SCOPE, ScanRegion
|
||||
from .regions import MODULE_SCOPE, ScanRegion, affected_context
|
||||
from .specs import ConfigSpec, DominanceSpec, TaintSpec, config_specs, dominance_specs, taint_specs
|
||||
from .taint import request_params as taint_params
|
||||
from .taxonomy import load_families
|
||||
@@ -117,6 +126,7 @@ class ModelScanResult:
|
||||
# could have been found was skipped. Reported so the request count is legible.
|
||||
requests_pruned: int = 0
|
||||
|
||||
change_context: ChangeContext | None = None
|
||||
scope: ScopeDecision | None = None
|
||||
question_outcomes: tuple[QuestionOutcome, ...] = ()
|
||||
family_coverage: tuple[FamilyCoverage, ...] = ()
|
||||
@@ -139,6 +149,7 @@ def scan_repository(
|
||||
ranking_mode: str | None = None,
|
||||
unit: str = "repository",
|
||||
population_complete: bool = False,
|
||||
change_context: ChangeContext | None = None,
|
||||
) -> ModelScanResult:
|
||||
"""Run with an optional transaction deadline; graph ownership ends on every exit."""
|
||||
if ranking_mode not in (None, "static", "evidence"):
|
||||
@@ -164,6 +175,7 @@ def scan_repository(
|
||||
ranking_mode=mode,
|
||||
unit=unit,
|
||||
population_complete=population_complete,
|
||||
change_context=change_context,
|
||||
execution_budget=execution_budget,
|
||||
owned=owned,
|
||||
)
|
||||
@@ -197,6 +209,7 @@ def _scan_repository_impl(
|
||||
ranking_mode: str,
|
||||
unit: str,
|
||||
population_complete: bool,
|
||||
change_context: ChangeContext | None,
|
||||
) -> ModelScanResult:
|
||||
"""Arbitrate a repository's regions under one CPG and one budget."""
|
||||
limits = budget if budget is not None else ScanBudget()
|
||||
@@ -213,8 +226,11 @@ def _scan_repository_impl(
|
||||
cpg = _build(backend, root, dominant, execution_budget=execution_budget)
|
||||
build_seconds = round(time.monotonic() - build_started, 2)
|
||||
if cpg is None:
|
||||
scope, _ = _scope_work(regions, {}, limits, unit, ranking_mode, population_complete, failure="cpg_build_failed")
|
||||
scope, _ = _scope_work(
|
||||
regions, {}, limits, unit, ranking_mode, population_complete, failure="cpg_build_failed", change_context=change_context
|
||||
)
|
||||
return ModelScanResult(
|
||||
change_context=change_context,
|
||||
scope=scope,
|
||||
family_coverage=_family_coverage(scope, ()),
|
||||
by_rung=_empty_tally(),
|
||||
@@ -273,6 +289,7 @@ def _scan_repository_impl(
|
||||
tiers: list[tuple[str, str, str, int]] = []
|
||||
per_kind: dict[str, float] = {}
|
||||
evidence_by_pair: dict[tuple[str, str, str], Evidence] = {}
|
||||
context_rows: dict[QuestionIdentity, list[Mapping[str, object]]] = {}
|
||||
if _within_deadline(execution_budget) and limits.tiering and callable(getattr(cpg, "run_batch", None)):
|
||||
evidence_candidates = (
|
||||
ordered_regions[: (limits.evidence_regions or len(ordered_regions))]
|
||||
@@ -280,7 +297,14 @@ def _scan_repository_impl(
|
||||
else ordered_regions[: limits.max_regions]
|
||||
)
|
||||
tier_started = time.monotonic()
|
||||
evidence_by_pair, failed = _evidence_pass(cpg, _collect(evidence_candidates), hooks, degradations=degradations)
|
||||
evidence_by_pair, failed = _evidence_pass(
|
||||
cpg,
|
||||
_collect(evidence_candidates),
|
||||
hooks,
|
||||
degradations=degradations,
|
||||
context_rows=context_rows if change_context is not None else None,
|
||||
unit=unit,
|
||||
)
|
||||
per_kind["evidence"] = round(time.monotonic() - tier_started, 2)
|
||||
if failed:
|
||||
degradations.append({"stage": "model", "reason": "query_failed", "kind": "evidence", "requests": failed})
|
||||
@@ -288,6 +312,12 @@ def _scan_repository_impl(
|
||||
if limits.order_by_evidence and evidence_by_pair:
|
||||
ordered_regions = order_by_evidence(ordered_regions, evidence_by_pair)
|
||||
|
||||
context_gaps: set[QuestionIdentity] = set()
|
||||
if change_context is not None:
|
||||
change_context, context_gaps = affected_context(
|
||||
change_context, [_identity(unit, r, f) for r in ordered_regions for f in r.families], context_rows, root, execution_budget
|
||||
)
|
||||
|
||||
# Authoritative selection is made once, from the actual final ranker order.
|
||||
# Stable question IDs are the IDs sent to every execution request below.
|
||||
scope_limits = (
|
||||
@@ -295,7 +325,16 @@ def _scan_repository_impl(
|
||||
if any(d.get("reason") in {"files_unparsed", "cpg_empty", "cpg_sharded"} for d in degradations)
|
||||
else limits
|
||||
)
|
||||
scope, work = _scope_work(ordered_regions, evidence_by_pair, scope_limits, unit, ranking_mode, population_complete)
|
||||
scope, work = _scope_work(
|
||||
ordered_regions,
|
||||
evidence_by_pair,
|
||||
scope_limits,
|
||||
unit,
|
||||
ranking_mode,
|
||||
population_complete,
|
||||
change_context=change_context,
|
||||
context_gaps=context_gaps,
|
||||
)
|
||||
outcomes: dict[str, QuestionOutcome] = {}
|
||||
|
||||
# Phase 2: ONE invocation per query kind. JVM startup dominates a repository scan -- a call per region
|
||||
@@ -429,6 +468,8 @@ def _scan_repository_impl(
|
||||
"deadline_exhausted" if not _within_deadline(execution_budget) else "query_unanswered",
|
||||
_rows_json(rows_by_id[rid]) if answered else None,
|
||||
)
|
||||
if question.identity in context_gaps and outcomes[rid].status == "completed":
|
||||
outcomes[rid] = replace(outcomes[rid], status="unresolved", reason="change_context_incomplete")
|
||||
if graph_gaps and outcomes[rid].status == "completed":
|
||||
outcomes[rid] = replace(outcomes[rid], status="unresolved", reason="graph_incomplete")
|
||||
scope = replace(scope, unresolved_boundaries=tuple(dict.fromkeys((*scope.unresolved_boundaries, *graph_gaps))))
|
||||
@@ -457,6 +498,7 @@ def _scan_repository_impl(
|
||||
scope=scope,
|
||||
question_outcomes=ordered_outcomes,
|
||||
family_coverage=_family_coverage(scope, ordered_outcomes),
|
||||
change_context=change_context,
|
||||
)
|
||||
|
||||
|
||||
@@ -477,6 +519,8 @@ def _scope_work(
|
||||
population_complete: bool,
|
||||
*,
|
||||
failure: str | None = None,
|
||||
change_context: ChangeContext | None = None,
|
||||
context_gaps: set[QuestionIdentity] | None = None,
|
||||
) -> tuple[ScopeDecision, list[tuple[str, ScanRegion, ArbiterSpec]]]:
|
||||
"""Consume the ranker's final order once; the returned work IS selected scope."""
|
||||
selected: list[RankedQuestion] = []
|
||||
@@ -485,6 +529,8 @@ def _scope_work(
|
||||
boundaries: list[str] = [] if population_complete else ["population_not_asserted_complete"]
|
||||
if failure:
|
||||
boundaries.append(failure)
|
||||
if change_context is not None:
|
||||
boundaries.extend(change_context.unresolved_boundaries)
|
||||
for index, region in enumerate(regions):
|
||||
for family in region.families:
|
||||
identity = _identity(unit, region, family)
|
||||
@@ -498,6 +544,25 @@ def _scope_work(
|
||||
boundaries.append("evidence_unknown:" + identity.question_id)
|
||||
else:
|
||||
facts += ("taint_evidence_not_applicable",)
|
||||
if change_context is not None:
|
||||
facts += (
|
||||
"change_sites="
|
||||
+ _rows_json(
|
||||
{
|
||||
"base_revision": change_context.base_revision,
|
||||
"head_revision": change_context.head_revision,
|
||||
"changed_paths": change_context.changed_paths,
|
||||
"deleted_paths": change_context.deleted_paths,
|
||||
"declaration_paths": change_context.declaration_paths,
|
||||
"path_encoding": change_context.path_encoding,
|
||||
}
|
||||
),
|
||||
)
|
||||
facts += tuple(
|
||||
"affected_relationship=" + _rows_json(r.to_payload()) for r in change_context.relationships if r.target == identity
|
||||
)
|
||||
if identity in (context_gaps or set()):
|
||||
facts += ("change_context_incomplete",)
|
||||
reason = (
|
||||
"unsupported_family"
|
||||
if spec is None
|
||||
@@ -506,7 +571,11 @@ def _scope_work(
|
||||
"region_budget"
|
||||
if index >= limits.max_regions
|
||||
else "tier_zero"
|
||||
if limits.prune_tier0 and isinstance(spec, TaintSpec) and vector is not None and vector.tier == TIER_EXCLUDE
|
||||
if limits.prune_tier0
|
||||
and identity not in (context_gaps or set())
|
||||
and isinstance(spec, TaintSpec)
|
||||
and vector is not None
|
||||
and vector.tier == TIER_EXCLUDE
|
||||
else None
|
||||
)
|
||||
)
|
||||
@@ -568,7 +637,13 @@ def _collect(regions: Sequence[ScanRegion]) -> list[tuple[str, ScanRegion, Arbit
|
||||
|
||||
|
||||
def _evidence_pass(
|
||||
cpg: Any, work: Sequence[tuple[str, ScanRegion, ArbiterSpec]], hooks: str, *, degradations: list[Mapping[str, object]] | None = None
|
||||
cpg: Any,
|
||||
work: Sequence[tuple[str, ScanRegion, ArbiterSpec]],
|
||||
hooks: str,
|
||||
*,
|
||||
degradations: list[Mapping[str, object]] | None = None,
|
||||
context_rows: dict[QuestionIdentity, list[Mapping[str, object]]] | None = None,
|
||||
unit: str = "repository",
|
||||
) -> tuple[dict[tuple[str, str, str], Evidence], int]:
|
||||
"""The evidence vector of every taint pair in ``work``, keyed by (path, function, family).
|
||||
|
||||
@@ -578,7 +653,10 @@ def _evidence_pass(
|
||||
grouped_taint = _grouped(list(work), hook_callbacks=hooks).get("taint", {})
|
||||
if not grouped_taint:
|
||||
return {}, 0
|
||||
requests = {rid: {**dict(params), "evidenceOnly": "true"} for rid, params in grouped_taint.items()}
|
||||
requests = {
|
||||
rid: {**dict(params), "evidenceOnly": "true", **({"contextEvidence": "true"} if context_rows is not None else {})}
|
||||
for rid, params in grouped_taint.items()
|
||||
}
|
||||
answered = cpg.run_batch("taint", requests)
|
||||
if answered is None:
|
||||
return {}, len(requests)
|
||||
@@ -601,6 +679,8 @@ def _evidence_pass(
|
||||
if pair is None or rid not in requests or not isinstance(rows, list):
|
||||
continue
|
||||
region, spec = pair
|
||||
if context_rows is not None:
|
||||
context_rows[_identity(unit, region, spec.family)] = [r for r in rows if isinstance(r, Mapping)]
|
||||
evidence = evidence_from_rows(
|
||||
rows,
|
||||
entry=_is_entry_point(region),
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
"""Change evidence informs exact existing scope, never another inclusion rule."""
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
from test_model_scope import Backend, region, vector
|
||||
|
||||
from openultrasast.model.contracts import ChangeContext, ChangedSpan, LineCorrespondence
|
||||
from openultrasast.model.scan import ScanBudget, scan_repository
|
||||
|
||||
|
||||
def context(path="source.php"):
|
||||
return ChangeContext(
|
||||
"base",
|
||||
"head",
|
||||
(path.encode().hex(),),
|
||||
(),
|
||||
(ChangedSpan(path.encode().hex(), 2, 2, "base"),),
|
||||
(),
|
||||
(),
|
||||
(),
|
||||
(),
|
||||
"filesystem-bytes-hex",
|
||||
(LineCorrespondence(path.encode().hex(), path.encode().hex(), 3, 3, 2),),
|
||||
)
|
||||
|
||||
|
||||
def row(path, kind="callee", function="run"):
|
||||
return {"kind": "context_method", "path": path, "function": function, "startLine": 1, "endLine": 10, "relationship": kind}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,ext", [("php", "php"), ("javascript", "js")])
|
||||
@pytest.mark.parametrize("kind", ["entry", "callee", "field", "hook", "guard"])
|
||||
def test_changed_dependency_reaches_unchanged_ranked_question(tmp_path, language, ext, kind):
|
||||
source, sink = f"source.{ext}", f"sink.{ext}"
|
||||
(tmp_path / source).write_text("source input\nremoved guard\nunchanged operation\n")
|
||||
(tmp_path / sink).write_text("unchanged sink(input)\n")
|
||||
backend = Backend({sink: vector() + [row(source, kind), {"kind": "context_summary"}]})
|
||||
result = scan_repository(tmp_path, [region(sink, language)], backend=backend, change_context=context(source), population_complete=True)
|
||||
assert len(result.scope.selected) == 1
|
||||
relationship = result.change_context.relationships[0]
|
||||
assert relationship.source.path == source
|
||||
assert relationship.target == result.scope.selected[0].identity
|
||||
assert relationship.kind == kind
|
||||
assert any("base_span" in e for e in relationship.evidence)
|
||||
assert any("affected_relationship=" in e for e in result.scope.selected[0].evidence)
|
||||
assert len(backend.executed) == 1
|
||||
|
||||
|
||||
def test_unavailable_relationships_cannot_be_completed_negative(tmp_path):
|
||||
(tmp_path / "sink.php").write_text("readable\n")
|
||||
result = scan_repository(
|
||||
tmp_path,
|
||||
[region("sink.php", "php")],
|
||||
backend=Backend({"sink.php": vector(False)}),
|
||||
change_context=context(),
|
||||
population_complete=True,
|
||||
)
|
||||
assert not any(q.reason == "tier_zero" for q in result.scope.deferred)
|
||||
assert result.question_outcomes[0].status == "unresolved"
|
||||
assert any("context_projection_unavailable" in b for b in result.scope.unresolved_boundaries)
|
||||
|
||||
|
||||
def test_change_does_not_replace_ranker_selection(tmp_path):
|
||||
for path in ("source.php", "sink.php"):
|
||||
(tmp_path / path).write_text("readable\n")
|
||||
regions = [region("source.php", "php", rank=0.1), region("sink.php", "php", rank=1)]
|
||||
rows = {p: vector() + [row(p), {"kind": "context_summary"}] for p in ("source.php", "sink.php")}
|
||||
kwargs = dict(backend=Backend(rows), budget=ScanBudget(max_regions=1), population_complete=True)
|
||||
baseline = scan_repository(tmp_path, regions, **kwargs)
|
||||
changed = scan_repository(tmp_path, regions, change_context=context(), **kwargs)
|
||||
assert [q.identity for q in baseline.scope.selected] == [q.identity for q in changed.scope.selected]
|
||||
assert [q.score for q in baseline.scope.selected] == [q.score for q in changed.scope.selected]
|
||||
|
||||
|
||||
def test_global_boundary_survives_answer(tmp_path):
|
||||
(tmp_path / "sink.php").write_text("readable\n")
|
||||
ctx = replace(context(), unresolved_boundaries=("external summary missing",))
|
||||
result = scan_repository(
|
||||
tmp_path, [region("sink.php", "php")], backend=Backend({"sink.php": vector() + [{"kind": "context_summary"}]}), change_context=ctx
|
||||
)
|
||||
assert result.question_outcomes[0].status == "unresolved"
|
||||
assert "external summary missing" in result.scope.unresolved_boundaries
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["../external.php", "/outside/missing.php", "missing.php"])
|
||||
def test_missing_or_external_source_cannot_manufacture_relationship(tmp_path, path):
|
||||
(tmp_path / "sink.php").write_text("readable\n")
|
||||
result = scan_repository(
|
||||
tmp_path,
|
||||
[region("sink.php", "php")],
|
||||
backend=Backend({"sink.php": vector() + [row(path), {"kind": "context_summary"}]}),
|
||||
change_context=context(),
|
||||
)
|
||||
assert result.change_context.relationships == ()
|
||||
assert result.question_outcomes[0].status == "unresolved"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["deleted_paths", "declaration_paths"])
|
||||
def test_unexported_configuration_and_deleted_dependencies_are_explicit(tmp_path, field):
|
||||
(tmp_path / "source.php").write_text("readable\n")
|
||||
ctx = replace(context(), **{field: (b"source.php".hex(),)})
|
||||
result = scan_repository(
|
||||
tmp_path,
|
||||
[region("source.php", "php")],
|
||||
backend=Backend({"source.php": vector() + [row("source.php"), {"kind": "context_summary"}]}),
|
||||
change_context=ctx,
|
||||
)
|
||||
assert result.question_outcomes[0].status == "unresolved"
|
||||
assert any("dependency_projection_unavailable" in b for b in result.scope.unresolved_boundaries)
|
||||
|
||||
|
||||
def test_failed_build_retains_comparison_and_boundary(tmp_path):
|
||||
(tmp_path / "source.php").write_text("readable\n")
|
||||
ctx = replace(context(), unresolved_boundaries=("missing base",))
|
||||
result = scan_repository(tmp_path, [region("source.php", "php")], backend=Backend(fail=True), change_context=ctx)
|
||||
assert result.change_context == ctx
|
||||
assert "missing base" in result.scope.unresolved_boundaries
|
||||
|
||||
|
||||
def test_full_paths_do_not_merge_duplicate_basenames(tmp_path):
|
||||
(tmp_path / "manifest.txt").write_text("input census\n")
|
||||
for directory in ("a", "b"):
|
||||
(tmp_path / directory).mkdir()
|
||||
(tmp_path / directory / "source.php").write_text("readable\n")
|
||||
result = scan_repository(
|
||||
tmp_path,
|
||||
[region("b/source.php", "php")],
|
||||
backend=Backend({"b/source.php": vector() + [row("b/source.php"), {"kind": "context_summary"}]}),
|
||||
change_context=context("a/source.php"),
|
||||
)
|
||||
assert result.change_context.relationships == ()
|
||||
|
||||
|
||||
def test_context_expansion_uses_transaction_deadline(tmp_path):
|
||||
from openultrasast.model.contracts import ExecutionBudget, QuestionIdentity
|
||||
from openultrasast.model.regions import affected_context
|
||||
|
||||
(tmp_path / "source.php").write_text("readable\n")
|
||||
question = QuestionIdentity("unit", "php", "source.php", "run", "injection")
|
||||
enriched, missing = affected_context(
|
||||
context(), [question], {question: [row("source.php"), {"kind": "context_summary"}]}, tmp_path, ExecutionBudget(0, 1)
|
||||
)
|
||||
assert missing == {question}
|
||||
assert enriched.relationships == ()
|
||||
assert "change_context_deadline_exhausted" in enriched.unresolved_boundaries
|
||||
|
||||
|
||||
def test_empty_method_projection_is_not_complete_negative(tmp_path):
|
||||
(tmp_path / "source.php").write_text("readable\n")
|
||||
result = scan_repository(
|
||||
tmp_path,
|
||||
[region("source.php", "php")],
|
||||
backend=Backend({"source.php": vector(False) + [{"kind": "context_summary"}]}),
|
||||
change_context=context(),
|
||||
)
|
||||
assert result.scope.selected
|
||||
assert result.question_outcomes[0].status == "unresolved"
|
||||
assert any("context_scope_empty" in gap for gap in result.scope.unresolved_boundaries)
|
||||
|
||||
|
||||
def test_missing_base_without_adapter_diagnostic_stays_incomplete(tmp_path):
|
||||
(tmp_path / "source.php").write_text("readable\n")
|
||||
result = scan_repository(
|
||||
tmp_path,
|
||||
[region("source.php", "php")],
|
||||
backend=Backend({"source.php": vector() + [row("source.php"), {"kind": "context_summary"}]}),
|
||||
change_context=replace(context(), base_revision=None),
|
||||
)
|
||||
assert result.question_outcomes[0].status == "unresolved"
|
||||
assert "comparison_base_unavailable" in result.scope.unresolved_boundaries
|
||||
Reference in New Issue
Block a user