souleyez 3.0.0__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 (325) 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 +101 -70
  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 -2
  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 +1 -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 +3 -18
  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 +57 -7
  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 +3 -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.py +5 -2
  227. souleyez/storage/credentials.py +14 -19
  228. souleyez/storage/crypto.py +7 -4
  229. souleyez/storage/database.py +6 -6
  230. souleyez/storage/db.py +8 -8
  231. souleyez/storage/deliverable_evidence.py +2 -1
  232. souleyez/storage/deliverable_exporter.py +3 -2
  233. souleyez/storage/deliverable_templates.py +2 -1
  234. souleyez/storage/deliverables.py +2 -1
  235. souleyez/storage/engagements.py +6 -4
  236. souleyez/storage/evidence.py +5 -4
  237. souleyez/storage/execution_log.py +4 -2
  238. souleyez/storage/exploit_attempts.py +3 -2
  239. souleyez/storage/exploits.py +3 -1
  240. souleyez/storage/findings.py +3 -1
  241. souleyez/storage/hosts.py +5 -2
  242. souleyez/storage/migrate_to_engagements.py +14 -24
  243. souleyez/storage/migrations/_001_add_credential_enhancements.py +12 -21
  244. souleyez/storage/migrations/_003_add_execution_log.py +8 -13
  245. souleyez/storage/migrations/_005_screenshots.py +2 -4
  246. souleyez/storage/migrations/_006_deliverables.py +2 -4
  247. souleyez/storage/migrations/_007_deliverable_templates.py +4 -8
  248. souleyez/storage/migrations/_008_add_nuclei_table.py +2 -4
  249. souleyez/storage/migrations/_010_evidence_linking.py +6 -12
  250. souleyez/storage/migrations/_012_team_collaboration.py +12 -24
  251. souleyez/storage/migrations/_013_add_host_tags.py +2 -4
  252. souleyez/storage/migrations/_014_exploit_attempts.py +10 -20
  253. souleyez/storage/migrations/_015_add_mac_os_fields.py +4 -8
  254. souleyez/storage/migrations/_016_add_domain_field.py +2 -4
  255. souleyez/storage/migrations/_017_msf_sessions.py +8 -16
  256. souleyez/storage/migrations/_018_add_osint_target.py +4 -8
  257. souleyez/storage/migrations/_019_add_engagement_type.py +4 -8
  258. souleyez/storage/migrations/_020_add_rbac.py +9 -17
  259. souleyez/storage/migrations/_021_wazuh_integration.py +4 -8
  260. souleyez/storage/migrations/_023_fix_detection_results_fk.py +2 -4
  261. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +4 -8
  262. souleyez/storage/migrations/_026_add_engagement_scope.py +4 -8
  263. souleyez/storage/migrations/_027_multi_siem_persistence.py +8 -16
  264. souleyez/storage/migrations/__init__.py +1 -4
  265. souleyez/storage/migrations/migration_manager.py +6 -9
  266. souleyez/storage/msf_sessions.py +1 -1
  267. souleyez/storage/osint.py +3 -1
  268. souleyez/storage/recommendation_engine.py +3 -2
  269. souleyez/storage/screenshots.py +2 -1
  270. souleyez/storage/smb_shares.py +3 -1
  271. souleyez/storage/sqlmap_data.py +6 -4
  272. souleyez/storage/team_collaboration.py +3 -2
  273. souleyez/storage/timeline_tracker.py +2 -1
  274. souleyez/storage/wazuh_vulns.py +3 -1
  275. souleyez/storage/web_paths.py +3 -1
  276. souleyez/testing/credential_tester.py +2 -0
  277. souleyez/ui/__init__.py +2 -1
  278. souleyez/ui/ai_quotes.py +1 -1
  279. souleyez/ui/attack_surface.py +50 -28
  280. souleyez/ui/chain_rules_view.py +6 -3
  281. souleyez/ui/correlation_view.py +3 -2
  282. souleyez/ui/dashboard.py +85 -139
  283. souleyez/ui/deliverables_view.py +1 -1
  284. souleyez/ui/design_system.py +5 -3
  285. souleyez/ui/errors.py +3 -1
  286. souleyez/ui/evidence_linking_view.py +2 -1
  287. souleyez/ui/evidence_vault.py +11 -6
  288. souleyez/ui/exploit_suggestions_view.py +11 -7
  289. souleyez/ui/export_view.py +3 -1
  290. souleyez/ui/gap_analysis_view.py +6 -3
  291. souleyez/ui/help_system.py +4 -1
  292. souleyez/ui/intelligence_view.py +7 -3
  293. souleyez/ui/interactive.py +1280 -558
  294. souleyez/ui/interactive_selector.py +3 -2
  295. souleyez/ui/log_formatter.py +1 -0
  296. souleyez/ui/menu_components.py +3 -1
  297. souleyez/ui/msf_auxiliary_menu.py +4 -1
  298. souleyez/ui/pending_chains_view.py +15 -12
  299. souleyez/ui/progress_indicators.py +5 -2
  300. souleyez/ui/recommendations_view.py +4 -2
  301. souleyez/ui/rule_builder.py +4 -1
  302. souleyez/ui/setup_wizard.py +10 -8
  303. souleyez/ui/shortcuts.py +1 -1
  304. souleyez/ui/splunk_gap_analysis_view.py +7 -4
  305. souleyez/ui/splunk_vulns_view.py +4 -1
  306. souleyez/ui/team_dashboard.py +7 -5
  307. souleyez/ui/template_selector.py +2 -1
  308. souleyez/ui/terminal.py +3 -2
  309. souleyez/ui/timeline_view.py +2 -1
  310. souleyez/ui/tool_setup.py +92 -31
  311. souleyez/ui/tutorial.py +7 -4
  312. souleyez/ui/tutorial_state.py +3 -2
  313. souleyez/ui/wazuh_vulns_view.py +5 -2
  314. souleyez/ui/wordlist_browser.py +4 -3
  315. souleyez/ui.py +13 -7
  316. souleyez/utils/tool_checker.py +61 -12
  317. souleyez/utils.py +4 -4
  318. souleyez/wordlists.py +1 -0
  319. {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/METADATA +1 -1
  320. souleyez-3.0.7.dist-info/RECORD +445 -0
  321. souleyez-3.0.0.dist-info/RECORD +0 -443
  322. {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/WHEEL +0 -0
  323. {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/entry_points.txt +0 -0
  324. {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/licenses/LICENSE +0 -0
  325. {souleyez-3.0.0.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
  )
@@ -14781,7 +14987,11 @@ def view_job_detail(job_id: int):
14781
14987
  creds = parse_result.get("credentials", [])
14782
14988
  # Check for SSH or telnet credentials
14783
14989
  for cred in creds if isinstance(creds, list) else []:
14784
- service = cred.get("service", "").lower() if isinstance(cred, dict) else ""
14990
+ service = (
14991
+ cred.get("service", "").lower()
14992
+ if isinstance(cred, dict)
14993
+ else ""
14994
+ )
14785
14995
  if service in ["ssh", "telnet"]:
14786
14996
  can_spawn_shell = True
14787
14997
  if service == "ssh":
@@ -14801,7 +15011,10 @@ def view_job_detail(job_id: int):
14801
15011
  try:
14802
15012
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
14803
15013
  log_content = f.read()
14804
- if "session" in log_content.lower() and "opened" in log_content.lower():
15014
+ if (
15015
+ "session" in log_content.lower()
15016
+ and "opened" in log_content.lower()
15017
+ ):
14805
15018
  can_spawn_shell = True
14806
15019
  # Check if SSH or telnet
14807
15020
  if "ssh" in log_content.lower():
@@ -15053,7 +15266,9 @@ def view_job_detail(job_id: int):
15053
15266
  # Use first credential
15054
15267
  first_cred = creds[0]
15055
15268
  if isinstance(first_cred, dict):
15056
- username = first_cred.get("username") or first_cred.get("login")
15269
+ username = first_cred.get("username") or first_cred.get(
15270
+ "login"
15271
+ )
15057
15272
  password = first_cred.get("password")
15058
15273
 
15059
15274
  if not username or (not password and not nt_hash):
@@ -15120,7 +15335,9 @@ def view_job_detail(job_id: int):
15120
15335
  click.echo(f" User: {username}")
15121
15336
  click.echo(f" Pass: {password}")
15122
15337
  click.echo()
15123
- click.echo(" Launching telnet... Enter password when prompted.")
15338
+ click.echo(
15339
+ " Launching telnet... Enter password when prompted."
15340
+ )
15124
15341
  click.echo()
15125
15342
  shell_cmd = f"telnet {target}"
15126
15343
  else:
@@ -15155,7 +15372,9 @@ def view_job_detail(job_id: int):
15155
15372
 
15156
15373
  elif tool_name == "hydra":
15157
15374
  # Hydra found valid credentials - determine service type
15158
- service_type = parse_result.get("service", "").lower() if parse_result else ""
15375
+ service_type = (
15376
+ parse_result.get("service", "").lower() if parse_result else ""
15377
+ )
15159
15378
 
15160
15379
  if service_type == "ssh" or is_ssh_shell:
15161
15380
  # SSH - use sshpass
@@ -15171,7 +15390,9 @@ def view_job_detail(job_id: int):
15171
15390
  click.echo(f" User: {username}")
15172
15391
  click.echo(f" Pass: {password}")
15173
15392
  click.echo()
15174
- click.echo(" Launching telnet... Enter password when prompted.")
15393
+ click.echo(
15394
+ " Launching telnet... Enter password when prompted."
15395
+ )
15175
15396
  click.echo()
15176
15397
  shell_cmd = f"telnet {target}"
15177
15398
  elif service_type == "ftp":
@@ -15253,9 +15474,7 @@ def view_job_detail(job_id: int):
15253
15474
  if shell_choice == "1":
15254
15475
  # evil-winrm
15255
15476
  if nt_hash:
15256
- shell_cmd = (
15257
- f"evil-winrm -i {target} -u '{username}' -H '{nt_hash}'"
15258
- )
15477
+ shell_cmd = f"evil-winrm -i {target} -u '{username}' -H '{nt_hash}'"
15259
15478
  else:
15260
15479
  shell_cmd = f"evil-winrm -i {target} -u '{username}' -p '{password}'"
15261
15480
  elif shell_choice == "2":
@@ -15263,9 +15482,7 @@ def view_job_detail(job_id: int):
15263
15482
  if nt_hash:
15264
15483
  shell_cmd = f"impacket-psexec '{cred_prefix}@{target}' -hashes ':{nt_hash}'"
15265
15484
  else:
15266
- shell_cmd = (
15267
- f"impacket-psexec '{cred_prefix}:{password}@{target}'"
15268
- )
15485
+ shell_cmd = f"impacket-psexec '{cred_prefix}:{password}@{target}'"
15269
15486
  elif shell_choice == "3":
15270
15487
  # SSH with sshpass
15271
15488
  shell_cmd = f"sshpass -p '{password}' ssh -o StrictHostKeyChecking=no -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa {username}@{target}"
@@ -17043,6 +17260,10 @@ def _license_management_menu():
17043
17260
  _bypass_validation=True, # Already validated
17044
17261
  )
17045
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)
17046
17267
  click.echo(
17047
17268
  click.style(
17048
17269
  f" ✓ User '{user.username}' upgraded to PRO tier",
@@ -17052,8 +17273,8 @@ def _license_management_menu():
17052
17273
  click.echo()
17053
17274
  click.echo(
17054
17275
  click.style(
17055
- " 🔐 Re-authenticate to unlock PRO capabilities.",
17056
- fg="cyan",
17276
+ " 💎 PRO features now unlocked!",
17277
+ fg="green",
17057
17278
  )
17058
17279
  )
17059
17280
  else:
@@ -17188,13 +17409,18 @@ def _license_management_menu():
17188
17409
 
17189
17410
  # Reset ALL users with PRO tier to FREE
17190
17411
  try:
17191
- from souleyez.auth import UserManager
17412
+ from souleyez.auth import (
17413
+ UserManager,
17414
+ get_session_manager,
17415
+ )
17192
17416
  from souleyez.storage.database import get_db
17193
17417
 
17194
17418
  user_mgr = UserManager(get_db().db_path)
17195
17419
  count, _ = user_mgr.reset_all_pro_tiers()
17196
17420
  if count > 0:
17197
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)
17198
17424
  except Exception:
17199
17425
  pass
17200
17426
  else:
@@ -23629,61 +23855,6 @@ def manage_credentials_menu():
23629
23855
  break
23630
23856
 
23631
23857
 
23632
- def view_additional_data_menu():
23633
- """Additional data viewing menu for OSINT and Web Paths."""
23634
- em = EngagementManager()
23635
- current_ws = em.get_current()
23636
-
23637
- if not current_ws:
23638
- click.echo(click.style("No engagement selected!", fg="red"))
23639
- click.pause()
23640
- return
23641
-
23642
- engagement_id = current_ws["id"]
23643
-
23644
- while True:
23645
- DesignSystem.clear_screen()
23646
- click.echo("\n" + "=" * 70)
23647
- click.echo("ADDITIONAL DATA")
23648
- click.echo("=" * 70 + "\n")
23649
-
23650
- click.echo(" 1. Web Paths - View and manage discovered web paths")
23651
- click.echo(" 2. SMB Shares - View and manage enumerated SMB shares")
23652
- click.echo(
23653
- " 3. SQLMap Data - View SQL injection discoveries and databases"
23654
- )
23655
- click.echo()
23656
- click.echo(" [q] ← Back")
23657
- click.echo()
23658
-
23659
- try:
23660
- choice_input = click.prompt(
23661
- "Select data type", type=str, default="q"
23662
- ).strip()
23663
-
23664
- if choice_input == "q":
23665
- return
23666
- try:
23667
- choice = int(choice_input)
23668
- except ValueError:
23669
- click.echo(click.style("Invalid selection!", fg="red"))
23670
- click.pause()
23671
- continue
23672
-
23673
- if choice == 1:
23674
- view_web_paths(engagement_id)
23675
- elif choice == 2:
23676
- view_smb_shares(engagement_id)
23677
- elif choice == 3:
23678
- view_sqlmap_data(engagement_id)
23679
- else:
23680
- click.echo(click.style("Invalid selection!", fg="red"))
23681
- click.pause()
23682
-
23683
- except (KeyboardInterrupt, click.Abort):
23684
- return
23685
-
23686
-
23687
23858
  def view_additional_data_menu():
23688
23859
  """Additional data viewing menu for OSINT and Web Paths."""
23689
23860
  em = EngagementManager()
@@ -27732,7 +27903,13 @@ def view_credentials(engagement_id: int):
27732
27903
  return
27733
27904
 
27734
27905
  # Active filters
27735
- 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
+ }
27736
27913
 
27737
27914
  # Pagination
27738
27915
  PAGE_SIZE = 20
@@ -27780,6 +27957,14 @@ def view_credentials(engagement_id: int):
27780
27957
  if filters["tool"] and credentials:
27781
27958
  credentials = [c for c in credentials if c.get("tool") == filters["tool"]]
27782
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
+
27783
27968
  # Pagination calculations
27784
27969
  total_creds = len(credentials) if credentials else 0
27785
27970
  total_pages = (
@@ -27903,6 +28088,7 @@ def view_credentials(engagement_id: int):
27903
28088
  click.echo(" [a] Add - Manually add credential")
27904
28089
  click.echo(" [f] Filter - Filter by service/status/host")
27905
28090
  click.echo(" [o] Tool - Filter by tool")
28091
+ click.echo(" [y] Type - Filter by credential type (password/hash/username)")
27906
28092
  click.echo(" [c] Clear filters - Reset all filters")
27907
28093
  click.echo(" [q] Back")
27908
28094
  click.echo()
@@ -28004,6 +28190,12 @@ def view_credentials(engagement_id: int):
28004
28190
  # Filter by tool
28005
28191
  filters["tool"] = _filter_credential_by_tool(engagement_id, cm)
28006
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
28007
28199
  elif choice == "c":
28008
28200
  filters = {k: None for k in filters}
28009
28201
  current_page = 1
@@ -28308,6 +28500,50 @@ def _filter_credential_by_tool(engagement_id: int, cm: "CredentialsManager"):
28308
28500
  return None
28309
28501
 
28310
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
+
28311
28547
  def _filter_credential_by_host(engagement_id: int):
28312
28548
  """Prompt for host filter."""
28313
28549
  from souleyez.storage.hosts import HostManager
@@ -31271,7 +31507,9 @@ def _execute_webpath_quick_action(engagement_id: int, paths: list, hm: "HostMana
31271
31507
  " ⚙️ Launching Gobuster for test directories...", fg="yellow"
31272
31508
  )
31273
31509
  )
31274
- click.echo(click.style(" ℹ️ Gobuster integration coming soon!", fg="cyan"))
31510
+ click.echo(
31511
+ click.style(" ℹ️ Gobuster integration coming soon!", fg="cyan")
31512
+ )
31275
31513
  click.echo(" For now, manually run:")
31276
31514
  for path in test_dirs[:3]:
31277
31515
  url = path.get("url", "")
@@ -34820,7 +35058,7 @@ def _view_sqlmap_dumped_data(engagement_id: int):
34820
35058
 
34821
35059
  for table in tables_with_dumps:
34822
35060
  console.print(
34823
- f" └─ [cyan]{table['table_name']}[/cyan] ({table['row_count']} rows)"
35061
+ f" [cyan]{table['table_name']}[/cyan] ({table['row_count']} rows)"
34824
35062
  )
34825
35063
 
34826
35064
  # Get dumped data
@@ -35473,18 +35711,24 @@ def view_sqlmap_data(engagement_id: int):
35473
35711
  all_columns[key] = set()
35474
35712
  all_columns[key].update(columns)
35475
35713
 
35476
- # Aggregate dumped data
35714
+ # Aggregate dumped data (track by target URL)
35715
+ job_target = job.get("target", "")
35477
35716
  if parsed.get("dumped_data"):
35478
35717
  for key, data in parsed["dumped_data"].items():
35479
- if key not in all_dumped:
35480
- 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
35481
35723
  else:
35482
35724
  # Merge rows if same table dumped multiple times
35483
- existing_rows = all_dumped[key].get("rows", [])
35725
+ existing_rows = all_dumped[target_key].get("rows", [])
35484
35726
  new_rows = data.get("rows", [])
35485
- all_dumped[key]["rows"] = existing_rows + new_rows
35486
- all_dumped[key]["row_count"] = len(
35487
- 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"]
35488
35732
  )
35489
35733
 
35490
35734
  for vuln in parsed.get("vulnerabilities", []):
@@ -35639,13 +35883,51 @@ def view_sqlmap_data(engagement_id: int):
35639
35883
  row_num = int(choice)
35640
35884
  if 1 <= row_num <= len(filtered_injections):
35641
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
+
35642
35924
  _view_sqlmap_injection_detail(
35643
35925
  inj,
35644
- all_databases,
35645
- all_tables,
35646
- all_columns,
35647
- db_to_dbms,
35648
- all_dumped,
35926
+ filtered_dbs,
35927
+ filtered_tables,
35928
+ filtered_columns,
35929
+ filtered_db_to_dbms,
35930
+ filtered_dumped,
35649
35931
  engagement_id,
35650
35932
  )
35651
35933
  else:
@@ -35820,12 +36102,46 @@ def _view_sqlmap_injection_detail(
35820
36102
  from rich.console import Console
35821
36103
  from rich.table import Table
35822
36104
 
36105
+ from souleyez.ui.interactive_selector import _get_key
36106
+
35823
36107
  console = Console()
35824
36108
 
35825
36109
  if all_dumped is None:
35826
36110
  all_dumped = {}
35827
36111
 
35828
- 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
+ )
35829
36145
 
35830
36146
  while True:
35831
36147
  DesignSystem.clear_screen()
@@ -35855,412 +36171,729 @@ def _view_sqlmap_injection_detail(
35855
36171
  )
35856
36172
  click.echo()
35857
36173
 
35858
- if current_view == "overview":
35859
- _sqli_detail_overview(
35860
- inj,
35861
- all_databases,
35862
- all_tables,
35863
- all_columns,
35864
- db_to_dbms,
36174
+ if current_view == "databases":
36175
+ # Database selector view
36176
+ _sqli_database_selector(
36177
+ db_list,
35865
36178
  all_dumped,
35866
36179
  console,
36180
+ db_cursor_pos,
35867
36181
  )
35868
36182
 
35869
- # Menu
35870
- click.echo()
36183
+ # Navigation footer
36184
+ click.echo("─" * width)
36185
+ click.echo(" ↑↓/jk: Navigate | Enter: View tables | q: Back")
35871
36186
  click.echo("─" * width)
35872
- options = []
35873
- if all_dumped:
35874
- # List available tables with numbers
35875
- table_keys = list(all_dumped.keys())
35876
- click.echo(click.style(" BROWSE EXTRACTED DATA:", bold=True))
35877
- for i, key in enumerate(table_keys, 1):
35878
- data = all_dumped[key]
35879
- row_count = data.get("row_count", len(data.get("rows", [])))
35880
- # Highlight credential tables
35881
- if any(
35882
- x in key.lower()
35883
- for x in [
35884
- "user",
35885
- "account",
35886
- "login",
35887
- "credential",
35888
- "password",
35889
- "card",
35890
- "payment",
35891
- ]
35892
- ):
35893
- click.echo(
35894
- f" [{i}] {click.style(key, fg='red', bold=True)} ({row_count} rows) 🔐"
35895
- )
35896
- else:
35897
- click.echo(f" [{i}] {key} ({row_count} rows)")
35898
- options.extend([str(i) for i in range(1, len(table_keys) + 1)])
35899
-
35900
- click.echo()
35901
- click.echo(f" [t] View all tables structure")
35902
- click.echo(f" [q] Back")
35903
- click.echo()
35904
36187
 
36188
+ # Handle keyboard input
35905
36189
  try:
35906
- choice = click.prompt(" Select", default="q").strip().lower()
36190
+ key = _get_key()
35907
36191
 
35908
- if choice == "q":
36192
+ if key in ("q", "Q", "\x1b"): # q or Escape
35909
36193
  return
35910
- elif choice == "t":
35911
- current_view = "tables"
35912
- elif choice.isdigit() and all_dumped:
35913
- idx = int(choice) - 1
35914
- table_keys = list(all_dumped.keys())
35915
- if 0 <= idx < len(table_keys):
35916
- 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
35917
36205
  except (KeyboardInterrupt, click.Abort):
35918
36206
  return
35919
36207
 
35920
- elif current_view == "tables":
35921
- _sqli_detail_tables(
35922
- 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,
35923
36270
  )
35924
36271
 
35925
- 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
+ )
35926
36282
  click.echo("─" * width)
35927
- click.echo(f" [b] Back to overview")
35928
- click.echo()
35929
36283
 
36284
+ # Handle keyboard input
35930
36285
  try:
35931
- choice = click.prompt(" Select", default="b").strip().lower()
35932
- if choice == "b":
35933
- 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
35934
36307
  except (KeyboardInterrupt, click.Abort):
35935
36308
  return
35936
36309
 
35937
36310
  elif current_view.startswith("data:"):
35938
36311
  table_key = current_view[5:]
35939
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
+
35940
36318
  _sqli_detail_data_table(
35941
- 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,
35942
36326
  )
35943
36327
 
35944
- click.echo()
35945
36328
  click.echo("─" * width)
35946
- 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")
35947
36337
  click.echo()
35948
36338
 
35949
36339
  try:
35950
- choice = click.prompt(" Select", default="b").strip().lower()
35951
- if choice == "b":
35952
- 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
35953
36356
  except (KeyboardInterrupt, click.Abort):
35954
36357
  return
35955
36358
 
35956
36359
 
35957
- def _sqli_detail_overview(
35958
- 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,
35959
36476
  ):
35960
- """Show overview of SQLi exploitation results."""
36477
+ """Show tables for a specific database with cursor navigation."""
35961
36478
  from rich.table import Table
35962
36479
 
35963
- # Quick stats
35964
- 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"])
35965
36484
  total_rows = sum(
35966
- 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"]
35967
36490
  )
35968
36491
 
35969
- # Check for high-value data
35970
- has_credentials = any(
35971
- "user" in k.lower() or "account" in k.lower() or "login" in k.lower()
35972
- for k in all_dumped.keys()
35973
- )
35974
- has_cards = any(
35975
- "card" in k.lower() or "payment" in k.lower() or "credit" in k.lower()
35976
- 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}"
35977
36498
  )
35978
- has_pii = any(
35979
- "address" in k.lower() or "personal" in k.lower() or "customer" in k.lower()
35980
- 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,
35981
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 = ""
35982
36547
 
35983
- # Impact summary
35984
- click.echo(click.style(" 📊 EXPLOITATION SUMMARY", bold=True, fg="green"))
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
+ )
36591
+
36592
+ console.print(table)
35985
36593
  click.echo()
35986
36594
 
35987
- if total_tables_dumped > 0:
35988
- click.echo(
35989
- f" Tables Extracted: {click.style(str(total_tables_dumped), bold=True, fg='green')}"
35990
- )
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")
35991
36636
  click.echo(
35992
- 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")
35993
36639
  )
35994
36640
  click.echo()
35995
36641
 
35996
- # Data classification
35997
- click.echo(click.style(" Data Classification:", bold=True))
35998
- if has_credentials:
35999
- click.echo(
36000
- f" 🔐 {click.style('CREDENTIALS FOUND', fg='red', bold=True)} - User accounts with passwords"
36001
- )
36002
- if has_cards:
36003
- click.echo(
36004
- f" 💳 {click.style('PAYMENT DATA FOUND', fg='red', bold=True)} - Credit card information"
36005
- )
36006
- if has_pii:
36007
- click.echo(
36008
- f" 👤 {click.style('PII FOUND', fg='yellow', bold=True)} - Personal addresses/information"
36009
- )
36010
- if not (has_credentials or has_cards or has_pii):
36011
- click.echo(f" 📄 Application data extracted")
36012
- else:
36013
- click.echo(f" {click.style('No data extracted yet', fg='yellow')}")
36014
- click.echo()
36015
- click.echo(
36016
- " 💡 Tables discovered but not yet dumped. Select a table to dump data."
36017
- )
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
36018
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
+ )
36019
36655
  click.echo()
36020
36656
 
36021
- # Show credential preview if available
36022
- if has_credentials:
36023
- 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})")
36024
36663
  click.echo()
36025
36664
 
36026
- for table_key, data in all_dumped.items():
36027
- if any(x in table_key.lower() for x in ["user", "account", "login"]):
36028
- rows = data.get("rows", [])
36029
- 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
+ )
36674
+
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)
36030
36681
 
36031
- # Find credential columns
36032
- user_col = next(
36033
- (
36034
- c
36035
- for c in columns
36036
- if any(
36037
- x in c.lower()
36038
- for x in ["username", "user", "login", "email"]
36039
- )
36040
- ),
36041
- None,
36042
- )
36043
- pass_col = next(
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(
36044
36689
  (
36045
- c
36046
- for c in columns
36047
- if any(
36048
- x in c.lower()
36049
- for x in ["password", "passwd", "hash", "pwd"]
36050
- )
36690
+ i
36691
+ for i, t in enumerate(all_table_list)
36692
+ if t["table"] == tbl and t["db"] == db
36051
36693
  ),
36052
- None,
36694
+ -1,
36053
36695
  )
36054
- email_col = next((c for c in columns if "email" in c.lower()), None)
36696
+ is_selected = tbl_idx == cursor_pos
36055
36697
 
36056
- if user_col or email_col:
36057
- # Show first 5 credentials
36058
- table = Table(
36059
- show_header=True,
36060
- header_style="bold red",
36061
- box=None,
36062
- padding=(0, 2),
36063
- )
36064
- if user_col:
36065
- table.add_column("Username", style="yellow")
36066
- if email_col and email_col != user_col:
36067
- table.add_column("Email", style="cyan")
36068
- if pass_col:
36069
- table.add_column("Password/Hash", style="red")
36070
-
36071
- for row in rows[:5]:
36072
- row_data = []
36073
- if user_col:
36074
- val = str(row.get(user_col, ""))[:30]
36075
- row_data.append(
36076
- val
36077
- if val and val not in ["<blank>", "None", "NULL"]
36078
- else "-"
36079
- )
36080
- if email_col and email_col != user_col:
36081
- val = str(row.get(email_col, ""))[:35]
36082
- row_data.append(val if val else "-")
36083
- if pass_col:
36084
- val = str(row.get(pass_col, ""))[:40]
36085
- row_data.append(val if val else "-")
36086
- if row_data:
36087
- table.add_row(*row_data)
36698
+ # Cursor indicator
36699
+ cursor = "▶" if is_selected else ""
36088
36700
 
36089
- 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
36090
36704
 
36091
- if len(rows) > 5:
36092
- click.echo(f" ... and {len(rows) - 5} more credentials")
36093
- click.echo()
36094
- 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)
36095
36767
 
36096
- # Show card preview if available
36097
- if has_cards:
36098
- click.echo(click.style(" 💳 PAYMENT DATA PREVIEW", bold=True, fg="red"))
36099
36768
  click.echo()
36100
36769
 
36101
- for table_key, data in all_dumped.items():
36102
- if any(x in table_key.lower() for x in ["card", "payment", "credit"]):
36103
- rows = data.get("rows", [])
36104
- columns = data.get("columns", [])
36105
36770
 
36106
- # Find card columns
36107
- name_col = next(
36108
- (
36109
- c
36110
- for c in columns
36111
- if any(x in c.lower() for x in ["name", "holder", "fullname"])
36112
- ),
36113
- None,
36114
- )
36115
- card_col = next(
36116
- (
36117
- c
36118
- for c in columns
36119
- if any(
36120
- x in c.lower()
36121
- for x in ["cardnum", "card_number", "ccnumber", "number"]
36122
- )
36123
- ),
36124
- None,
36125
- )
36126
- exp_col = next(
36127
- (
36128
- c
36129
- for c in columns
36130
- if any(x in c.lower() for x in ["exp", "expir"])
36131
- ),
36132
- None,
36133
- )
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
36134
36776
 
36135
- table = Table(
36136
- show_header=True, header_style="bold red", box=None, padding=(0, 2)
36137
- )
36138
- if name_col:
36139
- table.add_column("Cardholder", style="yellow")
36140
- if card_col:
36141
- table.add_column("Card Number", style="red")
36142
- if exp_col:
36143
- table.add_column("Expiry", style="cyan")
36144
-
36145
- for row in rows[:5]:
36146
- row_data = []
36147
- if name_col:
36148
- row_data.append(str(row.get(name_col, "-"))[:25])
36149
- if card_col:
36150
- row_data.append(str(row.get(card_col, "-")))
36151
- if exp_col:
36152
- row_data.append(str(row.get(exp_col, "-")))
36153
- if row_data:
36154
- 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()
36155
36808
 
36156
- 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
36157
36814
 
36158
- if len(rows) > 5:
36159
- click.echo(f" ... and {len(rows) - 5} more cards")
36160
- click.echo()
36161
- 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())
36162
36819
 
36163
- # Show PII/address preview if available
36164
- if has_pii:
36165
- click.echo(click.style(" 👤 PERSONAL DATA PREVIEW", bold=True, fg="yellow"))
36820
+ click.echo(f" {click.style('📂 ' + db, bold=True, fg='cyan')} ({dbms_type})")
36166
36821
  click.echo()
36167
36822
 
36168
- for table_key, data in all_dumped.items():
36169
- if any(
36170
- x in table_key.lower()
36171
- for x in ["address", "personal", "customer", "profile"]
36172
- ):
36173
- rows = data.get("rows", [])
36174
- 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
+ )
36175
36832
 
36176
- # Find PII columns
36177
- name_col = next(
36178
- (
36179
- c
36180
- for c in columns
36181
- if any(
36182
- x in c.lower() for x in ["name", "fullname", "full_name"]
36183
- )
36184
- ),
36185
- None,
36186
- )
36187
- city_col = next((c for c in columns if "city" in c.lower()), None)
36188
- country_col = next((c for c in columns if "country" in c.lower()), None)
36189
- address_col = next(
36190
- (
36191
- c
36192
- for c in columns
36193
- if any(x in c.lower() for x in ["address", "street"])
36194
- ),
36195
- None,
36196
- )
36197
- phone_col = next(
36198
- (
36199
- c
36200
- for c in columns
36201
- if any(x in c.lower() for x in ["phone", "mobile", "tel"])
36202
- ),
36203
- None,
36204
- )
36205
- zip_col = next(
36206
- (
36207
- c
36208
- for c in columns
36209
- if any(x in c.lower() for x in ["zip", "postal"])
36210
- ),
36211
- None,
36212
- )
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)
36213
36838
 
36214
- table = Table(
36215
- show_header=True,
36216
- header_style="bold yellow",
36217
- box=None,
36218
- padding=(0, 2),
36219
- )
36220
- if name_col:
36221
- table.add_column("Name", style="yellow")
36222
- if address_col:
36223
- table.add_column("Address", style="white")
36224
- if city_col:
36225
- table.add_column("City", style="cyan")
36226
- if country_col:
36227
- table.add_column("Country", style="cyan")
36228
- if zip_col:
36229
- table.add_column("ZIP", style="dim")
36230
- if phone_col:
36231
- table.add_column("Phone", style="green")
36232
-
36233
- for row in rows[:5]:
36234
- row_data = []
36235
- if name_col:
36236
- row_data.append(str(row.get(name_col, "-"))[:25])
36237
- if address_col:
36238
- row_data.append(str(row.get(address_col, "-"))[:30])
36239
- if city_col:
36240
- row_data.append(str(row.get(city_col, "-"))[:15])
36241
- if country_col:
36242
- row_data.append(str(row.get(country_col, "-"))[:15])
36243
- if zip_col:
36244
- row_data.append(str(row.get(zip_col, "-"))[:10])
36245
- if phone_col:
36246
- row_data.append(str(row.get(phone_col, "-"))[:15])
36247
- if row_data:
36248
- 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())
36249
36843
 
36250
- 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
36251
36847
 
36252
- if len(rows) > 5:
36253
- click.echo(f" ... and {len(rows) - 5} more records")
36254
- click.echo()
36255
- 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()
36256
36890
 
36257
36891
 
36258
36892
  def _sqli_detail_tables(
36259
36893
  all_databases, all_tables, all_columns, db_to_dbms, all_dumped, console
36260
36894
  ):
36261
36895
  """Show database structure overview."""
36262
- from rich.text import Text
36263
- from rich.tree import Tree
36896
+ from rich.table import Table
36264
36897
 
36265
36898
  click.echo(click.style(" 📁 DATABASE STRUCTURE", bold=True, fg="cyan"))
36266
36899
  click.echo()
@@ -36270,14 +36903,29 @@ def _sqli_detail_tables(
36270
36903
  click.echo(" 💡 Run SQLMap with --tables to enumerate tables.")
36271
36904
  return
36272
36905
 
36273
- # Build tree structure
36274
36906
  for db in sorted(all_databases) if all_databases else list(all_tables.keys()):
36275
36907
  dbms_type = db_to_dbms.get(db, "SQLite")
36276
36908
  tables = all_tables.get(db, set())
36277
36909
 
36278
36910
  click.echo(f" {click.style('📂 ' + db, bold=True, fg='cyan')} ({dbms_type})")
36911
+ click.echo()
36279
36912
 
36280
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
+
36281
36929
  sorted_tables = sorted(tables)
36282
36930
  for tbl in sorted_tables:
36283
36931
  table_key = f"{db}.{tbl}"
@@ -36287,55 +36935,74 @@ def _sqli_detail_tables(
36287
36935
  dumped_key = next((k for k in all_dumped.keys() if tbl in k), None)
36288
36936
  has_data = dumped_key is not None
36289
36937
 
36290
- # Highlight interesting tables
36291
- is_sensitive = any(
36292
- x in tbl.lower()
36293
- for x in [
36294
- "user",
36295
- "account",
36296
- "password",
36297
- "card",
36298
- "payment",
36299
- "credential",
36300
- "admin",
36301
- "secret",
36302
- ]
36303
- )
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
36304
36956
 
36305
36957
  if has_data:
36306
36958
  row_count = all_dumped[dumped_key].get("row_count", 0)
36307
- status = click.style(f"✓ {row_count} rows", fg="green")
36308
- else:
36309
- status = click.style(" not dumped", fg="yellow", dim=True)
36310
-
36311
- if is_sensitive:
36312
- click.echo(
36313
- 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,
36314
36965
  )
36315
36966
  else:
36316
- 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
+ )
36317
36974
 
36318
- # Show columns for dumped tables
36319
- if has_data and cols:
36320
- col_list = sorted(cols)[:8]
36321
- col_str = ", ".join(col_list)
36322
- if len(cols) > 8:
36323
- col_str += f" +{len(cols) - 8} more"
36324
- click.echo(f" │ └─ {click.style(col_str, dim=True)}")
36975
+ console.print(table)
36325
36976
 
36326
36977
  click.echo()
36327
36978
 
36328
36979
 
36329
- def _sqli_detail_data_table(table_key: str, data: dict, console, width: int):
36330
- """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."""
36331
36990
  from rich.table import Table
36332
36991
 
36333
36992
  rows = data.get("rows", [])
36334
36993
  columns = data.get("columns", [])
36335
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
36336
36996
 
36337
36997
  click.echo(click.style(f" 📋 TABLE: {table_key}", bold=True, fg="green"))
36338
- 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
+ )
36339
37006
  click.echo()
36340
37007
 
36341
37008
  if not rows:
@@ -36398,8 +37065,14 @@ def _sqli_detail_data_table(table_key: str, data: dict, console, width: int):
36398
37065
  else:
36399
37066
  table.add_column(col, max_width=col_width, overflow="ellipsis")
36400
37067
 
36401
- # Add rows (limit to 20 for display)
36402
- 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
+
36403
37076
  for row in display_rows:
36404
37077
  row_data = []
36405
37078
  for col in display_cols:
@@ -36412,9 +37085,11 @@ def _sqli_detail_data_table(table_key: str, data: dict, console, width: int):
36412
37085
 
36413
37086
  console.print(table)
36414
37087
 
36415
- 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))
36416
37091
  click.echo()
36417
- click.echo(f" ... showing 20 of {len(rows)} rows")
37092
+ click.echo(f" Showing rows {start_idx + 1}-{end_idx} of {len(rows)}")
36418
37093
 
36419
37094
  # Show hidden columns
36420
37095
  if len(columns) > max_cols:
@@ -40337,6 +41012,30 @@ def _launch_interactive_msfconsole(engagement_id: int):
40337
41012
  rc_path = rc.name
40338
41013
 
40339
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
+
40340
41039
  click.pause("Press Enter to launch msfconsole...")
40341
41040
 
40342
41041
  # Launch msfconsole with our resource script
@@ -40490,18 +41189,27 @@ def _ensure_msf_database_ready():
40490
41189
  click.style(" MSF database needs initialization...", fg="yellow")
40491
41190
  )
40492
41191
 
40493
- # 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
+
40494
41198
  click.echo(" Running msfdb init (this may take a moment)...")
41199
+ msfdb_cmd = ["sudo", "msfdb", "init"] if use_sudo else ["msfdb", "init"]
40495
41200
  result = subprocess.run(
40496
- ["sudo", "msfdb", "init"], capture_output=True, text=True, timeout=120
41201
+ msfdb_cmd, capture_output=True, text=True, timeout=120
40497
41202
  )
40498
41203
  if result.returncode == 0:
40499
41204
  click.echo(click.style(" ✓ MSF database initialized", fg="green"))
40500
41205
  else:
40501
41206
  # Try msfdb reinit if init fails
40502
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
+ )
40503
41211
  result = subprocess.run(
40504
- ["sudo", "msfdb", "reinit"],
41212
+ msfdb_reinit_cmd,
40505
41213
  capture_output=True,
40506
41214
  text=True,
40507
41215
  timeout=120,
@@ -40740,7 +41448,14 @@ def _guided_msf_setup():
40740
41448
  click.echo()
40741
41449
  click.echo(" Please run these commands manually:")
40742
41450
  click.echo(click.style(" sudo systemctl start postgresql", fg="cyan"))
40743
- 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"))
40744
41459
  click.echo()
40745
41460
  if not click.confirm(" Continue anyway?", default=False):
40746
41461
  click.pause()
@@ -43405,15 +44120,22 @@ def _check_msfdb_ready() -> bool:
43405
44120
  " Without it, you won't be able to store hosts, credentials, or loot."
43406
44121
  )
43407
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
+
43408
44132
  if click.confirm(
43409
- " Initialize database now? (runs: sudo msfdb init)", default=True
44133
+ f" Initialize database now? (runs: {msfdb_cmd_str})", default=True
43410
44134
  ):
43411
44135
  click.echo()
43412
- click.echo(click.style(" Running sudo msfdb init...", fg="cyan"))
44136
+ click.echo(click.style(f" Running {msfdb_cmd_str}...", fg="cyan"))
43413
44137
  try:
43414
- result = subprocess.run(
43415
- ["sudo", "msfdb", "init"], capture_output=False, text=True
43416
- )
44138
+ result = subprocess.run(msfdb_cmd, capture_output=False, text=True)
43417
44139
  if result.returncode == 0:
43418
44140
  click.echo(
43419
44141
  click.style(" Database initialized successfully!", fg="green")