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,423 @@
1
+ """
2
+ Confidence Gate — scores each finding 0.0–1.0 and routes by tier.
3
+
4
+ HIGH >= 0.7 -> defend (auto-fix / block)
5
+ MEDIUM >= 0.4 -> ai_analyze (Layer 2 AI confirmation)
6
+ LOW < 0.4 -> human_review (if severe) or discard
7
+
8
+ Scoring factors (additive):
9
+ + method_precision regex=0.3, AST=0.5, taint/flow=0.7, deterministic=0.9
10
+ + severity_bonus high=+0.1, critical=+0.2
11
+ + multi_agent_bonus +0.1 per confirming agent, max +0.3
12
+ + location_precision exact line=+0.1, within 5 lines=+0.05
13
+ - known_fp_penalty -0.3 if matches known false-positive pattern
14
+ - ambiguity_penalty -0.2 if no code snippet or heuristic match
15
+
16
+ Noise controls (config under ``confidence_gate``):
17
+ min_agents_for_defend default 1 — findings confirmed by fewer agents are
18
+ demoted from 'defend' to 'ai_analyze' even at high tier.
19
+ min_agents_to_keep default 0 (off) — findings confirmed by fewer agents
20
+ than this are discarded outright regardless of score.
21
+ ai_weight default 0.0 — blend weight for calibrated AI confidence:
22
+ final = (1-w)*heuristic_score + w*ai_confidence.
23
+ fp_penalty default 0.3 — score penalty for known false positives.
24
+
25
+ False-positive learning loop:
26
+ record_false_positives() persists analyst/AI verdicts to
27
+ .patchi/memory/known_false_positives.json so repeat noise is pre-penalized
28
+ (and, with fp_auto_discard, dropped outright) on every subsequent scan.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import logging
35
+ from pathlib import Path
36
+
37
+ from patchi.core.security.gated_finding import GatedFinding, GatedReport
38
+ from patchi.core.security.orchestrator import CorrelatedFinding, SecurityReport
39
+
40
+ _log = logging.getLogger("patchi.confidence_gate")
41
+
42
+ # ── Method precision lookup ──────────────────────────────────────────────────
43
+
44
+ _METHOD_PRECISION: dict[str, float] = {
45
+ "regex": 0.3,
46
+ "ast": 0.5,
47
+ "flow": 0.7,
48
+ "deterministic": 0.9,
49
+ }
50
+
51
+ _SEVERITY_BONUS: dict[str, float] = {
52
+ "critical": 0.2,
53
+ "high": 0.1,
54
+ "medium": 0.0,
55
+ "low": 0.0,
56
+ "info": -0.1,
57
+ }
58
+
59
+ _FP_MEMORY_FILE = ".patchi/memory/known_false_positives.json"
60
+
61
+
62
+ class ConfidenceGate:
63
+ """Scores findings and assigns routing tiers."""
64
+
65
+ def __init__(self, root: Path, config: dict | None = None):
66
+ self.root = root
67
+ self.config = config or {}
68
+ noise_cfg = self.config.get("confidence_gate", {})
69
+ self.min_agents_for_defend: int = int(noise_cfg.get("min_agents_for_defend", 1))
70
+ self.min_agents_to_keep: int = int(noise_cfg.get("min_agents_to_keep", 0))
71
+ self.ai_weight: float = float(noise_cfg.get("ai_weight", 0.3)) # default 0.3, was 0.0
72
+ self.fp_penalty: float = float(noise_cfg.get("fp_penalty", 0.3))
73
+ self.noise_penalty: float = float(noise_cfg.get("noise_penalty", 0.5))
74
+ self.fp_auto_discard: bool = bool(
75
+ noise_cfg.get("fp_auto_discard", True)
76
+ ) # default True, was False
77
+ self._known_fps: set[tuple] = set()
78
+ self._load_known_fps()
79
+ # Lazy-load AI validator
80
+ self._ai_validator = None
81
+
82
+ # ── Known-FP memory ─────────────────────────────────────────────────────
83
+
84
+ @property
85
+ def _fp_path(self) -> Path:
86
+ return self.root / _FP_MEMORY_FILE
87
+
88
+ def _load_known_fps(self) -> None:
89
+ if not self._fp_path.exists():
90
+ return
91
+ try:
92
+ data = json.loads(self._fp_path.read_text(encoding="utf-8"))
93
+ for entry in data:
94
+ fp_key = (
95
+ entry.get("file", ""),
96
+ entry.get("type", ""),
97
+ entry.get("line", 0),
98
+ )
99
+ self._known_fps.add(fp_key)
100
+ except Exception as exc: # noqa: BLE001 — corrupt memory = start fresh
101
+ _log.warning("Failed to parse known_false_positives.json: %s", exc)
102
+
103
+ def record_false_positives(self, entries: list[dict]) -> int:
104
+ """Persist false-positive verdicts and apply them immediately.
105
+
106
+ Each entry needs ``file``, ``type``, ``line``; optional ``reason``
107
+ and ``source`` ("analyst" | "ai") are kept for auditability.
108
+ Returns the number of NEW keys recorded.
109
+ """
110
+ existing: list[dict] = []
111
+ if self._fp_path.exists():
112
+ try:
113
+ existing = json.loads(self._fp_path.read_text(encoding="utf-8"))
114
+ except Exception: # noqa: BLE001
115
+ existing = []
116
+
117
+ seen = {(e.get("file", ""), e.get("type", ""), e.get("line", 0)) for e in existing}
118
+ added = 0
119
+ for entry in entries:
120
+ key = (entry.get("file", ""), entry.get("type", ""), entry.get("line", 0))
121
+ if key in seen or not any(key):
122
+ continue
123
+ seen.add(key)
124
+ existing.append(entry)
125
+ self._known_fps.add(key)
126
+ added += 1
127
+
128
+ if added:
129
+ self._fp_path.parent.mkdir(parents=True, exist_ok=True)
130
+ tmp = self._fp_path.with_suffix(".json.tmp")
131
+ tmp.write_text(json.dumps(existing, indent=2), encoding="utf-8")
132
+ tmp.replace(self._fp_path)
133
+ _log.info("Recorded %d new false positive(s)", added)
134
+ return added
135
+
136
+ def forget_false_positive(self, file: str, type_: str, line: int) -> bool:
137
+ """Remove one FP verdict (analyst says it was real after all)."""
138
+ key = (file, type_, line)
139
+ if key not in self._known_fps:
140
+ return False
141
+ self._known_fps.discard(key)
142
+ remaining: list[dict] = []
143
+ if self._fp_path.exists():
144
+ try:
145
+ data = json.loads(self._fp_path.read_text(encoding="utf-8"))
146
+ remaining = [
147
+ e
148
+ for e in data
149
+ if (e.get("file", ""), e.get("type", ""), e.get("line", 0)) != key
150
+ ]
151
+ except Exception: # noqa: BLE001
152
+ remaining = []
153
+ tmp = self._fp_path.with_suffix(".json.tmp")
154
+ tmp.write_text(json.dumps(remaining, indent=2), encoding="utf-8")
155
+ tmp.replace(self._fp_path)
156
+ return True
157
+
158
+ # ── Gating ──────────────────────────────────────────────────────────────
159
+
160
+ def gate(
161
+ self,
162
+ cf: CorrelatedFinding,
163
+ ai_confidence: float | None = None,
164
+ ) -> GatedFinding:
165
+ """Score and route one correlated finding.
166
+
167
+ ``ai_confidence`` (0.0–1.0, optional) blends a model verdict into the
168
+ heuristic score using the configured ``ai_weight``.
169
+ """
170
+ score = self._compute_score(cf, ai_confidence=ai_confidence)
171
+ tier = self._assign_tier(score)
172
+ routing = self._assign_routing(tier, cf)
173
+ return GatedFinding(
174
+ finding=cf.finding,
175
+ confidence_score=score,
176
+ confidence_tier=tier,
177
+ routing=routing,
178
+ routing_reason=self._routing_reason(routing, score, cf),
179
+ confirmed_by=cf.confirmed_by,
180
+ composite_score=cf.composite_score,
181
+ owasp_category=cf.owasp_category,
182
+ cwe_ids=cf.cwe_ids,
183
+ )
184
+
185
+ def gate_all(
186
+ self,
187
+ report: SecurityReport,
188
+ ai_confidences: list[float] | None = None,
189
+ ) -> GatedReport:
190
+ gated_list = [
191
+ self.gate(
192
+ cf,
193
+ ai_confidence=(
194
+ ai_confidences[i] if ai_confidences and i < len(ai_confidences) else None
195
+ ),
196
+ )
197
+ for i, cf in enumerate(report.findings)
198
+ ]
199
+ return self._build_report(gated_list)
200
+
201
+ def gate_with_ai_validation(
202
+ self,
203
+ report: SecurityReport,
204
+ route_context: dict | None = None,
205
+ ) -> GatedReport:
206
+ """Full harness: score → route → AIConfidenceGate second pass (L2).
207
+
208
+ L2 spec: keep deterministic scanners, add AI as second pass ONLY for
209
+ confidence==low AND severity in {high,critical} OR type in {secret, injection, auth}.
210
+ Schema-validated JSON {verdict: confirmed|fp, reason, exploitability 0-1}
211
+ via call_ai_structured:212. Offline → unverified.
212
+ """
213
+ from patchi.core.security.ai_validator import AIValidator
214
+
215
+ if self._ai_validator is None:
216
+ self._ai_validator = AIValidator(self.root, self.config)
217
+
218
+ # Step 1: Initial gating
219
+ gated = [self.gate(cf) for cf in report.findings]
220
+
221
+ # L2 targeted second pass: low confidence + high/critical OR sensitive types
222
+ import os as _os
223
+
224
+ if _os.environ.get("PATCHI_OFFLINE"):
225
+ # Offline honors contract — no AI calls, mark as unverified
226
+ for gf in gated:
227
+ if gf.confidence_score < 0.4 or gf.routing == "ai_analyze":
228
+ gf.routing_reason += " [offline unverified]"
229
+ return self._build_report(gated)
230
+
231
+ sensitive_types = ("secret", "injection", "auth", "cred", "token", "password")
232
+ l2_targets: list = []
233
+ for gf in gated:
234
+ is_low = gf.confidence_score < 0.4 or gf.routing in ("ai_analyze", "human_review")
235
+ sev = getattr(gf.finding.severity, "value", str(gf.finding.severity)).lower()
236
+ is_high = sev in ("high", "critical")
237
+ is_sensitive = any(t in (gf.finding.type or "").lower() for t in sensitive_types)
238
+ if (is_low and is_high) or (is_low and is_sensitive) or gf.routing == "ai_analyze":
239
+ # limit tokens: high/critical low OR sensitive types, plus all ai_analyze
240
+ l2_targets.append(gf)
241
+ # cap to bound cost ~10 Findings
242
+ l2_targets = l2_targets[:10]
243
+ if l2_targets:
244
+ _log.info("L2 AIConfidenceGate validating %d findings (low/high+secret/injection/auth) ...", len(l2_targets))
245
+ for gf in l2_targets:
246
+ result = self._ai_validator.validate(gf.finding, route_context=route_context)
247
+ # Schema: is_true_positive + confidence + explanation already
248
+ if result.confidence >= 0.7 and not result.is_true_positive:
249
+ gf.routing = "discard"
250
+ gf.routing_reason = f"L2 fp ({result.confidence:.0%}): {result.explanation} [verdict=fp exploitability~{1-result.confidence:.1f}]"
251
+ elif result.confidence >= 0.6 and result.is_true_positive:
252
+ gf.routing = "defend"
253
+ gf.routing_reason = f"L2 confirmed ({result.confidence:.0%}): {result.explanation} [exploitability={result.confidence:.1f}]"
254
+ else:
255
+ gf.routing = "human_review"
256
+ gf.routing_reason = f"L2 unverified ({result.confidence:.0%}): {result.explanation}"
257
+ ai_w = min(self.ai_weight, 0.5)
258
+ gf.confidence_score = (1.0 - ai_w) * gf.confidence_score + ai_w * (
259
+ 1.0 - result.confidence if not result.is_true_positive else result.confidence
260
+ )
261
+
262
+ # Step 3: Auto-learn from discarded findings
263
+ self.learn_from_dismissed(self._build_report(gated))
264
+
265
+ return self._build_report(gated)
266
+
267
+ def l2_second_pass(self, report: SecurityReport, route_context: dict | None = None) -> GatedReport:
268
+ """Alias for L2 spec naming — explicit entry point."""
269
+ return self.gate_with_ai_validation(report, route_context=route_context)
270
+
271
+ def _build_report(self, gated_list: list[GatedFinding]) -> GatedReport:
272
+ stats = {
273
+ "total": len(gated_list),
274
+ "defend": sum(1 for g in gated_list if g.routing == "defend"),
275
+ "ai_analyze": sum(1 for g in gated_list if g.routing == "ai_analyze"),
276
+ "human_review": sum(1 for g in gated_list if g.routing == "human_review"),
277
+ "discarded": sum(1 for g in gated_list if g.routing == "discard"),
278
+ }
279
+ return GatedReport(findings=gated_list, stats=stats)
280
+
281
+ def learn_from_dismissed(self, gated: GatedReport) -> int:
282
+ """Auto-record AI-dismissed + discarded findings as known false positives.
283
+
284
+ This closes the noise loop: anything the pipeline rejects once is
285
+ pre-penalized (or dropped, with ``fp_auto_discard``) on every future
286
+ scan instead of being re-surfaced thousands of times.
287
+ """
288
+ entries = []
289
+ for gf in gated.discarded:
290
+ f = gf.finding
291
+ entries.append(
292
+ {
293
+ "file": f.file,
294
+ "type": f.type,
295
+ "line": f.line,
296
+ "reason": gf.routing_reason[:200],
297
+ "source": "pipeline",
298
+ }
299
+ )
300
+ return self.record_false_positives(entries)
301
+
302
+ # ── Scoring internals ───────────────────────────────────────────────────
303
+
304
+ def _compute_score(
305
+ self,
306
+ cf: CorrelatedFinding,
307
+ ai_confidence: float | None = None,
308
+ ) -> float:
309
+ score = 0.0
310
+ f = cf.finding
311
+
312
+ # 1. Method precision — infer from finding metadata
313
+ method = "regex"
314
+ if f.code_snippet and len(f.code_snippet) > 50:
315
+ method = "flow"
316
+ elif f.cwe and f.cwe.strip():
317
+ method = "ast"
318
+ if cf.composite_score >= 70:
319
+ method = "deterministic"
320
+ score += _METHOD_PRECISION.get(method, 0.3)
321
+
322
+ # 2. Severity bonus
323
+ score += _SEVERITY_BONUS.get(
324
+ f.severity.value if hasattr(f.severity, "value") else str(f.severity), 0.0
325
+ )
326
+
327
+ # 3. Multi-agent confirmation
328
+ confirm_count = len(cf.confirmed_by)
329
+ score += min(confirm_count * 0.1, 0.3)
330
+
331
+ # 4. Location precision
332
+ if f.line > 0:
333
+ score += 0.1
334
+
335
+ # 5. Known false-positive penalty
336
+ fp_key = (f.file, f.type, f.line)
337
+ if fp_key in self._known_fps:
338
+ score -= self.fp_penalty
339
+
340
+ # 6. Ambiguity penalty
341
+ if not f.code_snippet:
342
+ score -= 0.2
343
+
344
+ # 7. Heuristic FP detection — catch common false-positive patterns
345
+ from patchi.core.security.ai_validator import heuristic_pre_filter
346
+
347
+ hp = heuristic_pre_filter(f)
348
+ if hp:
349
+ _, penalty = hp
350
+ score -= penalty * 0.5 # partial penalty (full penalty via AIValidator)
351
+
352
+ # 8. Noise penalty — findings the NoiseFilter capped (tests,
353
+ # lockfiles, generated code, docs) carry ``noise_category``.
354
+ # They may legitimately look dangerous; they must never score
355
+ # high enough to auto-defend.
356
+ if getattr(f, "noise_category", None):
357
+ score -= self.noise_penalty
358
+
359
+ # Clamp heuristic score first…
360
+ score = max(0.0, min(1.0, score))
361
+
362
+ # 9. AI confidence calibration (optional): blend model verdict.
363
+ if ai_confidence is not None and self.ai_weight > 0.0:
364
+ w = min(max(self.ai_weight, 0.0), 1.0)
365
+ score = (1.0 - w) * score + w * max(0.0, min(1.0, ai_confidence))
366
+
367
+ return max(0.0, min(1.0, score))
368
+
369
+ def _assign_tier(self, score: float) -> str:
370
+ if score >= 0.7:
371
+ return "high"
372
+ if score >= 0.4:
373
+ return "medium"
374
+ return "low"
375
+
376
+ def _assign_routing(self, tier: str, cf: CorrelatedFinding) -> str:
377
+ confirm_count = len(cf.confirmed_by)
378
+ f = cf.finding
379
+
380
+ # Hard consensus floor: too few agents => drop entirely (noise kill).
381
+ if self.min_agents_to_keep > 0 and confirm_count < self.min_agents_to_keep:
382
+ return "discard"
383
+
384
+ # Known-FP auto-discard: previously rejected => stay rejected.
385
+ if self.fp_auto_discard:
386
+ fp_key = (f.file, f.type, f.line)
387
+ if fp_key in self._known_fps:
388
+ return "discard"
389
+
390
+ if tier == "high":
391
+ # Structural noise block: capped files never auto-defend,
392
+ # no matter how high they score.
393
+ if getattr(cf.finding, "noise_category", None):
394
+ return "ai_analyze"
395
+ # Consensus ceiling: single-agent regex hits can't auto-defend.
396
+ if confirm_count < self.min_agents_for_defend:
397
+ return "ai_analyze"
398
+ return "defend"
399
+ if tier == "medium":
400
+ return "ai_analyze"
401
+
402
+ sev = f.severity.value if hasattr(f.severity, "value") else str(f.severity)
403
+ if sev in ("critical", "high"):
404
+ return "human_review"
405
+ return "discard"
406
+
407
+ def _routing_reason(self, routing: str, score: float, cf: CorrelatedFinding) -> str:
408
+ n_agents = len(cf.confirmed_by)
409
+ reasons = {
410
+ "defend": f"High confidence ({score:.2f}) — {n_agents} agent(s) confirmed",
411
+ "ai_analyze": (
412
+ f"Confidence {score:.2f} with {n_agents} agent(s) — needs AI confirmation "
413
+ f"(consensus floor: {self.min_agents_for_defend})"
414
+ if n_agents < self.min_agents_for_defend
415
+ else f"Medium confidence ({score:.2f}) — needs AI confirmation"
416
+ ),
417
+ "human_review": (
418
+ f"Low confidence ({score:.2f}) but severity is "
419
+ f"{cf.finding.severity} — needs human review"
420
+ ),
421
+ "discard": f"Low confidence ({score:.2f}) or below agent consensus — filtered",
422
+ }
423
+ return reasons.get(routing, f"Routed to {routing} (score={score:.2f})")