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,1142 @@
1
+ """
2
+ Red Team Engine — Orchestrates attack simulations and auto-fix verification.
3
+
4
+ This is the core engine that:
5
+ 1. Loads attack scenarios from YAML
6
+ 2. Selects relevant scenarios based on project context
7
+ 3. Executes attacks safely (with safe_mode)
8
+ 4. Generates findings with exploit evidence
9
+ 5. Triggers auto-fix generation
10
+ 6. Verifies fixes by re-running attacks
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import logging
17
+ import time
18
+ import uuid
19
+ from collections.abc import Callable
20
+ from dataclasses import dataclass, field
21
+ from datetime import UTC, datetime
22
+ from pathlib import Path
23
+
24
+ import yaml
25
+
26
+ from patchi.core.agents.base import (
27
+ AgentGroup,
28
+ AgentInput,
29
+ AgentResult,
30
+ BaseAgent,
31
+ Finding,
32
+ Severity,
33
+ register,
34
+ )
35
+ from patchi.core.security.domain_loader import DomainLoader
36
+
37
+ _log = logging.getLogger("patchi.security.red_team_engine")
38
+
39
+
40
+ @dataclass
41
+ class AttackStepResult:
42
+ """Result of a single attack step."""
43
+
44
+ step: int
45
+ action: str
46
+ success: bool
47
+ evidence: str = ""
48
+ response_data: dict = field(default_factory=dict)
49
+ error: str = ""
50
+ duration_ms: int = 0
51
+
52
+
53
+ @dataclass
54
+ class ScenarioResult:
55
+ """Result of running an attack scenario."""
56
+
57
+ scenario_id: str
58
+ scenario_name: str
59
+ status: str # "success", "failed", "blocked", "skipped"
60
+ steps_completed: int
61
+ total_steps: int
62
+ step_results: list[AttackStepResult] = field(default_factory=list)
63
+ findings: list[Finding] = field(default_factory=list)
64
+ exploit_evidence: dict = field(default_factory=dict)
65
+ started_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
66
+ completed_at: str = ""
67
+ duration_ms: int = 0
68
+
69
+
70
+ @dataclass
71
+ class RedTeamReport:
72
+ """Complete red team assessment report."""
73
+
74
+ assessment_id: str
75
+ project_root: str
76
+ scope: str
77
+ intensity: str
78
+ scenarios_run: list[ScenarioResult] = field(default_factory=list)
79
+ total_findings: int = 0
80
+ by_severity: dict[str, int] = field(default_factory=dict)
81
+ attack_tree: dict = field(default_factory=dict)
82
+ remediation_playbooks: list[str] = field(default_factory=list)
83
+ started_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
84
+ completed_at: str = ""
85
+ duration_ms: int = 0
86
+
87
+
88
+ class AttackExecutor:
89
+ """Executes individual attack steps with real Playwright/HTTP."""
90
+
91
+ def __init__(
92
+ self,
93
+ root: Path,
94
+ target_url: str = None,
95
+ safe_mode: bool = True,
96
+ on_progress: Callable[[str], None] = None,
97
+ evidence_dir: Path | None = None,
98
+ ):
99
+ self.root = root
100
+ self.target_url = target_url or "http://127.0.0.1:1612"
101
+ self.safe_mode = safe_mode
102
+ self.on_progress = on_progress or (lambda _: None)
103
+ self.session = None # aiohttp session
104
+ self.browser = None # Playwright browser
105
+ self._playwright = None
106
+ self._evidence_dir = evidence_dir or (root / ".patchi" / "evidence")
107
+ self._evidence_dir.mkdir(parents=True, exist_ok=True)
108
+ self._evidence: list[dict] = []
109
+ self._screenshot_count = 0
110
+
111
+ async def initialize(self):
112
+ """Initialize HTTP session and Playwright browser."""
113
+ # HTTP session
114
+ try:
115
+ import aiohttp
116
+
117
+ timeout = aiohttp.ClientTimeout(total=15)
118
+ self.session = aiohttp.ClientSession(timeout=timeout)
119
+ except ImportError:
120
+ _log.warning("aiohttp not available — HTTP attacks limited")
121
+
122
+ # Playwright browser
123
+ try:
124
+ from playwright.async_api import async_playwright
125
+
126
+ self._playwright = await async_playwright().start()
127
+ self.browser = await self._playwright.chromium.launch(
128
+ headless=True,
129
+ args=["--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage"],
130
+ )
131
+ self.on_progress(" Browser initialized (headless Chromium)")
132
+ except ImportError:
133
+ _log.warning("Playwright not available — browser attacks limited")
134
+ except Exception as e:
135
+ _log.warning("Playwright launch failed: %s", e)
136
+
137
+ async def cleanup(self):
138
+ """Clean up resources."""
139
+ if self.session:
140
+ await self.session.close()
141
+ if self.browser:
142
+ await self.browser.close()
143
+ if self._playwright:
144
+ await self._playwright.stop()
145
+
146
+ # ── Evidence capture ────────────────────────────────────────────────────
147
+
148
+ async def _capture_screenshot(self, page, label: str) -> str | None:
149
+ """Take a screenshot and return its path."""
150
+ if not page:
151
+ return None
152
+ self._screenshot_count += 1
153
+ path = self._evidence_dir / f"{self._screenshot_count:04d}_{label}.png"
154
+ try:
155
+ await page.screenshot(path=str(path), full_page=False)
156
+ return str(path)
157
+ except Exception as e:
158
+ _log.warning("Screenshot failed: %s", e)
159
+ return None
160
+
161
+ def _record_evidence(self, step_num: int, action: str, evidence: dict) -> None:
162
+ """Record evidence for the report."""
163
+ evidence["step"] = step_num
164
+ evidence["action"] = action
165
+ evidence["timestamp"] = time.time()
166
+ self._evidence.append(evidence)
167
+
168
+ async def _run_pentest_tool(self, tool: str, step: dict, scenario: dict, context: dict) -> dict:
169
+ """Delegate to real PentestRegistry engines (nuclei/sqlmap/shannon etc)."""
170
+ try:
171
+ from patchi.core.security.pentest.registry import PentestRegistry
172
+
173
+ reg = PentestRegistry()
174
+ target = step.get("target") or step.get("url") or self.target_url
175
+ safe = step.get("safe_mode", self.safe_mode)
176
+ # shannon needs repo_root
177
+ extra = {"repo_root": str(self.root), "templates": step.get("templates"), "wordlist": step.get("wordlist"), "data": step.get("data")}
178
+ # Run in thread to avoid blocking event loop (subprocess)
179
+ import asyncio as _aio
180
+
181
+ res = await _aio.to_thread(reg.run, tool, target, safe, self._evidence_dir, extra)
182
+ out: dict = {"success": bool(res.success and not res.error and (res.findings or res.evidence)), "evidence": res.evidence or res.error, "data": {"tool": res.tool, "findings": res.findings, "raw": res.raw_output[:2000], "duration_ms": res.duration_ms}, "error": res.error}
183
+ # Count as success even if 0 findings but no error (target clean)
184
+ if res.success and not res.error and not res.findings:
185
+ out["success"] = True
186
+ out["evidence"] = f"{tool}: no findings (target clean or not vulnerable)"
187
+ return out
188
+ except Exception as exc: # noqa: BLE001
189
+ return {"success": False, "error": f"pentest tool {tool} failed: {exc}"}
190
+
191
+ def get_evidence(self) -> list[dict]:
192
+ """Return all captured evidence."""
193
+ return list(self._evidence)
194
+
195
+ # ── Execute step ────────────────────────────────────────────────────────
196
+
197
+ async def execute_step(
198
+ self,
199
+ step: dict,
200
+ scenario: dict,
201
+ context: dict,
202
+ ) -> AttackStepResult:
203
+ """Execute a single attack step."""
204
+ start = time.monotonic()
205
+ step_num = step.get("step", 0)
206
+ action = step.get("action", "")
207
+ tool = step.get("tool", "")
208
+
209
+ self.on_progress(f" Step {step_num}: {action} ({tool})")
210
+
211
+ try:
212
+ if tool == "fuzz_params":
213
+ result = await self._fuzz_params(step, scenario, context)
214
+ elif tool == "sql_payload":
215
+ result = await self._sql_payload(step, scenario, context)
216
+ elif tool == "http_request":
217
+ result = await self._http_request(step, scenario, context)
218
+ elif tool == "code_scan":
219
+ result = await self._code_scan(step, scenario, context)
220
+ elif tool == "browser_action":
221
+ result = await self._browser_action(step, scenario, context)
222
+ elif tool == "jwt_tool":
223
+ result = await self._jwt_tool(step, scenario, context)
224
+ elif tool in ("nuclei", "sqlmap", "dalfox", "ffuf", "zap", "shannon"):
225
+ result = await self._run_pentest_tool(tool, step, scenario, context)
226
+ else:
227
+ result = {"success": False, "error": f"Unknown tool: {tool}"}
228
+
229
+ self._record_evidence(
230
+ step_num,
231
+ action,
232
+ {
233
+ "tool": tool,
234
+ "success": result.get("success", False),
235
+ "evidence": result.get("evidence", ""),
236
+ "data": result.get("data", {}),
237
+ },
238
+ )
239
+
240
+ duration = int((time.monotonic() - start) * 1000)
241
+ return AttackStepResult(
242
+ step=step_num,
243
+ action=action,
244
+ success=result.get("success", False),
245
+ evidence=result.get("evidence", ""),
246
+ response_data=result.get("data", {}),
247
+ error=result.get("error", ""),
248
+ duration_ms=duration,
249
+ )
250
+ except Exception as e:
251
+ duration = int((time.monotonic() - start) * 1000)
252
+ return AttackStepResult(
253
+ step=step_num,
254
+ action=action,
255
+ success=False,
256
+ error=str(e),
257
+ duration_ms=duration,
258
+ )
259
+
260
+ # ── HTTP requests (real) ────────────────────────────────────────────────
261
+
262
+ async def _http_request(self, step: dict, scenario: dict, context: dict) -> dict:
263
+ """Send real HTTP request with payload and capture response."""
264
+ if not self.session:
265
+ return {"success": False, "error": "No HTTP session"}
266
+
267
+ url = step.get("url", self.target_url)
268
+ method = step.get("method", "GET").upper()
269
+ payloads = step.get("payloads", [])
270
+ headers = step.get("headers", {})
271
+ data = step.get("data", {})
272
+
273
+ # Apply payloads to URL params or form data
274
+ if payloads:
275
+ for _i, payload in enumerate(payloads[:5]): # cap at 5
276
+ test_url = url
277
+ test_data = dict(data)
278
+
279
+ # Inject payload into URL params
280
+ if "?" in test_url:
281
+ test_url += f"&input={payload}"
282
+ else:
283
+ test_url += f"?input={payload}"
284
+
285
+ # Also inject into form data
286
+ if test_data:
287
+ for key in list(test_data.keys()):
288
+ test_data[key] = payload
289
+
290
+ try:
291
+ async with self.session.request(
292
+ method, test_url, headers=headers, json=test_data if test_data else None
293
+ ) as resp:
294
+ body = await resp.text()
295
+ # Check for SQL error messages
296
+ error_indicators = [
297
+ "sql syntax",
298
+ "mysql",
299
+ "sqlite",
300
+ "postgresql",
301
+ "ora-",
302
+ "unquoted parameter",
303
+ "microsoft ole db",
304
+ "odbc",
305
+ "jdbc",
306
+ "syntax error",
307
+ ]
308
+ has_error = any(ind in body.lower() for ind in error_indicators)
309
+
310
+ if has_error:
311
+ return {
312
+ "success": True,
313
+ "evidence": f"SQL error reflected with payload: {payload[:50]}",
314
+ "data": {
315
+ "payload": payload,
316
+ "status": resp.status,
317
+ "error_reflection": body[:200],
318
+ },
319
+ }
320
+ except Exception as e:
321
+ _log.debug("HTTP request failed: %s", e)
322
+ continue
323
+
324
+ # No payloads — just send a normal request
325
+ try:
326
+ async with self.session.request(method, url, headers=headers) as resp:
327
+ body = await resp.text()
328
+ return {
329
+ "success": resp.status < 400,
330
+ "evidence": f"HTTP {method} {url} → {resp.status} ({len(body)} bytes)",
331
+ "data": {
332
+ "status": resp.status,
333
+ "headers": dict(resp.headers),
334
+ "body_preview": body[:500],
335
+ },
336
+ }
337
+ except Exception as e:
338
+ return {"success": False, "error": str(e)}
339
+
340
+ async def _fuzz_params(self, step: dict, scenario: dict, context: dict) -> dict:
341
+ """Fuzz parameters to find injection points."""
342
+ if not self.session:
343
+ return {"success": False, "error": "No HTTP session"}
344
+
345
+ payloads = step.get("payloads", [])
346
+ if self.safe_mode:
347
+ payloads = [
348
+ p
349
+ for p in payloads
350
+ if not any(
351
+ d in p.upper() for d in ["DROP", "DELETE", "UPDATE", "INSERT", "EXEC", "SYSTEM"]
352
+ )
353
+ ]
354
+
355
+ url = step.get("url", self.target_url)
356
+ findings = []
357
+
358
+ for payload in payloads[:10]: # cap at 10
359
+ try:
360
+ test_url = f"{url}?fuzz={payload}"
361
+ async with self.session.get(test_url) as resp:
362
+ body = await resp.text()
363
+ # Check for error reflection, stack traces, or interesting responses
364
+ if resp.status >= 500:
365
+ findings.append(
366
+ {"payload": payload, "status": resp.status, "type": "server_error"}
367
+ )
368
+ elif any(
369
+ kw in body.lower() for kw in ["traceback", "exception", "stack trace"]
370
+ ):
371
+ findings.append(
372
+ {"payload": payload, "status": resp.status, "type": "info_disclosure"}
373
+ )
374
+ elif payload in body: # payload reflected in response
375
+ findings.append(
376
+ {"payload": payload, "status": resp.status, "type": "reflected"}
377
+ )
378
+ except Exception:
379
+ continue
380
+
381
+ return {
382
+ "success": True,
383
+ "evidence": f"Fuzzed {len(payloads)} payloads, found {len(findings)} interesting responses",
384
+ "data": {"findings": findings, "tested": len(payloads)},
385
+ }
386
+
387
+ async def _sql_payload(self, step: dict, scenario: dict, context: dict) -> dict:
388
+ """Execute SQL injection payload and check for error reflection."""
389
+ if not self.session:
390
+ return {"success": False, "error": "No HTTP session"}
391
+
392
+ url = step.get("url", self.target_url)
393
+ payloads = step.get(
394
+ "payloads",
395
+ [
396
+ "' OR '1'='1",
397
+ "' OR 1=1--",
398
+ "admin'--",
399
+ "' UNION SELECT NULL--",
400
+ "1; DROP TABLE users--",
401
+ ],
402
+ )
403
+
404
+ sql_indicators = [
405
+ "sql syntax",
406
+ "mysql",
407
+ "sqlite",
408
+ "postgresql",
409
+ "ora-",
410
+ "unquoted",
411
+ "microsoft ole db",
412
+ "odbc",
413
+ "warning.*mysql",
414
+ "unclosed quotation mark",
415
+ ]
416
+
417
+ results = []
418
+ for payload in payloads:
419
+ try:
420
+ test_url = f"{url}?id={payload}"
421
+ async with self.session.get(test_url) as resp:
422
+ body = await resp.text()
423
+ has_sql_error = any(ind in body.lower() for ind in sql_indicators)
424
+ results.append(
425
+ {
426
+ "payload": payload,
427
+ "status": resp.status,
428
+ "sql_error": has_sql_error,
429
+ "response_preview": body[:200] if has_sql_error else "",
430
+ }
431
+ )
432
+ if has_sql_error:
433
+ return {
434
+ "success": True,
435
+ "evidence": f"SQL injection confirmed: {payload} → SQL error reflected",
436
+ "data": {"results": results, "confirmed": True},
437
+ }
438
+ except Exception:
439
+ continue
440
+
441
+ return {
442
+ "success": True,
443
+ "evidence": f"Tested {len(payloads)} SQL payloads, no error reflection found",
444
+ "data": {"results": results, "confirmed": False},
445
+ }
446
+
447
+ # ── Browser automation (real Playwright) ────────────────────────────────
448
+
449
+ async def _browser_action(self, step: dict, scenario: dict, context: dict) -> dict:
450
+ """Execute real browser automation with Playwright."""
451
+ if not self.browser:
452
+ return {"success": False, "error": "Browser not available"}
453
+
454
+ action_type = step.get("action_type", "navigate")
455
+ url = step.get("url", self.target_url)
456
+ selector = step.get("selector", "")
457
+ value = step.get("value", "")
458
+
459
+ page = await self.browser.new_page()
460
+ evidence = {"url": url, "action": action_type}
461
+
462
+ try:
463
+ if action_type == "navigate":
464
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
465
+ evidence["title"] = await page.title()
466
+ evidence["screenshot"] = await self._capture_screenshot(
467
+ page, f"nav_{step.get('step', 0)}"
468
+ )
469
+
470
+ elif action_type == "click":
471
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
472
+ if selector:
473
+ await page.click(selector)
474
+ await page.wait_for_load_state("domcontentloaded")
475
+ evidence["screenshot"] = await self._capture_screenshot(
476
+ page, f"click_{step.get('step', 0)}"
477
+ )
478
+
479
+ elif action_type == "fill":
480
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
481
+ if selector and value:
482
+ await page.fill(selector, value)
483
+ evidence["screenshot"] = await self._capture_screenshot(
484
+ page, f"fill_{step.get('step', 0)}"
485
+ )
486
+
487
+ elif action_type == "fill_and_submit":
488
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
489
+ if selector and value:
490
+ await page.fill(selector, value)
491
+ # Try to find and click submit button
492
+ submit = await page.query_selector(
493
+ "button[type=submit], input[type=submit], button:has-text('Login'), button:has-text('Submit')"
494
+ )
495
+ if submit:
496
+ await submit.click()
497
+ await page.wait_for_load_state("domcontentloaded")
498
+ evidence["screenshot"] = await self._capture_screenshot(
499
+ page, f"submit_{step.get('step', 0)}"
500
+ )
501
+ evidence["final_url"] = page.url
502
+
503
+ elif action_type == "check_auth_bypass":
504
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
505
+ # Check if we can access protected resource without auth
506
+ content = await page.content()
507
+ has_login_form = bool(await page.query_selector("input[type=password]"))
508
+ has_dashboard = any(
509
+ kw in content.lower() for kw in ["dashboard", "welcome", "logout", "admin"]
510
+ )
511
+ evidence["has_login_form"] = has_login_form
512
+ evidence["has_dashboard"] = has_dashboard
513
+ evidence["bypassed"] = has_dashboard and not has_login_form
514
+ evidence["screenshot"] = await self._capture_screenshot(
515
+ page, f"bypass_{step.get('step', 0)}"
516
+ )
517
+
518
+ elif action_type == "screenshot":
519
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
520
+ evidence["screenshot"] = await self._capture_screenshot(
521
+ page, f"capture_{step.get('step', 0)}"
522
+ )
523
+ evidence["title"] = await page.title()
524
+
525
+ elif action_type == "intercept_requests":
526
+ captured_requests = []
527
+
528
+ async def on_request(request):
529
+ captured_requests.append(
530
+ {
531
+ "method": request.method,
532
+ "url": request.url,
533
+ "headers": dict(request.headers),
534
+ }
535
+ )
536
+
537
+ page.on("request", on_request)
538
+ await page.goto(url, wait_until="domcontentloaded", timeout=10000)
539
+ await page.wait_for_timeout(2000) # let requests settle
540
+ evidence["captured_requests"] = captured_requests[:20]
541
+ evidence["request_count"] = len(captured_requests)
542
+ evidence["screenshot"] = await self._capture_screenshot(
543
+ page, f"intercept_{step.get('step', 0)}"
544
+ )
545
+
546
+ else:
547
+ evidence["error"] = f"Unknown browser action: {action_type}"
548
+
549
+ return {
550
+ "success": True,
551
+ "evidence": f"Browser {action_type} on {url}",
552
+ "data": evidence,
553
+ }
554
+
555
+ except Exception as e:
556
+ evidence["error"] = str(e)
557
+ return {
558
+ "success": False,
559
+ "evidence": f"Browser action failed: {e}",
560
+ "data": evidence,
561
+ }
562
+ finally:
563
+ await page.close()
564
+
565
+ # ── Code scan (static) ──────────────────────────────────────────────────
566
+
567
+ async def _code_scan(self, step: dict, scenario: dict, context: dict) -> dict:
568
+ """Scan codebase for vulnerability patterns."""
569
+ patterns = step.get("patterns", [])
570
+ findings = []
571
+
572
+ for py_file in sorted(self.root.rglob("*.py")):
573
+ if ".patchi" in str(py_file) or "__pycache__" in str(py_file):
574
+ continue
575
+ try:
576
+ content = py_file.read_text(encoding="utf-8", errors="ignore")
577
+ except OSError:
578
+ continue
579
+
580
+ for pattern in patterns:
581
+ if pattern.lower() in content.lower():
582
+ # Find the line
583
+ for i, line in enumerate(content.splitlines(), 1):
584
+ if pattern.lower() in line.lower():
585
+ findings.append(
586
+ {
587
+ "file": str(py_file.relative_to(self.root)),
588
+ "line": i,
589
+ "pattern": pattern,
590
+ "context": line.strip()[:100],
591
+ }
592
+ )
593
+ break
594
+
595
+ return {
596
+ "success": True,
597
+ "evidence": f"Scanned for {len(patterns)} patterns, found {len(findings)} matches",
598
+ "data": {"findings": findings[:20]},
599
+ }
600
+
601
+ # ── JWT tool ────────────────────────────────────────────────────────────
602
+
603
+ async def _jwt_tool(self, step: dict, scenario: dict, context: dict) -> dict:
604
+ """Manipulate JWT tokens for auth testing."""
605
+ action = step.get("jwt_action", "decode")
606
+ token = step.get("token", "")
607
+
608
+ try:
609
+ import base64
610
+ import json as _json
611
+
612
+ if action == "decode" and token:
613
+ # Decode JWT without verification
614
+ parts = token.split(".")
615
+ if len(parts) >= 2:
616
+ # Decode payload
617
+ payload = parts[1] + "=" * (4 - len(parts[1]) % 4)
618
+ decoded = _json.loads(base64.urlsafe_b64decode(payload))
619
+ return {
620
+ "success": True,
621
+ "evidence": f"JWT decoded: {list(decoded.keys())}",
622
+ "data": {
623
+ "header": _json.loads(base64.urlsafe_b64decode(parts[0] + "==")),
624
+ "payload": decoded,
625
+ },
626
+ }
627
+
628
+ elif action == "alg_none":
629
+ # Try to forge token with alg:none
630
+ if token:
631
+ parts = token.split(".")
632
+ if len(parts) >= 3:
633
+ forged = parts[0] + "." + parts[1] + "."
634
+ return {
635
+ "success": True,
636
+ "evidence": "Forged JWT with alg:none (test if server accepts)",
637
+ "data": {"forged_token": forged[:100]},
638
+ }
639
+
640
+ return {"success": False, "error": f"Unknown JWT action: {action}"}
641
+
642
+ except Exception as e:
643
+ return {"success": False, "error": str(e)}
644
+
645
+
646
+ class RedTeamEngine:
647
+ """
648
+ Main Red Team Engine.
649
+
650
+ Orchestrates attack scenarios, manages execution, and produces reports.
651
+ """
652
+
653
+ def __init__(
654
+ self,
655
+ root: Path,
656
+ target_url: str = None,
657
+ safe_mode: bool = True,
658
+ on_progress: Callable[[str], None] = None,
659
+ ):
660
+ self.root = root
661
+ self.target_url = target_url
662
+ self.safe_mode = safe_mode
663
+ self.on_progress = on_progress or (lambda _: None)
664
+ self.scenarios_dir = root / "patchi" / "core" / "security" / "attack_scenarios"
665
+ self._scenarios_cache: dict[str, dict] = {}
666
+ self.domain_loader = DomainLoader(root)
667
+
668
+ def _load_scenarios(self) -> dict[str, dict]:
669
+ """Load all attack scenarios from YAML files."""
670
+ if self._scenarios_cache:
671
+ return self._scenarios_cache
672
+
673
+ scenarios = {}
674
+ if self.scenarios_dir.exists():
675
+ for yaml_file in self.scenarios_dir.glob("*.yaml"):
676
+ try:
677
+ with open(yaml_file) as f:
678
+ data = yaml.safe_load(f)
679
+ if data and "scenarios" in data:
680
+ for scenario in data["scenarios"]:
681
+ scenarios[scenario["id"]] = scenario
682
+ except Exception as e:
683
+ _log.warning(f"Failed to load scenarios from {yaml_file}: {e}")
684
+
685
+ self._scenarios_cache = scenarios
686
+ return scenarios
687
+
688
+ def select_scenarios(
689
+ self,
690
+ project_context: dict,
691
+ scope: str = "full",
692
+ intensity: str = "active",
693
+ forced_scenarios: list[str] = None,
694
+ ) -> list[dict]:
695
+ """Select relevant attack scenarios based on project context."""
696
+ all_scenarios = self._load_scenarios()
697
+ forced_scenarios = forced_scenarios or []
698
+
699
+ # Get active security domains
700
+ project_context.get("active_security_domains", [])
701
+ frameworks = [f.get("name", "").lower() for f in project_context.get("frameworks", [])]
702
+ component_type = project_context.get("component_type", "")
703
+
704
+ # Score scenarios
705
+ # Active domains use compound ids ("injection-sql"); scenario categories
706
+ # are simple tokens ("injection", "sql"). Match on both the full id and
707
+ # its hyphen-split parts so a domain activates its whole family.
708
+ domain_tokens: set[str] = set()
709
+ for d in project_context.get("active_security_domains", []):
710
+ domain_tokens.update(str(d).lower().split("-"))
711
+
712
+ scored = []
713
+ for scenario_id, scenario in all_scenarios.items():
714
+ score = 0
715
+
716
+ # Domain match (full id beats individual token)
717
+ sid_lower = scenario_id.lower()
718
+ for d in project_context.get("active_security_domains", []):
719
+ if str(d).lower() == sid_lower:
720
+ score += 15
721
+ break
722
+
723
+ category = scenario.get("category", "")
724
+ subcategory = scenario.get("subcategory", "")
725
+ if category and category in domain_tokens:
726
+ score += 6
727
+ if subcategory and subcategory in domain_tokens:
728
+ score += 8 # subcategory is a precise match — weight it highest
729
+
730
+ # Framework match
731
+ for fw in frameworks:
732
+ if fw in scenario_id.lower() or fw in category:
733
+ score += 3
734
+
735
+ # Component type match
736
+ if component_type:
737
+ if "frontend" in component_type and "xss" in scenario_id:
738
+ score += 5
739
+ if "backend" in component_type and "sqli" in scenario_id:
740
+ score += 5
741
+ if "backend" in component_type and "sql" in subcategory:
742
+ score += 5
743
+ if "api" in component_type and "auth" in scenario_id:
744
+ score += 5
745
+
746
+ # Scope filter
747
+ if scope == "api" and "browser" in scenario_id:
748
+ score -= 10
749
+ if scope == "web" and "sql" in scenario_id:
750
+ score -= 5
751
+
752
+ # Intensity filter
753
+ if intensity == "passive" and scenario.get("severity") == "critical":
754
+ score -= 5
755
+
756
+ # Forced scenarios get high score
757
+ if scenario_id in forced_scenarios:
758
+ score += 100
759
+
760
+ if score > 0:
761
+ scored.append((score, scenario))
762
+
763
+ # Sort by score
764
+ scored.sort(key=lambda x: x[0], reverse=True)
765
+
766
+ # Limit based on intensity
767
+ max_scenarios = {"passive": 10, "active": 25, "aggressive": 50}.get(intensity, 25)
768
+ selected = [s for _, s in scored[:max_scenarios]]
769
+
770
+ _log.info(f"Selected {len(selected)} scenarios for {scope}/{intensity} assessment")
771
+ return selected
772
+
773
+ async def run_assessment(
774
+ self,
775
+ project_context: dict,
776
+ scope: str = "full",
777
+ intensity: str = "active",
778
+ forced_scenarios: list[str] = None,
779
+ max_scenarios: int = None,
780
+ ) -> RedTeamReport:
781
+ """Run a complete red team assessment."""
782
+ assessment_id = f"rt-{uuid.uuid4().hex[:8]}"
783
+ start_time = time.monotonic()
784
+
785
+ self.on_progress(f"🎯 Starting Red Team Assessment: {assessment_id}")
786
+ self.on_progress(
787
+ f" Scope: {scope} | Intensity: {intensity} | Safe Mode: {self.safe_mode}"
788
+ )
789
+
790
+ # Select scenarios
791
+ scenarios = self.select_scenarios(project_context, scope, intensity, forced_scenarios)
792
+ if max_scenarios:
793
+ scenarios = scenarios[:max_scenarios]
794
+
795
+ self.on_progress(f"📋 Selected {len(scenarios)} attack scenarios")
796
+
797
+ # Initialize executor
798
+ evidence_dir = self.root / ".patchi" / "evidence" / assessment_id
799
+ executor = AttackExecutor(
800
+ self.root,
801
+ target_url=self.target_url,
802
+ safe_mode=self.safe_mode,
803
+ on_progress=self.on_progress,
804
+ evidence_dir=evidence_dir,
805
+ )
806
+ await executor.initialize()
807
+
808
+ report = RedTeamReport(
809
+ assessment_id=assessment_id,
810
+ project_root=str(self.root),
811
+ scope=scope,
812
+ intensity=intensity,
813
+ )
814
+
815
+ try:
816
+ # Run scenarios
817
+ for i, scenario in enumerate(scenarios):
818
+ self.on_progress(f"⚔️ Scenario {i + 1}/{len(scenarios)}: {scenario['name']}")
819
+
820
+ result = await self._run_scenario(scenario, executor, project_context)
821
+ report.scenarios_run.append(result)
822
+
823
+ # Count findings
824
+ report.total_findings += len(result.findings)
825
+ for finding in result.findings:
826
+ sev = finding.severity.value
827
+ report.by_severity[sev] = report.by_severity.get(sev, 0) + 1
828
+
829
+ # Collect remediation playbooks
830
+ playbook = scenario.get("remediation_playbook")
831
+ if playbook and playbook not in report.remediation_playbooks:
832
+ report.remediation_playbooks.append(playbook)
833
+
834
+ # Capture evidence from executor
835
+ report.exploit_evidence = {
836
+ "screenshots": len(
837
+ [e for e in executor.get_evidence() if e.get("data", {}).get("screenshot")]
838
+ ),
839
+ "total_steps": len(executor.get_evidence()),
840
+ "evidence_dir": str(executor._evidence_dir),
841
+ "evidence": executor.get_evidence()[:50],
842
+ }
843
+
844
+ finally:
845
+ await executor.cleanup()
846
+
847
+ report.completed_at = datetime.now(UTC).isoformat()
848
+ report.duration_ms = int((time.monotonic() - start_time) * 1000)
849
+
850
+ self.on_progress(
851
+ f"✅ Assessment complete: {report.total_findings} findings in {report.duration_ms}ms"
852
+ )
853
+
854
+ # Save report
855
+ await self._save_report(report)
856
+
857
+ return report
858
+
859
+ async def _run_scenario(
860
+ self,
861
+ scenario: dict,
862
+ executor: AttackExecutor,
863
+ project_context: dict,
864
+ ) -> ScenarioResult:
865
+ """Run a single attack scenario."""
866
+ scenario_id = scenario["id"]
867
+ start_time = time.monotonic()
868
+
869
+ result = ScenarioResult(
870
+ scenario_id=scenario_id,
871
+ scenario_name=scenario["name"],
872
+ status="running",
873
+ total_steps=len(scenario.get("attack_steps", [])),
874
+ )
875
+
876
+ steps = scenario.get("attack_steps", [])
877
+ context = {"scenario": scenario, "project_context": project_context}
878
+
879
+ for step in steps:
880
+ step_result = await executor.execute_step(step, scenario, context)
881
+ result.step_results.append(step_result)
882
+ result.steps_completed += 1
883
+
884
+ if not step_result.success and step.get("required", True):
885
+ result.status = "failed"
886
+ break
887
+
888
+ if result.status == "running":
889
+ result.status = "success"
890
+
891
+ # Generate findings from successful steps
892
+ result.findings = self._generate_findings(scenario, result)
893
+
894
+ # Collect exploit evidence
895
+ result.exploit_evidence = self._collect_evidence(scenario, result)
896
+
897
+ result.completed_at = datetime.now(UTC).isoformat()
898
+ result.duration_ms = int((time.monotonic() - start_time) * 1000)
899
+
900
+ return result
901
+
902
+ def _generate_findings(self, scenario: dict, result: ScenarioResult) -> list[Finding]:
903
+ """Generate findings from scenario results."""
904
+ findings = []
905
+
906
+ if result.status != "success":
907
+ return findings
908
+
909
+ # Create finding based on scenario
910
+ finding = Finding(
911
+ agent="RedTeamEngine",
912
+ type=scenario.get("category", "attack"),
913
+ severity=Severity(scenario.get("severity", "high")),
914
+ file="",
915
+ line=0,
916
+ message=f"{scenario['name']}: {scenario.get('description', '')}",
917
+ detail=f"Attack scenario {scenario['id']} completed successfully. Steps: {result.steps_completed}/{result.total_steps}",
918
+ cwe=scenario.get("cwe", ""),
919
+ suggestion=f"Apply remediation playbook: {scenario.get('remediation_playbook', 'manual review')}",
920
+ extra={
921
+ "owasp": scenario.get("owasp", ""),
922
+ "remediation_playbook": scenario.get("remediation_playbook", ""),
923
+ "tags": scenario.get("tags", []),
924
+ "scenario_id": scenario["id"],
925
+ },
926
+ )
927
+ findings.append(finding)
928
+
929
+ # Add findings for each successful step with evidence
930
+ for step_result in result.step_results:
931
+ if step_result.success and step_result.evidence:
932
+ step_finding = Finding(
933
+ agent="RedTeamEngine",
934
+ type=f"{scenario.get('category', 'attack')}.step",
935
+ severity=Severity.MEDIUM,
936
+ file="",
937
+ line=0,
938
+ message=f"Step {step_result.step} ({step_result.action}): {step_result.evidence}",
939
+ detail=f"Attack step evidence: {step_result.evidence}",
940
+ tags=["evidence", "step"],
941
+ )
942
+ findings.append(step_finding)
943
+
944
+ return findings
945
+
946
+ def _collect_evidence(self, scenario: dict, result: ScenarioResult) -> dict:
947
+ """Collect exploit evidence for reporting."""
948
+ return {
949
+ "scenario_id": scenario["id"],
950
+ "steps": [
951
+ {
952
+ "step": sr.step,
953
+ "action": sr.action,
954
+ "success": sr.success,
955
+ "evidence": sr.evidence,
956
+ "duration_ms": sr.duration_ms,
957
+ }
958
+ for sr in result.step_results
959
+ ],
960
+ "detection_signatures": scenario.get("detection_signatures", []),
961
+ "verification_method": scenario.get("verification", []),
962
+ }
963
+
964
+ async def _save_report(self, report: RedTeamReport):
965
+ """Save report to memory and file."""
966
+ from patchi.core import memory as mem
967
+
968
+ # Save to scan results
969
+ mem.save_scan_result(
970
+ "RedTeamEngine",
971
+ {
972
+ "assessment_id": report.assessment_id,
973
+ "findings": [f.to_dict() for sr in report.scenarios_run for f in sr.findings],
974
+ "summary": {
975
+ "total_findings": report.total_findings,
976
+ "by_severity": report.by_severity,
977
+ "scenarios_run": len(report.scenarios_run),
978
+ "duration_ms": report.duration_ms,
979
+ },
980
+ },
981
+ self.root,
982
+ )
983
+
984
+ # Save detailed report as JSON
985
+ report_path = self.root / ".patchi" / "reports" / f"redteam_{report.assessment_id}.json"
986
+ report_path.parent.mkdir(parents=True, exist_ok=True)
987
+
988
+ import json
989
+
990
+ report_data = {
991
+ "assessment_id": report.assessment_id,
992
+ "project_root": report.project_root,
993
+ "scope": report.scope,
994
+ "intensity": report.intensity,
995
+ "started_at": report.started_at,
996
+ "completed_at": report.completed_at,
997
+ "duration_ms": report.duration_ms,
998
+ "total_findings": report.total_findings,
999
+ "by_severity": report.by_severity,
1000
+ "scenarios": [
1001
+ {
1002
+ "id": sr.scenario_id,
1003
+ "name": sr.scenario_name,
1004
+ "status": sr.status,
1005
+ "steps_completed": sr.steps_completed,
1006
+ "total_steps": sr.total_steps,
1007
+ "findings": [f.to_dict() for f in sr.findings],
1008
+ "evidence": sr.exploit_evidence,
1009
+ "duration_ms": sr.duration_ms,
1010
+ }
1011
+ for sr in report.scenarios_run
1012
+ ],
1013
+ "remediation_playbooks": report.remediation_playbooks,
1014
+ }
1015
+
1016
+ try:
1017
+ report_path.write_text(json.dumps(report_data, indent=2))
1018
+ except Exception as e:
1019
+ _log.warning(f"Failed to save red team report: {e}")
1020
+
1021
+ async def verify_fixes(
1022
+ self,
1023
+ patch_ids: list[str],
1024
+ project_context: dict,
1025
+ ) -> dict:
1026
+ """Verify fixes by re-running relevant attack scenarios."""
1027
+ self.on_progress(f"🔍 Verifying {len(patch_ids)} fixes...")
1028
+
1029
+ # Get findings associated with patches
1030
+ from patchi.core.fix.patch import list_patches
1031
+
1032
+ patches = list_patches(self.root)
1033
+
1034
+ for patch_id in patch_ids:
1035
+ patch = next((p for p in patches if p.get("id") == patch_id), None)
1036
+ if patch:
1037
+ # Related findings wiring: deferred to post-assessment correlation
1038
+ _log.debug("patch %s found for re-assessment", patch_id)
1039
+
1040
+ # For now, run a focused assessment
1041
+ verification_report = await self.run_assessment(
1042
+ project_context,
1043
+ scope="targeted",
1044
+ intensity="active",
1045
+ max_scenarios=10,
1046
+ )
1047
+
1048
+ verified = 0
1049
+ for sr in verification_report.scenarios_run:
1050
+ if sr.status != "success":
1051
+ verified += 1
1052
+
1053
+ return {
1054
+ "verified_fixes": verified,
1055
+ "total_patches": len(patch_ids),
1056
+ "verification_report_id": verification_report.assessment_id,
1057
+ "remaining_vulnerabilities": verification_report.total_findings,
1058
+ }
1059
+
1060
+
1061
+ # Convenience function for CLI
1062
+ async def run_red_team(
1063
+ root: Path,
1064
+ target_url: str = None,
1065
+ safe_mode: bool = True,
1066
+ scope: str = "full",
1067
+ intensity: str = "active",
1068
+ on_progress: Callable[[str], None] = None,
1069
+ ) -> RedTeamReport:
1070
+ """Run a red team assessment."""
1071
+ # Get project context from brain
1072
+ from patchi.core import memory as mem
1073
+
1074
+ brain = mem.get_brain(root)
1075
+
1076
+ project_context = {
1077
+ "project_purpose": brain.get("project_purpose", ""),
1078
+ "project_domain": brain.get("project_domain", ""),
1079
+ "frameworks": brain.get("frameworks", []),
1080
+ "active_security_domains": brain.get("active_security_domains", []),
1081
+ "component_type": brain.get("component_type", ""),
1082
+ }
1083
+
1084
+ engine = RedTeamEngine(root, target_url, safe_mode, on_progress)
1085
+ return await engine.run_assessment(project_context, scope, intensity)
1086
+
1087
+
1088
+ # Agent wrapper for integration with Patchi's agent system
1089
+ @register
1090
+ class RedTeamEngineAgent(BaseAgent):
1091
+ """Red Team Engine as a Patchi agent."""
1092
+
1093
+ name = "RedTeamEngineAgent"
1094
+ group = AgentGroup.SECURITY
1095
+ timeout = 600 # 10 minutes
1096
+
1097
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
1098
+ # Get project context from brain
1099
+ project_context = {
1100
+ "project_purpose": inp.brain.get("project_purpose", ""),
1101
+ "project_domain": inp.brain.get("project_domain", ""),
1102
+ "frameworks": inp.brain.get("frameworks", []),
1103
+ "active_security_domains": inp.brain.get("active_security_domains", []),
1104
+ "component_type": inp.brain.get("component_type", ""),
1105
+ }
1106
+
1107
+ # Get config
1108
+ target_url = inp.extra.get("target_url")
1109
+ safe_mode = inp.extra.get("safe_mode", True)
1110
+ scope = inp.extra.get("scope", "full")
1111
+ intensity = inp.extra.get("intensity", "active")
1112
+
1113
+ # Run assessment
1114
+ async def run():
1115
+ engine = RedTeamEngine(
1116
+ inp.root,
1117
+ target_url=target_url,
1118
+ safe_mode=safe_mode,
1119
+ on_progress=lambda m: result.add_log(m),
1120
+ )
1121
+ return await engine.run_assessment(project_context, scope, intensity)
1122
+
1123
+ try:
1124
+ loop = asyncio.get_event_loop()
1125
+ except RuntimeError:
1126
+ loop = asyncio.new_event_loop()
1127
+ asyncio.set_event_loop(loop)
1128
+
1129
+ report = loop.run_until_complete(run())
1130
+
1131
+ # Add findings to result
1132
+ for sr in report.scenarios_run:
1133
+ for finding in sr.findings:
1134
+ result.add_finding(finding)
1135
+
1136
+ result.data["red_team_report"] = {
1137
+ "assessment_id": report.assessment_id,
1138
+ "total_findings": report.total_findings,
1139
+ "by_severity": report.by_severity,
1140
+ "scenarios_run": len(report.scenarios_run),
1141
+ "remediation_playbooks": report.remediation_playbooks,
1142
+ }