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,367 @@
1
+ """
2
+ Git Diff Activator — on-demand security domain activation from file changes.
3
+
4
+ When ``p scan --changed`` is used, this module:
5
+ 1. Reads changed files from ``git diff`` (or ``git log``)
6
+ 2. Maps each changed file to relevant security domains via path patterns,
7
+ file extensions, and content heuristics
8
+ 3. Returns a scored list of domains to activate
9
+
10
+ This lets the scan run only the agents relevant to what actually changed,
11
+ cutting scan time from minutes to seconds for small diffs.
12
+
13
+ Usage::
14
+
15
+ from patchi.core.security.git_diff_activator import activate_from_diff
16
+
17
+ domains = activate_from_diff(root, commits=1)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ import subprocess
25
+ import time
26
+ from dataclasses import dataclass, field
27
+ from pathlib import Path
28
+
29
+ _log = logging.getLogger("patchi.security.git_diff_activator")
30
+
31
+ _CACHE_FILE = ".patchi/diff_activation_cache.json"
32
+ _CACHE_TTL = 300 # 5 minutes max age even if HEAD unchanged
33
+
34
+
35
+ # ── Path → Domain Mapping ────────────────────────────────────────────────────
36
+
37
+ # Patterns: (glob-like substring match, domain, weight)
38
+ _PATH_DOMAIN_MAP: list[tuple[str, str, float]] = [
39
+ # Auth / session
40
+ ("auth", "authentication", 0.8),
41
+ ("login", "authentication", 0.8),
42
+ ("session", "session-management", 0.7),
43
+ ("jwt", "jwt-security", 0.9),
44
+ ("token", "jwt-security", 0.6),
45
+ ("oauth", "authentication", 0.8),
46
+ ("permission", "authorization", 0.8),
47
+ ("role", "authorization", 0.7),
48
+ ("admin", "authorization", 0.7),
49
+ ("guard", "authorization", 0.6),
50
+ ("middleware", "authentication", 0.5),
51
+ # Injection / XSS
52
+ ("query", "injection-sql", 0.6),
53
+ ("sql", "injection-sql", 0.8),
54
+ ("database", "injection-sql", 0.5),
55
+ ("search", "xss-reflected", 0.4),
56
+ ("template", "xss-stored", 0.4),
57
+ ("render", "xss-dom", 0.4),
58
+ ("markup", "xss-stored", 0.5),
59
+ ("form", "csrf", 0.5),
60
+ # Secrets / crypto
61
+ ("secret", "secrets-management", 0.9),
62
+ ("credential", "secrets-management", 0.9),
63
+ ("key", "secrets-management", 0.6),
64
+ ("env", "secrets-management", 0.5),
65
+ ("encrypt", "cryptography", 0.8),
66
+ ("decrypt", "cryptography", 0.8),
67
+ ("hash", "cryptography", 0.7),
68
+ ("ssl", "tls-ssl", 0.8),
69
+ ("tls", "tls-ssl", 0.8),
70
+ ("cert", "tls-ssl", 0.7),
71
+ # Network / API
72
+ ("api", "api-security", 0.5),
73
+ ("route", "api-security", 0.4),
74
+ ("endpoint", "api-security", 0.5),
75
+ ("fetch", "ssrf", 0.4),
76
+ ("request", "ssrf", 0.4),
77
+ ("http", "ssrf", 0.3),
78
+ ("cors", "cors", 0.9),
79
+ ("header", "security-headers", 0.7),
80
+ ("rate", "rate-limiting", 0.6),
81
+ ("limit", "rate-limiting", 0.5),
82
+ # Config / infra
83
+ ("config", "configuration-hardening", 0.5),
84
+ ("setting", "configuration-hardening", 0.4),
85
+ ("docker", "container-security", 0.8),
86
+ ("kubernetes", "kubernetes-security", 0.9),
87
+ ("k8s", "kubernetes-security", 0.9),
88
+ ("helm", "kubernetes-security", 0.7),
89
+ ("terraform", "cloud-security", 0.8),
90
+ ("cloud", "cloud-security", 0.6),
91
+ ("infra", "container-security", 0.5),
92
+ ("iac", "container-security", 0.7),
93
+ # Dependencies
94
+ ("requirements", "dependency-vulnerability", 0.7),
95
+ ("package.json", "dependency-vulnerability", 0.7),
96
+ ("Cargo.toml", "supply-chain", 0.7),
97
+ ("go.mod", "supply-chain", 0.7),
98
+ ("lock", "supply-chain", 0.5),
99
+ # Business logic / privacy
100
+ ("payment", "business-logic", 0.8),
101
+ ("order", "business-logic", 0.7),
102
+ ("cart", "business-logic", 0.6),
103
+ ("privacy", "privacy-gdpr", 0.9),
104
+ ("gdpr", "privacy-gdpr", 0.9),
105
+ ("pii", "privacy-gdpr", 0.8),
106
+ ("data", "data-layer", 0.4),
107
+ ("audit", "audit-logging", 0.7),
108
+ ("log", "audit-logging", 0.4),
109
+ ("history", "audit-logging", 0.5),
110
+ # File / upload
111
+ ("upload", "file-handling", 0.8),
112
+ ("file", "file-handling", 0.4),
113
+ ("download", "file-handling", 0.6),
114
+ ("path", "path-traversal", 0.5),
115
+ ("static", "file-handling", 0.3),
116
+ ]
117
+
118
+ # Extension → domain boost
119
+ _EXT_DOMAIN_MAP: list[tuple[str, str, float]] = [
120
+ (".yaml", "configuration-hardening", 0.4),
121
+ (".yml", "configuration-hardening", 0.4),
122
+ (".toml", "supply-chain", 0.4),
123
+ (".env", "secrets-management", 0.8),
124
+ (".tf", "cloud-security", 0.9),
125
+ (".hcl", "cloud-security", 0.7),
126
+ (".sql", "injection-sql", 0.8),
127
+ (".sh", "audit-logging", 0.3),
128
+ (".dockerfile", "container-security", 0.8),
129
+ (".go", "supply-chain", 0.3),
130
+ (".rs", "supply-chain", 0.3),
131
+ (".java", "supply-chain", 0.3),
132
+ (".rb", "supply-chain", 0.3),
133
+ ]
134
+
135
+ # Security-critical file names that always activate
136
+ _ALWAYS_ACTIVE: list[tuple[str, str, float]] = [
137
+ ("Dockerfile", "container-security", 0.9),
138
+ ("docker-compose", "container-security", 0.9),
139
+ ("Makefile", "audit-logging", 0.3),
140
+ (".github", "cicd-pipeline", 0.7),
141
+ ("ci.yml", "cicd-pipeline", 0.8),
142
+ ("cd.yml", "cicd-pipeline", 0.8),
143
+ ]
144
+
145
+
146
+ @dataclass
147
+ class DiffActivationResult:
148
+ """Result of git-diff-based domain activation."""
149
+
150
+ changed_files: list[str] = field(default_factory=list)
151
+ activated_domains: dict[str, float] = field(default_factory=dict)
152
+ commits_analyzed: int = 0
153
+ error: str = ""
154
+
155
+
156
+ def _get_head_hash(root: Path) -> str:
157
+ """Get the current HEAD commit hash."""
158
+ try:
159
+ result = subprocess.run(
160
+ ["git", "rev-parse", "HEAD"],
161
+ capture_output=True,
162
+ text=True,
163
+ cwd=str(root),
164
+ timeout=5,
165
+ )
166
+ return result.stdout.strip() if result.returncode == 0 else ""
167
+ except Exception:
168
+ return ""
169
+
170
+
171
+ def _load_cache(root: Path) -> dict:
172
+ path = root / _CACHE_FILE
173
+ if not path.is_file():
174
+ return {}
175
+ try:
176
+ return json.loads(path.read_text(encoding="utf-8"))
177
+ except Exception:
178
+ return {}
179
+
180
+
181
+ def _save_cache(root: Path, data: dict) -> None:
182
+ path = root / _CACHE_FILE
183
+ path.parent.mkdir(parents=True, exist_ok=True)
184
+ tmp = path.with_suffix(".json.tmp")
185
+ tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
186
+ tmp.replace(path)
187
+
188
+
189
+ def get_changed_files(root: Path, commits: int = 1) -> list[str]:
190
+ """Get files changed in the last N commits via git diff.
191
+
192
+ Falls back to uncommitted changes if HEAD has no prior commits.
193
+ """
194
+ try:
195
+ # Try diff against N commits back
196
+ result = subprocess.run(
197
+ ["git", "diff", "--name-only", f"HEAD~{commits}", "HEAD"],
198
+ capture_output=True,
199
+ text=True,
200
+ cwd=str(root),
201
+ timeout=10,
202
+ )
203
+ if result.returncode == 0 and result.stdout.strip():
204
+ return [f.strip() for f in result.stdout.strip().splitlines() if f.strip()]
205
+
206
+ # Fallback: uncommitted changes
207
+ result = subprocess.run(
208
+ ["git", "diff", "--name-only", "HEAD"],
209
+ capture_output=True,
210
+ text=True,
211
+ cwd=str(root),
212
+ timeout=10,
213
+ )
214
+ if result.returncode == 0 and result.stdout.strip():
215
+ return [f.strip() for f in result.stdout.strip().splitlines() if f.strip()]
216
+
217
+ # Fallback: staged changes
218
+ result = subprocess.run(
219
+ ["git", "diff", "--cached", "--name-only"],
220
+ capture_output=True,
221
+ text=True,
222
+ cwd=str(root),
223
+ timeout=10,
224
+ )
225
+ if result.returncode == 0 and result.stdout.strip():
226
+ return [f.strip() for f in result.stdout.strip().splitlines() if f.strip()]
227
+
228
+ return []
229
+ except Exception as exc:
230
+ _log.warning("git diff failed: %s", exc)
231
+ return []
232
+
233
+
234
+ def map_file_to_domains(file_path: str) -> dict[str, float]:
235
+ """Map a single file path to domain scores."""
236
+ scores: dict[str, float] = {}
237
+ fp_lower = file_path.lower()
238
+
239
+ # Check path patterns
240
+ for pattern, domain, weight in _PATH_DOMAIN_MAP:
241
+ if pattern in fp_lower:
242
+ scores[domain] = max(scores.get(domain, 0), weight)
243
+
244
+ # Check extension
245
+ for ext, domain, weight in _EXT_DOMAIN_MAP:
246
+ if fp_lower.endswith(ext):
247
+ scores[domain] = max(scores.get(domain, 0), weight)
248
+
249
+ # Check always-active names
250
+ for name, domain, weight in _ALWAYS_ACTIVE:
251
+ if name.lower() in fp_lower:
252
+ scores[domain] = max(scores.get(domain, 0), weight)
253
+
254
+ return scores
255
+
256
+
257
+ def activate_from_diff(
258
+ root: Path,
259
+ commits: int = 1,
260
+ min_score: float = 0.4,
261
+ max_domains: int = 20,
262
+ use_cache: bool = True,
263
+ ) -> DiffActivationResult:
264
+ """Activate security domains based on git-diff changed files.
265
+
266
+ Args:
267
+ root: Project root
268
+ commits: How many commits back to diff (1 = last commit only)
269
+ min_score: Minimum domain score to activate
270
+ max_domains: Maximum domains to activate
271
+ use_cache: Whether to use persisted cache (default True)
272
+
273
+ Returns:
274
+ DiffActivationResult with activated domains and their scores
275
+ """
276
+ # ── Cache lookup ──────────────────────────────────────────────────────
277
+ cache_key = f"{commits}:{min_score}:{max_domains}"
278
+ if use_cache:
279
+ head = _get_head_hash(root)
280
+ cache = _load_cache(root)
281
+ cached = cache.get(cache_key)
282
+ if (
283
+ cached
284
+ and cached.get("head") == head
285
+ and cached.get("uncommitted_count", 0) == 0
286
+ and (time.time() - cached.get("ts", 0)) < _CACHE_TTL
287
+ ):
288
+ _log.debug("Using cached diff activation (head=%s)", head[:8])
289
+ return DiffActivationResult(
290
+ changed_files=cached["changed_files"],
291
+ activated_domains={k: float(v) for k, v in cached["domains"].items()},
292
+ commits_analyzed=commits,
293
+ )
294
+
295
+ changed = get_changed_files(root, commits)
296
+ if not changed:
297
+ return DiffActivationResult(error="No changed files found (not a git repo or no changes)")
298
+
299
+ # Aggregate domain scores across all changed files
300
+ all_scores: dict[str, float] = {}
301
+ for fp in changed:
302
+ file_scores = map_file_to_domains(fp)
303
+ for domain, score in file_scores.items():
304
+ all_scores[domain] = max(all_scores.get(domain, 0), score)
305
+
306
+ # Filter by minimum score
307
+ activated = {d: s for d, s in all_scores.items() if s >= min_score}
308
+
309
+ # Sort by score descending and limit
310
+ sorted_domains = sorted(activated.items(), key=lambda x: -x[1])
311
+ if len(sorted_domains) > max_domains:
312
+ sorted_domains = sorted_domains[:max_domains]
313
+ activated = dict(sorted_domains)
314
+
315
+ _log.info(
316
+ "Git-diff activation: %d changed files → %d domains (from %d total signals)",
317
+ len(changed),
318
+ len(activated),
319
+ len(all_scores),
320
+ )
321
+
322
+ result = DiffActivationResult(
323
+ changed_files=changed,
324
+ activated_domains=activated,
325
+ commits_analyzed=commits,
326
+ )
327
+
328
+ # ── Persist cache ─────────────────────────────────────────────────────
329
+ if use_cache:
330
+ head = _get_head_hash(root)
331
+ # Count uncommitted changes to invalidate cache on dirty tree
332
+ uncommitted = 0
333
+ try:
334
+ u = subprocess.run(
335
+ ["git", "diff", "--name-only", "HEAD"],
336
+ capture_output=True,
337
+ text=True,
338
+ cwd=str(root),
339
+ timeout=5,
340
+ )
341
+ if u.returncode == 0:
342
+ uncommitted = len([line for line in u.stdout.strip().splitlines() if line.strip()])
343
+ except Exception as _exc:
344
+ _log.warning('activate_from_diff failed: %s', _exc)
345
+ cache = _load_cache(root)
346
+ cache[cache_key] = {
347
+ "head": head,
348
+ "ts": time.time(),
349
+ "changed_files": changed,
350
+ "domains": dict(activated.items()),
351
+ "uncommitted_count": uncommitted,
352
+ }
353
+ _save_cache(root, cache)
354
+
355
+ return result
356
+
357
+
358
+ def read_changed_file_content(root: Path, file_path: str, max_lines: int = 100) -> str:
359
+ """Read the content of a changed file for deeper analysis."""
360
+ full = root / file_path
361
+ if not full.is_file():
362
+ return ""
363
+ try:
364
+ lines = full.read_text(encoding="utf-8", errors="replace").splitlines()
365
+ return "\n".join(lines[:max_lines])
366
+ except Exception:
367
+ return ""
@@ -0,0 +1,208 @@
1
+ """
2
+ Governance & Audit (Layer 3) — action logging and policy gates.
3
+
4
+ - patchi_action_log: SQLite audit trail of all Patchi actions
5
+ - patchi_policy_gate: YAML allow/deny policy check before destructive actions
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import sqlite3
12
+ import time
13
+ from pathlib import Path
14
+
15
+ from patchi.core.agents.base import (
16
+ AgentGroup,
17
+ AgentInput,
18
+ AgentResult,
19
+ BaseAgent,
20
+ Finding,
21
+ Severity,
22
+ register,
23
+ )
24
+
25
+ # ── SQLite Action Log ─────────────────────────────────────────────────────────
26
+
27
+ _DB_NAME = "patchi_actions.db"
28
+
29
+ _CREATE_TABLES = """
30
+ CREATE TABLE IF NOT EXISTS action_log (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ timestamp TEXT NOT NULL,
33
+ action TEXT NOT NULL,
34
+ target TEXT NOT NULL,
35
+ agent TEXT,
36
+ detail TEXT,
37
+ status TEXT DEFAULT 'ok'
38
+ );
39
+ """
40
+
41
+
42
+ _log = logging.getLogger("patchi.security.governance")
43
+
44
+
45
+ def _get_db(root: Path) -> sqlite3.Connection:
46
+ db_path = root / ".patchi" / _DB_NAME
47
+ db_path.parent.mkdir(parents=True, exist_ok=True)
48
+ conn = sqlite3.connect(str(db_path), timeout=10.0)
49
+ # WAL + busy_timeout make concurrent writers (parallel agents calling
50
+ # patchi_action_log) queue instead of failing with "database or disk is full".
51
+ conn.execute("PRAGMA journal_mode=WAL")
52
+ conn.execute("PRAGMA busy_timeout=10000")
53
+ conn.execute(_CREATE_TABLES)
54
+ return conn
55
+
56
+
57
+ def patchi_action_log(
58
+ root: Path, action: str, target: str, agent: str = "", detail: str = "", status: str = "ok"
59
+ ) -> None:
60
+ """Log an action to the local SQLite audit trail."""
61
+ row = (
62
+ time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
63
+ action,
64
+ target,
65
+ agent,
66
+ detail,
67
+ status,
68
+ )
69
+ last_err: Exception | None = None
70
+ for attempt in range(3):
71
+ try:
72
+ conn = _get_db(root)
73
+ try:
74
+ conn.execute(
75
+ "INSERT INTO action_log (timestamp, action, target, agent, detail, status) VALUES (?, ?, ?, ?, ?, ?)",
76
+ row,
77
+ )
78
+ conn.commit()
79
+ finally:
80
+ conn.close()
81
+ return
82
+ except sqlite3.DatabaseError as e: # OperationalError is a subclass
83
+ last_err = e
84
+ time.sleep(0.1 * (attempt + 1))
85
+ _log.warning("patchi_action_log failed after 3 attempts: %s", last_err)
86
+
87
+
88
+ def patchi_get_actions(root: Path, limit: int = 50) -> list[dict]:
89
+ """Retrieve recent actions from the audit log."""
90
+ conn = _get_db(root)
91
+ try:
92
+ cursor = conn.execute(
93
+ "SELECT timestamp, action, target, agent, detail, status FROM action_log ORDER BY id DESC LIMIT ?",
94
+ (limit,),
95
+ )
96
+ return [
97
+ {
98
+ "timestamp": r[0],
99
+ "action": r[1],
100
+ "target": r[2],
101
+ "agent": r[3],
102
+ "detail": r[4],
103
+ "status": r[5],
104
+ }
105
+ for r in cursor.fetchall()
106
+ ]
107
+ finally:
108
+ conn.close()
109
+
110
+
111
+ # ── Policy Gate ───────────────────────────────────────────────────────────────
112
+
113
+ _DEFAULT_POLICY = {
114
+ "never_touch": [".env", "docker-compose.yml", "*.key", "*.pem", "*.p12"],
115
+ "auto_apply_max_severity": "medium",
116
+ "require_confirmation_above": "high",
117
+ }
118
+
119
+
120
+ def _load_policy(root: Path) -> dict:
121
+ """Load policy from .patchi/policies/default.yaml or use defaults."""
122
+ policy_file = root / ".patchi" / "policies" / "default.yaml"
123
+ if policy_file.exists():
124
+ try:
125
+ import yaml
126
+
127
+ data = yaml.safe_load(policy_file.read_text(encoding="utf-8"))
128
+ if isinstance(data, dict):
129
+ return {**_DEFAULT_POLICY, **data}
130
+ except Exception as e:
131
+ _log.warning("_load_policy failed: %s", e)
132
+ return _DEFAULT_POLICY.copy()
133
+
134
+
135
+ def patchi_policy_gate(
136
+ root: Path, action: str, target: str, severity: str = "medium"
137
+ ) -> tuple[bool, str]:
138
+ """
139
+ Check if an action is allowed by the policy gate.
140
+ Returns (allowed, reason).
141
+ """
142
+ policy = _load_policy(root)
143
+
144
+ # Check never_touch patterns
145
+ from fnmatch import fnmatch
146
+
147
+ for pattern in policy.get("never_touch", []):
148
+ if fnmatch(target, pattern) or fnmatch(target.split("/")[-1], pattern):
149
+ return (
150
+ False,
151
+ f"Policy blocks action on '{target}' (matches never_touch pattern: {pattern})",
152
+ )
153
+
154
+ # Check severity threshold
155
+ max_auto = policy.get("auto_apply_max_severity", "medium")
156
+ sev_order = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
157
+ if sev_order.get(severity, 0) > sev_order.get(max_auto, 2):
158
+ return (
159
+ False,
160
+ f"Severity '{severity}' exceeds auto-apply threshold '{max_auto}' — requires confirmation",
161
+ )
162
+
163
+ return True, "Allowed by policy"
164
+
165
+
166
+ # ── Governance Agent ──────────────────────────────────────────────────────────
167
+
168
+
169
+ @register
170
+ class GovernanceAgent(BaseAgent):
171
+ """Action logging and policy gate enforcement."""
172
+
173
+ name = "GovernanceAgent"
174
+ group = AgentGroup.GUARD
175
+ timeout = 30
176
+
177
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
178
+ # Log that a governance check was performed
179
+ patchi_action_log(inp.root, "governance_check", "security_scan", agent=self.name)
180
+
181
+ # Check policy violations — use pattern-targeted glob instead of O(all files) walk (ARCH-06 fix)
182
+ policy = _load_policy(inp.root)
183
+
184
+ never_touch_patterns = policy.get("never_touch", [])
185
+ if not never_touch_patterns:
186
+ result.files_scanned = 0
187
+ return
188
+
189
+ # Only walk files that could match the patterns — much faster than safe_rglob("*")
190
+ scanned = 0
191
+ for pattern in never_touch_patterns:
192
+ # Use glob directly with the pattern to avoid full tree walk
193
+ for fpath in inp.root.rglob(pattern):
194
+ if not fpath.is_file():
195
+ continue
196
+ rel = fpath.relative_to(inp.root).as_posix()
197
+ scanned += 1
198
+ result.add_finding(
199
+ Finding(
200
+ agent=self.name,
201
+ type="policy_violation",
202
+ severity=Severity.HIGH,
203
+ file=rel,
204
+ message=f"File matches never_touch policy pattern: {pattern}",
205
+ )
206
+ )
207
+
208
+ result.files_scanned = scanned
@@ -0,0 +1,34 @@
1
+ """
2
+ Gradual Enforcement §10.2.2 — ratcheting baseline down week by week.
3
+
4
+ Stores baseline file count, fails if count exceeds target ratchet.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+
12
+ _GATE_FILE = ".patchi/gradual_gate.json"
13
+
14
+ def ratchet_check(root: Path, current_count: int, weekly_reduction: int = 5) -> tuple[bool, str]:
15
+ p=root/_GATE_FILE
16
+ data=json.loads(p.read_text(encoding="utf-8")) if p.exists() else {"baseline": current_count, "target": current_count}
17
+ target=data.get("target", current_count)
18
+ # reduce target by weekly_reduction if week passed
19
+ import time
20
+ last=data.get("ts", time.time())
21
+ if time.time()-last > 7*24*3600 and target>0:
22
+ target=max(0, target-weekly_reduction)
23
+ data["target"]=target
24
+ data["ts"]=time.time()
25
+ p.write_text(json.dumps(data, indent=2), encoding="utf-8")
26
+ if current_count > target:
27
+ return False, f"Ratchet fail: {current_count} > target {target} (baseline {data.get('baseline')}) — reduce {current_count-target} findings this week"
28
+ return True, f"Ratchet pass: {current_count} <= target {target}"
29
+
30
+ def set_baseline(root: Path, count: int) -> None:
31
+ p=root/_GATE_FILE
32
+ p.parent.mkdir(parents=True, exist_ok=True)
33
+ import time, json
34
+ p.write_text(json.dumps({"baseline": count, "target": count, "ts": time.time()}, indent=2), encoding="utf-8")