@hoanghainh/komagu 2.5.0 → 2.6.1
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 +30 -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/laravel_routes.py +102 -0
- package/assess/lsa/scripts/map_routes.py +4 -1
- 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_laravel_routes.py +95 -0
- package/examples/member-portal/backend-dotnet/Auth/DemoAuthenticationHandler.cs +75 -0
- package/examples/member-portal/backend-dotnet/Program.cs +1 -72
- package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/Application.kt +4 -0
- package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/controller/Controllers.kt +3 -0
- package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/model/Entities.kt +4 -0
- package/examples/member-portal/tests/test_d6_demo_app.py +4 -4
- package/examples/member-portal/tests/test_d6_remediation_verification.py +6 -6
- package/operate/ai/client.py +27 -10
- package/operate/ai/runner.py +4 -2
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/scan.py +8 -1
- package/operate/cross_user_tester/runner.py +4 -1
- package/operate/devsecops/sbom.py +12 -4
- package/operate/simulate/scenarios.py +11 -2
- package/operate/tests/test_ai_reasoning_effort.py +65 -0
- package/operate/tests/test_ai_route_inventory.py +53 -0
- package/operate/tests/test_audit_minimum_versions.py +89 -0
- package/operate/tests/test_cli.py +1 -1
- package/operate/tests/test_scheme_and_xml_guards.py +67 -0
- package/package.json +1 -1
- package/remediate/authz/dotnet/src/Komagu.Security.Authorization/LoggingSecurityAuditSink.cs +19 -3
- package/remediate/authz/golang/authz_test.go +5 -2
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +74 -78
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/spring/OwnershipAspect.kt +129 -158
- package/remediate/deps/patcher.py +2 -1
- package/remediate/edge/mock-backend/app.py +2 -1
- package/remediate/edge/tests/test_edge_security.py +5 -5
- package/remediate/ekyc/requirements.txt +1 -1
- package/remediate/identity/dotnet/src/Komagu.Security.Identity/OidcServiceCollectionExtensions.cs +19 -6
- package/remediate/identity/python/requirements.txt +2 -2
- package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfiguration.kt +31 -38
- package/remediate/legacy_shims/tests/test_legacy_shims.py +8 -8
- package/remediate/query/dotnet/src/Komagu.Security.Query/SafeSortAllowList.cs +5 -2
- package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/SafeSortAllowList.kt +10 -11
- package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/StringBuiltSqlQueryRule.kt +19 -24
package/operate/ai/client.py
CHANGED
|
@@ -9,9 +9,13 @@ import json
|
|
|
9
9
|
import re
|
|
10
10
|
import time
|
|
11
11
|
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
12
13
|
import urllib.request
|
|
13
14
|
from typing import Any, Dict, List, Optional, Tuple
|
|
14
15
|
|
|
16
|
+
# OpenAI-compatible `reasoning_effort` values; Ollama's /v1 also accepts "none" to turn thinking off (#382)
|
|
17
|
+
REASONING_EFFORTS = ("none", "low", "medium", "high")
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
class LLMClient:
|
|
17
21
|
"""Standard-library client for OpenAI-compatible /chat/completions endpoints."""
|
|
@@ -22,7 +26,12 @@ class LLMClient:
|
|
|
22
26
|
model: str = "llama3.1",
|
|
23
27
|
api_key: Optional[str] = None,
|
|
24
28
|
timeout: int = 120,
|
|
29
|
+
reasoning_effort: Optional[str] = None,
|
|
25
30
|
) -> None:
|
|
31
|
+
if reasoning_effort is not None and reasoning_effort not in REASONING_EFFORTS:
|
|
32
|
+
raise ValueError(f"reasoning_effort must be one of {', '.join(REASONING_EFFORTS)}")
|
|
33
|
+
if urllib.parse.urlparse(endpoint).scheme.lower() not in ("http", "https"):
|
|
34
|
+
raise ValueError("LLM endpoint must be an http:// or https:// URL")
|
|
26
35
|
raw_endpoint = endpoint.rstrip("/")
|
|
27
36
|
if not raw_endpoint.endswith("/chat/completions"):
|
|
28
37
|
self.endpoint = f"{raw_endpoint}/chat/completions"
|
|
@@ -31,6 +40,7 @@ class LLMClient:
|
|
|
31
40
|
self.model = model
|
|
32
41
|
self.api_key = api_key or "ollama"
|
|
33
42
|
self.timeout = timeout
|
|
43
|
+
self.reasoning_effort = reasoning_effort
|
|
34
44
|
# Running totals across chat() calls, read by the runner for ai-run-stats.json
|
|
35
45
|
self.usage: Dict[str, float] = {"calls": 0, "prompt_tokens": 0, "completion_tokens": 0, "seconds": 0.0}
|
|
36
46
|
|
|
@@ -41,15 +51,7 @@ class LLMClient:
|
|
|
41
51
|
max_tokens: Optional[int] = None,
|
|
42
52
|
) -> str:
|
|
43
53
|
"""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")
|
|
54
|
+
data = json.dumps(self.build_payload(messages, temperature, max_tokens)).encode("utf-8")
|
|
53
55
|
headers = {
|
|
54
56
|
"Content-Type": "application/json",
|
|
55
57
|
"Authorization": f"Bearer {self.api_key}",
|
|
@@ -59,7 +61,8 @@ class LLMClient:
|
|
|
59
61
|
req = urllib.request.Request(self.endpoint, data=data, headers=headers, method="POST")
|
|
60
62
|
started = time.monotonic()
|
|
61
63
|
try:
|
|
62
|
-
|
|
64
|
+
# The endpoint scheme is checked to be http(s) in __init__.
|
|
65
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp: # nosec B310
|
|
63
66
|
body = resp.read().decode("utf-8")
|
|
64
67
|
res_json = json.loads(body)
|
|
65
68
|
self._record_usage(res_json.get("usage") or {}, time.monotonic() - started)
|
|
@@ -74,6 +77,20 @@ class LLMClient:
|
|
|
74
77
|
except urllib.error.URLError as e:
|
|
75
78
|
raise RuntimeError(f"Failed to connect to LLM endpoint at {self.endpoint}: {e.reason}") from e
|
|
76
79
|
|
|
80
|
+
def build_payload(
|
|
81
|
+
self,
|
|
82
|
+
messages: List[Dict[str, str]],
|
|
83
|
+
temperature: float = 0.1,
|
|
84
|
+
max_tokens: Optional[int] = None,
|
|
85
|
+
) -> Dict[str, Any]:
|
|
86
|
+
"""Request body for /chat/completions; `reasoning_effort` is sent only when it was set."""
|
|
87
|
+
payload: Dict[str, Any] = {"model": self.model, "messages": messages, "temperature": temperature}
|
|
88
|
+
if max_tokens:
|
|
89
|
+
payload["max_tokens"] = max_tokens
|
|
90
|
+
if self.reasoning_effort:
|
|
91
|
+
payload["reasoning_effort"] = self.reasoning_effort
|
|
92
|
+
return payload
|
|
93
|
+
|
|
77
94
|
def _record_usage(self, usage: Dict[str, Any], seconds: float) -> None:
|
|
78
95
|
self.usage = {
|
|
79
96
|
"calls": self.usage["calls"] + 1,
|
package/operate/ai/runner.py
CHANGED
|
@@ -167,6 +167,7 @@ class HeadlessAIRunner:
|
|
|
167
167
|
stats = {
|
|
168
168
|
"model": getattr(self.client, "model", None),
|
|
169
169
|
"endpoint": getattr(self.client, "endpoint", None),
|
|
170
|
+
"reasoning_effort": getattr(self.client, "reasoning_effort", None),
|
|
170
171
|
"phases": self.phase_stats,
|
|
171
172
|
"totals": self._usage(),
|
|
172
173
|
"parse_failures": self.parse_failures,
|
|
@@ -300,8 +301,9 @@ Output MUST be Markdown format adhering to Komagu auth-model.md specification.
|
|
|
300
301
|
{"name": p.get("name", "id"), "source": p.get("source", "path")}
|
|
301
302
|
for p in c.get("id_params", [])
|
|
302
303
|
]
|
|
303
|
-
|
|
304
|
-
|
|
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 []
|
|
305
307
|
|
|
306
308
|
mapped_routes.append({
|
|
307
309
|
"route_id": route_id,
|
package/operate/cli/__init__.py
CHANGED
|
@@ -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:
|
|
@@ -242,7 +242,10 @@ class CrossUserTestRunner:
|
|
|
242
242
|
error_message = None
|
|
243
243
|
|
|
244
244
|
try:
|
|
245
|
-
|
|
245
|
+
if urllib.parse.urlparse(url).scheme.lower() not in ("http", "https"):
|
|
246
|
+
raise ValueError("base URL must be an http:// or https:// URL")
|
|
247
|
+
# The scheme is checked just above.
|
|
248
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp: # nosec B310
|
|
246
249
|
status_code = resp.status
|
|
247
250
|
raw = resp.read()
|
|
248
251
|
response_body = raw.decode("utf-8", errors="replace")[:1000]
|
|
@@ -24,6 +24,16 @@ import xml.etree.ElementTree as ET
|
|
|
24
24
|
from typing import Any, Dict, List, Optional
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def _parse_xml_without_dtd(file_path: str) -> ET.Element:
|
|
28
|
+
"""Parse a manifest from the scanned project, refusing DTDs (entity expansion)."""
|
|
29
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as handle:
|
|
30
|
+
text = handle.read()
|
|
31
|
+
if "<!DOCTYPE" in text.upper() or "<!ENTITY" in text.upper():
|
|
32
|
+
raise ValueError(f"{file_path}: XML with a DTD is not parsed")
|
|
33
|
+
# DTDs are refused above, so no entity can be declared or expanded.
|
|
34
|
+
return ET.fromstring(text) # nosec B314
|
|
35
|
+
|
|
36
|
+
|
|
27
37
|
class Component:
|
|
28
38
|
"""Represents a software package or component in an SBOM."""
|
|
29
39
|
|
|
@@ -195,8 +205,7 @@ class SBOMGenerator:
|
|
|
195
205
|
def _parse_csproj(self, file_path: str) -> List[Component]:
|
|
196
206
|
components = []
|
|
197
207
|
try:
|
|
198
|
-
|
|
199
|
-
root = tree.getroot()
|
|
208
|
+
root = _parse_xml_without_dtd(file_path)
|
|
200
209
|
for pkg in root.findall(".//PackageReference"):
|
|
201
210
|
name = pkg.get("Include") or pkg.get("Update")
|
|
202
211
|
version = pkg.get("Version")
|
|
@@ -213,8 +222,7 @@ class SBOMGenerator:
|
|
|
213
222
|
def _parse_pom_xml(self, file_path: str) -> List[Component]:
|
|
214
223
|
components = []
|
|
215
224
|
try:
|
|
216
|
-
|
|
217
|
-
root = tree.getroot()
|
|
225
|
+
root = _parse_xml_without_dtd(file_path)
|
|
218
226
|
ns = {"m": "http://maven.apache.org/POM/4.0.0"}
|
|
219
227
|
# Search both with namespace and without
|
|
220
228
|
deps = root.findall(".//m:dependency", ns) or root.findall(".//dependency")
|
|
@@ -66,6 +66,11 @@ class ScenarioResult:
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _require_http(target: str) -> None:
|
|
70
|
+
if urllib.parse.urlparse(target).scheme.lower() not in ("http", "https"):
|
|
71
|
+
raise ValueError("target must be an http:// or https:// URL")
|
|
72
|
+
|
|
73
|
+
|
|
69
74
|
class BaseScenario:
|
|
70
75
|
"""Base class for BAS attack scenarios."""
|
|
71
76
|
|
|
@@ -101,6 +106,7 @@ class TimesCarBreachScenario(BaseScenario):
|
|
|
101
106
|
sample_ids: Optional[List[str]] = None,
|
|
102
107
|
timeout: float = 10.0,
|
|
103
108
|
) -> None:
|
|
109
|
+
_require_http(target)
|
|
104
110
|
super().__init__(target, timeout)
|
|
105
111
|
self.endpoint_pattern = endpoint_pattern
|
|
106
112
|
self.attacker_token = attacker_token
|
|
@@ -145,7 +151,8 @@ class TimesCarBreachScenario(BaseScenario):
|
|
|
145
151
|
response_sample = ""
|
|
146
152
|
|
|
147
153
|
try:
|
|
148
|
-
|
|
154
|
+
# self.target is checked to be http(s) in __init__.
|
|
155
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp: # nosec B310
|
|
149
156
|
status_code = resp.status
|
|
150
157
|
body = resp.read().decode("utf-8", errors="replace")[:200]
|
|
151
158
|
response_sample = body
|
|
@@ -218,6 +225,7 @@ class MetabaseZeroDayScenario(BaseScenario):
|
|
|
218
225
|
target_param: str = "q",
|
|
219
226
|
timeout: float = 10.0,
|
|
220
227
|
) -> None:
|
|
228
|
+
_require_http(target)
|
|
221
229
|
super().__init__(target, timeout)
|
|
222
230
|
self.query_path = query_path
|
|
223
231
|
self.target_param = target_param
|
|
@@ -269,7 +277,8 @@ class MetabaseZeroDayScenario(BaseScenario):
|
|
|
269
277
|
response_sample = ""
|
|
270
278
|
|
|
271
279
|
try:
|
|
272
|
-
|
|
280
|
+
# self.target is checked to be http(s) in __init__.
|
|
281
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp: # nosec B310
|
|
273
282
|
status_code = resp.status
|
|
274
283
|
body = resp.read().decode("utf-8", errors="replace")[:300]
|
|
275
284
|
response_sample = body
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""`--ai-reasoning-effort` reaches each /chat/completions request and ai-run-stats.json (#382)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
import unittest
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from operate.ai.client import LLMClient
|
|
14
|
+
from operate.ai.runner import HeadlessAIRunner
|
|
15
|
+
from operate.cli.commands.scan import register_subcommand
|
|
16
|
+
|
|
17
|
+
MESSAGES = [{"role": "user", "content": "audit"}]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestReasoningEffortPayload(unittest.TestCase):
|
|
21
|
+
def test_not_sent_by_default(self) -> None:
|
|
22
|
+
payload = LLMClient(model="m").build_payload(MESSAGES)
|
|
23
|
+
self.assertEqual(payload, {"model": "m", "messages": MESSAGES, "temperature": 0.1})
|
|
24
|
+
|
|
25
|
+
def test_sent_when_set(self) -> None:
|
|
26
|
+
payload = LLMClient(model="m", reasoning_effort="none").build_payload(MESSAGES, max_tokens=50)
|
|
27
|
+
self.assertEqual(payload["reasoning_effort"], "none")
|
|
28
|
+
self.assertEqual(payload["max_tokens"], 50)
|
|
29
|
+
|
|
30
|
+
def test_unknown_value_is_rejected(self) -> None:
|
|
31
|
+
with self.assertRaises(ValueError):
|
|
32
|
+
LLMClient(reasoning_effort="off")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestReasoningEffortCli(unittest.TestCase):
|
|
36
|
+
def parse(self, *argv: str) -> argparse.Namespace:
|
|
37
|
+
parser = argparse.ArgumentParser()
|
|
38
|
+
register_subcommand(parser.add_subparsers(dest="command"))
|
|
39
|
+
return parser.parse_args(["scan", ".", *argv])
|
|
40
|
+
|
|
41
|
+
def test_flag_is_optional(self) -> None:
|
|
42
|
+
self.assertIsNone(self.parse().ai_reasoning_effort)
|
|
43
|
+
self.assertEqual(self.parse("--ai-reasoning-effort", "none").ai_reasoning_effort, "none")
|
|
44
|
+
|
|
45
|
+
def test_flag_rejects_unknown_values(self) -> None:
|
|
46
|
+
with self.assertRaises(SystemExit):
|
|
47
|
+
self.parse("--ai-reasoning-effort", "off")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestReasoningEffortStats(unittest.TestCase):
|
|
51
|
+
def setUp(self) -> None:
|
|
52
|
+
self.tmp = Path(tempfile.mkdtemp(prefix="komagu_effort_"))
|
|
53
|
+
|
|
54
|
+
def tearDown(self) -> None:
|
|
55
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
56
|
+
|
|
57
|
+
def test_value_is_recorded(self) -> None:
|
|
58
|
+
runner = HeadlessAIRunner(self.tmp, self.tmp / "out", LLMClient(model="m", reasoning_effort="low"))
|
|
59
|
+
(self.tmp / "out").mkdir(parents=True, exist_ok=True)
|
|
60
|
+
stats = json.loads(runner.write_run_stats().read_text(encoding="utf-8"))
|
|
61
|
+
self.assertEqual(stats["reasoning_effort"], "low")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
unittest.main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Route inventory built by the headless runner from Tier 1 route batches (#380)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from operate.ai.runner import HeadlessAIRunner
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _NoCallClient:
|
|
16
|
+
model = "none"
|
|
17
|
+
endpoint = "test://"
|
|
18
|
+
|
|
19
|
+
def chat(self, messages, temperature=0.1): # noqa: ANN001
|
|
20
|
+
raise AssertionError("route inventory must not call the model")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestRouteInventory(unittest.TestCase):
|
|
24
|
+
def setUp(self) -> None:
|
|
25
|
+
self.tmp = Path(tempfile.mkdtemp(prefix="komagu_inventory_"))
|
|
26
|
+
self.out = self.tmp / "out"
|
|
27
|
+
(self.out / "batches").mkdir(parents=True)
|
|
28
|
+
|
|
29
|
+
def tearDown(self) -> None:
|
|
30
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
31
|
+
|
|
32
|
+
def inventory(self, candidates: list) -> list:
|
|
33
|
+
(self.out / "batches" / "routes-001.json").write_text(json.dumps({"candidates": candidates}), encoding="utf-8")
|
|
34
|
+
HeadlessAIRunner(self.tmp, self.out, _NoCallClient()).map_routes_inventory()
|
|
35
|
+
return json.loads((self.out / "routes" / "routes-001.json").read_text(encoding="utf-8"))["routes"]
|
|
36
|
+
|
|
37
|
+
def test_route_without_id_params_gets_no_invented_id(self) -> None:
|
|
38
|
+
routes = self.inventory([{"route_id": "R-0001", "method": ["POST"], "path": "/register",
|
|
39
|
+
"handler": {"file": "app/Http/Controllers/AccountController.php", "line": 11},
|
|
40
|
+
"id_params": []}])
|
|
41
|
+
self.assertEqual(routes[0]["id_inputs"], [])
|
|
42
|
+
|
|
43
|
+
def test_detected_id_params_are_kept(self) -> None:
|
|
44
|
+
routes = self.inventory([{"route_id": "R-0001", "method": ["GET"], "path": "/orders/{id}",
|
|
45
|
+
"handler": {"file": "routes/web.php", "line": 3},
|
|
46
|
+
"id_params": [{"name": "id", "source": "path", "is_id_like": True},
|
|
47
|
+
{"name": "orderId", "source": "query"}]}])
|
|
48
|
+
self.assertEqual(routes[0]["id_inputs"], [{"name": "id", "source": "path"},
|
|
49
|
+
{"name": "orderId", "source": "query"}])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
unittest.main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""`.github/scripts/audit_minimum_versions.py`: audits the lowest version each requirement allows (#390)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
SCRIPT = Path(__file__).resolve().parents[2] / ".github" / "scripts" / "audit_minimum_versions.py"
|
|
14
|
+
_spec = importlib.util.spec_from_file_location("audit_minimum_versions", SCRIPT)
|
|
15
|
+
amv = importlib.util.module_from_spec(_spec)
|
|
16
|
+
_spec.loader.exec_module(amv)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _report(*vulns: tuple) -> dict:
|
|
20
|
+
deps = {}
|
|
21
|
+
for name, version, vuln_id, fixes in vulns:
|
|
22
|
+
deps.setdefault((name, version), []).append({"id": vuln_id, "fix_versions": fixes})
|
|
23
|
+
return {"dependencies": [{"name": n, "version": v, "vulns": vs} for (n, v), vs in deps.items()]}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestMinimumPins(unittest.TestCase):
|
|
27
|
+
def test_lower_bounds_and_exact_pins_become_pins(self) -> None:
|
|
28
|
+
text = "# comment\npyjwt>=2.15.0\ncryptography[ssh] >= 50.0.0 # note\nrequests==2.32.3\n"
|
|
29
|
+
self.assertEqual(amv.minimum_pins(text), ["pyjwt==2.15.0", "cryptography==50.0.0", "requests==2.32.3"])
|
|
30
|
+
|
|
31
|
+
def test_unversioned_and_upper_bounds_are_ignored(self) -> None:
|
|
32
|
+
self.assertEqual(amv.minimum_pins("flask\nurllib3<3\n-r other.txt\n"), [])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestClassify(unittest.TestCase):
|
|
36
|
+
def test_fixable_and_unfixable_are_split_and_deduplicated(self) -> None:
|
|
37
|
+
report = _report(
|
|
38
|
+
("pyjwt", "2.8.0", "PYSEC-1", ["2.12.0"]),
|
|
39
|
+
("pyjwt", "2.8.0", "PYSEC-1", ["2.12.0"]),
|
|
40
|
+
("pyjwt", "2.8.0", "PYSEC-2", []),
|
|
41
|
+
)
|
|
42
|
+
fixable, unfixable = amv.classify(report)
|
|
43
|
+
self.assertEqual(fixable, ["pyjwt 2.8.0: PYSEC-1 (fixed in 2.12.0)"])
|
|
44
|
+
self.assertEqual(unfixable, ["pyjwt 2.8.0: PYSEC-2 (no fixed version)"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestMain(unittest.TestCase):
|
|
48
|
+
def setUp(self) -> None:
|
|
49
|
+
self.tmp = tempfile.mkdtemp(prefix="komagu_amv_")
|
|
50
|
+
self.req = os.path.join(self.tmp, "requirements.txt")
|
|
51
|
+
self.summary = os.path.join(self.tmp, "summary.md")
|
|
52
|
+
with open(self.req, "w", encoding="utf-8") as handle:
|
|
53
|
+
handle.write("pyjwt>=2.8.0\n")
|
|
54
|
+
|
|
55
|
+
def _run(self, report: dict) -> int:
|
|
56
|
+
with mock.patch.object(amv, "audit", return_value=report) as audit, \
|
|
57
|
+
mock.patch("builtins.print"):
|
|
58
|
+
code = amv.main(["--summary", self.summary, self.req])
|
|
59
|
+
audit.assert_called_once_with(["pyjwt==2.8.0"])
|
|
60
|
+
return code
|
|
61
|
+
|
|
62
|
+
def test_fixable_vulnerability_at_the_minimum_fails(self) -> None:
|
|
63
|
+
self.assertEqual(self._run(_report(("pyjwt", "2.8.0", "PYSEC-1", ["2.12.0"]))), 1)
|
|
64
|
+
with open(self.summary, encoding="utf-8") as handle:
|
|
65
|
+
self.assertIn("FAIL pyjwt 2.8.0: PYSEC-1", handle.read())
|
|
66
|
+
|
|
67
|
+
def test_vulnerability_without_a_fix_is_only_reported(self) -> None:
|
|
68
|
+
self.assertEqual(self._run(_report(("pyjwt", "2.8.0", "PYSEC-2", []))), 0)
|
|
69
|
+
with open(self.summary, encoding="utf-8") as handle:
|
|
70
|
+
self.assertIn("note pyjwt 2.8.0: PYSEC-2", handle.read())
|
|
71
|
+
|
|
72
|
+
def test_clean_minimum_passes(self) -> None:
|
|
73
|
+
self.assertEqual(self._run({"dependencies": [{"name": "pyjwt", "version": "2.8.0", "vulns": []}]}), 0)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestRepositoryMinimums(unittest.TestCase):
|
|
77
|
+
"""Minimums raised in #390: these versions had fixable advisories."""
|
|
78
|
+
|
|
79
|
+
def test_raised_minimums(self) -> None:
|
|
80
|
+
root = SCRIPT.parents[2]
|
|
81
|
+
ekyc = amv.minimum_pins((root / "remediate/ekyc/requirements.txt").read_text(encoding="utf-8"))
|
|
82
|
+
identity = amv.minimum_pins((root / "remediate/identity/python/requirements.txt").read_text(encoding="utf-8"))
|
|
83
|
+
self.assertIn("cryptography==50.0.0", ekyc)
|
|
84
|
+
self.assertIn("cryptography==50.0.0", identity)
|
|
85
|
+
self.assertIn("pyjwt==2.15.0", identity)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
unittest.main()
|
|
@@ -61,7 +61,7 @@ class TestKomaguCLI(unittest.TestCase):
|
|
|
61
61
|
def test_subcommand_parser_builds_cleanly(self) -> None:
|
|
62
62
|
"""In-process parser must parse subcommands without throwing."""
|
|
63
63
|
parser = build_parser()
|
|
64
|
-
args_scan = parser.parse_args(["scan", "
|
|
64
|
+
args_scan = parser.parse_args(["scan", "./src", "--format", "html"])
|
|
65
65
|
self.assertEqual(args_scan.subcommand, "scan")
|
|
66
66
|
self.assertEqual(args_scan.format, "html")
|
|
67
67
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Guards behind the Bandit B310 / B314 suppressions (#385).
|
|
3
|
+
|
|
4
|
+
Each `# nosec` added for #385 relies on a check: outbound requests only use
|
|
5
|
+
http(s) URLs, and SBOM manifests with a DTD are not parsed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import tempfile
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
from operate.ai.client import LLMClient
|
|
16
|
+
from operate.cross_user_tester.runner import CrossUserTestRunner, TestCase
|
|
17
|
+
from operate.devsecops.sbom import SBOMGenerator
|
|
18
|
+
from operate.simulate.scenarios import MetabaseZeroDayScenario, TimesCarBreachScenario
|
|
19
|
+
|
|
20
|
+
CSPROJ = '<Project Sdk="Microsoft.NET.Sdk"><ItemGroup><PackageReference Include="Dapper" Version="2.1.28" /></ItemGroup></Project>'
|
|
21
|
+
CSPROJ_WITH_DTD = (
|
|
22
|
+
'<?xml version="1.0"?>\n<!DOCTYPE Project [<!ENTITY a "aaaaaaaaaa">]>\n'
|
|
23
|
+
'<Project Sdk="Microsoft.NET.Sdk"><ItemGroup><PackageReference Include="&a;" Version="1.0.0" /></ItemGroup></Project>'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestHttpOnlyUrls(unittest.TestCase):
|
|
28
|
+
def test_llm_client_accepts_http_and_https(self) -> None:
|
|
29
|
+
self.assertEqual(LLMClient("http://127.0.0.1:11434/v1").endpoint, "http://127.0.0.1:11434/v1/chat/completions")
|
|
30
|
+
self.assertEqual(LLMClient("https://llm.example/v1/chat/completions").endpoint, "https://llm.example/v1/chat/completions")
|
|
31
|
+
|
|
32
|
+
def test_llm_client_rejects_other_schemes(self) -> None:
|
|
33
|
+
for endpoint in ("file:///etc/passwd", "ftp://host/v1", "127.0.0.1:11434/v1"):
|
|
34
|
+
with self.subTest(endpoint=endpoint), self.assertRaises(ValueError):
|
|
35
|
+
LLMClient(endpoint)
|
|
36
|
+
|
|
37
|
+
def test_scenarios_reject_other_schemes(self) -> None:
|
|
38
|
+
for scenario in (TimesCarBreachScenario, MetabaseZeroDayScenario):
|
|
39
|
+
with self.subTest(scenario=scenario.__name__), self.assertRaises(ValueError):
|
|
40
|
+
scenario("file:///etc")
|
|
41
|
+
|
|
42
|
+
def test_bearer_runner_does_not_open_other_schemes(self) -> None:
|
|
43
|
+
res = CrossUserTestRunner("file://").execute_test(TestCase("t", "/etc/passwd", "GET"))
|
|
44
|
+
self.assertFalse(res.is_secure or res.is_breach)
|
|
45
|
+
self.assertEqual(res.status_code, 0)
|
|
46
|
+
self.assertIn("http", res.error_message or "")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestSbomRefusesDtd(unittest.TestCase):
|
|
50
|
+
def setUp(self) -> None:
|
|
51
|
+
self.root = tempfile.mkdtemp(prefix="komagu_sbom_dtd_")
|
|
52
|
+
self.addCleanup(shutil.rmtree, self.root, True)
|
|
53
|
+
|
|
54
|
+
def _scan(self, content: str) -> list:
|
|
55
|
+
with open(os.path.join(self.root, "Service.csproj"), "w", encoding="utf-8") as handle:
|
|
56
|
+
handle.write(content)
|
|
57
|
+
return [c.name for c in SBOMGenerator(root_dir=self.root).scan()]
|
|
58
|
+
|
|
59
|
+
def test_plain_manifest_is_parsed(self) -> None:
|
|
60
|
+
self.assertIn("Dapper", self._scan(CSPROJ))
|
|
61
|
+
|
|
62
|
+
def test_manifest_with_dtd_is_skipped(self) -> None:
|
|
63
|
+
self.assertEqual(self._scan(CSPROJ_WITH_DTD), [])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
unittest.main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoanghainh/komagu",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Security-First Legacy Modernization Platform CLI (LSA Kit, IAM Perimeter, Cross-User Testing)",
|
|
5
5
|
"main": "remediate/authz/nodejs/src/index.js",
|
|
6
6
|
"types": "remediate/authz/nodejs/src/index.d.ts",
|
package/remediate/authz/dotnet/src/Komagu.Security.Authorization/LoggingSecurityAuditSink.cs
CHANGED
|
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging;
|
|
|
5
5
|
/// <summary>
|
|
6
6
|
/// Default security audit sink implementation logging structured events via ILogger.
|
|
7
7
|
/// </summary>
|
|
8
|
-
public sealed class LoggingSecurityAuditSink : ISecurityAuditSink
|
|
8
|
+
public sealed partial class LoggingSecurityAuditSink : ISecurityAuditSink
|
|
9
9
|
{
|
|
10
10
|
private readonly ILogger<LoggingSecurityAuditSink> _logger;
|
|
11
11
|
|
|
@@ -18,8 +18,8 @@ public sealed class LoggingSecurityAuditSink : ISecurityAuditSink
|
|
|
18
18
|
{
|
|
19
19
|
ArgumentNullException.ThrowIfNull(auditEvent);
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
LogAuditEvent(
|
|
22
|
+
_logger,
|
|
23
23
|
auditEvent.EventName,
|
|
24
24
|
auditEvent.TraceId ?? "none",
|
|
25
25
|
auditEvent.ActorUserId ?? "anonymous",
|
|
@@ -32,4 +32,20 @@ public sealed class LoggingSecurityAuditSink : ISecurityAuditSink
|
|
|
32
32
|
|
|
33
33
|
return ValueTask.CompletedTask;
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
[LoggerMessage(
|
|
37
|
+
EventId = 1,
|
|
38
|
+
Level = LogLevel.Warning,
|
|
39
|
+
Message = "[SECURITY AUDIT] {EventName} | TraceId: {TraceId} | Actor: {ActorUserId} | Tenant: {ActorTenantId} | Target: {ResourceType}/{ResourceId} | Op: {Operation} | IP: {ClientIp} | Reason: {Reason}")]
|
|
40
|
+
private static partial void LogAuditEvent(
|
|
41
|
+
ILogger logger,
|
|
42
|
+
string eventName,
|
|
43
|
+
string traceId,
|
|
44
|
+
string actorUserId,
|
|
45
|
+
string actorTenantId,
|
|
46
|
+
string resourceType,
|
|
47
|
+
string resourceId,
|
|
48
|
+
ResourceOperation operation,
|
|
49
|
+
string clientIp,
|
|
50
|
+
string reason);
|
|
35
51
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package golang
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"errors"
|
|
4
5
|
"net/http"
|
|
5
6
|
"net/http/httptest"
|
|
6
7
|
"testing"
|
|
@@ -67,7 +68,7 @@ func TestAuthorizer_AttackerDeniedWith404AndAudit(t *testing.T) {
|
|
|
67
68
|
// Verify RequireOwnership returns ErrResourceNotFound
|
|
68
69
|
sink.Events = nil // Reset sink
|
|
69
70
|
err := auth.RequireOwnership(attackerClaims, order, OpRead)
|
|
70
|
-
if err
|
|
71
|
+
if !errors.Is(err, ErrResourceNotFound) {
|
|
71
72
|
t.Fatalf("expected ErrResourceNotFound, got: %v", err)
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -129,7 +130,9 @@ func TestAuthorizer_HTTPMiddleware(t *testing.T) {
|
|
|
129
130
|
|
|
130
131
|
handler := auth.Middleware(claimsExtractor, loader, OpRead)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
131
132
|
w.WriteHeader(http.StatusOK)
|
|
132
|
-
w.Write([]byte(`{"status":"ok"}`))
|
|
133
|
+
if _, err := w.Write([]byte(`{"status":"ok"}`)); err != nil {
|
|
134
|
+
t.Errorf("write response: %v", err)
|
|
135
|
+
}
|
|
133
136
|
}))
|
|
134
137
|
|
|
135
138
|
// 1. Owner request -> 200 OK
|