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,156 @@
1
+ """
2
+ ScanBus §MASTER_ROADMAP Scan Bus — FileCorpus parse-once + shards + FindingBus.
3
+
4
+ Shards FileInfos for parallel agents without re-walking filesystem.
5
+ FindingBus is thread-safe queue for findings (replaces per-agent list merging).
6
+ QueueRunner batches agent classes across shard workers sharing one corpus.
7
+
8
+ File-sharding per agent activates only once agents honor `AgentInput.scope`
9
+ (4 do today: sast, security_config, unit_test, live_v2) — until then every
10
+ agent receives the full input and the bus is the single merge point.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import queue
17
+ import threading
18
+ from collections.abc import Callable
19
+ from concurrent.futures import ThreadPoolExecutor, as_completed
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from patchi.core.brain.file_corpus import FileCorpus
25
+ from patchi.core.brain.scanner import FileInfo
26
+
27
+
28
+ @dataclass
29
+ class ScanShard:
30
+ idx: int
31
+ files: list[FileInfo]
32
+ total_shards: int
33
+
34
+
35
+ class ScanBus:
36
+ """Shard-aware FileCorpus wrapper. Build once, reuse across agents."""
37
+
38
+ def __init__(self, root: Path, shard_count: int = 4):
39
+ self.root = root
40
+ self.corpus = FileCorpus(root)
41
+ self.shard_count = max(1, shard_count)
42
+ self._shards: list[ScanShard] | None = None
43
+
44
+ def build(self) -> None:
45
+ # Force corpus build
46
+ _ = self.corpus.entries
47
+
48
+ def shards(self, file_infos: list[FileInfo] | None = None) -> list[ScanShard]:
49
+ if file_infos is None:
50
+ # Build from corpus if available, else scan
51
+ from patchi.core.brain.scanner import FileScanner
52
+ file_infos = FileScanner(self.root, corpus=self.corpus).scan()
53
+ n = self.shard_count
54
+ # shard by hash of path for even distribution
55
+ buckets: list[list[FileInfo]] = [[] for _ in range(n)]
56
+ # Deterministic sharding via MD5 (Python's hash() is randomized across processes)
57
+ for fi in file_infos:
58
+ digest = hashlib.md5(fi.path.encode()).digest()
59
+ shard_id = int.from_bytes(digest[:4], "little") % n
60
+ buckets[shard_id].append(fi)
61
+ return [ScanShard(idx=i, files=buckets[i], total_shards=n) for i in range(n)]
62
+
63
+ def shard_paths(self, file_infos: list[FileInfo] | None = None) -> list[list[str]]:
64
+ """Relative path lists per shard — future per-agent scope input."""
65
+ return [[fi.path for fi in s.files] for s in self.shards(file_infos)]
66
+
67
+
68
+ @dataclass
69
+ class FindingBus:
70
+ """Thread-safe bus for findings. Agents push, coordinator drains."""
71
+
72
+ _q: queue.Queue = field(default_factory=queue.Queue)
73
+ _lock: threading.Lock = field(default_factory=threading.Lock)
74
+ _count: int = 0
75
+
76
+ def publish(self, finding: Any) -> None:
77
+ self._q.put(finding)
78
+ with self._lock:
79
+ self._count += 1
80
+
81
+ def drain(self) -> list[Any]:
82
+ out = []
83
+ while not self._q.empty():
84
+ try:
85
+ out.append(self._q.get_nowait())
86
+ except queue.Empty:
87
+ break
88
+ return out
89
+
90
+ @property
91
+ def count(self) -> int:
92
+ with self._lock:
93
+ return self._count
94
+
95
+
96
+ class QueueRunner:
97
+ """Shard-worked parallel runner over agent classes.
98
+
99
+ One ScanBus (single FileCorpus build) is shared by all workers. Agent
100
+ classes are fanned out over `shard_count` threads with the same per-agent
101
+ timeout semantics as the coordinator's thread pool. Every finished
102
+ result's findings are auto-published to the FindingBus — agents never
103
+ touch the bus directly — so `drain()` is the single merge point.
104
+ """
105
+
106
+ def __init__(self, bus: ScanBus, finding_bus: FindingBus | None = None):
107
+ self.scan_bus = bus
108
+ self.finding_bus = finding_bus or FindingBus()
109
+ self.scan_bus.build()
110
+
111
+ @property
112
+ def shards(self) -> list[ScanShard]:
113
+ return self.scan_bus.shards()
114
+
115
+ def run(
116
+ self,
117
+ agent_classes: list,
118
+ run_one: Callable[[Any], Any],
119
+ on_done: Callable[[Any], None],
120
+ default_timeout: int = 120,
121
+ ) -> list:
122
+ """Run every agent class, publish findings, call on_done per result."""
123
+ from concurrent.futures import Future
124
+
125
+ from patchi.core.agents.base import AgentGroup, AgentResult, AgentStatus
126
+
127
+ results: list = []
128
+ futures: dict[Future, str] = {}
129
+ timeout_map: dict[Future, int] = {}
130
+ with ThreadPoolExecutor(max_workers=self.scan_bus.shard_count) as ex:
131
+ for cls in agent_classes:
132
+ fut = ex.submit(self._run_and_publish, run_one, cls)
133
+ futures[fut] = getattr(cls, "name", str(cls))
134
+ timeout_map[fut] = getattr(cls, "timeout", default_timeout)
135
+ for fut in as_completed(futures):
136
+ try:
137
+ result = fut.result(timeout=timeout_map.get(fut, default_timeout))
138
+ except Exception as e:
139
+ result = AgentResult(
140
+ agent_name=futures[fut],
141
+ agent_group=AgentGroup.SCANNER,
142
+ status=AgentStatus.FAILED,
143
+ errors=[f"QueueRunner caught: {e}"],
144
+ )
145
+ on_done(result)
146
+ results.append(result)
147
+ return results
148
+
149
+ def _run_and_publish(self, run_one: Callable[[Any], Any], cls: Any) -> Any:
150
+ result = run_one(cls)
151
+ for f in getattr(result, "findings", []):
152
+ self.finding_bus.publish(f)
153
+ return result
154
+
155
+ def drain(self) -> list[Any]:
156
+ return self.finding_bus.drain()
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,462 @@
1
+ """
2
+ AI Validator — the core harness engine that eliminates false positives.
3
+
4
+ Instead of asking "is this a vulnerability?" (which produces noise), the
5
+ validator asks three chained questions:
6
+
7
+ 1. CODE READING: "Read the surrounding function. What does this code actually do?"
8
+ 2. DATA FLOW: "Where does the tainted input come from and where does it go?"
9
+ 3. EXPLOITABILITY: "Given the app context, is an attacker able to reach this?"
10
+
11
+ Each question builds on the previous answer, creating a reasoning chain that
12
+ produces a structured verdict with confidence and evidence.
13
+
14
+ The output is deterministic enough for the ConfidenceGate to score, but rich
15
+ enough for human review when needed.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import logging
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ _log = logging.getLogger("patchi.security.ai_validator")
27
+
28
+ # ── Result types ─────────────────────────────────────────────────────────────
29
+
30
+
31
+ @dataclass
32
+ class ReasoningStep:
33
+ """One step in the chain-of-reasoning."""
34
+
35
+ question: str
36
+ analysis: str
37
+ confidence: float # 0.0 - 1.0
38
+
39
+ def to_dict(self) -> dict:
40
+ return {
41
+ "question": self.question,
42
+ "analysis": self.analysis,
43
+ "confidence": self.confidence,
44
+ }
45
+
46
+
47
+ @dataclass
48
+ class ValidationResult:
49
+ """Full validation result with chain-of-reasoning."""
50
+
51
+ finding_id: str
52
+ is_true_positive: bool
53
+ confidence: float # 0.0 - 1.0
54
+ reasoning: list[ReasoningStep] = field(default_factory=list)
55
+ explanation: str = ""
56
+ fix_suggestion: str = ""
57
+ test_suggestion: str = ""
58
+ severity_adjustment: str = "" # "increase", "decrease", ""
59
+ tokens_used: int = 0
60
+ model: str = ""
61
+
62
+ @property
63
+ def summary(self) -> str:
64
+ verdict = "TRUE POSITIVE" if self.is_true_positive else "FALSE POSITIVE"
65
+ return f"{verdict} (confidence {self.confidence:.0%})"
66
+
67
+ def to_dict(self) -> dict:
68
+ return {
69
+ "finding_id": self.finding_id,
70
+ "is_true_positive": self.is_true_positive,
71
+ "confidence": self.confidence,
72
+ "reasoning": [s.to_dict() for s in self.reasoning],
73
+ "explanation": self.explanation,
74
+ "fix_suggestion": self.fix_suggestion,
75
+ "test_suggestion": self.test_suggestion,
76
+ "severity_adjustment": self.severity_adjustment,
77
+ "tokens_used": self.tokens_used,
78
+ "model": self.model,
79
+ }
80
+
81
+
82
+ # ── Heuristic pre-filters (no AI needed) ─────────────────────────────────────
83
+
84
+ _FP_PATTERNS: list[tuple[str, str, float]] = [
85
+ # (file pattern, finding type, confidence_penalty)
86
+ (".lock", "dependency", 0.8),
87
+ ("package-lock.json", "dependency", 0.9),
88
+ ("yarn.lock", "dependency", 0.9),
89
+ ("Pipfile.lock", "dependency", 0.9),
90
+ (".min.js", "code_quality", 0.7),
91
+ (".min.css", "code_quality", 0.7),
92
+ ("__pycache__", "code_quality", 0.9),
93
+ (".generated.", "code_quality", 0.8),
94
+ (".pb.", "code_quality", 0.7),
95
+ ("_pb2.py", "code_quality", 0.8),
96
+ (".proto", "code_quality", 0.6),
97
+ ]
98
+
99
+ _TEST_PATTERNS = [
100
+ "test_",
101
+ "_test.",
102
+ "spec_",
103
+ "_spec.",
104
+ "tests/",
105
+ "test/",
106
+ "__tests__/",
107
+ "fixture",
108
+ "mock_",
109
+ "fake_",
110
+ "conftest",
111
+ ]
112
+
113
+ _GENERATED_MARKERS = [
114
+ "generated by",
115
+ "auto-generated",
116
+ "auto generated",
117
+ "do not edit",
118
+ "do not modify",
119
+ "autogenerated",
120
+ "@generated",
121
+ "# noqa",
122
+ "// generated",
123
+ "code-generated",
124
+ "machine-generated",
125
+ ]
126
+
127
+
128
+ def heuristic_pre_filter(finding: Any) -> tuple[str, float] | None:
129
+ """Quick heuristic check — returns (reason, penalty) if likely FP, None otherwise.
130
+
131
+ This runs BEFORE the AI validator to skip obviously non-exploitable
132
+ findings, saving tokens and improving signal.
133
+ """
134
+ ffile = getattr(finding, "file", "") or ""
135
+ ftype = getattr(finding, "type", "") or ""
136
+ snippet = getattr(finding, "code_snippet", "") or ""
137
+
138
+ # 1. File pattern FP
139
+ ffile_lower = ffile.lower()
140
+ for pattern, ftype_match, penalty in _FP_PATTERNS:
141
+ if pattern in ffile_lower and (not ftype_match or ftype_match in ftype.lower()):
142
+ return f"File pattern match: {pattern}", penalty
143
+
144
+ # 2. Test file FP
145
+ if any(tp in ffile_lower for tp in _TEST_PATTERNS):
146
+ # Test files with injection findings are often intentional test payloads
147
+ if any(kw in ftype.lower() for kw in ["injection", "xss", "command", "traversal"]):
148
+ return "Test file with injection pattern (likely test payload)", 0.6
149
+
150
+ # 3. Generated code FP
151
+ snippet_lower = snippet.lower()
152
+ if any(marker in snippet_lower for marker in _GENERATED_MARKERS):
153
+ return "Generated code marker detected", 0.7
154
+
155
+ # 4. TODO/FIXME with security mention = not a real vuln yet
156
+ if "todo" in snippet_lower and any(
157
+ kw in ftype.lower() for kw in ["security", "auth", "crypto"]
158
+ ):
159
+ return "TODO/FIXME marker — not implemented yet", 0.5
160
+
161
+ return None
162
+
163
+
164
+ # ── AI Validator ──────────────────────────────────────────────────────────────
165
+
166
+ _SYSTEM_PROMPT = """\
167
+ You are Patchi's security harness validator. You receive a security finding with its full file context and must determine if it is a TRUE POSITIVE or FALSE POSITIVE.
168
+
169
+ You must produce a chain-of-reasoning in 3 steps:
170
+
171
+ STEP 1 — CODE READING:
172
+ Read the function/method containing the finding. What does the code actually do? Is the pattern suspicious or benign in context?
173
+
174
+ STEP 2 — DATA FLOW:
175
+ Trace the data flow. Where does user-controlled input come from? Is there any sanitization between source and sink? Is the path realistic for an attacker?
176
+
177
+ STEP 3 — EXPLOITABILITY:
178
+ Given the application context (routes, middleware, auth), can an attacker actually reach this code? What is the real-world impact?
179
+
180
+ After the 3 steps, output a structured JSON verdict:
181
+ {
182
+ "is_true_positive": bool,
183
+ "confidence": float (0.0-1.0),
184
+ "reasoning": [
185
+ {"question": "...", "analysis": "...", "confidence": float},
186
+ {"question": "...", "analysis": "...", "confidence": float},
187
+ {"question": "...", "analysis": "...", "confidence": float}
188
+ ],
189
+ "explanation": "one-sentence verdict summary",
190
+ "fix_suggestion": "concrete fix code or empty string",
191
+ "test_suggestion": "test case to verify fix or empty string",
192
+ "severity_adjustment": "increase" | "decrease" | ""
193
+ }
194
+
195
+ Rules:
196
+ - Be CONSERVATIVE: if you are not sure, mark as true positive with lower confidence
197
+ - Never dismiss a real vulnerability to reduce noise
198
+ - Always explain WHY it is/isn't exploitable
199
+ - Generate minimal, targeted fixes (not architectural rewrites)
200
+ - The fix must preserve existing functionality
201
+ """
202
+
203
+ _CACHE_TTL = 86400 # 24h
204
+
205
+
206
+ class AIValidator:
207
+ """AI-powered true/false positive validator with chain-of-reasoning.
208
+
209
+ Usage::
210
+
211
+ validator = AIValidator(root)
212
+ result = validator.validate(finding, file_content="...")
213
+ if result.confidence >= 0.8 and not result.is_true_positive:
214
+ # High-confidence false positive — safe to discard
215
+ pass
216
+ """
217
+
218
+ def __init__(self, root: Path, config: dict | None = None):
219
+ self.root = root
220
+ self.config = config or {}
221
+ self._cache: dict[str, tuple[float, ValidationResult]] = {}
222
+ self._stats = {"validated": 0, "cached": 0, "heuristic_skip": 0}
223
+
224
+ def validate(
225
+ self,
226
+ finding: Any,
227
+ file_content: str | None = None,
228
+ route_context: dict | None = None,
229
+ use_heuristics: bool = True,
230
+ ) -> ValidationResult:
231
+ """Validate a single finding.
232
+
233
+ Args:
234
+ finding: A Finding or GatedFinding with .file, .line, .type, .message,
235
+ .code_snippet, .severity, .cwe attributes.
236
+ file_content: Full source file content (read if not provided).
237
+ route_context: Dict with 'routes', 'middleware', 'auth' for context.
238
+ use_heuristics: Run heuristic pre-filter before AI.
239
+
240
+ Returns:
241
+ ValidationResult with chain-of-reasoning.
242
+ """
243
+ fid = self._finding_id(finding)
244
+ self._stats["validated"] += 1
245
+
246
+ # Check cache
247
+ if fid in self._cache:
248
+ ts, cached = self._cache[fid]
249
+ import time
250
+
251
+ if time.time() - ts < _CACHE_TTL:
252
+ self._stats["cached"] += 1
253
+ return cached
254
+
255
+ # Heuristic pre-filter
256
+ if use_heuristics:
257
+ hp = heuristic_pre_filter(finding)
258
+ if hp:
259
+ reason, penalty = hp
260
+ sev = getattr(finding, "severity", None)
261
+ sev_val = sev.value if hasattr(sev, "value") else str(sev)
262
+ # Only auto-discard if severity is low AND penalty is high
263
+ if penalty >= 0.7 and sev_val in ("info", "low"):
264
+ result = ValidationResult(
265
+ finding_id=fid,
266
+ is_true_positive=False,
267
+ confidence=max(0.1, 1.0 - penalty),
268
+ explanation=f"Heuristic filter: {reason}",
269
+ )
270
+ self._cache[fid] = (self._now(), result)
271
+ self._stats["heuristic_skip"] += 1
272
+ return result
273
+
274
+ # Read file content if not provided
275
+ if file_content is None:
276
+ file_content = self._read_file(getattr(finding, "file", ""))
277
+
278
+ # Build the 3-step prompt
279
+ prompt = self._build_prompt(finding, file_content, route_context)
280
+
281
+ # Call AI
282
+ result = self._call_ai(fid, prompt)
283
+ self._cache[fid] = (self._now(), result)
284
+ return result
285
+
286
+ def validate_batch(
287
+ self,
288
+ findings: list,
289
+ use_heuristics: bool = True,
290
+ ) -> list[ValidationResult]:
291
+ """Validate a batch of findings, using cache and heuristics."""
292
+ return [self.validate(f, use_heuristics=use_heuristics) for f in findings]
293
+
294
+ def get_stats(self) -> dict:
295
+ return dict(self._stats)
296
+
297
+ # ── Internal ──────────────────────────────────────────────────────────────
298
+
299
+ def _finding_id(self, finding: Any) -> str:
300
+ raw = (
301
+ f"{getattr(finding, 'file', '')}:"
302
+ f"{getattr(finding, 'line', 0)}:"
303
+ f"{getattr(finding, 'type', '')}:"
304
+ f"{getattr(finding, 'message', '')[:80]}"
305
+ )
306
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]
307
+
308
+ def _read_file(self, filepath: str) -> str:
309
+ if not filepath:
310
+ return ""
311
+ try:
312
+ full = self.root / filepath
313
+ if full.is_file():
314
+ return full.read_text(encoding="utf-8", errors="replace")[:8000]
315
+ except Exception as _exc:
316
+ _log.warning('_read_file failed: %s', _exc)
317
+ return ""
318
+
319
+ def _build_prompt(
320
+ self,
321
+ finding: Any,
322
+ file_content: str,
323
+ route_context: dict | None,
324
+ ) -> str:
325
+ ffile = getattr(finding, "file", "")
326
+ fline = getattr(finding, "line", 0)
327
+ ftype = getattr(finding, "type", "")
328
+ message = getattr(finding, "message", "")
329
+ snippet = getattr(finding, "code_snippet", "")
330
+ severity = getattr(finding, "severity", "")
331
+ sev_val = severity.value if hasattr(severity, "value") else str(severity)
332
+ cwe = getattr(finding, "cwe", "")
333
+ confirmed_by = getattr(finding, "confirmed_by", [])
334
+
335
+ # Extract the function around the finding line
336
+ context_block = self._extract_context_block(file_content, fline)
337
+
338
+ parts = [
339
+ f"FINDING: {ftype} at {ffile}:{fline}",
340
+ f"Severity: {sev_val}",
341
+ f"CWE: {cwe}" if cwe else "",
342
+ f"Message: {message}",
343
+ f"Confirmed by: {', '.join(confirmed_by) if confirmed_by else 'N/A'}",
344
+ "",
345
+ "--- CODE SNIPPET (from finding) ---",
346
+ snippet if snippet else "(no snippet available)",
347
+ "",
348
+ f"--- SURROUNDING FUNCTION (lines {max(1, fline - 20)}-{fline + 20}) ---",
349
+ context_block if context_block else "(could not extract context)",
350
+ ]
351
+
352
+ if route_context:
353
+ routes = route_context.get("routes", [])
354
+ auth = route_context.get("auth", "")
355
+ if routes:
356
+ parts.append(f"\n--- ROUTE CONTEXT ---\nAuth: {auth}")
357
+ for r in routes[:5]:
358
+ if isinstance(r, dict):
359
+ parts.append(f" {r.get('method', '?')} {r.get('path', '?')}")
360
+
361
+ parts.append("\nAnalyze this finding through the 3-step reasoning chain.")
362
+ return "\n".join(p for p in parts if p)
363
+
364
+ def _extract_context_block(self, file_content: str, line: int) -> str:
365
+ """Extract the function/block surrounding the finding line."""
366
+ if not file_content or line <= 0:
367
+ return ""
368
+ lines = file_content.splitlines()
369
+ if line > len(lines):
370
+ return ""
371
+
372
+ # Find function start (go up to find `def` or `function` or `class`)
373
+ start = max(0, line - 30)
374
+ for i in range(line - 1, max(0, line - 40), -1):
375
+ line_text = lines[i].strip()
376
+ if (
377
+ line_text.startswith("def ")
378
+ or line_text.startswith("function ")
379
+ or line_text.startswith("class ")
380
+ ):
381
+ start = i
382
+ break
383
+ if line_text.startswith("@") and i + 1 < len(lines):
384
+ start = i
385
+ break
386
+
387
+ # Find function end (go down to find next def/class at same indent)
388
+ end = min(len(lines), line + 30)
389
+ base_indent = (
390
+ len(lines[line - 1]) - len(lines[line - 1].lstrip()) if line <= len(lines) else 0
391
+ )
392
+ for i in range(line, min(len(lines), line + 50)):
393
+ cur_line = lines[i]
394
+ if cur_line.strip() and not cur_line.strip().startswith("#"):
395
+ indent = len(cur_line) - len(cur_line.lstrip())
396
+ if indent <= base_indent and (
397
+ cur_line.strip().startswith("def ")
398
+ or cur_line.strip().startswith("class ")
399
+ or cur_line.strip().startswith("async def ")
400
+ ):
401
+ end = i
402
+ break
403
+
404
+ # Format with line numbers
405
+ block_lines = lines[start:end]
406
+ formatted = []
407
+ for i, line_text in enumerate(block_lines, start=start + 1):
408
+ marker = " >>>" if i == line else " "
409
+ formatted.append(f"{i:4d}{marker} {line_text}")
410
+ return "\n".join(formatted)
411
+
412
+ def _call_ai(self, fid: str, prompt: str) -> ValidationResult:
413
+ """Make the AI call and parse the structured response."""
414
+ try:
415
+ from patchi.core.ai.client import call_ai_structured
416
+
417
+ response = call_ai_structured(
418
+ prompt=prompt,
419
+ system=_SYSTEM_PROMPT,
420
+ output_model=dict,
421
+ )
422
+
423
+ if response and isinstance(response, dict):
424
+ reasoning_steps = []
425
+ for step in response.get("reasoning", []):
426
+ if isinstance(step, dict):
427
+ reasoning_steps.append(
428
+ ReasoningStep(
429
+ question=step.get("question", ""),
430
+ analysis=step.get("analysis", ""),
431
+ confidence=float(step.get("confidence", 0.5)),
432
+ )
433
+ )
434
+
435
+ return ValidationResult(
436
+ finding_id=fid,
437
+ is_true_positive=bool(response.get("is_true_positive", True)),
438
+ confidence=float(response.get("confidence", 0.5)),
439
+ reasoning=reasoning_steps,
440
+ explanation=response.get("explanation", ""),
441
+ fix_suggestion=response.get("fix_suggestion", ""),
442
+ test_suggestion=response.get("test_suggestion", ""),
443
+ severity_adjustment=response.get("severity_adjustment", ""),
444
+ tokens_used=int(response.get("tokens_used", 0)),
445
+ model=response.get("model", "unknown"),
446
+ )
447
+ except Exception as e:
448
+ _log.debug("AIValidator._call_ai failed: %s", e)
449
+
450
+ # Offline fallback — assume true positive with low confidence
451
+ return ValidationResult(
452
+ finding_id=fid,
453
+ is_true_positive=True,
454
+ confidence=0.3,
455
+ explanation="AI unavailable — assumed true positive for safety",
456
+ )
457
+
458
+ @staticmethod
459
+ def _now() -> float:
460
+ import time
461
+
462
+ return time.time()