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,238 @@
1
+ """
2
+ DefectDojoReporter — optional push of Patchi defense results to DefectDojo.
3
+
4
+ DefectDojo is an open-source vulnerability management platform.
5
+ https://github.com/DefectDojo/django-DefectDojo
6
+
7
+ Configuration (opt-in, disabled by default):
8
+ {
9
+ "defectdojo": {
10
+ "enabled": false,
11
+ "url": "https://demo.defectdojo.org",
12
+ "api_key": "",
13
+ "auto_create_product": true,
14
+ "product_name": "auto", // "auto" = use project directory name
15
+ "engagement_prefix": "scan-"
16
+ }
17
+ }
18
+
19
+ Usage:
20
+ reporter = DefectDojoReporter(config)
21
+ reporter.push_gated_report(gated_report, project_root)
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import time
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from loguru import logger
31
+
32
+
33
+ class DefectDojoReporter:
34
+ """
35
+ Push Patchi defense pipeline results to DefectDojo.
36
+
37
+ All operations are optional and fail gracefully — never block scans.
38
+ """
39
+
40
+ def __init__(self, config: dict | None = None):
41
+ self.config = config or {}
42
+ dd_cfg = self.config.get("defectdojo", {})
43
+ self.enabled = dd_cfg.get("enabled", False)
44
+ self.base_url = dd_cfg.get("url", "").rstrip("/")
45
+ self.api_key = dd_cfg.get("api_key", "")
46
+ self.auto_create_product = dd_cfg.get("auto_create_product", True)
47
+ self.product_name = dd_cfg.get("product_name", "auto")
48
+ self.engagement_prefix = dd_cfg.get("engagement_prefix", "scan-")
49
+
50
+ if self.enabled and not self.base_url:
51
+ logger.warning("DefectDojo enabled but no URL configured — disabling")
52
+ self.enabled = False
53
+ if self.enabled and not self.api_key:
54
+ logger.warning("DefectDojo enabled but no API key configured — disabling")
55
+ self.enabled = False
56
+
57
+ def push_gated_report(self, gated_report: Any, root: Path) -> dict:
58
+ """
59
+ Push a GatedReport to DefectDojo.
60
+
61
+ Args:
62
+ gated_report: GatedReport from DetectionPipeline.process()
63
+ root: Project root Path (used for product name)
64
+
65
+ Returns:
66
+ dict with status and details
67
+ """
68
+ if not self.enabled:
69
+ return {"status": "skipped", "reason": "DefectDojo not enabled"}
70
+
71
+ try:
72
+ import httpx # noqa: F401 — used to check availability
73
+ except ImportError:
74
+ logger.warning("httpx not installed — cannot push to DefectDojo")
75
+ return {"status": "skipped", "reason": "httpx not installed"}
76
+
77
+ product_name = Path(root).name if self.product_name == "auto" else self.product_name
78
+
79
+ # Build findings payload
80
+ findings = []
81
+ for gf in getattr(gated_report, "findings", []):
82
+ f = getattr(gf, "finding", gf)
83
+ findings.append(self._finding_to_dd(f, gf))
84
+
85
+ if not findings:
86
+ return {"status": "skipped", "reason": "No findings to push"}
87
+
88
+ # Ensure product and engagement exist
89
+ product_id = self._get_or_create_product(product_name)
90
+ if not product_id:
91
+ return {"status": "error", "reason": "Failed to get/create product"}
92
+
93
+ engagement_id = self._create_engagement(product_id, product_name)
94
+ if not engagement_id:
95
+ return {"status": "error", "reason": "Failed to create engagement"}
96
+
97
+ # Push import scan
98
+ return self._import_scan(engagement_id, findings, product_name)
99
+
100
+ # ── Finding conversion ────────────────────────────────────────────────
101
+
102
+ def _finding_to_dd(self, finding: Any, gated_finding: Any = None) -> dict:
103
+ """Convert a Patchi Finding to DefectDojo finding format."""
104
+ severity_map = {
105
+ "critical": "Critical",
106
+ "high": "High",
107
+ "medium": "Medium",
108
+ "low": "Low",
109
+ "info": "Info",
110
+ }
111
+
112
+ cwe = 0
113
+ if hasattr(finding, "cwe") and finding.cwe:
114
+ try:
115
+ cwe = int(str(finding.cwe).replace("CWE-", ""))
116
+ except ValueError:
117
+ cwe = 0
118
+
119
+ can_defend = False
120
+ if gated_finding is not None:
121
+ can_defend = getattr(gated_finding, "can_defend", False)
122
+
123
+ severity = severity_map.get(
124
+ getattr(finding, "severity", "info").value
125
+ if hasattr(getattr(finding, "severity", ""), "value")
126
+ else str(getattr(finding, "severity", "info")).lower(),
127
+ "Info",
128
+ )
129
+
130
+ return {
131
+ "title": getattr(finding, "message", "Untitled finding")[:500],
132
+ "severity": severity,
133
+ "file_path": getattr(finding, "file", ""),
134
+ "line": getattr(finding, "line", 0),
135
+ "cwe": cwe,
136
+ "description": getattr(finding, "detail", "")[:2000],
137
+ "remediation": getattr(finding, "suggestion", "")[:1000],
138
+ "active": True,
139
+ "verified": can_defend,
140
+ "false_positive": False,
141
+ "duplicate": False,
142
+ }
143
+
144
+ # ── DefectDojo API calls ──────────────────────────────────────────────
145
+
146
+ def _api_call(self, method: str, endpoint: str, **kwargs) -> dict | None:
147
+ """Make an HTTP call to DefectDojo API."""
148
+ try:
149
+ import httpx
150
+ except ImportError:
151
+ return None
152
+
153
+ url = f"{self.base_url}/api/v2/{endpoint}"
154
+ headers = {
155
+ "Authorization": f"Token {self.api_key}",
156
+ "Content-Type": "application/json",
157
+ }
158
+
159
+ try:
160
+ if method == "GET":
161
+ r = httpx.get(url, headers=headers, params=kwargs.get("params"), timeout=30)
162
+ elif method == "POST":
163
+ r = httpx.post(url, headers=headers, json=kwargs.get("json", {}), timeout=30)
164
+ else:
165
+ return None
166
+
167
+ if r.status_code in (200, 201):
168
+ return r.json()
169
+ logger.debug(f"DefectDojo API error: {r.status_code} {r.text[:200]}")
170
+ return None
171
+ except Exception as e:
172
+ logger.debug(f"DefectDojo request failed: {e}")
173
+ return None
174
+
175
+ def _get_or_create_product(self, product_name: str) -> int | None:
176
+ """Get existing product or create new one. Returns product_id."""
177
+ # Try to find existing product
178
+ result = self._api_call("GET", "products", params={"limit": 100})
179
+ if result:
180
+ for p in result.get("results", []):
181
+ if p.get("name") == product_name:
182
+ return p["id"]
183
+
184
+ # Create if auto-create enabled
185
+ if not self.auto_create_product:
186
+ logger.warning(f"Product '{product_name}' not found and auto_create is false")
187
+ return None
188
+
189
+ result = self._api_call(
190
+ "POST",
191
+ "products",
192
+ json={
193
+ "name": product_name,
194
+ "description": f"Patchi scan findings for {product_name}",
195
+ "prod_type": 1, # default product type
196
+ },
197
+ )
198
+ if result:
199
+ return result["id"]
200
+ return None
201
+
202
+ def _create_engagement(self, product_id: int, product_name: str) -> int | None:
203
+ """Create a new engagement for a scan. Returns engagement_id."""
204
+ engagement_name = f"{self.engagement_prefix}{int(time.time())}"
205
+ result = self._api_call(
206
+ "POST",
207
+ "engagements",
208
+ json={
209
+ "name": engagement_name,
210
+ "product": product_id,
211
+ "target_start": time.strftime("%Y-%m-%d"),
212
+ "target_end": time.strftime("%Y-%m-%d"),
213
+ "engagement_type": "CI/CD",
214
+ "status": "In Progress",
215
+ "description": f"Patchi scan of {product_name}",
216
+ },
217
+ )
218
+ if result:
219
+ return result["id"]
220
+ return None
221
+
222
+ def _import_scan(self, engagement_id: int, findings: list[dict], product_name: str) -> dict:
223
+ """Push findings as an import-scan."""
224
+ result = self._api_call(
225
+ "POST",
226
+ "import-scan/",
227
+ json={
228
+ "engagement": engagement_id,
229
+ "scan_type": "Patchi Scan",
230
+ "product_name": product_name,
231
+ "findings": findings,
232
+ },
233
+ )
234
+ if result:
235
+ count = len(findings)
236
+ logger.info(f"Pushed {count} findings to DefectDojo (engagement {engagement_id})")
237
+ return {"status": "success", "finding_count": count, "engagement_id": engagement_id}
238
+ return {"status": "error", "reason": "Import scan API call failed"}
@@ -0,0 +1,75 @@
1
+ """Defenders package — adapter registry for defense actions.
2
+
3
+ Usage::
4
+
5
+ from patchi.core.security.defenders import ADAPTER_REGISTRY, get_adapter
6
+ from patchi.core.security.defenders.base import DefenseAction
7
+
8
+ adapter = get_adapter("fix_code", root=path, risk_gate=gate)
9
+ result = adapter.execute(action)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ from .auth_middleware import AuthMiddlewareAdapter
17
+ from .base import BaseAdapter, DefendResult, DefenseAction
18
+ from .block_ip import BlockIpAdapter
19
+ from .block_ws_origin import BlockWsOriginAdapter
20
+ from .crypto_fix import CryptoFixAdapter
21
+ from .enforce_rate_limit import EnforceRateLimitAdapter
22
+ from .fix_code import FixCodeAdapter
23
+ from .invalidate_session import InvalidateSessionAdapter
24
+ from .patch_config import PatchConfigAdapter
25
+ from .rotate_secret import RotateSecretAdapter
26
+ from .suspend_account import SuspendAccountAdapter
27
+ from .update_dependency import UpdateDependencyAdapter
28
+
29
+ # Registry: action_type string → adapter class
30
+ ADAPTER_REGISTRY: dict[str, type[BaseAdapter]] = {
31
+ "fix_code": FixCodeAdapter,
32
+ "update_dependency": UpdateDependencyAdapter,
33
+ "block_ip": BlockIpAdapter,
34
+ "rotate_secret": RotateSecretAdapter,
35
+ "patch_config": PatchConfigAdapter,
36
+ "crypto_fix": CryptoFixAdapter,
37
+ "auth_middleware": AuthMiddlewareAdapter,
38
+ "invalidate_session": InvalidateSessionAdapter,
39
+ "enforce_rate_limit": EnforceRateLimitAdapter,
40
+ "suspend_account": SuspendAccountAdapter,
41
+ "block_ws_origin": BlockWsOriginAdapter,
42
+ }
43
+
44
+ __all__ = [
45
+ "ADAPTER_REGISTRY",
46
+ "BaseAdapter",
47
+ "DefenseAction",
48
+ "DefendResult",
49
+ "get_adapter",
50
+ ]
51
+
52
+
53
+ def get_adapter(action_type: str, root: Path, risk_gate) -> BaseAdapter:
54
+ """Get an adapter instance for the given action type.
55
+
56
+ Falls back to a no-op adapter if the type is unknown.
57
+ """
58
+ cls = ADAPTER_REGISTRY.get(action_type)
59
+ if cls is not None:
60
+ return cls(root=root, risk_gate=risk_gate)
61
+
62
+ # Fallback: return a stub that queues for human review
63
+ class _FallbackAdapter(BaseAdapter):
64
+ action_type = "" # overridden below
65
+
66
+ def execute(self, _action: DefenseAction) -> DefendResult:
67
+ return DefendResult(
68
+ action="queued",
69
+ reason=f"No auto-defense available for '{_action.type}' — queued for human review",
70
+ defense_action=_action,
71
+ )
72
+
73
+ adapter = _FallbackAdapter(root=root, risk_gate=risk_gate)
74
+ adapter.action_type = action_type
75
+ return adapter
@@ -0,0 +1,115 @@
1
+ """Adapter: auth_middleware — add authentication middleware / decorator to an endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import uuid
7
+ from pathlib import Path
8
+
9
+ from patchi.core.fix.patch import FileChange, Patch
10
+
11
+ from .base import BaseAdapter, DefendResult, DefenseAction
12
+
13
+ _log = logging.getLogger("patchi.security.defenders.auth_middleware")
14
+
15
+
16
+ class AuthMiddlewareAdapter(BaseAdapter):
17
+ """Add authentication middleware / decorator to an endpoint."""
18
+
19
+ action_type = "auth_middleware"
20
+
21
+ def execute(self, action: DefenseAction) -> DefendResult:
22
+ if not action.target or not Path(action.target).exists():
23
+ return DefendResult(
24
+ action="skipped",
25
+ reason=f"File does not exist: {action.target}",
26
+ defense_action=action,
27
+ )
28
+ fpath = Path(action.target)
29
+ try:
30
+ content = fpath.read_text(encoding="utf-8", errors="ignore")
31
+ except OSError as e:
32
+ return DefendResult(action="blocked", reason=f"Read error: {e}", defense_action=action)
33
+
34
+ changes = []
35
+ if "flask" in content.lower() or "from flask" in content:
36
+ if (
37
+ "from flask_login import login_required" not in content
38
+ and "from flask_login import" not in content
39
+ ):
40
+ lines = content.splitlines()
41
+ insert_at = 0
42
+ for i, line in enumerate(lines):
43
+ if line.startswith("#!") or line.startswith("# -*-"):
44
+ insert_at = i + 1
45
+ else:
46
+ break
47
+ lines.insert(insert_at, "from flask_login import login_required")
48
+ modified = "\n".join(lines)
49
+ changes.append(FileChange(path=str(fpath), original=content, proposed=modified))
50
+ elif "django" in content.lower() or "from django" in content:
51
+ if "from django.contrib.auth.decorators import login_required" not in content:
52
+ changes.append(
53
+ FileChange(
54
+ path=str(fpath),
55
+ original=content,
56
+ proposed="from django.contrib.auth.decorators import login_required\n"
57
+ + content,
58
+ )
59
+ )
60
+ elif "fastapi" in content.lower() or "from fastapi" in content:
61
+ if "Depends" not in content and "get_current_user" not in content:
62
+ changes.append(
63
+ FileChange(
64
+ path=str(fpath),
65
+ original=content,
66
+ proposed="from fastapi import Depends\nfrom .auth import get_current_user\n"
67
+ + content,
68
+ )
69
+ )
70
+
71
+ if not changes:
72
+ return DefendResult(
73
+ action="skipped",
74
+ reason="No recognizable framework found for auth middleware insertion",
75
+ defense_action=action,
76
+ )
77
+
78
+ patch = Patch(
79
+ id=uuid.uuid4().hex[:12],
80
+ agent=action.finding.agent,
81
+ finding_id=action.finding.type,
82
+ patch_type="SECURITY",
83
+ changes=changes,
84
+ description="Add authentication middleware",
85
+ risk_score=30,
86
+ confidence=70,
87
+ blast_radius=2,
88
+ )
89
+ gate_result = self.risk_gate.evaluate(patch)
90
+ if gate_result.is_auto:
91
+ try:
92
+ patch.apply()
93
+ return DefendResult(
94
+ action="applied",
95
+ reason="Auth middleware template added",
96
+ defense_action=action,
97
+ patch=patch,
98
+ gate_result=gate_result,
99
+ )
100
+ except Exception as e:
101
+ return DefendResult(
102
+ action="blocked",
103
+ reason=f"Apply failed: {e}",
104
+ defense_action=action,
105
+ patch=patch,
106
+ gate_result=gate_result,
107
+ )
108
+ else:
109
+ return DefendResult(
110
+ action="queued" if not gate_result.is_blocked else "blocked",
111
+ reason=gate_result.reason,
112
+ defense_action=action,
113
+ patch=patch,
114
+ gate_result=gate_result,
115
+ )
@@ -0,0 +1,97 @@
1
+ """Base adapter for defense actions.
2
+
3
+ Each concrete adapter handles one defense action type
4
+ (fix_code, block_ip, rotate_secret, etc.) and implements
5
+ ``execute(action) -> DefendResult``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import abc
11
+ import time
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+
15
+ from patchi.core.agents.base import Finding
16
+ from patchi.core.fix.patch import Patch
17
+ from patchi.core.fix.risk_gate import GateResult
18
+
19
+ _log_name = "patchi.security.defenders"
20
+
21
+
22
+ @dataclass
23
+ class DefenseAction:
24
+ """A concrete action to defend against a confirmed finding."""
25
+
26
+ type: str
27
+ target: str
28
+ finding: Finding
29
+ severity: str
30
+ fix_code: str = ""
31
+
32
+ def to_dict(self) -> dict:
33
+ return {
34
+ "type": self.type,
35
+ "target": self.target,
36
+ "severity": self.severity,
37
+ "finding_id": self.finding.type,
38
+ "finding_file": self.finding.file,
39
+ }
40
+
41
+
42
+ @dataclass
43
+ class DefendResult:
44
+ """Result of executing a defense action."""
45
+
46
+ action: str # "applied" | "queued" | "blocked" | "skipped"
47
+ reason: str
48
+ defense_action: DefenseAction | None = None
49
+ patch: Patch | None = None
50
+ gate_result: GateResult | None = None
51
+ timestamp: float = field(default_factory=time.time)
52
+
53
+ def to_dict(self) -> dict:
54
+ d = {
55
+ "action": self.action,
56
+ "reason": self.reason,
57
+ "timestamp": self.timestamp,
58
+ }
59
+ if self.defense_action:
60
+ d["defense_action"] = self.defense_action.to_dict()
61
+ if self.patch:
62
+ d["patch_id"] = self.patch.id
63
+ return d
64
+
65
+
66
+ class BaseAdapter(abc.ABC):
67
+ """All defense adapters must subclass this."""
68
+
69
+ action_type: str # must be set by subclass
70
+
71
+ def __init__(self, root: Path, risk_gate):
72
+ self.root = root
73
+ self.risk_gate = risk_gate
74
+
75
+ @abc.abstractmethod
76
+ def execute(self, action: DefenseAction) -> DefendResult:
77
+ """Execute the defense action and return a result."""
78
+
79
+ def _estimate_risk(self, action: DefenseAction) -> int:
80
+ """Estimate risk score for a defense action (0-100)."""
81
+ sev_scores = {"critical": 80, "high": 60, "medium": 40, "low": 20, "info": 10}
82
+ base = sev_scores.get(action.severity, 30)
83
+ action_risk = {
84
+ "fix_code": 0,
85
+ "update_dependency": 10,
86
+ "block_ip": 5,
87
+ "rotate_secret": 15,
88
+ "patch_config": 20,
89
+ "crypto_fix": 5,
90
+ "auth_middleware": 15,
91
+ "invalidate_session": 25,
92
+ "enforce_rate_limit": 10,
93
+ "suspend_account": 40,
94
+ "block_ws_origin": 10,
95
+ "escalate": 0,
96
+ }
97
+ return min(100, base + action_risk.get(action.type, 0))
@@ -0,0 +1,71 @@
1
+ """Adapter: block_ip — block IP via OS firewall and persist to .patchi/blocked_ips.json."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import subprocess
8
+
9
+ from .base import BaseAdapter, DefendResult, DefenseAction
10
+
11
+ _log = logging.getLogger("patchi.security.defenders.block_ip")
12
+
13
+
14
+ class BlockIpAdapter(BaseAdapter):
15
+ """Block an IP address via OS firewall (hosted mode only)."""
16
+
17
+ action_type = "block_ip"
18
+
19
+ def execute(self, action: DefenseAction) -> DefendResult:
20
+ ip = action.target
21
+ if not ip:
22
+ return DefendResult(
23
+ action="skipped",
24
+ reason="No IP address to block",
25
+ defense_action=action,
26
+ )
27
+
28
+ blocked_path = self.root / ".patchi" / "blocked_ips.json"
29
+ blocked: list = []
30
+ if blocked_path.exists():
31
+ try:
32
+ blocked = json.loads(blocked_path.read_text(encoding="utf-8"))
33
+ except Exception as e:
34
+ _log.warning("BlockIpAdapter read failed: %s", e)
35
+ blocked = []
36
+ if ip not in blocked:
37
+ blocked.append(ip)
38
+ blocked_path.parent.mkdir(parents=True, exist_ok=True)
39
+ blocked_path.write_text(json.dumps(blocked, indent=2), encoding="utf-8")
40
+
41
+ try:
42
+ result = subprocess.run(
43
+ ["iptables", "-A", "INPUT", "-s", ip, "-j", "DROP"],
44
+ capture_output=True,
45
+ text=True,
46
+ timeout=10,
47
+ )
48
+ if result.returncode == 0:
49
+ return DefendResult(
50
+ action="applied",
51
+ reason=f"Blocked IP {ip} via iptables (persisted to .patchi/blocked_ips.json)",
52
+ defense_action=action,
53
+ )
54
+ else:
55
+ return DefendResult(
56
+ action="applied",
57
+ reason=f"Blocked IP {ip} persisted to .patchi/blocked_ips.json (iptables unavailable: {result.stderr[:100]})",
58
+ defense_action=action,
59
+ )
60
+ except FileNotFoundError:
61
+ return DefendResult(
62
+ action="applied",
63
+ reason=f"Blocked IP {ip} persisted to .patchi/blocked_ips.json (iptables not available)",
64
+ defense_action=action,
65
+ )
66
+ except subprocess.TimeoutExpired:
67
+ return DefendResult(
68
+ action="applied",
69
+ reason=f"Blocked IP {ip} persisted to .patchi/blocked_ips.json (iptables timed out)",
70
+ defense_action=action,
71
+ )
@@ -0,0 +1,36 @@
1
+ """Adapter: block_ws_origin — block a WebSocket origin via config patch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+
8
+ from .base import BaseAdapter, DefendResult, DefenseAction
9
+
10
+ _log = logging.getLogger("patchi.security.defenders.block_ws_origin")
11
+
12
+
13
+ class BlockWsOriginAdapter(BaseAdapter):
14
+ """Block a WebSocket origin via config patch."""
15
+
16
+ action_type = "block_ws_origin"
17
+
18
+ def execute(self, action: DefenseAction) -> DefendResult:
19
+ ws_path = self.root / ".patchi" / "blocked_ws_origins.json"
20
+ target = action.target or "unknown_origin"
21
+ blocked: list = []
22
+ if ws_path.exists():
23
+ try:
24
+ blocked = json.loads(ws_path.read_text(encoding="utf-8"))
25
+ except Exception as e:
26
+ _log.warning("BlockWsOriginAdapter read failed: %s", e)
27
+ blocked = []
28
+ if target not in blocked:
29
+ blocked.append(target)
30
+ ws_path.parent.mkdir(parents=True, exist_ok=True)
31
+ ws_path.write_text(json.dumps(blocked, indent=2), encoding="utf-8")
32
+ return DefendResult(
33
+ action="applied",
34
+ reason=f"Blocked WebSocket origin: {target} — written to .patchi/blocked_ws_origins.json",
35
+ defense_action=action,
36
+ )