souleyez 2.43.29__py3-none-any.whl → 3.0.0__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 (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9564 -2881
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +564 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +409 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +417 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +913 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +219 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +237 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23034 -10679
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
  353. souleyez-3.0.0.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
@@ -9,13 +9,13 @@ from typing import Dict, Any
9
9
  def strip_ansi_codes(text: str) -> str:
10
10
  """Remove ANSI escape codes and other terminal control sequences from text."""
11
11
  # Pattern 1: Standard ANSI escape sequences
12
- text = re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', '', text)
12
+ text = re.sub(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])", "", text)
13
13
  # Pattern 2: OSC sequences (Operating System Command)
14
- text = re.sub(r'\x1B\].*?\x07', '', text)
14
+ text = re.sub(r"\x1B\].*?\x07", "", text)
15
15
  # Pattern 3: Simple color codes
16
- text = re.sub(r'\x1b\[[0-9;]*m', '', text)
16
+ text = re.sub(r"\x1b\[[0-9;]*m", "", text)
17
17
  # Pattern 4: Carriage returns and other control chars (except newlines)
18
- text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', text)
18
+ text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text)
19
19
  return text
20
20
 
21
21
 
@@ -36,47 +36,51 @@ def parse_msf_ssh_version(output: str, target: str) -> Dict[str, Any]:
36
36
  clean_output = strip_ansi_codes(output)
37
37
 
38
38
  # Extract SSH version
39
- version_match = re.search(r'SSH server version:\s*(.+)', clean_output)
39
+ version_match = re.search(r"SSH server version:\s*(.+)", clean_output)
40
40
  if version_match:
41
41
  ssh_version = version_match.group(1).strip()
42
42
 
43
43
  # Extract just the version number and product
44
44
  # e.g., "SSH-2.0-OpenSSH_4.7p1 Debian-8ubuntu1"
45
- product_match = re.search(r'SSH-[\d.]+-(\S+)', ssh_version)
45
+ product_match = re.search(r"SSH-[\d.]+-(\S+)", ssh_version)
46
46
  if product_match:
47
47
  product = product_match.group(1)
48
48
 
49
- services.append({
50
- 'port': 22,
51
- 'protocol': 'tcp',
52
- 'service_name': 'ssh',
53
- 'service_version': product
54
- })
49
+ services.append(
50
+ {
51
+ "port": 22,
52
+ "protocol": "tcp",
53
+ "service_name": "ssh",
54
+ "service_version": product,
55
+ }
56
+ )
55
57
 
56
58
  # Extract OS information
57
59
  os_version = None
58
- os_match = re.search(r'os\.version\s+(.+)', clean_output)
60
+ os_match = re.search(r"os\.version\s+(.+)", clean_output)
59
61
  if os_match:
60
62
  os_version = os_match.group(1).strip()
61
63
 
62
64
  os_vendor = None
63
- vendor_match = re.search(r'os\.vendor\s+(.+)', clean_output)
65
+ vendor_match = re.search(r"os\.vendor\s+(.+)", clean_output)
64
66
  if vendor_match:
65
67
  os_vendor = vendor_match.group(1).strip()
66
68
 
67
69
  if os_vendor and os_version:
68
- findings.append({
69
- 'title': f'SSH OS Detection: {os_vendor} {os_version}',
70
- 'severity': 'info',
71
- 'description': f'SSH banner reveals OS: {os_vendor} {os_version}',
72
- 'port': 22,
73
- 'service': 'ssh'
74
- })
70
+ findings.append(
71
+ {
72
+ "title": f"SSH OS Detection: {os_vendor} {os_version}",
73
+ "severity": "info",
74
+ "description": f"SSH banner reveals OS: {os_vendor} {os_version}",
75
+ "port": 22,
76
+ "service": "ssh",
77
+ }
78
+ )
75
79
 
76
80
  # Extract deprecated encryption algorithms
77
81
  deprecated_algos = []
78
- for line in clean_output.split('\n'):
79
- if 'Deprecated' in line and 'encryption.encryption' in line:
82
+ for line in clean_output.split("\n"):
83
+ if "Deprecated" in line and "encryption.encryption" in line:
80
84
  # Extract algorithm name
81
85
  parts = line.split()
82
86
  if len(parts) >= 2:
@@ -84,54 +88,57 @@ def parse_msf_ssh_version(output: str, target: str) -> Dict[str, Any]:
84
88
  deprecated_algos.append(algo)
85
89
 
86
90
  if deprecated_algos:
87
- findings.append({
88
- 'title': 'SSH Deprecated Encryption Algorithms',
89
- 'severity': 'medium',
90
- 'description': f'SSH server supports deprecated encryption: {", ".join(deprecated_algos[:5])}{"..." if len(deprecated_algos) > 5 else ""}',
91
- 'port': 22,
92
- 'service': 'ssh'
93
- })
91
+ findings.append(
92
+ {
93
+ "title": "SSH Deprecated Encryption Algorithms",
94
+ "severity": "medium",
95
+ "description": f'SSH server supports deprecated encryption: {", ".join(deprecated_algos[:5])}{"..." if len(deprecated_algos) > 5 else ""}',
96
+ "port": 22,
97
+ "service": "ssh",
98
+ }
99
+ )
94
100
 
95
101
  # Extract deprecated HMAC algorithms
96
102
  deprecated_hmac = []
97
- for line in clean_output.split('\n'):
98
- if 'Deprecated' in line and 'encryption.hmac' in line:
103
+ for line in clean_output.split("\n"):
104
+ if "Deprecated" in line and "encryption.hmac" in line:
99
105
  parts = line.split()
100
106
  if len(parts) >= 2:
101
107
  algo = parts[1]
102
108
  deprecated_hmac.append(algo)
103
109
 
104
110
  if deprecated_hmac:
105
- findings.append({
106
- 'title': 'SSH Deprecated HMAC Algorithms',
107
- 'severity': 'low',
108
- 'description': f'SSH server supports deprecated HMAC: {", ".join(deprecated_hmac[:3])}{"..." if len(deprecated_hmac) > 3 else ""}',
109
- 'port': 22,
110
- 'service': 'ssh'
111
- })
111
+ findings.append(
112
+ {
113
+ "title": "SSH Deprecated HMAC Algorithms",
114
+ "severity": "low",
115
+ "description": f'SSH server supports deprecated HMAC: {", ".join(deprecated_hmac[:3])}{"..." if len(deprecated_hmac) > 3 else ""}',
116
+ "port": 22,
117
+ "service": "ssh",
118
+ }
119
+ )
112
120
 
113
121
  # Extract weak key exchange methods
114
122
  weak_kex = []
115
- for line in clean_output.split('\n'):
116
- if 'Deprecated' in line and 'encryption.key_exchange' in line:
123
+ for line in clean_output.split("\n"):
124
+ if "Deprecated" in line and "encryption.key_exchange" in line:
117
125
  parts = line.split()
118
126
  if len(parts) >= 2:
119
127
  algo = parts[1]
120
128
  weak_kex.append(algo)
121
129
 
122
130
  if weak_kex:
123
- findings.append({
124
- 'title': 'SSH Weak Key Exchange Methods',
125
- 'severity': 'medium',
126
- 'description': f'SSH server supports weak key exchange: {", ".join(weak_kex)}',
127
- 'port': 22,
128
- 'service': 'ssh'
129
- })
131
+ findings.append(
132
+ {
133
+ "title": "SSH Weak Key Exchange Methods",
134
+ "severity": "medium",
135
+ "description": f'SSH server supports weak key exchange: {", ".join(weak_kex)}',
136
+ "port": 22,
137
+ "service": "ssh",
138
+ }
139
+ )
130
140
 
131
- return {
132
- 'services': services,
133
- 'findings': findings
134
- }
141
+ return {"services": services, "findings": findings}
135
142
 
136
143
 
137
144
  def parse_msf_mysql_login(output: str, target: str) -> Dict[str, Any]:
@@ -152,9 +159,26 @@ def parse_msf_mysql_login(output: str, target: str) -> Dict[str, Any]:
152
159
  credentials = []
153
160
  clean_output = strip_ansi_codes(output)
154
161
 
162
+ # Check if scan was skipped due to unsupported version
163
+ # MSF still reports "credential was successful" even when skipped, which is misleading
164
+ scan_skipped = (
165
+ "Unsupported target version" in clean_output and "Skipping" in clean_output
166
+ )
167
+
168
+ if scan_skipped:
169
+ findings.append(
170
+ {
171
+ "title": "MySQL Login Scan Skipped",
172
+ "severity": "info",
173
+ "description": "MySQL version is unsupported by the mysql_login module. The target may be running a very old or very new MySQL version.",
174
+ "port": 3306,
175
+ "service": "mysql",
176
+ }
177
+ )
178
+
155
179
  # Extract MySQL version
156
180
  # Format: [+] 10.0.0.73:3306 - 10.0.0.73:3306 - Found remote MySQL version 5.0.51a
157
- version_pattern = r'\[\+\]\s+([\d.]+):(\d+).*Found remote MySQL version\s+(\S+)'
181
+ version_pattern = r"\[\+\]\s+([\d.]+):(\d+).*Found remote MySQL version\s+(\S+)"
158
182
  version_match = re.search(version_pattern, clean_output)
159
183
 
160
184
  if version_match:
@@ -162,27 +186,31 @@ def parse_msf_mysql_login(output: str, target: str) -> Dict[str, Any]:
162
186
  port = int(version_match.group(2))
163
187
  mysql_version = version_match.group(3)
164
188
 
165
- services.append({
166
- 'port': port,
167
- 'protocol': 'tcp',
168
- 'service_name': 'mysql',
169
- 'service_version': f'MySQL {mysql_version}'
170
- })
171
-
172
- findings.append({
173
- 'title': f'MySQL Version Detected: {mysql_version}',
174
- 'severity': 'info',
175
- 'description': f'MySQL server version {mysql_version} detected.',
176
- 'port': port,
177
- 'service': 'mysql'
178
- })
189
+ services.append(
190
+ {
191
+ "port": port,
192
+ "protocol": "tcp",
193
+ "service_name": "mysql",
194
+ "service_version": f"MySQL {mysql_version}",
195
+ }
196
+ )
197
+
198
+ findings.append(
199
+ {
200
+ "title": f"MySQL Version Detected: {mysql_version}",
201
+ "severity": "info",
202
+ "description": f"MySQL server version {mysql_version} detected.",
203
+ "port": port,
204
+ "service": "mysql",
205
+ }
206
+ )
179
207
 
180
208
  # Check for successful logins
181
209
  # Format: [+] 10.0.0.73:3306 - 10.0.0.73:3306 - Success: 'root:password'
182
210
  # Format: [+] 10.0.0.73:3306 - Login Successful: root:password@database
183
211
  success_patterns = [
184
212
  r'\[\+\]\s+[\d.]+:(\d+).*Success:\s+[\'"]?([^:\'\"]+):([^\'\"@\s]+)',
185
- r'\[\+\]\s+[\d.]+:(\d+).*Login Successful:\s+([^:]+):([^@\s]+)',
213
+ r"\[\+\]\s+[\d.]+:(\d+).*Login Successful:\s+([^:]+):([^@\s]+)",
186
214
  ]
187
215
 
188
216
  seen_creds = set()
@@ -196,27 +224,49 @@ def parse_msf_mysql_login(output: str, target: str) -> Dict[str, Any]:
196
224
  if cred_key not in seen_creds:
197
225
  seen_creds.add(cred_key)
198
226
 
199
- findings.append({
200
- 'title': 'MySQL Valid Credentials Found',
201
- 'severity': 'critical',
202
- 'description': f'Valid MySQL credentials: {username}:{password}',
203
- 'port': port,
204
- 'service': 'mysql'
205
- })
206
-
207
- credentials.append({
208
- 'username': username,
209
- 'password': password,
210
- 'service': 'mysql',
211
- 'port': port,
212
- 'status': 'valid'
213
- })
214
-
215
- return {
216
- 'services': services,
217
- 'findings': findings,
218
- 'credentials': credentials
219
- }
227
+ findings.append(
228
+ {
229
+ "title": "MySQL Valid Credentials Found",
230
+ "severity": "critical",
231
+ "description": f"Valid MySQL credentials: {username}:{password}",
232
+ "port": port,
233
+ "service": "mysql",
234
+ }
235
+ )
236
+
237
+ credentials.append(
238
+ {
239
+ "username": username,
240
+ "password": password,
241
+ "service": "mysql",
242
+ "port": port,
243
+ "status": "valid",
244
+ }
245
+ )
246
+
247
+ # Check for newer MSF format: "Bruteforce completed, X credential was successful"
248
+ # This format doesn't show the actual credential in logs
249
+ # IMPORTANT: Skip this if scan was skipped - MSF incorrectly reports success for pre-existing creds
250
+ bruteforce_pattern = (
251
+ r"\[\*\]\s+[\d.]+:(\d+).*Bruteforce completed,\s*(\d+)\s+credential.*successful"
252
+ )
253
+ bf_match = re.search(bruteforce_pattern, clean_output)
254
+ if (
255
+ bf_match and not credentials and not scan_skipped
256
+ ): # Only if we didn't find explicit credentials AND scan wasn't skipped
257
+ port = int(bf_match.group(1))
258
+ num_creds = int(bf_match.group(2))
259
+ findings.append(
260
+ {
261
+ "title": f"MySQL Credentials Found ({num_creds} valid)",
262
+ "severity": "critical",
263
+ "description": f'{num_creds} valid MySQL credential(s) discovered. Run "creds" command in msfconsole to view credentials.',
264
+ "port": port,
265
+ "service": "mysql",
266
+ }
267
+ )
268
+
269
+ return {"services": services, "findings": findings, "credentials": credentials}
220
270
 
221
271
 
222
272
  def parse_msf_login_success(output: str, target: str, module: str) -> Dict[str, Any]:
@@ -238,79 +288,94 @@ def parse_msf_login_success(output: str, target: str, module: str) -> Dict[str,
238
288
  clean_output = strip_ansi_codes(output)
239
289
 
240
290
  # Determine service name from module
241
- service = 'unknown'
242
- if 'ssh' in module:
243
- service = 'ssh'
244
- elif 'telnet' in module:
245
- service = 'telnet'
246
- elif 'mysql' in module:
247
- service = 'mysql'
248
- elif 'postgres' in module:
249
- service = 'postgresql'
250
- elif 'vnc' in module:
251
- service = 'vnc'
252
- elif 'rlogin' in module:
253
- service = 'rlogin'
254
- elif 'smb' in module:
255
- service = 'smb'
256
- elif 'rdp' in module:
257
- service = 'rdp'
258
- elif 'ftp' in module:
259
- service = 'ftp'
291
+ service = "unknown"
292
+ if "ssh" in module:
293
+ service = "ssh"
294
+ elif "telnet" in module:
295
+ service = "telnet"
296
+ elif "mysql" in module:
297
+ service = "mysql"
298
+ elif "postgres" in module:
299
+ service = "postgresql"
300
+ elif "vnc" in module:
301
+ service = "vnc"
302
+ elif "rlogin" in module:
303
+ service = "rlogin"
304
+ elif "smb" in module:
305
+ service = "smb"
306
+ elif "rdp" in module:
307
+ service = "rdp"
308
+ elif "ftp" in module:
309
+ service = "ftp"
260
310
 
261
311
  seen_creds = set() # Avoid duplicates
262
312
 
263
313
  # Pattern 1: [+] 10.0.0.82:22 - Success: 'username:password' 'additional info'
264
314
  # Also handles: [+] 10.0.0.82:22 - Success: "username:password"
265
- success_pattern1 = r'\[\+\]\s+[\d.]+:(\d+)\s+-\s+Success:\s+[\'"]([^:]+):([^\'\"]+)[\'"]'
315
+ success_pattern1 = (
316
+ r'\[\+\]\s+[\d.]+:(\d+)\s+-\s+Success:\s+[\'"]([^:]+):([^\'\"]+)[\'"]'
317
+ )
266
318
 
267
319
  # Pattern 2: [+] IP:PORT - IP:PORT - Login Successful: user:pass@database
268
320
  # Note: MSF often outputs IP:PORT twice. Using .* to handle both cases.
269
- success_pattern2 = r'\[\+\]\s+[\d.]+:(\d+).*Login Successful:\s+([^:]*):([^@\s]+)'
321
+ success_pattern2 = r"\[\+\]\s+[\d.]+:(\d+).*Login Successful:\s+([^:]*):([^@\s]+)"
270
322
 
271
323
  # Pattern 3: VNC format [+] 10.0.0.73:5900 - 10.0.0.73:5900 - Login Successful: :password
272
324
  # Note: VNC often has empty username
273
- success_pattern3 = r'\[\+\]\s+[\d.]+:(\d+).*Login Successful:\s*:(\S+)'
325
+ success_pattern3 = r"\[\+\]\s+[\d.]+:(\d+).*Login Successful:\s*:(\S+)"
274
326
 
275
327
  # Pattern 4: Telnet format [+] 192.168.2.230:23 - 192.168.2.230:23 - msfadmin:msfadmin login: Login OK
276
328
  # MSF telnet_login uses "username:password login: Login OK" format
277
- success_pattern_telnet = r'\[\+\]\s+[\d.]+:(\d+).*-\s+([^:\s]+):([^\s]+)\s+login:\s+Login OK'
329
+ success_pattern_telnet = (
330
+ r"\[\+\]\s+[\d.]+:(\d+).*-\s+([^:\s]+):([^\s]+)\s+login:\s+Login OK"
331
+ )
278
332
 
279
333
  # Pattern 5: Flexible [+] with credentials anywhere (fallback)
280
334
  # Handles: [+] 10.0.0.82:22 Found credentials: user:pass
281
335
  success_pattern_flexible = r'\[\+\]\s+[\d.]+:(\d+).*(?:credential|found|valid).*?[\'"]?([^:\s\'\"]+):([^\'\"@\s]+)[\'"]?'
282
336
 
283
337
  # Pattern 6: RDP format [+] 10.0.0.82:3389 - DOMAIN\user:password - Success
284
- success_pattern_rdp = r'\[\+\]\s+[\d.]+:(\d+).*?([^\\:\s]+\\)?([^:\s]+):([^\s-]+)\s*-\s*Success'
338
+ success_pattern_rdp = (
339
+ r"\[\+\]\s+[\d.]+:(\d+).*?([^\\:\s]+\\)?([^:\s]+):([^\s-]+)\s*-\s*Success"
340
+ )
285
341
 
286
342
  # Try pattern 3 first (VNC with empty username)
287
343
  for match in re.finditer(success_pattern3, clean_output):
288
344
  port = int(match.group(1))
289
345
  password = match.group(2)
290
- username = '' # VNC typically has no username
346
+ username = "" # VNC typically has no username
291
347
 
292
348
  cred_key = (port, username, password)
293
349
  if cred_key not in seen_creds:
294
350
  seen_creds.add(cred_key)
295
351
 
296
- findings.append({
297
- 'title': f'{service.upper()} Valid Credentials Found',
298
- 'severity': 'critical',
299
- 'description': f'Valid {service} password found (no username required): {password}',
300
- 'port': port,
301
- 'service': service
302
- })
303
-
304
- credentials.append({
305
- 'username': username,
306
- 'password': password,
307
- 'service': service,
308
- 'port': port,
309
- 'status': 'valid'
310
- })
352
+ findings.append(
353
+ {
354
+ "title": f"{service.upper()} Valid Credentials Found",
355
+ "severity": "critical",
356
+ "description": f"Valid {service} password found (no username required): {password}",
357
+ "port": port,
358
+ "service": service,
359
+ }
360
+ )
361
+
362
+ credentials.append(
363
+ {
364
+ "username": username,
365
+ "password": password,
366
+ "service": service,
367
+ "port": port,
368
+ "status": "valid",
369
+ }
370
+ )
311
371
 
312
372
  # Try other patterns (username:password style)
313
- for pattern in [success_pattern1, success_pattern2, success_pattern_telnet, success_pattern_flexible]:
373
+ for pattern in [
374
+ success_pattern1,
375
+ success_pattern2,
376
+ success_pattern_telnet,
377
+ success_pattern_flexible,
378
+ ]:
314
379
  for match in re.finditer(pattern, clean_output, re.IGNORECASE):
315
380
  port = int(match.group(1))
316
381
  username = match.group(2)
@@ -320,55 +385,79 @@ def parse_msf_login_success(output: str, target: str, module: str) -> Dict[str,
320
385
  if cred_key not in seen_creds:
321
386
  seen_creds.add(cred_key)
322
387
 
323
- findings.append({
324
- 'title': f'{service.upper()} Valid Credentials Found',
325
- 'severity': 'critical',
326
- 'description': f'Valid {service} credentials: {username}:{password}',
327
- 'port': port,
328
- 'service': service
329
- })
330
-
331
- credentials.append({
332
- 'username': username,
333
- 'password': password,
334
- 'service': service,
335
- 'port': port,
336
- 'status': 'valid'
337
- })
388
+ findings.append(
389
+ {
390
+ "title": f"{service.upper()} Valid Credentials Found",
391
+ "severity": "critical",
392
+ "description": f"Valid {service} credentials: {username}:{password}",
393
+ "port": port,
394
+ "service": service,
395
+ }
396
+ )
397
+
398
+ credentials.append(
399
+ {
400
+ "username": username,
401
+ "password": password,
402
+ "service": service,
403
+ "port": port,
404
+ "status": "valid",
405
+ }
406
+ )
338
407
 
339
408
  # Check for session opened (login modules can spawn sessions)
340
409
  # Format: [*] Command shell session 1 opened (192.168.1.224:37125 -> 192.168.1.240:23)
341
- session_pattern = r'\[\*\]\s+(Command shell|Meterpreter)\s+session\s+(\d+)\s+opened\s+\(([^)]+)\)'
410
+ session_pattern = (
411
+ r"\[\*\]\s+(Command shell|Meterpreter)\s+session\s+(\d+)\s+opened\s+\(([^)]+)\)"
412
+ )
342
413
  for match in re.finditer(session_pattern, clean_output, re.IGNORECASE):
343
414
  session_type = match.group(1)
344
415
  session_id = match.group(2)
345
416
  tunnel = match.group(3)
346
417
 
347
- sessions.append({
348
- 'id': session_id,
349
- 'type': session_type.lower(),
350
- 'tunnel': tunnel,
351
- 'module': module
352
- })
353
-
354
- findings.append({
355
- 'title': f'{service.upper()} Session Opened',
356
- 'severity': 'critical',
357
- 'description': f'{session_type} session {session_id} opened via {service} login. Tunnel: {tunnel}',
358
- 'service': service,
359
- 'data': {
360
- 'module': module,
361
- 'session_id': session_id,
362
- 'session_type': session_type.lower(),
363
- 'tunnel': tunnel
418
+ sessions.append(
419
+ {
420
+ "id": session_id,
421
+ "type": session_type.lower(),
422
+ "tunnel": tunnel,
423
+ "module": module,
364
424
  }
365
- })
366
-
367
- return {
368
- 'findings': findings,
369
- 'credentials': credentials,
370
- 'sessions': sessions
371
- }
425
+ )
426
+
427
+ findings.append(
428
+ {
429
+ "title": f"{service.upper()} Session Opened",
430
+ "severity": "critical",
431
+ "description": f"{session_type} session {session_id} opened via {service} login. Tunnel: {tunnel}",
432
+ "service": service,
433
+ "data": {
434
+ "module": module,
435
+ "session_id": session_id,
436
+ "session_type": session_type.lower(),
437
+ "tunnel": tunnel,
438
+ },
439
+ }
440
+ )
441
+
442
+ # Check for newer MSF format: "Bruteforce completed, X credential was successful"
443
+ # This format doesn't show the actual credential in logs
444
+ if not credentials: # Only if we didn't find explicit credentials
445
+ bruteforce_pattern = r"\[\*\]\s+[\d.]+:?(\d*)\s*-?\s*Bruteforce completed,\s*(\d+)\s+credential.*successful"
446
+ bf_match = re.search(bruteforce_pattern, clean_output)
447
+ if bf_match:
448
+ port = int(bf_match.group(1)) if bf_match.group(1) else None
449
+ num_creds = int(bf_match.group(2))
450
+ findings.append(
451
+ {
452
+ "title": f"{service.upper()} Credentials Found ({num_creds} valid)",
453
+ "severity": "critical",
454
+ "description": f'{num_creds} valid {service} credential(s) discovered via bruteforce. Run "creds" command in msfconsole to view.',
455
+ "service": service,
456
+ "port": port,
457
+ }
458
+ )
459
+
460
+ return {"findings": findings, "credentials": credentials, "sessions": sessions}
372
461
 
373
462
 
374
463
  def parse_msf_smb_version(output: str, target: str) -> Dict[str, Any]:
@@ -388,13 +477,18 @@ def parse_msf_smb_version(output: str, target: str) -> Dict[str, Any]:
388
477
  # Pattern 1: Host could not be identified: Unix (Samba 3.0.20-Debian)
389
478
  # Pattern 2: Host is running Windows 10 Pro (build 19041)
390
479
  # Pattern 3: SMB Detected (versions:1, 2, 3) (preferred dialect:SMB 3.1.1)
480
+ # Pattern 4: [+] Host is running Version X.X.X (unknown OS)
391
481
  version_patterns = [
392
482
  # Samba detection - [*] 10.0.0.73:445 - Host could not be identified: Unix (Samba 3.0.20-Debian)
393
- r'\[\*\]\s+([\d.]+):(\d+)\s+-\s+Host could not be identified:\s+(\S+)\s+\(([^)]+)\)',
483
+ r"\[\*\]\s+([\d.]+):(\d+)\s+-\s+Host could not be identified:\s+(\S+)\s+\(([^)]+)\)",
394
484
  # Windows detection - [*] 10.0.0.73:445 - Host is running Windows 10 Pro (build 19041)
395
- r'\[\*\]\s+([\d.]+):(\d+)\s+-\s+Host is running\s+(.+)',
485
+ r"\[\*\]\s+([\d.]+):(\d+)\s+-\s+Host is running\s+(.+)",
486
+ # Version detection with [+] - [+] 10.0.0.73:445 - Host is running Version 6.1.0 (unknown OS)
487
+ r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+Host is running\s+(.+)",
396
488
  # Generic SMB version - [+] 10.0.0.73:445 - SMB Detected...
397
- r'\[\+\]\s+([\d.]+):(\d+)\s+-\s+SMB Detected\s+(.+)',
489
+ r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+SMB Detected\s+(.+)",
490
+ # SMB Detected with [*] - [*] 10.0.0.73:445 - SMB Detected (versions:1, 2, 3)
491
+ r"\[\*\]\s+([\d.]+):(\d+)\s+-\s+SMB Detected\s+(.+)",
398
492
  ]
399
493
 
400
494
  smb_version = None
@@ -405,7 +499,7 @@ def parse_msf_smb_version(output: str, target: str) -> Dict[str, Any]:
405
499
  match = re.search(pattern, clean_output)
406
500
  if match:
407
501
  port = int(match.group(2))
408
- if 'could not be identified' in pattern:
502
+ if "could not be identified" in pattern:
409
503
  # Samba format: OS type and version in parens
410
504
  os_info = match.group(3) # e.g., "Unix"
411
505
  smb_version = match.group(4) # e.g., "Samba 3.0.20-Debian"
@@ -415,50 +509,82 @@ def parse_msf_smb_version(output: str, target: str) -> Dict[str, Any]:
415
509
  break
416
510
 
417
511
  if smb_version:
418
- services.append({
419
- 'port': port,
420
- 'protocol': 'tcp',
421
- 'service_name': 'smb',
422
- 'service_version': smb_version
423
- })
512
+ services.append(
513
+ {
514
+ "port": port,
515
+ "protocol": "tcp",
516
+ "service_name": "smb",
517
+ "service_version": smb_version,
518
+ }
519
+ )
424
520
 
425
521
  # Check for known vulnerable Samba versions
426
522
  vuln_samba_versions = {
427
- 'Samba 3.0.20': ('CVE-2007-2447', 'critical', 'Samba username map script command injection'),
428
- 'Samba 3.0.21': ('CVE-2007-2447', 'critical', 'Samba username map script command injection'),
429
- 'Samba 3.0.22': ('CVE-2007-2447', 'critical', 'Samba username map script command injection'),
430
- 'Samba 3.0.23': ('CVE-2007-2447', 'critical', 'Samba username map script command injection'),
431
- 'Samba 3.0.24': ('CVE-2007-2447', 'critical', 'Samba username map script command injection'),
432
- 'Samba 3.0.25': ('CVE-2007-2447', 'critical', 'Samba username map script command injection'),
433
- 'Samba 3.5.0': ('CVE-2017-7494', 'critical', 'SambaCry/EternalRed remote code execution'),
523
+ "Samba 3.0.20": (
524
+ "CVE-2007-2447",
525
+ "critical",
526
+ "Samba username map script command injection",
527
+ ),
528
+ "Samba 3.0.21": (
529
+ "CVE-2007-2447",
530
+ "critical",
531
+ "Samba username map script command injection",
532
+ ),
533
+ "Samba 3.0.22": (
534
+ "CVE-2007-2447",
535
+ "critical",
536
+ "Samba username map script command injection",
537
+ ),
538
+ "Samba 3.0.23": (
539
+ "CVE-2007-2447",
540
+ "critical",
541
+ "Samba username map script command injection",
542
+ ),
543
+ "Samba 3.0.24": (
544
+ "CVE-2007-2447",
545
+ "critical",
546
+ "Samba username map script command injection",
547
+ ),
548
+ "Samba 3.0.25": (
549
+ "CVE-2007-2447",
550
+ "critical",
551
+ "Samba username map script command injection",
552
+ ),
553
+ "Samba 3.5.0": (
554
+ "CVE-2017-7494",
555
+ "critical",
556
+ "SambaCry/EternalRed remote code execution",
557
+ ),
434
558
  }
435
559
 
436
560
  # Check if version matches any known vulnerable version
437
561
  for vuln_ver, (cve, severity, desc) in vuln_samba_versions.items():
438
562
  if vuln_ver in smb_version:
439
- findings.append({
440
- 'title': f'Vulnerable Samba Version ({cve})',
441
- 'severity': severity,
442
- 'description': f'{desc}. Detected version: {smb_version}',
443
- 'port': port,
444
- 'service': 'smb',
445
- 'data': {'cve': cve, 'version': smb_version}
446
- })
563
+ findings.append(
564
+ {
565
+ "title": f"Vulnerable Samba Version ({cve})",
566
+ "severity": severity,
567
+ "description": f"{desc}. Detected version: {smb_version}",
568
+ "port": port,
569
+ "service": "smb",
570
+ "data": {"cve": cve, "version": smb_version},
571
+ }
572
+ )
447
573
  break
448
574
  else:
449
575
  # No specific vulnerability, just report version as info
450
- findings.append({
451
- 'title': f'SMB Version Detected: {smb_version}',
452
- 'severity': 'info',
453
- 'description': f'SMB/Samba version detected: {smb_version}' + (f' (OS: {os_info})' if os_info else ''),
454
- 'port': port,
455
- 'service': 'smb'
456
- })
576
+ findings.append(
577
+ {
578
+ "title": f"SMB Version Detected: {smb_version}",
579
+ "severity": "info",
580
+ "description": f"SMB/Samba version detected: {smb_version}"
581
+ + (f" (OS: {os_info})" if os_info else ""),
582
+ "port": port,
583
+ "service": "smb",
584
+ }
585
+ )
457
586
 
458
- return {
459
- 'services': services,
460
- 'findings': findings
461
- }
587
+ return {"services": services, "findings": findings}
462
588
 
463
589
 
464
590
  def parse_msf_smb_enumshares(output: str, target: str) -> Dict[str, Any]:
@@ -476,7 +602,7 @@ def parse_msf_smb_enumshares(output: str, target: str) -> Dict[str, Any]:
476
602
  # Parse share lines
477
603
  # Format: [+] 10.0.0.82:445 - ADMIN$ - (DISK) Remote Admin
478
604
  # Format: [+] 10.0.0.82:445 - IPC$ - (IPC) Remote IPC
479
- share_pattern = r'\[\+\]\s+[\d.]+:(\d+)\s+-\s+(\S+)\s+-\s+\((\w+)\)\s*(.*)'
605
+ share_pattern = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+(\S+)\s+-\s+\((\w+)\)\s*(.*)"
480
606
 
481
607
  shares = []
482
608
  for match in re.finditer(share_pattern, clean_output):
@@ -485,32 +611,29 @@ def parse_msf_smb_enumshares(output: str, target: str) -> Dict[str, Any]:
485
611
  share_type = match.group(3)
486
612
  comment = match.group(4).strip()
487
613
 
488
- shares.append({
489
- 'name': share_name,
490
- 'type': share_type,
491
- 'comment': comment,
492
- 'port': port
493
- })
614
+ shares.append(
615
+ {"name": share_name, "type": share_type, "comment": comment, "port": port}
616
+ )
494
617
 
495
618
  if shares:
496
619
  # Determine severity based on share types
497
- severity = 'info'
498
- if any(s['name'] not in ['IPC$', 'ADMIN$', 'C$'] for s in shares):
499
- severity = 'medium' # Non-default shares found
500
-
501
- share_list = ', '.join([s['name'] for s in shares])
502
- findings.append({
503
- 'title': f'SMB Shares Discovered ({len(shares)} shares)',
504
- 'severity': severity,
505
- 'description': f'Found {len(shares)} SMB shares: {share_list}',
506
- 'port': 445,
507
- 'service': 'smb',
508
- 'data': {'shares': shares}
509
- })
620
+ severity = "info"
621
+ if any(s["name"] not in ["IPC$", "ADMIN$", "C$"] for s in shares):
622
+ severity = "medium" # Non-default shares found
623
+
624
+ share_list = ", ".join([s["name"] for s in shares])
625
+ findings.append(
626
+ {
627
+ "title": f"SMB Shares Discovered ({len(shares)} shares)",
628
+ "severity": severity,
629
+ "description": f"Found {len(shares)} SMB shares: {share_list}",
630
+ "port": 445,
631
+ "service": "smb",
632
+ "data": {"shares": shares},
633
+ }
634
+ )
510
635
 
511
- return {
512
- 'findings': findings
513
- }
636
+ return {"findings": findings}
514
637
 
515
638
 
516
639
  def parse_msf_ssh_enumusers(output: str, target: str) -> Dict[str, Any]:
@@ -521,11 +644,25 @@ def parse_msf_ssh_enumusers(output: str, target: str) -> Dict[str, Any]:
521
644
  {
522
645
  'findings': [] # Discovered SSH users
523
646
  'credentials': [] # Username-only credentials
647
+ 'status': str # Optional status override (e.g., 'warning' for false positives)
648
+ 'warning': str # Optional warning message
524
649
  }
525
650
  """
526
651
  findings = []
527
652
  credentials = []
528
653
  clean_output = strip_ansi_codes(output)
654
+ result = {}
655
+
656
+ # Check for false positive detection (module aborted)
657
+ # Format: [-] 192.168.1.157:22 - SSH - throws false positive results. Aborting.
658
+ if "false positive" in clean_output.lower() and "aborting" in clean_output.lower():
659
+ result["status"] = "warning"
660
+ result["warning"] = (
661
+ "SSH user enumeration aborted: target throws false positive results"
662
+ )
663
+ result["findings"] = []
664
+ result["credentials"] = []
665
+ return result
529
666
 
530
667
  # Parse user enumeration results
531
668
  # Format: [+] 10.0.0.82:22 - SSH - User 'root' found
@@ -537,31 +674,88 @@ def parse_msf_ssh_enumusers(output: str, target: str) -> Dict[str, Any]:
537
674
  port = int(match.group(1))
538
675
  username = match.group(2)
539
676
  users.append(username)
540
-
677
+
541
678
  # Add as credential (username-only, no password)
542
- credentials.append({
543
- 'username': username,
544
- 'password': '', # Empty password for username-only
545
- 'service': 'ssh',
546
- 'port': port,
547
- 'status': 'untested' # Username discovered but not validated
548
- })
679
+ credentials.append(
680
+ {
681
+ "username": username,
682
+ "password": "", # Empty password for username-only
683
+ "service": "ssh",
684
+ "port": port,
685
+ "status": "untested", # Username discovered but not validated
686
+ }
687
+ )
549
688
 
550
689
  if users:
551
- user_list = ', '.join(users)
552
- findings.append({
553
- 'title': f'SSH Users Enumerated ({len(users)} users)',
554
- 'severity': 'medium',
555
- 'description': f'Found {len(users)} SSH users: {user_list}',
556
- 'port': 22,
557
- 'service': 'ssh',
558
- 'data': {'users': users}
559
- })
560
-
561
- return {
562
- 'findings': findings,
563
- 'credentials': credentials
564
- }
690
+ user_list = ", ".join(users)
691
+ findings.append(
692
+ {
693
+ "title": f"SSH Users Enumerated ({len(users)} users)",
694
+ "severity": "medium",
695
+ "description": f"Found {len(users)} SSH users: {user_list}",
696
+ "port": 22,
697
+ "service": "ssh",
698
+ "data": {"users": users},
699
+ }
700
+ )
701
+
702
+ result["findings"] = findings
703
+ result["credentials"] = credentials
704
+ return result
705
+
706
+
707
+ def parse_msf_kerberos_enumusers(output: str, target: str) -> Dict[str, Any]:
708
+ """
709
+ Parse Kerberos user enumeration output (kerberos_enumusers module).
710
+
711
+ Returns:
712
+ {
713
+ 'findings': [] # Discovered Kerberos users
714
+ 'credentials': [] # Username-only credentials
715
+ }
716
+ """
717
+ findings = []
718
+ credentials = []
719
+ clean_output = strip_ansi_codes(output)
720
+
721
+ # Parse valid usernames - MSF kerberos_enumusers format
722
+ # Format: [+] 10.129.234.72 - User: "administrator" is present
723
+ # Format: [+] 10.129.234.72 - User: "guest" is present
724
+ valid_pattern = r'\[\+\]\s+[\d.]+\s+-\s+User:\s+"([^"]+)"\s+is present'
725
+
726
+ users = []
727
+ port = 88 # Kerberos port
728
+
729
+ for match in re.finditer(valid_pattern, clean_output, re.IGNORECASE):
730
+ username = match.group(1)
731
+ if username not in users:
732
+ users.append(username)
733
+
734
+ # Add as credential (username-only, no password)
735
+ credentials.append(
736
+ {
737
+ "username": username,
738
+ "password": "", # Empty password for username-only
739
+ "service": "kerberos",
740
+ "port": port,
741
+ "status": "untested", # Username discovered but not validated
742
+ }
743
+ )
744
+
745
+ if users:
746
+ user_list = ", ".join(users)
747
+ findings.append(
748
+ {
749
+ "title": f"Kerberos Users Enumerated ({len(users)} users)",
750
+ "severity": "medium",
751
+ "description": f"Found {len(users)} valid Kerberos users: {user_list}",
752
+ "port": port,
753
+ "service": "kerberos",
754
+ "data": {"users": users},
755
+ }
756
+ )
757
+
758
+ return {"findings": findings, "credentials": credentials}
565
759
 
566
760
 
567
761
  def parse_msf_smtp_enum(output: str, target: str) -> Dict[str, Any]:
@@ -583,45 +777,46 @@ def parse_msf_smtp_enum(output: str, target: str) -> Dict[str, Any]:
583
777
  users = []
584
778
 
585
779
  # Method 1: Users found line
586
- users_found_pattern = r'Users found:\s*(.+)'
780
+ users_found_pattern = r"Users found:\s*(.+)"
587
781
  match = re.search(users_found_pattern, clean_output)
588
782
  if match:
589
783
  user_list = match.group(1).strip()
590
- users = [u.strip() for u in user_list.split(',') if u.strip()]
784
+ users = [u.strip() for u in user_list.split(",") if u.strip()]
591
785
 
592
786
  # Method 2: Individual user lines
593
787
  # Format: [+] 10.0.0.82:25 - Found user: root
594
- user_pattern = r'\[\+\]\s+[\d.]+:(\d+)\s+-\s+Found user:\s+(\S+)'
788
+ user_pattern = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+Found user:\s+(\S+)"
595
789
  for match in re.finditer(user_pattern, clean_output):
596
790
  username = match.group(2)
597
791
  if username not in users:
598
792
  users.append(username)
599
793
 
600
794
  if users:
601
- user_list = ', '.join(users)
602
- findings.append({
603
- 'title': f'SMTP Users Enumerated ({len(users)} users)',
604
- 'severity': 'medium',
605
- 'description': f'Found {len(users)} SMTP users: {user_list}',
606
- 'port': 25,
607
- 'service': 'smtp',
608
- 'data': {'users': users}
609
- })
795
+ user_list = ", ".join(users)
796
+ findings.append(
797
+ {
798
+ "title": f"SMTP Users Enumerated ({len(users)} users)",
799
+ "severity": "medium",
800
+ "description": f"Found {len(users)} SMTP users: {user_list}",
801
+ "port": 25,
802
+ "service": "smtp",
803
+ "data": {"users": users},
804
+ }
805
+ )
610
806
 
611
807
  # Add each user as a credential (username only, no password)
612
808
  for username in users:
613
- credentials.append({
614
- 'username': username,
615
- 'password': None,
616
- 'service': 'smtp',
617
- 'port': 25,
618
- 'status': 'enumerated' # Not validated, just discovered
619
- })
809
+ credentials.append(
810
+ {
811
+ "username": username,
812
+ "password": None,
813
+ "service": "smtp",
814
+ "port": 25,
815
+ "status": "enumerated", # Not validated, just discovered
816
+ }
817
+ )
620
818
 
621
- return {
622
- 'findings': findings,
623
- 'credentials': credentials
624
- }
819
+ return {"findings": findings, "credentials": credentials}
625
820
 
626
821
 
627
822
  def parse_msf_ftp_anonymous(output: str, target: str) -> Dict[str, Any]:
@@ -640,35 +835,36 @@ def parse_msf_ftp_anonymous(output: str, target: str) -> Dict[str, Any]:
640
835
 
641
836
  # Pattern: [+] 10.0.0.73:21 - 10.0.0.73:21 - Anonymous READ (220 (vsFTPd 2.3.4))
642
837
  # Pattern: [+] 10.0.0.73:21 - Anonymous READ/WRITE (...)
643
- anon_pattern = r'\[\+\]\s+[\d.]+:(\d+).*Anonymous\s+(READ|WRITE|READ/WRITE)'
838
+ anon_pattern = r"\[\+\]\s+[\d.]+:(\d+).*Anonymous\s+(READ|WRITE|READ/WRITE)"
644
839
 
645
840
  for match in re.finditer(anon_pattern, clean_output, re.IGNORECASE):
646
841
  port = int(match.group(1))
647
842
  access_type = match.group(2).upper()
648
843
 
649
- severity = 'high' if 'WRITE' in access_type else 'medium'
844
+ severity = "high" if "WRITE" in access_type else "medium"
650
845
 
651
- findings.append({
652
- 'title': f'FTP Anonymous Access ({access_type})',
653
- 'severity': severity,
654
- 'description': f'FTP server allows anonymous access with {access_type} permissions. This may expose sensitive files.',
655
- 'port': port,
656
- 'service': 'ftp'
657
- })
846
+ findings.append(
847
+ {
848
+ "title": f"FTP Anonymous Access ({access_type})",
849
+ "severity": severity,
850
+ "description": f"FTP server allows anonymous access with {access_type} permissions. This may expose sensitive files.",
851
+ "port": port,
852
+ "service": "ftp",
853
+ }
854
+ )
658
855
 
659
856
  # Add anonymous credential
660
- credentials.append({
661
- 'username': 'anonymous',
662
- 'password': 'anonymous@',
663
- 'service': 'ftp',
664
- 'port': port,
665
- 'status': 'valid'
666
- })
857
+ credentials.append(
858
+ {
859
+ "username": "anonymous",
860
+ "password": "anonymous@",
861
+ "service": "ftp",
862
+ "port": port,
863
+ "status": "valid",
864
+ }
865
+ )
667
866
 
668
- return {
669
- 'findings': findings,
670
- 'credentials': credentials
671
- }
867
+ return {"findings": findings, "credentials": credentials}
672
868
 
673
869
 
674
870
  def parse_msf_nfs_mount(output: str, target: str) -> Dict[str, Any]:
@@ -687,56 +883,52 @@ def parse_msf_nfs_mount(output: str, target: str) -> Dict[str, Any]:
687
883
 
688
884
  # Pattern 1: New MSF format
689
885
  # Format: [+] 10.0.0.73:111 - 10.0.0.73 Mountable NFS Export: / [*]
690
- export_pattern1 = r'\[\+\]\s+[\d.]+:(\d+)\s+-\s+[\d.]+\s+Mountable NFS Export:\s+(\S+)\s*(\[.*?\])?'
886
+ export_pattern1 = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+[\d.]+\s+Mountable NFS Export:\s+(\S+)\s*(\[.*?\])?"
691
887
  for match in re.finditer(export_pattern1, clean_output):
692
888
  port = int(match.group(1))
693
889
  mount_path = match.group(2)
694
- permissions = match.group(3) or '*'
890
+ permissions = match.group(3) or "*"
695
891
 
696
- exports.append({
697
- 'path': mount_path,
698
- 'permissions': permissions.strip(),
699
- 'port': port
700
- })
892
+ exports.append(
893
+ {"path": mount_path, "permissions": permissions.strip(), "port": port}
894
+ )
701
895
 
702
896
  # Pattern 2: Old format
703
897
  # Format: [+] 10.0.0.82:111 - /home *
704
898
  # Format: [+] 10.0.0.82:2049 - /var/nfs *(rw,sync,no_subtree_check)
705
899
  if not exports:
706
- export_pattern2 = r'\[\+\]\s+[\d.]+:(\d+)\s+-\s+(/\S+)\s+(.*)'
900
+ export_pattern2 = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+(/\S+)\s+(.*)"
707
901
  for match in re.finditer(export_pattern2, clean_output):
708
902
  port = int(match.group(1))
709
903
  mount_path = match.group(2)
710
904
  permissions = match.group(3).strip()
711
905
 
712
- exports.append({
713
- 'path': mount_path,
714
- 'permissions': permissions,
715
- 'port': port
716
- })
906
+ exports.append(
907
+ {"path": mount_path, "permissions": permissions, "port": port}
908
+ )
717
909
 
718
910
  if exports:
719
911
  # Determine severity based on path and permissions
720
- severity = 'medium'
912
+ severity = "medium"
721
913
  # Root export is always high severity
722
- if any(e['path'] == '/' for e in exports):
723
- severity = 'high'
724
- elif any('rw' in e['permissions'] for e in exports):
725
- severity = 'high' # Writable mounts are more severe
726
-
727
- export_list = ', '.join([e['path'] for e in exports])
728
- findings.append({
729
- 'title': f'NFS Exports Discovered ({len(exports)} mounts)',
730
- 'severity': severity,
731
- 'description': f'Found {len(exports)} NFS exports: {export_list}',
732
- 'port': 2049,
733
- 'service': 'nfs',
734
- 'data': {'exports': exports}
735
- })
736
-
737
- return {
738
- 'findings': findings
739
- }
914
+ if any(e["path"] == "/" for e in exports):
915
+ severity = "high"
916
+ elif any("rw" in e["permissions"] for e in exports):
917
+ severity = "high" # Writable mounts are more severe
918
+
919
+ export_list = ", ".join([e["path"] for e in exports])
920
+ findings.append(
921
+ {
922
+ "title": f"NFS Exports Discovered ({len(exports)} mounts)",
923
+ "severity": severity,
924
+ "description": f"Found {len(exports)} NFS exports: {export_list}",
925
+ "port": 2049,
926
+ "service": "nfs",
927
+ "data": {"exports": exports},
928
+ }
929
+ )
930
+
931
+ return {"findings": findings}
740
932
 
741
933
 
742
934
  def parse_msf_java_rmi(output: str, target: str) -> Dict[str, Any]:
@@ -755,7 +947,7 @@ def parse_msf_java_rmi(output: str, target: str) -> Dict[str, Any]:
755
947
 
756
948
  # Parse Java RMI detection
757
949
  # Format: [+] 10.0.0.82:1099 - Java RMI Endpoint Detected: Class Loader Enabled
758
- rmi_pattern = r'\[\+\]\s+([\d.]+):(\d+)\s+-\s+(.+)'
950
+ rmi_pattern = r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+(.+)"
759
951
 
760
952
  for match in re.finditer(rmi_pattern, clean_output):
761
953
  ip = match.group(1)
@@ -763,37 +955,40 @@ def parse_msf_java_rmi(output: str, target: str) -> Dict[str, Any]:
763
955
  message = match.group(3).strip()
764
956
 
765
957
  # Check for specific vulnerabilities
766
- severity = 'medium'
767
- if 'Class Loader Enabled' in message:
768
- severity = 'high'
769
- findings.append({
770
- 'title': 'Java RMI Class Loader Enabled',
771
- 'severity': severity,
772
- 'description': f'Java RMI endpoint with Class Loader enabled detected. This may allow remote code execution via deserialization attacks.',
773
- 'port': port,
774
- 'service': 'java-rmi'
775
- })
776
- elif 'Endpoint Detected' in message:
777
- findings.append({
778
- 'title': 'Java RMI Endpoint Detected',
779
- 'severity': 'medium',
780
- 'description': f'Java RMI endpoint detected: {message}',
781
- 'port': port,
782
- 'service': 'java-rmi'
783
- })
958
+ severity = "medium"
959
+ if "Class Loader Enabled" in message:
960
+ severity = "high"
961
+ findings.append(
962
+ {
963
+ "title": "Java RMI Class Loader Enabled",
964
+ "severity": severity,
965
+ "description": f"Java RMI endpoint with Class Loader enabled detected. This may allow remote code execution via deserialization attacks.",
966
+ "port": port,
967
+ "service": "java-rmi",
968
+ }
969
+ )
970
+ elif "Endpoint Detected" in message:
971
+ findings.append(
972
+ {
973
+ "title": "Java RMI Endpoint Detected",
974
+ "severity": "medium",
975
+ "description": f"Java RMI endpoint detected: {message}",
976
+ "port": port,
977
+ "service": "java-rmi",
978
+ }
979
+ )
784
980
 
785
981
  # Add service info
786
- services.append({
787
- 'port': port,
788
- 'protocol': 'tcp',
789
- 'service_name': 'java-rmi',
790
- 'service_version': 'Java RMI Registry'
791
- })
982
+ services.append(
983
+ {
984
+ "port": port,
985
+ "protocol": "tcp",
986
+ "service_name": "java-rmi",
987
+ "service_version": "Java RMI Registry",
988
+ }
989
+ )
792
990
 
793
- return {
794
- 'findings': findings,
795
- 'services': services
796
- }
991
+ return {"findings": findings, "services": services}
797
992
 
798
993
 
799
994
  def parse_msf_vnc_auth(output: str, target: str) -> Dict[str, Any]:
@@ -813,7 +1008,9 @@ def parse_msf_vnc_auth(output: str, target: str) -> Dict[str, Any]:
813
1008
  # Parse VNC security types
814
1009
  # Format: [+] 10.0.0.82:5900 - VNC server security types supported: VNC
815
1010
  # Format: [+] 10.0.0.82:5900 - VNC server security types supported: None
816
- vnc_pattern = r'\[\+\]\s+([\d.]+):(\d+)\s+-\s+VNC server security types supported:\s*(.+)'
1011
+ vnc_pattern = (
1012
+ r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+VNC server security types supported:\s*(.+)"
1013
+ )
817
1014
 
818
1015
  for match in re.finditer(vnc_pattern, clean_output):
819
1016
  ip = match.group(1)
@@ -821,52 +1018,223 @@ def parse_msf_vnc_auth(output: str, target: str) -> Dict[str, Any]:
821
1018
  sec_types = match.group(3).strip()
822
1019
 
823
1020
  # Check for no authentication
824
- if 'None' in sec_types:
825
- findings.append({
826
- 'title': 'VNC No Authentication Required',
827
- 'severity': 'critical',
828
- 'description': f'VNC server at port {port} allows connections without authentication. Security types: {sec_types}',
829
- 'port': port,
830
- 'service': 'vnc'
831
- })
1021
+ if "None" in sec_types:
1022
+ findings.append(
1023
+ {
1024
+ "title": "VNC No Authentication Required",
1025
+ "severity": "critical",
1026
+ "description": f"VNC server at port {port} allows connections without authentication. Security types: {sec_types}",
1027
+ "port": port,
1028
+ "service": "vnc",
1029
+ }
1030
+ )
832
1031
  else:
833
- findings.append({
834
- 'title': f'VNC Security Types Detected',
835
- 'severity': 'info',
836
- 'description': f'VNC server security types: {sec_types}',
837
- 'port': port,
838
- 'service': 'vnc'
839
- })
840
-
841
- services.append({
842
- 'port': port,
843
- 'protocol': 'tcp',
844
- 'service_name': 'vnc',
845
- 'service_version': f'Security: {sec_types}'
846
- })
1032
+ findings.append(
1033
+ {
1034
+ "title": f"VNC Security Types Detected",
1035
+ "severity": "info",
1036
+ "description": f"VNC server security types: {sec_types}",
1037
+ "port": port,
1038
+ "service": "vnc",
1039
+ }
1040
+ )
1041
+
1042
+ services.append(
1043
+ {
1044
+ "port": port,
1045
+ "protocol": "tcp",
1046
+ "service_name": "vnc",
1047
+ "service_version": f"Security: {sec_types}",
1048
+ }
1049
+ )
847
1050
 
848
1051
  # Also check for vnc_none_auth specific output
849
1052
  # Format: [*] 10.0.0.82:5900 - VNC server protocol version: ...
850
1053
  # Format: [+] 10.0.0.82:5900 - VNC server does not require authentication
851
- no_auth_pattern = r'\[\+\]\s+([\d.]+):(\d+)\s+-\s+VNC server does not require authentication'
1054
+ no_auth_pattern = (
1055
+ r"\[\+\]\s+([\d.]+):(\d+)\s+-\s+VNC server does not require authentication"
1056
+ )
852
1057
  for match in re.finditer(no_auth_pattern, clean_output):
853
1058
  ip = match.group(1)
854
1059
  port = int(match.group(2))
855
1060
 
856
1061
  # Avoid duplicates
857
- if not any(f['port'] == port and 'No Authentication' in f['title'] for f in findings):
858
- findings.append({
859
- 'title': 'VNC No Authentication Required',
860
- 'severity': 'critical',
861
- 'description': f'VNC server at port {port} does not require authentication.',
862
- 'port': port,
863
- 'service': 'vnc'
864
- })
1062
+ if not any(
1063
+ f["port"] == port and "No Authentication" in f["title"] for f in findings
1064
+ ):
1065
+ findings.append(
1066
+ {
1067
+ "title": "VNC No Authentication Required",
1068
+ "severity": "critical",
1069
+ "description": f"VNC server at port {port} does not require authentication.",
1070
+ "port": port,
1071
+ "service": "vnc",
1072
+ }
1073
+ )
1074
+
1075
+ return {"findings": findings, "services": services}
1076
+
1077
+
1078
+ def parse_msf_endpoint_mapper(output: str, target: str) -> Dict[str, Any]:
1079
+ """
1080
+ Parse MSF endpoint_mapper (RPC enumeration) output.
865
1081
 
866
- return {
867
- 'findings': findings,
868
- 'services': services
869
- }
1082
+ The endpoint_mapper module discovers RPC endpoints which can reveal:
1083
+ - Services running on the target
1084
+ - Named pipes available for exploitation
1085
+ - Protocol bindings and UUIDs
1086
+
1087
+ Returns:
1088
+ {
1089
+ 'findings': [], # RPC endpoint discoveries
1090
+ 'services': [] # Services detected
1091
+ }
1092
+ """
1093
+ findings = []
1094
+ services = []
1095
+ clean_output = strip_ansi_codes(output)
1096
+
1097
+ endpoints = []
1098
+ pipes = []
1099
+ service_names = []
1100
+
1101
+ # Pattern for endpoint entries - actual MSF format:
1102
+ # [*] 10.129.48.183:135 - d95afe70-a6d5-4259-822e-2c84da1ddb0d v1.0 TCP (49152) 10.129.48.183
1103
+ # [*] 10.129.48.183:135 - 897e2e5f-93f3-4376-9c9c-fd2277495c27 v1.0 LRPC (...) [Frs2 Service]
1104
+ uuid_pattern = r"\[\*\]\s+[\d.]+:(\d+)\s+-\s+([a-f0-9-]{36})\s+v?([0-9.]+)\s+(\w+)\s+\([^)]+\)(?:\s+[\d.]+)?(?:\s+\[([^\]]+)\])?"
1105
+ for match in re.finditer(uuid_pattern, clean_output, re.IGNORECASE):
1106
+ port = int(match.group(1))
1107
+ uuid = match.group(2)
1108
+ version = match.group(3) or ""
1109
+ protocol = match.group(4) # TCP, LRPC, PIPE, HTTP
1110
+ svc_name = match.group(5) # Service name in brackets if present
1111
+ endpoints.append(
1112
+ {
1113
+ "uuid": uuid,
1114
+ "version": version,
1115
+ "protocol": protocol,
1116
+ "port": port,
1117
+ "service": svc_name,
1118
+ }
1119
+ )
1120
+ if svc_name and svc_name not in service_names:
1121
+ service_names.append(svc_name)
1122
+
1123
+ # Pattern for PIPE entries specifically
1124
+ # [*] 10.129.48.183:135 - e3514235-4b06-11d1-ab04-00c04fc2dcd2 v4.0 PIPE (\pipe\lsass) \\DC [MS NT Directory DRS Interface]
1125
+ pipe_pattern = (
1126
+ r"\[\*\]\s+[\d.]+:(\d+)\s+-\s+[a-f0-9-]+\s+v?[0-9.]+\s+PIPE\s+\(([^)]+)\)"
1127
+ )
1128
+ for match in re.finditer(pipe_pattern, clean_output, re.IGNORECASE):
1129
+ port = int(match.group(1))
1130
+ pipe = match.group(2)
1131
+ if pipe not in pipes:
1132
+ pipes.append(pipe)
1133
+
1134
+ # Also look for old format patterns as fallback
1135
+ # Format: [*] 10.0.0.1:135 - UUID: d95afe70-a6d5-4259-822e-2c84da1ddb0d v1.0
1136
+ old_uuid_pattern = (
1137
+ r"\[\*\]\s+[\d.]+:(\d+)\s+-\s+UUID:\s+([a-f0-9-]+)\s+v?([0-9.]+)?"
1138
+ )
1139
+ for match in re.finditer(old_uuid_pattern, clean_output, re.IGNORECASE):
1140
+ port = int(match.group(1))
1141
+ uuid = match.group(2)
1142
+ version = match.group(3) or ""
1143
+ if not any(e.get("uuid") == uuid for e in endpoints):
1144
+ endpoints.append({"uuid": uuid, "version": version, "port": port})
1145
+
1146
+ # Pattern for [+] success lines (discovered services)
1147
+ success_pattern = r"\[\+\]\s+[\d.]+:(\d+)\s+-\s+(.+)"
1148
+ for match in re.finditer(success_pattern, clean_output):
1149
+ port = int(match.group(1))
1150
+ message = match.group(2).strip()
1151
+ if message and message not in [e.get("message") for e in endpoints]:
1152
+ endpoints.append({"message": message, "port": port})
1153
+
1154
+ # Create findings if we discovered anything
1155
+ if endpoints or pipes or service_names:
1156
+ # Unique endpoints count
1157
+ unique_uuids = len(set(e.get("uuid", "") for e in endpoints if e.get("uuid")))
1158
+ desc_parts = []
1159
+ if unique_uuids:
1160
+ desc_parts.append(f"{unique_uuids} RPC UUIDs")
1161
+ if pipes:
1162
+ desc_parts.append(f"{len(pipes)} named pipes")
1163
+ if service_names:
1164
+ desc_parts.append(f"{len(service_names)} services")
1165
+
1166
+ # Build description with discovered services
1167
+ description = f'RPC endpoint enumeration found: {", ".join(desc_parts)}.'
1168
+ if service_names:
1169
+ svc_list = ", ".join(service_names[:8])
1170
+ if len(service_names) > 8:
1171
+ svc_list += f"... (+{len(service_names) - 8} more)"
1172
+ description += f" Services: {svc_list}"
1173
+ if pipes:
1174
+ description += (
1175
+ f' Pipes: {", ".join(pipes[:5])}{"..." if len(pipes) > 5 else ""}'
1176
+ )
1177
+
1178
+ findings.append(
1179
+ {
1180
+ "title": f'RPC Endpoints Discovered ({", ".join(desc_parts)})',
1181
+ "severity": "medium", # Upgraded from info - this is useful recon
1182
+ "description": description,
1183
+ "port": 135,
1184
+ "service": "msrpc",
1185
+ "data": {
1186
+ "endpoints": endpoints[:20], # Limit for storage
1187
+ "pipes": pipes,
1188
+ "service_names": service_names,
1189
+ },
1190
+ }
1191
+ )
1192
+
1193
+ # Add msrpc service
1194
+ services.append(
1195
+ {
1196
+ "port": 135,
1197
+ "protocol": "tcp",
1198
+ "service_name": "msrpc",
1199
+ "service_version": f"RPC ({len(endpoints)} endpoints)",
1200
+ }
1201
+ )
1202
+
1203
+ # Check for interesting pipes that indicate attack vectors
1204
+ interesting_pipes = {
1205
+ "spoolss": ("Print Spooler", "medium", "PrintNightmare potential"),
1206
+ "samr": ("SAM Remote", "medium", "User enumeration possible"),
1207
+ "lsass": ("LSASS", "medium", "Domain/credential operations"),
1208
+ "lsarpc": ("LSA Remote", "medium", "Domain enumeration possible"),
1209
+ "netlogon": ("Netlogon", "high", "ZeroLogon check recommended"),
1210
+ "epmapper": ("Endpoint Mapper", "info", "RPC enumeration confirmed"),
1211
+ "protected_storage": (
1212
+ "Protected Storage",
1213
+ "medium",
1214
+ "Credential storage access",
1215
+ ),
1216
+ }
1217
+
1218
+ found_interesting = set()
1219
+ for pipe in pipes:
1220
+ # Normalize pipe path - handles \pipe\lsass and \\pipe\\lsass formats
1221
+ pipe_lower = (
1222
+ pipe.lower().replace("\\pipe\\", "").replace("\\", "").replace("/", "")
1223
+ )
1224
+ for key, (name, sev, desc) in interesting_pipes.items():
1225
+ if key in pipe_lower and key not in found_interesting:
1226
+ found_interesting.add(key)
1227
+ findings.append(
1228
+ {
1229
+ "title": f"{name} Pipe Available",
1230
+ "severity": sev,
1231
+ "description": f"{desc}. Pipe: {pipe}",
1232
+ "port": 135,
1233
+ "service": "msrpc",
1234
+ }
1235
+ )
1236
+
1237
+ return {"findings": findings, "services": services}
870
1238
 
871
1239
 
872
1240
  def parse_msf_ghostcat(output: str, target: str) -> Dict[str, Any]:
@@ -883,38 +1251,40 @@ def parse_msf_ghostcat(output: str, target: str) -> Dict[str, Any]:
883
1251
 
884
1252
  # Look for file content (indicates successful file read)
885
1253
  # Ghostcat returns file contents like web.xml
886
- has_xml_content = '<?xml' in clean_output or '<web-app' in clean_output
887
- has_file_content = 'WEB-INF' in clean_output or 'servlet' in clean_output.lower()
1254
+ has_xml_content = "<?xml" in clean_output or "<web-app" in clean_output
1255
+ has_file_content = "WEB-INF" in clean_output or "servlet" in clean_output.lower()
888
1256
 
889
1257
  # Check for explicit success messages
890
1258
  # Format: [+] File contents retrieved successfully
891
- success_pattern = r'\[\+\]\s+(.+)'
1259
+ success_pattern = r"\[\+\]\s+(.+)"
892
1260
  success_messages = re.findall(success_pattern, clean_output)
893
1261
 
894
1262
  if has_xml_content or has_file_content:
895
- findings.append({
896
- 'title': 'Tomcat Ghostcat File Read (CVE-2020-1938)',
897
- 'severity': 'high',
898
- 'description': 'Successfully read file contents via Tomcat AJP connector. This confirms CVE-2020-1938 vulnerability allowing arbitrary file read from the web application directory.',
899
- 'port': 8009,
900
- 'service': 'ajp13',
901
- 'data': {'evidence': 'XML/file content returned in response'}
902
- })
1263
+ findings.append(
1264
+ {
1265
+ "title": "Tomcat Ghostcat File Read (CVE-2020-1938)",
1266
+ "severity": "high",
1267
+ "description": "Successfully read file contents via Tomcat AJP connector. This confirms CVE-2020-1938 vulnerability allowing arbitrary file read from the web application directory.",
1268
+ "port": 8009,
1269
+ "service": "ajp13",
1270
+ "data": {"evidence": "XML/file content returned in response"},
1271
+ }
1272
+ )
903
1273
  elif success_messages:
904
1274
  for msg in success_messages:
905
- if 'File' in msg or 'content' in msg.lower():
906
- findings.append({
907
- 'title': 'Tomcat Ghostcat Vulnerability Confirmed',
908
- 'severity': 'high',
909
- 'description': f'Ghostcat vulnerability (CVE-2020-1938) confirmed: {msg}',
910
- 'port': 8009,
911
- 'service': 'ajp13'
912
- })
1275
+ if "File" in msg or "content" in msg.lower():
1276
+ findings.append(
1277
+ {
1278
+ "title": "Tomcat Ghostcat Vulnerability Confirmed",
1279
+ "severity": "high",
1280
+ "description": f"Ghostcat vulnerability (CVE-2020-1938) confirmed: {msg}",
1281
+ "port": 8009,
1282
+ "service": "ajp13",
1283
+ }
1284
+ )
913
1285
  break
914
1286
 
915
- return {
916
- 'findings': findings
917
- }
1287
+ return {"findings": findings}
918
1288
 
919
1289
 
920
1290
  def parse_msf_exploit(output: str, target: str, module: str) -> Dict[str, Any]:
@@ -932,82 +1302,92 @@ def parse_msf_exploit(output: str, target: str, module: str) -> Dict[str, Any]:
932
1302
  clean_output = strip_ansi_codes(output)
933
1303
 
934
1304
  # Extract module name for display
935
- module_name = module.split('/')[-1] if '/' in module else module
1305
+ module_name = module.split("/")[-1] if "/" in module else module
936
1306
 
937
1307
  # Check for session opened
938
1308
  # Format: [*] Command shell session 1 opened (192.168.1.224:4444 -> 192.168.1.240:35807)
939
1309
  # Format: [*] Meterpreter session 1 opened (10.0.0.1:4444 -> 10.0.0.82:45678)
940
- session_pattern = r'\[\*\]\s+(Command shell|Meterpreter)\s+session\s+(\d+)\s+opened\s+\(([^)]+)\)'
1310
+ session_pattern = (
1311
+ r"\[\*\]\s+(Command shell|Meterpreter)\s+session\s+(\d+)\s+opened\s+\(([^)]+)\)"
1312
+ )
941
1313
  for match in re.finditer(session_pattern, clean_output, re.IGNORECASE):
942
1314
  session_type = match.group(1)
943
1315
  session_id = match.group(2)
944
1316
  tunnel = match.group(3)
945
1317
 
946
- sessions.append({
947
- 'id': session_id,
948
- 'type': session_type.lower(),
949
- 'tunnel': tunnel,
950
- 'exploit': module
951
- })
952
-
953
- findings.append({
954
- 'title': f'Exploit Successful: {module_name}',
955
- 'severity': 'critical',
956
- 'description': f'{session_type} session {session_id} opened via {module}. Tunnel: {tunnel}',
957
- 'service': 'exploit',
958
- 'data': {
959
- 'exploit': module,
960
- 'session_id': session_id,
961
- 'session_type': session_type.lower(),
962
- 'tunnel': tunnel
1318
+ sessions.append(
1319
+ {
1320
+ "id": session_id,
1321
+ "type": session_type.lower(),
1322
+ "tunnel": tunnel,
1323
+ "exploit": module,
1324
+ }
1325
+ )
1326
+
1327
+ findings.append(
1328
+ {
1329
+ "title": f"Exploit Successful: {module_name}",
1330
+ "severity": "critical",
1331
+ "description": f"{session_type} session {session_id} opened via {module}. Tunnel: {tunnel}",
1332
+ "service": "exploit",
1333
+ "data": {
1334
+ "exploit": module,
1335
+ "session_id": session_id,
1336
+ "session_type": session_type.lower(),
1337
+ "tunnel": tunnel,
1338
+ },
963
1339
  }
964
- })
1340
+ )
965
1341
 
966
1342
  # Also check for "Session X created in the background"
967
- bg_session_pattern = r'\[\*\]\s+Session\s+(\d+)\s+created in the background'
1343
+ bg_session_pattern = r"\[\*\]\s+Session\s+(\d+)\s+created in the background"
968
1344
  for match in re.finditer(bg_session_pattern, clean_output, re.IGNORECASE):
969
1345
  session_id = match.group(1)
970
1346
  # Only add if not already found
971
- if not any(s['id'] == session_id for s in sessions):
972
- sessions.append({
973
- 'id': session_id,
974
- 'type': 'unknown',
975
- 'tunnel': 'background',
976
- 'exploit': module
977
- })
1347
+ if not any(s["id"] == session_id for s in sessions):
1348
+ sessions.append(
1349
+ {
1350
+ "id": session_id,
1351
+ "type": "unknown",
1352
+ "tunnel": "background",
1353
+ "exploit": module,
1354
+ }
1355
+ )
978
1356
 
979
1357
  # Check for exploit failure indicators
980
1358
  if not sessions:
981
1359
  # Look for common failure patterns
982
1360
  failure_patterns = [
983
- (r'Exploit completed, but no session was created', 'Exploit ran but target not vulnerable or payload failed'),
984
- (r'Exploit failed', 'Exploit execution failed'),
985
- (r'Target is not vulnerable', 'Target not vulnerable to this exploit'),
986
- (r'Connection refused', 'Could not connect to target service'),
987
- (r'Connection timed out', 'Connection to target timed out'),
1361
+ (
1362
+ r"Exploit completed, but no session was created",
1363
+ "Exploit ran but target not vulnerable or payload failed",
1364
+ ),
1365
+ (r"Exploit failed", "Exploit execution failed"),
1366
+ (r"Target is not vulnerable", "Target not vulnerable to this exploit"),
1367
+ (r"Connection refused", "Could not connect to target service"),
1368
+ (r"Connection timed out", "Connection to target timed out"),
988
1369
  ]
989
1370
 
990
1371
  for pattern, desc in failure_patterns:
991
1372
  if re.search(pattern, clean_output, re.IGNORECASE):
992
- findings.append({
993
- 'title': f'Exploit Failed: {module_name}',
994
- 'severity': 'info',
995
- 'description': desc,
996
- 'service': 'exploit',
997
- 'data': {'exploit': module, 'reason': desc}
998
- })
1373
+ findings.append(
1374
+ {
1375
+ "title": f"Exploit Failed: {module_name}",
1376
+ "severity": "info",
1377
+ "description": desc,
1378
+ "service": "exploit",
1379
+ "data": {"exploit": module, "reason": desc},
1380
+ }
1381
+ )
999
1382
  break
1000
1383
 
1001
- return {
1002
- 'findings': findings,
1003
- 'sessions': sessions
1004
- }
1384
+ return {"findings": findings, "sessions": sessions}
1005
1385
 
1006
1386
 
1007
1387
  def parse_msf_generic(output: str, target: str, module: str) -> Dict[str, Any]:
1008
1388
  """
1009
1389
  Generic parser for MSF modules without specific parsers.
1010
- Extracts [+] success lines as findings.
1390
+ Extracts [+] success lines and important [*] info lines as findings.
1011
1391
 
1012
1392
  Returns:
1013
1393
  {
@@ -1021,9 +1401,18 @@ def parse_msf_generic(output: str, target: str, module: str) -> Dict[str, Any]:
1021
1401
 
1022
1402
  # Extract all [+] lines (success indicators in MSF)
1023
1403
  # Format: [+] 10.0.0.82:port - Message
1024
- success_pattern = r'\[\+\]\s+([\d.]+):?(\d*)\s*-?\s*(.+)'
1404
+ success_pattern = r"\[\+\]\s+([\d.]+):?(\d*)\s*-?\s*(.+)"
1405
+
1406
+ # Also extract important [*] lines (info that contains findings)
1407
+ # These patterns indicate actual results worth reporting
1408
+ important_info_patterns = [
1409
+ r"\[\*\]\s+([\d.]+):?(\d*)\s*-?\s*(.*(?:Host is running|SMB Detected|detected|found|version|running).*)",
1410
+ r"\[\*\]\s+([\d.]+):?(\d*)\s*-?\s*(.*(?:credential|successful|authenticated|session).*)",
1411
+ ]
1025
1412
 
1026
1413
  seen_messages = set() # Avoid duplicate findings
1414
+
1415
+ # Process [+] lines first (always findings)
1027
1416
  for match in re.finditer(success_pattern, clean_output):
1028
1417
  ip = match.group(1)
1029
1418
  port_str = match.group(2)
@@ -1037,29 +1426,79 @@ def parse_msf_generic(output: str, target: str, module: str) -> Dict[str, Any]:
1037
1426
  port = int(port_str) if port_str else None
1038
1427
 
1039
1428
  # Determine severity based on message content
1040
- severity = 'info'
1041
- if any(word in message.lower() for word in ['vulnerability', 'vulnerable', 'exploit', 'rce', 'injection']):
1042
- severity = 'high'
1043
- elif any(word in message.lower() for word in ['password', 'credential', 'authentication', 'success']):
1044
- severity = 'critical'
1045
- elif any(word in message.lower() for word in ['detected', 'found', 'enabled', 'open']):
1046
- severity = 'medium'
1429
+ severity = "info"
1430
+ if any(
1431
+ word in message.lower()
1432
+ for word in ["vulnerability", "vulnerable", "exploit", "rce", "injection"]
1433
+ ):
1434
+ severity = "high"
1435
+ elif any(
1436
+ word in message.lower()
1437
+ for word in ["password", "credential", "authentication", "success"]
1438
+ ):
1439
+ severity = "critical"
1440
+ elif any(
1441
+ word in message.lower() for word in ["detected", "found", "enabled", "open"]
1442
+ ):
1443
+ severity = "medium"
1047
1444
 
1048
1445
  # Extract module name for title
1049
- module_name = module.split('/')[-1] if '/' in module else module
1050
-
1051
- findings.append({
1052
- 'title': f'{module_name}: {message[:60]}{"..." if len(message) > 60 else ""}',
1053
- 'severity': severity,
1054
- 'description': message,
1055
- 'port': port,
1056
- 'service': 'unknown'
1057
- })
1446
+ module_name = module.split("/")[-1] if "/" in module else module
1447
+
1448
+ findings.append(
1449
+ {
1450
+ "title": f'{module_name}: {message[:60]}{"..." if len(message) > 60 else ""}',
1451
+ "severity": severity,
1452
+ "description": message,
1453
+ "port": port,
1454
+ "service": "unknown",
1455
+ }
1456
+ )
1058
1457
 
1059
- return {
1060
- 'findings': findings,
1061
- 'services': services
1062
- }
1458
+ # Process important [*] lines (only if they contain real findings)
1459
+ for pattern in important_info_patterns:
1460
+ for match in re.finditer(pattern, clean_output, re.IGNORECASE):
1461
+ ip = match.group(1)
1462
+ port_str = match.group(2)
1463
+ message = match.group(3).strip()
1464
+
1465
+ # Skip empty, duplicate, or progress messages
1466
+ if not message or message in seen_messages:
1467
+ continue
1468
+ if "Scanned" in message and "of" in message and "hosts" in message:
1469
+ continue # Skip "Scanned X of Y hosts" progress messages
1470
+ if "module execution completed" in message.lower():
1471
+ continue # Skip completion messages
1472
+
1473
+ seen_messages.add(message)
1474
+
1475
+ port = int(port_str) if port_str else None
1476
+
1477
+ # Determine severity
1478
+ severity = "info"
1479
+ if any(
1480
+ word in message.lower()
1481
+ for word in ["credential", "successful", "password"]
1482
+ ):
1483
+ severity = "critical"
1484
+ elif any(
1485
+ word in message.lower() for word in ["detected", "version", "running"]
1486
+ ):
1487
+ severity = "medium"
1488
+
1489
+ module_name = module.split("/")[-1] if "/" in module else module
1490
+
1491
+ findings.append(
1492
+ {
1493
+ "title": f'{module_name}: {message[:60]}{"..." if len(message) > 60 else ""}',
1494
+ "severity": severity,
1495
+ "description": message,
1496
+ "port": port,
1497
+ "service": "unknown",
1498
+ }
1499
+ )
1500
+
1501
+ return {"findings": findings, "services": services}
1063
1502
 
1064
1503
 
1065
1504
  def parse_msf_log(log_path: str) -> Dict[str, Any]:
@@ -1073,16 +1512,16 @@ def parse_msf_log(log_path: str) -> Dict[str, Any]:
1073
1512
  Parsed data with services and findings
1074
1513
  """
1075
1514
  try:
1076
- with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
1515
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
1077
1516
  content = f.read()
1078
1517
 
1079
1518
  # Extract module and target from header
1080
1519
  # New format: "=== Plugin: Metasploit Auxiliary ===" + "Args: ['auxiliary/...', '-o', ...]"
1081
1520
  # Old format: "Module: auxiliary/..." + "Target: ..."
1082
- module_match = re.search(r'^Module:\s*(.+)$', content, re.MULTILINE)
1521
+ module_match = re.search(r"^Module:\s*(.+)$", content, re.MULTILINE)
1083
1522
  # Fixed regex: extract first element from Args list (handles lists with multiple items)
1084
1523
  args_match = re.search(r'^Args:\s*\[[\'"]([^\'"]+)[\'"]', content, re.MULTILINE)
1085
- target_match = re.search(r'^Target:\s*(.+)$', content, re.MULTILINE)
1524
+ target_match = re.search(r"^Target:\s*(.+)$", content, re.MULTILINE)
1086
1525
 
1087
1526
  # Determine module name (prefer Module: header, fallback to Args: parsing)
1088
1527
  if module_match:
@@ -1091,11 +1530,13 @@ def parse_msf_log(log_path: str) -> Dict[str, Any]:
1091
1530
  module = args_match.group(1).strip()
1092
1531
  else:
1093
1532
  # Try to find module from "use" command in output
1094
- use_match = re.search(r'use\s+(auxiliary/\S+|exploit/\S+)', content)
1533
+ use_match = re.search(r"use\s+(auxiliary/\S+|exploit/\S+)", content)
1095
1534
  if use_match:
1096
1535
  module = use_match.group(1).strip()
1097
1536
  else:
1098
- return {"error": "Could not parse MSF log header - no module/args found"}
1537
+ return {
1538
+ "error": "Could not parse MSF log header - no module/args found"
1539
+ }
1099
1540
 
1100
1541
  if not target_match:
1101
1542
  return {"error": "Could not parse MSF log header - no target found"}
@@ -1103,37 +1544,41 @@ def parse_msf_log(log_path: str) -> Dict[str, Any]:
1103
1544
  target = target_match.group(1).strip()
1104
1545
 
1105
1546
  # Route to appropriate parser based on module
1106
- if 'ssh_version' in module:
1547
+ if "ssh_version" in module:
1107
1548
  return parse_msf_ssh_version(content, target)
1108
- elif 'smb_version' in module:
1549
+ elif "smb_version" in module:
1109
1550
  return parse_msf_smb_version(content, target)
1110
- elif 'ssh_enumusers' in module:
1551
+ elif "ssh_enumusers" in module:
1111
1552
  return parse_msf_ssh_enumusers(content, target)
1112
- elif 'smb_enumshares' in module:
1553
+ elif "kerberos_enumusers" in module:
1554
+ return parse_msf_kerberos_enumusers(content, target)
1555
+ elif "smb_enumshares" in module:
1113
1556
  return parse_msf_smb_enumshares(content, target)
1114
- elif 'smtp_enum' in module:
1557
+ elif "smtp_enum" in module:
1115
1558
  return parse_msf_smtp_enum(content, target)
1116
- elif 'nfsmount' in module:
1559
+ elif "nfsmount" in module:
1117
1560
  return parse_msf_nfs_mount(content, target)
1118
- elif 'ftp/anonymous' in module or 'ftp_anonymous' in module:
1561
+ elif "ftp/anonymous" in module or "ftp_anonymous" in module:
1119
1562
  # FTP anonymous access scanner
1120
1563
  return parse_msf_ftp_anonymous(content, target)
1121
- elif 'mysql_login' in module:
1564
+ elif "mysql_login" in module:
1122
1565
  # MySQL login scanner - extracts version + credentials
1123
1566
  return parse_msf_mysql_login(content, target)
1124
- elif 'vnc_login' in module:
1567
+ elif "vnc_login" in module:
1125
1568
  # VNC login scanner - route to login parser for credentials
1126
1569
  return parse_msf_login_success(content, target, module)
1127
- elif any(x in module for x in ['_login', 'brute']):
1570
+ elif any(x in module for x in ["_login", "brute"]):
1128
1571
  # Any login/brute force module
1129
1572
  return parse_msf_login_success(content, target, module)
1130
- elif 'java_rmi' in module:
1573
+ elif "java_rmi" in module:
1131
1574
  return parse_msf_java_rmi(content, target)
1132
- elif 'vnc_none_auth' in module:
1575
+ elif "vnc_none_auth" in module:
1133
1576
  return parse_msf_vnc_auth(content, target)
1134
- elif 'ghostcat' in module or 'tomcat_ghostcat' in module:
1577
+ elif "ghostcat" in module or "tomcat_ghostcat" in module:
1135
1578
  return parse_msf_ghostcat(content, target)
1136
- elif module.startswith('exploit/'):
1579
+ elif "endpoint_mapper" in module:
1580
+ return parse_msf_endpoint_mapper(content, target)
1581
+ elif module.startswith("exploit/"):
1137
1582
  # Route exploit modules to exploit parser
1138
1583
  return parse_msf_exploit(content, target, module)
1139
1584
  else: