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,153 @@
1
+ """DataFlowAbuseCampaign — test data flow security.
2
+
3
+ Maps data flows, tests sanitization, encoding bypass, cross-boundary
4
+ flows, and security meaning changes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ from .base import Campaign, CampaignStep
12
+
13
+ _log = logging.getLogger("patchi.core.campaigns.data_flow_abuse")
14
+
15
+
16
+ class DataFlowAbuseCampaign(Campaign):
17
+ """Test data flow injection and security meaning."""
18
+
19
+ name = "data_flow_abuse"
20
+ description = "Tests data flow injection and security"
21
+
22
+ def steps(self) -> list[CampaignStep]:
23
+ return [
24
+ CampaignStep(name="map_flows", description="Map all data flows from the graph"),
25
+ CampaignStep(
26
+ name="identify_transformations",
27
+ description="Identify data transformations in flows",
28
+ ),
29
+ CampaignStep(
30
+ name="test_sanitization", description="Test sanitization on sensitive flows"
31
+ ),
32
+ CampaignStep(
33
+ name="test_encoding_bypass", description="Test encoding bypass on transformations"
34
+ ),
35
+ CampaignStep(name="test_cross_boundary", description="Test cross-boundary data flows"),
36
+ CampaignStep(name="test_security_meaning", description="Test security meaning changes"),
37
+ ]
38
+
39
+ def _execute_step(self, step: CampaignStep) -> None:
40
+ if step.name == "map_flows":
41
+ self._map_flows(step)
42
+ elif step.name == "identify_transformations":
43
+ self._identify_transformations(step)
44
+ elif step.name == "test_sanitization":
45
+ self._test_sanitization(step)
46
+ elif step.name == "test_encoding_bypass":
47
+ self._test_encoding_bypass(step)
48
+ elif step.name == "test_cross_boundary":
49
+ self._test_cross_boundary(step)
50
+ elif step.name == "test_security_meaning":
51
+ self._test_security_meaning(step)
52
+
53
+ def _map_flows(self, step: CampaignStep) -> None:
54
+ """Map all data flows mentioned in the graph."""
55
+ flows: list[dict] = []
56
+ for claim in self._graph.claims.values():
57
+ for ev in claim.evidence:
58
+ a = ev.artifact
59
+ if "source" in a and "sink" in a:
60
+ flows.append(
61
+ {
62
+ "source": a["source"],
63
+ "sink": a["sink"],
64
+ "sanitized": a.get("sanitized", False),
65
+ "cross_boundary": a.get("cross_boundary", False),
66
+ }
67
+ )
68
+
69
+ if not flows:
70
+ step.findings.append(
71
+ {
72
+ "type": "no_flows_found",
73
+ "severity": "info",
74
+ "detail": "No data flows discovered from the assurance graph",
75
+ }
76
+ )
77
+ else:
78
+ step.findings.append(
79
+ {
80
+ "type": "flows_mapped",
81
+ "severity": "info",
82
+ "detail": f"Found {len(flows)} data flows",
83
+ }
84
+ )
85
+
86
+ def _identify_transformations(self, step: CampaignStep) -> None:
87
+ """Check for unsanitized transformations in data flows."""
88
+ for claim in self._graph.claims.values():
89
+ for ev in claim.evidence:
90
+ a = ev.artifact
91
+ if "source" in a and "sink" in a:
92
+ if not a.get("sanitized", True):
93
+ step.findings.append(
94
+ {
95
+ "type": "unsanitized_flow",
96
+ "severity": "high",
97
+ "detail": f"Unsanitized flow: {a['source']} → {a['sink']}",
98
+ }
99
+ )
100
+
101
+ def _test_sanitization(self, step: CampaignStep) -> None:
102
+ """Test that sensitive data flows are properly sanitized."""
103
+ for claim in self._graph.claims.values():
104
+ if "sanitiz" in claim.statement.lower():
105
+ if claim.verdict.value == "disproved":
106
+ step.findings.append(
107
+ {
108
+ "type": "sanitization_bypass",
109
+ "severity": "critical",
110
+ "detail": f"Sanitization bypassed: {claim.statement}",
111
+ }
112
+ )
113
+
114
+ def _test_encoding_bypass(self, step: CampaignStep) -> None:
115
+ """Test for encoding bypass on data transformations."""
116
+ for claim in self._graph.claims.values():
117
+ if "encod" in claim.statement.lower() or "transform" in claim.statement.lower():
118
+ if claim.verdict.value == "disproved":
119
+ step.findings.append(
120
+ {
121
+ "type": "encoding_bypass",
122
+ "severity": "high",
123
+ "detail": f"Encoding bypass found: {claim.statement}",
124
+ }
125
+ )
126
+
127
+ def _test_cross_boundary(self, step: CampaignStep) -> None:
128
+ """Test that cross-boundary flows have proper controls."""
129
+ for claim in self._graph.claims.values():
130
+ for ev in claim.evidence:
131
+ a = ev.artifact
132
+ if a.get("cross_boundary", False):
133
+ if ev.supports: # evidence supports that it's uncontrolled
134
+ step.findings.append(
135
+ {
136
+ "type": "uncontrolled_cross_boundary",
137
+ "severity": "critical",
138
+ "detail": f"Cross-boundary flow without controls: {a.get('source', '?')} → {a.get('sink', '?')}",
139
+ }
140
+ )
141
+
142
+ def _test_security_meaning(self, step: CampaignStep) -> None:
143
+ """Test that data flow transformations don't change security meaning."""
144
+ for claim in self._graph.claims.values():
145
+ if "meaning" in claim.statement.lower() and "security" in claim.statement.lower():
146
+ if claim.verdict.value == "disproved":
147
+ step.findings.append(
148
+ {
149
+ "type": "security_meaning_change",
150
+ "severity": "high",
151
+ "detail": f"Security meaning changed: {claim.statement}",
152
+ }
153
+ )
@@ -0,0 +1,140 @@
1
+ """CampaignOrchestrator — run all registered campaigns and aggregate results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+
8
+ from patchi.core.assurance.graph import AssuranceGraph
9
+
10
+ from .base import Campaign, CampaignStep
11
+ from .data_flow_abuse import DataFlowAbuseCampaign
12
+ from .state_transitions import StateTransitionCampaign
13
+
14
+ _log = logging.getLogger("patchi.core.campaigns.orchestrator")
15
+
16
+ # Registry of available campaigns
17
+ CAMPAIGN_REGISTRY: dict[str, type[Campaign]] = {
18
+ "state_transitions": StateTransitionCampaign,
19
+ "data_flow_abuse": DataFlowAbuseCampaign,
20
+ }
21
+
22
+
23
+ @dataclass
24
+ class CampaignResult:
25
+ """Result of running a single campaign."""
26
+
27
+ name: str
28
+ steps: list[CampaignStep]
29
+ total_findings: int = 0
30
+ critical: int = 0
31
+ high: int = 0
32
+
33
+ def to_dict(self) -> dict:
34
+ return {
35
+ "name": self.name,
36
+ "steps": [s.to_dict() for s in self.steps],
37
+ "total_findings": self.total_findings,
38
+ "critical": self.critical,
39
+ "high": self.high,
40
+ }
41
+
42
+
43
+ @dataclass
44
+ class OrchestratorResult:
45
+ """Aggregated result of all campaigns."""
46
+
47
+ campaigns: list[CampaignResult]
48
+ total_findings: int = 0
49
+ critical: int = 0
50
+ high: int = 0
51
+
52
+ def to_dict(self) -> dict:
53
+ return {
54
+ "campaigns": [c.to_dict() for c in self.campaigns],
55
+ "total_findings": self.total_findings,
56
+ "critical": self.critical,
57
+ "high": self.high,
58
+ }
59
+
60
+
61
+ class CampaignOrchestrator:
62
+ """Orchestrate all registered campaigns against the assurance graph."""
63
+
64
+ def __init__(self, graph: AssuranceGraph):
65
+ self._graph = graph
66
+
67
+ def run_all(self) -> OrchestratorResult:
68
+ """Run all registered campaigns."""
69
+ results: list[CampaignResult] = []
70
+ total_findings = 0
71
+ critical = 0
72
+ high = 0
73
+
74
+ for name, cls in CAMPAIGN_REGISTRY.items():
75
+ try:
76
+ campaign = cls(self._graph)
77
+ steps = campaign.run()
78
+
79
+ campaign_findings = 0
80
+ campaign_critical = 0
81
+ campaign_high = 0
82
+ for step in steps:
83
+ campaign_findings += len(step.findings)
84
+ for f in step.findings:
85
+ sev = f.get("severity", "")
86
+ if sev == "critical":
87
+ campaign_critical += 1
88
+ elif sev == "high":
89
+ campaign_high += 1
90
+
91
+ result = CampaignResult(
92
+ name=name,
93
+ steps=steps,
94
+ total_findings=campaign_findings,
95
+ critical=campaign_critical,
96
+ high=campaign_high,
97
+ )
98
+ results.append(result)
99
+ total_findings += campaign_findings
100
+ critical += campaign_critical
101
+ high += campaign_high
102
+ except Exception as e:
103
+ _log.warning("Campaign %s failed: %s", name, e)
104
+ results.append(
105
+ CampaignResult(
106
+ name=name,
107
+ steps=[CampaignStep(name="error", description=str(e), status="failed")],
108
+ total_findings=1,
109
+ )
110
+ )
111
+
112
+ return OrchestratorResult(
113
+ campaigns=results,
114
+ total_findings=total_findings,
115
+ critical=critical,
116
+ high=high,
117
+ )
118
+
119
+ def run_campaign(self, name: str) -> CampaignResult:
120
+ """Run a specific campaign by name."""
121
+ cls = CAMPAIGN_REGISTRY.get(name)
122
+ if cls is None:
123
+ raise ValueError(
124
+ f"Unknown campaign: {name}. Available: {list(CAMPAIGN_REGISTRY.keys())}"
125
+ )
126
+
127
+ campaign = cls(self._graph)
128
+ steps = campaign.run()
129
+
130
+ total = sum(len(s.findings) for s in steps)
131
+ critical = sum(1 for s in steps for f in s.findings if f.get("severity") == "critical")
132
+ high = sum(1 for s in steps for f in s.findings if f.get("severity") == "high")
133
+
134
+ return CampaignResult(
135
+ name=name,
136
+ steps=steps,
137
+ total_findings=total,
138
+ critical=critical,
139
+ high=high,
140
+ )
@@ -0,0 +1,178 @@
1
+ """StateTransitionCampaign — test application state machine security.
2
+
3
+ Discovers states, maps transitions, tests legal/illegal sequences,
4
+ checks for replay attacks and concurrent race conditions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ from .base import Campaign, CampaignStep
12
+
13
+ _log = logging.getLogger("patchi.core.campaigns.state_transitions")
14
+
15
+
16
+ class StateTransitionCampaign(Campaign):
17
+ """Test state machine security: legal transitions, illegal transitions, replay, concurrency."""
18
+
19
+ name = "state_transitions"
20
+ description = "Tests application state machine security"
21
+
22
+ def steps(self) -> list[CampaignStep]:
23
+ return [
24
+ CampaignStep(
25
+ name="discover_states", description="Discover all application states from the graph"
26
+ ),
27
+ CampaignStep(name="map_transitions", description="Map all legal state transitions"),
28
+ CampaignStep(
29
+ name="identify_guards", description="Identify guards protecting state transitions"
30
+ ),
31
+ CampaignStep(
32
+ name="test_legal_sequences", description="Test legal state transition sequences"
33
+ ),
34
+ CampaignStep(
35
+ name="test_illegal_sequences", description="Test illegal state transition sequences"
36
+ ),
37
+ CampaignStep(
38
+ name="test_replay", description="Test replay attacks on state transitions"
39
+ ),
40
+ CampaignStep(name="test_concurrent", description="Test concurrent state transitions"),
41
+ ]
42
+
43
+ def _execute_step(self, step: CampaignStep) -> None:
44
+ if step.name == "discover_states":
45
+ self._discover_states(step)
46
+ elif step.name == "map_transitions":
47
+ self._map_transitions(step)
48
+ elif step.name == "identify_guards":
49
+ self._identify_guards(step)
50
+ elif step.name == "test_legal_sequences":
51
+ self._test_legal_sequences(step)
52
+ elif step.name == "test_illegal_sequences":
53
+ self._test_illegal_sequences(step)
54
+ elif step.name == "test_replay":
55
+ self._test_replay(step)
56
+ elif step.name == "test_concurrent":
57
+ self._test_concurrent(step)
58
+
59
+ def _discover_states(self, step: CampaignStep) -> None:
60
+ """Find all states mentioned in the assurance graph."""
61
+ states: set[str] = set()
62
+ for claim in self._graph.claims.values():
63
+ for ev in claim.evidence:
64
+ a = ev.artifact
65
+ if "from_state" in a:
66
+ states.add(a["from_state"])
67
+ if "to_state" in a:
68
+ states.add(a["to_state"])
69
+
70
+ if not states:
71
+ step.findings.append(
72
+ {
73
+ "type": "no_states_found",
74
+ "severity": "info",
75
+ "detail": "No application states discovered from the assurance graph",
76
+ }
77
+ )
78
+ else:
79
+ step.findings.append(
80
+ {
81
+ "type": "states_discovered",
82
+ "severity": "info",
83
+ "detail": f"Found {len(states)} states: {', '.join(sorted(states))}",
84
+ }
85
+ )
86
+
87
+ def _map_transitions(self, step: CampaignStep) -> None:
88
+ """Map all legal transitions and check for completeness."""
89
+ transitions: list[dict] = []
90
+ for claim in self._graph.claims.values():
91
+ for ev in claim.evidence:
92
+ a = ev.artifact
93
+ if "from_state" in a and "to_state" in a:
94
+ transitions.append(
95
+ {
96
+ "from": a["from_state"],
97
+ "to": a["to_state"],
98
+ "action": a.get("action", "unknown"),
99
+ "guarded": a.get("guarded", False),
100
+ }
101
+ )
102
+
103
+ # Check for transitions without guards
104
+ unguarded = [t for t in transitions if not t["guarded"]]
105
+ if unguarded:
106
+ for t in unguarded:
107
+ step.findings.append(
108
+ {
109
+ "type": "unguarded_transition",
110
+ "severity": "medium",
111
+ "detail": f"Unguarded transition: {t['from']} → {t['to']} via {t['action']}",
112
+ }
113
+ )
114
+
115
+ def _identify_guards(self, step: CampaignStep) -> None:
116
+ """Check that transitions have authorization guards."""
117
+ for claim in self._graph.claims.values():
118
+ if "guard" in claim.statement.lower() or "auth" in claim.statement.lower():
119
+ if claim.verdict.value == "disproved":
120
+ step.findings.append(
121
+ {
122
+ "type": "guard_broken",
123
+ "severity": "critical",
124
+ "detail": f"Guard violated: {claim.statement}",
125
+ }
126
+ )
127
+
128
+ def _test_legal_sequences(self, step: CampaignStep) -> None:
129
+ """Test that legal transition sequences don't bypass guards."""
130
+ # Check for claims about legal sequences
131
+ for claim in self._graph.claims.values():
132
+ if "sequence" in claim.statement.lower() and claim.verdict.value == "disproved":
133
+ step.findings.append(
134
+ {
135
+ "type": "sequence_bypass",
136
+ "severity": "high",
137
+ "detail": f"Legal sequence bypasses guard: {claim.statement}",
138
+ }
139
+ )
140
+
141
+ def _test_illegal_sequences(self, step: CampaignStep) -> None:
142
+ """Test that illegal transitions are properly blocked."""
143
+ # Find claims about forbidden transitions
144
+ for claim in self._graph.claims.values():
145
+ if "must not" in claim.statement.lower() or "forbidden" in claim.statement.lower():
146
+ if claim.verdict.value == "disproved":
147
+ step.findings.append(
148
+ {
149
+ "type": "illegal_transition_allowed",
150
+ "severity": "critical",
151
+ "detail": f"Forbidden transition is possible: {claim.statement}",
152
+ }
153
+ )
154
+
155
+ def _test_replay(self, step: CampaignStep) -> None:
156
+ """Test for replay vulnerabilities in state transitions."""
157
+ for claim in self._graph.claims.values():
158
+ if "replay" in claim.statement.lower() and claim.verdict.value == "disproved":
159
+ step.findings.append(
160
+ {
161
+ "type": "replay_vulnerability",
162
+ "severity": "high",
163
+ "detail": f"Replay attack possible: {claim.statement}",
164
+ }
165
+ )
166
+
167
+ def _test_concurrent(self, step: CampaignStep) -> None:
168
+ """Test for concurrent state transition race conditions."""
169
+ for claim in self._graph.claims.values():
170
+ if "concurrent" in claim.statement.lower() or "race" in claim.statement.lower():
171
+ if claim.verdict.value == "disproved":
172
+ step.findings.append(
173
+ {
174
+ "type": "race_condition",
175
+ "severity": "high",
176
+ "detail": f"Race condition found: {claim.statement}",
177
+ }
178
+ )
@@ -0,0 +1,104 @@
1
+ """
2
+ CI/PR Bundle — Finding stable id + Baseline+delta + --since + SARIF + fix --safe-all.
3
+
4
+ Stable id: hash(file+type+line//5) for dedup across runs.
5
+ Renderer registry: markdown/json/sarif renderers via register_renderer().
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ from collections.abc import Callable
13
+ from pathlib import Path
14
+
15
+ def stable_id(finding: dict) -> str:
16
+ raw = f"{finding.get('file','')}:{finding.get('type','')}:{finding.get('line',0)//5}"
17
+ return hashlib.sha256(raw.encode()).hexdigest()[:12]
18
+
19
+ def baseline_delta(baseline: list[dict], current: list[dict]) -> dict:
20
+ b_ids = {stable_id(f) for f in baseline}
21
+ c_ids = {stable_id(f) for f in current}
22
+ added = [f for f in current if stable_id(f) not in b_ids]
23
+ fixed = [f for f in baseline if stable_id(f) not in c_ids]
24
+ return {"added": added, "fixed": fixed, "added_count": len(added), "fixed_count": len(fixed)}
25
+
26
+ def filter_since(findings: list[dict], since_ref: str, root: Path) -> list[dict]:
27
+ """Filter findings to files changed since git ref."""
28
+ try:
29
+ import subprocess
30
+ out = subprocess.run(["git","diff","--name-only", since_ref], capture_output=True, text=True, timeout=5, cwd=str(root))
31
+ if out.returncode != 0:
32
+ return findings
33
+ changed = set(out.stdout.splitlines())
34
+ return [f for f in findings if f.get("file","") in changed]
35
+ except Exception:
36
+ return findings
37
+
38
+ def to_sarif(findings: list[dict], root: Path | None = None) -> dict:
39
+ """Single SARIF implementation — delegates to export.sarif.
40
+
41
+ (root kept for compat; SARIF carries no repo-root field.)
42
+ """
43
+ from patchi.core.export.sarif import convert_findings
44
+
45
+ return convert_findings(findings)
46
+
47
+ def write_sarif(findings: list[dict], out_path: Path) -> None:
48
+ out_path.parent.mkdir(parents=True, exist_ok=True)
49
+ out_path.write_text(json.dumps(to_sarif(findings), indent=2), encoding="utf-8")
50
+
51
+
52
+ def render_markdown(findings: list[dict]) -> str:
53
+ lines = ["# Patchi Findings", ""]
54
+ order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
55
+ def _sort_key(d: dict) -> tuple:
56
+ return (order.get(d.get("severity", "info"), 5), d.get("file", ""))
57
+ for f in sorted(findings, key=_sort_key):
58
+ lines.append(
59
+ f"- `{f.get('file', '?')}:{f.get('line', 0)}` "
60
+ f"[{f.get('severity', 'info')}] {f.get('type', '?')} — {f.get('message', '')}"
61
+ )
62
+ return "\n".join(lines) + "\n"
63
+
64
+
65
+ RENDERERS: dict[str, Callable[[list[dict]], str]] = {
66
+ "markdown": render_markdown,
67
+ "json": lambda findings: json.dumps(findings, indent=2),
68
+ "sarif": lambda findings: json.dumps(to_sarif(findings), indent=2),
69
+ }
70
+
71
+
72
+ _SEV_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
73
+
74
+
75
+ def exit_code_for(findings: list[dict], fail_on: str | None) -> int:
76
+ """CI gate: 1 when any finding meets the --fail-on severity, else 0.
77
+
78
+ No threshold (None) never fails — plain `p scan` stays exit 0.
79
+ """
80
+ if not fail_on:
81
+ return 0
82
+ try:
83
+ bar = _SEV_ORDER[fail_on.lower()]
84
+ except KeyError:
85
+ raise ValueError(f"unknown severity {fail_on!r} (have: {sorted(_SEV_ORDER)})") from None
86
+ for f in findings:
87
+ sev = f.get("severity", "info")
88
+ sev = sev.value if hasattr(sev, "value") else str(sev)
89
+ if _SEV_ORDER.get(sev.lower(), 5) <= bar:
90
+ return 1
91
+ return 0
92
+
93
+
94
+ def register_renderer(name: str, fn: Callable[[list[dict]], str]) -> None:
95
+ """Add or override a findings renderer (e.g. plugins)."""
96
+ RENDERERS[name.lower()] = fn
97
+
98
+
99
+ def render_findings(findings: list[dict], fmt: str = "markdown") -> str:
100
+ try:
101
+ renderer = RENDERERS[fmt.lower()]
102
+ except KeyError:
103
+ raise ValueError(f"unknown findings format {fmt!r} (have: {sorted(RENDERERS)})") from None
104
+ return renderer(findings)