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,272 @@
1
+ """
2
+ Policy Engine — YAML/JSON policy enforcement for security compliance.
3
+
4
+ Loads policy definitions from .patchi/policies/ and enforces them
5
+ against code and findings. Supports SOC2, HIPAA, PCI-DSS, CIS packs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import re
13
+
14
+ import yaml
15
+
16
+ from patchi.core.agents.base import (
17
+ AgentGroup,
18
+ AgentInput,
19
+ AgentResult,
20
+ BaseAgent,
21
+ Finding,
22
+ Severity,
23
+ register,
24
+ safe_rglob,
25
+ )
26
+
27
+ # ── Built-in policy packs ─────────────────────────────────────────────────────
28
+
29
+ _BUILTIN_POLICIES: dict[str, list[dict]] = {
30
+ "soc2": [
31
+ {
32
+ "id": "SOC2-CC6.1",
33
+ "name": "Access Control",
34
+ "check": "no_default_credentials",
35
+ "severity": "critical",
36
+ },
37
+ {
38
+ "id": "SOC2-CC6.6",
39
+ "name": "Encryption in Transit",
40
+ "check": "no_plaintext_http",
41
+ "severity": "high",
42
+ },
43
+ {"id": "SOC2-CC7.1", "name": "Logging", "check": "has_logging", "severity": "medium"},
44
+ {
45
+ "id": "SOC2-CC8.1",
46
+ "name": "Change Management",
47
+ "check": "has_audit_trail",
48
+ "severity": "medium",
49
+ },
50
+ ],
51
+ "hipaa": [
52
+ {
53
+ "id": "HIPAA-164.312",
54
+ "name": "Access Control",
55
+ "check": "no_default_credentials",
56
+ "severity": "critical",
57
+ },
58
+ {
59
+ "id": "HIPAA-164.312",
60
+ "name": "Encryption",
61
+ "check": "no_weak_crypto",
62
+ "severity": "critical",
63
+ },
64
+ {
65
+ "id": "HIPAA-164.312",
66
+ "name": "Audit Controls",
67
+ "check": "has_logging",
68
+ "severity": "high",
69
+ },
70
+ {
71
+ "id": "HIPAA-164.312",
72
+ "name": "Data Integrity",
73
+ "check": "no_hardcoded_secrets",
74
+ "severity": "critical",
75
+ },
76
+ ],
77
+ "pci_dss": [
78
+ {
79
+ "id": "PCI-DSS-2.1",
80
+ "name": "No Default Credentials",
81
+ "check": "no_default_credentials",
82
+ "severity": "critical",
83
+ },
84
+ {
85
+ "id": "PCI-DSS-3.4",
86
+ "name": "Encryption",
87
+ "check": "no_weak_crypto",
88
+ "severity": "critical",
89
+ },
90
+ {
91
+ "id": "PCI-DSS-6.5",
92
+ "name": "Secure Development",
93
+ "check": "no_sql_injection",
94
+ "severity": "critical",
95
+ },
96
+ {
97
+ "id": "PCI-DSS-10.2",
98
+ "name": "Audit Trail",
99
+ "check": "has_audit_trail",
100
+ "severity": "high",
101
+ },
102
+ ],
103
+ "cis": [
104
+ {
105
+ "id": "CIS-1.1",
106
+ "name": "File Permissions",
107
+ "check": "no_world_readable",
108
+ "severity": "high",
109
+ },
110
+ {
111
+ "id": "CIS-2.1",
112
+ "name": "Software Updates",
113
+ "check": "no_outdated_deps",
114
+ "severity": "medium",
115
+ },
116
+ {"id": "CIS-4.1", "name": "Firewall", "check": "no_open_ports", "severity": "high"},
117
+ {
118
+ "id": "CIS-5.1",
119
+ "name": "Password Policy",
120
+ "check": "strong_passwords",
121
+ "severity": "medium",
122
+ },
123
+ ],
124
+ }
125
+
126
+
127
+ _log = logging.getLogger("patchi.security.policy_engine")
128
+
129
+
130
+ def _check_policy(check_type: str, content: str, findings: list[Finding]) -> list[str]:
131
+ """Run a policy check against file content. Returns list of violation messages."""
132
+ violations = []
133
+
134
+ if check_type == "no_default_credentials":
135
+ if re.search(
136
+ r'(?:admin|root|password)\s*[:=]\s*["\'](?:admin|root|password|changeme)["\']',
137
+ content,
138
+ re.I,
139
+ ):
140
+ violations.append("Default credentials found")
141
+
142
+ elif check_type == "no_plaintext_http":
143
+ if re.search(r"http://(?!localhost|127\.0\.0\.1)", content):
144
+ violations.append("Plaintext HTTP URL found")
145
+
146
+ elif check_type == "has_logging":
147
+ if not re.search(r"(?:log|logging|logger|audit)", content, re.I):
148
+ violations.append("No logging implementation found")
149
+
150
+ elif check_type == "has_audit_trail":
151
+ if not re.search(r"(?:audit|log|track|record)", content, re.I):
152
+ violations.append("No audit trail found")
153
+
154
+ elif check_type == "no_weak_crypto":
155
+ if re.search(r"\b(?:MD5|SHA1|DES|RC4)\b", content, re.I):
156
+ violations.append("Weak cryptographic algorithm detected")
157
+
158
+ elif check_type == "no_hardcoded_secrets":
159
+ if re.search(
160
+ r'(?:api[_-]?key|secret|password)\s*[:=]\s*["\'][^"\']{8,}["\']', content, re.I
161
+ ):
162
+ violations.append("Hardcoded secret detected")
163
+
164
+ elif check_type == "no_sql_injection":
165
+ if re.search(r"(?:execute|query)\s*\([^)]*\+", content):
166
+ violations.append("Potential SQL injection — string concatenation in query")
167
+
168
+ elif check_type == "no_world_readable":
169
+ if re.search(r"chmod\s+777|chmod\s+\+r", content):
170
+ violations.append("World-readable file permissions")
171
+
172
+ elif check_type == "no_outdated_deps":
173
+ pass # Handled by dependency scanner
174
+
175
+ elif check_type == "no_open_ports":
176
+ if re.search(r"EXPOSE\s+(?:22|3389)\b", content, re.I):
177
+ violations.append("Sensitive port exposed")
178
+
179
+ elif check_type == "strong_passwords":
180
+ if re.search(r'(?:password|passwd)\s*[:=]\s*["\'][^"\']{1,7}["\']', content, re.I):
181
+ violations.append("Weak password (less than 8 characters)")
182
+
183
+ return violations
184
+
185
+
186
+ @register
187
+ class PolicyEngineAgent(BaseAgent):
188
+ """Enforces YAML/JSON security policies against code."""
189
+
190
+ name = "PolicyEngineAgent"
191
+ group = AgentGroup.SECURITY
192
+ timeout = 60
193
+
194
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
195
+ # Load policies from .patchi/policies/ or use built-in packs
196
+ policy_packs = self._load_policies(inp)
197
+
198
+ # Flatten all rules once — avoids re-iterating packs per file
199
+ all_rules: list[tuple[str, dict]] = [] # (pack_name, rule)
200
+ for pack_name, rules in policy_packs.items():
201
+ for rule in rules:
202
+ all_rules.append((pack_name, rule))
203
+
204
+ if not all_rules:
205
+ return
206
+
207
+ # Scan all source files — read each file ONCE, apply all rules (O(files × rules))
208
+ source_patterns = [
209
+ "*.py",
210
+ "*.js",
211
+ "*.ts",
212
+ "*.jsx",
213
+ "*.tsx",
214
+ "*.go",
215
+ "*.java",
216
+ "*.rb",
217
+ "*.php",
218
+ ]
219
+ scanned = 0
220
+
221
+ for pattern in source_patterns:
222
+ for fpath in safe_rglob(inp.root, pattern):
223
+ if not fpath.is_file():
224
+ continue
225
+ rel = fpath.relative_to(inp.root).as_posix()
226
+ try:
227
+ content = fpath.read_text(encoding="utf-8", errors="ignore")
228
+ except OSError:
229
+ continue
230
+ scanned += 1
231
+
232
+ # Apply all rules in one pass over this file's content
233
+ for pack_name, rule in all_rules:
234
+ violations = _check_policy(rule["check"], content, [])
235
+ for v in violations:
236
+ result.add_finding(
237
+ Finding(
238
+ agent=self.name,
239
+ type="policy_violation",
240
+ severity=Severity(rule["severity"]),
241
+ file=rel,
242
+ message=f"[{rule['id']}] {rule['name']}: {v}",
243
+ cwe=rule["id"],
244
+ extra={"policy_pack": pack_name},
245
+ )
246
+ )
247
+
248
+ result.files_scanned = scanned
249
+
250
+ def _load_policies(self, inp: AgentInput) -> dict[str, list[dict]]:
251
+ """Load policies from .patchi/policies/ directory, falling back to built-in."""
252
+ policies_dir = inp.root / ".patchi" / "policies"
253
+ custom_policies: dict[str, list[dict]] = {}
254
+
255
+ if policies_dir.is_dir():
256
+ for pf in policies_dir.glob("*.yaml"):
257
+ try:
258
+ data = yaml.safe_load(pf.read_text(encoding="utf-8"))
259
+ if isinstance(data, dict) and "rules" in data:
260
+ custom_policies[pf.stem] = data["rules"]
261
+ except Exception as e:
262
+ _log.warning("PolicyEngineAgent._load_policies failed: %s", e)
263
+ for pf in policies_dir.glob("*.json"):
264
+ try:
265
+ data = json.loads(pf.read_text(encoding="utf-8"))
266
+ if isinstance(data, dict) and "rules" in data:
267
+ custom_policies[pf.stem] = data["rules"]
268
+ except Exception as e:
269
+ _log.warning("PolicyEngineAgent._load_policies failed: %s", e)
270
+
271
+ # Use custom if found, otherwise built-in
272
+ return custom_policies if custom_policies else _BUILTIN_POLICIES
@@ -0,0 +1,172 @@
1
+ """
2
+ Cheap Pre-Checks (Layer 0) — instant gate before expensive scans.
3
+
4
+ Pure deterministic checks that run in <1 second:
5
+ - patchi_pattern_count: count pattern occurrences (banned APIs, refactor verification)
6
+ - patchi_lint_check: wrap ruff/eslint for fast style/bug detection
7
+ - patchi_diff_stat: before/after line counts on a change
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ import subprocess
14
+ from pathlib import Path
15
+
16
+ from patchi.core.agents.base import (
17
+ AgentGroup,
18
+ AgentInput,
19
+ AgentResult,
20
+ BaseAgent,
21
+ Finding,
22
+ Severity,
23
+ register,
24
+ safe_rglob,
25
+ )
26
+
27
+ # ── Banned API patterns (zero tolerance) ──────────────────────────────────────
28
+
29
+ _BANNED_APIS = [
30
+ (re.compile(r"\beval\s*\("), "eval() — code injection risk"),
31
+ (re.compile(r"\bexec\s*\("), "exec() — code injection risk"),
32
+ (re.compile(r"\bpickle\.loads?\s*\("), "pickle deserialization — RCE risk"),
33
+ (re.compile(r"\bmarshal\.loads?\s*\("), "marshal deserialization — RCE risk"),
34
+ (re.compile(r"\b__import__\s*\("), "dynamic import — verify necessity"),
35
+ ]
36
+
37
+
38
+ def patchi_pattern_count(root: Path, pattern: str, path_filter: str = "*.py") -> int:
39
+ """Count occurrences of a regex pattern across files. Returns total count."""
40
+ count = 0
41
+ for fpath in safe_rglob(root, path_filter):
42
+ if not fpath.is_file():
43
+ continue
44
+ try:
45
+ content = fpath.read_text(encoding="utf-8", errors="ignore")
46
+ except OSError:
47
+ continue
48
+ count += len(re.findall(pattern, content))
49
+ return count
50
+
51
+
52
+ def patchi_lint_check(root: Path, language: str = "python") -> list[dict]:
53
+ """Run ruff (Python) or eslint (JS/TS) and return parsed violations."""
54
+ violations = []
55
+
56
+ if language == "python":
57
+ try:
58
+ proc = subprocess.run(
59
+ ["ruff", "check", "--output-format=json", str(root)],
60
+ capture_output=True,
61
+ text=True,
62
+ timeout=30,
63
+ cwd=str(root),
64
+ )
65
+ import json
66
+
67
+ data = json.loads(proc.stdout) if proc.stdout.strip() else []
68
+ for item in data:
69
+ violations.append(
70
+ {
71
+ "file": item.get("filename", ""),
72
+ "line": item.get("location", {}).get("row", 0),
73
+ "code": item.get("code", ""),
74
+ "message": item.get("message", ""),
75
+ }
76
+ )
77
+ except FileNotFoundError:
78
+ import logging
79
+
80
+ logging.getLogger("patchi.prechecks").warning(
81
+ "ruff not installed — skipping Python lint check"
82
+ )
83
+ except (subprocess.TimeoutExpired, Exception):
84
+ pass
85
+
86
+ elif language in ("javascript", "typescript"):
87
+ try:
88
+ proc = subprocess.run(
89
+ ["npx", "eslint", "--format=json", str(root)],
90
+ capture_output=True,
91
+ text=True,
92
+ timeout=30,
93
+ cwd=str(root),
94
+ )
95
+ import json
96
+
97
+ data = json.loads(proc.stdout) if proc.stdout.strip() else []
98
+ for item in data:
99
+ for msg in item.get("messages", []):
100
+ violations.append(
101
+ {
102
+ "file": item.get("filePath", ""),
103
+ "line": msg.get("line", 0),
104
+ "code": msg.get("ruleId", ""),
105
+ "message": msg.get("message", ""),
106
+ }
107
+ )
108
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
109
+ pass
110
+
111
+ return violations
112
+
113
+
114
+ def patchi_diff_stat(original: str, proposed: str) -> dict:
115
+ """Compare before/after line counts. Returns add/delete/total stats."""
116
+ orig_lines = original.splitlines()
117
+ prop_lines = proposed.splitlines()
118
+ orig_set = set(orig_lines)
119
+ prop_set = set(prop_lines)
120
+ added = prop_set - orig_set
121
+ deleted = orig_set - prop_set
122
+ return {
123
+ "orig_lines": len(orig_lines),
124
+ "new_lines": len(prop_lines),
125
+ "lines_added": len(added),
126
+ "lines_deleted": len(deleted),
127
+ "net_change": len(prop_lines) - len(orig_lines),
128
+ }
129
+
130
+
131
+ # ── Pre-checks Agent ──────────────────────────────────────────────────────────
132
+
133
+
134
+ @register
135
+ class PreCheckAgent(BaseAgent):
136
+ """Layer 0: instant deterministic pre-checks before expensive scans."""
137
+
138
+ name = "PreCheckAgent"
139
+ group = AgentGroup.SECURITY
140
+ timeout = 15
141
+
142
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
143
+ # 1. Banned API checks
144
+ for pattern, msg in _BANNED_APIS:
145
+ count = patchi_pattern_count(inp.root, pattern.pattern)
146
+ if count > 0:
147
+ result.add_finding(
148
+ Finding(
149
+ agent=self.name,
150
+ type="banned_api",
151
+ severity=Severity.CRITICAL,
152
+ file="",
153
+ message=f"{msg} — {count} occurrence(s) found",
154
+ cwe="CWE-94",
155
+ )
156
+ )
157
+
158
+ # 2. Quick lint check
159
+ violations = patchi_lint_check(inp.root, "python")
160
+ for v in violations[:20]:
161
+ result.add_finding(
162
+ Finding(
163
+ agent=self.name,
164
+ type="lint_violation",
165
+ severity=Severity.LOW,
166
+ file=v["file"],
167
+ line=v["line"],
168
+ message=f"[{v['code']}] {v['message']}",
169
+ )
170
+ )
171
+
172
+ result.files_scanned = len(list(safe_rglob(inp.root, "*.py")))
@@ -0,0 +1,247 @@
1
+ """
2
+ Pre-Commit Hook Generator - produces a POSIX sh pre-commit hook.
3
+
4
+ The generated hook runs:
5
+ 1. ruff check (fast lint gate)
6
+ 2. p scan --changed --quiet (on-demand security scan of changed files)
7
+
8
+ Both steps are non-blocking warnings by default; pass --strict to fail
9
+ the commit on violations.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from pathlib import Path
16
+
17
+ _log = logging.getLogger("patchi.security.precommit_hook")
18
+
19
+ # -- Hook Script Template -----------------------------------------------------
20
+
21
+
22
+ def generate_hook(
23
+ strict: bool = False,
24
+ with_tests: bool = False,
25
+ with_scan: bool = True,
26
+ auto_fix: bool = False,
27
+ ) -> str:
28
+ """Generate a pre-commit hook script.
29
+
30
+ Args:
31
+ strict: If True, failures block the commit (exit 1).
32
+ with_tests: If True, include pytest gate after ruff.
33
+ with_scan: If True, include p scan --changed gate.
34
+ auto_fix: If True, run proactive auto-fix before lint.
35
+
36
+ Returns:
37
+ POSIX sh script text.
38
+ """
39
+ lines = [
40
+ "#!/bin/sh",
41
+ "# Patchi pre-commit hook - generated by `p dev hook`",
42
+ "#",
43
+ "# Gates:",
44
+ ]
45
+
46
+ step_num = 1
47
+ if auto_fix:
48
+ lines.append(f"# {step_num}. proactive auto-fix (charter)")
49
+ step_num += 1
50
+ lines.append(f"# {step_num}. ruff check (lint)")
51
+ if with_tests:
52
+ step_num += 1
53
+ lines.append(f"# {step_num}. pytest (tests)")
54
+ if with_scan:
55
+ step_num += 1
56
+ lines.append(f"# {step_num}. p scan --changed (security)")
57
+
58
+ # NOTE: no `set -e` - each step checks $? manually so warnings
59
+ # are non-blocking when STRICT=0.
60
+ lines.append("STRICT=" + ("1" if strict else "0"))
61
+ lines.append("FAIL=0")
62
+ lines.append("AUTO_FIX=" + ("1" if auto_fix else "0"))
63
+ lines.append("")
64
+ lines.append('echo "=== Patchi pre-commit hook ==="')
65
+ lines.append("")
66
+
67
+ total = step_num
68
+ current_step = 0
69
+
70
+ # Step: Auto-fix (optional)
71
+ if auto_fix:
72
+ current_step += 1
73
+ lines.extend(
74
+ [
75
+ f"# -- Step {current_step}/{total}: Proactive auto-fix ---------------------",
76
+ f'echo " [{current_step}/{total}] proactive auto-fix..."',
77
+ "python -c \"import patchi.core.security.auto_fix_proactive as afp; \
78
+ import sys, pathlib; \
79
+ files = [pathlib.Path(f) for f in sys.argv[1:]]; \
80
+ result = afp.proactive_fix_path(pathlib.Path.cwd(), files[0], apply=True) if files else None; \
81
+ print(' auto-fixed' if result else ' no changes needed')\" $GIT_PARAMS 2>/dev/null",
82
+ "if [ $? -ne 0 ]; then",
83
+ ' echo " [WARN] auto-fix skipped (non-blocking)"',
84
+ "else",
85
+ ' echo " [OK] auto-fix complete"',
86
+ "fi",
87
+ "",
88
+ ]
89
+ )
90
+
91
+ # Step: Ruff
92
+ current_step += 1
93
+ lines.extend(
94
+ [
95
+ f"# -- Step {current_step}/{total}: Ruff lint ----------------------------",
96
+ f'echo " [{current_step}/{total}] ruff check..."',
97
+ "ruff check patchi/ --select E,F,W --ignore E501 --quiet 2>/dev/null",
98
+ "if [ $? -ne 0 ]; then",
99
+ ' echo " [WARN] ruff found issues (non-blocking)"',
100
+ ' if [ "$STRICT" = "1" ]; then',
101
+ ' echo " [BLOCKED] Commit blocked: fix ruff errors first"',
102
+ " exit 1",
103
+ " fi",
104
+ " FAIL=1",
105
+ "else",
106
+ ' echo " [OK] ruff clean"',
107
+ "fi",
108
+ "",
109
+ ]
110
+ )
111
+
112
+ # Step 2: Tests (optional)
113
+ if with_tests:
114
+ lines.extend(
115
+ [
116
+ f"# -- Step {current_step}/{total}: Pytest -------------------------------",
117
+ f'echo " [{current_step}/{total}] pytest..."',
118
+ "PATCHI_OFFLINE=1 python -m pytest tests/ -x -q --timeout=30 --tb=no 2>/dev/null",
119
+ "if [ $? -ne 0 ]; then",
120
+ ' echo " [WARN] pytest failed (non-blocking)"',
121
+ ' if [ "$STRICT" = "1" ]; then',
122
+ ]
123
+ )
124
+ if with_scan:
125
+ lines.append(' echo " [BLOCKED] Commit blocked: fix failing tests first"')
126
+ else:
127
+ lines.append(' echo " [BLOCKED] Commit blocked: fix failing tests first"')
128
+ lines.extend(
129
+ [
130
+ " exit 1",
131
+ " fi",
132
+ " FAIL=1",
133
+ "else",
134
+ ' echo " [OK] tests pass"',
135
+ "fi",
136
+ "",
137
+ ]
138
+ )
139
+
140
+ # Step: Scan --changed (optional)
141
+ if with_scan:
142
+ current_step += 1
143
+ lines.extend(
144
+ [
145
+ f"# -- Step {current_step}/{total}: On-demand security scan ----------------",
146
+ f'echo " [{current_step}/{total}] p scan --changed..."',
147
+ "python -m patchi.cli.main scan --changed --quiet &",
148
+ "SCAN_PID=$!",
149
+ "for i in 1 2 3 4 5 6; do sleep 10; kill -0 $SCAN_PID 2>/dev/null || break; done",
150
+ "kill $SCAN_PID 2>/dev/null && echo ' (scan timed out)' || true",
151
+ "",
152
+ ]
153
+ )
154
+
155
+ # Step: Fast SAST gate on staged files (Bandit + Semgrep, synchronous)
156
+ if with_scan:
157
+ current_step += 1
158
+ lines.extend(
159
+ [
160
+ f"# -- Step {current_step}/{total}: Fast SAST gate (Bandit+Semgrep) ------",
161
+ f'echo " [{current_step}/{total}] fast SAST gate on staged files..."',
162
+ 'STAGED=$(git diff --cached --name-only | grep -E "\\.py$" || true)',
163
+ 'if [ -n "$STAGED" ]; then',
164
+ " python -m patchi.core.security.sast_gate $STAGED",
165
+ " SAST_RC=$?",
166
+ ' if [ "$SAST_RC" -ne 0 ]; then',
167
+ ' echo " [WARN] fast SAST gate found issues (non-blocking)"',
168
+ ' if [ "$STRICT" = "1" ]; then',
169
+ ' echo " [BLOCKED] Commit blocked: resolve SAST findings first"',
170
+ " exit 1",
171
+ " fi",
172
+ " FAIL=1",
173
+ " else",
174
+ ' echo " [OK] fast SAST gate clean"',
175
+ " fi",
176
+ "else",
177
+ ' echo " [OK] no staged Python files"',
178
+ "fi",
179
+ "",
180
+ ]
181
+ )
182
+
183
+ # Summary
184
+ lines.extend(
185
+ [
186
+ "# -- Summary --------------------------------------------------------",
187
+ 'if [ "$FAIL" = "1" ]; then',
188
+ ' echo "[WARN] Commit succeeded with warnings. Run "p dev check" for full gate."',
189
+ "else",
190
+ ' echo "[OK] All gates passed."',
191
+ "fi",
192
+ "",
193
+ "exit 0",
194
+ ]
195
+ )
196
+
197
+ return "\n".join(lines) + "\n"
198
+
199
+
200
+ def install_hook(
201
+ root: Path,
202
+ strict: bool = False,
203
+ with_tests: bool = False,
204
+ with_scan: bool = True,
205
+ auto_fix: bool = False,
206
+ ) -> Path:
207
+ """Generate and install the pre-commit hook.
208
+
209
+ Returns the path to the installed hook.
210
+ """
211
+ git_dir = root / ".git"
212
+ if not git_dir.exists():
213
+ raise FileNotFoundError(f"Not a git repository: {root}")
214
+
215
+ hook_path = git_dir / "hooks" / "pre-commit"
216
+ hook_path.parent.mkdir(parents=True, exist_ok=True)
217
+
218
+ script = generate_hook(
219
+ strict=strict, with_tests=with_tests, with_scan=with_scan, auto_fix=auto_fix
220
+ )
221
+ hook_path.write_text(script, encoding="utf-8")
222
+
223
+ try:
224
+ hook_path.chmod(0o755)
225
+ except OSError:
226
+ pass # Windows
227
+
228
+ _log.info("Installed pre-commit hook at %s", hook_path)
229
+ return hook_path
230
+
231
+
232
+ def read_hook_status(root: Path) -> dict:
233
+ """Check what the installed pre-commit hook does."""
234
+ hook_path = root / ".git" / "hooks" / "pre-commit"
235
+ if not hook_path.is_file():
236
+ return {"installed": False}
237
+
238
+ content = hook_path.read_text(encoding="utf-8", errors="replace")
239
+ return {
240
+ "installed": True,
241
+ "path": str(hook_path),
242
+ "has_ruff": "ruff check" in content,
243
+ "has_pytest": "pytest" in content,
244
+ "has_scan": "scan --changed" in content or "scan --quiet" in content,
245
+ "strict": "STRICT=1" in content,
246
+ "auto_fix": "AUTO_FIX=1" in content,
247
+ }