patchi 0.7.5__py3-none-any.whl

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.
Files changed (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. patchi-0.7.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,341 @@
1
+ """
2
+ Layer 2 AI Orchestrator — confirms medium-confidence findings via LLM.
3
+
4
+ Design:
5
+ - Batches similar findings (same file + CWE) into single AI calls
6
+ - Rate-limits: max N calls/minute (configurable, default 10)
7
+ - Caches results by (CWE + file + type) hash with 24h TTL
8
+ - Returns structured AIAnalysisResult per input finding
9
+
10
+ Token optimization:
11
+ - One AI call per batch instead of per-finding
12
+ - Cache avoids re-analyzing identical findings
13
+ - Rate limiting prevents token bursts
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import json
20
+ import logging
21
+ import time
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+
25
+ _log = logging.getLogger("patchi.security.layer2_orchestrator")
26
+
27
+
28
+ @dataclass
29
+ class AIAnalysisResult:
30
+ """Result from Layer 2 AI analysis."""
31
+
32
+ confirmed: bool = False
33
+ summary: str = ""
34
+ fix_code: str = ""
35
+ test_code: str = ""
36
+ confidence_adjustment: float = 0.0
37
+ tokens_used: int = 0
38
+ model: str = ""
39
+ need_human_review: bool = False
40
+
41
+ def to_dict(self) -> dict:
42
+ return {
43
+ "confirmed": self.confirmed,
44
+ "summary": self.summary,
45
+ "fix_code": self.fix_code,
46
+ "test_code": self.test_code,
47
+ "confidence_adjustment": self.confidence_adjustment,
48
+ "tokens_used": self.tokens_used,
49
+ "model": self.model,
50
+ "need_human_review": self.need_human_review,
51
+ }
52
+
53
+
54
+ _SYSTEM_PROMPT = """You are Patchi's Layer 2 AI security arbiter — the final
55
+ evidence check before a finding is auto-defended or discarded. Your verdicts
56
+ directly drive automation, so they must be evidence-based, never vibes.
57
+
58
+ For EACH finding you must decide: real exploitable issue, or false positive?
59
+
60
+ CONFIRM only if ALL of these hold:
61
+ 1. You can quote the exact vulnerable code from the provided snippet as
62
+ `evidence_quote` (verbatim substring). No quote -> no confirmation.
63
+ 2. You can name the taint SOURCE (where untrusted data enters) and the SINK
64
+ (dangerous operation) and connect them. Pattern-shaped code without an
65
+ attacker-reachable source is NOT confirmed (e.g. test fixtures, sample
66
+ configs, migrations, admin-only CLI scripts).
67
+ 3. The code path is reachable in this project (not obviously dead code).
68
+
69
+ DISMISS (confirmed=false) when:
70
+ - The match is a test file, fixture, mock, or documentation example.
71
+ - A sanitizer/validation neutralizes the flow between source and sink.
72
+ - The "vulnerability" requires assumptions the snippet contradicts.
73
+ - It matches a known-safe wrapper the project defines.
74
+
75
+ Tool context: `tool_confidence` values are PRIORS from deterministic tools,
76
+ not proof. Multiple agreeing tools raise your prior; a lone regex-tier tool
77
+ lowers it. You are the tie-breaker, not an echo.
78
+
79
+ Output STRICT JSON only (no prose, no markdown fences):
80
+ {"findings": [
81
+ {"index": 0,
82
+ "confirmed": false,
83
+ "confidence_adjustment": -0.6,
84
+ "summary": "one sentence verdict with the reason",
85
+ "evidence_quote": "exact line from Code, or empty if dismissed",
86
+ "source_sink_path": "request.args.get -> execute (or 'none found')",
87
+ "fix_code": "",
88
+ "test_code": "",
89
+ "need_human_review": false}
90
+ ]}
91
+ Rules:
92
+ - confidence_adjustment in [-1.0, +1.0]; dismissals usually negative,
93
+ confirmations positive; magnitude = how sure.
94
+ - Fixes minimal & secure (parameterized queries, arg lists, no eval).
95
+ - Tests must pass without mocking security controls.
96
+ - One entry per input finding, same order, same count."""
97
+
98
+
99
+ _CACHE_TTL = 86400 # 24 hours
100
+
101
+
102
+ class Layer2Orchestrator:
103
+ """AI analysis engine for medium-confidence findings."""
104
+
105
+ def __init__(self, root: Path, config: dict | None = None):
106
+ self.root = root
107
+ self.config = config or {}
108
+ self._batch_size = self.config.get("pipeline", {}).get("ai_batch_size", 3)
109
+ self._rate_per_min = self.config.get("pipeline", {}).get("ai_rate_per_min", 10)
110
+ self._cache: dict[str, tuple[float, AIAnalysisResult]] = {}
111
+ self._call_timestamps: list[float] = []
112
+
113
+ def analyze(self, findings: list) -> list[AIAnalysisResult]:
114
+ """Analyze findings, batching similar ones. Returns one result per input."""
115
+ if not findings:
116
+ return []
117
+
118
+ results: list[AIAnalysisResult | None] = [None] * len(findings)
119
+ uncached_indices: list[int] = []
120
+
121
+ # Step 1: Check cache
122
+ for i, gf in enumerate(findings):
123
+ key = self._cache_key(gf)
124
+ if key in self._cache:
125
+ ts, cached = self._cache[key]
126
+ if time.time() - ts < _CACHE_TTL:
127
+ results[i] = cached
128
+ continue
129
+ uncached_indices.append(i)
130
+
131
+ if not uncached_indices:
132
+ return results # type: ignore
133
+
134
+ # Step 2: Build batches from uncached findings
135
+ uncached = [findings[i] for i in uncached_indices]
136
+ batches = self._build_batches(uncached)
137
+ batch_map: dict[int, int] = {} # finding_index -> batch_index
138
+ for bi, batch in enumerate(batches):
139
+ for gf in batch:
140
+ idx = uncached_indices[uncached.index(gf)]
141
+ batch_map[idx] = bi
142
+
143
+ # Step 3: Analyze each batch (one AI call, per-finding verdicts)
144
+ batch_results: list[list[AIAnalysisResult]] = []
145
+ for batch in batches:
146
+ self._throttle_if_needed()
147
+ ai_results = self._analyze_batch(batch)
148
+ batch_results.append(ai_results)
149
+
150
+ # Cache per-finding results
151
+ for j, gf in enumerate(batch):
152
+ key = self._cache_key(gf)
153
+ cached_res = ai_results[j] if j < len(ai_results) else ai_results[-1]
154
+ self._cache[key] = (time.time(), cached_res)
155
+
156
+ # Step 4: Map batch results back to finding order
157
+ for idx, bi in batch_map.items():
158
+ per_finding = batch_results[bi]
159
+ results[idx] = per_finding[0] if len(per_finding) == 1 else None
160
+
161
+ # Positional pass for multi-finding batches: _analyze_batch already
162
+ # returned results aligned to the batch order; recover via batches.
163
+ if any(r is None for r in results):
164
+ for bi, batch in enumerate(batches):
165
+ per_finding = batch_results[bi]
166
+ for j, gf in enumerate(batch):
167
+ idx = uncached_indices[uncached.index(gf)]
168
+ if j < len(per_finding):
169
+ results[idx] = per_finding[j]
170
+
171
+ return results # type: ignore
172
+
173
+ def _build_batches(self, findings: list) -> list[list]:
174
+ """Group findings by (file, CWE) to batch into single AI calls."""
175
+ groups: dict[str, list] = {}
176
+ for gf in findings:
177
+ f = gf.finding
178
+ group_key = f"{f.file}||{f.cwe}"
179
+ if group_key not in groups:
180
+ groups[group_key] = []
181
+ groups[group_key].append(gf)
182
+
183
+ batches = list(groups.values())
184
+ # Further split if a single group exceeds batch_size
185
+ result: list[list] = []
186
+ for batch in batches:
187
+ for i in range(0, len(batch), self._batch_size):
188
+ result.append(batch[i : i + self._batch_size])
189
+ return result
190
+
191
+ def _analyze_batch(self, batch: list) -> list[AIAnalysisResult]:
192
+ """One AI call per batch; returns one verdict per finding, in order."""
193
+ try:
194
+ from patchi.core.ai.client import call_ai_structured
195
+ except ImportError:
196
+ return [self._offline_result(batch)] * len(batch)
197
+
198
+ prompt = self._build_prompt(batch)
199
+ try:
200
+ response = call_ai_structured(
201
+ prompt=prompt,
202
+ system=_SYSTEM_PROMPT,
203
+ output_model=dict,
204
+ )
205
+ parsed = (
206
+ self._extract_json(response)
207
+ if isinstance(response, str)
208
+ else (response if isinstance(response, dict) else None)
209
+ )
210
+ if not parsed and isinstance(response, str):
211
+ return [self._offline_result(batch)] * len(batch)
212
+ if parsed is None and isinstance(response, dict):
213
+ parsed = response
214
+
215
+ if parsed and isinstance(parsed.get("findings"), list):
216
+ entries = parsed["findings"]
217
+ out: list[AIAnalysisResult] = []
218
+ for _j, entry in enumerate(entries[: len(batch)]):
219
+ if not isinstance(entry, dict):
220
+ continue
221
+ out.append(
222
+ AIAnalysisResult(
223
+ confirmed=bool(entry.get("confirmed", False)),
224
+ summary=str(entry.get("summary", ""))[:300],
225
+ fix_code=str(entry.get("fix_code", "") or ""),
226
+ test_code=str(entry.get("test_code", "") or ""),
227
+ confidence_adjustment=float(
228
+ entry.get("confidence_adjustment", 0.0) or 0.0
229
+ ),
230
+ need_human_review=bool(entry.get("need_human_review", False)),
231
+ tokens_used=int(parsed.get("tokens_used", 0) or 0),
232
+ model=str(parsed.get("model", "unknown")),
233
+ )
234
+ )
235
+ # fill any missing tail positions honestly
236
+ while len(out) < len(batch):
237
+ out.append(self._offline_result([batch[len(out)]]))
238
+ return out
239
+
240
+ # Legacy single-verdict shape: applies to whole batch
241
+ if parsed and "confirmed" in parsed:
242
+ single = AIAnalysisResult(
243
+ confirmed=bool(parsed.get("confirmed", False)),
244
+ summary=str(parsed.get("summary", ""))[:300],
245
+ fix_code=str(parsed.get("fix_code", "") or ""),
246
+ test_code=str(parsed.get("test_code", "") or ""),
247
+ confidence_adjustment=float(parsed.get("confidence_adjustment", 0.0) or 0.0),
248
+ need_human_review=bool(parsed.get("need_human_review", False)),
249
+ tokens_used=int(parsed.get("tokens_used", 0) or 0),
250
+ model=str(parsed.get("model", "unknown")),
251
+ )
252
+ return [single] * len(batch)
253
+ except Exception as e:
254
+ _log.warning("Layer2Orchestrator._analyze_batch failed: %s", e)
255
+
256
+ return [self._offline_result(batch)] * len(batch)
257
+
258
+ def _offline_result(self, batch: list) -> AIAnalysisResult:
259
+ """Default result when AI is unavailable — queue for human review."""
260
+ return AIAnalysisResult(
261
+ confirmed=False,
262
+ summary="AI unavailable — queued for human review",
263
+ need_human_review=True,
264
+ confidence_adjustment=0.0,
265
+ )
266
+
267
+ def _build_prompt(self, batch: list) -> str:
268
+ parts = [f"Analyze {len(batch)} security finding(s) from the same context:"]
269
+ for i, gf in enumerate(batch):
270
+ f = gf.finding
271
+ ext = Path(f.file).suffix if f.file else ""
272
+ sev = f.severity.value if hasattr(f.severity, "value") else str(f.severity)
273
+ # Tool-native confidence priors (bandit/semgrep tiers etc.)
274
+ extra = getattr(f, "extra", {}) or {}
275
+ tool_conf = extra.get("tool_confidence")
276
+ conf_note = (
277
+ f"ToolConfidence: {tool_conf:.2f} (deterministic-tool prior)"
278
+ if isinstance(tool_conf, (int, float))
279
+ else "ToolConfidence: none"
280
+ )
281
+ noise = getattr(f, "noise_category", None)
282
+ noise_note = f"\nNoiseCategory: {noise} (from a low-signal file class)" if noise else ""
283
+ parts.append(
284
+ f"\n--- Finding {i + 1} ---\n"
285
+ f"File: {f.file}:{f.line}\n"
286
+ f"CWE: {f.cwe}\n"
287
+ f"Severity: {sev}\n"
288
+ f"Type: {f.type}\n"
289
+ f"Message: {f.message}\n"
290
+ f"Detail: {getattr(f, 'detail', '')}\n"
291
+ f"Code:\n```{ext}\n{f.code_snippet}\n```\n"
292
+ f"Confirmed by: {', '.join(gf.confirmed_by) or 'single agent'}\n"
293
+ f"{conf_note}{noise_note}\n"
294
+ f"OWASP: {gf.owasp_category}\n"
295
+ )
296
+ parts.append(
297
+ "\nFor EACH finding return a verdict entry with evidence_quote "
298
+ "quoted verbatim from its Code block. JSON only."
299
+ )
300
+ return "\n".join(parts)
301
+
302
+ def _extract_json(self, text: str) -> dict | None:
303
+ """Tolerant structured-output parser: bare JSON, fenced, or embedded."""
304
+ if not text:
305
+ return None
306
+ candidate = text.strip()
307
+ # strip markdown fences if present
308
+ if candidate.startswith("```"):
309
+ candidate = candidate.strip("`")
310
+ if candidate.lower().startswith("json"):
311
+ candidate = candidate[4:]
312
+ try:
313
+ parsed = json.loads(candidate)
314
+ return parsed if isinstance(parsed, dict) else None
315
+ except (json.JSONDecodeError, ValueError):
316
+ pass
317
+ # embedded object fallback: first { to matching last }
318
+ start, end = candidate.find("{"), candidate.rfind("}")
319
+ if 0 <= start < end:
320
+ try:
321
+ parsed = json.loads(candidate[start : end + 1])
322
+ return parsed if isinstance(parsed, dict) else None
323
+ except (json.JSONDecodeError, ValueError):
324
+ return None
325
+ return None
326
+
327
+ def _throttle_if_needed(self) -> None:
328
+ now = time.time()
329
+ window = 60
330
+ # Remove timestamps older than 60s
331
+ self._call_timestamps = [t for t in self._call_timestamps if now - t < window]
332
+ if len(self._call_timestamps) >= self._rate_per_min:
333
+ sleep_sec = window - (now - self._call_timestamps[0])
334
+ if sleep_sec > 0:
335
+ time.sleep(sleep_sec)
336
+ self._call_timestamps.append(time.time())
337
+
338
+ def _cache_key(self, gf) -> str:
339
+ f = gf.finding
340
+ raw = f"{f.file}|{f.line}|{f.type}|{f.cwe}"
341
+ return hashlib.sha256(raw.encode()).hexdigest()
@@ -0,0 +1,252 @@
1
+ """
2
+ LLMSecurityAgent — LLM/Agent prompt injection, insecure output handling, tool abuse.
3
+
4
+ Detects LLM security issues:
5
+ - Direct/indirect prompt injection vectors
6
+ - LLM output executed as code, SQL, or rendered as HTML
7
+ - Unbounded input lengths / recursive agent loops
8
+ - Sensitive data in prompt context
9
+ - Untrusted model sources
10
+ - Verbose error messages to LLM
11
+ - Tool permissions too broad
12
+ - Missing human-in-the-loop for destructive actions
13
+
14
+ Uses static pattern matching, NOT AI.
15
+ Does NOT write to disk. Does NOT touch the queue.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import ast as py_ast
21
+ import re
22
+
23
+ from ..agents.base import (
24
+ AgentDomain,
25
+ AgentGroup,
26
+ AgentInput,
27
+ AgentResult,
28
+ BaseAgent,
29
+ Finding,
30
+ Severity,
31
+ register,
32
+ safe_rglob,
33
+ )
34
+
35
+
36
+ @register
37
+ class LLMSecurityAgent(BaseAgent):
38
+ """Detects LLM/agent security vulnerabilities."""
39
+
40
+ group = AgentGroup.SECURITY
41
+ domain = AgentDomain.SECURITY
42
+ name = "LLMSecurityAgent"
43
+ description = "LLM prompt injection, insecure output handling, tool abuse, excessive agency"
44
+
45
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
46
+ inp.extra.get("skill_context", {})
47
+ source_patterns = ["*.py", "*.js", "*.ts", "*.jsx", "*.tsx"]
48
+
49
+ for pattern in source_patterns:
50
+ for fpath in safe_rglob(inp.root, pattern):
51
+ if not fpath.is_file():
52
+ continue
53
+ rel = fpath.relative_to(inp.root).as_posix()
54
+ try:
55
+ content = fpath.read_text(encoding="utf-8", errors="ignore")
56
+ except OSError:
57
+ continue
58
+ result.files_scanned += 1
59
+ lang = "python" if fpath.suffix == ".py" else "javascript"
60
+
61
+ if lang == "python":
62
+ self._scan_python(content, rel, result)
63
+ else:
64
+ self._scan_js_ts(content, rel, result)
65
+
66
+ def _scan_python(self, content: str, rel: str, result: AgentResult) -> None:
67
+ # ── Prompt injection — user input in system prompt ─────────────────────
68
+ for m in re.finditer(
69
+ r'(?i)(?:system_prompt|system_message|prompt)\s*=\s*f["\'].*\{', content
70
+ ):
71
+ result.add_finding(
72
+ Finding(
73
+ agent=self.name,
74
+ type="prompt_injection",
75
+ severity=Severity.HIGH,
76
+ file=rel,
77
+ line=content[: m.start()].count("\n") + 1,
78
+ message="User-controlled string in system prompt — potential prompt injection",
79
+ suggestion="Use chat template separation with instruction/data delimiters",
80
+ cwe="CWE-94",
81
+ extra={"skill": "agent-security-audit.skill"},
82
+ )
83
+ )
84
+
85
+ # ── Insecure output — LLM output exec/eval'd ─────────────────────────
86
+ for m in re.finditer(
87
+ r"(?i)(?:exec|eval)\s*\(\s*(?:llm\.|model\.|response|result)\s*\[", content
88
+ ):
89
+ result.add_finding(
90
+ Finding(
91
+ agent=self.name,
92
+ type="insecure_llm_output",
93
+ severity=Severity.CRITICAL,
94
+ file=rel,
95
+ line=content[: m.start()].count("\n") + 1,
96
+ message="LLM output passed to exec/eval — remote code execution risk",
97
+ suggestion="Use structured output (Pydantic) with schema validation instead",
98
+ cwe="CWE-94",
99
+ extra={"skill": "llm-risk-assess.skill"},
100
+ )
101
+ )
102
+
103
+ # ── Recursive agent loop without depth limit ──────────────────────────
104
+ # (The function-self-call AST check below is authoritative; the old
105
+ # regex heuristic referenced a nonexistent \1 capture group and raised
106
+ # "invalid group reference" — removed.)
107
+ try:
108
+ tree = py_ast.parse(content)
109
+ for node in py_ast.walk(tree):
110
+ if isinstance(node, py_ast.FunctionDef):
111
+ self_name = node.name
112
+ for child in py_ast.walk(node):
113
+ if isinstance(child, py_ast.Call) and isinstance(child.func, py_ast.Name):
114
+ if child.func.id == self_name:
115
+ # Self-call — check no iteration guard
116
+ body_str = (
117
+ content[node.lineno : node.end_lineno]
118
+ if hasattr(node, "end_lineno")
119
+ else ""
120
+ )
121
+ if (
122
+ "max_iter" not in body_str
123
+ and "depth" not in body_str
124
+ and "count" not in body_str
125
+ ):
126
+ result.add_finding(
127
+ Finding(
128
+ agent=self.name,
129
+ type="recursive_agent_loop",
130
+ severity=Severity.MEDIUM,
131
+ file=rel,
132
+ line=node.lineno,
133
+ message=f"Recursive self-call in {self_name} without iteration guard",
134
+ suggestion="Add max iteration counter (recommended: 25)",
135
+ cwe="CWE-835",
136
+ )
137
+ )
138
+ break
139
+ except SyntaxError:
140
+ pass
141
+
142
+ # ── Untrusted model source ─────────────────────────────────────────
143
+ bad_loads = [
144
+ r'(?i)AutoModel\.from_pretrained\(\s*["\'][^"\']*["\']\s*\)',
145
+ r'(?i)torch\.hub\.load\(\s*["\'][^"\']*["\']\s*\)',
146
+ r'(?i)torch\.load\(\s*["\'][^"\']*\.pt',
147
+ r"(?i)pickle\.load",
148
+ ]
149
+ for pat in bad_loads:
150
+ for m in re.finditer(pat, content):
151
+ result.add_finding(
152
+ Finding(
153
+ agent=self.name,
154
+ type="untrusted_model_source",
155
+ severity=Severity.HIGH,
156
+ file=rel,
157
+ line=content[: m.start()].count("\n") + 1,
158
+ message="Model loaded from potentially untrusted source — verify origin or use safetensors",
159
+ suggestion="Pin model to SHA256 digest or verify source against allowlist",
160
+ cwe="CWE-1104",
161
+ extra={"skill": "llm-risk-assess.skill"},
162
+ )
163
+ )
164
+
165
+ # ── Sensitive data in prompt context ────────────────────────────────
166
+ for m in re.finditer(
167
+ r'(?i)(?:db_url|api_key|password|\bsecret\b|\btoken\b).*llm\.|context.*=.*f["\']',
168
+ content,
169
+ ):
170
+ result.add_finding(
171
+ Finding(
172
+ agent=self.name,
173
+ type="sensitive_data_in_prompt",
174
+ severity=Severity.HIGH,
175
+ file=rel,
176
+ line=content[: m.start()].count("\n") + 1,
177
+ message="Sensitive data potentially passed to LLM context",
178
+ suggestion="Filter or redact secrets before LLM call",
179
+ cwe="CWE-200",
180
+ extra={"skill": "llm-risk-assess.skill"},
181
+ )
182
+ )
183
+
184
+ # ── Tool permissions too broad ──────────────────────────────────────
185
+ for m in re.finditer(
186
+ r'(?i)permissions?\s*=\s*\["read"[,\s]*"write"[,\s]*"execute"\]', content
187
+ ):
188
+ result.add_finding(
189
+ Finding(
190
+ agent=self.name,
191
+ type="excessive_tool_permissions",
192
+ severity=Severity.MEDIUM,
193
+ file=rel,
194
+ line=content[: m.start()].count("\n") + 1,
195
+ message="Tool has read+write+execute — apply least privilege",
196
+ suggestion="Restrict permissions per tool; use granular scopes",
197
+ cwe="CWE-269",
198
+ extra={"skill": "agent-security-audit.skill"},
199
+ )
200
+ )
201
+
202
+ # ── Auto-approve destructive actions ───────────────────────────────
203
+ for m in re.finditer(r'(?i)agent\.run\(\s*["\'](?:delete|drop|remove|destroy)', content):
204
+ result.add_finding(
205
+ Finding(
206
+ agent=self.name,
207
+ type="excessive_agency",
208
+ severity=Severity.CRITICAL,
209
+ file=rel,
210
+ line=content[: m.start()].count("\n") + 1,
211
+ message="Destructive action auto-approved — no human in the loop",
212
+ suggestion="Implement confirmation gate for DELETE/DROP/REMOVE operations",
213
+ cwe="CWE-862",
214
+ extra={"skill": "agent-security-audit.skill"},
215
+ )
216
+ )
217
+
218
+ def _scan_js_ts(self, content: str, rel: str, result: AgentResult) -> None:
219
+ # ── LLM output rendered as HTML ─────────────────────────────────────
220
+ for m in re.finditer(
221
+ r"(?i)(?:innerHTML|outerHTML|\.html\(|dangerouslySetInnerHTML)\s*[=:].*llm|response|result",
222
+ content,
223
+ ):
224
+ result.add_finding(
225
+ Finding(
226
+ agent=self.name,
227
+ type="insecure_llm_output_rendering",
228
+ severity=Severity.HIGH,
229
+ file=rel,
230
+ line=content[: m.start()].count("\n") + 1,
231
+ message="LLM output rendered as HTML without sanitization",
232
+ suggestion="Use textContent or DOMPurify sanitization",
233
+ cwe="CWE-79",
234
+ extra={"skill": "agent-security-audit.skill"},
235
+ )
236
+ )
237
+
238
+ # ── Tool shell injection (no validation) ───────────────────────────
239
+ for m in re.finditer(r"(?i)shell\s*:\s*true|shell\s*=\s*true", content):
240
+ result.add_finding(
241
+ Finding(
242
+ agent=self.name,
243
+ type="tool_shell_injection",
244
+ severity=Severity.HIGH,
245
+ file=rel,
246
+ line=content[: m.start()].count("\n") + 1,
247
+ message="Tool uses shell=true without input validation",
248
+ suggestion="Use subprocess without shell=True, validate input against allowlist",
249
+ cwe="CWE-78",
250
+ extra={"skill": "agent-security-audit.skill"},
251
+ )
252
+ )