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
patchi/web/api/cicd.py ADDED
@@ -0,0 +1,576 @@
1
+ """CI/CD REST API — trigger scans, retrieve results, health checks.
2
+
3
+ Endpoints:
4
+ POST /api/cicd/scan — trigger scan and wait for results (sync)
5
+ POST /api/cicd/scan/async — trigger scan, return immediately (async)
6
+ GET /api/cicd/scan/status — get current scan status
7
+ GET /api/cicd/scan/results — get latest scan results
8
+ GET /api/cicd/health — health check
9
+ GET /api/cicd/assurance — get assurance graph data
10
+ GET /api/cicd/findings — get findings with filters
11
+ GET /api/cicd/summary — project summary (findings, agents, health)
12
+
13
+ Usage from CI/CD:
14
+ curl -X POST http://localhost:1612/api/cicd/scan -d '{"scan_type":"all"}'
15
+ curl http://localhost:1612/api/cicd/health
16
+ curl http://localhost:1612/api/cicd/findings?severity=critical&limit=10
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import hashlib
23
+ import logging
24
+ import os
25
+ import time
26
+ from pathlib import Path
27
+
28
+ from fastapi import APIRouter, Depends, Header, Query, Request
29
+ from fastapi.responses import JSONResponse
30
+ from pydantic import BaseModel
31
+
32
+ _log = logging.getLogger("patchi.web.cicd")
33
+
34
+ router = APIRouter(prefix="/api/cicd")
35
+
36
+ # ── API Key Authentication ───────────────────────────────────────────────────
37
+ # The API key is read from:
38
+ # 1. Environment variable PATCHI_API_KEY (recommended for production)
39
+ # 2. .patchi/api_key file (auto-generated on first run if missing)
40
+ # 3. If neither exists, auth is disabled (local-only mode)
41
+ #
42
+ # Clients send the key via X-API-Key header.
43
+ # Health check endpoint is always unauthenticated.
44
+
45
+
46
+ def _get_api_key(root: Path | None = None) -> str | None:
47
+ """Resolve the API key from env or .patchi/api_key file."""
48
+ # 1. Environment variable (always checked first)
49
+ env_key = os.environ.get("PATCHI_API_KEY", "")
50
+ if env_key:
51
+ return env_key.strip()
52
+ # 2. File-based key (generated lazily)
53
+ if root is None:
54
+ try:
55
+ from patchi.core.config import require_project_root
56
+
57
+ root = require_project_root()
58
+ except Exception:
59
+ return None
60
+ try:
61
+ key_file = root / ".patchi" / "api_key"
62
+ if key_file.is_file():
63
+ return key_file.read_text(encoding="utf-8").strip()
64
+ # Auto-generate on first access
65
+ import secrets
66
+
67
+ new_key = secrets.token_urlsafe(32)
68
+ key_file.parent.mkdir(parents=True, exist_ok=True)
69
+ key_file.write_text(new_key, encoding="utf-8")
70
+ _log.info("Generated CI/CD API key: %s...", new_key[:8])
71
+ return new_key
72
+ except Exception:
73
+ return None
74
+
75
+
76
+ def _verify_api_key(x_api_key: str | None = Header(None)) -> str | None:
77
+ """FastAPI dependency: verify X-API-Key header.
78
+
79
+ Returns the API key if valid, or None if auth is disabled.
80
+ Raises HTTPException 401 if auth is enabled but key is missing/wrong.
81
+ """
82
+ required = _get_api_key()
83
+ if not required:
84
+ return None # auth disabled
85
+ if not x_api_key:
86
+ from fastapi import HTTPException
87
+
88
+ raise HTTPException(
89
+ status_code=401,
90
+ detail={
91
+ "error": "Missing X-API-Key header",
92
+ "hint": "Send X-API-Key header with your request",
93
+ },
94
+ )
95
+ # Constant-time comparison to prevent timing attacks
96
+ if not hashlib.compare_digest(x_api_key, required):
97
+ from fastapi import HTTPException
98
+
99
+ raise HTTPException(
100
+ status_code=401,
101
+ detail={
102
+ "error": "Invalid API key",
103
+ "hint": "Send X-API-Key header with your request",
104
+ },
105
+ )
106
+ return x_api_key
107
+
108
+
109
+ # ── Models ──────────────────────────────────────────────────────────────────
110
+
111
+
112
+ class ScanRequest(BaseModel):
113
+ scan_type: str = "all"
114
+ deep: bool = False
115
+ pipeline: bool = False
116
+
117
+
118
+ # ── Scan status (shared state) ──────────────────────────────────────────────
119
+
120
+ _scan_state = {
121
+ "running": False,
122
+ "started_at": None,
123
+ "completed_at": None,
124
+ "last_result": None,
125
+ "last_error": None,
126
+ "agents_run": 0,
127
+ "total_findings": 0,
128
+ }
129
+
130
+
131
+ # ── Endpoints ───────────────────────────────────────────────────────────────
132
+
133
+
134
+ @router.get("/health")
135
+ async def health(request: Request) -> JSONResponse:
136
+ """Health check endpoint for CI/CD monitoring."""
137
+ root = request.app.state.root
138
+
139
+ # Check brain data
140
+ brain_exists = (root / ".patchi" / "brain.json").is_file()
141
+
142
+ # Check assurance graph
143
+ assurance_exists = (root / ".patchi" / "assurance_graph.json").is_file()
144
+
145
+ # Check scan results
146
+ scan_results_exist = False
147
+ try:
148
+ from patchi.core import memory as mem
149
+
150
+ results = mem.get_scan_results(root)
151
+ scan_results_exist = bool(results)
152
+ except Exception as _exc:
153
+ _log.warning('health failed: %s', _exc)
154
+
155
+ return JSONResponse(
156
+ {
157
+ "status": "healthy",
158
+ "version": "0.6.0",
159
+ "project_root": str(root),
160
+ "brain_ready": brain_exists,
161
+ "assurance_ready": assurance_exists,
162
+ "scan_results_ready": scan_results_exist,
163
+ "scan_running": _scan_state["running"],
164
+ "last_scan": _scan_state["completed_at"],
165
+ }
166
+ )
167
+
168
+
169
+ @router.post("/scan")
170
+ async def scan_sync(
171
+ request: Request, body: ScanRequest = None, _key: str | None = Depends(_verify_api_key)
172
+ ) -> JSONResponse:
173
+ """Trigger scan and wait for results (synchronous, for CI/CD).
174
+
175
+ Returns scan results when complete. Timeout: 300s.
176
+ """
177
+ if _scan_state["running"]:
178
+ return JSONResponse(
179
+ {
180
+ "ok": False,
181
+ "error": "Scan already running",
182
+ "started_at": _scan_state["started_at"],
183
+ },
184
+ status_code=409,
185
+ )
186
+
187
+ body = body or ScanRequest()
188
+ root = request.app.state.root
189
+
190
+ _scan_state["running"] = True
191
+ _scan_state["started_at"] = time.time()
192
+ _scan_state["completed_at"] = None
193
+ _scan_state["last_error"] = None
194
+
195
+ try:
196
+ result = await _run_scan(root, body.scan_type, body.deep, body.pipeline)
197
+ _scan_state["last_result"] = result
198
+ _scan_state["completed_at"] = time.time()
199
+ return JSONResponse(result)
200
+ except Exception as e:
201
+ _scan_state["last_error"] = str(e)
202
+ return JSONResponse(
203
+ {
204
+ "ok": False,
205
+ "error": str(e),
206
+ "duration_ms": int((time.time() - _scan_state["started_at"]) * 1000),
207
+ },
208
+ status_code=500,
209
+ )
210
+ finally:
211
+ _scan_state["running"] = False
212
+
213
+
214
+ @router.post("/scan/async")
215
+ async def scan_async(
216
+ request: Request, body: ScanRequest = None, _key: str | None = Depends(_verify_api_key)
217
+ ) -> JSONResponse:
218
+ """Trigger scan and return immediately (async, for CI/CD).
219
+
220
+ Poll /api/cicd/scan/status for completion.
221
+ """
222
+ if _scan_state["running"]:
223
+ return JSONResponse(
224
+ {
225
+ "ok": False,
226
+ "error": "Scan already running",
227
+ "started_at": _scan_state["started_at"],
228
+ },
229
+ status_code=409,
230
+ )
231
+
232
+ body = body or ScanRequest()
233
+ root = request.app.state.root
234
+
235
+ _scan_state["running"] = True
236
+ _scan_state["started_at"] = time.time()
237
+ _scan_state["completed_at"] = None
238
+
239
+ async def _bg():
240
+ try:
241
+ result = await _run_scan(root, body.scan_type, body.deep, body.pipeline)
242
+ _scan_state["last_result"] = result
243
+ _scan_state["completed_at"] = time.time()
244
+ except Exception as e:
245
+ _scan_state["last_error"] = str(e)
246
+ finally:
247
+ _scan_state["running"] = False
248
+
249
+ asyncio.create_task(_bg())
250
+
251
+ return JSONResponse(
252
+ {
253
+ "ok": True,
254
+ "message": "Scan started",
255
+ "started_at": _scan_state["started_at"],
256
+ "poll_url": "/api/cicd/scan/status",
257
+ }
258
+ )
259
+
260
+
261
+ @router.get("/scan/status")
262
+ async def scan_status(_key: str | None = Depends(_verify_api_key)) -> JSONResponse:
263
+ """Get current scan status."""
264
+ return JSONResponse(
265
+ {
266
+ "running": _scan_state["running"],
267
+ "started_at": _scan_state["started_at"],
268
+ "completed_at": _scan_state["completed_at"],
269
+ "agents_run": _scan_state["agents_run"],
270
+ "total_findings": _scan_state["total_findings"],
271
+ "error": _scan_state["last_error"],
272
+ }
273
+ )
274
+
275
+
276
+ @router.get("/scan/results")
277
+ async def scan_results(
278
+ request: Request, _key: str | None = Depends(_verify_api_key)
279
+ ) -> JSONResponse:
280
+ """Get latest scan results."""
281
+ root = request.app.state.root
282
+
283
+ if _scan_state["last_result"]:
284
+ return JSONResponse(_scan_state["last_result"])
285
+
286
+ # Load from memory
287
+ try:
288
+ from patchi.core import memory as mem
289
+
290
+ results = mem.get_scan_results(root)
291
+ findings = []
292
+ for agent_name, data in (results or {}).items():
293
+ for f in data.get("findings", []):
294
+ f["agent"] = agent_name
295
+ findings.append(f)
296
+
297
+ sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
298
+ findings.sort(key=lambda f: sev_order.get(f.get("severity", "info"), 5))
299
+
300
+ return JSONResponse(
301
+ {
302
+ "ok": True,
303
+ "total": len(findings),
304
+ "findings": findings[:100],
305
+ "by_severity": {
306
+ s: sum(1 for f in findings if f.get("severity") == s)
307
+ for s in ["critical", "high", "medium", "low", "info"]
308
+ },
309
+ }
310
+ )
311
+ except Exception as e:
312
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
313
+
314
+
315
+ @router.get("/findings")
316
+ async def findings(
317
+ request: Request,
318
+ severity: str | None = None,
319
+ agent: str | None = None,
320
+ limit: int = Query(50, ge=1, le=500),
321
+ offset: int = Query(0, ge=0),
322
+ _key: str | None = Depends(_verify_api_key),
323
+ ) -> JSONResponse:
324
+ """Get findings with filters."""
325
+ root = request.app.state.root
326
+
327
+ try:
328
+ from patchi.core import memory as mem
329
+
330
+ results = mem.get_scan_results(root)
331
+ all_findings = []
332
+ for agent_name, data in (results or {}).items():
333
+ for f in data.get("findings", []):
334
+ f["agent"] = agent_name
335
+ all_findings.append(f)
336
+
337
+ # Apply filters
338
+ if severity:
339
+ all_findings = [f for f in all_findings if f.get("severity") == severity]
340
+ if agent:
341
+ all_findings = [f for f in all_findings if f.get("agent") == agent]
342
+
343
+ # Sort by severity
344
+ sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
345
+ all_findings.sort(key=lambda f: sev_order.get(f.get("severity", "info"), 5))
346
+
347
+ total = len(all_findings)
348
+ page = all_findings[offset : offset + limit]
349
+
350
+ return JSONResponse(
351
+ {
352
+ "ok": True,
353
+ "total": total,
354
+ "offset": offset,
355
+ "limit": limit,
356
+ "findings": page,
357
+ }
358
+ )
359
+ except Exception as e:
360
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
361
+
362
+
363
+ @router.get("/assurance")
364
+ async def assurance(request: Request, _key: str | None = Depends(_verify_api_key)) -> JSONResponse:
365
+ """Get assurance graph data."""
366
+ root = request.app.state.root
367
+
368
+ try:
369
+ from patchi.core.assurance.graph import AssuranceGraph
370
+
371
+ graph = AssuranceGraph.load(root)
372
+ coverage = graph.coverage()
373
+
374
+ claims = []
375
+ for claim in graph.claims.values():
376
+ claims.append(
377
+ {
378
+ "id": claim.id,
379
+ "statement": claim.statement,
380
+ "domain": claim.domain,
381
+ "verdict": claim.verdict.value,
382
+ "severity": claim.severity_if_disproved,
383
+ "evidence_count": len(claim.evidence),
384
+ }
385
+ )
386
+
387
+ return JSONResponse(
388
+ {
389
+ "ok": True,
390
+ "coverage": coverage,
391
+ "claims": claims,
392
+ "total_claims": len(claims),
393
+ }
394
+ )
395
+ except Exception as e:
396
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
397
+
398
+
399
+ @router.get("/summary")
400
+ async def summary(request: Request, _key: str | None = Depends(_verify_api_key)) -> JSONResponse:
401
+ """Project summary for CI/CD dashboards."""
402
+ root = request.app.state.root
403
+
404
+ try:
405
+ from patchi.core import memory as mem
406
+
407
+ # Brain data
408
+ brain = mem.get_brain(root)
409
+ file_count = brain.get("file_count", 0)
410
+ route_count = brain.get("route_count", 0)
411
+
412
+ # Findings
413
+ results = mem.get_scan_results(root)
414
+ all_findings = []
415
+ for agent_name, data in (results or {}).items():
416
+ for f in data.get("findings", []):
417
+ f["agent"] = agent_name
418
+ all_findings.append(f)
419
+
420
+ by_severity = {
421
+ s: sum(1 for f in all_findings if f.get("severity") == s)
422
+ for s in ["critical", "high", "medium", "low", "info"]
423
+ }
424
+
425
+ # Assurance
426
+ assurance_claims = 0
427
+ assurance_proved = 0
428
+ try:
429
+ from patchi.core.assurance.graph import AssuranceGraph
430
+
431
+ graph = AssuranceGraph.load(root)
432
+ assurance_claims = len(graph.claims)
433
+ assurance_proved = sum(1 for c in graph.claims.values() if c.verdict.value == "proved")
434
+ except Exception as _exc:
435
+ _log.warning('summary failed: %s', _exc)
436
+
437
+ # Model routing stats
438
+ routing_stats = {}
439
+ try:
440
+ from patchi.core.ai.model_router import get_model_router
441
+
442
+ router = get_model_router(root=root)
443
+ routing_stats = router.get_routing_stats()
444
+ except Exception as _exc:
445
+ _log.warning('summary failed: %s', _exc)
446
+
447
+ # Tenant cost
448
+ tenant_cost = 0.0
449
+ try:
450
+ from patchi.core.tenant import get_tenant_cost
451
+
452
+ tenant_cost = get_tenant_cost(root)
453
+ except Exception as _exc:
454
+ _log.warning('summary failed: %s', _exc)
455
+
456
+ return JSONResponse(
457
+ {
458
+ "ok": True,
459
+ "project": str(root),
460
+ "files": file_count,
461
+ "routes": route_count,
462
+ "findings": {
463
+ "total": len(all_findings),
464
+ "by_severity": by_severity,
465
+ },
466
+ "assurance": {
467
+ "claims": assurance_claims,
468
+ "proved": assurance_proved,
469
+ },
470
+ "routing": routing_stats,
471
+ "tenant_cost": tenant_cost,
472
+ "health_score": _compute_health_score(
473
+ all_findings, assurance_claims, assurance_proved
474
+ ),
475
+ }
476
+ )
477
+ except Exception as e:
478
+ return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
479
+
480
+
481
+ # ── Helpers ─────────────────────────────────────────────────────────────────
482
+
483
+
484
+ async def _run_scan(root: Path, scan_type: str, deep: bool, pipeline: bool) -> dict:
485
+ """Run a scan and return results."""
486
+ import patchi.core.security.security_agents # noqa: F401
487
+ from patchi.core import config as cfg
488
+ from patchi.core import memory as mem
489
+ from patchi.core.agents.base import AgentGroup, AgentInput, list_agents
490
+
491
+ config = cfg.load(root) if (root / "pyproject.toml").exists() else {}
492
+ brain = mem.get_brain(root)
493
+
494
+ # Determine agents
495
+ _SKIP = {"SecurityProber", "RedTeamAgent"}
496
+ if scan_type == "all":
497
+ to_run = [a for a in list_agents(AgentGroup.SECURITY) if a.name not in _SKIP]
498
+ else:
499
+ all_agents = {a.name: a for a in list_agents(AgentGroup.SECURITY)}
500
+ if scan_type in all_agents:
501
+ to_run = [all_agents[scan_type]]
502
+ else:
503
+ raise ValueError(f"Unknown scan type: {scan_type}")
504
+
505
+ # Run agents
506
+ results = []
507
+ for agent_cls in to_run:
508
+ inp = AgentInput(root=root, scope=[], brain=brain, config=config)
509
+ result = await asyncio.to_thread(agent_cls().run, inp)
510
+ results.append(result)
511
+
512
+ _scan_state["agents_run"] = len(results)
513
+ _scan_state["total_findings"] = sum(r.finding_count for r in results)
514
+
515
+ # Correlate
516
+ try:
517
+ from patchi.core.security.orchestrator import SecurityOrchestrator
518
+
519
+ report = SecurityOrchestrator().correlate(results)
520
+ total = report.total_findings
521
+ except Exception:
522
+ total = sum(r.finding_count for r in results)
523
+
524
+ # Build response
525
+ all_findings = []
526
+ for r in results:
527
+ for f in r.findings:
528
+ all_findings.append(
529
+ {
530
+ "agent": r.agent_name,
531
+ "type": f.type,
532
+ "severity": f.severity.value
533
+ if hasattr(f.severity, "value")
534
+ else str(f.severity),
535
+ "file": f.file,
536
+ "line": f.line,
537
+ "message": f.message,
538
+ }
539
+ )
540
+
541
+ return {
542
+ "ok": True,
543
+ "scan_type": scan_type,
544
+ "agents_run": len(results),
545
+ "total_findings": total,
546
+ "findings": all_findings[:200],
547
+ "by_severity": {
548
+ s: sum(1 for f in all_findings if f.get("severity") == s)
549
+ for s in ["critical", "high", "medium", "low", "info"]
550
+ },
551
+ "duration_ms": int((time.time() - _scan_state["started_at"]) * 1000),
552
+ }
553
+
554
+
555
+ def _compute_health_score(findings: list, claims: int, proved: int) -> int:
556
+ """Compute a 0-100 health score."""
557
+ score = 100
558
+
559
+ # Deductions for findings
560
+ for f in findings:
561
+ sev = f.get("severity", "info")
562
+ if sev == "critical":
563
+ score -= 15
564
+ elif sev == "high":
565
+ score -= 8
566
+ elif sev == "medium":
567
+ score -= 3
568
+ elif sev == "low":
569
+ score -= 1
570
+
571
+ # Bonus for assurance coverage
572
+ if claims > 0:
573
+ coverage_pct = proved / claims
574
+ score += int(coverage_pct * 10)
575
+
576
+ return max(0, min(100, score))
@@ -0,0 +1,49 @@
1
+ """Serve DAST evidence files (screenshots, logs) from .patchi/evidence/dast/."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from fastapi import APIRouter, HTTPException
9
+ from fastapi.responses import FileResponse
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ @router.get("/evidence/dast/{filename:path}")
15
+ async def serve_dast_evidence(filename: str):
16
+ """Serve a DAST evidence file (screenshot, etc.).
17
+
18
+ Path: /evidence/dast/<filename>
19
+ The file must be under the project's .patchi/evidence/dast/ directory.
20
+ """
21
+ # Security: only allow files from .patchi/evidence/dast/
22
+ safe_name = os.path.basename(filename) # prevent path traversal
23
+ # Walk up to find project root (look for .patchi dir)
24
+ project_root = Path.cwd()
25
+ evidence_path = project_root / ".patchi" / "evidence" / "dast" / safe_name
26
+
27
+ if not evidence_path.is_file():
28
+ raise HTTPException(status_code=404, detail="Evidence file not found")
29
+
30
+ # Determine media type from extension
31
+ ext = evidence_path.suffix.lower()
32
+ media_types = {
33
+ ".png": "image/png",
34
+ ".jpg": "image/jpeg",
35
+ ".jpeg": "image/jpeg",
36
+ ".gif": "image/gif",
37
+ ".webp": "image/webp",
38
+ ".svg": "image/svg+xml",
39
+ ".txt": "text/plain",
40
+ ".log": "text/plain",
41
+ ".json": "application/json",
42
+ }
43
+ media_type = media_types.get(ext, "application/octet-stream")
44
+
45
+ return FileResponse(
46
+ path=str(evidence_path),
47
+ media_type=media_type,
48
+ filename=safe_name,
49
+ )