@hoanghainh/komagu 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/LICENSE +202 -0
- package/README.md +1 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/ai-run-stats.json +87 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/auth-model.md +120 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/index.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/injection-001.json +383 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/injection-002.json +205 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/privilege-001.json +838 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/privilege-002.json +124 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-001.json +778 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-002.json +546 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-003.json +661 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-004.json +659 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-005.json +612 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-006.json +106 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/coverage.json +19 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-001.json +516 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-002.json +332 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-003.json +218 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-004.json +561 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-005.json +316 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-006.json +14 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/inj-injection-001.json +782 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/inj-injection-002.json +418 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/priv-privilege-001.json +627 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/priv-privilege-002.json +253 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-1.json +68 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-10.json +68 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-11.json +61 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-12.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-13.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-14.json +66 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-15.json +69 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-16.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-17.json +61 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-18.json +69 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-19.json +64 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-2.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-20.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-21.json +61 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-22.json +50 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-3.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-4.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-5.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-6.json +70 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-7.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-8.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-9.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings.json +4857 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/report.ja.md +2112 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/report.md +2112 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/score.json +82 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/stack-profile.json +225 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/ai-run-stats.json +91 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/auth-model.md +120 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/index.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/injection-001.json +383 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/injection-002.json +205 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/privilege-001.json +838 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/privilege-002.json +124 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-001.json +778 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-002.json +546 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-003.json +661 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-004.json +659 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-005.json +612 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-006.json +106 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/coverage.json +21 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-001.json +593 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-002.json +367 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-003.json +207 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-004.json +477 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-005.json +341 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-006.json +14 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/inj-injection-001.json +821 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/inj-injection-002.json +410 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/priv-privilege-001.json +649 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/priv-privilege-002.json +222 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-1.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-10.json +67 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-11.json +68 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-12.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-13.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-14.json +64 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-15.json +70 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-16.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-17.json +61 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-18.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-19.json +65 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-2.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-20.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-21.json +62 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-22.json +50 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-3.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-4.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-5.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-6.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-7.json +64 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-8.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-9.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings.json +4966 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/score.json +87 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/stack-profile.json +225 -0
- package/assess/lsa/examples/eval-run-6/ground-truth.json +851 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/ai-run-stats.json +96 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/auth-model.md +191 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/index.json +60 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/injection-001.json +383 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/injection-002.json +205 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/privilege-001.json +838 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/privilege-002.json +124 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-001.json +778 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-002.json +546 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-003.json +661 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-004.json +659 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-005.json +612 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-006.json +106 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/coverage.json +31 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-001.json +527 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-002.json +568 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-003.json +467 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-004.json +342 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-005.json +693 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-006.json +16 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/inj-injection-001.json +1123 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/inj-injection-002.json +391 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/priv-privilege-001.json +726 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/priv-privilege-002.json +306 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-1.json +137 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-10.json +106 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-11.json +81 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-12.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-13.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-14.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-15.json +110 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-16.json +116 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-17.json +141 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-18.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-19.json +140 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-2.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-20.json +82 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-21.json +74 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-22.json +99 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-3.json +140 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-4.json +145 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-5.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-6.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-7.json +87 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-8.json +125 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-9.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings.json +6989 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/memory.log +36 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/score.json +106 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/stack-profile.json +225 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/auth-model.md +517 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-001.json +5 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-002.json +428 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-003.json +388 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-004.json +463 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-005.json +5 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-006.json +67 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/inj-injection-001.json +964 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings.partial.json +1872 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/memory.log +194 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/score.partial.json +108 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/stack-profile.json +225 -0
- package/assess/lsa/scripts/detect_stack.py +13 -5
- package/assess/lsa/scripts/find_injection.py +35 -2
- package/assess/lsa/scripts/java_routes.py +323 -0
- package/assess/lsa/scripts/laravel_routes.py +102 -0
- package/assess/lsa/scripts/lsa_common.py +72 -2
- package/assess/lsa/scripts/map_routes.py +40 -161
- package/assess/lsa/scripts/merge_findings.py +2 -0
- package/assess/lsa/tests/EVAL-RESULTS.md +126 -0
- package/assess/lsa/tests/test_eval_run_6.py +104 -0
- package/assess/lsa/tests/test_java_legacy_realistic.py +453 -0
- package/assess/lsa/tests/test_laravel_routes.py +95 -0
- package/contracts/schemas/persona-suite.schema.json +38 -2
- package/examples/member-portal/backend-dotnet/Dockerfile +2 -1
- package/examples/member-portal/backend-spring/Dockerfile +2 -0
- package/examples/member-portal/docker-compose.yml +1 -1
- package/examples/member-portal/frontend/Dockerfile +3 -2
- package/examples/member-portal/frontend/package-lock.json +24 -656
- package/examples/member-portal/frontend/package.json +1 -1
- package/operate/ai/client.py +22 -9
- package/operate/ai/runner.py +24 -4
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/fix.py +4 -0
- package/operate/cli/commands/leak_check.py +75 -0
- package/operate/cli/commands/protect.py +32 -3
- package/operate/cli/commands/scan.py +8 -1
- package/operate/cli/commands/test.py +131 -15
- package/operate/cli/main.py +5 -3
- package/operate/cross_user_tester/persona_matrix.py +118 -25
- package/operate/cross_user_tester/runner.py +190 -7
- package/operate/cross_user_tester/session_config.py +180 -0
- package/operate/cross_user_tester/session_http.py +288 -0
- package/operate/devsecops/leak_check.py +314 -0
- package/operate/remediator/runner.py +18 -1
- package/operate/remediator/strategies.py +2 -2
- package/operate/simulate/scenarios.py +2 -2
- package/operate/tests/mock_legacy_session_app.py +160 -0
- package/operate/tests/test_ai_reasoning_effort.py +65 -0
- package/operate/tests/test_ai_route_inventory.py +53 -0
- package/operate/tests/test_cross_user_session.py +565 -0
- package/operate/tests/test_docker_packaging.py +3 -3
- package/operate/tests/test_headless_ai.py +6 -0
- package/operate/tests/test_leak_check.py +250 -0
- package/operate/tests/test_legacy_session_edge.py +192 -0
- package/operate/tests/test_pilot_safeguards.py +245 -0
- package/operate/tests/test_remediator_java_legacy.py +330 -0
- package/package.json +2 -1
- package/remediate/authz/patcher.py +120 -11
- package/remediate/authz/tests/test_bola_patcher.py +4 -1
- package/remediate/edge/coraza/Caddyfile +1 -1
- package/remediate/edge/coraza/Dockerfile +9 -0
- package/remediate/edge/coraza/rules/coraza-crs-custom.conf +12 -0
- package/remediate/edge/coraza/virtual_patcher.py +24 -0
- package/remediate/edge/docker-compose.yml +7 -2
- package/remediate/edge/kong/kong-legacy-session.yaml +51 -0
- package/remediate/edge/mock-backend/Dockerfile +2 -0
- package/remediate/edge/tests/test_edge_security.py +21 -1
- package/remediate/resilience/terraform/backup_worm.tf +39 -1
- package/remediate/resilience/terraform/main.tf +15 -3
- package/remediate/resilience/terraform/variables.tf +11 -0
- package/remediate/resilience/tests/test_resilience.py +16 -0
|
@@ -37,6 +37,11 @@ STRONG_SQL = re.compile(
|
|
|
37
37
|
r"(?i)(\bselect\b[\s\S]*?\bfrom\b|\bselect\s+[\w*]|\binsert\s+into\b|\bupdate\s+[\w\[\].`\"]+\s+set\b|\bupdate\s+[\w\[\].`\"]+\s*$"
|
|
38
38
|
r"|\bdelete\s+from\b|\bwhere\b\s*[\w(`\[]|\border\s+by\b|\bgroup\s+by\b|\bvalues\s*\(|\bexec(ute)?\s+\w|\bunion\s+select\b"
|
|
39
39
|
r"|\bfrom\s+[\w\[\].`\"]+\s+(where|order|group|inner|left|join)\b|\bhaving\b)")
|
|
40
|
+
# Java only: an HTML opening tag is markup, and a literal ending in WHERE is SQL/JPQL about to take a concatenated clause
|
|
41
|
+
HTML_OPEN_TAG = re.compile(r"</?[a-zA-Z][\w:-]*\b")
|
|
42
|
+
JAVA_TRAILING_WHERE = re.compile(r"(?i)\bwhere\s*\"$")
|
|
43
|
+
# JPQL positional parameter: the appended value is the numeric index of a bind mark, as in " = ?").append(size)
|
|
44
|
+
JAVA_POSITIONAL_APPEND = re.compile(r"\?\"\s*\)\s*\.\s*append\s*\(\s*(?:size|index|idx|i|n|pos|position|count|\w*[Ii]ndex)\s*\)\s*;?\s*$")
|
|
40
45
|
WEAK_SQL = re.compile(r"(?i)\b(and|or|like|in|not|between|set|where|order\s+by|limit|offset|values|join|on)\b|=\s*['\"]?$|[(,=<>]\s*'?$")
|
|
41
46
|
SQL_IDENT = re.compile(r"(?i)\b(order\s+by|group\s+by|asc|desc|limit)\b")
|
|
42
47
|
|
|
@@ -132,6 +137,20 @@ def raw_fragment_arg_is_constant(arg: str) -> bool:
|
|
|
132
137
|
return False
|
|
133
138
|
|
|
134
139
|
|
|
140
|
+
def is_sql_literal(text: str, lang: str) -> bool:
|
|
141
|
+
if lang == "java":
|
|
142
|
+
return bool(STRONG_SQL.search(HTML_OPEN_TAG.sub("", text)) or JAVA_TRAILING_WHERE.search(text))
|
|
143
|
+
return bool(STRONG_SQL.search(text))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def java_continuation_is_dynamic(line: str, lit: re.Pattern) -> bool:
|
|
147
|
+
"""True when a continuation line of a multi-line SQL statement joins a non-constant operand."""
|
|
148
|
+
if concat_kind(line, "java", lit) == "dynamic":
|
|
149
|
+
return True
|
|
150
|
+
leading = re.match(r"\s*\+\s*([A-Za-z_][\w.]*)", strip_literals(line, lit))
|
|
151
|
+
return bool(leading and not CONST_OPERAND.match(leading.group(1)))
|
|
152
|
+
|
|
153
|
+
|
|
135
154
|
def strip_literals(line: str, lit: re.Pattern) -> str:
|
|
136
155
|
return lit.sub('""', line)
|
|
137
156
|
|
|
@@ -249,7 +268,7 @@ class Finder:
|
|
|
249
268
|
if com and com.search(line):
|
|
250
269
|
continue
|
|
251
270
|
lits = untagged(line, literals(line, lit), lang)
|
|
252
|
-
if not any(
|
|
271
|
+
if not any(is_sql_literal(m.group(0), lang) for m in lits):
|
|
253
272
|
continue
|
|
254
273
|
if assign_rx:
|
|
255
274
|
m = assign_rx.search(line)
|
|
@@ -259,6 +278,7 @@ class Finder:
|
|
|
259
278
|
if am:
|
|
260
279
|
sqlvars.setdefault(am.group(1), i)
|
|
261
280
|
heredoc_end = None
|
|
281
|
+
open_sql = False
|
|
262
282
|
# pass 2 — flag dynamic building
|
|
263
283
|
for i, line in enumerate(lines, 1):
|
|
264
284
|
if com and com.search(line):
|
|
@@ -283,7 +303,12 @@ class Finder:
|
|
|
283
303
|
self.add(lang, rel, i, lines, "heredoc-interpolation", "string_interpolation", "sql", None, file_has_input)
|
|
284
304
|
continue
|
|
285
305
|
lits = untagged(line, literals(line, lit), lang)
|
|
286
|
-
sql_lits = [m for m in lits if
|
|
306
|
+
sql_lits = [m for m in lits if is_sql_literal(m.group(0), lang)]
|
|
307
|
+
continuing = False
|
|
308
|
+
if lang == "java":
|
|
309
|
+
code = strip_literals(line, lit)
|
|
310
|
+
continuing = open_sql
|
|
311
|
+
open_sql = (bool(sql_lits) or open_sql) and not re.search(r"[;{}]", code)
|
|
287
312
|
target_var = None
|
|
288
313
|
if assign_rx:
|
|
289
314
|
am = assign_rx.search(line)
|
|
@@ -294,6 +319,8 @@ class Finder:
|
|
|
294
319
|
target_var = ap.group(1)
|
|
295
320
|
is_sqlish = bool(sql_lits) or target_var is not None
|
|
296
321
|
if not is_sqlish:
|
|
322
|
+
if continuing and java_continuation_is_dynamic(line, lit):
|
|
323
|
+
self.add(lang, rel, i, lines, "sql-concat", "string_concatenation", "sql", None, file_has_input)
|
|
297
324
|
continue
|
|
298
325
|
# interpolation inside a SQL literal (or SQL-variable literal)
|
|
299
326
|
cand_lits = sql_lits or (lits if target_var else [])
|
|
@@ -317,6 +344,10 @@ class Finder:
|
|
|
317
344
|
self.suppressed.append({"file": rel, "line": i, "reason": "concatenation with constant-looking operands only",
|
|
318
345
|
"snippet": redact(snippet(lines, i))})
|
|
319
346
|
continue
|
|
347
|
+
if lang == "java" and JAVA_POSITIONAL_APPEND.search(line):
|
|
348
|
+
self.suppressed.append({"file": rel, "line": i, "reason": "appended value is a positional bind index",
|
|
349
|
+
"snippet": redact(snippet(lines, i))})
|
|
350
|
+
continue
|
|
320
351
|
if target_var and re.search(r"\.\s*(append|Append|AppendLine|concat)\s*\(\s*(?![\"'@\s])", line):
|
|
321
352
|
self.add(lang, rel, i, lines, "sql-append", "string_concatenation", "sql", target_var, file_has_input)
|
|
322
353
|
continue
|
|
@@ -350,6 +381,8 @@ class Finder:
|
|
|
350
381
|
def scan_xml(self, rel: str, text: str) -> None:
|
|
351
382
|
if not re.search(r"<mapper\b|<sqlMap\b|<!DOCTYPE\s+(mapper|sqlMap)", text):
|
|
352
383
|
return
|
|
384
|
+
if re.search(r"<#(?:if|list|assign|macro|include)\b", text):
|
|
385
|
+
return # FreeMarker code-generator template: ${...} is the template language, not MyBatis
|
|
353
386
|
lines = text.splitlines()
|
|
354
387
|
in_stmt = False
|
|
355
388
|
for i, line in enumerate(lines, 1):
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Declared-route extractors for Java web stacks (web.xml, Struts 1/2, Spring XML, annotations).
|
|
2
|
+
|
|
3
|
+
Used by map_routes.py. Python 3.8+, standard library only.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from lsa_common import blank_java_comments, call_args, iter_source_files, line_of, read_text, split_top
|
|
12
|
+
|
|
13
|
+
JAVA_METHOD_ANN = re.compile(r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|Path)\b")
|
|
14
|
+
JAVA_HTTP_ANN = re.compile(r"@(GET|POST|PUT|DELETE|PATCH)\b")
|
|
15
|
+
JAVA_CLASS_AFTER = re.compile(r"\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:(?:public|protected|private|abstract|final|static)\s+)*"
|
|
16
|
+
r"(?:class|interface)\b")
|
|
17
|
+
# The return type may follow annotations, as in: public @ResponseBody String show(
|
|
18
|
+
JAVA_HANDLER_SIG = re.compile(r"(?:(?:public|protected)\s+(?:@\w+(?:\([^)]*\))?\s+|(?:final|static|synchronized)\s+)*"
|
|
19
|
+
r"[\w<>\[\],.?\s]+?|fun|(?:override|suspend)\s+fun)\s+(\w+)\s*\(")
|
|
20
|
+
JAVA_NEXT_ANN = re.compile(r"\n\s*@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|Path)\b(?!Variable|Param)")
|
|
21
|
+
STRING_LITERAL = re.compile(r"\"((?:[^\"\\]|\\.)*)\"")
|
|
22
|
+
JAVA_STRING_CONSTANT = re.compile(r"\bfinal\s+String\s+(\w+)\s*=\s*\"([^\"]*)\"\s*;")
|
|
23
|
+
SPRING_PLACEHOLDER = re.compile(r"\$\{[^}]*\}")
|
|
24
|
+
XML_COMMENT = re.compile(r"<!--[\s\S]*?-->")
|
|
25
|
+
STRUTS2_MARKER = re.compile(r"<!DOCTYPE\s+struts\b(?!-)|<struts[\s>]")
|
|
26
|
+
STRUTS_EXT_CONSTANT = re.compile(r"<constant\s+name=\"struts\.action\.extension\"\s+value=\"([^\"]*)\"")
|
|
27
|
+
STRUTS_EXT_PROPERTY = re.compile(r"^\s*struts\.action\.extension\s*[=:]\s*(\S*)", re.M)
|
|
28
|
+
STRUTS_MODULE_PARAM = re.compile(r"<param-name>\s*config/([^<\s]+)\s*</param-name>\s*<param-value>\s*([^<]+?)\s*</param-value>")
|
|
29
|
+
SPRING_BEAN = re.compile(r"<bean\b([^>]*)>")
|
|
30
|
+
SPRING_URL_MAPPER = re.compile(r"<bean\b[^>]*class=\"[^\"]*UrlHandlerMapping\"[^>]*>")
|
|
31
|
+
SPRING_URL_PROPS = re.compile(r"<props>([\s\S]*?)</props>")
|
|
32
|
+
SPRING_URL_PROP = re.compile(r"<prop\s+key=\"([^\"]+)\"\s*>\s*([^<]+?)\s*</prop>")
|
|
33
|
+
XML_ATTR = re.compile(r"(\w+)\s*=\s*\"([^\"]*)\"")
|
|
34
|
+
|
|
35
|
+
DEFAULT_ACTION_EXTENSION = "action"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def strip_xml_comments(text: str) -> str:
|
|
39
|
+
"""Blank XML comments out, keeping every other character at its original offset."""
|
|
40
|
+
return XML_COMMENT.sub(lambda m: re.sub(r"[^\n]", " ", m.group(0)), text)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_class_index(rels: Iterable[str]) -> Dict[str, List[str]]:
|
|
44
|
+
"""Map a simple class name to every Java source file with that name."""
|
|
45
|
+
index: Dict[str, List[str]] = {}
|
|
46
|
+
for rel in rels:
|
|
47
|
+
if rel.endswith(".java"):
|
|
48
|
+
index.setdefault(rel.rsplit("/", 1)[-1][:-5], []).append(rel)
|
|
49
|
+
return index
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_class(cls: str, index: Dict[str, List[str]], default: str) -> str:
|
|
53
|
+
"""Find the source file of a class name, matching on the package path when one is given."""
|
|
54
|
+
name = cls.split("$", 1)[0]
|
|
55
|
+
candidates = index.get(name.split(".")[-1], [])
|
|
56
|
+
if "." not in name:
|
|
57
|
+
return candidates[0] if candidates else default
|
|
58
|
+
suffix = name.replace(".", "/") + ".java"
|
|
59
|
+
return next((rel for rel in candidates if rel == suffix or rel.endswith("/" + suffix)), default)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _annotation_args(text: str, m: re.Match) -> Tuple[str, int]:
|
|
63
|
+
"""Return (argument text, offset after the annotation)."""
|
|
64
|
+
paren = re.match(r"\s*\(", text[m.end():])
|
|
65
|
+
if not paren:
|
|
66
|
+
return "", m.end()
|
|
67
|
+
return call_args(text, m.end() + paren.end() - 1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _annotation_attrs(args: str) -> Dict[str, str]:
|
|
71
|
+
attrs: Dict[str, str] = {}
|
|
72
|
+
for part in split_top(args):
|
|
73
|
+
named = re.match(r"\s*(\w+)\s*=", part)
|
|
74
|
+
if named:
|
|
75
|
+
attrs[named.group(1)] = part[named.end():]
|
|
76
|
+
else:
|
|
77
|
+
attrs["value"] = part
|
|
78
|
+
return attrs
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _literals(expr: str) -> List[str]:
|
|
82
|
+
return STRING_LITERAL.findall(expr or "")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _string_constants(text: str) -> Dict[str, str]:
|
|
86
|
+
return dict(JAVA_STRING_CONSTANT.findall(text))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _resolve_term(term: str, constants: Dict[str, str]) -> str:
|
|
90
|
+
term = term.strip()
|
|
91
|
+
literal = STRING_LITERAL.fullmatch(term)
|
|
92
|
+
if literal:
|
|
93
|
+
return literal.group(1)
|
|
94
|
+
name = term.rsplit(".", 1)[-1]
|
|
95
|
+
return constants.get(name, "{" + name + "}" if name else "")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _path_values(expr: str, constants: Dict[str, str]) -> List[str]:
|
|
99
|
+
"""Evaluate a mapping attribute: a literal, a constant, a concatenation of both, or an array of those."""
|
|
100
|
+
inner = expr.strip()
|
|
101
|
+
if inner.startswith("{") and inner.endswith("}"):
|
|
102
|
+
inner = inner[1:-1]
|
|
103
|
+
return ["".join(_resolve_term(t, constants) for t in re.split(r"\+(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", element))
|
|
104
|
+
for element in split_top(inner)]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _mapped_paths(attrs: Dict[str, str], constants: Optional[Dict[str, str]] = None) -> List[str]:
|
|
108
|
+
"""Every mapped path with its required request parameters as a query string."""
|
|
109
|
+
paths = _path_values(attrs.get("value", "") or attrs.get("path", ""), constants or {}) or [""]
|
|
110
|
+
required = [p for p in _literals(attrs.get("params", "")) if not p.startswith("!")]
|
|
111
|
+
query = "?" + "&".join(required) if required else ""
|
|
112
|
+
return [p + query for p in paths]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _join_path(prefix: str, sub: str) -> str:
|
|
116
|
+
sub_path, sep, query = sub.partition("?")
|
|
117
|
+
return "/" + "/".join(p.strip("/") for p in (prefix, sub_path) if p.strip("/")) + sep + query
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _class_mappings(text: str, constants: Dict[str, str]) -> List[Tuple[int, List[str]]]:
|
|
121
|
+
"""(offset, mapped paths) of every class-level @RequestMapping / @Path, in source order."""
|
|
122
|
+
out: List[Tuple[int, List[str]]] = []
|
|
123
|
+
for m in JAVA_METHOD_ANN.finditer(text):
|
|
124
|
+
args, end = _annotation_args(text, m)
|
|
125
|
+
if JAVA_CLASS_AFTER.match(text[end:end + 600]):
|
|
126
|
+
out.append((m.start(), [p.partition("?")[0] for p in _mapped_paths(_annotation_attrs(args), constants)]))
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _servlet_declared(c, rel: str, text: str) -> bool:
|
|
131
|
+
found = False
|
|
132
|
+
ws = re.search(r"@WebServlet\s*\(\s*(?:urlPatterns\s*=\s*|value\s*=\s*)?\{?\s*\"([^\"]+)\"", text)
|
|
133
|
+
for m in re.finditer(r"(?:public|protected)\s+void\s+(doGet|doPost|doPut|doDelete|service)\s*\(", text):
|
|
134
|
+
start = m.start()
|
|
135
|
+
nxt = re.search(r"\n\s*(?:public|protected|private)\s+[\w<>\[\]]+\s+\w+\s*\(", text[m.end():])
|
|
136
|
+
body = text[start: m.end() + (nxt.start() if nxt else len(text))]
|
|
137
|
+
line = line_of(text, start)
|
|
138
|
+
verb = {"doGet": "GET", "doPost": "POST", "doPut": "PUT", "doDelete": "DELETE"}.get(m.group(1), "ANY")
|
|
139
|
+
c.add(stack="java-servlet", kind="declared_route", method=[verb],
|
|
140
|
+
path=ws.group(1) if ws else "<see web.xml servlet-mapping>",
|
|
141
|
+
handler={"file": rel, "line": line, "symbol": m.group(1)},
|
|
142
|
+
id_params=c.inputs("java", body, line), guard_hints=c.guards(body, line),
|
|
143
|
+
data_hints=c.data_ops(body, line), notes="" if ws else "resolve path from web.xml")
|
|
144
|
+
found = True
|
|
145
|
+
return found
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def java_declared(c, rel: str, raw: str) -> bool:
|
|
149
|
+
text = blank_java_comments(raw)
|
|
150
|
+
if re.search(r"extends\s+HttpServlet", text):
|
|
151
|
+
return _servlet_declared(c, rel, text)
|
|
152
|
+
found = False
|
|
153
|
+
constants = _string_constants(text)
|
|
154
|
+
classes = _class_mappings(text, constants)
|
|
155
|
+
for m in JAVA_METHOD_ANN.finditer(text):
|
|
156
|
+
args, ann_end = _annotation_args(text, m)
|
|
157
|
+
after = text[ann_end: ann_end + 600]
|
|
158
|
+
if JAVA_CLASS_AFTER.match(after):
|
|
159
|
+
continue
|
|
160
|
+
sig = JAVA_HANDLER_SIG.search(after)
|
|
161
|
+
if not sig:
|
|
162
|
+
continue
|
|
163
|
+
attrs = _annotation_attrs(args)
|
|
164
|
+
ann = m.group(1)
|
|
165
|
+
verbs = re.findall(r"RequestMethod\.(\w+)", args)
|
|
166
|
+
if ann.endswith("Mapping") and ann != "RequestMapping":
|
|
167
|
+
verbs = [ann[:-7].upper()]
|
|
168
|
+
if ann == "Path":
|
|
169
|
+
hv = JAVA_HTTP_ANN.search(text[max(0, m.start() - 200): ann_end + 200])
|
|
170
|
+
verbs = [hv.group(1)] if hv else ["ANY"]
|
|
171
|
+
nxt = JAVA_NEXT_ANN.search(text, ann_end)
|
|
172
|
+
body = text[m.start(): nxt.start() if nxt else len(text)]
|
|
173
|
+
line = line_of(text, m.start())
|
|
174
|
+
enclosing = [paths for offset, paths in classes if offset < m.start()]
|
|
175
|
+
for prefix in (enclosing[-1] if enclosing else [""]):
|
|
176
|
+
for sub in _mapped_paths(attrs, constants):
|
|
177
|
+
path = _join_path(prefix, sub)
|
|
178
|
+
c.add(stack="java-spring-mvc" if ann != "Path" else "java-jaxrs", kind="declared_route",
|
|
179
|
+
method=verbs or ["ANY"], path=path,
|
|
180
|
+
handler={"file": rel, "line": line, "symbol": sig.group(1)},
|
|
181
|
+
id_params=c.path_params(SPRING_PLACEHOLDER.sub("", path)) + c.inputs("java", body, line),
|
|
182
|
+
guard_hints=c.guards(text[:m.start()][-1500:], 1)[:2] + c.guards(body, line),
|
|
183
|
+
data_hints=c.data_ops(body, line), notes="")
|
|
184
|
+
found = True
|
|
185
|
+
# Struts 1 / 2 Action classes without annotations are mapped from XML (see xml_declared)
|
|
186
|
+
return found
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def struts_modules(rel: str, text: str) -> Dict[str, str]:
|
|
190
|
+
"""Struts 1 module prefix for every config file a web.xml assigns to a module."""
|
|
191
|
+
webapp = rel.rsplit("WEB-INF/", 1)[0]
|
|
192
|
+
modules: Dict[str, str] = {}
|
|
193
|
+
for m in STRUTS_MODULE_PARAM.finditer(strip_xml_comments(text)):
|
|
194
|
+
for value in m.group(2).split(","):
|
|
195
|
+
modules[webapp + value.strip().lstrip("/")] = "/" + m.group(1)
|
|
196
|
+
return modules
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def struts2_action_extension(target: Path, config: Dict, xml_texts: Iterable[str], encodings: List[str]) -> str:
|
|
200
|
+
"""The URL extension of Struts 2 actions: struts.properties overrides constants in struts.xml."""
|
|
201
|
+
value: Optional[str] = None
|
|
202
|
+
for text in xml_texts:
|
|
203
|
+
m = STRUTS_EXT_CONSTANT.search(strip_xml_comments(text))
|
|
204
|
+
value = m.group(1) if m else value
|
|
205
|
+
for path, rel in iter_source_files(target, config, exts={".properties"}):
|
|
206
|
+
if rel.rsplit("/", 1)[-1] == "struts.properties":
|
|
207
|
+
m = STRUTS_EXT_PROPERTY.search(read_text(path, encodings))
|
|
208
|
+
value = m.group(1) if m else value
|
|
209
|
+
if value is None:
|
|
210
|
+
return DEFAULT_ACTION_EXTENSION
|
|
211
|
+
return next((e for e in value.split(",") if e), "")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def collect_spring_beans(text: str) -> Dict[str, str]:
|
|
215
|
+
"""Map bean id / name to class for one Spring XML file."""
|
|
216
|
+
beans: Dict[str, str] = {}
|
|
217
|
+
for m in SPRING_BEAN.finditer(strip_xml_comments(text)):
|
|
218
|
+
attrs = dict(XML_ATTR.findall(m.group(1)))
|
|
219
|
+
if attrs.get("class"):
|
|
220
|
+
for key in filter(None, (attrs.get("id"), attrs.get("name"))):
|
|
221
|
+
beans[key] = attrs["class"]
|
|
222
|
+
return beans
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def entry_method_line(text: str, symbol: Optional[str]) -> Optional[int]:
|
|
226
|
+
"""Line of the method a route declared in XML dispatches to, or None when the class has no known entry method."""
|
|
227
|
+
named = symbol.split("#", 1)[1] if symbol and "#" in symbol else None
|
|
228
|
+
names = [re.escape(named)] if named else ["execute", "doPost", "doGet", "service", "handleRequest"]
|
|
229
|
+
for name in names:
|
|
230
|
+
m = re.search(rf"\b(?:public|protected)\b[^;{{}}(]*\b{name}\s*\(", text)
|
|
231
|
+
if m:
|
|
232
|
+
return text.count("\n", 0, m.end()) + 1
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _spring_url_mappings(c, rel: str, text: str, class_index: Dict[str, List[str]], beans: Dict[str, str]) -> bool:
|
|
237
|
+
found = False
|
|
238
|
+
mappers = list(SPRING_URL_MAPPER.finditer(text))
|
|
239
|
+
for i, mapper in enumerate(mappers):
|
|
240
|
+
# nested <bean> elements (interceptors) make </bean> useless as the end of the mapper definition
|
|
241
|
+
end = mappers[i + 1].start() if i + 1 < len(mappers) else len(text)
|
|
242
|
+
props = SPRING_URL_PROPS.search(text, mapper.end(), end)
|
|
243
|
+
if not props:
|
|
244
|
+
continue
|
|
245
|
+
for m in SPRING_URL_PROP.finditer(props.group(1)):
|
|
246
|
+
bean = m.group(2)
|
|
247
|
+
cls = beans.get(bean, "")
|
|
248
|
+
c.add(stack="java-spring-mvc", kind="declared_route", method=["ANY"], path=m.group(1),
|
|
249
|
+
handler={"file": resolve_class(cls, class_index, rel) if cls else rel, "line": 1, "symbol": cls or bean},
|
|
250
|
+
id_params=[], guard_hints=[], data_hints=[],
|
|
251
|
+
notes=f"declared in {rel}:{line_of(text, props.start(1) + m.start())}; UrlHandlerMapping bean '{bean}'")
|
|
252
|
+
found = True
|
|
253
|
+
return found
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _web_xml_routes(c, rel: str, text: str, class_index: Dict[str, List[str]]) -> bool:
|
|
257
|
+
found = False
|
|
258
|
+
servlets: Dict[str, str] = {}
|
|
259
|
+
for sm in re.finditer(r"<servlet>([\s\S]*?)</servlet>", text):
|
|
260
|
+
name = re.search(r"<servlet-name>\s*([^<]+?)\s*</servlet-name>", sm.group(1))
|
|
261
|
+
cls = re.search(r"<servlet-class>\s*([^<]+?)\s*</servlet-class>", sm.group(1))
|
|
262
|
+
if name and cls:
|
|
263
|
+
servlets[name.group(1)] = cls.group(1)
|
|
264
|
+
for m in re.finditer(r"<servlet-mapping>([\s\S]*?)</servlet-mapping>", text):
|
|
265
|
+
name = re.search(r"<servlet-name>\s*([^<]+?)\s*</servlet-name>", m.group(1))
|
|
266
|
+
if not name:
|
|
267
|
+
continue
|
|
268
|
+
cls = servlets.get(name.group(1), "")
|
|
269
|
+
for pattern in re.findall(r"<url-pattern>\s*([^<]+?)\s*</url-pattern>", m.group(1)):
|
|
270
|
+
c.add(stack="java-servlet", kind="declared_route", method=["ANY"], path=pattern,
|
|
271
|
+
handler={"file": resolve_class(cls, class_index, rel), "line": 1, "symbol": cls or name.group(1)},
|
|
272
|
+
id_params=[], guard_hints=[], data_hints=[], notes=f"mapped in {rel}:{line_of(text, m.start())}")
|
|
273
|
+
found = True
|
|
274
|
+
return found
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _struts1_routes(c, rel: str, text: str, class_index: Dict[str, List[str]], module: str) -> bool:
|
|
278
|
+
found = False
|
|
279
|
+
for m in re.finditer(r"<action\b([^>]*)>", text):
|
|
280
|
+
attrs = dict(XML_ATTR.findall(m.group(1)))
|
|
281
|
+
if "path" not in attrs:
|
|
282
|
+
continue
|
|
283
|
+
cls = attrs.get("type", "")
|
|
284
|
+
note = f"declared in {rel}:{line_of(text, m.start())}"
|
|
285
|
+
if attrs.get("parameter"):
|
|
286
|
+
note += f"; DispatchAction parameter '{attrs['parameter']}' -> one endpoint per method"
|
|
287
|
+
c.add(stack="java-struts1", kind="declared_route", method=["ANY"], path=module + attrs["path"] + ".do",
|
|
288
|
+
handler={"file": resolve_class(cls, class_index, rel), "line": 1, "symbol": cls}, id_params=[],
|
|
289
|
+
guard_hints=[], data_hints=[], notes=note)
|
|
290
|
+
found = True
|
|
291
|
+
return found
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _struts2_routes(c, rel: str, text: str, class_index: Dict[str, List[str]], extension: str) -> bool:
|
|
295
|
+
found = False
|
|
296
|
+
suffix = "." + extension if extension else ""
|
|
297
|
+
for pm in re.finditer(r"<package\b([^>]*)>([\s\S]*?)</package>", text):
|
|
298
|
+
ns = dict(XML_ATTR.findall(pm.group(1))).get("namespace", "/").rstrip("/")
|
|
299
|
+
for am in re.finditer(r"<action\b([^>]*)>", pm.group(2)):
|
|
300
|
+
attrs = dict(XML_ATTR.findall(am.group(1)))
|
|
301
|
+
cls = attrs.get("class", "")
|
|
302
|
+
c.add(stack="java-struts2", kind="declared_route", method=["ANY"], path=f"{ns}/{attrs.get('name', '?')}{suffix}",
|
|
303
|
+
handler={"file": resolve_class(cls, class_index, rel), "line": 1, "symbol": f"{cls}#{attrs.get('method', 'execute')}"},
|
|
304
|
+
id_params=[], guard_hints=[], data_hints=[],
|
|
305
|
+
notes=f"declared in {rel}:{line_of(text, pm.start(2) + am.start())}; action setters are inputs")
|
|
306
|
+
found = True
|
|
307
|
+
return found
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def xml_declared(c, rel: str, raw: str, class_index: Dict[str, List[str]], modules: Optional[Dict[str, str]] = None,
|
|
311
|
+
beans: Optional[Dict[str, str]] = None, action_extension: str = DEFAULT_ACTION_EXTENSION) -> bool:
|
|
312
|
+
"""Extract every route a deployment or framework XML file declares, ignoring commented-out ones."""
|
|
313
|
+
text = strip_xml_comments(raw)
|
|
314
|
+
found = False
|
|
315
|
+
if rel.endswith("web.xml"):
|
|
316
|
+
found = _web_xml_routes(c, rel, text, class_index) or found
|
|
317
|
+
if "struts-config" in rel:
|
|
318
|
+
found = _struts1_routes(c, rel, text, class_index, (modules or {}).get(rel, "")) or found
|
|
319
|
+
if rel.endswith("struts.xml") or STRUTS2_MARKER.search(text):
|
|
320
|
+
found = _struts2_routes(c, rel, text, class_index, action_extension) or found
|
|
321
|
+
if "UrlHandlerMapping" in text:
|
|
322
|
+
found = _spring_url_mappings(c, rel, text, class_index, beans or {}) or found
|
|
323
|
+
return found
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Resolve Laravel route declarations to the controller method that handles them (#380).
|
|
2
|
+
|
|
3
|
+
`Route::post('/x', [AccountController::class, 'register'])` is found in `routes/web.php`, but the
|
|
4
|
+
code an auditor needs is `AccountController::register`. Without this step every Laravel route
|
|
5
|
+
pointed at its declaration line, so the auditors read the route file and never the controller.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from lsa_common import line_of, read_text
|
|
14
|
+
|
|
15
|
+
# [XController::class, 'method'] or 'XController@method' / 'App\Http\Controllers\XController@method'
|
|
16
|
+
ROUTE_ACTION = re.compile(
|
|
17
|
+
r"\[\s*\\?([\w\\]+)::class\s*,\s*['\"](\w+)['\"]\s*\]"
|
|
18
|
+
r"|['\"]\\?([\w\\]+)@(\w+)['\"]"
|
|
19
|
+
)
|
|
20
|
+
PHP_FUNCTION = re.compile(
|
|
21
|
+
r"^[ \t]*(?:(?:public|protected|private|static|final|abstract)\s+)*function\s+&?(\w+)\s*\(", re.M)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def route_action(stmt: str) -> Optional[str]:
|
|
25
|
+
"""`Class@method` named by a Laravel route statement (class without namespace), or None."""
|
|
26
|
+
m = ROUTE_ACTION.search(stmt)
|
|
27
|
+
if not m:
|
|
28
|
+
return None
|
|
29
|
+
cls, method = (m.group(1), m.group(2)) if m.group(1) else (m.group(3), m.group(4))
|
|
30
|
+
return f"{cls.rsplit(chr(92), 1)[-1]}@{method}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def php_class_index(rels: Iterable[str]) -> Dict[str, List[str]]:
|
|
34
|
+
"""Map a PHP file's base name (the class name by PSR-4 convention) to every file with that name."""
|
|
35
|
+
index: Dict[str, List[str]] = {}
|
|
36
|
+
for rel in rels:
|
|
37
|
+
if rel.endswith(".php"):
|
|
38
|
+
index.setdefault(rel.rsplit("/", 1)[-1][:-4], []).append(rel)
|
|
39
|
+
return index
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_method(text: str, cls: str, method: str) -> Optional[Tuple[int, int]]:
|
|
43
|
+
"""(start, end) offsets of `method` in class `cls`, or None when the class or method is absent."""
|
|
44
|
+
if not re.search(rf"\bclass\s+{re.escape(cls)}\b", text):
|
|
45
|
+
return None
|
|
46
|
+
starts = list(PHP_FUNCTION.finditer(text))
|
|
47
|
+
for i, m in enumerate(starts):
|
|
48
|
+
if m.group(1) == method:
|
|
49
|
+
end = starts[i + 1].start() if i + 1 < len(starts) else len(text)
|
|
50
|
+
return m.start(), end
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _merge(existing: List[Dict[str, Any]], extra: List[Dict[str, Any]],
|
|
55
|
+
key: Callable[[Dict[str, Any]], Any]) -> List[Dict[str, Any]]:
|
|
56
|
+
seen = {key(d) for d in existing}
|
|
57
|
+
return existing + [d for d in extra if key(d) not in seen]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _locate(index: Dict[str, List[str]], texts: Dict[str, str], target: Path, enc: Any,
|
|
61
|
+
cls: str, method: str) -> Optional[Tuple[str, int, int]]:
|
|
62
|
+
for rel in index.get(cls, []):
|
|
63
|
+
if rel not in texts:
|
|
64
|
+
texts[rel] = read_text(target / rel, enc)
|
|
65
|
+
span = find_method(texts[rel], cls, method)
|
|
66
|
+
if span:
|
|
67
|
+
return rel, span[0], span[1]
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_laravel_handlers(items: List[Dict[str, Any]], target: Path, enc: Any, collector: Any,
|
|
72
|
+
rels: Iterable[str]) -> List[Dict[str, Any]]:
|
|
73
|
+
"""Return items with Laravel routes pointed at their controller method, when it can be found.
|
|
74
|
+
|
|
75
|
+
Inputs, guard hints and data hints from the method body are added to those taken from the route
|
|
76
|
+
statement. A route whose controller or method is not in the repo keeps its declaration.
|
|
77
|
+
"""
|
|
78
|
+
index = php_class_index(rels)
|
|
79
|
+
texts: Dict[str, str] = {}
|
|
80
|
+
out: List[Dict[str, Any]] = []
|
|
81
|
+
for it in items:
|
|
82
|
+
symbol = (it.get("handler") or {}).get("symbol") or ""
|
|
83
|
+
found = (_locate(index, texts, target, enc, *symbol.split("@", 1))
|
|
84
|
+
if it.get("stack") == "php-laravel" and "@" in symbol else None)
|
|
85
|
+
if not found:
|
|
86
|
+
out.append(it)
|
|
87
|
+
continue
|
|
88
|
+
rel, start, end = found
|
|
89
|
+
line = line_of(texts[rel], start)
|
|
90
|
+
body = texts[rel][start:end]
|
|
91
|
+
out.append({
|
|
92
|
+
**it,
|
|
93
|
+
"handler": {"file": rel, "line": line, "symbol": symbol},
|
|
94
|
+
"id_params": _merge(list(it.get("id_params") or []), collector.inputs("php", body, line),
|
|
95
|
+
lambda d: (d.get("name"), d.get("source"))),
|
|
96
|
+
"guard_hints": _merge(list(it.get("guard_hints") or []), collector.guards(body, line),
|
|
97
|
+
lambda d: d.get("hint")),
|
|
98
|
+
"data_hints": _merge(list(it.get("data_hints") or []), collector.data_ops(body, line),
|
|
99
|
+
lambda d: (d.get("op"), d.get("line"))),
|
|
100
|
+
"notes": f"declared in {it['handler']['file']}:{it['handler']['line']}",
|
|
101
|
+
})
|
|
102
|
+
return out
|
|
@@ -82,12 +82,32 @@ def read_text(path: Path, encodings: List[str]) -> str:
|
|
|
82
82
|
return data.decode("latin-1", errors="replace")
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
# Default exclude names that are also ordinary Java package and web directory names
|
|
86
|
+
AMBIGUOUS_DIR_EXCLUDES = {"vendor/", "packages/", "bin/", "obj/", "target/", "build/", "dist/",
|
|
87
|
+
"test/", "tests/", "spec/", "docs/", "out/"}
|
|
88
|
+
JAVA_PACKAGE_ROOT = re.compile(r"(?:^|/)src/main/java/|(?:^|/)(?=(?:com|org|net|jp|vn|cn|kr|de|fr|uk|io|edu|gov|biz|info)/[\w-]+/)")
|
|
89
|
+
JAVA_WEB_ROOT = re.compile(r"(?:^|/)(?:src/main/webapp|WebContent|WebRoot|webapp)/")
|
|
90
|
+
JAVA_TREE_EXTS = (".java", ".kt", ".groovy", ".jsp", ".xml")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _inside_java_tree(rel_slash: str, index: int, is_dir: bool) -> bool:
|
|
94
|
+
"""True when the directory at `index` is a package inside a Java source tree or a directory under a web root."""
|
|
95
|
+
name = rel_slash.rstrip("/")
|
|
96
|
+
package_root = JAVA_PACKAGE_ROOT.search(rel_slash)
|
|
97
|
+
if package_root and package_root.start() <= index and (is_dir or name.endswith(JAVA_TREE_EXTS)):
|
|
98
|
+
return True
|
|
99
|
+
web_root = JAVA_WEB_ROOT.search(rel_slash)
|
|
100
|
+
return bool(web_root and web_root.start() <= index and (is_dir or name.endswith(".jsp")))
|
|
101
|
+
|
|
102
|
+
|
|
85
103
|
def _excluded(rel: str, excludes: List[str]) -> bool:
|
|
86
|
-
|
|
104
|
+
is_dir = rel.endswith("/")
|
|
105
|
+
rel_slash = rel.replace(os.sep, "/") + ("" if is_dir else "/")
|
|
87
106
|
for ex in excludes:
|
|
88
107
|
ex = ex.replace("\\", "/")
|
|
89
108
|
if ex.endswith("/"):
|
|
90
|
-
|
|
109
|
+
index = ("/" + rel_slash).find("/" + ex)
|
|
110
|
+
if index != -1 and not (ex in AMBIGUOUS_DIR_EXCLUDES and _inside_java_tree(rel_slash, index, is_dir)):
|
|
91
111
|
return True
|
|
92
112
|
elif re.fullmatch(ex.replace(".", r"\.").replace("*", ".*"), rel.replace(os.sep, "/")):
|
|
93
113
|
return True
|
|
@@ -137,6 +157,56 @@ def iter_source_files(root: Path, config: Dict, exts: Optional[set] = None) -> I
|
|
|
137
157
|
yield p, rel
|
|
138
158
|
|
|
139
159
|
|
|
160
|
+
JAVA_COMMENT = re.compile(r"(?m:^[ \t]*)(?:/\*[\s\S]*?\*/|//[^\n]*)")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def blank_java_comments(text: str) -> str:
|
|
164
|
+
"""Blank whole-line Java comments, keeping every other character at its original offset."""
|
|
165
|
+
return JAVA_COMMENT.sub(lambda m: re.sub(r"[^\n]", " ", m.group(0)), text)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def call_args(text: str, open_idx: int, limit: int = 4000) -> Tuple[str, int]:
|
|
169
|
+
"""Return (argument text, index after the closing paren) for the call whose '(' is at open_idx."""
|
|
170
|
+
depth, i, end = 0, open_idx, min(len(text), open_idx + limit)
|
|
171
|
+
while i < end:
|
|
172
|
+
ch = text[i]
|
|
173
|
+
if ch in "\"`":
|
|
174
|
+
close = text.find(ch, i + 1)
|
|
175
|
+
while ch == '"' and close != -1 and text[close - 1] == "\\":
|
|
176
|
+
close = text.find(ch, close + 1)
|
|
177
|
+
i = close if close != -1 else end
|
|
178
|
+
elif ch in "([{":
|
|
179
|
+
depth += 1
|
|
180
|
+
elif ch in ")]}":
|
|
181
|
+
depth -= 1
|
|
182
|
+
if depth == 0:
|
|
183
|
+
return text[open_idx + 1:i], i + 1
|
|
184
|
+
i += 1
|
|
185
|
+
return text[open_idx + 1:end], end
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def split_top(args: str) -> List[str]:
|
|
189
|
+
"""Split call arguments on top-level commas."""
|
|
190
|
+
parts, depth, cur, quote = [], 0, [], None
|
|
191
|
+
for ch in args:
|
|
192
|
+
if quote:
|
|
193
|
+
quote = None if ch == quote else quote
|
|
194
|
+
elif ch in "\"`":
|
|
195
|
+
quote = ch
|
|
196
|
+
elif ch in "([{":
|
|
197
|
+
depth += 1
|
|
198
|
+
elif ch in ")]}":
|
|
199
|
+
depth -= 1
|
|
200
|
+
elif ch == "," and depth == 0:
|
|
201
|
+
parts.append("".join(cur).strip())
|
|
202
|
+
cur = []
|
|
203
|
+
continue
|
|
204
|
+
cur.append(ch)
|
|
205
|
+
if "".join(cur).strip():
|
|
206
|
+
parts.append("".join(cur).strip())
|
|
207
|
+
return parts
|
|
208
|
+
|
|
209
|
+
|
|
140
210
|
def line_of(text: str, index: int) -> int:
|
|
141
211
|
return text.count("\n", 0, index) + 1
|
|
142
212
|
|