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,360 @@
1
+ """
2
+ Auto-Remediation Suggestions — maps finding types to actionable fix guidance.
3
+
4
+ Each finding type gets:
5
+ - A short fix action (one-liner for the UI)
6
+ - A playbook reference (if available)
7
+ - Code-level fix patterns (for the chain explorer)
8
+ - Severity-adjusted urgency (critical → immediate, info → optional)
9
+
10
+ This data feeds into:
11
+ 1. Chain explorer step cards (inline fix suggestion)
12
+ 2. Assurance chain claims (remediation field)
13
+ 3. CLI chain output (p chains --fix-hints)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ _log = logging.getLogger("patchi.security.remediation")
23
+
24
+
25
+ @dataclass
26
+ class Remediation:
27
+ """One fix suggestion for a finding type."""
28
+
29
+ finding_type: str
30
+ action: str
31
+ code_pattern: str = ""
32
+ playbook_id: str = ""
33
+ reference_url: str = ""
34
+ auto_fixable: bool = False
35
+ risk_level: str = "low" # low / medium / high (risk of the fix itself)
36
+ lang_patterns: dict[str, str] | None = None # lang -> code_pattern override
37
+
38
+ def for_language(self, lang: str) -> str:
39
+ """Get the code pattern for a specific language, falling back to default."""
40
+ if self.lang_patterns and lang in self.lang_patterns:
41
+ return self.lang_patterns[lang]
42
+ return self.code_pattern
43
+
44
+
45
+ # ── Remediation Database ────────────────────────────────────────────────────
46
+
47
+ _REMEDIATIONS: dict[str, Remediation] = {
48
+ # Secrets
49
+ "hardcoded_secret": Remediation(
50
+ finding_type="hardcoded_secret",
51
+ action="Move secret to environment variable or secrets manager",
52
+ code_pattern='Use os.environ.get("SECRET_KEY") instead of hardcoded string',
53
+ playbook_id="secrets-runtime-management",
54
+ auto_fixable=True,
55
+ ),
56
+ "weak_hash": Remediation(
57
+ finding_type="weak_hash",
58
+ action="Replace MD5/SHA1 with bcrypt, argon2, or SHA-256+",
59
+ code_pattern="Use bcrypt.checkpw() or hashlib.sha256() instead",
60
+ auto_fixable=True,
61
+ ),
62
+ "hardcoded_password": Remediation(
63
+ finding_type="hardcoded_password",
64
+ action="Remove hardcoded password, use environment variable or vault",
65
+ code_pattern='Replace with os.environ.get("DB_PASSWORD")',
66
+ playbook_id="secrets-runtime-management",
67
+ auto_fixable=True,
68
+ ),
69
+ # Injection
70
+ "sql_injection": Remediation(
71
+ finding_type="sql_injection",
72
+ action="Use parameterized queries or ORM instead of string formatting",
73
+ code_pattern='cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))',
74
+ playbook_id="data-layer",
75
+ auto_fixable=True,
76
+ lang_patterns={
77
+ "go": 'db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", userID)',
78
+ "java": 'PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); ps.setInt(1, userId);',
79
+ "rust": 'sqlx::query!("SELECT * FROM users WHERE id = $1", user_id).fetch_one(&pool)',
80
+ "php": '$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); $stmt->execute(["id" => $userId]);',
81
+ "ruby": "User.where(id: user_id).first # ActiveRecord sanitizes automatically",
82
+ },
83
+ ),
84
+ "command_injection": Remediation(
85
+ finding_type="command_injection",
86
+ action="Use subprocess.run with list args instead of shell=True",
87
+ code_pattern='subprocess.run(["ls", path], check=False)',
88
+ auto_fixable=True,
89
+ lang_patterns={
90
+ "go": 'exec.Command("ls", path).Output() # avoid exec.Command("sh", "-c", ...)',
91
+ "java": 'ProcessBuilder pb = new ProcessBuilder("ls", path); pb.start();',
92
+ "rust": 'std::process::Command::new("ls").arg(path).output()',
93
+ "php": '$output = shell_exec("ls " . escapeshellarg($path));',
94
+ "ruby": 'Open3.capture3("ls", path) # array form avoids shell interpretation',
95
+ },
96
+ ),
97
+ "xss_reflected": Remediation(
98
+ finding_type="xss_reflected",
99
+ action="Escape user input before rendering, use template auto-escaping",
100
+ code_pattern="Use {{ variable | e }} in Jinja2 templates",
101
+ auto_fixable=False,
102
+ lang_patterns={
103
+ "go": "template.HTML(template.HTMLEscapeString(userInput)) // or use html/template auto-escaping",
104
+ "java": '<%= request.getParameter("name") %> // JSP auto-escapes; use OWASP Encoder for raw',
105
+ "rust": "askama or tera templates auto-escape; avoid Markup::new()",
106
+ "php": 'htmlspecialchars($input, ENT_QUOTES, "UTF-8")',
107
+ "ruby": "ERB::Util.html_escape(user_input) // or Rails auto-escaping",
108
+ },
109
+ ),
110
+ "path_traversal": Remediation(
111
+ finding_type="path_traversal",
112
+ action="Validate and sanitize file paths, use pathlib.resolve()",
113
+ code_pattern="Path(user_input).resolve().is_relative_to(base_dir)",
114
+ auto_fixable=True,
115
+ lang_patterns={
116
+ "go": "filepath.Clean(path) + must be inside baseDir; check with strings.HasPrefix",
117
+ "java": "Path resolved = Paths.get(baseDir, userInput).normalize(); if (!resolved.startsWith(baseDir)) throw;",
118
+ "rust": "let resolved = std::fs::canonicalize(base_dir.join(&user_input))?; if !resolved.starts_with(&base_dir) { return Err(...); }",
119
+ "php": 'realpath($baseDir . "/" . $userInput) must start with realpath($baseDir)',
120
+ "ruby": "File.expand_path(user_input, base_dir).start_with?(base_dir)",
121
+ },
122
+ ),
123
+ # Auth
124
+ "missing_auth": Remediation(
125
+ finding_type="missing_auth",
126
+ action="Add authentication decorator or middleware to route",
127
+ code_pattern="@require_auth or @login_required",
128
+ playbook_id="auth-session",
129
+ auto_fixable=False,
130
+ ),
131
+ "session_fixation": Remediation(
132
+ finding_type="session_fixation",
133
+ action="Regenerate session ID after login",
134
+ code_pattern="request.session.regenerate() after successful auth",
135
+ playbook_id="auth-session",
136
+ ),
137
+ "weak_password_policy": Remediation(
138
+ finding_type="weak_password_policy",
139
+ action="Enforce minimum length, complexity, and breach-check",
140
+ code_pattern="Use password validation: minlength=12, complexity=True",
141
+ playbook_id="auth-session",
142
+ ),
143
+ # Config
144
+ "debug_enabled": Remediation(
145
+ finding_type="debug_enabled",
146
+ action="Disable debug mode in production, use environment-based config",
147
+ code_pattern='DEBUG = os.environ.get("DEBUG", "false").lower() == "true"',
148
+ auto_fixable=True,
149
+ ),
150
+ "cors_wildcard": Remediation(
151
+ finding_type="cors_wildcard",
152
+ action="Replace CORS wildcard with explicit allowed origins",
153
+ code_pattern="Access-Control-Allow-Origin: https://yourdomain.com",
154
+ playbook_id="cdn-cache-security",
155
+ auto_fixable=True,
156
+ ),
157
+ "missing_security_header": Remediation(
158
+ finding_type="missing_security_header",
159
+ action="Add security headers (HSTS, CSP, X-Frame-Options)",
160
+ code_pattern="Add middleware that sets security response headers",
161
+ playbook_id="cdn-cache-security",
162
+ auto_fixable=True,
163
+ ),
164
+ "missing_hsts": Remediation(
165
+ finding_type="missing_hsts",
166
+ action="Add Strict-Transport-Security header with long max-age",
167
+ code_pattern="Strict-Transport-Security: max-age=31536000; includeSubDomains",
168
+ playbook_id="cdn-cache-security",
169
+ ),
170
+ "missing_csp": Remediation(
171
+ finding_type="missing_csp",
172
+ action="Add Content-Security-Policy header restricting sources",
173
+ code_pattern="Content-Security-Policy: default-src 'self'; script-src 'self'",
174
+ playbook_id="cdn-cache-security",
175
+ ),
176
+ # Crypto
177
+ "weak_crypto": Remediation(
178
+ finding_type="weak_crypto",
179
+ action="Replace deprecated cipher with AES-256-GCM or ChaCha20",
180
+ code_pattern="Use cryptography.hazmat.primitives.ciphers.aead.AESGCM",
181
+ playbook_id="secrets-runtime-management",
182
+ lang_patterns={
183
+ "go": "crypto/aes + crypto/cipher.NewGCM() // AES-256-GCM",
184
+ "java": 'javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")',
185
+ "rust": "aes_gcm::Aes256Gcm::new(key) // use aes-gcm crate",
186
+ "php": 'openssl_encrypt($data, "aes-256-gcm", $key, 0, $iv, $tag)',
187
+ "ruby": 'AES-256-GCM via OpenSSL::Cipher.new("aes-256-gcm")',
188
+ },
189
+ ),
190
+ "insecure_random": Remediation(
191
+ finding_type="insecure_random",
192
+ action="Replace random module with secrets for security-sensitive values",
193
+ code_pattern="Use secrets.token_hex(32) instead of random.random()",
194
+ auto_fixable=True,
195
+ lang_patterns={
196
+ "go": "crypto/rand.Read(buf) // never math/rand for security",
197
+ "java": "java.security.SecureRandom.getInstanceStrong().nextBytes(buf)",
198
+ "rust": "use rand::rngs::OsRng; rand::RngCore::fill_bytes(&mut OsRng, &mut buf)",
199
+ "php": "random_bytes(32) // never mt_rand or rand()",
200
+ "ruby": "SecureRandom.hex(32) // never rand() for security",
201
+ },
202
+ ),
203
+ # SSRF / Network
204
+ "ssrf": Remediation(
205
+ finding_type="ssrf",
206
+ action="Validate and allowlist target URLs before making requests",
207
+ code_pattern="Check URL against allowlist before urllib.request.urlopen()",
208
+ ),
209
+ "open_redirect": Remediation(
210
+ finding_type="open_redirect",
211
+ action="Validate redirect target is internal or on allowlist",
212
+ code_pattern="if redirect_url.startswith('/'): return redirect(redirect_url)",
213
+ ),
214
+ # Business logic
215
+ "idor": Remediation(
216
+ finding_type="idor",
217
+ action="Verify requesting user owns the resource (authorization check)",
218
+ code_pattern="if resource.owner_id != current_user.id: abort(403)",
219
+ playbook_id="auth-session",
220
+ ),
221
+ "race_condition": Remediation(
222
+ finding_type="race_condition",
223
+ action="Use database-level locking or atomic operations",
224
+ code_pattern="Use SELECT FOR UPDATE or atomic increment",
225
+ ),
226
+ }
227
+
228
+
229
+ # ── Finding Type → Remediation Lookup ───────────────────────────────────────
230
+
231
+
232
+ def _normalize_type(finding_type: str) -> str:
233
+ """Normalize a finding type to a known remediation key."""
234
+ ft = finding_type.lower().strip()
235
+ # Direct match
236
+ if ft in _REMEDIATIONS:
237
+ return ft
238
+ # Substring match
239
+ for key in _REMEDIATIONS:
240
+ if key in ft or ft in key:
241
+ return key
242
+ # Fuzzy: extract core concept
243
+ mappings = {
244
+ "secret": "hardcoded_secret",
245
+ "credential": "hardcoded_secret",
246
+ "password": "hardcoded_password",
247
+ "hash": "weak_hash",
248
+ "sql": "sql_injection",
249
+ "inject": "command_injection",
250
+ "xss": "xss_reflected",
251
+ "traversal": "path_traversal",
252
+ "auth": "missing_auth",
253
+ "session": "session_fixation",
254
+ "debug": "debug_enabled",
255
+ "cors": "cors_wildcard",
256
+ "header": "missing_security_header",
257
+ "hsts": "missing_hsts",
258
+ "csp": "missing_csp",
259
+ "crypto": "weak_crypto",
260
+ "random": "insecure_random",
261
+ "ssrf": "ssrf",
262
+ "redirect": "open_redirect",
263
+ "idor": "idor",
264
+ "race": "race_condition",
265
+ }
266
+ for keyword, mapped in mappings.items():
267
+ if keyword in ft:
268
+ return mapped
269
+ return ft
270
+
271
+
272
+ def get_remediation(finding_type: str) -> Remediation | None:
273
+ """Look up remediation suggestion for a finding type."""
274
+ key = _normalize_type(finding_type)
275
+ return _REMEDIATIONS.get(key)
276
+
277
+
278
+ def get_remediation_confidence(finding_type: str, root: Path | None = None) -> float:
279
+ """Return confidence 0.0-1.0 that this remediation will fix the issue.
280
+
281
+ Factors:
282
+ - Historical fix acceptance rate (from attack_feedback learning)
283
+ - Whether the fix is auto_fixable (higher base confidence)
284
+ - Whether we have a language-specific pattern (more precise)
285
+ - Risk level of the fix itself (high risk = lower confidence)
286
+ """
287
+ rem = get_remediation(finding_type)
288
+ if not rem:
289
+ return 0.3 # unknown type = low confidence
290
+
291
+ # Base confidence from fix properties
292
+ base = 0.5
293
+ if rem.auto_fixable:
294
+ base += 0.15
295
+ if rem.code_pattern:
296
+ base += 0.05
297
+ if rem.lang_patterns:
298
+ base += 0.05
299
+ risk_penalty = {"low": 0, "medium": 0.1, "high": 0.2}.get(rem.risk_level, 0)
300
+ base -= risk_penalty
301
+
302
+ # Historical acceptance rate (if root provided)
303
+ if root:
304
+ try:
305
+ from patchi.core.security.attack_feedback import get_learning_summary
306
+
307
+ summary = get_learning_summary(root)
308
+ accepted = summary.get("accepted_fixes", {}).get(finding_type, 0)
309
+ rejected = summary.get("rejected_fixes", {}).get(finding_type, 0)
310
+ total = accepted + rejected
311
+ if total >= 3:
312
+ acceptance_rate = accepted / total
313
+ # Blend: 60% base + 40% historical
314
+ base = 0.6 * base + 0.4 * acceptance_rate
315
+ except Exception as _exc:
316
+ _log.warning('get_remediation_confidence failed: %s', _exc)
317
+
318
+ return max(0.1, min(0.95, base))
319
+
320
+
321
+ def get_remediation_for_step(step: dict) -> dict | None:
322
+ """Get remediation for a chain step (dict form from chain.to_dict())."""
323
+ ftype = step.get("type", "")
324
+ rem = get_remediation(ftype)
325
+ if not rem:
326
+ return None
327
+ return {
328
+ "action": rem.action,
329
+ "code_pattern": rem.code_pattern,
330
+ "playbook_id": rem.playbook_id,
331
+ "auto_fixable": rem.auto_fixable,
332
+ "risk_level": rem.risk_level,
333
+ }
334
+
335
+
336
+ def get_remediation_for_chain(chain: dict) -> list[dict | None]:
337
+ """Get remediation for each step in a chain."""
338
+ steps = chain.get("steps", [])
339
+ return [get_remediation_for_step(s) for s in steps]
340
+
341
+
342
+ def load_playbook_fix(playbook_id: str, control_id: str = "") -> str | None:
343
+ """Load the fix strategy from a playbook YAML file."""
344
+ if not playbook_id:
345
+ return None
346
+ pb_path = Path(__file__).parent / "fix-playbooks" / f"{playbook_id}.playbook.yaml"
347
+ if not pb_path.is_file():
348
+ return None
349
+ try:
350
+ import yaml
351
+
352
+ data = yaml.safe_load(pb_path.read_text(encoding="utf-8"))
353
+ for pb in data.get("playbooks", []):
354
+ if control_id and pb.get("control_id") != control_id:
355
+ continue
356
+ return pb.get("fix_strategy", "")
357
+ return None
358
+ except Exception as exc:
359
+ _log.debug("Playbook load failed: %s", exc)
360
+ return None
@@ -0,0 +1,294 @@
1
+ """
2
+ Runtime Request Interceptor — real-time request/response defense for hosted mode.
3
+
4
+ Intercepts HTTP requests, feeds them through the DetectionPipeline, and blocks
5
+ or modifies responses based on confidence-gated findings.
6
+
7
+ Three integration modes:
8
+ 1. ASGI middleware (FastAPI/Starlette) — recommended for Patchi's web UI
9
+ 2. WSGI middleware (Flask/Django) — fallback
10
+ 3. Reverse proxy sidecar (standalone) — separate process intercepting traffic
11
+
12
+ All modes are optional, opt-in via config:
13
+ {
14
+ "pipeline": {
15
+ "interceptor": {
16
+ "enabled": false,
17
+ "mode": "asgi", // asgi | wsgi | sidecar
18
+ "block_threshold": 0.7, // confidence score threshold for blocking
19
+ "rate_limit": 100, // max inspected requests per minute
20
+ "exclude_paths": ["/health", "/metrics", "/static"]
21
+ }
22
+ }
23
+ }
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import logging
30
+ import time
31
+ from pathlib import Path
32
+
33
+ from loguru import logger
34
+
35
+ _log = logging.getLogger("patchi.security.request_interceptor")
36
+
37
+
38
+ class RequestInterceptor:
39
+ """
40
+ Runtime request interceptor for hosted mode.
41
+
42
+ Usage (ASGI):
43
+ from patchi.core.security.request_interceptor import RequestInterceptor
44
+ interceptor = RequestInterceptor(root, config)
45
+ app.add_middleware(interceptor.asgi_middleware)
46
+
47
+ Usage (standalone inspection):
48
+ result = interceptor.inspect_request(method, path, headers, body)
49
+ if result.should_block:
50
+ return 403
51
+ """
52
+
53
+ def __init__(self, root: Path, config: dict | None = None):
54
+ self.root = root
55
+ self.config = config or {}
56
+ interceptor_cfg = self.config.get("pipeline", {}).get("interceptor", {})
57
+ self.enabled = interceptor_cfg.get("enabled", False)
58
+ self.block_threshold = interceptor_cfg.get("block_threshold", 0.7)
59
+ self.rate_limit = interceptor_cfg.get("rate_limit", 100) # per minute
60
+ self.exclude_paths = set(
61
+ interceptor_cfg.get("exclude_paths", ["/health", "/metrics", "/static"])
62
+ )
63
+
64
+ # Rate limit tracking
65
+ self._window_start = time.monotonic()
66
+ self._window_count = 0
67
+
68
+ # Threat intelligence cache (IP → threat score)
69
+ self._threat_ips: dict[str, dict] = {}
70
+ self._injection_cache: dict[str, float] = {} # normalized payload → confidence
71
+ # Bound the caches: keys are client-controlled, so unbounded growth is
72
+ # a memory DoS on a long-running hosted process.
73
+ self._max_threat_ips = 10_000
74
+ self._max_injection_cache = 10_000
75
+
76
+ def should_inspect(self, path: str) -> bool:
77
+ """Check if path should be inspected (not excluded, not static)."""
78
+ if not self.enabled:
79
+ return False
80
+ for excluded in self.exclude_paths:
81
+ if path.startswith(excluded):
82
+ return False
83
+ return True
84
+
85
+ def _check_rate_limit(self) -> bool:
86
+ """Check if we're within rate limit. Returns True if allowed."""
87
+ now = time.monotonic()
88
+ if now - self._window_start > 60:
89
+ self._window_start = now
90
+ self._window_count = 0
91
+ if self._window_count >= self.rate_limit:
92
+ return False
93
+ self._window_count += 1
94
+ return True
95
+
96
+ # ── Request inspection ────────────────────────────────────────────────
97
+
98
+ def inspect_request(self, method: str, path: str, headers: dict, body: str = "") -> dict:
99
+ """
100
+ Inspect a single HTTP request for threats.
101
+
102
+ Returns:
103
+ {
104
+ "should_block": bool,
105
+ "confidence": float,
106
+ "threat_type": str | None,
107
+ "reason": str | None,
108
+ "findings": list[dict],
109
+ }
110
+ """
111
+ if not self._check_rate_limit():
112
+ return {
113
+ "should_block": False,
114
+ "confidence": 0.0,
115
+ "threat_type": None,
116
+ "reason": "Rate limited (inspector)",
117
+ "findings": [],
118
+ }
119
+
120
+ findings = []
121
+ ip = (
122
+ headers.get("x-forwarded-for", headers.get("remote-addr", "unknown"))
123
+ .split(",")[0]
124
+ .strip()
125
+ )
126
+
127
+ # 1. Known malicious IP check
128
+ if ip in self._threat_ips:
129
+ ti = self._threat_ips[ip]
130
+ if ti["score"] >= self.block_threshold:
131
+ return {
132
+ "should_block": True,
133
+ "confidence": ti["score"],
134
+ "threat_type": "known_malicious_ip",
135
+ "reason": f"Known malicious IP (score: {ti['score']})",
136
+ "findings": [],
137
+ }
138
+
139
+ # 2. Injection detection in path/body
140
+ body_lower = body.lower()
141
+ query = path.lower()
142
+
143
+ injection_patterns = [
144
+ ("sqli", ["'", '"', "union select", "or 1=1", "drop table", ";--", "admin'--"]),
145
+ ("xss", ["<script", "onerror=", "onload=", "javascript:", "alert("]),
146
+ ("path_traversal", ["../", "..\\", "%2e%2e%2f", "....//"]),
147
+ ("command_injection", ["; ", "| ", "`", "$(", "&& ", "|| "]),
148
+ ("ssti", ["{{", "{%", "${", "#{"]),
149
+ ]
150
+
151
+ for threat_type, patterns in injection_patterns:
152
+ for pat in patterns:
153
+ if pat in body_lower or pat in query:
154
+ confidence = 0.8 if pat in body_lower else 0.6
155
+ if len(self._injection_cache) < self._max_injection_cache:
156
+ cache_key = f"{threat_type}:{hash(body)}"
157
+ self._injection_cache[cache_key] = confidence
158
+ findings.append(
159
+ {
160
+ "type": threat_type,
161
+ "confidence": confidence,
162
+ "pattern": pat,
163
+ "location": "body" if pat in body_lower else "path",
164
+ }
165
+ )
166
+
167
+ # 3. Rate spike detection (same IP, multiple requests)
168
+ if ip not in self._threat_ips:
169
+ if len(self._threat_ips) >= self._max_threat_ips:
170
+ self.clear_stale_threats()
171
+ if len(self._threat_ips) >= self._max_threat_ips:
172
+ # Still at the cap — drop the oldest entry instead of growing
173
+ # without bound.
174
+ self._threat_ips.pop(next(iter(self._threat_ips)))
175
+ self._threat_ips[ip] = {"score": 0.0, "count": 0, "first_seen": time.time()}
176
+ self._threat_ips[ip]["count"] += 1
177
+
178
+ ip_data = self._threat_ips[ip]
179
+ elapsed = time.time() - ip_data["first_seen"]
180
+ # Only judge sustained request rates over a real window. Measuring
181
+ # bursts against a sub-second elapsed time makes any two quick
182
+ # requests look like thousands per second (false positive).
183
+ if elapsed >= 1.0:
184
+ rps = ip_data["count"] / elapsed
185
+ if rps > 50: # more than 50 req/s from same IP
186
+ ip_data["score"] = min(1.0, ip_data["score"] + 0.2)
187
+ findings.append(
188
+ {
189
+ "type": "rate_spike",
190
+ "confidence": min(0.9, rps / 100),
191
+ "pattern": f"{ip_data['count']} requests in {elapsed:.0f}s",
192
+ "location": "ip",
193
+ }
194
+ )
195
+
196
+ if not findings:
197
+ return {
198
+ "should_block": False,
199
+ "confidence": 0.0,
200
+ "threat_type": None,
201
+ "reason": "No threats detected",
202
+ "findings": [],
203
+ }
204
+
205
+ # Compute max confidence
206
+ max_conf = max(f["confidence"] for f in findings)
207
+ top_threat = max(findings, key=lambda f: f["confidence"])
208
+
209
+ return {
210
+ "should_block": max_conf >= self.block_threshold,
211
+ "confidence": max_conf,
212
+ "threat_type": top_threat["type"],
213
+ "reason": f"Detected {top_threat['type']} (confidence: {max_conf:.2f})",
214
+ "findings": findings,
215
+ }
216
+
217
+ # ── ASGI middleware ───────────────────────────────────────────────────
218
+
219
+ async def asgi_middleware(self, scope: dict, receive: callable, send: callable) -> None:
220
+ """ASGI middleware for FastAPI/Starlette."""
221
+ if scope["type"] != "http":
222
+ # Non-HTTP scopes (lifespan, websockets) must pass through
223
+ # untouched — `send` takes a message, not the scope tuple.
224
+ return
225
+
226
+ path = scope.get("path", "/")
227
+ if not self.should_inspect(path):
228
+ return
229
+
230
+ method = scope.get("method", "GET")
231
+ headers = {k.decode(): v.decode() for k, v in scope.get("headers", [])}
232
+
233
+ # Read request body
234
+ body = ""
235
+ if method in ("POST", "PUT", "PATCH"):
236
+ try:
237
+ more = True
238
+ while more:
239
+ message = await receive()
240
+ if message["type"] == "http.request":
241
+ body += message.get("body", b"").decode("utf-8", errors="ignore")
242
+ more = message.get("more_body", False)
243
+ else:
244
+ break
245
+ except Exception as e:
246
+ _log.warning("RequestInterceptor.asgi_middleware failed: %s", e)
247
+
248
+ result = self.inspect_request(method, path, headers, body)
249
+ if result["should_block"]:
250
+ logger.warning(f"Blocked request: {method} {path} — {result['reason']}")
251
+ await send(
252
+ {
253
+ "type": "http.response.start",
254
+ "status": 403,
255
+ "headers": [(b"content-type", b"application/json")],
256
+ }
257
+ )
258
+ await send(
259
+ {
260
+ "type": "http.response.body",
261
+ "body": json.dumps(
262
+ {"error": "Request blocked", "reason": result["reason"]}
263
+ ).encode(),
264
+ }
265
+ )
266
+ return
267
+
268
+ # ── Threat intelligence maintenance ───────────────────────────────────
269
+
270
+ def add_threat_ip(self, ip: str, score: float = 1.0, reason: str = "") -> None:
271
+ """Manually add an IP to the threat list (from DefenseLayer block_ip)."""
272
+ self._threat_ips[ip] = {
273
+ "score": score,
274
+ "count": 0,
275
+ "first_seen": time.time(),
276
+ "reason": reason,
277
+ }
278
+
279
+ def clear_stale_threats(self, max_age: int = 3600) -> int:
280
+ """Remove threat IPs older than max_age seconds. Returns count removed."""
281
+ now = time.time()
282
+ stale = [ip for ip, data in self._threat_ips.items() if now - data["first_seen"] > max_age]
283
+ for ip in stale:
284
+ del self._threat_ips[ip]
285
+ return len(stale)
286
+
287
+ def export_threats(self) -> dict:
288
+ """Export current threat intelligence for dashboard."""
289
+ return {
290
+ "threat_ips": len(self._threat_ips),
291
+ "injection_cache": len(self._injection_cache),
292
+ "block_threshold": self.block_threshold,
293
+ "rate_limit_per_min": self.rate_limit,
294
+ }