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,169 @@
1
+ """
2
+ IP Reputation database for hosted mode.
3
+
4
+ Maintains a local database of known-bad IPs from public blocklists.
5
+ Supports:
6
+ - Loading public blocklists (abuse.ch, firehol, etc.)
7
+ - Fast O(1) lookup via set
8
+ - Periodic refresh (daily)
9
+ - Auto-blocking for high-threat IPs
10
+
11
+ All data stored locally in .patchi/hosted/ip_reputation.json.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+ import time
18
+ from pathlib import Path
19
+
20
+ _REPUTATION_FILE = ".patchi/hosted/ip_reputation.json"
21
+ _BLOCKLIST_URLS = [
22
+ "https://raw.githubusercontent.com/stamparm/ipsum/master/ipsum.txt",
23
+ "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset",
24
+ ]
25
+
26
+ # IPs from these lists are automatically blocked
27
+ _AUTO_BLOCK_THRESHOLD = 3 # IPs seen on 3+ lists
28
+
29
+
30
+ _log = logging.getLogger("patchi.core.ip_reputation")
31
+
32
+
33
+ def _load(root: Path) -> dict:
34
+ path = root / _REPUTATION_FILE
35
+ if not path.exists():
36
+ return {"bad_ips": {}, "last_refresh": 0, "blocked": []}
37
+ try:
38
+ return json.loads(path.read_text(encoding="utf-8"))
39
+ except Exception as e:
40
+ _log.warning("_load failed: %s", e)
41
+ return {"bad_ips": {}, "last_refresh": 0, "blocked": []}
42
+
43
+
44
+ def _save(root: Path, data: dict) -> None:
45
+ path = root / _REPUTATION_FILE
46
+ path.parent.mkdir(parents=True, exist_ok=True)
47
+ tmp = path.with_suffix(".tmp")
48
+ tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
49
+ tmp.replace(path)
50
+
51
+
52
+ def lookup(ip: str, root: Path) -> dict:
53
+ """
54
+ Look up an IP's reputation.
55
+ Returns {"known_bad": bool, "threat_level": str, "lists_hit": int, "blocked": bool}
56
+ """
57
+ data = _load(root)
58
+ bad_ips = data.get("bad_ips", {})
59
+ blocked = set(data.get("blocked", []))
60
+
61
+ if ip in bad_ips:
62
+ info = bad_ips[ip]
63
+ return {
64
+ "known_bad": True,
65
+ "threat_level": _threat_level(info.get("lists_hit", 0)),
66
+ "lists_hit": info.get("lists_hit", 0),
67
+ "sources": info.get("sources", []),
68
+ "blocked": ip in blocked,
69
+ }
70
+
71
+ return {
72
+ "known_bad": False,
73
+ "threat_level": "clean",
74
+ "lists_hit": 0,
75
+ "sources": [],
76
+ "blocked": ip in blocked,
77
+ }
78
+
79
+
80
+ def record_hit(ip: str, source: str, root: Path) -> None:
81
+ """Record that an IP was flagged by a detector."""
82
+ data = _load(root)
83
+ bad_ips = data.setdefault("bad_ips", {})
84
+
85
+ if ip not in bad_ips:
86
+ bad_ips[ip] = {"lists_hit": 0, "sources": [], "first_seen": time.time()}
87
+
88
+ entry = bad_ips[ip]
89
+ if source not in entry["sources"]:
90
+ entry["sources"].append(source)
91
+ entry["lists_hit"] = len(entry["sources"])
92
+ entry["last_seen"] = time.time()
93
+
94
+ # Auto-block if threshold crossed
95
+ if entry["lists_hit"] >= _AUTO_BLOCK_THRESHOLD:
96
+ blocked = data.setdefault("blocked", [])
97
+ if ip not in blocked:
98
+ blocked.append(ip)
99
+
100
+ _save(root, data)
101
+
102
+
103
+ def is_blocked(ip: str, root: Path) -> bool:
104
+ """Check if an IP is auto-blocked."""
105
+ data = _load(root)
106
+ return ip in data.get("blocked", [])
107
+
108
+
109
+ def block(ip: str, root: Path) -> None:
110
+ """Manually block an IP."""
111
+ data = _load(root)
112
+ blocked = data.setdefault("blocked", [])
113
+ if ip not in blocked:
114
+ blocked.append(ip)
115
+ _save(root, data)
116
+
117
+
118
+ def unblock(ip: str, root: Path) -> bool:
119
+ """Unblock an IP. Returns True if it was blocked."""
120
+ data = _load(root)
121
+ blocked = data.get("blocked", [])
122
+ if ip in blocked:
123
+ blocked.remove(ip)
124
+ data["blocked"] = blocked
125
+ _save(root, data)
126
+ return True
127
+ return False
128
+
129
+
130
+ def needs_refresh(root: Path, max_age_hours: int = 24) -> bool:
131
+ """Check if the reputation database needs refreshing."""
132
+ data = _load(root)
133
+ last_refresh = data.get("last_refresh", 0)
134
+ age_hours = (time.time() - last_refresh) / 3600
135
+ return age_hours > max_age_hours
136
+
137
+
138
+ def get_top_threats(root: Path, limit: int = 20) -> list[dict]:
139
+ """Get the top threatening IPs sorted by lists_hit."""
140
+ data = _load(root)
141
+ bad_ips = data.get("bad_ips", {})
142
+
143
+ sorted_ips = sorted(
144
+ bad_ips.items(),
145
+ key=lambda x: x[1].get("lists_hit", 0),
146
+ reverse=True,
147
+ )[:limit]
148
+
149
+ return [
150
+ {
151
+ "ip": ip,
152
+ "lists_hit": info.get("lists_hit", 0),
153
+ "sources": info.get("sources", []),
154
+ "threat_level": _threat_level(info.get("lists_hit", 0)),
155
+ "first_seen": info.get("first_seen", 0),
156
+ "last_seen": info.get("last_seen", 0),
157
+ }
158
+ for ip, info in sorted_ips
159
+ ]
160
+
161
+
162
+ def _threat_level(lists_hit: int) -> str:
163
+ if lists_hit >= 5:
164
+ return "critical"
165
+ if lists_hit >= 3:
166
+ return "high"
167
+ if lists_hit >= 1:
168
+ return "medium"
169
+ return "clean"
@@ -0,0 +1,251 @@
1
+ """
2
+ Log parsers for Patchi hosted mode.
3
+
4
+ Parses 7 log formats into a unified LogEntry structure.
5
+ Uses loguru for internal structured logging of parse errors.
6
+
7
+ Supported formats:
8
+ nginx, apache, caddy, uvicorn, gunicorn, cloudflare (JSON), generic JSON
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ import time
16
+ from dataclasses import dataclass, field
17
+
18
+ from loguru import logger
19
+
20
+
21
+ @dataclass
22
+ class LogEntry:
23
+ """Unified log entry across all formats."""
24
+
25
+ timestamp: float
26
+ level: str # INFO / WARNING / ERROR / CRITICAL
27
+ method: str # HTTP method or ""
28
+ path: str # URL path or ""
29
+ status: int # HTTP status or 0
30
+ ip: str # client IP or ""
31
+ message: str # raw log line or parsed message
32
+ source: str # parser name
33
+ extra: dict = field(default_factory=dict)
34
+
35
+
36
+ # ── Shared patterns ────────────────────────────────────────────────────────────
37
+
38
+ _NGINX_RE = re.compile(
39
+ r"(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "
40
+ r'"(?P<method>\w+) (?P<path>\S+) [^"]*" (?P<status>\d+) (?P<bytes>\d+)'
41
+ )
42
+
43
+ _APACHE_RE = re.compile(
44
+ r"(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "
45
+ r'"(?P<method>\w+) (?P<path>\S+) [^"]*" (?P<status>\d+)'
46
+ )
47
+
48
+ _CADDY_RE = re.compile(
49
+ r"(?P<ts>\d{4}-\d{2}-\d{2}T\S+)\s+(?P<level>\w+)\s+"
50
+ r'http\.log\.access.*?"method":"(?P<method>\w+)".*?"uri":"(?P<path>[^"]+)"'
51
+ r'.*?"status":(?P<status>\d+).*?"remote_ip":"(?P<ip>[^"]+)"'
52
+ )
53
+
54
+ _UVICORN_RE = re.compile(
55
+ r'(?P<ip>\S+):(?:\d+) - "(?P<method>\w+) (?P<path>\S+) [^"]*" (?P<status>\d+)'
56
+ )
57
+
58
+ _GUNICORN_RE = re.compile(
59
+ r"(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "
60
+ r'"(?P<method>\w+) (?P<path>\S+) [^"]*" (?P<status>\d+)'
61
+ )
62
+
63
+
64
+ def _status_to_level(status: int) -> str:
65
+ if status >= 500:
66
+ return "ERROR"
67
+ if status >= 400:
68
+ return "WARNING"
69
+ return "INFO"
70
+
71
+
72
+ def _now() -> float:
73
+ return time.time()
74
+
75
+
76
+ # ── Individual parsers ─────────────────────────────────────────────────────────
77
+
78
+
79
+ def parse_nginx(line: str) -> LogEntry | None:
80
+ m = _NGINX_RE.match(line)
81
+ if not m:
82
+ return None
83
+ status = int(m.group("status"))
84
+ return LogEntry(
85
+ timestamp=_now(),
86
+ level=_status_to_level(status),
87
+ method=m.group("method"),
88
+ path=m.group("path"),
89
+ status=status,
90
+ ip=m.group("ip"),
91
+ message=line,
92
+ source="nginx",
93
+ extra={"bytes": m.group("bytes"), "raw_time": m.group("time")},
94
+ )
95
+
96
+
97
+ def parse_apache(line: str) -> LogEntry | None:
98
+ m = _APACHE_RE.match(line)
99
+ if not m:
100
+ return None
101
+ status = int(m.group("status"))
102
+ return LogEntry(
103
+ timestamp=_now(),
104
+ level=_status_to_level(status),
105
+ method=m.group("method"),
106
+ path=m.group("path"),
107
+ status=status,
108
+ ip=m.group("ip"),
109
+ message=line,
110
+ source="apache",
111
+ )
112
+
113
+
114
+ def parse_caddy(line: str) -> LogEntry | None:
115
+ m = _CADDY_RE.search(line)
116
+ if not m:
117
+ return None
118
+ status = int(m.group("status"))
119
+ return LogEntry(
120
+ timestamp=_now(),
121
+ level=_status_to_level(status),
122
+ method=m.group("method"),
123
+ path=m.group("path"),
124
+ status=status,
125
+ ip=m.group("ip"),
126
+ message=line,
127
+ source="caddy",
128
+ )
129
+
130
+
131
+ def parse_uvicorn(line: str) -> LogEntry | None:
132
+ m = _UVICORN_RE.search(line)
133
+ if not m:
134
+ return None
135
+ status = int(m.group("status"))
136
+ return LogEntry(
137
+ timestamp=_now(),
138
+ level=_status_to_level(status),
139
+ method=m.group("method"),
140
+ path=m.group("path"),
141
+ status=status,
142
+ ip=m.group("ip"),
143
+ message=line,
144
+ source="uvicorn",
145
+ )
146
+
147
+
148
+ def parse_gunicorn(line: str) -> LogEntry | None:
149
+ m = _GUNICORN_RE.match(line)
150
+ if not m:
151
+ return None
152
+ status = int(m.group("status"))
153
+ return LogEntry(
154
+ timestamp=_now(),
155
+ level=_status_to_level(status),
156
+ method=m.group("method"),
157
+ path=m.group("path"),
158
+ status=status,
159
+ ip=m.group("ip"),
160
+ message=line,
161
+ source="gunicorn",
162
+ )
163
+
164
+
165
+ def parse_cloudflare(line: str) -> LogEntry | None:
166
+ """Cloudflare logs ship as JSON objects."""
167
+ try:
168
+ d = json.loads(line)
169
+ status = int(d.get("EdgeResponseStatus", d.get("CacheResponseStatus", 0)))
170
+ return LogEntry(
171
+ timestamp=_now(),
172
+ level=_status_to_level(status),
173
+ method=d.get("ClientRequestMethod", ""),
174
+ path=d.get("ClientRequestURI", ""),
175
+ status=status,
176
+ ip=d.get("ClientIP", ""),
177
+ message=line,
178
+ source="cloudflare",
179
+ extra={
180
+ k: v
181
+ for k, v in d.items()
182
+ if k
183
+ not in (
184
+ "ClientRequestMethod",
185
+ "ClientRequestURI",
186
+ "ClientIP",
187
+ "EdgeResponseStatus",
188
+ "CacheResponseStatus",
189
+ )
190
+ },
191
+ )
192
+ except (json.JSONDecodeError, ValueError):
193
+ return None
194
+
195
+
196
+ def parse_json(line: str) -> LogEntry | None:
197
+ """Generic JSON log line — best-effort field extraction."""
198
+ try:
199
+ d = json.loads(line)
200
+ status = int(d.get("status", d.get("status_code", 0)))
201
+ return LogEntry(
202
+ timestamp=float(d.get("timestamp", d.get("ts", _now()))),
203
+ level=d.get("level", d.get("severity", _status_to_level(status))).upper(),
204
+ method=d.get("method", ""),
205
+ path=d.get("path", d.get("url", d.get("uri", ""))),
206
+ status=status,
207
+ ip=d.get("ip", d.get("remote_addr", d.get("client_ip", ""))),
208
+ message=d.get("message", d.get("msg", line)),
209
+ source="json",
210
+ extra=d,
211
+ )
212
+ except (json.JSONDecodeError, ValueError):
213
+ return None
214
+
215
+
216
+ # ── Auto-detect and parse ──────────────────────────────────────────────────────
217
+
218
+ _PARSERS = [
219
+ parse_cloudflare,
220
+ parse_nginx,
221
+ parse_apache,
222
+ parse_caddy,
223
+ parse_uvicorn,
224
+ parse_gunicorn,
225
+ parse_json, # generic JSON last — format-specific parsers take priority
226
+ ]
227
+
228
+
229
+ def parse_line(line: str) -> LogEntry | None:
230
+ """Try all parsers in order. Returns first successful result or None."""
231
+ line = line.strip()
232
+ if not line:
233
+ return None
234
+ for parser in _PARSERS:
235
+ try:
236
+ entry = parser(line)
237
+ if entry:
238
+ return entry
239
+ except Exception as e:
240
+ logger.debug(f"Parser {parser.__name__} error: {e}")
241
+ return None
242
+
243
+
244
+ def parse_lines(lines: list[str]) -> list[LogEntry]:
245
+ """Parse a batch of lines, skipping unparseable ones."""
246
+ results = []
247
+ for line in lines:
248
+ entry = parse_line(line)
249
+ if entry:
250
+ results.append(entry)
251
+ return results
@@ -0,0 +1,127 @@
1
+ """
2
+ Admin token management for Patchi hosted mode.
3
+
4
+ Tokens are generated as random 32-byte hex strings.
5
+ Only the HMAC-SHA256 hash is ever stored — plaintext is shown once on generation.
6
+
7
+ Storage: .patchi/hosted/tokens.json
8
+ Format: [{id, name, hash, created_at, last_used}]
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import hmac
15
+ import json
16
+ import os
17
+ import time
18
+ import uuid
19
+ from pathlib import Path
20
+
21
+ _TOKENS_FILE = ".patchi/hosted/tokens.json"
22
+ _SECRET_FILE = ".patchi/hosted/.secret"
23
+ _HMAC_KEY_LEN = 32
24
+
25
+
26
+ def _tokens_path(root: Path) -> Path:
27
+ return root / _TOKENS_FILE
28
+
29
+
30
+ def _secret_path(root: Path) -> Path:
31
+ return root / _SECRET_FILE
32
+
33
+
34
+ def _get_hmac_key(root: Path) -> bytes:
35
+ """Load or generate a per-installation HMAC key."""
36
+ path = _secret_path(root)
37
+ if path.exists():
38
+ try:
39
+ return bytes.fromhex(path.read_text(encoding="utf-8").strip())
40
+ except (ValueError, OSError):
41
+ pass
42
+ # Generate and persist a new key
43
+ key = os.urandom(_HMAC_KEY_LEN)
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ path.write_text(key.hex(), encoding="utf-8")
46
+ return key
47
+
48
+
49
+ def _load(root: Path) -> list[dict]:
50
+ path = _tokens_path(root)
51
+ if not path.exists():
52
+ return []
53
+ try:
54
+ return json.loads(path.read_text(encoding="utf-8"))
55
+ except (json.JSONDecodeError, OSError):
56
+ return []
57
+
58
+
59
+ def _save(root: Path, tokens: list[dict]) -> None:
60
+ path = _tokens_path(root)
61
+ path.parent.mkdir(parents=True, exist_ok=True)
62
+ path.write_text(json.dumps(tokens, indent=2), encoding="utf-8")
63
+
64
+
65
+ def _hash(plaintext: str, root: Path) -> str:
66
+ """HMAC-SHA256 hash of the token using per-installation key."""
67
+ key = _get_hmac_key(root)
68
+ return hmac.new(key, plaintext.encode(), hashlib.sha256).hexdigest()
69
+
70
+
71
+ def generate(root: Path, name: str) -> str:
72
+ """
73
+ Generate a new admin token. Stores the hash, returns the plaintext once.
74
+ The caller must display this to the user — it cannot be recovered later.
75
+ """
76
+ plaintext = os.urandom(32).hex()
77
+ token_id = str(uuid.uuid4())[:8]
78
+ tokens = _load(root)
79
+ tokens.append(
80
+ {
81
+ "id": token_id,
82
+ "name": name,
83
+ "hash": _hash(plaintext, root),
84
+ "created_at": time.time(),
85
+ "last_used": None,
86
+ }
87
+ )
88
+ _save(root, tokens)
89
+ return plaintext
90
+
91
+
92
+ def validate(root: Path, plaintext: str) -> dict | None:
93
+ """
94
+ Check if a plaintext token matches any stored hash.
95
+ Returns the token record (without hash) on match, None otherwise.
96
+ Updates last_used timestamp on success.
97
+ """
98
+ candidate_hash = _hash(plaintext, root)
99
+ tokens = _load(root)
100
+
101
+ for token in tokens:
102
+ if hmac.compare_digest(token["hash"], candidate_hash):
103
+ token["last_used"] = time.time()
104
+ _save(root, tokens)
105
+ return {k: v for k, v in token.items() if k != "hash"}
106
+
107
+ return None
108
+
109
+
110
+ def list_tokens(root: Path) -> list[dict]:
111
+ """Return all tokens without hash field."""
112
+ return [{k: v for k, v in t.items() if k != "hash"} for t in _load(root)]
113
+
114
+
115
+ def revoke(root: Path, token_id: str) -> bool:
116
+ """Remove a token by ID. Returns True if found and removed."""
117
+ tokens = _load(root)
118
+ before = len(tokens)
119
+ tokens = [t for t in tokens if t["id"] != token_id]
120
+ if len(tokens) == before:
121
+ return False
122
+ _save(root, tokens)
123
+ return True
124
+
125
+
126
+ def count(root: Path) -> int:
127
+ return len(_load(root))
@@ -0,0 +1,164 @@
1
+ """
2
+ IP watchlist for Patchi hosted mode.
3
+
4
+ Tracks per-IP threat scores over time. Scores decay with TTL.
5
+ Escalates to Notifier when an IP crosses the configured threshold.
6
+
7
+ Storage: .patchi/hosted/watchlist.json
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import time
14
+ from pathlib import Path
15
+
16
+ _WATCHLIST_FILE = ".patchi/hosted/watchlist.json"
17
+ _SCORE_TTL_SECS = 3600 # scores decay to 0 after 1 hour of silence
18
+ _ESCALATE_HIGH = 50 # score threshold → HIGH alert
19
+ _ESCALATE_CRITICAL = 100 # score threshold → CRITICAL alert
20
+
21
+ # Score increments per finding type
22
+ _SCORE_MAP: dict[str, int] = {
23
+ "brute_force": 40,
24
+ "injection_probe": 30,
25
+ "scanner_sweep": 20,
26
+ "rate_spike": 10,
27
+ "error_spike": 5,
28
+ "ml_outlier": 15,
29
+ }
30
+
31
+
32
+ def _path(root: Path) -> Path:
33
+ return root / _WATCHLIST_FILE
34
+
35
+
36
+ def _load(root: Path) -> dict[str, dict]:
37
+ p = _path(root)
38
+ if not p.exists():
39
+ return {}
40
+ try:
41
+ return json.loads(p.read_text(encoding="utf-8"))
42
+ except (json.JSONDecodeError, OSError):
43
+ return {}
44
+
45
+
46
+ def _save(root: Path, data: dict[str, dict]) -> None:
47
+ p = _path(root)
48
+ p.parent.mkdir(parents=True, exist_ok=True)
49
+ tmp = p.with_suffix(".tmp")
50
+ tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
51
+ tmp.replace(p)
52
+
53
+
54
+ def _decay(entry: dict, now: float) -> float:
55
+ """Linear decay to 0 over TTL since last_seen."""
56
+ age = now - entry.get("last_seen", now)
57
+ ratio = max(0.0, 1.0 - age / _SCORE_TTL_SECS)
58
+ return entry.get("score", 0.0) * ratio
59
+
60
+
61
+ class WatchlistTracker:
62
+ """
63
+ Tracks IP threat scores and fires escalation callbacks when thresholds cross.
64
+
65
+ escalation_fn(ip, score, severity) is called when an IP crosses a threshold.
66
+ """
67
+
68
+ def __init__(self, root: Path, escalation_fn=None) -> None:
69
+ self._root = root
70
+ self._escalation_fn = escalation_fn or (lambda *a: None)
71
+
72
+ def record(self, ip: str, detector: str, note: str = "") -> float:
73
+ """
74
+ Add score for a detector hit on this IP.
75
+ Returns the new effective score after decay + increment.
76
+ """
77
+ if not ip:
78
+ return 0.0
79
+
80
+ now = time.time()
81
+ data = _load(self._root)
82
+ entry = data.get(
83
+ ip,
84
+ {
85
+ "ip": ip,
86
+ "score": 0.0,
87
+ "last_seen": now,
88
+ "hit_count": 0,
89
+ "escalated_high": False,
90
+ "escalated_critical": False,
91
+ "notes": [],
92
+ },
93
+ )
94
+
95
+ # Apply decay since last event
96
+ decayed = _decay(entry, now)
97
+ increment = _SCORE_MAP.get(detector, 10)
98
+ new_score = decayed + increment
99
+
100
+ entry["score"] = new_score
101
+ entry["last_seen"] = now
102
+ entry["hit_count"] = entry.get("hit_count", 0) + 1
103
+ if note:
104
+ entry["notes"] = (entry.get("notes", []) + [note])[-20:]
105
+
106
+ data[ip] = entry
107
+
108
+ # Check escalation before saving so we use in-memory data (no re-read race)
109
+ should_escalate_critical = new_score >= _ESCALATE_CRITICAL and not entry.get(
110
+ "escalated_critical"
111
+ )
112
+ should_escalate_high = (
113
+ new_score >= _ESCALATE_HIGH
114
+ and not should_escalate_critical
115
+ and not entry.get("escalated_high")
116
+ )
117
+
118
+ if should_escalate_critical:
119
+ entry["escalated_critical"] = True
120
+ elif should_escalate_high:
121
+ entry["escalated_high"] = True
122
+
123
+ _save(self._root, data)
124
+
125
+ if should_escalate_critical:
126
+ self._escalation_fn(ip, new_score, "critical")
127
+ elif should_escalate_high:
128
+ self._escalation_fn(ip, new_score, "high")
129
+
130
+ return new_score
131
+
132
+ def score(self, ip: str) -> float:
133
+ """Current decayed score for an IP."""
134
+ data = _load(self._root)
135
+ entry = data.get(ip)
136
+ if not entry:
137
+ return 0.0
138
+ return _decay(entry, time.time())
139
+
140
+ def top(self, n: int = 10) -> list[dict]:
141
+ """Return top-n IPs by current decayed score."""
142
+ now = time.time()
143
+ data = _load(self._root)
144
+ scored = [{**entry, "current_score": _decay(entry, now)} for entry in data.values()]
145
+ return sorted(scored, key=lambda e: e["current_score"], reverse=True)[:n]
146
+
147
+ def clear_ip(self, ip: str) -> bool:
148
+ """Remove an IP from the watchlist. Returns True if found."""
149
+ data = _load(self._root)
150
+ if ip not in data:
151
+ return False
152
+ del data[ip]
153
+ _save(self._root, data)
154
+ return True
155
+
156
+ def purge_expired(self) -> int:
157
+ """Remove IPs whose score has fully decayed. Returns count removed."""
158
+ now = time.time()
159
+ data = _load(self._root)
160
+ keep = {ip: e for ip, e in data.items() if _decay(e, now) > 0}
161
+ removed = len(data) - len(keep)
162
+ if removed:
163
+ _save(self._root, keep)
164
+ return removed