@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
package/operate/ai/client.py
CHANGED
|
@@ -12,6 +12,9 @@ import urllib.error
|
|
|
12
12
|
import urllib.request
|
|
13
13
|
from typing import Any, Dict, List, Optional, Tuple
|
|
14
14
|
|
|
15
|
+
# OpenAI-compatible `reasoning_effort` values; Ollama's /v1 also accepts "none" to turn thinking off (#382)
|
|
16
|
+
REASONING_EFFORTS = ("none", "low", "medium", "high")
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
class LLMClient:
|
|
17
20
|
"""Standard-library client for OpenAI-compatible /chat/completions endpoints."""
|
|
@@ -22,7 +25,10 @@ class LLMClient:
|
|
|
22
25
|
model: str = "llama3.1",
|
|
23
26
|
api_key: Optional[str] = None,
|
|
24
27
|
timeout: int = 120,
|
|
28
|
+
reasoning_effort: Optional[str] = None,
|
|
25
29
|
) -> None:
|
|
30
|
+
if reasoning_effort is not None and reasoning_effort not in REASONING_EFFORTS:
|
|
31
|
+
raise ValueError(f"reasoning_effort must be one of {', '.join(REASONING_EFFORTS)}")
|
|
26
32
|
raw_endpoint = endpoint.rstrip("/")
|
|
27
33
|
if not raw_endpoint.endswith("/chat/completions"):
|
|
28
34
|
self.endpoint = f"{raw_endpoint}/chat/completions"
|
|
@@ -31,6 +37,7 @@ class LLMClient:
|
|
|
31
37
|
self.model = model
|
|
32
38
|
self.api_key = api_key or "ollama"
|
|
33
39
|
self.timeout = timeout
|
|
40
|
+
self.reasoning_effort = reasoning_effort
|
|
34
41
|
# Running totals across chat() calls, read by the runner for ai-run-stats.json
|
|
35
42
|
self.usage: Dict[str, float] = {"calls": 0, "prompt_tokens": 0, "completion_tokens": 0, "seconds": 0.0}
|
|
36
43
|
|
|
@@ -41,15 +48,7 @@ class LLMClient:
|
|
|
41
48
|
max_tokens: Optional[int] = None,
|
|
42
49
|
) -> str:
|
|
43
50
|
"""Send a chat completion request and return the assistant response content."""
|
|
44
|
-
|
|
45
|
-
"model": self.model,
|
|
46
|
-
"messages": messages,
|
|
47
|
-
"temperature": temperature,
|
|
48
|
-
}
|
|
49
|
-
if max_tokens:
|
|
50
|
-
payload["max_tokens"] = max_tokens
|
|
51
|
-
|
|
52
|
-
data = json.dumps(payload).encode("utf-8")
|
|
51
|
+
data = json.dumps(self.build_payload(messages, temperature, max_tokens)).encode("utf-8")
|
|
53
52
|
headers = {
|
|
54
53
|
"Content-Type": "application/json",
|
|
55
54
|
"Authorization": f"Bearer {self.api_key}",
|
|
@@ -74,6 +73,20 @@ class LLMClient:
|
|
|
74
73
|
except urllib.error.URLError as e:
|
|
75
74
|
raise RuntimeError(f"Failed to connect to LLM endpoint at {self.endpoint}: {e.reason}") from e
|
|
76
75
|
|
|
76
|
+
def build_payload(
|
|
77
|
+
self,
|
|
78
|
+
messages: List[Dict[str, str]],
|
|
79
|
+
temperature: float = 0.1,
|
|
80
|
+
max_tokens: Optional[int] = None,
|
|
81
|
+
) -> Dict[str, Any]:
|
|
82
|
+
"""Request body for /chat/completions; `reasoning_effort` is sent only when it was set."""
|
|
83
|
+
payload: Dict[str, Any] = {"model": self.model, "messages": messages, "temperature": temperature}
|
|
84
|
+
if max_tokens:
|
|
85
|
+
payload["max_tokens"] = max_tokens
|
|
86
|
+
if self.reasoning_effort:
|
|
87
|
+
payload["reasoning_effort"] = self.reasoning_effort
|
|
88
|
+
return payload
|
|
89
|
+
|
|
77
90
|
def _record_usage(self, usage: Dict[str, Any], seconds: float) -> None:
|
|
78
91
|
self.usage = {
|
|
79
92
|
"calls": self.usage["calls"] + 1,
|
package/operate/ai/runner.py
CHANGED
|
@@ -15,6 +15,7 @@ Standard-library only (Python 3.8+ compatible).
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
|
+
import re
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
import subprocess
|
|
20
21
|
import sys
|
|
@@ -35,6 +36,7 @@ FINDING_STATUSES = {"unverified", "confirmed", "likely", "rejected", "needs_revi
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
VERIFY_BATCH_SIZE = 5
|
|
39
|
+
ID_INPUT = re.compile(r"(?i)(?:^id$|id$|_no$|no$|key$|code$)")
|
|
38
40
|
AUTH_MODEL_LIMIT = 6000
|
|
39
41
|
RETRY_REMINDER = ("Your previous reply was empty or not valid JSON. Answer again with the JSON object only, "
|
|
40
42
|
"exactly in the format requested above, with no other text.")
|
|
@@ -165,6 +167,7 @@ class HeadlessAIRunner:
|
|
|
165
167
|
stats = {
|
|
166
168
|
"model": getattr(self.client, "model", None),
|
|
167
169
|
"endpoint": getattr(self.client, "endpoint", None),
|
|
170
|
+
"reasoning_effort": getattr(self.client, "reasoning_effort", None),
|
|
168
171
|
"phases": self.phase_stats,
|
|
169
172
|
"totals": self._usage(),
|
|
170
173
|
"parse_failures": self.parse_failures,
|
|
@@ -298,8 +301,9 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
|
|
|
298
301
|
{"name": p.get("name", "id"), "source": p.get("source", "path")}
|
|
299
302
|
for p in c.get("id_params", [])
|
|
300
303
|
]
|
|
301
|
-
|
|
302
|
-
|
|
304
|
+
# No detected id input stays empty: inventing a path `id` made the BOLA auditor
|
|
305
|
+
# flag routes that take no object id (#380)
|
|
306
|
+
id_inputs = id_inputs or []
|
|
303
307
|
|
|
304
308
|
mapped_routes.append({
|
|
305
309
|
"route_id": route_id,
|
|
@@ -321,6 +325,16 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
|
|
|
321
325
|
|
|
322
326
|
self._log("ok", f"Mapped {len(route_batches)} route inventories")
|
|
323
327
|
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _with_route_target(finding: Dict[str, Any], route: Dict[str, Any]) -> Dict[str, Any]:
|
|
330
|
+
"""Record the request parameter holding the object id, which `komagu test --from-scan` needs."""
|
|
331
|
+
attack_vector = finding.get("attack_vector") if isinstance(finding.get("attack_vector"), dict) else {}
|
|
332
|
+
if attack_vector.get("target_param"):
|
|
333
|
+
return finding
|
|
334
|
+
name = next((i.get("name") for i in route.get("id_inputs") or []
|
|
335
|
+
if isinstance(i, dict) and ID_INPUT.search(str(i.get("name") or ""))), None)
|
|
336
|
+
return {**finding, "attack_vector": {**attack_vector, "target_param": name}} if name else finding
|
|
337
|
+
|
|
324
338
|
@staticmethod
|
|
325
339
|
def _sanitize_finding(f: Dict[str, Any]) -> Dict[str, Any]:
|
|
326
340
|
"""Normalize finding fields to strictly comply with finding.schema.json."""
|
|
@@ -330,6 +344,11 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
|
|
|
330
344
|
if isinstance(f.get("impact"), list):
|
|
331
345
|
f["impact"] = "; ".join(str(i) for i in f["impact"])
|
|
332
346
|
|
|
347
|
+
# routes reach the model with a method list and models echo it; the schema wants one string joined with |
|
|
348
|
+
endpoint = f.get("endpoint")
|
|
349
|
+
if isinstance(endpoint, dict) and isinstance(endpoint.get("method"), list):
|
|
350
|
+
f["endpoint"] = {**endpoint, "method": "|".join(str(m) for m in endpoint["method"])}
|
|
351
|
+
|
|
333
352
|
# a verdict written into subtype belongs in status; fall back to the category's first subtype
|
|
334
353
|
allowed = CATEGORY_SUBTYPES.get(f.get("category"))
|
|
335
354
|
if allowed and f.get("subtype") not in allowed:
|
|
@@ -461,9 +480,10 @@ Write one finding for every "vulnerable" verdict.
|
|
|
461
480
|
raw_findings = parsed.get("findings", [])
|
|
462
481
|
findings = self._anchor([self._sanitize_finding(f) for f in raw_findings])
|
|
463
482
|
route_verdicts = parsed.get("route_verdicts", [])
|
|
483
|
+
by_route = {r.get("route_id"): r for r in routes_to_audit}
|
|
464
484
|
findings = self._with_verdict_findings(
|
|
465
|
-
"4a-bola-auditor", findings, route_verdicts, "route_id",
|
|
466
|
-
|
|
485
|
+
"4a-bola-auditor", findings, route_verdicts, "route_id", by_route, "bola")
|
|
486
|
+
findings = [self._with_route_target(f, by_route.get(f.get("route_id")) or {}) for f in findings]
|
|
467
487
|
except Exception:
|
|
468
488
|
self._parse_failed("4a-bola-auditor", r_file.stem, reply)
|
|
469
489
|
findings = []
|
package/operate/cli/__init__.py
CHANGED
|
@@ -130,6 +130,10 @@ def run_fix(args: argparse.Namespace) -> int:
|
|
|
130
130
|
for plan in report.plans:
|
|
131
131
|
if plan.diff:
|
|
132
132
|
print(plan.diff)
|
|
133
|
+
else:
|
|
134
|
+
# stderr keeps stdout a valid patch; without this a run with only manual fixes prints nothing
|
|
135
|
+
print(f"[{plan.status.upper()}] {plan.finding_id} {plan.target_file}:{plan.start_line} - {plan.reason}",
|
|
136
|
+
file=sys.stderr)
|
|
133
137
|
return 0
|
|
134
138
|
|
|
135
139
|
step("[2/2] Remediation execution completed")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Command: komagu leak-check - find secrets that reached pilot outputs (runbook §9)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import dataclasses
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Tuple
|
|
10
|
+
|
|
11
|
+
from operate.devsecops.leak_check import CanaryFileError, ScanResult, load_canaries, scan_paths
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_subcommand(subparsers: argparse._SubParsersAction) -> None:
|
|
15
|
+
"""Register the leak-check subcommand parser."""
|
|
16
|
+
parser = subparsers.add_parser(
|
|
17
|
+
"leak-check",
|
|
18
|
+
help="Scan ai-raw/, findings and reports for leaked secrets and client canary strings",
|
|
19
|
+
description=(
|
|
20
|
+
"Checks audit output for secrets the redaction step missed. Values are always masked in the report. "
|
|
21
|
+
"Exit codes: 0 clean, 1 suspected leak, 2 incomplete scan or bad input."
|
|
22
|
+
),
|
|
23
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
24
|
+
epilog="""
|
|
25
|
+
Examples:
|
|
26
|
+
komagu leak-check ~/pilot/out --canary-file ~/pilot/canaries.txt
|
|
27
|
+
komagu leak-check ~/pilot/out/ai-raw ~/pilot/out/report.md --format json > ~/pilot/leak-check.json
|
|
28
|
+
""",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("paths", nargs="+", metavar="PATH", help="Output directories or files to scan")
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--canary-file",
|
|
33
|
+
help="UTF-8 file with one client-provided canary string per line; keep it outside the repository",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument("--format", choices=["text", "json"], default="text", help="Report format (default: text)")
|
|
36
|
+
parser.set_defaults(func=run)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _print_text(result: ScanResult) -> None:
|
|
40
|
+
for hit in result.hits:
|
|
41
|
+
print(f"{hit.path}:{hit.line}: {hit.detector}: {hit.rule}: {hit.masked}")
|
|
42
|
+
for path, reason in result.unscanned:
|
|
43
|
+
print(f"NOT SCANNED {path}: {reason}")
|
|
44
|
+
print(
|
|
45
|
+
f"leak-check: {len(result.hits)} suspected leak(s), {result.files_scanned} file(s) scanned, "
|
|
46
|
+
f"{len(result.unscanned)} not scanned, {result.canaries_loaded} canary string(s)"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _print_json(result: ScanResult) -> None:
|
|
51
|
+
report = {
|
|
52
|
+
"exitCode": result.exit_code,
|
|
53
|
+
"filesScanned": result.files_scanned,
|
|
54
|
+
"canariesLoaded": result.canaries_loaded,
|
|
55
|
+
"hits": [dataclasses.asdict(hit) for hit in result.hits],
|
|
56
|
+
"notScanned": [{"path": path, "reason": reason} for path, reason in result.unscanned],
|
|
57
|
+
}
|
|
58
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run(args: argparse.Namespace) -> int:
|
|
62
|
+
"""Run the scan and return its exit code."""
|
|
63
|
+
canaries: Tuple[str, ...] = ()
|
|
64
|
+
if args.canary_file is not None:
|
|
65
|
+
try:
|
|
66
|
+
canaries = load_canaries(args.canary_file)
|
|
67
|
+
except CanaryFileError as exc:
|
|
68
|
+
print(f"leak-check: {exc}", file=sys.stderr)
|
|
69
|
+
return 2
|
|
70
|
+
result = scan_paths(args.paths, canaries)
|
|
71
|
+
if args.format == "json":
|
|
72
|
+
_print_json(result)
|
|
73
|
+
else:
|
|
74
|
+
_print_text(result)
|
|
75
|
+
return result.exit_code
|
|
@@ -9,6 +9,7 @@ import subprocess
|
|
|
9
9
|
import sys
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Dict
|
|
12
|
+
from urllib.parse import urlsplit
|
|
12
13
|
|
|
13
14
|
from operate.cli.ui import (
|
|
14
15
|
BOLD,
|
|
@@ -110,10 +111,31 @@ Documentation:
|
|
|
110
111
|
|
|
111
112
|
|
|
112
113
|
|
|
114
|
+
def _valid_target(url: str) -> bool:
|
|
115
|
+
parts = urlsplit(url)
|
|
116
|
+
return parts.scheme in ("http", "https") and bool(parts.hostname)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _compose_env(args: argparse.Namespace, config_path: Path) -> Dict[str, str]:
|
|
120
|
+
"""Settings the compose file reads; without them the stack would front its demo backend."""
|
|
121
|
+
env = dict(os.environ)
|
|
122
|
+
if args.target:
|
|
123
|
+
env["KOMAGU_UPSTREAM"] = args.target
|
|
124
|
+
if args.config:
|
|
125
|
+
env["KOMAGU_KONG_CONFIG"] = str(config_path)
|
|
126
|
+
if getattr(args, "patch_out", None):
|
|
127
|
+
env["KOMAGU_VIRTUAL_PATCH_FILE"] = str(Path(args.patch_out).resolve())
|
|
128
|
+
return env
|
|
129
|
+
|
|
130
|
+
|
|
113
131
|
def execute_protect(args: argparse.Namespace) -> int:
|
|
114
132
|
"""Execute perimeter protection actions."""
|
|
115
133
|
step("Validating Komagu Perimeter Protection assets")
|
|
116
134
|
|
|
135
|
+
if args.target and not _valid_target(args.target):
|
|
136
|
+
step_fail(f"--target must be an http(s) URL with a host: {args.target}")
|
|
137
|
+
return 2
|
|
138
|
+
|
|
117
139
|
if not EDGE_DIR.is_dir():
|
|
118
140
|
step_fail(f"Edge directory not found at {EDGE_DIR}")
|
|
119
141
|
return 1
|
|
@@ -141,6 +163,8 @@ def execute_protect(args: argparse.Namespace) -> int:
|
|
|
141
163
|
step_info(f"Synthesizing Coraza WAF virtual patches from: {scan_path.name}")
|
|
142
164
|
out_file, manifest = patcher.patch_to_file(scan_path, target_conf)
|
|
143
165
|
step_ok(f"Synthesized {len(manifest)} SecRules into {out_file}")
|
|
166
|
+
for note in patcher.skipped:
|
|
167
|
+
step_warn(note)
|
|
144
168
|
|
|
145
169
|
if manifest:
|
|
146
170
|
patch_rows = [(f"Rule {m['rule_id']} [{m['category'].upper()}]", f"{m['finding_id']} -> {m['endpoint']}") for m in manifest]
|
|
@@ -151,7 +175,7 @@ def execute_protect(args: argparse.Namespace) -> int:
|
|
|
151
175
|
)
|
|
152
176
|
|
|
153
177
|
# If --target is supplied, report upstream routing
|
|
154
|
-
upstream_url = args.target or "http://
|
|
178
|
+
upstream_url = args.target or "http://mock-backend:5000 (demo backend)"
|
|
155
179
|
if args.target:
|
|
156
180
|
step_info(f"Configured upstream target: {args.target}")
|
|
157
181
|
|
|
@@ -160,11 +184,16 @@ def execute_protect(args: argparse.Namespace) -> int:
|
|
|
160
184
|
compose_cmd = [docker_bin, "compose", "-f", str(EDGE_COMPOSE)] if docker_bin else None
|
|
161
185
|
|
|
162
186
|
if args.up:
|
|
187
|
+
patch_out = getattr(args, "patch_out", None)
|
|
188
|
+
if patch_out and not Path(patch_out).is_file():
|
|
189
|
+
step_fail(f"Virtual patch file not found, Docker would mount a directory instead: {patch_out}")
|
|
190
|
+
return 2
|
|
163
191
|
if args.dry_run or not docker_bin:
|
|
164
192
|
step_info(f"[DRY-RUN] Would execute: docker compose -f {EDGE_COMPOSE} up -d")
|
|
165
193
|
else:
|
|
166
194
|
step("Starting perimeter containers (Kong Gateway + Coraza WAF + Mock Backend)")
|
|
167
|
-
res = subprocess.run(compose_cmd + ["up", "-d"], capture_output=True, text=True
|
|
195
|
+
res = subprocess.run(compose_cmd + ["up", "-d"], capture_output=True, text=True,
|
|
196
|
+
env=_compose_env(args, config_path))
|
|
168
197
|
if res.returncode != 0:
|
|
169
198
|
step_fail("Failed to start perimeter containers")
|
|
170
199
|
print(res.stderr or res.stdout, file=sys.stderr)
|
|
@@ -196,7 +225,7 @@ def execute_protect(args: argparse.Namespace) -> int:
|
|
|
196
225
|
("Upstream Target", upstream_url),
|
|
197
226
|
("Public Proxy Port", "http://localhost:8000"),
|
|
198
227
|
("Kong Admin API", "http://localhost:8001"),
|
|
199
|
-
("Coraza WAF
|
|
228
|
+
("Coraza WAF", "internal only, port 8080 is not published on the host"),
|
|
200
229
|
]
|
|
201
230
|
|
|
202
231
|
print_summary_table(
|
|
@@ -161,6 +161,12 @@ Examples:
|
|
|
161
161
|
default=120,
|
|
162
162
|
help="Timeout in seconds per LLM request (default: 120)",
|
|
163
163
|
)
|
|
164
|
+
parser.add_argument(
|
|
165
|
+
"--ai-reasoning-effort",
|
|
166
|
+
choices=("none", "low", "medium", "high"),
|
|
167
|
+
default=None,
|
|
168
|
+
help="Send reasoning_effort with each LLM request; 'none' turns thinking off on Ollama (default: not sent)",
|
|
169
|
+
)
|
|
164
170
|
parser.set_defaults(func=execute_scan)
|
|
165
171
|
|
|
166
172
|
|
|
@@ -284,7 +290,8 @@ def execute_scan(args: argparse.Namespace) -> int:
|
|
|
284
290
|
step_info(msg)
|
|
285
291
|
|
|
286
292
|
try:
|
|
287
|
-
client = LLMClient(endpoint=endpoint, model=model, api_key=api_key, timeout=timeout
|
|
293
|
+
client = LLMClient(endpoint=endpoint, model=model, api_key=api_key, timeout=timeout,
|
|
294
|
+
reasoning_effort=getattr(args, "ai_reasoning_effort", None))
|
|
288
295
|
runner = HeadlessAIRunner(target_path, out_dir, client, step_callback=_scan_callback)
|
|
289
296
|
runner.run()
|
|
290
297
|
except Exception as e:
|
|
@@ -7,7 +7,7 @@ import json
|
|
|
7
7
|
import os
|
|
8
8
|
import sys
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Dict, List
|
|
10
|
+
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
11
11
|
|
|
12
12
|
from operate.cli.ui import (
|
|
13
13
|
BOLD,
|
|
@@ -34,6 +34,17 @@ from operate.cross_user_tester.persona_matrix import (
|
|
|
34
34
|
is_v2_suite,
|
|
35
35
|
load_persona_suite,
|
|
36
36
|
)
|
|
37
|
+
from operate.cross_user_tester.session_config import (
|
|
38
|
+
SessionOptions,
|
|
39
|
+
add_session_arguments,
|
|
40
|
+
build_session_options,
|
|
41
|
+
check_session,
|
|
42
|
+
cookie_file_is_open,
|
|
43
|
+
global_csrf,
|
|
44
|
+
read_cookie,
|
|
45
|
+
strict_gaps,
|
|
46
|
+
)
|
|
47
|
+
from operate.cross_user_tester.session_http import SessionConfigError
|
|
37
48
|
from operate.cross_user_tester.runner import (
|
|
38
49
|
CrossUserTestRunner,
|
|
39
50
|
format_report,
|
|
@@ -46,6 +57,8 @@ REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
|
46
57
|
DEFAULT_SUITE = REPO_ROOT / "operate" / "tests" / "test_suite.json"
|
|
47
58
|
# --token is passed to persona-matrix cases through this private env entry.
|
|
48
59
|
CLI_TOKEN_ENV = "KOMAGU_CLI_TOKEN"
|
|
60
|
+
# The attacker's cookie reaches persona-matrix cases the same way, in a private copy of the environment.
|
|
61
|
+
CLI_COOKIE_ENV = "KOMAGU_CLI_COOKIE"
|
|
49
62
|
|
|
50
63
|
|
|
51
64
|
def register_subcommand(subparsers: argparse._SubParsersAction) -> None:
|
|
@@ -75,6 +88,11 @@ Examples:
|
|
|
75
88
|
# Persona matrix (suite "version": 2): BFLA and mass assignment checks
|
|
76
89
|
KOMAGU_TOKEN_USER=... KOMAGU_TOKEN_ADMIN=... komagu test --url http://staging:8000 --suite ./personas.json
|
|
77
90
|
|
|
91
|
+
# Session-based Java app (JSP / Struts): cookies from the environment, declared denial signal
|
|
92
|
+
ATTACKER_COOKIE=... OWNER_COOKIE=... komagu test --url http://test:8080 --from-scan ./findings.json --strict \\
|
|
93
|
+
--attacker-cookie-env ATTACKER_COOKIE --owner-cookie-env OWNER_COOKIE \\
|
|
94
|
+
--session-check /home.do --deny-status 404 --deny-redirect /login.do
|
|
95
|
+
|
|
78
96
|
Documentation:
|
|
79
97
|
See docs/cli-guide.md#34-komagu-test--offensive-exploit-verification-bola-invariant--persona-matrix
|
|
80
98
|
""",
|
|
@@ -126,7 +144,8 @@ Documentation:
|
|
|
126
144
|
parser.add_argument(
|
|
127
145
|
"--strict",
|
|
128
146
|
action="store_true",
|
|
129
|
-
help="Enforce strict 404 requirement (reject 401/403 which leak resource existence)"
|
|
147
|
+
help="Enforce strict 404 requirement (reject 401/403 which leak resource existence). "
|
|
148
|
+
"With session cookies, also requires a declared denial signal, --session-check and the owner's cookie",
|
|
130
149
|
)
|
|
131
150
|
parser.add_argument(
|
|
132
151
|
"--token-env",
|
|
@@ -139,6 +158,7 @@ Documentation:
|
|
|
139
158
|
dest="export_poc",
|
|
140
159
|
help="Directory path to export standalone executable Python and cURL exploit PoC scripts",
|
|
141
160
|
)
|
|
161
|
+
add_session_arguments(parser)
|
|
142
162
|
parser.set_defaults(func=execute_test)
|
|
143
163
|
|
|
144
164
|
|
|
@@ -150,6 +170,19 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
150
170
|
tests: List[Any] = []
|
|
151
171
|
matrix_cases: List[MatrixCase] = []
|
|
152
172
|
|
|
173
|
+
try:
|
|
174
|
+
session = build_session_options(args, os.environ)
|
|
175
|
+
csrf = global_csrf(args)
|
|
176
|
+
except SessionConfigError as e:
|
|
177
|
+
step_fail(str(e))
|
|
178
|
+
return 2
|
|
179
|
+
for cookie_file in (args.attacker_cookie_file, args.owner_cookie_file):
|
|
180
|
+
if cookie_file_is_open(cookie_file):
|
|
181
|
+
step_warn("A cookie file can be read by other users; restrict it with chmod 600")
|
|
182
|
+
# A session run sends the cookie only; the --token default must not go with it.
|
|
183
|
+
attacker_token = None if session and session.attacker_cookie else args.attacker_token
|
|
184
|
+
suite_body_format = "form" if args.body_format == "form" else "json"
|
|
185
|
+
|
|
153
186
|
# 1. Ingest from scan output if provided
|
|
154
187
|
if getattr(args, "from_scan", None):
|
|
155
188
|
scan_file = Path(args.from_scan).resolve()
|
|
@@ -161,15 +194,19 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
161
194
|
scan_cases = generate_test_cases_from_findings(
|
|
162
195
|
scan_data_or_path=scan_file,
|
|
163
196
|
default_resource_id=args.resource_id,
|
|
164
|
-
attacker_token=
|
|
197
|
+
attacker_token=attacker_token,
|
|
165
198
|
min_consensus_score=min_c,
|
|
199
|
+
body_format=args.body_format,
|
|
200
|
+
csrf=csrf,
|
|
166
201
|
)
|
|
167
202
|
step_info(f"Synthesized {len(scan_cases)} active verification cases from scan findings")
|
|
168
203
|
tests.extend(scan_cases)
|
|
169
|
-
matrix_cases.extend(_matrix_cases_from_scan(scan_file, args, min_c))
|
|
204
|
+
matrix_cases.extend(_matrix_cases_from_scan(scan_file, args, min_c, session, csrf))
|
|
170
205
|
|
|
171
206
|
# Export PoC exploits if requested
|
|
172
207
|
if getattr(args, "export_poc", None):
|
|
208
|
+
if session and session.attacker_cookie:
|
|
209
|
+
step_warn("--export-poc writes Bearer scripts; they do not carry the session cookie")
|
|
173
210
|
from operate.exploit.generator import ExploitGenerator
|
|
174
211
|
gen = ExploitGenerator(
|
|
175
212
|
base_url=args.base_url,
|
|
@@ -191,14 +228,20 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
191
228
|
return 2
|
|
192
229
|
if is_v2_suite(suite_data):
|
|
193
230
|
try:
|
|
194
|
-
matrix_cases.extend(load_persona_suite(
|
|
195
|
-
|
|
231
|
+
matrix_cases.extend(load_persona_suite(
|
|
232
|
+
suite_data, default_id=args.resource_id,
|
|
233
|
+
default_body_format=suite_body_format, default_csrf=csrf,
|
|
234
|
+
))
|
|
235
|
+
except (SuiteError, SessionConfigError) as e:
|
|
196
236
|
step_fail(f"Invalid persona suite {suite_path.name}: {e}")
|
|
197
237
|
return 2
|
|
198
238
|
step_info(f"Persona matrix: {len(matrix_cases)} BFLA / mass assignment cases")
|
|
199
239
|
else:
|
|
200
240
|
try:
|
|
201
|
-
tests.extend(load_suite_from_file(str(suite_path)))
|
|
241
|
+
tests.extend(load_suite_from_file(str(suite_path), suite_body_format, csrf))
|
|
242
|
+
except SessionConfigError as e:
|
|
243
|
+
step_fail(f"Invalid test suite {suite_path.name}: {e}")
|
|
244
|
+
return 2
|
|
202
245
|
except Exception as e:
|
|
203
246
|
step_warn(f"Could not load test suite {suite_path}: {e}")
|
|
204
247
|
|
|
@@ -210,16 +253,30 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
210
253
|
generate_crud_test_cases(
|
|
211
254
|
resource_path=crud_path,
|
|
212
255
|
resource_id=args.resource_id,
|
|
213
|
-
attacker_token=
|
|
256
|
+
attacker_token=attacker_token,
|
|
257
|
+
body_format=suite_body_format,
|
|
258
|
+
csrf=csrf,
|
|
214
259
|
)
|
|
215
260
|
)
|
|
216
261
|
|
|
217
262
|
|
|
218
263
|
runner = CrossUserTestRunner(
|
|
219
264
|
base_url=args.base_url,
|
|
220
|
-
default_attacker_token=
|
|
265
|
+
default_attacker_token=attacker_token,
|
|
266
|
+
session=session,
|
|
221
267
|
)
|
|
222
268
|
|
|
269
|
+
has_cookie_personas = any(c.persona.cookie_env or c.persona.cookie_file for c in matrix_cases)
|
|
270
|
+
if args.strict and has_cookie_personas:
|
|
271
|
+
gaps = strict_gaps(args, False, False)
|
|
272
|
+
if gaps:
|
|
273
|
+
step_fail("--strict on a session app needs " + "; ".join(gaps))
|
|
274
|
+
return 2
|
|
275
|
+
problem = _session_problem(args, session, matrix_cases, os.environ)
|
|
276
|
+
if problem:
|
|
277
|
+
step_fail(f"{problem}. No test was sent.")
|
|
278
|
+
return 2
|
|
279
|
+
|
|
223
280
|
results = []
|
|
224
281
|
success = True
|
|
225
282
|
if tests:
|
|
@@ -230,7 +287,7 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
230
287
|
print(report_text)
|
|
231
288
|
|
|
232
289
|
try:
|
|
233
|
-
matrix_results = _run_matrix(matrix_cases, args)
|
|
290
|
+
matrix_results = _run_matrix(matrix_cases, args, session)
|
|
234
291
|
except ValueError as e:
|
|
235
292
|
step_fail(str(e))
|
|
236
293
|
return 2
|
|
@@ -242,6 +299,12 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
242
299
|
print(matrix_text)
|
|
243
300
|
success = success and matrix_ok
|
|
244
301
|
|
|
302
|
+
# A session lost during the run turns refusals into login redirects: the verdicts cannot be trusted.
|
|
303
|
+
lost_session = _session_problem(args, session, matrix_cases, os.environ)
|
|
304
|
+
if lost_session:
|
|
305
|
+
step_fail(f"{lost_session} after the run. The results below are not valid.")
|
|
306
|
+
success = False
|
|
307
|
+
|
|
245
308
|
# Save output if requested
|
|
246
309
|
if args.output:
|
|
247
310
|
out_path = Path(args.output).resolve()
|
|
@@ -252,6 +315,8 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
252
315
|
"results": [r.to_dict() for r in results],
|
|
253
316
|
"matrix_results": [r.to_dict() for r in matrix_results],
|
|
254
317
|
}
|
|
318
|
+
if lost_session:
|
|
319
|
+
report_data["session_check"] = "failed_after_run"
|
|
255
320
|
out_path.write_text(json.dumps(report_data, indent=2), encoding="utf-8")
|
|
256
321
|
step_ok(f"Saved test report to {out_path}")
|
|
257
322
|
|
|
@@ -282,7 +347,7 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
282
347
|
if r.is_breach:
|
|
283
348
|
verdict = color("CONFIRMED BREACH (EXPLOITABLE)", RED, BOLD)
|
|
284
349
|
elif r.is_secure:
|
|
285
|
-
verdict = color("PROTECTED (404 INVARIANT)", GREEN, BOLD)
|
|
350
|
+
verdict = color("PROTECTED (DENIAL SIGNAL)" if session else "PROTECTED (404 INVARIANT)", GREEN, BOLD)
|
|
286
351
|
else:
|
|
287
352
|
verdict = color(f"UNEXPECTED HTTP {r.status_code}", YELLOW)
|
|
288
353
|
exploit_rows.append((f"{f_id} [{tc.method} {tc.path}]", f"Consensus: {c_score_str} | Verdict: {verdict}"))
|
|
@@ -293,29 +358,80 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
293
358
|
footer="Offensive Verification: Green = 404 Enforced, Red = Active BOLA Breach Confirmed.",
|
|
294
359
|
)
|
|
295
360
|
|
|
361
|
+
if lost_session:
|
|
362
|
+
return 2
|
|
296
363
|
return 0 if success else 1
|
|
297
364
|
|
|
298
365
|
|
|
299
|
-
def
|
|
366
|
+
def _session_targets(
|
|
367
|
+
session: Optional[SessionOptions],
|
|
368
|
+
cases: List[MatrixCase],
|
|
369
|
+
env: Mapping[str, str],
|
|
370
|
+
) -> List[Tuple[str, str]]:
|
|
371
|
+
"""(label, cookie) for every session persona in this run."""
|
|
372
|
+
targets: List[Tuple[str, str]] = []
|
|
373
|
+
if session and session.attacker_cookie:
|
|
374
|
+
targets.append(("attacker", session.attacker_cookie))
|
|
375
|
+
if session and session.owner_cookie:
|
|
376
|
+
targets.append(("owner", session.owner_cookie))
|
|
377
|
+
for persona in {c.persona for c in cases if c.persona.cookie_env != CLI_COOKIE_ENV}:
|
|
378
|
+
cookie = read_cookie(persona.cookie_env, persona.cookie_file, env, persona.name)
|
|
379
|
+
if cookie:
|
|
380
|
+
targets.append((persona.name, cookie))
|
|
381
|
+
return targets
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _session_problem(
|
|
385
|
+
args: argparse.Namespace,
|
|
386
|
+
session: Optional[SessionOptions],
|
|
387
|
+
cases: List[MatrixCase],
|
|
388
|
+
env: Mapping[str, str],
|
|
389
|
+
) -> Optional[str]:
|
|
390
|
+
"""Why a session persona cannot be used, or None when all are alive."""
|
|
391
|
+
if session is None or not session.session_check:
|
|
392
|
+
return None
|
|
393
|
+
try:
|
|
394
|
+
targets = _session_targets(session, cases, env)
|
|
395
|
+
except SessionConfigError as err:
|
|
396
|
+
return str(err)
|
|
397
|
+
for label, cookie in targets:
|
|
398
|
+
problem = check_session(args.base_url, label, cookie, session.session_check, session.session_marker, session.denial)
|
|
399
|
+
if problem:
|
|
400
|
+
return problem
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _matrix_cases_from_scan(
|
|
405
|
+
scan_file: Path,
|
|
406
|
+
args: argparse.Namespace,
|
|
407
|
+
min_consensus: float,
|
|
408
|
+
session: Optional[SessionOptions],
|
|
409
|
+
csrf: Optional[Any],
|
|
410
|
+
) -> List[MatrixCase]:
|
|
300
411
|
"""BFLA and mass assignment findings from findings.json or SARIF."""
|
|
301
412
|
try:
|
|
302
413
|
data = json.loads(scan_file.read_text(encoding="utf-8"))
|
|
303
414
|
except (OSError, ValueError):
|
|
304
415
|
return []
|
|
305
416
|
token_env = getattr(args, "token_env", None) or CLI_TOKEN_ENV
|
|
417
|
+
cookie_env = CLI_COOKIE_ENV if session and session.attacker_cookie else None
|
|
306
418
|
cases = generate_matrix_cases_from_findings(
|
|
307
|
-
data, token_env=token_env, default_id=args.resource_id, min_consensus=min_consensus
|
|
419
|
+
data, token_env=token_env, default_id=args.resource_id, min_consensus=min_consensus,
|
|
420
|
+
cookie_env=cookie_env, body_format=args.body_format, csrf=csrf,
|
|
308
421
|
)
|
|
309
422
|
if cases:
|
|
310
423
|
step_info(f"Synthesized {len(cases)} BFLA / mass assignment cases from scan findings")
|
|
311
424
|
return cases
|
|
312
425
|
|
|
313
426
|
|
|
314
|
-
def _run_matrix(cases: List[MatrixCase], args: argparse.Namespace) -> List[Any]:
|
|
427
|
+
def _run_matrix(cases: List[MatrixCase], args: argparse.Namespace, session: Optional[SessionOptions]) -> List[Any]:
|
|
315
428
|
if not cases:
|
|
316
429
|
return []
|
|
317
430
|
env: Dict[str, str] = dict(os.environ)
|
|
318
431
|
if args.attacker_token:
|
|
319
432
|
env.setdefault(CLI_TOKEN_ENV, args.attacker_token)
|
|
433
|
+
if session and session.attacker_cookie:
|
|
434
|
+
env[CLI_COOKIE_ENV] = session.attacker_cookie
|
|
320
435
|
step(f"Executing {len(cases)} persona matrix cases")
|
|
321
|
-
|
|
436
|
+
denial = session.denial if session else None
|
|
437
|
+
return PersonaMatrixRunner(args.base_url, env=env, strict=args.strict, denial=denial).run(cases)
|