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,357 @@
1
+ """
2
+ CDNCacheSecurityAgent — CDN/edge cache security issues.
3
+
4
+ Detects CDN-related security issues:
5
+ - Cache poisoning via unvalidated headers
6
+ - Origin bypass (missing origin access controls)
7
+ - Cache-key normalization flaws
8
+ - Missing cache-control directives
9
+ - CDN configuration misconfigurations (CloudFront, Fastly, etc.)
10
+
11
+ Uses static file analysis and optional external tools (curl).
12
+ Does NOT call AI.
13
+ Does NOT write to disk.
14
+ Does NOT touch the queue.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import re
21
+ import shutil
22
+ import subprocess
23
+ from pathlib import Path
24
+
25
+ from ..agents.base import (
26
+ AgentDomain,
27
+ AgentGroup,
28
+ AgentInput,
29
+ AgentResult,
30
+ AgentStatus,
31
+ BaseAgent,
32
+ Finding,
33
+ Severity,
34
+ make_finding,
35
+ register,
36
+ safe_rglob,
37
+ )
38
+ from ..brain.trace_log import trace_agent
39
+
40
+ _SOURCE_EXTENSIONS = {
41
+ "*.py",
42
+ "*.js",
43
+ "*.jsx",
44
+ "*.ts",
45
+ "*.tsx",
46
+ "*.java",
47
+ "*.php",
48
+ "*.rb",
49
+ "*.go",
50
+ "*.rs",
51
+ "*.cs",
52
+ "*.yaml",
53
+ "*.yml",
54
+ "*.toml",
55
+ "*.json",
56
+ "*.conf",
57
+ "*.cfg",
58
+ "*.nginx",
59
+ }
60
+
61
+ _CDN_CONFIG_NAMES = {
62
+ "cloudfront.yaml",
63
+ "cloudfront.yml",
64
+ "cloudfront.json",
65
+ "fastly.toml",
66
+ "fastly.yaml",
67
+ "fastly.yml",
68
+ "cloudflare.toml",
69
+ "cloudflare.yaml",
70
+ "cloudflare.yml",
71
+ "akamai.json",
72
+ "akamai.yaml",
73
+ "cloudfront-properties.json",
74
+ }
75
+
76
+ _CACHE_CONTROL_PATTERNS = [
77
+ re.compile(r"cache-control", re.IGNORECASE),
78
+ re.compile(r"surrogate-control", re.IGNORECASE),
79
+ re.compile(r"x-cache", re.IGNORECASE),
80
+ re.compile(r"cdn-cache", re.IGNORECASE),
81
+ ]
82
+
83
+ _ORIGIN_BYPASS_PATTERNS = [
84
+ (re.compile(r"X-Forwarded-Host", re.IGNORECASE), "X-Forwarded-Host header manipulation"),
85
+ (re.compile(r"X-Original-URL", re.IGNORECASE), "X-Original-URL header injection"),
86
+ (re.compile(r"X-Rewrite-URL", re.IGNORECASE), "X-Rewrite-URL header injection"),
87
+ (re.compile(r"X-Real-IP", re.IGNORECASE), "X-Real-IP header spoofing"),
88
+ ]
89
+
90
+ _CACHE_KEY_INJECTION_PATTERNS = [
91
+ (
92
+ re.compile(
93
+ r"(?:cache.?key|cacheKey|cache_key)\s*[=:]\s*.*\+|f['\"].*\{.*\}", re.IGNORECASE
94
+ ),
95
+ "Dynamic cache key construction via string concatenation",
96
+ ),
97
+ (re.compile(r"Vary\s*:\s*\*", re.IGNORECASE), "Wildcard Vary header may cause cache poisoning"),
98
+ ]
99
+
100
+
101
+ _log = logging.getLogger("patchi.security.cdn_cache_agent")
102
+
103
+
104
+ @register
105
+ class CDNCacheSecurityAgent(BaseAgent):
106
+ """Agent for detecting CDN and edge cache security issues."""
107
+
108
+ group = AgentGroup.SECURITY
109
+ domain = AgentDomain.SECURITY
110
+ name = "CDNCacheSecurityAgent"
111
+ description = "CDN/edge cache security: cache poisoning, origin bypass, cache-key normalization"
112
+
113
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
114
+ findings: list[Finding] = []
115
+ files_scanned = 0
116
+
117
+ with trace_agent(self.name, inp.root) as trace:
118
+ # ── Static analysis ────────────────────────────────────────────
119
+ findings.extend(self._scan_cdn_configs(inp))
120
+ findings.extend(self._scan_cache_headers(inp))
121
+ findings.extend(self._scan_origin_bypass(inp))
122
+ findings.extend(self._scan_cache_key_issues(inp))
123
+
124
+ for pattern in _SOURCE_EXTENSIONS:
125
+ for fp in safe_rglob(inp.root, pattern):
126
+ if fp.is_file():
127
+ files_scanned += 1
128
+ findings.extend(self._scan_file(fp, inp.root))
129
+
130
+ # ── External tool: curl ────────────────────────────────────────
131
+ if shutil.which("curl"):
132
+ findings.extend(self._run_curl_check(inp))
133
+
134
+ trace.findings = len(findings)
135
+ trace.files_scanned = files_scanned
136
+
137
+ result.status = AgentStatus.SUCCEEDED
138
+ result.findings = findings
139
+ result.data.update(
140
+ {
141
+ "cdn_findings": len(findings),
142
+ "curl_available": shutil.which("curl") is not None,
143
+ }
144
+ )
145
+ return
146
+
147
+ # ── CDN config file scan ───────────────────────────────────────────────
148
+
149
+ def _scan_cdn_configs(self, inp: AgentInput) -> list[Finding]:
150
+ findings: list[Finding] = []
151
+ for fp in safe_rglob(inp.root, "*"):
152
+ if fp.name in _CDN_CONFIG_NAMES:
153
+ rel = fp.relative_to(inp.root).as_posix()
154
+ try:
155
+ content = fp.read_text(encoding="utf-8", errors="replace")
156
+ if (
157
+ "OriginAccessControl" not in content
158
+ and "origin_access" not in content.lower()
159
+ ):
160
+ findings.append(
161
+ make_finding(
162
+ severity=Severity.HIGH,
163
+ file=rel,
164
+ line_start=0,
165
+ title="CDN-01: Missing Origin Access Control",
166
+ description=(
167
+ "CDN configuration file found without origin access control "
168
+ "settings. This may allow direct origin bypass."
169
+ ),
170
+ evidence=content[:200],
171
+ suggestion="Enable Origin Access Identity (CloudFront) or Shield Origin (Fastly).",
172
+ )
173
+ )
174
+ if "signed" not in content.lower() and "token" not in content.lower():
175
+ findings.append(
176
+ make_finding(
177
+ severity=Severity.MEDIUM,
178
+ file=rel,
179
+ line_start=0,
180
+ title="CDN-02: No Signed URLs/Cookies Configured",
181
+ description="CDN distribution has no signed URL or signed cookie configuration.",
182
+ suggestion="Enable signed URLs or signed cookies for authenticated content.",
183
+ )
184
+ )
185
+ except Exception as e:
186
+ _log.warning("CDNCacheSecurityAgent._scan_cdn_configs failed: %s", e)
187
+ return findings
188
+
189
+ # ── Cache header analysis ──────────────────────────────────────────────
190
+
191
+ def _scan_cache_headers(self, inp: AgentInput) -> list[Finding]:
192
+ findings: list[Finding] = []
193
+ header_patterns = [
194
+ ("*.conf", ["nginx", "apache", "httpd"]),
195
+ ("*.cfg", ["nginx", "apache"]),
196
+ ("*.yaml", ["proxy", "upstream", "cache"]),
197
+ ("*.yml", ["proxy", "upstream", "cache"]),
198
+ ]
199
+ for pattern, _context_words in header_patterns:
200
+ for fp in safe_rglob(inp.root, pattern):
201
+ if not fp.is_file():
202
+ continue
203
+ rel = fp.relative_to(inp.root).as_posix()
204
+ try:
205
+ content = fp.read_text(encoding="utf-8", errors="replace")
206
+ lines = content.splitlines()
207
+ for i, line in enumerate(lines, 1):
208
+ for rx in _CACHE_CONTROL_PATTERNS:
209
+ if rx.search(line):
210
+ if "no-store" not in line.lower() and "private" not in line.lower():
211
+ findings.append(
212
+ make_finding(
213
+ severity=Severity.MEDIUM,
214
+ file=rel,
215
+ line_start=i,
216
+ title="CDN-03: Cache-Control May Allow Sensitive Data Caching",
217
+ description=(
218
+ "Cache header found without no-store/private directive. "
219
+ "Sensitive data may be cached at the edge."
220
+ ),
221
+ evidence=line.strip(),
222
+ suggestion="Add 'no-store' or 'private' for sensitive endpoints.",
223
+ )
224
+ )
225
+ break
226
+ except Exception as e:
227
+ _log.warning("CDNCacheSecurityAgent._scan_cache_headers failed: %s", e)
228
+ return findings
229
+
230
+ # ── Origin bypass patterns ─────────────────────────────────────────────
231
+
232
+ def _scan_origin_bypass(self, inp: AgentInput) -> list[Finding]:
233
+ findings: list[Finding] = []
234
+ for pattern in ("*.py", "*.js", "*.ts", "*.java", "*.go", "*.rb", "*.php"):
235
+ for fp in safe_rglob(inp.root, pattern):
236
+ if not fp.is_file():
237
+ continue
238
+ rel = fp.relative_to(inp.root).as_posix()
239
+ try:
240
+ content = fp.read_text(encoding="utf-8", errors="replace")
241
+ lines = content.splitlines()
242
+ for i, line in enumerate(lines, 1):
243
+ for rx, desc in _ORIGIN_BYPASS_PATTERNS:
244
+ if rx.search(line) and "test" not in rel.lower():
245
+ findings.append(
246
+ make_finding(
247
+ severity=Severity.HIGH,
248
+ file=rel,
249
+ line_start=i,
250
+ title="CDN-04: Origin Bypass via Header Injection",
251
+ description=desc,
252
+ evidence=line.strip(),
253
+ suggestion="Validate and strip CDN-injected headers at the origin.",
254
+ )
255
+ )
256
+ except Exception as e:
257
+ _log.warning("CDNCacheSecurityAgent._scan_origin_bypass failed: %s", e)
258
+ return findings
259
+
260
+ # ── Cache-key normalization ────────────────────────────────────────────
261
+
262
+ def _scan_cache_key_issues(self, inp: AgentInput) -> list[Finding]:
263
+ findings: list[Finding] = []
264
+ for pattern in ("*.py", "*.js", "*.ts", "*.go", "*.rb", "*.php", "*.conf"):
265
+ for fp in safe_rglob(inp.root, pattern):
266
+ if not fp.is_file():
267
+ continue
268
+ rel = fp.relative_to(inp.root).as_posix()
269
+ try:
270
+ content = fp.read_text(encoding="utf-8", errors="replace")
271
+ lines = content.splitlines()
272
+ for i, line in enumerate(lines, 1):
273
+ for rx, desc in _CACHE_KEY_INJECTION_PATTERNS:
274
+ if rx.search(line):
275
+ findings.append(
276
+ make_finding(
277
+ severity=Severity.MEDIUM,
278
+ file=rel,
279
+ line_start=i,
280
+ title="CDN-05: Cache Key Injection Risk",
281
+ description=desc,
282
+ evidence=line.strip(),
283
+ suggestion="Normalize cache keys to prevent injection via headers.",
284
+ )
285
+ )
286
+ except Exception as e:
287
+ _log.warning("CDNCacheSecurityAgent._scan_cache_key_issues failed: %s", e)
288
+ return findings
289
+
290
+ # ── General source file scan ───────────────────────────────────────────
291
+
292
+ def _scan_file(self, fp: Path, root: Path) -> list[Finding]:
293
+ findings: list[Finding] = []
294
+ rel = fp.relative_to(root).as_posix()
295
+ try:
296
+ content = fp.read_text(encoding="utf-8", errors="replace")
297
+ # CDN-specific secrets
298
+ secret_patterns = [
299
+ (
300
+ re.compile(
301
+ r"(?:cloudfront|fastly|cloudflare)[_-]?(?:key|token|secret)\s*[:=]\s*['\"][^'\"]{8,}['\"]",
302
+ re.IGNORECASE,
303
+ ),
304
+ "CDN-06: Hardcoded CDN Credential",
305
+ ),
306
+ ]
307
+ lines = content.splitlines()
308
+ for i, line in enumerate(lines, 1):
309
+ for rx, title in secret_patterns:
310
+ if rx.search(line):
311
+ findings.append(
312
+ make_finding(
313
+ severity=Severity.CRITICAL,
314
+ file=rel,
315
+ line_start=i,
316
+ title=title,
317
+ description="Hardcoded CDN credential found in source code.",
318
+ evidence=line.strip()[:120],
319
+ suggestion="Move CDN credentials to environment variables or a secrets manager.",
320
+ )
321
+ )
322
+ except Exception as e:
323
+ _log.warning("CDNCacheSecurityAgent._scan_file failed: %s", e)
324
+ return findings
325
+
326
+ # ── External tool: curl ────────────────────────────────────────────────
327
+
328
+ def _run_curl_check(self, inp: AgentInput) -> list[Finding]:
329
+ """Use curl to probe cache headers if a URL is provided in config."""
330
+ findings: list[Finding] = []
331
+ url = inp.config.get("cdn_probe_url")
332
+ if not url or not isinstance(url, str):
333
+ return findings
334
+
335
+ try:
336
+ proc = subprocess.run(
337
+ ["curl", "-sI", "-m", "10", url],
338
+ capture_output=True,
339
+ text=True,
340
+ timeout=15,
341
+ )
342
+ if proc.returncode == 0:
343
+ headers_lower = proc.stdout.lower()
344
+ if "x-cache:" in headers_lower and "hit" in headers_lower:
345
+ findings.append(
346
+ make_finding(
347
+ severity=Severity.LOW,
348
+ file="(cdn_probe)",
349
+ line_start=0,
350
+ title="CDN-03: Cache Hit Detected on Probe URL",
351
+ description="The probed URL returned a cache HIT. Verify sensitive data is not cached.",
352
+ evidence=proc.stdout[:300],
353
+ )
354
+ )
355
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
356
+ pass
357
+ return findings
@@ -0,0 +1,321 @@
1
+ """
2
+ Exploit Chain Engine — turns isolated findings into attack narratives.
3
+
4
+ A SQL injection in a rarely-hit endpoint is Medium. The same SQL injection
5
+ reachable without authentication, chained past a missing rate limit into an
6
+ unencrypted admin cookie, is Critical. Static scanners see the pieces; this
7
+ module assembles the pieces into the story an attacker would tell.
8
+
9
+ How it works (deterministic, zero AI, bounded):
10
+
11
+ 1. Each Finding becomes a node classified as an ENTRY (exposes attack
12
+ surface: missing auth, open CORS, debug mode...), an EXPLOIT (injection,
13
+ XSS, SSRF...), or an IMPACT amplifier (secrets, weak crypto, sensitive
14
+ data exposure).
15
+ 2. An edge A -> B ("A enables B") is added when the pair matches a chain
16
+ rule AND the findings are co-located (same file) or import-connected
17
+ (B's module imports A's module).
18
+ 3. A chain is any path entry -> ... -> impact of length >= 2.
19
+ 4. Chains are scored by exploitability and sorted worst-first.
20
+
21
+ Severity escalation: a chain's severity is at least HIGH when it links two
22
+ or more confirmed findings — the compound condition is what makes it
23
+ dangerous, even if each piece alone looked Medium.
24
+
25
+ No AI calls. No network. Pure graph analysis over the findings you already have.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ from collections import defaultdict
32
+ from collections.abc import Iterable
33
+ from dataclasses import dataclass, field
34
+ from enum import StrEnum
35
+ from typing import Any
36
+
37
+ _log = logging.getLogger("patchi.security.chain_analyzer")
38
+
39
+
40
+ class NodeRole(StrEnum):
41
+ """What role a finding plays in an attack chain."""
42
+
43
+ ENTRY = "entry" # opens the door
44
+ EXPLOIT = "exploit" # the actual attack step
45
+ IMPACT = "impact" # what the attacker walks away with
46
+ STANDALONE = "standalone" # not part of any known chain pattern
47
+
48
+
49
+ # ── Chain rules ────────────────────────────────────────────────────────────────
50
+ # Each rule: (source_type_pattern, target_type_pattern, edge_label).
51
+ # Type patterns are matched with substring semantics against finding.type so
52
+ # "sqli", "sql_injection", "injection_sql" all match "sql".
53
+
54
+ _CHAIN_RULES: tuple[tuple[str, str, str], ...] = (
55
+ # Entry -> exploit edges
56
+ ("auth", "inject", "unauthenticated input reaches injector"),
57
+ ("auth", "xss", "unauthenticated input reaches XSS sink"),
58
+ ("auth", "ssrf", "unauthenticated request forges SSRF"),
59
+ ("auth", "idor", "no auth boundary protects object access"),
60
+ ("open_redirect", "xss", "redirect target smuggles script"),
61
+ ("cors", "xss", "permissive CORS lets attacker read XSS payload"),
62
+ ("debug", "inject", "debug mode leaks internals that shape payloads"),
63
+ ("rate_limit", "brute", "no throttle permits unlimited attempts"),
64
+ ("rate_limit", "inject", "no throttle permits payload fuzzing"),
65
+ # Exploit -> impact edges
66
+ ("inject", "secret", "injection exfiltrates stored secrets"),
67
+ ("inject", "sensitive_data", "injection dumps sensitive tables"),
68
+ ("ssrf", "secret", "SSRF reaches internal metadata/credential service"),
69
+ ("idor", "sensitive_data", "object access crosses ownership boundary"),
70
+ ("xss", "session", "script lifts session cookies/tokens"),
71
+ ("xss", "secret", "script scrapes secrets rendered into DOM"),
72
+ ("deserialization", "rce", "deserialization gadget yields code execution"),
73
+ ("command_injection", "secret", "command execution reads env/key material"),
74
+ ("path_traversal", "secret", "traversal reads key/config files"),
75
+ ("weak_crypto", "session", "forgeable tokens defeat session integrity"),
76
+ ("hardcoded_secret", "secret", "committed key grants direct access"),
77
+ # Amplifier edges
78
+ ("jwt", "auth", "token flaw defeats the auth layer itself"),
79
+ ("csrf", "state_change", "forged request drives privileged mutation"),
80
+ )
81
+
82
+
83
+ def _matches(pattern: str, finding_type: str) -> bool:
84
+ return pattern in finding_type.lower()
85
+
86
+
87
+ def classify(finding_type: str) -> NodeRole:
88
+ """Classify a finding type into its chain role."""
89
+ t = finding_type.lower()
90
+ if any(k in t for k in ("auth", "cors", "debug", "rate_limit", "redirect", "open")):
91
+ return NodeRole.ENTRY
92
+ if any(
93
+ k in t
94
+ for k in (
95
+ "inject",
96
+ "xss",
97
+ "ssrf",
98
+ "idor",
99
+ "deserial",
100
+ "traversal",
101
+ "rce",
102
+ "sqli",
103
+ "xxe",
104
+ )
105
+ ):
106
+ return NodeRole.EXPLOIT
107
+ if any(
108
+ k in t
109
+ for k in (
110
+ "secret",
111
+ "crypto",
112
+ "sensitive",
113
+ "session",
114
+ "jwt",
115
+ "hardcoded",
116
+ "password",
117
+ "key",
118
+ )
119
+ ):
120
+ return NodeRole.IMPACT
121
+ return NodeRole.STANDALONE
122
+
123
+
124
+ @dataclass
125
+ class ChainNode:
126
+ """One finding participating in a chain."""
127
+
128
+ finding: Any # patchi.core.agents.base.Finding
129
+ role: NodeRole
130
+
131
+ @property
132
+ def key(self) -> tuple:
133
+ f = self.finding
134
+ return (f.file, f.line, f.type)
135
+
136
+ @property
137
+ def severity(self) -> str:
138
+ return self.finding.severity.value
139
+
140
+
141
+ @dataclass
142
+ class Chain:
143
+ """An ordered attack path: entry -> exploits -> impact."""
144
+
145
+ nodes: list[ChainNode]
146
+ edges: list[str] = field(default_factory=list)
147
+
148
+ @property
149
+ def length(self) -> int:
150
+ return len(self.nodes)
151
+
152
+ @property
153
+ def severity(self) -> str:
154
+ """Escalated severity: multi-step chains are at least HIGH."""
155
+ order = ["info", "low", "medium", "high", "critical"]
156
+ worst = max(
157
+ (order.index(n.severity) for n in self.nodes if n.severity in order),
158
+ default=0,
159
+ )
160
+ if self.length >= 2:
161
+ worst = max(worst, order.index("high"))
162
+ if self.length >= 3:
163
+ worst = max(worst, order.index("critical"))
164
+ return order[worst]
165
+
166
+ @property
167
+ def score(self) -> float:
168
+ """Exploitability score 0-100: depth × severity × confirmation spread."""
169
+ sev_weight = {"info": 5, "low": 15, "medium": 35, "high": 70, "critical": 90}
170
+ base = max((sev_weight.get(n.severity, 20) for n in self.nodes), default=20)
171
+ depth_bonus = min((self.length - 1) * 8, 24)
172
+ agents = {n.finding.agent for n in self.nodes}
173
+ confirm_bonus = min(len(agents) * 4, 12)
174
+ return min(base + depth_bonus + confirm_bonus, 100)
175
+
176
+ @property
177
+ def narrative(self) -> str:
178
+ """Human-readable attack story assembled from the edge labels."""
179
+ parts = [f"[{n.finding.type}] {n.finding.file}:{n.finding.line}" for n in self.nodes]
180
+ story = " -> ".join(parts)
181
+ if self.edges:
182
+ story += " | " + "; ".join(self.edges)
183
+ return story
184
+
185
+ def to_dict(self) -> dict:
186
+ return {
187
+ "severity": self.severity,
188
+ "score": round(self.score, 1),
189
+ "length": self.length,
190
+ "narrative": self.narrative,
191
+ "steps": [
192
+ {
193
+ "role": n.role.value,
194
+ "type": n.finding.type,
195
+ "file": n.finding.file,
196
+ "line": n.finding.line,
197
+ "severity": n.severity,
198
+ "agent": n.finding.agent,
199
+ }
200
+ for n in self.nodes
201
+ ],
202
+ }
203
+
204
+
205
+ class ChainAnalyzer:
206
+ """Builds exploit chains from a set of findings."""
207
+
208
+ def __init__(
209
+ self,
210
+ findings: Iterable[Any],
211
+ import_edges: dict[str, set[str]] | None = None,
212
+ ):
213
+ """
214
+ Args:
215
+ findings: patchi.core.agents.base.Finding objects (or duck-typed:
216
+ need .type/.file/.line/.severity/.agent).
217
+ import_edges: optional {importer_file: {imported_files...}} map.
218
+ Co-location alone is enough to link findings; import edges let
219
+ chains cross module boundaries (route -> service -> repo).
220
+ """
221
+ self.import_edges = import_edges or {}
222
+ self.nodes: list[ChainNode] = []
223
+ for f in findings:
224
+ role = classify(getattr(f, "type", ""))
225
+ if role is not NodeRole.STANDALONE:
226
+ self.nodes.append(ChainNode(finding=f, role=role))
227
+ self._index()
228
+
229
+ def _index(self) -> None:
230
+ self.by_file: dict[str, list[ChainNode]] = defaultdict(list)
231
+ for n in self.nodes:
232
+ self.by_file[n.finding.file].append(n)
233
+
234
+ def _connected(self, a: ChainNode, b: ChainNode) -> bool:
235
+ """True when b is reachable from a's location (same file or import path)."""
236
+ fa, fb = a.finding.file, b.finding.file
237
+ if fa == fb:
238
+ return True
239
+ return fb in self.import_edges.get(fa, set())
240
+
241
+ def _edge_label(self, src: ChainNode, dst: ChainNode) -> str | None:
242
+ st, dt = src.finding.type.lower(), dst.finding.type.lower()
243
+ for sp, dp, label in _CHAIN_RULES:
244
+ if _matches(sp, st) and _matches(dp, dt):
245
+ return label
246
+ return None
247
+
248
+ def _adjacency(self) -> dict[int, list[tuple[int, str]]]:
249
+ adj: dict[int, list[tuple[int, str]]] = defaultdict(list)
250
+ for i, src in enumerate(self.nodes):
251
+ for j, dst in enumerate(self.nodes):
252
+ if i == j:
253
+ continue
254
+ # Enforce direction: entries start chains, impacts end them.
255
+ order = {NodeRole.ENTRY: 0, NodeRole.EXPLOIT: 1, NodeRole.IMPACT: 2}
256
+ if order[src.role] > order[dst.role]:
257
+ continue
258
+ if not self._connected(src, dst):
259
+ continue
260
+ label = self._edge_label(src, dst)
261
+ if label:
262
+ adj[i].append((j, label))
263
+ return adj
264
+
265
+ def find_chains(self, max_chains: int = 50, max_depth: int = 5) -> list[Chain]:
266
+ """Return all entry->...->impact chains, worst-first, capped."""
267
+ adj = self._adjacency()
268
+ chains: list[Chain] = []
269
+
270
+ def dfs(idx: int, path: list[int], labels: list[str], seen: set[int]) -> None:
271
+ if len(chains) >= max_chains * 4 or len(path) > max_depth:
272
+ return
273
+ node = self.nodes[idx]
274
+ # Entry->exploit pairs are already actionable ("unauthenticated
275
+ # SQL injection"); record them AND keep walking toward impact.
276
+ if len(path) >= 2 and node.role in (NodeRole.EXPLOIT, NodeRole.IMPACT):
277
+ chains.append(Chain(nodes=[self.nodes[i] for i in path], edges=list(labels)))
278
+ if node.role == NodeRole.IMPACT:
279
+ return # impact terminates a chain
280
+ for j, label in adj.get(idx, []):
281
+ if j in seen:
282
+ continue
283
+ dfs(j, path + [j], labels + [label], seen | {j})
284
+
285
+ for i, n in enumerate(self.nodes):
286
+ if n.role == NodeRole.ENTRY:
287
+ dfs(i, [i], [], {i})
288
+ elif n.role == NodeRole.EXPLOIT:
289
+ # Exploit-to-impact pairs also count (2-node chains).
290
+ for j, label in adj.get(i, []):
291
+ if self.nodes[j].role == NodeRole.IMPACT:
292
+ chains.append(
293
+ Chain(
294
+ nodes=[self.nodes[i], self.nodes[j]],
295
+ edges=[label],
296
+ )
297
+ )
298
+
299
+ chains.sort(key=lambda c: c.score, reverse=True)
300
+ # Dedup identical narratives (same steps via different DFS orders)
301
+ seen_narratives: set[str] = set()
302
+ unique: list[Chain] = []
303
+ for c in chains:
304
+ sig = tuple(n.key for n in c.nodes)
305
+ if sig not in seen_narratives:
306
+ seen_narratives.add(sig)
307
+ unique.append(c)
308
+ return unique[:max_chains]
309
+
310
+ def summary(self) -> dict[str, Any]:
311
+ """Aggregate stats for reporting / web panels."""
312
+ chains = self.find_chains()
313
+ sev_counts: dict[str, int] = defaultdict(int)
314
+ for c in chains:
315
+ sev_counts[c.severity] += 1
316
+ return {
317
+ "findings_analyzed": len(self.nodes),
318
+ "chains_found": len(chains),
319
+ "by_severity": dict(sev_counts),
320
+ "worst": chains[0].to_dict() if chains else None,
321
+ }