souleyez 2.43.34__py3-none-any.whl → 3.0.7__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 (326) hide show
  1. souleyez/__init__.py +1 -1
  2. souleyez/ai/__init__.py +7 -7
  3. souleyez/ai/action_mapper.py +3 -2
  4. souleyez/ai/chain_advisor.py +2 -1
  5. souleyez/ai/claude_provider.py +2 -2
  6. souleyez/ai/context_builder.py +4 -2
  7. souleyez/ai/executor.py +9 -6
  8. souleyez/ai/feedback_handler.py +4 -2
  9. souleyez/ai/llm_provider.py +2 -2
  10. souleyez/ai/ollama_provider.py +2 -2
  11. souleyez/ai/ollama_service.py +10 -26
  12. souleyez/ai/path_scorer.py +2 -1
  13. souleyez/ai/recommender.py +6 -4
  14. souleyez/ai/report_context.py +2 -2
  15. souleyez/ai/report_service.py +5 -5
  16. souleyez/ai/result_parser.py +3 -2
  17. souleyez/ai/safety.py +5 -2
  18. souleyez/auth/__init__.py +6 -6
  19. souleyez/auth/audit.py +2 -2
  20. souleyez/auth/engagement_access.py +5 -7
  21. souleyez/auth/permissions.py +1 -1
  22. souleyez/auth/session_manager.py +5 -5
  23. souleyez/auth/user_manager.py +4 -5
  24. souleyez/commands/audit.py +6 -5
  25. souleyez/commands/auth.py +6 -5
  26. souleyez/commands/deliverables.py +2 -3
  27. souleyez/commands/engagement.py +3 -3
  28. souleyez/commands/license.py +3 -2
  29. souleyez/commands/screenshots.py +5 -4
  30. souleyez/commands/user.py +10 -8
  31. souleyez/config.py +4 -2
  32. souleyez/core/credential_tester.py +4 -2
  33. souleyez/core/cve_mappings.py +2 -1
  34. souleyez/core/cve_matcher.py +2 -1
  35. souleyez/core/msf_auto_mapper.py +2 -0
  36. souleyez/core/msf_chain_engine.py +3 -1
  37. souleyez/core/msf_database.py +7 -13
  38. souleyez/core/msf_integration.py +2 -2
  39. souleyez/core/msf_rpc_client.py +3 -2
  40. souleyez/core/msf_rpc_manager.py +4 -4
  41. souleyez/core/msf_sync_manager.py +7 -7
  42. souleyez/core/network_utils.py +1 -1
  43. souleyez/core/parser_handler.py +2 -1
  44. souleyez/core/pending_chains.py +4 -3
  45. souleyez/core/templates.py +5 -2
  46. souleyez/core/tool_chaining.py +297 -230
  47. souleyez/core/version_utils.py +1 -0
  48. souleyez/core/vuln_correlation.py +3 -2
  49. souleyez/core/web_utils.py +2 -1
  50. souleyez/detection/__init__.py +1 -1
  51. souleyez/detection/attack_signatures.py +1 -1
  52. souleyez/detection/mitre_mappings.py +1 -2
  53. souleyez/detection/validator.py +5 -4
  54. souleyez/devtools.py +4 -2
  55. souleyez/docs/README.md +2 -2
  56. souleyez/engine/background.py +168 -7
  57. souleyez/engine/base.py +2 -1
  58. souleyez/engine/loader.py +4 -2
  59. souleyez/engine/log_sanitizer.py +1 -0
  60. souleyez/engine/manager.py +3 -1
  61. souleyez/engine/result_handler.py +50 -67
  62. souleyez/engine/worker_manager.py +6 -4
  63. souleyez/export/evidence_bundle.py +1 -0
  64. souleyez/handlers/base.py +1 -0
  65. souleyez/handlers/bash_handler.py +1 -0
  66. souleyez/handlers/bloodhound_handler.py +1 -0
  67. souleyez/handlers/certipy_handler.py +1 -0
  68. souleyez/handlers/crackmapexec_handler.py +2 -20
  69. souleyez/handlers/dnsrecon_handler.py +2 -1
  70. souleyez/handlers/enum4linux_handler.py +65 -37
  71. souleyez/handlers/evil_winrm_handler.py +1 -0
  72. souleyez/handlers/ffuf_handler.py +3 -1
  73. souleyez/handlers/gobuster_handler.py +7 -6
  74. souleyez/handlers/gpp_extract_handler.py +1 -0
  75. souleyez/handlers/hashcat_handler.py +1 -0
  76. souleyez/handlers/hydra_handler.py +5 -1
  77. souleyez/handlers/impacket_getuserspns_handler.py +1 -0
  78. souleyez/handlers/impacket_psexec_handler.py +1 -0
  79. souleyez/handlers/impacket_secretsdump_handler.py +1 -0
  80. souleyez/handlers/john_handler.py +1 -0
  81. souleyez/handlers/katana_handler.py +39 -2
  82. souleyez/handlers/kerbrute_handler.py +1 -0
  83. souleyez/handlers/ldapsearch_handler.py +90 -17
  84. souleyez/handlers/lfi_extract_handler.py +1 -0
  85. souleyez/handlers/msf_auxiliary_handler.py +2 -0
  86. souleyez/handlers/msf_exploit_handler.py +1 -0
  87. souleyez/handlers/nikto_handler.py +2 -1
  88. souleyez/handlers/nmap_handler.py +2 -1
  89. souleyez/handlers/nuclei_handler.py +2 -1
  90. souleyez/handlers/nxc_handler.py +50 -19
  91. souleyez/handlers/rdp_sec_check_handler.py +1 -0
  92. souleyez/handlers/registry.py +1 -0
  93. souleyez/handlers/responder_handler.py +1 -0
  94. souleyez/handlers/service_explorer_handler.py +2 -1
  95. souleyez/handlers/smbclient_handler.py +1 -0
  96. souleyez/handlers/smbmap_handler.py +3 -2
  97. souleyez/handlers/sqlmap_handler.py +6 -4
  98. souleyez/handlers/theharvester_handler.py +2 -1
  99. souleyez/handlers/web_login_test_handler.py +1 -0
  100. souleyez/handlers/whois_handler.py +3 -2
  101. souleyez/handlers/wpscan_handler.py +2 -1
  102. souleyez/history.py +4 -3
  103. souleyez/importers/msf_importer.py +5 -3
  104. souleyez/importers/smart_importer.py +6 -4
  105. souleyez/integrations/siem/__init__.py +6 -6
  106. souleyez/integrations/siem/base.py +1 -1
  107. souleyez/integrations/siem/elastic.py +3 -3
  108. souleyez/integrations/siem/factory.py +1 -2
  109. souleyez/integrations/siem/googlesecops.py +4 -4
  110. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +1 -1
  111. souleyez/integrations/siem/sentinel.py +3 -3
  112. souleyez/integrations/siem/splunk.py +3 -3
  113. souleyez/integrations/siem/wazuh.py +4 -4
  114. souleyez/integrations/wazuh/__init__.py +1 -1
  115. souleyez/integrations/wazuh/client.py +3 -2
  116. souleyez/integrations/wazuh/config.py +3 -2
  117. souleyez/integrations/wazuh/host_mapper.py +3 -1
  118. souleyez/integrations/wazuh/sync.py +4 -1
  119. souleyez/intelligence/__init__.py +1 -1
  120. souleyez/intelligence/correlation_analyzer.py +6 -5
  121. souleyez/intelligence/exploit_knowledge.py +4 -4
  122. souleyez/intelligence/exploit_suggestions.py +4 -3
  123. souleyez/intelligence/gap_analyzer.py +5 -3
  124. souleyez/intelligence/gap_detector.py +2 -0
  125. souleyez/intelligence/sensitive_tables.py +1 -1
  126. souleyez/intelligence/service_parser.py +1 -0
  127. souleyez/intelligence/surface_analyzer.py +9 -9
  128. souleyez/intelligence/target_parser.py +1 -0
  129. souleyez/licensing/__init__.py +3 -3
  130. souleyez/main.py +25 -18
  131. souleyez/migrations/fix_job_counter.py +2 -1
  132. souleyez/parsers/bloodhound_parser.py +1 -0
  133. souleyez/parsers/crackmapexec_parser.py +2 -1
  134. souleyez/parsers/dalfox_parser.py +3 -2
  135. souleyez/parsers/dnsrecon_parser.py +2 -1
  136. souleyez/parsers/enum4linux_parser.py +2 -1
  137. souleyez/parsers/ffuf_parser.py +2 -1
  138. souleyez/parsers/gobuster_parser.py +2 -1
  139. souleyez/parsers/hashcat_parser.py +3 -2
  140. souleyez/parsers/http_fingerprint_parser.py +2 -1
  141. souleyez/parsers/hydra_parser.py +2 -1
  142. souleyez/parsers/impacket_parser.py +2 -1
  143. souleyez/parsers/john_parser.py +4 -3
  144. souleyez/parsers/katana_parser.py +134 -2
  145. souleyez/parsers/msf_parser.py +2 -1
  146. souleyez/parsers/nikto_parser.py +2 -1
  147. souleyez/parsers/nmap_parser.py +14 -3
  148. souleyez/parsers/nuclei_parser.py +3 -2
  149. souleyez/parsers/responder_parser.py +1 -0
  150. souleyez/parsers/searchsploit_parser.py +3 -2
  151. souleyez/parsers/service_explorer_parser.py +1 -0
  152. souleyez/parsers/smbmap_parser.py +2 -1
  153. souleyez/parsers/sqlmap_parser.py +36 -2
  154. souleyez/parsers/theharvester_parser.py +2 -1
  155. souleyez/parsers/whois_parser.py +2 -1
  156. souleyez/parsers/wpscan_parser.py +3 -2
  157. souleyez/plugins/afp.py +3 -1
  158. souleyez/plugins/afp_brute.py +3 -1
  159. souleyez/plugins/ard.py +3 -1
  160. souleyez/plugins/bloodhound.py +3 -2
  161. souleyez/plugins/certipy.py +1 -0
  162. souleyez/plugins/crackmapexec.py +11 -7
  163. souleyez/plugins/dalfox.py +5 -2
  164. souleyez/plugins/dns_hijack.py +3 -1
  165. souleyez/plugins/dnsrecon.py +3 -1
  166. souleyez/plugins/enum4linux.py +3 -1
  167. souleyez/plugins/evil_winrm.py +1 -0
  168. souleyez/plugins/ffuf.py +3 -1
  169. souleyez/plugins/firmware_extract.py +3 -2
  170. souleyez/plugins/gobuster.py +6 -3
  171. souleyez/plugins/gpp_extract.py +1 -0
  172. souleyez/plugins/hashcat.py +2 -1
  173. souleyez/plugins/http_fingerprint.py +149 -40
  174. souleyez/plugins/hydra.py +5 -3
  175. souleyez/plugins/impacket_common.py +40 -0
  176. souleyez/plugins/impacket_getnpusers.py +19 -2
  177. souleyez/plugins/impacket_getuserspns.py +158 -0
  178. souleyez/plugins/impacket_psexec.py +19 -2
  179. souleyez/plugins/impacket_secretsdump.py +19 -2
  180. souleyez/plugins/impacket_smbclient.py +19 -2
  181. souleyez/plugins/john.py +2 -1
  182. souleyez/plugins/katana.py +48 -6
  183. souleyez/plugins/kerbrute.py +1 -0
  184. souleyez/plugins/lfi_extract.py +1 -0
  185. souleyez/plugins/macos_ssh.py +3 -1
  186. souleyez/plugins/mdns.py +3 -1
  187. souleyez/plugins/msf_auxiliary.py +3 -2
  188. souleyez/plugins/msf_exploit.py +6 -5
  189. souleyez/plugins/nikto.py +5 -2
  190. souleyez/plugins/nmap.py +6 -4
  191. souleyez/plugins/nuclei.py +3 -1
  192. souleyez/plugins/nxc.py +1 -0
  193. souleyez/plugins/plugin_base.py +3 -2
  194. souleyez/plugins/plugin_template.py +3 -2
  195. souleyez/plugins/rdp_sec_check.py +1 -0
  196. souleyez/plugins/responder.py +2 -1
  197. souleyez/plugins/router_http_brute.py +3 -1
  198. souleyez/plugins/router_ssh_brute.py +3 -1
  199. souleyez/plugins/router_telnet_brute.py +3 -1
  200. souleyez/plugins/routersploit.py +5 -3
  201. souleyez/plugins/routersploit_exploit.py +5 -3
  202. souleyez/plugins/searchsploit.py +1 -0
  203. souleyez/plugins/service_explorer.py +2 -1
  204. souleyez/plugins/smbmap.py +3 -1
  205. souleyez/plugins/smbpasswd.py +1 -0
  206. souleyez/plugins/sqlmap.py +3 -1
  207. souleyez/plugins/theharvester.py +3 -1
  208. souleyez/plugins/tr069.py +3 -1
  209. souleyez/plugins/upnp.py +3 -1
  210. souleyez/plugins/upnp_abuse.py +4 -2
  211. souleyez/plugins/vnc_access.py +4 -2
  212. souleyez/plugins/vnc_brute.py +3 -1
  213. souleyez/plugins/web_login_test.py +1 -0
  214. souleyez/plugins/whois.py +3 -1
  215. souleyez/plugins/wpscan.py +49 -1
  216. souleyez/reporting/attack_chain.py +2 -1
  217. souleyez/reporting/charts.py +1 -0
  218. souleyez/reporting/compliance_mappings.py +1 -0
  219. souleyez/reporting/detection_report.py +10 -10
  220. souleyez/reporting/formatters.py +7 -12
  221. souleyez/reporting/generator.py +34 -46
  222. souleyez/reporting/metrics.py +2 -1
  223. souleyez/scanner.py +6 -3
  224. souleyez/security/__init__.py +7 -5
  225. souleyez/security/scope_validator.py +5 -4
  226. souleyez/security/validation.py +14 -0
  227. souleyez/security.py +5 -2
  228. souleyez/storage/credentials.py +14 -19
  229. souleyez/storage/crypto.py +7 -4
  230. souleyez/storage/database.py +6 -6
  231. souleyez/storage/db.py +8 -8
  232. souleyez/storage/deliverable_evidence.py +2 -1
  233. souleyez/storage/deliverable_exporter.py +3 -2
  234. souleyez/storage/deliverable_templates.py +2 -1
  235. souleyez/storage/deliverables.py +2 -1
  236. souleyez/storage/engagements.py +6 -4
  237. souleyez/storage/evidence.py +5 -4
  238. souleyez/storage/execution_log.py +4 -2
  239. souleyez/storage/exploit_attempts.py +3 -2
  240. souleyez/storage/exploits.py +3 -1
  241. souleyez/storage/findings.py +3 -1
  242. souleyez/storage/hosts.py +5 -2
  243. souleyez/storage/migrate_to_engagements.py +14 -24
  244. souleyez/storage/migrations/_001_add_credential_enhancements.py +12 -21
  245. souleyez/storage/migrations/_003_add_execution_log.py +8 -13
  246. souleyez/storage/migrations/_005_screenshots.py +2 -4
  247. souleyez/storage/migrations/_006_deliverables.py +2 -4
  248. souleyez/storage/migrations/_007_deliverable_templates.py +4 -8
  249. souleyez/storage/migrations/_008_add_nuclei_table.py +2 -4
  250. souleyez/storage/migrations/_010_evidence_linking.py +6 -12
  251. souleyez/storage/migrations/_012_team_collaboration.py +12 -24
  252. souleyez/storage/migrations/_013_add_host_tags.py +2 -4
  253. souleyez/storage/migrations/_014_exploit_attempts.py +10 -20
  254. souleyez/storage/migrations/_015_add_mac_os_fields.py +4 -8
  255. souleyez/storage/migrations/_016_add_domain_field.py +2 -4
  256. souleyez/storage/migrations/_017_msf_sessions.py +8 -16
  257. souleyez/storage/migrations/_018_add_osint_target.py +4 -8
  258. souleyez/storage/migrations/_019_add_engagement_type.py +4 -8
  259. souleyez/storage/migrations/_020_add_rbac.py +9 -17
  260. souleyez/storage/migrations/_021_wazuh_integration.py +4 -8
  261. souleyez/storage/migrations/_023_fix_detection_results_fk.py +2 -4
  262. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +4 -8
  263. souleyez/storage/migrations/_026_add_engagement_scope.py +4 -8
  264. souleyez/storage/migrations/_027_multi_siem_persistence.py +8 -16
  265. souleyez/storage/migrations/__init__.py +1 -4
  266. souleyez/storage/migrations/migration_manager.py +6 -9
  267. souleyez/storage/msf_sessions.py +1 -1
  268. souleyez/storage/osint.py +3 -1
  269. souleyez/storage/recommendation_engine.py +3 -2
  270. souleyez/storage/screenshots.py +2 -1
  271. souleyez/storage/smb_shares.py +3 -1
  272. souleyez/storage/sqlmap_data.py +6 -4
  273. souleyez/storage/team_collaboration.py +3 -2
  274. souleyez/storage/timeline_tracker.py +2 -1
  275. souleyez/storage/wazuh_vulns.py +3 -1
  276. souleyez/storage/web_paths.py +3 -1
  277. souleyez/testing/credential_tester.py +2 -0
  278. souleyez/ui/__init__.py +2 -1
  279. souleyez/ui/ai_quotes.py +1 -1
  280. souleyez/ui/attack_surface.py +50 -28
  281. souleyez/ui/chain_rules_view.py +6 -3
  282. souleyez/ui/correlation_view.py +3 -2
  283. souleyez/ui/dashboard.py +85 -139
  284. souleyez/ui/deliverables_view.py +1 -1
  285. souleyez/ui/design_system.py +5 -3
  286. souleyez/ui/errors.py +3 -1
  287. souleyez/ui/evidence_linking_view.py +2 -1
  288. souleyez/ui/evidence_vault.py +11 -6
  289. souleyez/ui/exploit_suggestions_view.py +11 -7
  290. souleyez/ui/export_view.py +3 -1
  291. souleyez/ui/gap_analysis_view.py +6 -3
  292. souleyez/ui/help_system.py +4 -1
  293. souleyez/ui/intelligence_view.py +7 -3
  294. souleyez/ui/interactive.py +1512 -584
  295. souleyez/ui/interactive_selector.py +3 -2
  296. souleyez/ui/log_formatter.py +1 -0
  297. souleyez/ui/menu_components.py +3 -1
  298. souleyez/ui/msf_auxiliary_menu.py +4 -1
  299. souleyez/ui/pending_chains_view.py +15 -12
  300. souleyez/ui/progress_indicators.py +5 -2
  301. souleyez/ui/recommendations_view.py +4 -2
  302. souleyez/ui/rule_builder.py +4 -1
  303. souleyez/ui/setup_wizard.py +10 -8
  304. souleyez/ui/shortcuts.py +1 -1
  305. souleyez/ui/splunk_gap_analysis_view.py +7 -4
  306. souleyez/ui/splunk_vulns_view.py +4 -1
  307. souleyez/ui/team_dashboard.py +7 -5
  308. souleyez/ui/template_selector.py +2 -1
  309. souleyez/ui/terminal.py +3 -2
  310. souleyez/ui/timeline_view.py +2 -1
  311. souleyez/ui/tool_setup.py +92 -31
  312. souleyez/ui/tutorial.py +7 -4
  313. souleyez/ui/tutorial_state.py +3 -2
  314. souleyez/ui/wazuh_vulns_view.py +5 -2
  315. souleyez/ui/wordlist_browser.py +4 -3
  316. souleyez/ui.py +13 -7
  317. souleyez/utils/tool_checker.py +61 -12
  318. souleyez/utils.py +4 -4
  319. souleyez/wordlists.py +1 -0
  320. {souleyez-2.43.34.dist-info → souleyez-3.0.7.dist-info}/METADATA +2 -2
  321. souleyez-3.0.7.dist-info/RECORD +445 -0
  322. souleyez-2.43.34.dist-info/RECORD +0 -443
  323. {souleyez-2.43.34.dist-info → souleyez-3.0.7.dist-info}/WHEEL +0 -0
  324. {souleyez-2.43.34.dist-info → souleyez-3.0.7.dist-info}/entry_points.txt +0 -0
  325. {souleyez-2.43.34.dist-info → souleyez-3.0.7.dist-info}/licenses/LICENSE +0 -0
  326. {souleyez-2.43.34.dist-info → souleyez-3.0.7.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,7 @@ import os
8
8
  import platform
9
9
  import shlex
10
10
  import shutil
11
+ import subprocess
11
12
  import sys
12
13
  import tempfile
13
14
  import time
@@ -1947,78 +1948,132 @@ def show_tool_menu(tool_name: str) -> Optional[Dict[str, Any]]:
1947
1948
 
1948
1949
  elif choice == 3:
1949
1950
  # Select from credentials in current engagement
1950
- from souleyez.storage.credentials import CredentialManager
1951
+ from souleyez.storage.credentials import CredentialsManager
1951
1952
  from souleyez.storage.engagements import EngagementManager
1952
1953
 
1953
1954
  em = EngagementManager()
1954
1955
  current_eng = em.get_current()
1955
1956
 
1956
1957
  if not current_eng:
1958
+ click.echo()
1957
1959
  click.echo(click.style("No engagement selected!", fg="red"))
1958
1960
  click.echo("Use 'souleyez engagement use <name>' first")
1961
+ click.echo()
1962
+ click.pause()
1959
1963
  return None
1960
1964
 
1961
- cm = CredentialManager()
1965
+ cm = CredentialsManager()
1962
1966
  creds = cm.list_credentials(current_eng["id"])
1963
1967
 
1964
- # Filter for hashes only (credential_type contains 'hash')
1965
- hashes = [
1966
- c for c in creds if "hash" in c.get("credential_type", "").lower()
1967
- ]
1968
+ # Filter for actual crackable hashes
1969
+ # Hash is stored in 'password' field, must look like a hash (not plaintext)
1970
+ def is_crackable_hash(cred):
1971
+ cred_type = cred.get("credential_type", "").lower()
1972
+ # Hash value is in 'password' field
1973
+ hash_value = cred.get("password", "") or ""
1974
+
1975
+ if not hash_value or hash_value == "None":
1976
+ return False
1977
+
1978
+ # Must have hash-related type
1979
+ if "hash" not in cred_type:
1980
+ return False
1981
+
1982
+ # Check if value looks like a hash (not plaintext)
1983
+ # Hex hash (32+ chars for MD5, mostly hex characters)
1984
+ hex_chars = sum(c in "0123456789abcdefABCDEF" for c in hash_value)
1985
+ if len(hash_value) >= 32 and hex_chars / len(hash_value) > 0.9:
1986
+ return True
1987
+ # Hash format like $1$salt$hash or $2a$... (bcrypt, etc.)
1988
+ if hash_value.startswith("$") and "$" in hash_value[1:]:
1989
+ return True
1990
+ # Kerberos hashes start with $krb5
1991
+ if hash_value.startswith("$krb5"):
1992
+ return True
1993
+ # NTLM:LM format (hash:hash)
1994
+ if ":" in hash_value and len(hash_value) >= 65:
1995
+ return True
1996
+
1997
+ # If it looks like plaintext (has spaces, common words), reject
1998
+ if " " in hash_value or len(hash_value) < 20:
1999
+ return False
2000
+
2001
+ return False
2002
+
2003
+ hashes = [c for c in creds if is_crackable_hash(c)]
1968
2004
 
1969
2005
  if not hashes:
2006
+ click.echo()
1970
2007
  click.echo(
1971
- click.style("No hashes found in current engagement!", fg="yellow")
2008
+ click.style(
2009
+ "No crackable hashes found in current engagement!", fg="yellow"
2010
+ )
1972
2011
  )
2012
+ click.echo()
1973
2013
  click.echo("Try discovering hashes first with:")
1974
2014
  click.echo(" • secretsdump (extract SAM/NTDS)")
1975
2015
  click.echo(" • GetNPUsers (AS-REP roasting)")
2016
+ click.echo(" • SQLMap (database password hashes)")
2017
+ click.echo()
2018
+ click.pause()
1976
2019
  return None
1977
2020
 
1978
- # Show hash list
1979
- click.echo()
1980
- click.echo(
1981
- click.style(
1982
- f"Found {len(hashes)} hash(es) in current engagement:",
1983
- fg="green",
1984
- bold=True,
1985
- )
1986
- )
1987
- click.echo()
2021
+ # Use interactive selector for hash list
2022
+ from souleyez.ui.interactive_selector import interactive_select
1988
2023
 
1989
- for idx, cred in enumerate(hashes[:20], 1): # Show max 20
1990
- username = cred.get("username", "unknown")[:30].ljust(30)
1991
- hash_type = cred.get("credential_type", "unknown")[:20]
1992
- hash_preview = cred.get("credential", "")[:40] + "..."
1993
- click.echo(f" {idx:2d}. {username} | {hash_type} | {hash_preview}")
2024
+ # Prepare hash items with display-friendly fields
2025
+ hash_items = []
2026
+ for idx, cred in enumerate(hashes):
2027
+ hash_value = cred.get("password", "") or ""
2028
+ # Create preview of hash
2029
+ if len(hash_value) > 40:
2030
+ hash_preview = hash_value[:20] + "..." + hash_value[-10:]
2031
+ else:
2032
+ hash_preview = hash_value
1994
2033
 
1995
- if len(hashes) > 20:
1996
- click.echo(f" ... and {len(hashes) - 20} more")
2034
+ hash_items.append(
2035
+ {
2036
+ "idx": idx,
2037
+ "username": cred.get("username", "unknown")[:25],
2038
+ "hash_type": cred.get("credential_type", "unknown"),
2039
+ "hash_preview": hash_preview,
2040
+ "hash_value": hash_value, # Full hash for use
2041
+ "original": cred, # Keep original for later
2042
+ }
2043
+ )
1997
2044
 
1998
- click.echo()
1999
- selection = click.prompt(
2000
- click.style(
2001
- "Select hash number (or 0 to cancel)", fg="yellow", bold=True
2002
- ),
2003
- type=int,
2004
- default=0,
2045
+ columns = [
2046
+ {"name": "#", "width": 5, "key": "idx"},
2047
+ {"name": "Username", "width": 25, "key": "username"},
2048
+ {"name": "Type", "width": 20, "key": "hash_type"},
2049
+ {"name": "Hash Preview", "key": "hash_preview"},
2050
+ ]
2051
+
2052
+ selected_ids = set()
2053
+ interactive_select(
2054
+ items=hash_items,
2055
+ columns=columns,
2056
+ selected_ids=selected_ids,
2057
+ get_id=lambda h: h["idx"],
2058
+ title="SELECT HASH TO CRACK",
2005
2059
  )
2006
2060
 
2007
- if selection < 1 or selection > len(hashes):
2061
+ if not selected_ids:
2008
2062
  click.echo(click.style("Cancelled", fg="yellow"))
2009
2063
  return None
2010
2064
 
2011
- # Use selected hash
2012
- selected = hashes[selection - 1]
2013
- target = selected.get("credential", "")
2065
+ # Get first selected hash
2066
+ selected_idx = list(selected_ids)[0]
2067
+ selected_item = hash_items[selected_idx]
2068
+ target = selected_item["hash_value"]
2014
2069
 
2015
- if not target:
2070
+ if not target or target == "None":
2016
2071
  click.echo(click.style("Hash is empty!", fg="red"))
2017
2072
  return None
2018
2073
 
2019
2074
  click.echo(
2020
2075
  click.style(
2021
- f"\n✓ Selected: {selected.get('username')} ({selected.get('credential_type')})",
2076
+ f"\n✓ Selected: {selected_item['username']} ({selected_item['hash_type']})",
2022
2077
  fg="green",
2023
2078
  )
2024
2079
  )
@@ -2044,6 +2099,121 @@ def show_tool_menu(tool_name: str) -> Optional[Dict[str, Any]]:
2044
2099
  em = EngagementManager()
2045
2100
  current_eng = em.get_current()
2046
2101
 
2102
+ # Valid TLDs for filtering (common ones)
2103
+ valid_tlds = {
2104
+ "com",
2105
+ "org",
2106
+ "net",
2107
+ "edu",
2108
+ "gov",
2109
+ "mil",
2110
+ "int",
2111
+ "io",
2112
+ "co",
2113
+ "us",
2114
+ "uk",
2115
+ "ca",
2116
+ "au",
2117
+ "de",
2118
+ "fr",
2119
+ "jp",
2120
+ "cn",
2121
+ "ru",
2122
+ "br",
2123
+ "in",
2124
+ "mx",
2125
+ "es",
2126
+ "it",
2127
+ "nl",
2128
+ "se",
2129
+ "no",
2130
+ "fi",
2131
+ "dk",
2132
+ "pl",
2133
+ "cz",
2134
+ "at",
2135
+ "ch",
2136
+ "be",
2137
+ "ie",
2138
+ "nz",
2139
+ "za",
2140
+ "sg",
2141
+ "hk",
2142
+ "kr",
2143
+ "tw",
2144
+ "th",
2145
+ "my",
2146
+ "ph",
2147
+ "id",
2148
+ "vn",
2149
+ "ar",
2150
+ "cl",
2151
+ "co",
2152
+ "pe",
2153
+ "ve",
2154
+ "pt",
2155
+ "gr",
2156
+ "tr",
2157
+ "il",
2158
+ "ae",
2159
+ "sa",
2160
+ "eg",
2161
+ "ng",
2162
+ "ke",
2163
+ "info",
2164
+ "biz",
2165
+ "name",
2166
+ "pro",
2167
+ "mobi",
2168
+ "tv",
2169
+ "cc",
2170
+ "ws",
2171
+ "me",
2172
+ "ly",
2173
+ "to",
2174
+ "fm",
2175
+ "am",
2176
+ "gg",
2177
+ "xyz",
2178
+ "app",
2179
+ "dev",
2180
+ "cloud",
2181
+ "tech",
2182
+ "online",
2183
+ "site",
2184
+ "store",
2185
+ "shop",
2186
+ "blog",
2187
+ }
2188
+
2189
+ # Local/invalid TLDs to exclude
2190
+ invalid_tlds = {"lan", "local", "home", "internal", "localdomain", "localhost"}
2191
+
2192
+ def is_valid_domain(domain):
2193
+ """Check if domain is valid for OSINT tools."""
2194
+ if not domain or not isinstance(domain, str):
2195
+ return False
2196
+ # Must have at least one dot
2197
+ if "." not in domain:
2198
+ return False
2199
+ parts = domain.split(".")
2200
+ if len(parts) < 2:
2201
+ return False
2202
+ # Check TLD
2203
+ tld = parts[-1].lower()
2204
+ if tld in invalid_tlds:
2205
+ return False
2206
+ # Reject if TLD is all digits (partial IP)
2207
+ if tld.isdigit():
2208
+ return False
2209
+ # Reject if domain part is all digits (partial IP)
2210
+ if parts[-2].isdigit():
2211
+ return False
2212
+ # Must have valid TLD or at least 2 chars
2213
+ if tld not in valid_tlds and len(tld) < 2:
2214
+ return False
2215
+ return True
2216
+
2047
2217
  discovered_domains = []
2048
2218
  if current_eng:
2049
2219
  hm = HostManager()
@@ -2054,11 +2224,10 @@ def show_tool_menu(tool_name: str) -> Optional[Dict[str, Any]]:
2054
2224
  for host in all_hosts:
2055
2225
  hostname = host.get("hostname")
2056
2226
  if hostname and "." in hostname:
2057
- # Extract domain (everything after first subdomain)
2058
2227
  parts = hostname.split(".")
2059
2228
  if len(parts) >= 2:
2060
- domain = ".".join(parts[-2:]) # Get last two parts (domain.tld)
2061
- if domain not in discovered_domains:
2229
+ domain = ".".join(parts[-2:])
2230
+ if is_valid_domain(domain) and domain not in discovered_domains:
2062
2231
  discovered_domains.append(domain)
2063
2232
 
2064
2233
  # Also check OSINT data for hosts/domains (from theHarvester, etc.)
@@ -2067,77 +2236,109 @@ def show_tool_menu(tool_name: str) -> Optional[Dict[str, Any]]:
2067
2236
  for osint_entry in osint_hosts:
2068
2237
  hostname = osint_entry.get("value", "")
2069
2238
  if hostname and "." in hostname:
2070
- # Extract domain from hostname
2071
2239
  parts = hostname.split(".")
2072
2240
  if len(parts) >= 2:
2073
- # If it's a subdomain (e.g., testasp.vulnweb.com), extract base domain
2074
- domain = ".".join(
2075
- parts[-2:]
2076
- ) # Get last two parts (domain.tld)
2077
- if domain not in discovered_domains:
2241
+ domain = ".".join(parts[-2:])
2242
+ if (
2243
+ is_valid_domain(domain)
2244
+ and domain not in discovered_domains
2245
+ ):
2078
2246
  discovered_domains.append(domain)
2079
2247
  except Exception:
2080
- pass # Silently ignore OSINT lookup errors
2248
+ pass
2081
2249
 
2082
- if discovered_domains:
2250
+ # Show menu-based selection like Nmap
2251
+ domain_count = len(discovered_domains)
2252
+ if domain_count > 0:
2083
2253
  click.echo(
2084
- click.style(
2085
- f"🎯 Discovered {len(discovered_domains)} domain(s) in current engagement:",
2086
- fg="green",
2087
- bold=True,
2088
- )
2254
+ f"Found {click.style(str(domain_count), fg='green', bold=True)} valid domain(s) in current engagement."
2089
2255
  )
2090
- click.echo()
2091
-
2092
- for idx, domain in enumerate(discovered_domains[:10], 1):
2093
- click.echo(f" {idx}. {domain}")
2094
-
2095
- if len(discovered_domains) > 10:
2096
- click.echo(f" ... and {len(discovered_domains) - 10} more")
2256
+ else:
2257
+ click.echo(click.style("No valid domains discovered yet.", fg="yellow"))
2258
+ click.echo()
2097
2259
 
2098
- click.echo()
2099
- click.echo(" [0] Enter custom domain")
2100
- click.echo(" [q] Back")
2101
- click.echo()
2102
- click.echo(
2103
- click.style(" 💡 TIP: ", fg="yellow", bold=True)
2104
- + "Type "
2105
- + click.style("?", fg="cyan", bold=True)
2106
- + " for help guide"
2260
+ click.echo(
2261
+ " 1. Select from discovered domains"
2262
+ if domain_count > 0
2263
+ else click.style(
2264
+ " 1. Select from discovered domains (none found)", dim=True
2107
2265
  )
2108
- click.echo()
2266
+ )
2267
+ click.echo(" 2. Enter custom domain")
2268
+ click.echo(" [q] Back")
2269
+ click.echo()
2270
+ click.echo(
2271
+ click.style(" 💡 TIP: ", fg="yellow", bold=True)
2272
+ + "Type "
2273
+ + click.style("?", fg="cyan", bold=True)
2274
+ + " for help guide"
2275
+ )
2276
+ click.echo()
2109
2277
 
2110
- choice = click.prompt(
2111
- click.style("Select option", fg="green", bold=True),
2112
- type=str,
2113
- default="0",
2114
- show_default=False,
2115
- ).strip()
2278
+ choice = click.prompt(
2279
+ click.style("Select option", fg="green", bold=True),
2280
+ type=str,
2281
+ default="2" if domain_count == 0 else "1",
2282
+ show_default=False,
2283
+ ).strip()
2116
2284
 
2117
- # Handle help command
2118
- if choice == "?":
2119
- show_tool_help(tool_name, help_info)
2120
- DesignSystem.clear_screen()
2121
- return {"action": "retry"}
2285
+ # Handle help command
2286
+ if choice == "?":
2287
+ show_tool_help(tool_name, help_info)
2288
+ DesignSystem.clear_screen()
2289
+ return {"action": "retry"}
2122
2290
 
2123
- # Handle back/quit
2124
- if choice.lower() == "q":
2125
- return {"action": "back"}
2291
+ # Handle back/quit
2292
+ if choice.lower() == "q":
2293
+ return {"action": "back"}
2126
2294
 
2127
- if choice.isdigit() and 1 <= int(choice) <= len(discovered_domains):
2128
- target = discovered_domains[int(choice) - 1]
2129
- click.echo(click.style(f"\n✓ Selected: {target}", fg="green"))
2130
- click.echo()
2131
- else:
2132
- target = None
2133
- else:
2134
- click.echo(
2135
- click.style(
2136
- "No domains discovered yet in current engagement", fg="yellow"
2295
+ target = None
2296
+ if choice == "1" and domain_count > 0:
2297
+ # Use interactive selector for domain list
2298
+ from souleyez.ui.interactive_selector import interactive_select
2299
+
2300
+ # Prepare domain items
2301
+ domain_items = []
2302
+ for idx, domain in enumerate(discovered_domains):
2303
+ # Extract TLD for display
2304
+ parts = domain.split(".")
2305
+ tld = parts[-1].upper() if parts else ""
2306
+ domain_items.append(
2307
+ {
2308
+ "idx": idx,
2309
+ "domain": domain,
2310
+ "tld": f".{tld}",
2311
+ }
2137
2312
  )
2313
+
2314
+ columns = [
2315
+ {"name": "#", "width": 5, "key": "idx"},
2316
+ {"name": "Domain", "key": "domain"},
2317
+ {"name": "TLD", "width": 8, "key": "tld"},
2318
+ ]
2319
+
2320
+ selected_ids = set()
2321
+ interactive_select(
2322
+ items=domain_items,
2323
+ columns=columns,
2324
+ selected_ids=selected_ids,
2325
+ get_id=lambda d: d["idx"],
2326
+ title="SELECT TARGET DOMAIN",
2138
2327
  )
2328
+
2329
+ if not selected_ids:
2330
+ return {"action": "back"}
2331
+
2332
+ # Get first selected domain
2333
+ selected_idx = list(selected_ids)[0]
2334
+ target = domain_items[selected_idx]["domain"]
2335
+ click.echo(click.style(f"\n✓ Selected: {target}", fg="green"))
2139
2336
  click.echo()
2140
- target = None
2337
+ elif choice == "2" or (choice == "1" and domain_count == 0):
2338
+ target = None # Will prompt for manual entry below
2339
+ else:
2340
+ click.echo(click.style("Invalid option!", fg="red"))
2341
+ return None
2141
2342
 
2142
2343
  # Manual entry if not selected from list
2143
2344
  if not target:
@@ -6144,10 +6345,12 @@ def view_jobs_menu():
6144
6345
  "hashes"
6145
6346
  ):
6146
6347
  critical_indicator = "[bold red]💀[/bold red]"
6147
- # Shell access (evil_winrm, psexec success)
6148
- elif tool in ["evil_winrm", "impacket-psexec"] and parse_result.get(
6149
- "success"
6150
- ):
6348
+ # Shell access (evil_winrm, psexec, msf_exploit success)
6349
+ elif tool in [
6350
+ "evil_winrm",
6351
+ "impacket-psexec",
6352
+ "msf_exploit",
6353
+ ] and parse_result.get("success"):
6151
6354
  critical_indicator = "[bold green]🐚[/bold green]"
6152
6355
  # Credentials found (hydra, hashcat cracked, smbpasswd, etc.)
6153
6356
  else:
@@ -6229,8 +6432,8 @@ def view_jobs_menu():
6229
6432
  click.echo()
6230
6433
 
6231
6434
  # Build menu and legend side by side using Rich
6232
- from rich.table import Table
6233
6435
  from rich import box
6436
+ from rich.table import Table
6234
6437
 
6235
6438
  console = Console()
6236
6439
 
@@ -11850,7 +12053,10 @@ def view_job_detail(job_id: int):
11850
12053
  if web_login_test_handler:
11851
12054
  try:
11852
12055
  current_status = job.get("status", "")
11853
- if current_status == "done" and web_login_test_handler.has_done_handler:
12056
+ if (
12057
+ current_status == "done"
12058
+ and web_login_test_handler.has_done_handler
12059
+ ):
11854
12060
  web_login_test_handler.display_done(
11855
12061
  job, log_path, show_all_paths, show_passwords
11856
12062
  )
@@ -14747,20 +14953,26 @@ def view_job_detail(job_id: int):
14747
14953
  actions.append("n")
14748
14954
 
14749
14955
  # [s] Spawn shell (for jobs with admin credentials)
14750
- # Supported: evil_winrm, crackmapexec (Pwn3d!), nxc (Pwn3d!), secretsdump, psexec
14956
+ # Supported: evil_winrm, crackmapexec (Pwn3d!), nxc (Pwn3d! or SSH Shell access!), secretsdump, psexec
14751
14957
  can_spawn_shell = False
14958
+ is_ssh_shell = False # Track if this is an SSH shell (for sshpass)
14752
14959
  tool_name = job.get("tool", "")
14753
14960
 
14754
14961
  if tool_name == "evil_winrm" and job.get("status") == "done":
14755
14962
  if parse_result and parse_result.get("success"):
14756
14963
  can_spawn_shell = True
14757
14964
  elif tool_name in ["crackmapexec", "nxc"] and job.get("status") == "done":
14758
- # Check if Pwn3d! in log (works for both crackmapexec and nxc)
14965
+ # Check if Pwn3d! or SSH Shell access! in log
14759
14966
  if log_path and os.path.exists(log_path):
14760
14967
  try:
14761
14968
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
14762
- if "Pwn3d!" in f.read():
14969
+ log_content = f.read()
14970
+ if "Pwn3d!" in log_content:
14763
14971
  can_spawn_shell = True
14972
+ elif "Shell access!" in log_content:
14973
+ # SSH shell access (Linux) - use sshpass
14974
+ can_spawn_shell = True
14975
+ is_ssh_shell = True
14764
14976
  except Exception:
14765
14977
  pass
14766
14978
  elif tool_name == "impacket-secretsdump" and job.get("status") == "done":
@@ -14769,6 +14981,46 @@ def view_job_detail(job_id: int):
14769
14981
  elif tool_name == "impacket-psexec" and job.get("status") == "done":
14770
14982
  # psexec with done status means we have working shell access
14771
14983
  can_spawn_shell = True
14984
+ elif tool_name == "msf_auxiliary" and job.get("status") == "done":
14985
+ # Check if msf_auxiliary found SSH or telnet credentials
14986
+ if parse_result:
14987
+ creds = parse_result.get("credentials", [])
14988
+ # Check for SSH or telnet credentials
14989
+ for cred in creds if isinstance(creds, list) else []:
14990
+ service = (
14991
+ cred.get("service", "").lower()
14992
+ if isinstance(cred, dict)
14993
+ else ""
14994
+ )
14995
+ if service in ["ssh", "telnet"]:
14996
+ can_spawn_shell = True
14997
+ if service == "ssh":
14998
+ is_ssh_shell = True
14999
+ break
15000
+ elif tool_name == "hydra" and job.get("status") == "done":
15001
+ # Hydra found valid credentials - check if SSH or telnet
15002
+ if parse_result:
15003
+ service = parse_result.get("service", "").lower()
15004
+ creds = parse_result.get("credentials", [])
15005
+ if service in ["ssh", "telnet", "ftp"] and creds:
15006
+ can_spawn_shell = True
15007
+ if service == "ssh":
15008
+ is_ssh_shell = True
15009
+ # Also check log for session opened
15010
+ if not can_spawn_shell and log_path and os.path.exists(log_path):
15011
+ try:
15012
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
15013
+ log_content = f.read()
15014
+ if (
15015
+ "session" in log_content.lower()
15016
+ and "opened" in log_content.lower()
15017
+ ):
15018
+ can_spawn_shell = True
15019
+ # Check if SSH or telnet
15020
+ if "ssh" in log_content.lower():
15021
+ is_ssh_shell = True
15022
+ except Exception:
15023
+ pass
14772
15024
 
14773
15025
  if can_spawn_shell:
14774
15026
  click.echo(
@@ -14986,6 +15238,39 @@ def view_job_detail(job_id: int):
14986
15238
  if not target:
14987
15239
  target = match.group(4)
14988
15240
 
15241
+ elif tool_name == "msf_auxiliary":
15242
+ # Get credentials from parse_result
15243
+ if parse_result:
15244
+ creds = parse_result.get("credentials", [])
15245
+ if isinstance(creds, list) and creds:
15246
+ # Find first SSH or telnet credential
15247
+ for cred in creds:
15248
+ if isinstance(cred, dict):
15249
+ service = cred.get("service", "").lower()
15250
+ if service in ["ssh", "telnet"]:
15251
+ username = cred.get("username")
15252
+ password = cred.get("password")
15253
+ break
15254
+ # If no SSH/telnet, use first credential
15255
+ if not username and creds:
15256
+ first_cred = creds[0]
15257
+ if isinstance(first_cred, dict):
15258
+ username = first_cred.get("username")
15259
+ password = first_cred.get("password")
15260
+
15261
+ elif tool_name == "hydra":
15262
+ # Get credentials from parse_result
15263
+ if parse_result:
15264
+ creds = parse_result.get("credentials", [])
15265
+ if isinstance(creds, list) and creds:
15266
+ # Use first credential
15267
+ first_cred = creds[0]
15268
+ if isinstance(first_cred, dict):
15269
+ username = first_cred.get("username") or first_cred.get(
15270
+ "login"
15271
+ )
15272
+ password = first_cred.get("password")
15273
+
14989
15274
  if not username or (not password and not nt_hash):
14990
15275
  click.echo(
14991
15276
  click.style(
@@ -15025,47 +15310,185 @@ def view_job_detail(job_id: int):
15025
15310
  f"impacket-psexec '{cred_prefix}:{password}@{target}'"
15026
15311
  )
15027
15312
 
15028
- else:
15029
- # For credential jobs (crackmapexec, nxc, secretsdump) - show menu
15030
- click.echo()
15031
- click.echo(click.style("=" * 70, fg="green"))
15032
- click.echo(click.style("SPAWN SHELL", bold=True, fg="green"))
15033
- click.echo(click.style("=" * 70, fg="green"))
15034
- click.echo()
15035
- click.echo(f" Target: {target}")
15036
- click.echo(f" User: {cred_prefix}")
15037
- click.echo()
15038
- click.echo(" [1] evil-winrm (WinRM - port 5985)")
15039
- click.echo(" [2] psexec (SMB - port 445)")
15040
- click.echo(" [q] Cancel")
15041
- click.echo()
15313
+ elif tool_name == "msf_auxiliary":
15314
+ # msf_auxiliary found credentials - determine service type
15315
+ service_type = None
15316
+ if parse_result:
15317
+ creds = parse_result.get("credentials", [])
15318
+ for cred in creds if isinstance(creds, list) else []:
15319
+ if isinstance(cred, dict):
15320
+ service_type = cred.get("service", "").lower()
15321
+ if service_type in ["ssh", "telnet"]:
15322
+ break
15042
15323
 
15043
- shell_choice = click.prompt(
15044
- "Select shell type", type=str, default="2"
15045
- ).strip()
15324
+ if service_type == "ssh" or is_ssh_shell:
15325
+ # SSH - use sshpass
15326
+ shell_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa {username}@{target}"
15327
+ elif service_type == "telnet":
15328
+ # Telnet - show command to run manually (telnet doesn't support password on cmdline easily)
15329
+ click.echo()
15330
+ click.echo(click.style("=" * 70, fg="green"))
15331
+ click.echo(click.style("TELNET SHELL", bold=True, fg="green"))
15332
+ click.echo(click.style("=" * 70, fg="green"))
15333
+ click.echo()
15334
+ click.echo(f" Target: {target}")
15335
+ click.echo(f" User: {username}")
15336
+ click.echo(f" Pass: {password}")
15337
+ click.echo()
15338
+ click.echo(
15339
+ " Launching telnet... Enter password when prompted."
15340
+ )
15341
+ click.echo()
15342
+ shell_cmd = f"telnet {target}"
15343
+ else:
15344
+ # Unknown service - show menu
15345
+ click.echo()
15346
+ click.echo(click.style("=" * 70, fg="green"))
15347
+ click.echo(click.style("SPAWN SHELL", bold=True, fg="green"))
15348
+ click.echo(click.style("=" * 70, fg="green"))
15349
+ click.echo()
15350
+ click.echo(f" Target: {target}")
15351
+ click.echo(f" User: {username}")
15352
+ click.echo()
15353
+ click.echo(" [1] ssh (SSH - port 22)")
15354
+ click.echo(" [2] telnet (Telnet - port 23)")
15355
+ click.echo(" [q] Cancel")
15356
+ click.echo()
15046
15357
 
15047
- if shell_choice == "q":
15048
- continue
15358
+ shell_choice = click.prompt(
15359
+ "Select shell type", type=str, default="1"
15360
+ ).strip()
15049
15361
 
15050
- if shell_choice == "1":
15051
- # evil-winrm
15052
- if nt_hash:
15053
- shell_cmd = (
15054
- f"evil-winrm -i {target} -u '{username}' -H '{nt_hash}'"
15055
- )
15362
+ if shell_choice == "q":
15363
+ continue
15364
+ elif shell_choice == "1":
15365
+ shell_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa {username}@{target}"
15366
+ elif shell_choice == "2":
15367
+ click.echo(f"\n Password: {password}")
15368
+ shell_cmd = f"telnet {target}"
15056
15369
  else:
15057
- shell_cmd = f"evil-winrm -i {target} -u '{username}' -p '{password}'"
15058
- elif shell_choice == "2":
15059
- # psexec
15060
- if nt_hash:
15061
- shell_cmd = f"impacket-psexec '{cred_prefix}@{target}' -hashes ':{nt_hash}'"
15370
+ click.echo(click.style(" Invalid choice", fg="red"))
15371
+ continue
15372
+
15373
+ elif tool_name == "hydra":
15374
+ # Hydra found valid credentials - determine service type
15375
+ service_type = (
15376
+ parse_result.get("service", "").lower() if parse_result else ""
15377
+ )
15378
+
15379
+ if service_type == "ssh" or is_ssh_shell:
15380
+ # SSH - use sshpass
15381
+ shell_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa {username}@{target}"
15382
+ elif service_type == "telnet":
15383
+ # Telnet
15384
+ click.echo()
15385
+ click.echo(click.style("=" * 70, fg="green"))
15386
+ click.echo(click.style("TELNET SHELL", bold=True, fg="green"))
15387
+ click.echo(click.style("=" * 70, fg="green"))
15388
+ click.echo()
15389
+ click.echo(f" Target: {target}")
15390
+ click.echo(f" User: {username}")
15391
+ click.echo(f" Pass: {password}")
15392
+ click.echo()
15393
+ click.echo(
15394
+ " Launching telnet... Enter password when prompted."
15395
+ )
15396
+ click.echo()
15397
+ shell_cmd = f"telnet {target}"
15398
+ elif service_type == "ftp":
15399
+ # FTP
15400
+ click.echo()
15401
+ click.echo(click.style("=" * 70, fg="green"))
15402
+ click.echo(click.style("FTP SHELL", bold=True, fg="green"))
15403
+ click.echo(click.style("=" * 70, fg="green"))
15404
+ click.echo()
15405
+ click.echo(f" Target: {target}")
15406
+ click.echo(f" User: {username}")
15407
+ click.echo(f" Pass: {password}")
15408
+ click.echo()
15409
+ shell_cmd = f"ftp {target}"
15410
+ else:
15411
+ # Unknown service - show menu
15412
+ click.echo()
15413
+ click.echo(click.style("=" * 70, fg="green"))
15414
+ click.echo(click.style("SPAWN SHELL", bold=True, fg="green"))
15415
+ click.echo(click.style("=" * 70, fg="green"))
15416
+ click.echo()
15417
+ click.echo(f" Target: {target}")
15418
+ click.echo(f" User: {username}")
15419
+ click.echo(f" Service: {service_type or 'unknown'}")
15420
+ click.echo()
15421
+ click.echo(" [1] ssh (SSH - port 22)")
15422
+ click.echo(" [2] telnet (Telnet - port 23)")
15423
+ click.echo(" [3] ftp (FTP - port 21)")
15424
+ click.echo(" [q] Cancel")
15425
+ click.echo()
15426
+
15427
+ shell_choice = click.prompt(
15428
+ "Select shell type", type=str, default="1"
15429
+ ).strip()
15430
+
15431
+ if shell_choice == "q":
15432
+ continue
15433
+ elif shell_choice == "1":
15434
+ shell_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa {username}@{target}"
15435
+ elif shell_choice == "2":
15436
+ click.echo(f"\n Password: {password}")
15437
+ shell_cmd = f"telnet {target}"
15438
+ elif shell_choice == "3":
15439
+ click.echo(f"\n Password: {password}")
15440
+ shell_cmd = f"ftp {target}"
15062
15441
  else:
15063
- shell_cmd = (
15064
- f"impacket-psexec '{cred_prefix}:{password}@{target}'"
15065
- )
15442
+ click.echo(click.style(" Invalid choice", fg="red"))
15443
+ continue
15444
+
15445
+ else:
15446
+ # For credential jobs (crackmapexec, nxc, secretsdump) - show menu
15447
+ # Check if this is an SSH shell (set by detection logic above)
15448
+ if is_ssh_shell:
15449
+ # SSH shell - use sshpass directly
15450
+ shell_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa {username}@{target}"
15066
15451
  else:
15067
- click.echo(click.style(" Invalid choice", fg="red"))
15068
- continue
15452
+ # Windows shell options menu
15453
+ click.echo()
15454
+ click.echo(click.style("=" * 70, fg="green"))
15455
+ click.echo(click.style("SPAWN SHELL", bold=True, fg="green"))
15456
+ click.echo(click.style("=" * 70, fg="green"))
15457
+ click.echo()
15458
+ click.echo(f" Target: {target}")
15459
+ click.echo(f" User: {cred_prefix}")
15460
+ click.echo()
15461
+ click.echo(" [1] evil-winrm (WinRM - port 5985)")
15462
+ click.echo(" [2] psexec (SMB - port 445)")
15463
+ click.echo(" [3] ssh (SSH - port 22, requires sshpass)")
15464
+ click.echo(" [q] Cancel")
15465
+ click.echo()
15466
+
15467
+ shell_choice = click.prompt(
15468
+ "Select shell type", type=str, default="2"
15469
+ ).strip()
15470
+
15471
+ if shell_choice == "q":
15472
+ continue
15473
+
15474
+ if shell_choice == "1":
15475
+ # evil-winrm
15476
+ if nt_hash:
15477
+ shell_cmd = f"evil-winrm -i {target} -u '{username}' -H '{nt_hash}'"
15478
+ else:
15479
+ shell_cmd = f"evil-winrm -i {target} -u '{username}' -p '{password}'"
15480
+ elif shell_choice == "2":
15481
+ # psexec
15482
+ if nt_hash:
15483
+ shell_cmd = f"impacket-psexec '{cred_prefix}@{target}' -hashes ':{nt_hash}'"
15484
+ else:
15485
+ shell_cmd = f"impacket-psexec '{cred_prefix}:{password}@{target}'"
15486
+ elif shell_choice == "3":
15487
+ # SSH with sshpass
15488
+ shell_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa {username}@{target}"
15489
+ else:
15490
+ click.echo(click.style(" Invalid choice", fg="red"))
15491
+ continue
15069
15492
 
15070
15493
  if not shell_cmd:
15071
15494
  click.echo(
@@ -16837,6 +17260,10 @@ def _license_management_menu():
16837
17260
  _bypass_validation=True, # Already validated
16838
17261
  )
16839
17262
  if tier_success:
17263
+ # Refresh in-memory user cache so PRO is active immediately
17264
+ from souleyez.auth import get_session_manager
17265
+
17266
+ get_session_manager().set_current_user(None)
16840
17267
  click.echo(
16841
17268
  click.style(
16842
17269
  f" ✓ User '{user.username}' upgraded to PRO tier",
@@ -16846,8 +17273,8 @@ def _license_management_menu():
16846
17273
  click.echo()
16847
17274
  click.echo(
16848
17275
  click.style(
16849
- " 🔐 Re-authenticate to unlock PRO capabilities.",
16850
- fg="cyan",
17276
+ " 💎 PRO features now unlocked!",
17277
+ fg="green",
16851
17278
  )
16852
17279
  )
16853
17280
  else:
@@ -16982,13 +17409,18 @@ def _license_management_menu():
16982
17409
 
16983
17410
  # Reset ALL users with PRO tier to FREE
16984
17411
  try:
16985
- from souleyez.auth import UserManager
17412
+ from souleyez.auth import (
17413
+ UserManager,
17414
+ get_session_manager,
17415
+ )
16986
17416
  from souleyez.storage.database import get_db
16987
17417
 
16988
17418
  user_mgr = UserManager(get_db().db_path)
16989
17419
  count, _ = user_mgr.reset_all_pro_tiers()
16990
17420
  if count > 0:
16991
17421
  click.echo(f" Reset {count} user(s) to FREE tier.")
17422
+ # Refresh in-memory user cache
17423
+ get_session_manager().set_current_user(None)
16992
17424
  except Exception:
16993
17425
  pass
16994
17426
  else:
@@ -23423,61 +23855,6 @@ def manage_credentials_menu():
23423
23855
  break
23424
23856
 
23425
23857
 
23426
- def view_additional_data_menu():
23427
- """Additional data viewing menu for OSINT and Web Paths."""
23428
- em = EngagementManager()
23429
- current_ws = em.get_current()
23430
-
23431
- if not current_ws:
23432
- click.echo(click.style("No engagement selected!", fg="red"))
23433
- click.pause()
23434
- return
23435
-
23436
- engagement_id = current_ws["id"]
23437
-
23438
- while True:
23439
- DesignSystem.clear_screen()
23440
- click.echo("\n" + "=" * 70)
23441
- click.echo("ADDITIONAL DATA")
23442
- click.echo("=" * 70 + "\n")
23443
-
23444
- click.echo(" 1. Web Paths - View and manage discovered web paths")
23445
- click.echo(" 2. SMB Shares - View and manage enumerated SMB shares")
23446
- click.echo(
23447
- " 3. SQLMap Data - View SQL injection discoveries and databases"
23448
- )
23449
- click.echo()
23450
- click.echo(" [q] ← Back")
23451
- click.echo()
23452
-
23453
- try:
23454
- choice_input = click.prompt(
23455
- "Select data type", type=str, default="q"
23456
- ).strip()
23457
-
23458
- if choice_input == "q":
23459
- return
23460
- try:
23461
- choice = int(choice_input)
23462
- except ValueError:
23463
- click.echo(click.style("Invalid selection!", fg="red"))
23464
- click.pause()
23465
- continue
23466
-
23467
- if choice == 1:
23468
- view_web_paths(engagement_id)
23469
- elif choice == 2:
23470
- view_smb_shares(engagement_id)
23471
- elif choice == 3:
23472
- view_sqlmap_data(engagement_id)
23473
- else:
23474
- click.echo(click.style("Invalid selection!", fg="red"))
23475
- click.pause()
23476
-
23477
- except (KeyboardInterrupt, click.Abort):
23478
- return
23479
-
23480
-
23481
23858
  def view_additional_data_menu():
23482
23859
  """Additional data viewing menu for OSINT and Web Paths."""
23483
23860
  em = EngagementManager()
@@ -27526,7 +27903,13 @@ def view_credentials(engagement_id: int):
27526
27903
  return
27527
27904
 
27528
27905
  # Active filters
27529
- filters = {"service": None, "status": None, "host_id": None, "tool": None}
27906
+ filters = {
27907
+ "service": None,
27908
+ "status": None,
27909
+ "host_id": None,
27910
+ "tool": None,
27911
+ "credential_type": None,
27912
+ }
27530
27913
 
27531
27914
  # Pagination
27532
27915
  PAGE_SIZE = 20
@@ -27574,6 +27957,14 @@ def view_credentials(engagement_id: int):
27574
27957
  if filters["tool"] and credentials:
27575
27958
  credentials = [c for c in credentials if c.get("tool") == filters["tool"]]
27576
27959
 
27960
+ # Apply credential_type filter
27961
+ if filters["credential_type"] and credentials:
27962
+ credentials = [
27963
+ c
27964
+ for c in credentials
27965
+ if c.get("credential_type") == filters["credential_type"]
27966
+ ]
27967
+
27577
27968
  # Pagination calculations
27578
27969
  total_creds = len(credentials) if credentials else 0
27579
27970
  total_pages = (
@@ -27697,6 +28088,7 @@ def view_credentials(engagement_id: int):
27697
28088
  click.echo(" [a] Add - Manually add credential")
27698
28089
  click.echo(" [f] Filter - Filter by service/status/host")
27699
28090
  click.echo(" [o] Tool - Filter by tool")
28091
+ click.echo(" [y] Type - Filter by credential type (password/hash/username)")
27700
28092
  click.echo(" [c] Clear filters - Reset all filters")
27701
28093
  click.echo(" [q] Back")
27702
28094
  click.echo()
@@ -27798,6 +28190,12 @@ def view_credentials(engagement_id: int):
27798
28190
  # Filter by tool
27799
28191
  filters["tool"] = _filter_credential_by_tool(engagement_id, cm)
27800
28192
  current_page = 1
28193
+ elif choice == "y":
28194
+ # Filter by credential type
28195
+ filters["credential_type"] = _filter_credential_by_type(
28196
+ engagement_id, cm
28197
+ )
28198
+ current_page = 1
27801
28199
  elif choice == "c":
27802
28200
  filters = {k: None for k in filters}
27803
28201
  current_page = 1
@@ -28102,6 +28500,50 @@ def _filter_credential_by_tool(engagement_id: int, cm: "CredentialsManager"):
28102
28500
  return None
28103
28501
 
28104
28502
 
28503
+ def _filter_credential_by_type(engagement_id: int, cm: "CredentialsManager"):
28504
+ """Prompt for credential type filter."""
28505
+ credentials = cm.list_credentials(engagement_id, decrypt=False)
28506
+ types = sorted(
28507
+ set([c.get("credential_type") for c in credentials if c.get("credential_type")])
28508
+ )
28509
+
28510
+ if not types:
28511
+ click.echo(click.style("\nNo credential types found.", fg="yellow"))
28512
+ click.pause()
28513
+ return None
28514
+
28515
+ # Add friendly names for common types
28516
+ type_labels = {
28517
+ "password": "password (tested credentials)",
28518
+ "hash": "hash (NTLM/other hashes)",
28519
+ "username": "username (enumerated users only)",
28520
+ "kerberos_tgs": "kerberos_tgs (Kerberoast hashes)",
28521
+ "plaintext": "plaintext (GPP/unencrypted)",
28522
+ "database": "database (DB credentials)",
28523
+ "smb": "smb (SMB credentials)",
28524
+ }
28525
+
28526
+ click.echo("\nAvailable credential types:")
28527
+ click.echo(" [q] Clear filter")
28528
+ for idx, ctype in enumerate(types, 1):
28529
+ count = sum(1 for c in credentials if c.get("credential_type") == ctype)
28530
+ label = type_labels.get(ctype, ctype)
28531
+ click.echo(f" [{idx}] {label} ({count})")
28532
+
28533
+ try:
28534
+ choice_input = click.prompt("Select type", type=str, default="q").strip()
28535
+ if choice_input == "q":
28536
+ return None
28537
+ try:
28538
+ choice = int(choice_input)
28539
+ if 1 <= choice <= len(types):
28540
+ return types[choice - 1]
28541
+ except ValueError:
28542
+ return None
28543
+ except (KeyboardInterrupt, click.Abort):
28544
+ return None
28545
+
28546
+
28105
28547
  def _filter_credential_by_host(engagement_id: int):
28106
28548
  """Prompt for host filter."""
28107
28549
  from souleyez.storage.hosts import HostManager
@@ -31065,7 +31507,9 @@ def _execute_webpath_quick_action(engagement_id: int, paths: list, hm: "HostMana
31065
31507
  " ⚙️ Launching Gobuster for test directories...", fg="yellow"
31066
31508
  )
31067
31509
  )
31068
- click.echo(click.style(" ℹ️ Gobuster integration coming soon!", fg="cyan"))
31510
+ click.echo(
31511
+ click.style(" ℹ️ Gobuster integration coming soon!", fg="cyan")
31512
+ )
31069
31513
  click.echo(" For now, manually run:")
31070
31514
  for path in test_dirs[:3]:
31071
31515
  url = path.get("url", "")
@@ -34614,7 +35058,7 @@ def _view_sqlmap_dumped_data(engagement_id: int):
34614
35058
 
34615
35059
  for table in tables_with_dumps:
34616
35060
  console.print(
34617
- f" └─ [cyan]{table['table_name']}[/cyan] ({table['row_count']} rows)"
35061
+ f" [cyan]{table['table_name']}[/cyan] ({table['row_count']} rows)"
34618
35062
  )
34619
35063
 
34620
35064
  # Get dumped data
@@ -35267,18 +35711,24 @@ def view_sqlmap_data(engagement_id: int):
35267
35711
  all_columns[key] = set()
35268
35712
  all_columns[key].update(columns)
35269
35713
 
35270
- # Aggregate dumped data
35714
+ # Aggregate dumped data (track by target URL)
35715
+ job_target = job.get("target", "")
35271
35716
  if parsed.get("dumped_data"):
35272
35717
  for key, data in parsed["dumped_data"].items():
35273
- if key not in all_dumped:
35274
- all_dumped[key] = data
35718
+ # Create target-specific key to avoid cross-host bleeding
35719
+ target_key = f"{job_target}::{key}"
35720
+ if target_key not in all_dumped:
35721
+ data["_target"] = job_target
35722
+ all_dumped[target_key] = data
35275
35723
  else:
35276
35724
  # Merge rows if same table dumped multiple times
35277
- existing_rows = all_dumped[key].get("rows", [])
35725
+ existing_rows = all_dumped[target_key].get("rows", [])
35278
35726
  new_rows = data.get("rows", [])
35279
- all_dumped[key]["rows"] = existing_rows + new_rows
35280
- all_dumped[key]["row_count"] = len(
35281
- all_dumped[key]["rows"]
35727
+ all_dumped[target_key]["rows"] = (
35728
+ existing_rows + new_rows
35729
+ )
35730
+ all_dumped[target_key]["row_count"] = len(
35731
+ all_dumped[target_key]["rows"]
35282
35732
  )
35283
35733
 
35284
35734
  for vuln in parsed.get("vulnerabilities", []):
@@ -35433,13 +35883,51 @@ def view_sqlmap_data(engagement_id: int):
35433
35883
  row_num = int(choice)
35434
35884
  if 1 <= row_num <= len(filtered_injections):
35435
35885
  inj = filtered_injections[row_num - 1]
35886
+ inj_url = inj.get("url", "")
35887
+
35888
+ # Filter databases to only this injection's target
35889
+ filtered_dbs = set()
35890
+ filtered_db_to_dbms = {}
35891
+ for db in all_databases:
35892
+ db_target = db_to_target.get(db, "")
35893
+ if db_target in inj_url or inj_url in db_target:
35894
+ filtered_dbs.add(db)
35895
+ if db in db_to_dbms:
35896
+ filtered_db_to_dbms[db] = db_to_dbms[db]
35897
+
35898
+ # Filter tables to only this injection's target
35899
+ filtered_tables = {}
35900
+ filtered_columns = {}
35901
+ for db, tables in all_tables.items():
35902
+ for tbl in tables:
35903
+ table_key = f"{db}.{tbl}"
35904
+ tbl_target = table_to_target.get(table_key, "")
35905
+ if tbl_target in inj_url or inj_url in tbl_target:
35906
+ if db not in filtered_tables:
35907
+ filtered_tables[db] = set()
35908
+ filtered_tables[db].add(tbl)
35909
+ if table_key in all_columns:
35910
+ filtered_columns[table_key] = all_columns[table_key]
35911
+
35912
+ # Filter dumped data to only this injection's target
35913
+ filtered_dumped = {}
35914
+ for key, data in all_dumped.items():
35915
+ # Key format is "target_url::db.table"
35916
+ if "::" in key:
35917
+ target_part = key.split("::")[0]
35918
+ table_part = key.split("::", 1)[1]
35919
+ if target_part in inj_url or inj_url in target_part:
35920
+ filtered_dumped[table_part] = data
35921
+ elif data.get("_target", "") in inj_url:
35922
+ filtered_dumped[key] = data
35923
+
35436
35924
  _view_sqlmap_injection_detail(
35437
35925
  inj,
35438
- all_databases,
35439
- all_tables,
35440
- all_columns,
35441
- db_to_dbms,
35442
- all_dumped,
35926
+ filtered_dbs,
35927
+ filtered_tables,
35928
+ filtered_columns,
35929
+ filtered_db_to_dbms,
35930
+ filtered_dumped,
35443
35931
  engagement_id,
35444
35932
  )
35445
35933
  else:
@@ -35614,12 +36102,46 @@ def _view_sqlmap_injection_detail(
35614
36102
  from rich.console import Console
35615
36103
  from rich.table import Table
35616
36104
 
36105
+ from souleyez.ui.interactive_selector import _get_key
36106
+
35617
36107
  console = Console()
35618
36108
 
35619
36109
  if all_dumped is None:
35620
36110
  all_dumped = {}
35621
36111
 
35622
- current_view = "overview" # 'overview', 'tables', 'data:<table_key>'
36112
+ # View states: 'databases', 'tables:<db>', 'data:<table_key>'
36113
+ current_view = "databases"
36114
+ selected_db = None
36115
+ data_page = 1
36116
+ data_view_all = False
36117
+ db_cursor_pos = 0 # Cursor for database list
36118
+ tbl_cursor_pos = 0 # Cursor for table list
36119
+ show_empty_tables = False # Hide empty tables by default
36120
+
36121
+ # Build database list with stats
36122
+ db_list = []
36123
+ for db in sorted(all_databases) if all_databases else sorted(all_tables.keys()):
36124
+ tables = all_tables.get(db, set())
36125
+ table_count = len(tables)
36126
+ # Count extracted rows for this database
36127
+ extracted_count = 0
36128
+ extracted_tables = 0
36129
+ for tbl in tables:
36130
+ dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
36131
+ if dumped_key:
36132
+ extracted_tables += 1
36133
+ extracted_count += all_dumped[dumped_key].get(
36134
+ "row_count", len(all_dumped[dumped_key].get("rows", []))
36135
+ )
36136
+ db_list.append(
36137
+ {
36138
+ "name": db,
36139
+ "table_count": table_count,
36140
+ "extracted_tables": extracted_tables,
36141
+ "extracted_rows": extracted_count,
36142
+ "dbms": db_to_dbms.get(db, "Unknown"),
36143
+ }
36144
+ )
35623
36145
 
35624
36146
  while True:
35625
36147
  DesignSystem.clear_screen()
@@ -35649,412 +36171,729 @@ def _view_sqlmap_injection_detail(
35649
36171
  )
35650
36172
  click.echo()
35651
36173
 
35652
- if current_view == "overview":
35653
- _sqli_detail_overview(
35654
- inj,
35655
- all_databases,
35656
- all_tables,
35657
- all_columns,
35658
- db_to_dbms,
36174
+ if current_view == "databases":
36175
+ # Database selector view
36176
+ _sqli_database_selector(
36177
+ db_list,
35659
36178
  all_dumped,
35660
36179
  console,
36180
+ db_cursor_pos,
35661
36181
  )
35662
36182
 
35663
- # Menu
35664
- click.echo()
36183
+ # Navigation footer
36184
+ click.echo("─" * width)
36185
+ click.echo(" ↑↓/jk: Navigate | Enter: View tables | q: Back")
35665
36186
  click.echo("─" * width)
35666
- options = []
35667
- if all_dumped:
35668
- # List available tables with numbers
35669
- table_keys = list(all_dumped.keys())
35670
- click.echo(click.style(" BROWSE EXTRACTED DATA:", bold=True))
35671
- for i, key in enumerate(table_keys, 1):
35672
- data = all_dumped[key]
35673
- row_count = data.get("row_count", len(data.get("rows", [])))
35674
- # Highlight credential tables
35675
- if any(
35676
- x in key.lower()
35677
- for x in [
35678
- "user",
35679
- "account",
35680
- "login",
35681
- "credential",
35682
- "password",
35683
- "card",
35684
- "payment",
35685
- ]
35686
- ):
35687
- click.echo(
35688
- f" [{i}] {click.style(key, fg='red', bold=True)} ({row_count} rows) 🔐"
35689
- )
35690
- else:
35691
- click.echo(f" [{i}] {key} ({row_count} rows)")
35692
- options.extend([str(i) for i in range(1, len(table_keys) + 1)])
35693
-
35694
- click.echo()
35695
- click.echo(f" [t] View all tables structure")
35696
- click.echo(f" [q] Back")
35697
- click.echo()
35698
36187
 
36188
+ # Handle keyboard input
35699
36189
  try:
35700
- choice = click.prompt(" Select", default="q").strip().lower()
36190
+ key = _get_key()
35701
36191
 
35702
- if choice == "q":
36192
+ if key in ("q", "Q", "\x1b"): # q or Escape
35703
36193
  return
35704
- elif choice == "t":
35705
- current_view = "tables"
35706
- elif choice.isdigit() and all_dumped:
35707
- idx = int(choice) - 1
35708
- table_keys = list(all_dumped.keys())
35709
- if 0 <= idx < len(table_keys):
35710
- current_view = f"data:{table_keys[idx]}"
36194
+ elif key in ("\x1b[A", "k"): # Up arrow or k
36195
+ if db_cursor_pos > 0:
36196
+ db_cursor_pos -= 1
36197
+ elif key in ("\x1b[B", "j"): # Down arrow or j
36198
+ if db_cursor_pos < len(db_list) - 1:
36199
+ db_cursor_pos += 1
36200
+ elif key in ("\r", "\n"): # Enter
36201
+ if db_list:
36202
+ selected_db = db_list[db_cursor_pos]["name"]
36203
+ current_view = f"tables:{selected_db}"
36204
+ tbl_cursor_pos = 0
35711
36205
  except (KeyboardInterrupt, click.Abort):
35712
36206
  return
35713
36207
 
35714
- elif current_view == "tables":
35715
- _sqli_detail_tables(
35716
- all_databases, all_tables, all_columns, db_to_dbms, all_dumped, console
36208
+ elif current_view.startswith("tables:"):
36209
+ db_name = current_view[7:]
36210
+ tables = all_tables.get(db_name, set())
36211
+
36212
+ # Build full table list for this database
36213
+ full_table_list = []
36214
+ for tbl in sorted(tables):
36215
+ dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
36216
+ # Check if table has data or is a high-value table
36217
+ tbl_lower = tbl.lower()
36218
+ is_high_value = any(
36219
+ x in tbl_lower
36220
+ for x in [
36221
+ "user",
36222
+ "account",
36223
+ "login",
36224
+ "credential",
36225
+ "password",
36226
+ "card",
36227
+ "payment",
36228
+ ]
36229
+ )
36230
+ full_table_list.append(
36231
+ {
36232
+ "db": db_name,
36233
+ "table": tbl,
36234
+ "dumped_key": dumped_key,
36235
+ "has_data": dumped_key is not None,
36236
+ "is_high_value": is_high_value,
36237
+ }
36238
+ )
36239
+
36240
+ # Filter table list based on show_empty_tables
36241
+ if show_empty_tables:
36242
+ table_list = full_table_list
36243
+ else:
36244
+ # Show only tables with data or high-value tables
36245
+ table_list = [
36246
+ t for t in full_table_list if t["has_data"] or t["is_high_value"]
36247
+ ]
36248
+ # If no tables match filter, show all
36249
+ if not table_list:
36250
+ table_list = full_table_list
36251
+
36252
+ # Ensure cursor is within bounds
36253
+ if tbl_cursor_pos >= len(table_list):
36254
+ tbl_cursor_pos = max(0, len(table_list) - 1)
36255
+
36256
+ # Count hidden tables
36257
+ hidden_count = len(full_table_list) - len(table_list)
36258
+
36259
+ # Show tables for this database
36260
+ _sqli_tables_for_database(
36261
+ db_name,
36262
+ table_list,
36263
+ all_columns,
36264
+ db_to_dbms,
36265
+ all_dumped,
36266
+ console,
36267
+ tbl_cursor_pos,
36268
+ hidden_count=hidden_count,
36269
+ show_all=show_empty_tables,
35717
36270
  )
35718
36271
 
35719
- click.echo()
36272
+ # Navigation footer
36273
+ click.echo("─" * width)
36274
+ if show_empty_tables:
36275
+ click.echo(
36276
+ " ↑↓/jk: Navigate | Enter: View data | a: Hide empty | q: Back"
36277
+ )
36278
+ else:
36279
+ click.echo(
36280
+ " ↑↓/jk: Navigate | Enter: View data | a: Show all | q: Back"
36281
+ )
35720
36282
  click.echo("─" * width)
35721
- click.echo(f" [b] Back to overview")
35722
- click.echo()
35723
36283
 
36284
+ # Handle keyboard input
35724
36285
  try:
35725
- choice = click.prompt(" Select", default="b").strip().lower()
35726
- if choice == "b":
35727
- current_view = "overview"
36286
+ key = _get_key()
36287
+
36288
+ if key in ("q", "Q", "\x1b"): # q or Escape - back to databases
36289
+ current_view = "databases"
36290
+ tbl_cursor_pos = 0
36291
+ elif key in ("\x1b[A", "k"): # Up arrow or k
36292
+ if tbl_cursor_pos > 0:
36293
+ tbl_cursor_pos -= 1
36294
+ elif key in ("\x1b[B", "j"): # Down arrow or j
36295
+ if tbl_cursor_pos < len(table_list) - 1:
36296
+ tbl_cursor_pos += 1
36297
+ elif key in ("a", "A"): # Toggle show all tables
36298
+ show_empty_tables = not show_empty_tables
36299
+ tbl_cursor_pos = 0 # Reset cursor when toggling
36300
+ elif key in ("\r", "\n"): # Enter
36301
+ if table_list and table_list[tbl_cursor_pos]["has_data"]:
36302
+ current_view = (
36303
+ f"data:{table_list[tbl_cursor_pos]['dumped_key']}"
36304
+ )
36305
+ data_page = 1
36306
+ data_view_all = False
35728
36307
  except (KeyboardInterrupt, click.Abort):
35729
36308
  return
35730
36309
 
35731
36310
  elif current_view.startswith("data:"):
35732
36311
  table_key = current_view[5:]
35733
36312
  if table_key in all_dumped:
36313
+ table_data = all_dumped[table_key]
36314
+ total_rows = len(table_data.get("rows", []))
36315
+ page_size = 20
36316
+ total_pages = (total_rows + page_size - 1) // page_size
36317
+
35734
36318
  _sqli_detail_data_table(
35735
- table_key, all_dumped[table_key], console, width
36319
+ table_key,
36320
+ table_data,
36321
+ console,
36322
+ width,
36323
+ page=data_page,
36324
+ page_size=page_size,
36325
+ view_all=data_view_all,
35736
36326
  )
35737
36327
 
35738
- click.echo()
35739
36328
  click.echo("─" * width)
35740
- click.echo(f" [b] Back to overview")
36329
+ click.echo()
36330
+ if total_rows > page_size and not data_view_all:
36331
+ click.echo(" [n] Next - Next page")
36332
+ click.echo(" [p] Prev - Previous page")
36333
+ click.echo(" [a] All - View all records")
36334
+ elif data_view_all:
36335
+ click.echo(" [a] Paginate - Return to paginated view")
36336
+ click.echo(" [q] Back")
35741
36337
  click.echo()
35742
36338
 
35743
36339
  try:
35744
- choice = click.prompt(" Select", default="b").strip().lower()
35745
- if choice == "b":
35746
- current_view = "overview"
36340
+ choice = click.prompt("Select option", default="q").strip().lower()
36341
+ if choice == "q":
36342
+ # Go back to tables view for the selected database
36343
+ if selected_db:
36344
+ current_view = f"tables:{selected_db}"
36345
+ else:
36346
+ current_view = "databases"
36347
+ data_page = 1
36348
+ data_view_all = False
36349
+ elif choice == "n" and not data_view_all and data_page < total_pages:
36350
+ data_page += 1
36351
+ elif choice == "p" and not data_view_all and data_page > 1:
36352
+ data_page -= 1
36353
+ elif choice == "a":
36354
+ data_view_all = not data_view_all
36355
+ data_page = 1
35747
36356
  except (KeyboardInterrupt, click.Abort):
35748
36357
  return
35749
36358
 
35750
36359
 
35751
- def _sqli_detail_overview(
35752
- inj, all_databases, all_tables, all_columns, db_to_dbms, all_dumped, console
36360
+ def _sqli_database_selector(db_list, all_dumped, console, cursor_pos):
36361
+ """Show database selector with table counts and extracted data."""
36362
+ from rich.table import Table
36363
+
36364
+ # Count total extracted data
36365
+ total_extracted = 0
36366
+ cred_count = 0
36367
+ for key, data in all_dumped.items():
36368
+ row_count = data.get("row_count", len(data.get("rows", [])))
36369
+ total_extracted += row_count
36370
+ key_lower = key.lower()
36371
+ if any(x in key_lower for x in ["user", "account", "login", "credential"]):
36372
+ cred_count += row_count
36373
+
36374
+ # Summary alert if data extracted
36375
+ if total_extracted > 0:
36376
+ alerts = []
36377
+ if cred_count > 0:
36378
+ alerts.append(f"🔑 {cred_count} credentials")
36379
+ if total_extracted > cred_count:
36380
+ alerts.append(f"📊 {total_extracted} total rows")
36381
+ click.echo(
36382
+ click.style(" ⚠️ EXTRACTED: ", fg="red", bold=True)
36383
+ + click.style(" | ".join(alerts), fg="red")
36384
+ )
36385
+ click.echo()
36386
+
36387
+ # Summary
36388
+ total_tables = sum(db["table_count"] for db in db_list)
36389
+ extracted_tables = sum(db["extracted_tables"] for db in db_list)
36390
+ click.echo(
36391
+ f" {click.style('Databases:', bold=True)} {len(db_list)} | "
36392
+ f"{click.style('Tables:', bold=True)} {total_tables} | "
36393
+ f"{click.style('Extracted:', bold=True, fg='green')} {extracted_tables}"
36394
+ )
36395
+ click.echo()
36396
+
36397
+ # Database table
36398
+ table = Table(
36399
+ show_header=True,
36400
+ header_style="bold cyan",
36401
+ box=DesignSystem.TABLE_BOX,
36402
+ padding=(0, 1),
36403
+ expand=True,
36404
+ )
36405
+ table.add_column("", width=3, no_wrap=True) # Cursor
36406
+ table.add_column("Database", no_wrap=True)
36407
+ table.add_column("DBMS", width=12)
36408
+ table.add_column("Tables", width=10, justify="right")
36409
+ table.add_column("Extracted", width=12, justify="right")
36410
+ table.add_column("Rows", width=10, justify="right")
36411
+
36412
+ for idx, db in enumerate(db_list):
36413
+ is_selected = idx == cursor_pos
36414
+ cursor = "▶" if is_selected else ""
36415
+
36416
+ name = db["name"]
36417
+ dbms_type = db["dbms"]
36418
+ table_count = db["table_count"]
36419
+ extracted_tables = db["extracted_tables"]
36420
+ extracted_rows = db["extracted_rows"]
36421
+
36422
+ # Highlight rows with extracted data
36423
+ if extracted_tables > 0:
36424
+ if is_selected:
36425
+ table.add_row(
36426
+ f"[bold yellow]{cursor}[/bold yellow]",
36427
+ f"[bold yellow]📂 {name}[/bold yellow]",
36428
+ f"[bold yellow]{dbms_type}[/bold yellow]",
36429
+ f"[bold yellow]{table_count}[/bold yellow]",
36430
+ f"[bold yellow]✓ {extracted_tables}[/bold yellow]",
36431
+ f"[bold yellow]{extracted_rows}[/bold yellow]",
36432
+ )
36433
+ else:
36434
+ table.add_row(
36435
+ cursor,
36436
+ f"[green bold]📂 {name}[/green bold]",
36437
+ dbms_type,
36438
+ str(table_count),
36439
+ f"[green]✓ {extracted_tables}[/green]",
36440
+ f"[green]{extracted_rows}[/green]",
36441
+ )
36442
+ else:
36443
+ if is_selected:
36444
+ table.add_row(
36445
+ f"[bold yellow]{cursor}[/bold yellow]",
36446
+ f"[bold yellow]📂 {name}[/bold yellow]",
36447
+ f"[bold yellow]{dbms_type}[/bold yellow]",
36448
+ f"[bold yellow]{table_count}[/bold yellow]",
36449
+ "[bold yellow]-[/bold yellow]",
36450
+ "[bold yellow]-[/bold yellow]",
36451
+ )
36452
+ else:
36453
+ table.add_row(
36454
+ cursor,
36455
+ f"[dim]📂 {name}[/dim]",
36456
+ f"[dim]{dbms_type}[/dim]",
36457
+ f"[dim]{table_count}[/dim]",
36458
+ "[dim]-[/dim]",
36459
+ "[dim]-[/dim]",
36460
+ )
36461
+
36462
+ console.print(table)
36463
+ click.echo()
36464
+
36465
+
36466
+ def _sqli_tables_for_database(
36467
+ db_name,
36468
+ table_list,
36469
+ all_columns,
36470
+ db_to_dbms,
36471
+ all_dumped,
36472
+ console,
36473
+ cursor_pos,
36474
+ hidden_count=0,
36475
+ show_all=False,
35753
36476
  ):
35754
- """Show overview of SQLi exploitation results."""
36477
+ """Show tables for a specific database with cursor navigation."""
35755
36478
  from rich.table import Table
35756
36479
 
35757
- # Quick stats
35758
- total_tables_dumped = len(all_dumped)
36480
+ dbms_type = db_to_dbms.get(db_name, "Unknown")
36481
+
36482
+ # Count extracted data
36483
+ extracted_count = sum(1 for t in table_list if t["has_data"])
35759
36484
  total_rows = sum(
35760
- d.get("row_count", len(d.get("rows", []))) for d in all_dumped.values()
36485
+ all_dumped[t["dumped_key"]].get(
36486
+ "row_count", len(all_dumped[t["dumped_key"]].get("rows", []))
36487
+ )
36488
+ for t in table_list
36489
+ if t["has_data"]
35761
36490
  )
35762
36491
 
35763
- # Check for high-value data
35764
- has_credentials = any(
35765
- "user" in k.lower() or "account" in k.lower() or "login" in k.lower()
35766
- for k in all_dumped.keys()
35767
- )
35768
- has_cards = any(
35769
- "card" in k.lower() or "payment" in k.lower() or "credit" in k.lower()
35770
- for k in all_dumped.keys()
36492
+ # Database header
36493
+ click.echo(f" {click.style('📂 ' + db_name, bold=True, fg='cyan')} ({dbms_type})")
36494
+ summary_line = (
36495
+ f" {click.style('Tables:', bold=True)} {len(table_list)} | "
36496
+ f"{click.style('Extracted:', bold=True, fg='green')} {extracted_count} | "
36497
+ f"{click.style('Rows:', bold=True)} {total_rows}"
35771
36498
  )
35772
- has_pii = any(
35773
- "address" in k.lower() or "personal" in k.lower() or "customer" in k.lower()
35774
- for k in all_dumped.keys()
36499
+ if hidden_count > 0 and not show_all:
36500
+ summary_line += click.style(f" | {hidden_count} hidden", dim=True)
36501
+ click.echo(summary_line)
36502
+ click.echo()
36503
+
36504
+ if not table_list:
36505
+ click.echo(" [dim]No tables discovered for this database.[/dim]")
36506
+ return
36507
+
36508
+ # Table list
36509
+ table = Table(
36510
+ show_header=True,
36511
+ header_style="bold cyan",
36512
+ box=DesignSystem.TABLE_BOX,
36513
+ padding=(0, 1),
36514
+ expand=True,
35775
36515
  )
36516
+ table.add_column("", width=3, no_wrap=True) # Cursor
36517
+ table.add_column("○", width=3, justify="center", no_wrap=True)
36518
+ table.add_column("Table Name", no_wrap=True)
36519
+ table.add_column("Cols", width=8, justify="right")
36520
+ table.add_column("Rows", width=12, justify="right")
36521
+ table.add_column("Type", width=12)
36522
+
36523
+ for idx, tbl_info in enumerate(table_list):
36524
+ tbl = tbl_info["table"]
36525
+ is_selected = idx == cursor_pos
36526
+ cursor = "▶" if is_selected else ""
36527
+
36528
+ table_key = f"{db_name}.{tbl}"
36529
+ cols = all_columns.get(table_key, set())
36530
+ col_count = len(cols) if cols else 0
36531
+
36532
+ # Determine category
36533
+ tbl_lower = tbl.lower()
36534
+ if any(
36535
+ x in tbl_lower
36536
+ for x in ["user", "account", "login", "credential", "password"]
36537
+ ):
36538
+ category = "🔑 Creds"
36539
+ elif any(x in tbl_lower for x in ["card", "payment", "credit"]):
36540
+ category = "💳 Cards"
36541
+ elif any(
36542
+ x in tbl_lower for x in ["address", "personal", "customer", "profile"]
36543
+ ):
36544
+ category = "👤 PII"
36545
+ else:
36546
+ category = ""
36547
+
36548
+ has_data = tbl_info["has_data"]
36549
+ if has_data:
36550
+ dumped_key = tbl_info["dumped_key"]
36551
+ row_count = all_dumped[dumped_key].get(
36552
+ "row_count", len(all_dumped[dumped_key].get("rows", []))
36553
+ )
36554
+ if is_selected:
36555
+ table.add_row(
36556
+ f"[bold yellow]{cursor}[/bold yellow]",
36557
+ "[bold yellow]○[/bold yellow]",
36558
+ f"[bold yellow]{tbl}[/bold yellow]",
36559
+ f"[bold yellow]{col_count}[/bold yellow]",
36560
+ f"[bold yellow]✓ {row_count}[/bold yellow]",
36561
+ f"[bold yellow]{category}[/bold yellow]",
36562
+ )
36563
+ else:
36564
+ table.add_row(
36565
+ cursor,
36566
+ "○",
36567
+ f"[green bold]{tbl}[/green bold]",
36568
+ str(col_count),
36569
+ f"[green]✓ {row_count}[/green]",
36570
+ category,
36571
+ )
36572
+ else:
36573
+ if is_selected:
36574
+ table.add_row(
36575
+ f"[bold yellow]{cursor}[/bold yellow]",
36576
+ "[bold yellow]○[/bold yellow]",
36577
+ f"[bold yellow]{tbl}[/bold yellow]",
36578
+ f"[bold yellow]{col_count}[/bold yellow]",
36579
+ "[bold yellow]-[/bold yellow]",
36580
+ f"[bold yellow]{category}[/bold yellow]",
36581
+ )
36582
+ else:
36583
+ table.add_row(
36584
+ cursor,
36585
+ "○",
36586
+ tbl,
36587
+ str(col_count),
36588
+ "[dim]-[/dim]",
36589
+ category,
36590
+ )
35776
36591
 
35777
- # Impact summary
35778
- click.echo(click.style(" 📊 EXPLOITATION SUMMARY", bold=True, fg="green"))
36592
+ console.print(table)
35779
36593
  click.echo()
35780
36594
 
35781
- if total_tables_dumped > 0:
35782
- click.echo(
35783
- f" Tables Extracted: {click.style(str(total_tables_dumped), bold=True, fg='green')}"
35784
- )
36595
+
36596
+ def _sqli_detail_overview_interactive(
36597
+ inj,
36598
+ all_databases,
36599
+ all_tables,
36600
+ all_columns,
36601
+ db_to_dbms,
36602
+ all_dumped,
36603
+ console,
36604
+ cursor_pos,
36605
+ all_table_list,
36606
+ ):
36607
+ """Show interactive overview with cursor navigation."""
36608
+ from rich.table import Table
36609
+
36610
+ # Count extracted data by category
36611
+ cred_count = 0
36612
+ card_count = 0
36613
+ pii_count = 0
36614
+
36615
+ for key, data in all_dumped.items():
36616
+ row_count = data.get("row_count", len(data.get("rows", [])))
36617
+ key_lower = key.lower()
36618
+ if any(x in key_lower for x in ["user", "account", "login", "credential"]):
36619
+ cred_count += row_count
36620
+ elif any(x in key_lower for x in ["card", "payment", "credit"]):
36621
+ card_count += row_count
36622
+ elif any(
36623
+ x in key_lower for x in ["address", "personal", "customer", "profile"]
36624
+ ):
36625
+ pii_count += row_count
36626
+
36627
+ # Summary alert if data extracted
36628
+ if cred_count > 0 or card_count > 0 or pii_count > 0:
36629
+ alerts = []
36630
+ if cred_count > 0:
36631
+ alerts.append(f"🔑 {cred_count} credentials")
36632
+ if card_count > 0:
36633
+ alerts.append(f"💳 {card_count} cards")
36634
+ if pii_count > 0:
36635
+ alerts.append(f"👤 {pii_count} PII records")
35785
36636
  click.echo(
35786
- f" Total Records: {click.style(str(total_rows), bold=True, fg='green')}"
36637
+ click.style(" ⚠️ EXTRACTED: ", fg="red", bold=True)
36638
+ + click.style(" | ".join(alerts), fg="red")
35787
36639
  )
35788
36640
  click.echo()
35789
36641
 
35790
- # Data classification
35791
- click.echo(click.style(" Data Classification:", bold=True))
35792
- if has_credentials:
35793
- click.echo(
35794
- f" 🔐 {click.style('CREDENTIALS FOUND', fg='red', bold=True)} - User accounts with passwords"
35795
- )
35796
- if has_cards:
35797
- click.echo(
35798
- f" 💳 {click.style('PAYMENT DATA FOUND', fg='red', bold=True)} - Credit card information"
35799
- )
35800
- if has_pii:
35801
- click.echo(
35802
- f" 👤 {click.style('PII FOUND', fg='yellow', bold=True)} - Personal addresses/information"
35803
- )
35804
- if not (has_credentials or has_cards or has_pii):
35805
- click.echo(f" 📄 Application data extracted")
35806
- else:
35807
- click.echo(f" {click.style('No data extracted yet', fg='yellow')}")
35808
- click.echo()
35809
- click.echo(
35810
- " 💡 Tables discovered but not yet dumped. Select a table to dump data."
35811
- )
36642
+ # Database structure
36643
+ if not all_databases and not all_tables:
36644
+ click.echo(" No database structure discovered yet.")
36645
+ click.echo(" 💡 Run SQLMap with --tables to enumerate tables.")
36646
+ return
35812
36647
 
36648
+ # Summary line
36649
+ total_tables = len(all_table_list)
36650
+ dumped_count = len(all_dumped)
36651
+ click.echo(
36652
+ f" {click.style('Total:', bold=True)} {total_tables} tables | "
36653
+ f"{click.style('Extracted:', bold=True, fg='green')} {dumped_count}"
36654
+ )
35813
36655
  click.echo()
35814
36656
 
35815
- # Show credential preview if available
35816
- if has_credentials:
35817
- click.echo(click.style(" 🔑 CREDENTIALS PREVIEW", bold=True, fg="red"))
36657
+ # Show database structure with Rich table and cursor
36658
+ for db in sorted(all_databases) if all_databases else list(all_tables.keys()):
36659
+ dbms_type = db_to_dbms.get(db, "SQLite")
36660
+ tables = all_tables.get(db, set())
36661
+
36662
+ click.echo(f" {click.style('📂 ' + db, bold=True, fg='cyan')} ({dbms_type})")
35818
36663
  click.echo()
35819
36664
 
35820
- for table_key, data in all_dumped.items():
35821
- if any(x in table_key.lower() for x in ["user", "account", "login"]):
35822
- rows = data.get("rows", [])
35823
- columns = data.get("columns", [])
36665
+ if tables:
36666
+ # Create Rich table with cursor column
36667
+ table = Table(
36668
+ show_header=True,
36669
+ header_style="bold cyan",
36670
+ box=DesignSystem.TABLE_BOX,
36671
+ padding=(0, 1),
36672
+ expand=True,
36673
+ )
35824
36674
 
35825
- # Find credential columns
35826
- user_col = next(
35827
- (
35828
- c
35829
- for c in columns
35830
- if any(
35831
- x in c.lower()
35832
- for x in ["username", "user", "login", "email"]
35833
- )
35834
- ),
35835
- None,
35836
- )
35837
- pass_col = next(
36675
+ table.add_column("", width=3, no_wrap=True) # Cursor
36676
+ table.add_column("○", width=3, justify="center", no_wrap=True)
36677
+ table.add_column("Table Name", width=28, no_wrap=True)
36678
+ table.add_column("Cols", width=6, justify="right", no_wrap=True)
36679
+ table.add_column("Rows", width=12, justify="right", no_wrap=True)
36680
+ table.add_column("Type", width=12, no_wrap=True)
36681
+
36682
+ sorted_tables = sorted(tables)
36683
+ for tbl in sorted_tables:
36684
+ table_key = f"{db}.{tbl}"
36685
+ cols = all_columns.get(table_key, set())
36686
+
36687
+ # Find this table's index in all_table_list
36688
+ tbl_idx = next(
35838
36689
  (
35839
- c
35840
- for c in columns
35841
- if any(
35842
- x in c.lower()
35843
- for x in ["password", "passwd", "hash", "pwd"]
35844
- )
36690
+ i
36691
+ for i, t in enumerate(all_table_list)
36692
+ if t["table"] == tbl and t["db"] == db
35845
36693
  ),
35846
- None,
36694
+ -1,
35847
36695
  )
35848
- email_col = next((c for c in columns if "email" in c.lower()), None)
36696
+ is_selected = tbl_idx == cursor_pos
35849
36697
 
35850
- if user_col or email_col:
35851
- # Show first 5 credentials
35852
- table = Table(
35853
- show_header=True,
35854
- header_style="bold red",
35855
- box=None,
35856
- padding=(0, 2),
35857
- )
35858
- if user_col:
35859
- table.add_column("Username", style="yellow")
35860
- if email_col and email_col != user_col:
35861
- table.add_column("Email", style="cyan")
35862
- if pass_col:
35863
- table.add_column("Password/Hash", style="red")
35864
-
35865
- for row in rows[:5]:
35866
- row_data = []
35867
- if user_col:
35868
- val = str(row.get(user_col, ""))[:30]
35869
- row_data.append(
35870
- val
35871
- if val and val not in ["<blank>", "None", "NULL"]
35872
- else "-"
35873
- )
35874
- if email_col and email_col != user_col:
35875
- val = str(row.get(email_col, ""))[:35]
35876
- row_data.append(val if val else "-")
35877
- if pass_col:
35878
- val = str(row.get(pass_col, ""))[:40]
35879
- row_data.append(val if val else "-")
35880
- if row_data:
35881
- table.add_row(*row_data)
36698
+ # Cursor indicator
36699
+ cursor = "▶" if is_selected else ""
35882
36700
 
35883
- console.print(table)
36701
+ # Check if this table has been dumped
36702
+ dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
36703
+ has_data = dumped_key is not None
35884
36704
 
35885
- if len(rows) > 5:
35886
- click.echo(f" ... and {len(rows) - 5} more credentials")
35887
- click.echo()
35888
- break # Only show first credential table in preview
36705
+ # Determine category
36706
+ tbl_lower = tbl.lower()
36707
+ if any(
36708
+ x in tbl_lower
36709
+ for x in ["user", "account", "login", "credential", "password"]
36710
+ ):
36711
+ category = "🔑 Creds"
36712
+ elif any(x in tbl_lower for x in ["card", "payment", "credit"]):
36713
+ category = "💳 Cards"
36714
+ elif any(
36715
+ x in tbl_lower
36716
+ for x in ["address", "personal", "customer", "profile"]
36717
+ ):
36718
+ category = "👤 PII"
36719
+ else:
36720
+ category = ""
36721
+
36722
+ col_count = len(cols) if cols else 0
36723
+
36724
+ if has_data:
36725
+ row_count = all_dumped[dumped_key].get(
36726
+ "row_count", len(all_dumped[dumped_key].get("rows", []))
36727
+ )
36728
+ if is_selected:
36729
+ table.add_row(
36730
+ f"[bold yellow]{cursor}[/bold yellow]",
36731
+ "[bold yellow]○[/bold yellow]",
36732
+ f"[bold yellow]{tbl}[/bold yellow]",
36733
+ f"[bold yellow]{col_count}[/bold yellow]",
36734
+ f"[bold yellow]✓ {row_count}[/bold yellow]",
36735
+ f"[bold yellow]{category}[/bold yellow]",
36736
+ )
36737
+ else:
36738
+ table.add_row(
36739
+ cursor,
36740
+ "○",
36741
+ f"[green bold]{tbl}[/green bold]",
36742
+ str(col_count),
36743
+ f"[green]✓ {row_count}[/green]",
36744
+ category,
36745
+ )
36746
+ else:
36747
+ if is_selected:
36748
+ table.add_row(
36749
+ f"[bold yellow]{cursor}[/bold yellow]",
36750
+ "[bold yellow]○[/bold yellow]",
36751
+ f"[bold yellow]{tbl}[/bold yellow]",
36752
+ f"[bold yellow]{col_count}[/bold yellow]",
36753
+ "[bold yellow]-[/bold yellow]",
36754
+ f"[bold yellow]{category}[/bold yellow]",
36755
+ )
36756
+ else:
36757
+ table.add_row(
36758
+ cursor,
36759
+ "○",
36760
+ tbl,
36761
+ str(col_count),
36762
+ "[dim]-[/dim]",
36763
+ category,
36764
+ )
36765
+
36766
+ console.print(table)
35889
36767
 
35890
- # Show card preview if available
35891
- if has_cards:
35892
- click.echo(click.style(" 💳 PAYMENT DATA PREVIEW", bold=True, fg="red"))
35893
36768
  click.echo()
35894
36769
 
35895
- for table_key, data in all_dumped.items():
35896
- if any(x in table_key.lower() for x in ["card", "payment", "credit"]):
35897
- rows = data.get("rows", [])
35898
- columns = data.get("columns", [])
35899
36770
 
35900
- # Find card columns
35901
- name_col = next(
35902
- (
35903
- c
35904
- for c in columns
35905
- if any(x in c.lower() for x in ["name", "holder", "fullname"])
35906
- ),
35907
- None,
35908
- )
35909
- card_col = next(
35910
- (
35911
- c
35912
- for c in columns
35913
- if any(
35914
- x in c.lower()
35915
- for x in ["cardnum", "card_number", "ccnumber", "number"]
35916
- )
35917
- ),
35918
- None,
35919
- )
35920
- exp_col = next(
35921
- (
35922
- c
35923
- for c in columns
35924
- if any(x in c.lower() for x in ["exp", "expir"])
35925
- ),
35926
- None,
35927
- )
36771
+ def _sqli_detail_overview(
36772
+ inj, all_databases, all_tables, all_columns, db_to_dbms, all_dumped, console
36773
+ ):
36774
+ """Show overview of SQLi exploitation results with database structure."""
36775
+ from rich.table import Table
35928
36776
 
35929
- table = Table(
35930
- show_header=True, header_style="bold red", box=None, padding=(0, 2)
35931
- )
35932
- if name_col:
35933
- table.add_column("Cardholder", style="yellow")
35934
- if card_col:
35935
- table.add_column("Card Number", style="red")
35936
- if exp_col:
35937
- table.add_column("Expiry", style="cyan")
35938
-
35939
- for row in rows[:5]:
35940
- row_data = []
35941
- if name_col:
35942
- row_data.append(str(row.get(name_col, "-"))[:25])
35943
- if card_col:
35944
- row_data.append(str(row.get(card_col, "-")))
35945
- if exp_col:
35946
- row_data.append(str(row.get(exp_col, "-")))
35947
- if row_data:
35948
- table.add_row(*row_data)
36777
+ # Count extracted data by category
36778
+ cred_count = 0
36779
+ card_count = 0
36780
+ pii_count = 0
36781
+
36782
+ for key, data in all_dumped.items():
36783
+ row_count = data.get("row_count", len(data.get("rows", [])))
36784
+ key_lower = key.lower()
36785
+ if any(x in key_lower for x in ["user", "account", "login", "credential"]):
36786
+ cred_count += row_count
36787
+ elif any(x in key_lower for x in ["card", "payment", "credit"]):
36788
+ card_count += row_count
36789
+ elif any(
36790
+ x in key_lower for x in ["address", "personal", "customer", "profile"]
36791
+ ):
36792
+ pii_count += row_count
36793
+
36794
+ # Summary alert if data extracted
36795
+ if cred_count > 0 or card_count > 0 or pii_count > 0:
36796
+ alerts = []
36797
+ if cred_count > 0:
36798
+ alerts.append(f"🔑 {cred_count} credentials")
36799
+ if card_count > 0:
36800
+ alerts.append(f"💳 {card_count} cards")
36801
+ if pii_count > 0:
36802
+ alerts.append(f"👤 {pii_count} PII records")
36803
+ click.echo(
36804
+ click.style(" ⚠️ EXTRACTED: ", fg="red", bold=True)
36805
+ + click.style(" | ".join(alerts), fg="red")
36806
+ )
36807
+ click.echo()
35949
36808
 
35950
- console.print(table)
36809
+ # Database structure
36810
+ if not all_databases and not all_tables:
36811
+ click.echo(" No database structure discovered yet.")
36812
+ click.echo(" 💡 Run SQLMap with --tables to enumerate tables.")
36813
+ return
35951
36814
 
35952
- if len(rows) > 5:
35953
- click.echo(f" ... and {len(rows) - 5} more cards")
35954
- click.echo()
35955
- break
36815
+ # Show database structure with Rich table (like Host Management)
36816
+ for db in sorted(all_databases) if all_databases else list(all_tables.keys()):
36817
+ dbms_type = db_to_dbms.get(db, "SQLite")
36818
+ tables = all_tables.get(db, set())
35956
36819
 
35957
- # Show PII/address preview if available
35958
- if has_pii:
35959
- click.echo(click.style(" 👤 PERSONAL DATA PREVIEW", bold=True, fg="yellow"))
36820
+ click.echo(f" {click.style('📂 ' + db, bold=True, fg='cyan')} ({dbms_type})")
35960
36821
  click.echo()
35961
36822
 
35962
- for table_key, data in all_dumped.items():
35963
- if any(
35964
- x in table_key.lower()
35965
- for x in ["address", "personal", "customer", "profile"]
35966
- ):
35967
- rows = data.get("rows", [])
35968
- columns = data.get("columns", [])
36823
+ if tables:
36824
+ # Create Rich table like Host Management
36825
+ table = Table(
36826
+ show_header=True,
36827
+ header_style="bold cyan",
36828
+ box=DesignSystem.TABLE_BOX,
36829
+ padding=(0, 1),
36830
+ expand=True,
36831
+ )
35969
36832
 
35970
- # Find PII columns
35971
- name_col = next(
35972
- (
35973
- c
35974
- for c in columns
35975
- if any(
35976
- x in c.lower() for x in ["name", "fullname", "full_name"]
35977
- )
35978
- ),
35979
- None,
35980
- )
35981
- city_col = next((c for c in columns if "city" in c.lower()), None)
35982
- country_col = next((c for c in columns if "country" in c.lower()), None)
35983
- address_col = next(
35984
- (
35985
- c
35986
- for c in columns
35987
- if any(x in c.lower() for x in ["address", "street"])
35988
- ),
35989
- None,
35990
- )
35991
- phone_col = next(
35992
- (
35993
- c
35994
- for c in columns
35995
- if any(x in c.lower() for x in ["phone", "mobile", "tel"])
35996
- ),
35997
- None,
35998
- )
35999
- zip_col = next(
36000
- (
36001
- c
36002
- for c in columns
36003
- if any(x in c.lower() for x in ["zip", "postal"])
36004
- ),
36005
- None,
36006
- )
36833
+ table.add_column("○", width=5, justify="center", no_wrap=True)
36834
+ table.add_column("Table Name", width=28, no_wrap=True)
36835
+ table.add_column("Cols", width=6, justify="right", no_wrap=True)
36836
+ table.add_column("Rows", width=12, justify="right", no_wrap=True)
36837
+ table.add_column("Type", width=12, no_wrap=True)
36007
36838
 
36008
- table = Table(
36009
- show_header=True,
36010
- header_style="bold yellow",
36011
- box=None,
36012
- padding=(0, 2),
36013
- )
36014
- if name_col:
36015
- table.add_column("Name", style="yellow")
36016
- if address_col:
36017
- table.add_column("Address", style="white")
36018
- if city_col:
36019
- table.add_column("City", style="cyan")
36020
- if country_col:
36021
- table.add_column("Country", style="cyan")
36022
- if zip_col:
36023
- table.add_column("ZIP", style="dim")
36024
- if phone_col:
36025
- table.add_column("Phone", style="green")
36026
-
36027
- for row in rows[:5]:
36028
- row_data = []
36029
- if name_col:
36030
- row_data.append(str(row.get(name_col, "-"))[:25])
36031
- if address_col:
36032
- row_data.append(str(row.get(address_col, "-"))[:30])
36033
- if city_col:
36034
- row_data.append(str(row.get(city_col, "-"))[:15])
36035
- if country_col:
36036
- row_data.append(str(row.get(country_col, "-"))[:15])
36037
- if zip_col:
36038
- row_data.append(str(row.get(zip_col, "-"))[:10])
36039
- if phone_col:
36040
- row_data.append(str(row.get(phone_col, "-"))[:15])
36041
- if row_data:
36042
- table.add_row(*row_data)
36839
+ sorted_tables = sorted(tables)
36840
+ for tbl in sorted_tables:
36841
+ table_key = f"{db}.{tbl}"
36842
+ cols = all_columns.get(table_key, set())
36043
36843
 
36044
- console.print(table)
36844
+ # Check if this table has been dumped
36845
+ dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
36846
+ has_data = dumped_key is not None
36045
36847
 
36046
- if len(rows) > 5:
36047
- click.echo(f" ... and {len(rows) - 5} more records")
36048
- click.echo()
36049
- break
36848
+ # Determine category
36849
+ tbl_lower = tbl.lower()
36850
+ if any(
36851
+ x in tbl_lower
36852
+ for x in ["user", "account", "login", "credential", "password"]
36853
+ ):
36854
+ category = "🔑 Creds"
36855
+ elif any(x in tbl_lower for x in ["card", "payment", "credit"]):
36856
+ category = "💳 Cards"
36857
+ elif any(
36858
+ x in tbl_lower
36859
+ for x in ["address", "personal", "customer", "profile"]
36860
+ ):
36861
+ category = "👤 PII"
36862
+ else:
36863
+ category = ""
36864
+
36865
+ col_count = len(cols) if cols else 0
36866
+
36867
+ if has_data:
36868
+ row_count = all_dumped[dumped_key].get(
36869
+ "row_count", len(all_dumped[dumped_key].get("rows", []))
36870
+ )
36871
+ table.add_row(
36872
+ "○",
36873
+ f"[green bold]{tbl}[/green bold]",
36874
+ str(col_count),
36875
+ f"[green]✓ {row_count}[/green]",
36876
+ category,
36877
+ )
36878
+ else:
36879
+ table.add_row(
36880
+ "○",
36881
+ tbl,
36882
+ str(col_count),
36883
+ "[dim]-[/dim]",
36884
+ category,
36885
+ )
36886
+
36887
+ console.print(table)
36888
+
36889
+ click.echo()
36050
36890
 
36051
36891
 
36052
36892
  def _sqli_detail_tables(
36053
36893
  all_databases, all_tables, all_columns, db_to_dbms, all_dumped, console
36054
36894
  ):
36055
36895
  """Show database structure overview."""
36056
- from rich.text import Text
36057
- from rich.tree import Tree
36896
+ from rich.table import Table
36058
36897
 
36059
36898
  click.echo(click.style(" 📁 DATABASE STRUCTURE", bold=True, fg="cyan"))
36060
36899
  click.echo()
@@ -36064,14 +36903,29 @@ def _sqli_detail_tables(
36064
36903
  click.echo(" 💡 Run SQLMap with --tables to enumerate tables.")
36065
36904
  return
36066
36905
 
36067
- # Build tree structure
36068
36906
  for db in sorted(all_databases) if all_databases else list(all_tables.keys()):
36069
36907
  dbms_type = db_to_dbms.get(db, "SQLite")
36070
36908
  tables = all_tables.get(db, set())
36071
36909
 
36072
36910
  click.echo(f" {click.style('📂 ' + db, bold=True, fg='cyan')} ({dbms_type})")
36911
+ click.echo()
36073
36912
 
36074
36913
  if tables:
36914
+ # Create Rich table like Host Management
36915
+ table = Table(
36916
+ show_header=True,
36917
+ header_style="bold cyan",
36918
+ box=DesignSystem.TABLE_BOX,
36919
+ padding=(0, 1),
36920
+ expand=True,
36921
+ )
36922
+
36923
+ table.add_column("○", width=5, justify="center", no_wrap=True)
36924
+ table.add_column("Table Name", width=28, no_wrap=True)
36925
+ table.add_column("Cols", width=6, justify="right", no_wrap=True)
36926
+ table.add_column("Rows", width=10, justify="right", no_wrap=True)
36927
+ table.add_column("Type", width=12, no_wrap=True)
36928
+
36075
36929
  sorted_tables = sorted(tables)
36076
36930
  for tbl in sorted_tables:
36077
36931
  table_key = f"{db}.{tbl}"
@@ -36081,55 +36935,74 @@ def _sqli_detail_tables(
36081
36935
  dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
36082
36936
  has_data = dumped_key is not None
36083
36937
 
36084
- # Highlight interesting tables
36085
- is_sensitive = any(
36086
- x in tbl.lower()
36087
- for x in [
36088
- "user",
36089
- "account",
36090
- "password",
36091
- "card",
36092
- "payment",
36093
- "credential",
36094
- "admin",
36095
- "secret",
36096
- ]
36097
- )
36938
+ # Determine category
36939
+ tbl_lower = tbl.lower()
36940
+ if any(
36941
+ x in tbl_lower
36942
+ for x in ["user", "account", "login", "credential", "password"]
36943
+ ):
36944
+ category = "🔑 Creds"
36945
+ elif any(x in tbl_lower for x in ["card", "payment", "credit"]):
36946
+ category = "💳 Cards"
36947
+ elif any(
36948
+ x in tbl_lower
36949
+ for x in ["address", "personal", "customer", "profile"]
36950
+ ):
36951
+ category = "👤 PII"
36952
+ else:
36953
+ category = ""
36954
+
36955
+ col_count = len(cols) if cols else 0
36098
36956
 
36099
36957
  if has_data:
36100
36958
  row_count = all_dumped[dumped_key].get("row_count", 0)
36101
- status = click.style(f"✓ {row_count} rows", fg="green")
36102
- else:
36103
- status = click.style(" not dumped", fg="yellow", dim=True)
36104
-
36105
- if is_sensitive:
36106
- click.echo(
36107
- f" ├─ {click.style(tbl, fg='red', bold=True)} ({len(cols)} cols) {status}"
36959
+ table.add_row(
36960
+ "○",
36961
+ f"[green bold]{tbl}[/green bold]",
36962
+ str(col_count),
36963
+ f"[green]✓ {row_count}[/green]",
36964
+ category,
36108
36965
  )
36109
36966
  else:
36110
- click.echo(f" ├─ {tbl} ({len(cols)} cols) {status}")
36967
+ table.add_row(
36968
+ "○",
36969
+ tbl,
36970
+ str(col_count),
36971
+ "[dim]-[/dim]",
36972
+ category,
36973
+ )
36111
36974
 
36112
- # Show columns for dumped tables
36113
- if has_data and cols:
36114
- col_list = sorted(cols)[:8]
36115
- col_str = ", ".join(col_list)
36116
- if len(cols) > 8:
36117
- col_str += f" +{len(cols) - 8} more"
36118
- click.echo(f" │ └─ {click.style(col_str, dim=True)}")
36975
+ console.print(table)
36119
36976
 
36120
36977
  click.echo()
36121
36978
 
36122
36979
 
36123
- def _sqli_detail_data_table(table_key: str, data: dict, console, width: int):
36124
- """Show full data for a specific table."""
36980
+ def _sqli_detail_data_table(
36981
+ table_key: str,
36982
+ data: dict,
36983
+ console,
36984
+ width: int,
36985
+ page: int = 1,
36986
+ page_size: int = 20,
36987
+ view_all: bool = False,
36988
+ ):
36989
+ """Show full data for a specific table with pagination."""
36125
36990
  from rich.table import Table
36126
36991
 
36127
36992
  rows = data.get("rows", [])
36128
36993
  columns = data.get("columns", [])
36129
36994
  row_count = data.get("row_count", len(rows))
36995
+ total_pages = (len(rows) + page_size - 1) // page_size if not view_all else 1
36130
36996
 
36131
36997
  click.echo(click.style(f" 📋 TABLE: {table_key}", bold=True, fg="green"))
36132
- click.echo(f" {row_count} records | {len(columns)} columns")
36998
+ if view_all:
36999
+ click.echo(
37000
+ f" {row_count} records | {len(columns)} columns | Showing ALL"
37001
+ )
37002
+ else:
37003
+ click.echo(
37004
+ f" {row_count} records | {len(columns)} columns | Page {page}/{total_pages}"
37005
+ )
36133
37006
  click.echo()
36134
37007
 
36135
37008
  if not rows:
@@ -36192,8 +37065,14 @@ def _sqli_detail_data_table(table_key: str, data: dict, console, width: int):
36192
37065
  else:
36193
37066
  table.add_column(col, max_width=col_width, overflow="ellipsis")
36194
37067
 
36195
- # Add rows (limit to 20 for display)
36196
- display_rows = rows[:20]
37068
+ # Add rows with pagination
37069
+ if view_all:
37070
+ display_rows = rows
37071
+ else:
37072
+ start_idx = (page - 1) * page_size
37073
+ end_idx = start_idx + page_size
37074
+ display_rows = rows[start_idx:end_idx]
37075
+
36197
37076
  for row in display_rows:
36198
37077
  row_data = []
36199
37078
  for col in display_cols:
@@ -36206,9 +37085,11 @@ def _sqli_detail_data_table(table_key: str, data: dict, console, width: int):
36206
37085
 
36207
37086
  console.print(table)
36208
37087
 
36209
- if len(rows) > 20:
37088
+ if not view_all and len(rows) > page_size:
37089
+ start_idx = (page - 1) * page_size
37090
+ end_idx = min(start_idx + page_size, len(rows))
36210
37091
  click.echo()
36211
- click.echo(f" ... showing 20 of {len(rows)} rows")
37092
+ click.echo(f" Showing rows {start_idx + 1}-{end_idx} of {len(rows)}")
36212
37093
 
36213
37094
  # Show hidden columns
36214
37095
  if len(columns) > max_cols:
@@ -40131,6 +41012,30 @@ def _launch_interactive_msfconsole(engagement_id: int):
40131
41012
  rc_path = rc.name
40132
41013
 
40133
41014
  try:
41015
+ # Auto-copy database.yml to /root/.msf4/ if needed
41016
+ # When msfdb init runs as normal user, config is in ~/.msf4/
41017
+ # But sudo msfconsole looks in /root/.msf4/, so we need to copy it
41018
+ from pathlib import Path
41019
+
41020
+ user_db = Path.home() / ".msf4" / "database.yml"
41021
+ root_db = Path("/root/.msf4/database.yml")
41022
+ if user_db.exists():
41023
+ # Check if root's config exists (use sudo to check)
41024
+ check_result = subprocess.run(
41025
+ ["sudo", "test", "-f", str(root_db)], capture_output=True
41026
+ )
41027
+ if check_result.returncode != 0:
41028
+ # Root's config doesn't exist, copy it
41029
+ click.echo(
41030
+ click.style(" Configuring database for sudo access...", fg="cyan")
41031
+ )
41032
+ subprocess.run(
41033
+ ["sudo", "mkdir", "-p", "/root/.msf4"], capture_output=True
41034
+ )
41035
+ subprocess.run(
41036
+ ["sudo", "cp", str(user_db), str(root_db)], capture_output=True
41037
+ )
41038
+
40134
41039
  click.pause("Press Enter to launch msfconsole...")
40135
41040
 
40136
41041
  # Launch msfconsole with our resource script
@@ -40284,18 +41189,27 @@ def _ensure_msf_database_ready():
40284
41189
  click.style(" MSF database needs initialization...", fg="yellow")
40285
41190
  )
40286
41191
 
40287
- # Initialize msfdb
41192
+ # Initialize msfdb - distro-aware (Kali needs sudo, Ubuntu doesn't)
41193
+ from souleyez.utils.tool_checker import detect_distro
41194
+
41195
+ distro = detect_distro()
41196
+ use_sudo = distro in ("kali", "parrot")
41197
+
40288
41198
  click.echo(" Running msfdb init (this may take a moment)...")
41199
+ msfdb_cmd = ["sudo", "msfdb", "init"] if use_sudo else ["msfdb", "init"]
40289
41200
  result = subprocess.run(
40290
- ["sudo", "msfdb", "init"], capture_output=True, text=True, timeout=120
41201
+ msfdb_cmd, capture_output=True, text=True, timeout=120
40291
41202
  )
40292
41203
  if result.returncode == 0:
40293
41204
  click.echo(click.style(" ✓ MSF database initialized", fg="green"))
40294
41205
  else:
40295
41206
  # Try msfdb reinit if init fails
40296
41207
  click.echo(click.style(" Init failed, trying reinit...", fg="yellow"))
41208
+ msfdb_reinit_cmd = (
41209
+ ["sudo", "msfdb", "reinit"] if use_sudo else ["msfdb", "reinit"]
41210
+ )
40297
41211
  result = subprocess.run(
40298
- ["sudo", "msfdb", "reinit"],
41212
+ msfdb_reinit_cmd,
40299
41213
  capture_output=True,
40300
41214
  text=True,
40301
41215
  timeout=120,
@@ -40534,7 +41448,14 @@ def _guided_msf_setup():
40534
41448
  click.echo()
40535
41449
  click.echo(" Please run these commands manually:")
40536
41450
  click.echo(click.style(" sudo systemctl start postgresql", fg="cyan"))
40537
- click.echo(click.style(" sudo msfdb init", fg="cyan"))
41451
+ # Show distro-appropriate msfdb command
41452
+ from souleyez.utils.tool_checker import detect_distro
41453
+
41454
+ distro = detect_distro()
41455
+ if distro in ("kali", "parrot"):
41456
+ click.echo(click.style(" sudo msfdb init", fg="cyan"))
41457
+ else:
41458
+ click.echo(click.style(" msfdb init", fg="cyan"))
40538
41459
  click.echo()
40539
41460
  if not click.confirm(" Continue anyway?", default=False):
40540
41461
  click.pause()
@@ -43199,15 +44120,22 @@ def _check_msfdb_ready() -> bool:
43199
44120
  " Without it, you won't be able to store hosts, credentials, or loot."
43200
44121
  )
43201
44122
  click.echo()
44123
+
44124
+ # Distro-aware msfdb init (Kali needs sudo, Ubuntu doesn't)
44125
+ from souleyez.utils.tool_checker import detect_distro
44126
+
44127
+ distro = detect_distro()
44128
+ use_sudo = distro in ("kali", "parrot")
44129
+ msfdb_cmd_str = "sudo msfdb init" if use_sudo else "msfdb init"
44130
+ msfdb_cmd = ["sudo", "msfdb", "init"] if use_sudo else ["msfdb", "init"]
44131
+
43202
44132
  if click.confirm(
43203
- " Initialize database now? (runs: sudo msfdb init)", default=True
44133
+ f" Initialize database now? (runs: {msfdb_cmd_str})", default=True
43204
44134
  ):
43205
44135
  click.echo()
43206
- click.echo(click.style(" Running sudo msfdb init...", fg="cyan"))
44136
+ click.echo(click.style(f" Running {msfdb_cmd_str}...", fg="cyan"))
43207
44137
  try:
43208
- result = subprocess.run(
43209
- ["sudo", "msfdb", "init"], capture_output=False, text=True
43210
- )
44138
+ result = subprocess.run(msfdb_cmd, capture_output=False, text=True)
43211
44139
  if result.returncode == 0:
43212
44140
  click.echo(
43213
44141
  click.style(" Database initialized successfully!", fg="green")