souleyez 2.43.26__py3-none-any.whl → 2.43.34__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.

Potentially problematic release.


This version of souleyez might be problematic. Click here for more details.

Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23434 -10286
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.26.dist-info/RECORD +0 -379
  356. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -59,12 +59,14 @@ If no hosts are discovered yet, suggest reconnaissance actions.
59
59
  self.context_builder = ContextBuilder()
60
60
 
61
61
  # For backward compatibility, also set ollama attribute if using OllamaProvider
62
- if hasattr(self.provider, 'service'):
62
+ if hasattr(self.provider, "service"):
63
63
  self.ollama = self.provider.service
64
64
  else:
65
65
  self.ollama = None
66
66
 
67
- def suggest_next_step(self, engagement_id: int, target_host_ids: Optional[list] = None) -> Optional[Dict[str, Any]]:
67
+ def suggest_next_step(
68
+ self, engagement_id: int, target_host_ids: Optional[list] = None
69
+ ) -> Optional[Dict[str, Any]]:
68
70
  """
69
71
  Suggest the next best attack step for an engagement.
70
72
 
@@ -85,36 +87,40 @@ If no hosts are discovered yet, suggest reconnaissance actions.
85
87
  if not self.provider or not self.provider.is_available():
86
88
  logger.error("Cannot generate recommendation: AI provider not available")
87
89
  return {
88
- 'error': 'AI provider not available. Configure Ollama or Claude in Settings → AI Settings.',
89
- 'action': None,
90
- 'target': None,
91
- 'rationale': None,
92
- 'expected_outcome': None,
93
- 'risk_level': None
90
+ "error": "AI provider not available. Configure Ollama or Claude in Settings → AI Settings.",
91
+ "action": None,
92
+ "target": None,
93
+ "rationale": None,
94
+ "expected_outcome": None,
95
+ "risk_level": None,
94
96
  }
95
97
 
96
98
  # Build context from engagement data (with optional host filtering)
97
99
  try:
98
- context = self.context_builder.build_context(engagement_id, target_host_ids=target_host_ids)
100
+ context = self.context_builder.build_context(
101
+ engagement_id, target_host_ids=target_host_ids
102
+ )
99
103
  except Exception as e:
100
104
  logger.error(f"Failed to build context: {e}")
101
105
  return {
102
- 'error': f'Failed to load engagement data: {e}',
103
- 'action': None,
104
- 'target': None,
105
- 'rationale': None,
106
- 'expected_outcome': None,
107
- 'risk_level': None
106
+ "error": f"Failed to load engagement data: {e}",
107
+ "action": None,
108
+ "target": None,
109
+ "rationale": None,
110
+ "expected_outcome": None,
111
+ "risk_level": None,
108
112
  }
109
113
 
110
114
  # Build prompt with state awareness
111
- state_summary = self.context_builder.get_state_summary(engagement_id, target_host_ids=target_host_ids)
112
-
115
+ state_summary = self.context_builder.get_state_summary(
116
+ engagement_id, target_host_ids=target_host_ids
117
+ )
118
+
113
119
  # Add targeting note if specific hosts selected
114
120
  targeting_note = ""
115
121
  if target_host_ids:
116
122
  targeting_note = f"\nNOTE: Only target the {len(target_host_ids)} selected host(s) listed above. Do not suggest actions against other hosts.\n"
117
-
123
+
118
124
  prompt = f"""You are an expert penetration tester analyzing an engagement and suggesting the next most promising attack step.
119
125
 
120
126
  {state_summary}
@@ -158,47 +164,53 @@ Be specific and actionable. Focus on practical pentesting methodology.
158
164
  Continue from current state - don't restart from scratch."""
159
165
 
160
166
  # Generate recommendation
161
- provider_type = getattr(self.provider, 'provider_type', 'unknown')
162
- logger.info(f"Generating recommendation for engagement {engagement_id} using {provider_type}")
167
+ provider_type = getattr(self.provider, "provider_type", "unknown")
168
+ logger.info(
169
+ f"Generating recommendation for engagement {engagement_id} using {provider_type}"
170
+ )
163
171
  try:
164
- response = self.provider.generate(prompt=prompt, max_tokens=2000, temperature=0.7)
172
+ response = self.provider.generate(
173
+ prompt=prompt, max_tokens=2000, temperature=0.7
174
+ )
165
175
  if not response:
166
176
  logger.error("LLM returned empty response")
167
177
  return {
168
- 'error': 'LLM generation failed (empty response). Check AI provider configuration.',
169
- 'action': None,
170
- 'target': None,
171
- 'rationale': None,
172
- 'expected_outcome': None,
173
- 'risk_level': None
178
+ "error": "LLM generation failed (empty response). Check AI provider configuration.",
179
+ "action": None,
180
+ "target": None,
181
+ "rationale": None,
182
+ "expected_outcome": None,
183
+ "risk_level": None,
174
184
  }
175
185
  except Exception as e:
176
186
  logger.error(f"LLM generation failed: {e}")
177
187
  return {
178
- 'error': f'LLM generation failed: {e}',
179
- 'action': None,
180
- 'target': None,
181
- 'rationale': None,
182
- 'expected_outcome': None,
183
- 'risk_level': None
188
+ "error": f"LLM generation failed: {e}",
189
+ "action": None,
190
+ "target": None,
191
+ "rationale": None,
192
+ "expected_outcome": None,
193
+ "risk_level": None,
184
194
  }
185
195
 
186
196
  # Parse response
187
197
  try:
188
198
  recommendation = self._parse_response(response)
189
- logger.info(f"Generated recommendation: {recommendation.get('action', 'unknown')}")
199
+ logger.info(
200
+ f"Generated recommendation: {recommendation.get('action', 'unknown')}"
201
+ )
190
202
  return recommendation
191
203
  except Exception as e:
192
204
  logger.error(f"Failed to parse LLM response: {e}")
193
205
  logger.debug(f"Raw response: {response}")
194
206
  return {
195
- 'error': f'Failed to parse LLM response: {e}',
196
- 'action': None,
197
- 'target': None,
198
- 'rationale': None,
199
- 'expected_outcome': None,
200
- 'risk_level': None,
201
- 'raw_response': response
207
+ "error": f"Failed to parse LLM response: {e}",
208
+ "action": None,
209
+ "target": None,
210
+ "rationale": None,
211
+ "expected_outcome": None,
212
+ "risk_level": None,
213
+ "raw_response": response,
202
214
  }
203
215
 
204
216
  def _parse_response(self, response: str) -> Dict[str, Any]:
@@ -222,25 +234,37 @@ Continue from current state - don't restart from scratch."""
222
234
  ValueError: If response format is invalid
223
235
  """
224
236
  # Extract fields using regex
225
- action_match = re.search(r'ACTION:\s*(.+?)(?=\n[A-Z]+:|$)', response, re.DOTALL | re.IGNORECASE)
226
- target_match = re.search(r'TARGET:\s*(.+?)(?=\n[A-Z]+:|$)', response, re.DOTALL | re.IGNORECASE)
227
- rationale_match = re.search(r'RATIONALE:\s*(.+?)(?=\n[A-Z]+:|$)', response, re.DOTALL | re.IGNORECASE)
228
- expected_match = re.search(r'EXPECTED:\s*(.+?)(?=\n[A-Z]+:|$)', response, re.DOTALL | re.IGNORECASE)
229
- risk_match = re.search(r'RISK:\s*(.+?)(?=\n[A-Z]+:|$)', response, re.DOTALL | re.IGNORECASE)
237
+ action_match = re.search(
238
+ r"ACTION:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
239
+ )
240
+ target_match = re.search(
241
+ r"TARGET:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
242
+ )
243
+ rationale_match = re.search(
244
+ r"RATIONALE:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
245
+ )
246
+ expected_match = re.search(
247
+ r"EXPECTED:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
248
+ )
249
+ risk_match = re.search(
250
+ r"RISK:\s*(.+?)(?=\n[A-Z]+:|$)", response, re.DOTALL | re.IGNORECASE
251
+ )
230
252
 
231
253
  # Validate all fields present
232
- if not all([action_match, target_match, rationale_match, expected_match, risk_match]):
254
+ if not all(
255
+ [action_match, target_match, rationale_match, expected_match, risk_match]
256
+ ):
233
257
  missing = []
234
258
  if not action_match:
235
- missing.append('ACTION')
259
+ missing.append("ACTION")
236
260
  if not target_match:
237
- missing.append('TARGET')
261
+ missing.append("TARGET")
238
262
  if not rationale_match:
239
- missing.append('RATIONALE')
263
+ missing.append("RATIONALE")
240
264
  if not expected_match:
241
- missing.append('EXPECTED')
265
+ missing.append("EXPECTED")
242
266
  if not risk_match:
243
- missing.append('RISK')
267
+ missing.append("RISK")
244
268
  raise ValueError(f"Missing required fields: {', '.join(missing)}")
245
269
 
246
270
  # Extract and clean values
@@ -248,33 +272,35 @@ Continue from current state - don't restart from scratch."""
248
272
  target = target_match.group(1).strip()
249
273
  rationale = rationale_match.group(1).strip()
250
274
  expected = expected_match.group(1).strip()
251
-
275
+
252
276
  # Extract risk level (grab only first word, handle multi-line responses)
253
277
  risk_text = risk_match.group(1).strip()
254
- risk = risk_text.split()[0].lower() if risk_text else 'medium'
278
+ risk = risk_text.split()[0].lower() if risk_text else "medium"
255
279
 
256
280
  # Validate risk level
257
- if risk not in ['low', 'medium', 'high']:
281
+ if risk not in ["low", "medium", "high"]:
258
282
  logger.warning(f"Invalid risk level '{risk}', defaulting to 'medium'")
259
- risk = 'medium'
283
+ risk = "medium"
260
284
 
261
285
  return {
262
- 'error': None,
263
- 'action': action,
264
- 'target': target,
265
- 'rationale': rationale,
266
- 'expected_outcome': expected,
267
- 'risk_level': risk
286
+ "error": None,
287
+ "action": action,
288
+ "target": target,
289
+ "rationale": rationale,
290
+ "expected_outcome": expected,
291
+ "risk_level": risk,
268
292
  }
269
293
 
270
- def generate_chain(self, engagement_id: int, num_steps: int = 5) -> Optional[Dict[str, Any]]:
294
+ def generate_chain(
295
+ self, engagement_id: int, num_steps: int = 5
296
+ ) -> Optional[Dict[str, Any]]:
271
297
  """
272
298
  Generate a multi-step attack chain.
273
-
299
+
274
300
  Args:
275
301
  engagement_id: ID of engagement to analyze
276
302
  num_steps: Number of steps to generate
277
-
303
+
278
304
  Returns:
279
305
  dict: Chain with list of steps, or error dict if failed
280
306
  """
@@ -282,8 +308,8 @@ Continue from current state - don't restart from scratch."""
282
308
  if not self.provider or not self.provider.is_available():
283
309
  logger.error("Cannot generate chain: AI provider not available")
284
310
  return {
285
- 'error': 'AI provider not available. Configure Ollama or Claude in Settings → AI Settings.',
286
- 'steps': []
311
+ "error": "AI provider not available. Configure Ollama or Claude in Settings → AI Settings.",
312
+ "steps": [],
287
313
  }
288
314
 
289
315
  # Build context from engagement data
@@ -291,62 +317,61 @@ Continue from current state - don't restart from scratch."""
291
317
  context = self.context_builder.build_context(engagement_id)
292
318
  except Exception as e:
293
319
  logger.error(f"Failed to build context: {e}")
294
- return {
295
- 'error': f'Failed to load engagement data: {e}',
296
- 'steps': []
297
- }
320
+ return {"error": f"Failed to load engagement data: {e}", "steps": []}
298
321
 
299
322
  # Create multi-step prompt
300
323
  prompt = self._build_chain_prompt(context, num_steps, engagement_id)
301
324
 
302
325
  # Get AI response
303
- provider_type = getattr(self.provider, 'provider_type', 'unknown')
304
- logger.info(f"Generating {num_steps}-step chain for engagement {engagement_id} using {provider_type}")
326
+ provider_type = getattr(self.provider, "provider_type", "unknown")
327
+ logger.info(
328
+ f"Generating {num_steps}-step chain for engagement {engagement_id} using {provider_type}"
329
+ )
305
330
  try:
306
- response = self.provider.generate(prompt=prompt, max_tokens=3000, temperature=0.7)
331
+ response = self.provider.generate(
332
+ prompt=prompt, max_tokens=3000, temperature=0.7
333
+ )
307
334
  if not response:
308
335
  logger.error("LLM returned empty response")
309
- return {
310
- 'error': 'LLM generation failed (empty response)',
311
- 'steps': []
312
- }
336
+ return {"error": "LLM generation failed (empty response)", "steps": []}
313
337
  except Exception as e:
314
338
  logger.error(f"LLM generation failed: {e}")
315
- return {
316
- 'error': f'LLM generation failed: {e}',
317
- 'steps': []
318
- }
339
+ return {"error": f"LLM generation failed: {e}", "steps": []}
319
340
 
320
341
  # Parse multi-step response
321
342
  try:
322
343
  steps = self._parse_chain_response(response)
323
344
  logger.info(f"Generated {len(steps)}-step attack chain")
324
-
345
+
325
346
  # DEBUG: Log if we got empty steps
326
347
  if not steps:
327
- logger.warning(f"Parsed 0 steps from response. Raw response:\n{response}")
328
-
348
+ logger.warning(
349
+ f"Parsed 0 steps from response. Raw response:\n{response}"
350
+ )
351
+
329
352
  return {
330
- 'error': None,
331
- 'engagement_id': engagement_id,
332
- 'num_steps': len(steps),
333
- 'steps': steps
353
+ "error": None,
354
+ "engagement_id": engagement_id,
355
+ "num_steps": len(steps),
356
+ "steps": steps,
334
357
  }
335
358
  except Exception as e:
336
359
  logger.error(f"Failed to parse chain response: {e}")
337
360
  logger.debug(f"Raw response: {response}")
338
361
  return {
339
- 'error': f'Failed to parse chain response: {e}',
340
- 'steps': [],
341
- 'raw_response': response
362
+ "error": f"Failed to parse chain response: {e}",
363
+ "steps": [],
364
+ "raw_response": response,
342
365
  }
343
366
 
344
- def _build_chain_prompt(self, context: str, num_steps: int, engagement_id: int) -> str:
367
+ def _build_chain_prompt(
368
+ self, context: str, num_steps: int, engagement_id: int
369
+ ) -> str:
345
370
  """Build prompt for multi-step attack chain with state awareness."""
346
-
371
+
347
372
  # Get current state summary
348
373
  state_summary = self.context_builder.get_state_summary(engagement_id)
349
-
374
+
350
375
  return f"""You are an expert penetration tester. Analyze this engagement and create a {num_steps}-step attack chain.
351
376
 
352
377
  {state_summary}
@@ -388,19 +413,21 @@ Be specific and actionable. Think like a professional pentester. Continue from c
388
413
  logger.debug(f"Parsing chain response: {response[:200]}...")
389
414
 
390
415
  # Split by "STEP N:" pattern (handle both with and without leading newline)
391
- step_blocks = re.split(r'STEP \d+:', response)
392
-
416
+ step_blocks = re.split(r"STEP \d+:", response)
417
+
393
418
  logger.debug(f"Split into {len(step_blocks)} blocks")
394
419
 
395
- for i, block in enumerate(step_blocks[1:], 1): # Skip first block (before first STEP)
420
+ for i, block in enumerate(
421
+ step_blocks[1:], 1
422
+ ): # Skip first block (before first STEP)
396
423
  step = {
397
- 'step_number': i,
398
- 'action': self._extract_field(block, 'ACTION'),
399
- 'target': self._extract_field(block, 'TARGET'),
400
- 'rationale': self._extract_field(block, 'RATIONALE'),
401
- 'expected': self._extract_field(block, 'EXPECTED'),
402
- 'risk': self._extract_risk_field(block),
403
- 'dependencies': self._extract_field(block, 'DEPENDENCIES')
424
+ "step_number": i,
425
+ "action": self._extract_field(block, "ACTION"),
426
+ "target": self._extract_field(block, "TARGET"),
427
+ "rationale": self._extract_field(block, "RATIONALE"),
428
+ "expected": self._extract_field(block, "EXPECTED"),
429
+ "risk": self._extract_risk_field(block),
430
+ "dependencies": self._extract_field(block, "DEPENDENCIES"),
404
431
  }
405
432
  steps.append(step)
406
433
 
@@ -408,7 +435,7 @@ Be specific and actionable. Think like a professional pentester. Continue from c
408
435
 
409
436
  def _extract_field(self, text: str, field_name: str) -> str:
410
437
  """Extract a field value from text block."""
411
- pattern = rf'{field_name}:\s*(.+?)(?=\n[A-Z]+:|$)'
438
+ pattern = rf"{field_name}:\s*(.+?)(?=\n[A-Z]+:|$)"
412
439
  match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
413
440
  if match:
414
441
  return match.group(1).strip()
@@ -416,17 +443,23 @@ Be specific and actionable. Think like a professional pentester. Continue from c
416
443
 
417
444
  def _extract_risk_field(self, text: str) -> str:
418
445
  """Extract and validate risk field (handles multi-line responses)."""
419
- risk_text = self._extract_field(text, 'RISK')
420
- risk = risk_text.split()[0].upper() if risk_text and risk_text != "Unknown" else 'MEDIUM'
421
-
446
+ risk_text = self._extract_field(text, "RISK")
447
+ risk = (
448
+ risk_text.split()[0].upper()
449
+ if risk_text and risk_text != "Unknown"
450
+ else "MEDIUM"
451
+ )
452
+
422
453
  # Validate risk level
423
- if risk not in ['LOW', 'MEDIUM', 'HIGH']:
454
+ if risk not in ["LOW", "MEDIUM", "HIGH"]:
424
455
  logger.warning(f"Invalid risk level '{risk}', defaulting to 'MEDIUM'")
425
- risk = 'MEDIUM'
426
-
456
+ risk = "MEDIUM"
457
+
427
458
  return risk
428
459
 
429
- def suggest_multiple_paths(self, engagement_id: int, num_paths: int = 3) -> Dict[str, Any]:
460
+ def suggest_multiple_paths(
461
+ self, engagement_id: int, num_paths: int = 3
462
+ ) -> Dict[str, Any]:
430
463
  """
431
464
  Generate multiple alternative attack paths and rank them.
432
465
 
@@ -444,8 +477,8 @@ Be specific and actionable. Think like a professional pentester. Continue from c
444
477
  if not self.provider or not self.provider.is_available():
445
478
  logger.error("Cannot generate paths: AI provider not available")
446
479
  return {
447
- 'error': 'AI provider not available. Configure Ollama or Claude in Settings → AI Settings.',
448
- 'paths': []
480
+ "error": "AI provider not available. Configure Ollama or Claude in Settings → AI Settings.",
481
+ "paths": [],
449
482
  }
450
483
 
451
484
  # Build context
@@ -453,31 +486,29 @@ Be specific and actionable. Think like a professional pentester. Continue from c
453
486
  context = self.context_builder.build_context(engagement_id)
454
487
  except Exception as e:
455
488
  logger.error(f"Failed to build context: {e}")
456
- return {
457
- 'error': f'Failed to load engagement data: {e}',
458
- 'paths': []
459
- }
489
+ return {"error": f"Failed to load engagement data: {e}", "paths": []}
460
490
 
461
491
  # Build multi-path prompt
462
492
  prompt = self._build_multi_path_prompt(context, num_paths)
463
493
 
464
494
  # Generate recommendations
465
- provider_type = getattr(self.provider, 'provider_type', 'unknown')
466
- logger.info(f"Generating {num_paths} alternative paths for engagement {engagement_id} using {provider_type}")
495
+ provider_type = getattr(self.provider, "provider_type", "unknown")
496
+ logger.info(
497
+ f"Generating {num_paths} alternative paths for engagement {engagement_id} using {provider_type}"
498
+ )
467
499
  try:
468
- response = self.provider.generate(prompt=prompt, max_tokens=2500, temperature=0.7)
500
+ response = self.provider.generate(
501
+ prompt=prompt, max_tokens=2500, temperature=0.7
502
+ )
469
503
  if not response:
470
504
  logger.error("LLM returned empty response")
471
505
  return {
472
- 'error': 'AI generation failed. Check provider configuration.',
473
- 'paths': []
506
+ "error": "AI generation failed. Check provider configuration.",
507
+ "paths": [],
474
508
  }
475
509
  except Exception as e:
476
510
  logger.error(f"LLM generation failed: {e}")
477
- return {
478
- 'error': f'LLM generation failed: {e}',
479
- 'paths': []
480
- }
511
+ return {"error": f"LLM generation failed: {e}", "paths": []}
481
512
 
482
513
  # Parse response
483
514
  try:
@@ -486,6 +517,7 @@ Be specific and actionable. Think like a professional pentester. Continue from c
486
517
 
487
518
  # Score and rank paths
488
519
  from .path_scorer import PathScorer
520
+
489
521
  scorer = PathScorer()
490
522
 
491
523
  # Get engagement data for scoring
@@ -493,16 +525,16 @@ Be specific and actionable. Think like a professional pentester. Continue from c
493
525
  ranked_paths = scorer.rank_paths(paths, engagement_data)
494
526
 
495
527
  return {
496
- 'error': None,
497
- 'engagement_id': engagement_id,
498
- 'paths': ranked_paths
528
+ "error": None,
529
+ "engagement_id": engagement_id,
530
+ "paths": ranked_paths,
499
531
  }
500
532
  except Exception as e:
501
533
  logger.error(f"Failed to parse/score paths: {e}")
502
534
  return {
503
- 'error': f'Failed to process paths: {e}',
504
- 'paths': [],
505
- 'raw_response': response
535
+ "error": f"Failed to process paths: {e}",
536
+ "paths": [],
537
+ "raw_response": response,
506
538
  }
507
539
 
508
540
  def _build_multi_path_prompt(self, context: str, num_paths: int) -> str:
@@ -537,15 +569,15 @@ Be specific and actionable. Think like a professional pentester considering mult
537
569
  paths = []
538
570
 
539
571
  # Split by "PATH N:" pattern
540
- path_blocks = re.split(r'PATH \d+:', response)
572
+ path_blocks = re.split(r"PATH \d+:", response)
541
573
 
542
574
  for i, block in enumerate(path_blocks[1:], 1): # Skip first block
543
575
  path = {
544
- 'action': self._extract_field(block, 'ACTION'),
545
- 'target': self._extract_field(block, 'TARGET'),
546
- 'rationale': self._extract_field(block, 'RATIONALE'),
547
- 'expected': self._extract_field(block, 'EXPECTED'),
548
- 'risk_level': self._extract_risk_field(block)
576
+ "action": self._extract_field(block, "ACTION"),
577
+ "target": self._extract_field(block, "TARGET"),
578
+ "rationale": self._extract_field(block, "RATIONALE"),
579
+ "expected": self._extract_field(block, "EXPECTED"),
580
+ "risk_level": self._extract_risk_field(block),
549
581
  }
550
582
  paths.append(path)
551
583
 
@@ -564,8 +596,8 @@ Be specific and actionable. Think like a professional pentester considering mult
564
596
  fm = FindingsManager()
565
597
 
566
598
  return {
567
- 'engagement': em.get_by_id(engagement_id),
568
- 'hosts': hm.list_hosts(engagement_id),
569
- 'credentials': cm.list_credentials(engagement_id),
570
- 'findings': fm.list_findings(engagement_id)
599
+ "engagement": em.get_by_id(engagement_id),
600
+ "hosts": hm.list_hosts(engagement_id),
601
+ "credentials": cm.list_credentials(engagement_id),
602
+ "findings": fm.list_findings(engagement_id),
571
603
  }