souleyez 2.43.26__py3-none-any.whl → 2.43.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23434 -10286
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.26.dist-info/RECORD +0 -379
  356. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -38,7 +38,7 @@ def parse_nmap_vuln_scripts(output: str) -> List[Dict[str, Any]]:
38
38
  current_host_ip = None
39
39
  current_port = None
40
40
 
41
- lines = output.split('\n')
41
+ lines = output.split("\n")
42
42
  i = 0
43
43
 
44
44
  while i < len(lines):
@@ -46,47 +46,53 @@ def parse_nmap_vuln_scripts(output: str) -> List[Dict[str, Any]]:
46
46
 
47
47
  # Track current host - "Nmap scan report for 10.0.0.73"
48
48
  if line.startswith("Nmap scan report for"):
49
- match = re.search(r'for (\d+\.\d+\.\d+\.\d+)', line)
49
+ match = re.search(r"for (\d+\.\d+\.\d+\.\d+)", line)
50
50
  if match:
51
51
  current_host_ip = match.group(1)
52
52
  else:
53
53
  # Try hostname (IP in parens)
54
- match = re.search(r'\((\d+\.\d+\.\d+\.\d+)\)', line)
54
+ match = re.search(r"\((\d+\.\d+\.\d+\.\d+)\)", line)
55
55
  if match:
56
56
  current_host_ip = match.group(1)
57
57
 
58
58
  # Track current port - "80/tcp open http"
59
- elif re.match(r'^\d+/(tcp|udp)', line):
59
+ elif re.match(r"^\d+/(tcp|udp)", line):
60
60
  parts = line.split()
61
61
  if parts:
62
- port_proto = parts[0].split('/')
62
+ port_proto = parts[0].split("/")
63
63
  current_port = int(port_proto[0])
64
64
 
65
65
  # Parse vulnerability script blocks
66
- elif line.startswith('| ') and ':' in line and current_host_ip:
66
+ elif line.startswith("| ") and ":" in line and current_host_ip:
67
67
  # Could be start of a script block like "| ssl-poodle:"
68
- script_match = re.match(r'\|\s*([a-zA-Z0-9_-]+):\s*$', line)
68
+ script_match = re.match(r"\|\s*([a-zA-Z0-9_-]+):\s*$", line)
69
69
  if script_match:
70
70
  script_name = script_match.group(1)
71
71
 
72
72
  # Collect all lines of this script block
73
73
  script_lines = []
74
74
  i += 1
75
- while i < len(lines) and (lines[i].startswith('|') or lines[i].startswith('|_')):
75
+ while i < len(lines) and (
76
+ lines[i].startswith("|") or lines[i].startswith("|_")
77
+ ):
76
78
  script_lines.append(lines[i])
77
- if lines[i].startswith('|_'):
79
+ if lines[i].startswith("|_"):
78
80
  break
79
81
  i += 1
80
82
 
81
- script_block = '\n'.join(script_lines)
83
+ script_block = "\n".join(script_lines)
82
84
 
83
85
  # Parse vulners output specially (CVE lists with scores)
84
- if script_name == 'vulners':
85
- vulns = _parse_vulners_block(script_block, current_host_ip, current_port)
86
+ if script_name == "vulners":
87
+ vulns = _parse_vulners_block(
88
+ script_block, current_host_ip, current_port
89
+ )
86
90
  vulnerabilities.extend(vulns)
87
91
  else:
88
92
  # Parse standard vuln script output
89
- vuln = _parse_vuln_script_block(script_name, script_block, current_host_ip, current_port)
93
+ vuln = _parse_vuln_script_block(
94
+ script_name, script_block, current_host_ip, current_port
95
+ )
90
96
  if vuln:
91
97
  vulnerabilities.extend(vuln)
92
98
 
@@ -112,14 +118,16 @@ def _parse_vulners_block(block: str, host_ip: str, port: int) -> List[Dict[str,
112
118
  vulnerabilities = []
113
119
  current_software = None
114
120
 
115
- for line in block.split('\n'):
116
- line = line.strip().lstrip('|').lstrip('_').strip()
121
+ for line in block.split("\n"):
122
+ line = line.strip().lstrip("|").lstrip("_").strip()
117
123
  if not line:
118
124
  continue
119
125
 
120
126
  # Track CPE or software version (e.g., "nginx 1.19.0:")
121
- if line.startswith('cpe:/') or (line.endswith(':') and not line.startswith('http')):
122
- current_software = line.rstrip(':')
127
+ if line.startswith("cpe:/") or (
128
+ line.endswith(":") and not line.startswith("http")
129
+ ):
130
+ current_software = line.rstrip(":")
123
131
  continue
124
132
 
125
133
  # Parse vulners line - multiple formats:
@@ -129,21 +137,21 @@ def _parse_vulners_block(block: str, host_ip: str, port: int) -> List[Dict[str,
129
137
  # 4. "EDB-ID:50973 7.7 https://..."
130
138
 
131
139
  # Generic pattern: ID SCORE URL [*EXPLOIT*]
132
- vuln_match = re.match(r'(\S+)\s+(\d+\.?\d*)\s+(https?://\S+)', line)
140
+ vuln_match = re.match(r"(\S+)\s+(\d+\.?\d*)\s+(https?://\S+)", line)
133
141
  if vuln_match:
134
142
  vuln_id = vuln_match.group(1)
135
143
  cvss_score = float(vuln_match.group(2))
136
144
  ref_url = vuln_match.group(3)
137
- is_exploit = '*EXPLOIT*' in line
145
+ is_exploit = "*EXPLOIT*" in line
138
146
 
139
147
  # Extract CVE from various formats
140
148
  cve_ids = []
141
- cve_in_id = re.search(r'CVE-\d{4}-\d+', vuln_id, re.IGNORECASE)
149
+ cve_in_id = re.search(r"CVE-\d{4}-\d+", vuln_id, re.IGNORECASE)
142
150
  if cve_in_id:
143
151
  cve_ids.append(cve_in_id.group(0).upper())
144
152
 
145
153
  # Also check the URL for CVE
146
- cve_in_url = re.search(r'CVE-\d{4}-\d+', ref_url, re.IGNORECASE)
154
+ cve_in_url = re.search(r"CVE-\d{4}-\d+", ref_url, re.IGNORECASE)
147
155
  if cve_in_url and cve_in_url.group(0).upper() not in cve_ids:
148
156
  cve_ids.append(cve_in_url.group(0).upper())
149
157
 
@@ -157,24 +165,28 @@ def _parse_vulners_block(block: str, host_ip: str, port: int) -> List[Dict[str,
157
165
 
158
166
  # Include high-severity vulns (CVSS >= 7.0) and all exploits
159
167
  if cvss_score >= 7.0 or is_exploit:
160
- vulnerabilities.append({
161
- 'host_ip': host_ip,
162
- 'port': port,
163
- 'script': 'vulners',
164
- 'title': title,
165
- 'state': 'VULNERABLE',
166
- 'cve_ids': cve_ids,
167
- 'cvss_score': cvss_score,
168
- 'references': [ref_url],
169
- 'description': f"{'Exploit available: ' if is_exploit else ''}{vuln_id} detected via vulners database",
170
- 'software': current_software,
171
- 'is_exploit': is_exploit
172
- })
168
+ vulnerabilities.append(
169
+ {
170
+ "host_ip": host_ip,
171
+ "port": port,
172
+ "script": "vulners",
173
+ "title": title,
174
+ "state": "VULNERABLE",
175
+ "cve_ids": cve_ids,
176
+ "cvss_score": cvss_score,
177
+ "references": [ref_url],
178
+ "description": f"{'Exploit available: ' if is_exploit else ''}{vuln_id} detected via vulners database",
179
+ "software": current_software,
180
+ "is_exploit": is_exploit,
181
+ }
182
+ )
173
183
 
174
184
  return vulnerabilities
175
185
 
176
186
 
177
- def _parse_vuln_script_block(script_name: str, block: str, host_ip: str, port: int) -> List[Dict[str, Any]]:
187
+ def _parse_vuln_script_block(
188
+ script_name: str, block: str, host_ip: str, port: int
189
+ ) -> List[Dict[str, Any]]:
178
190
  """
179
191
  Parse a standard nmap vuln script block.
180
192
 
@@ -183,76 +195,91 @@ def _parse_vuln_script_block(script_name: str, block: str, host_ip: str, port: i
183
195
  vulnerabilities = []
184
196
 
185
197
  # Check if this block indicates a vulnerability
186
- if 'VULNERABLE' not in block.upper():
198
+ if "VULNERABLE" not in block.upper():
187
199
  return []
188
200
 
189
201
  # Split into individual vulnerability entries (some scripts report multiple)
190
202
  # Each vuln typically starts with a title line after VULNERABLE:
191
- vuln_sections = re.split(r'\|\s+VULNERABLE:', block, flags=re.IGNORECASE)
203
+ vuln_sections = re.split(r"\|\s+VULNERABLE:", block, flags=re.IGNORECASE)
192
204
 
193
205
  for section in vuln_sections[1:]: # Skip first empty part
194
206
  vuln = {
195
- 'host_ip': host_ip,
196
- 'port': port,
197
- 'script': script_name,
198
- 'title': None,
199
- 'state': 'VULNERABLE',
200
- 'cve_ids': [],
201
- 'cvss_score': None,
202
- 'references': [],
203
- 'description': ''
207
+ "host_ip": host_ip,
208
+ "port": port,
209
+ "script": script_name,
210
+ "title": None,
211
+ "state": "VULNERABLE",
212
+ "cve_ids": [],
213
+ "cvss_score": None,
214
+ "references": [],
215
+ "description": "",
204
216
  }
205
217
 
206
- lines = section.strip().split('\n')
218
+ lines = section.strip().split("\n")
207
219
  description_parts = []
208
220
 
209
221
  for line in lines:
210
- line = line.strip().lstrip('|').lstrip('_').strip()
222
+ line = line.strip().lstrip("|").lstrip("_").strip()
211
223
 
212
224
  if not line:
213
225
  continue
214
226
 
215
227
  # Extract title (first non-empty line after VULNERABLE:)
216
- if not vuln['title'] and line and not line.startswith('State:') and not line.startswith('IDs:'):
217
- vuln['title'] = line
228
+ if (
229
+ not vuln["title"]
230
+ and line
231
+ and not line.startswith("State:")
232
+ and not line.startswith("IDs:")
233
+ ):
234
+ vuln["title"] = line
218
235
 
219
236
  # Extract CVE IDs
220
- cve_matches = re.findall(r'CVE[:\s-]*(CVE-\d{4}-\d+|(\d{4}-\d+))', line, re.IGNORECASE)
237
+ cve_matches = re.findall(
238
+ r"CVE[:\s-]*(CVE-\d{4}-\d+|(\d{4}-\d+))", line, re.IGNORECASE
239
+ )
221
240
  for match in cve_matches:
222
- cve = match[0] if match[0].startswith('CVE-') else f"CVE-{match[1]}"
223
- if cve not in vuln['cve_ids']:
224
- vuln['cve_ids'].append(cve)
241
+ cve = match[0] if match[0].startswith("CVE-") else f"CVE-{match[1]}"
242
+ if cve not in vuln["cve_ids"]:
243
+ vuln["cve_ids"].append(cve)
225
244
 
226
245
  # Also check for CVE format without prefix
227
- simple_cve = re.findall(r'\bCVE-(\d{4}-\d+)\b', line)
246
+ simple_cve = re.findall(r"\bCVE-(\d{4}-\d+)\b", line)
228
247
  for cve_num in simple_cve:
229
248
  cve = f"CVE-{cve_num}"
230
- if cve not in vuln['cve_ids']:
231
- vuln['cve_ids'].append(cve)
249
+ if cve not in vuln["cve_ids"]:
250
+ vuln["cve_ids"].append(cve)
232
251
 
233
252
  # Extract references
234
- if 'http' in line.lower():
235
- urls = re.findall(r'https?://\S+', line)
236
- vuln['references'].extend(urls)
253
+ if "http" in line.lower():
254
+ urls = re.findall(r"https?://\S+", line)
255
+ vuln["references"].extend(urls)
237
256
 
238
257
  # Build description (skip metadata lines)
239
- if not line.startswith('State:') and not line.startswith('IDs:') and not line.startswith('References:'):
240
- if line.startswith('Disclosure date:') or line.startswith('Check results:'):
258
+ if (
259
+ not line.startswith("State:")
260
+ and not line.startswith("IDs:")
261
+ and not line.startswith("References:")
262
+ ):
263
+ if line.startswith("Disclosure date:") or line.startswith(
264
+ "Check results:"
265
+ ):
241
266
  description_parts.append(line)
242
- elif vuln['title'] and line != vuln['title']:
267
+ elif vuln["title"] and line != vuln["title"]:
243
268
  description_parts.append(line)
244
269
 
245
- vuln['description'] = ' '.join(description_parts[:3]) # First 3 lines of description
270
+ vuln["description"] = " ".join(
271
+ description_parts[:3]
272
+ ) # First 3 lines of description
246
273
 
247
274
  # Generate title from script name if not found
248
- if not vuln['title']:
249
- vuln['title'] = script_name.replace('-', ' ').replace('_', ' ').title()
275
+ if not vuln["title"]:
276
+ vuln["title"] = script_name.replace("-", " ").replace("_", " ").title()
250
277
 
251
278
  # Add CVE to title if found
252
- if vuln['cve_ids'] and vuln['cve_ids'][0] not in vuln['title']:
253
- vuln['title'] = f"{vuln['cve_ids'][0]} - {vuln['title']}"
279
+ if vuln["cve_ids"] and vuln["cve_ids"][0] not in vuln["title"]:
280
+ vuln["title"] = f"{vuln['cve_ids'][0]} - {vuln['title']}"
254
281
 
255
- if vuln['title']:
282
+ if vuln["title"]:
256
283
  vulnerabilities.append(vuln)
257
284
 
258
285
  return vulnerabilities
@@ -278,13 +305,33 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
278
305
  }
279
306
  """
280
307
  hosts = []
308
+ domains = [] # Track AD domains discovered from LDAP/SMB banners
281
309
  current_host = None
282
310
 
283
- lines = output.split('\n')
311
+ lines = output.split("\n")
284
312
 
285
313
  for line in lines:
286
314
  line = line.strip()
287
315
 
316
+ # Extract AD domain from LDAP/SMB banners like:
317
+ # "Microsoft Windows Active Directory LDAP (Domain: active.htb, Site: Default-First-Site-Name)"
318
+ # Note: Nmap's LDAP scripts sometimes incorrectly append digits to domain names
319
+ # (e.g., "baby.vl0." instead of "baby.vl"). Clean up these artifacts.
320
+ domain_match = re.search(r"\(Domain:\s*([^,\)]+)", line)
321
+ if domain_match:
322
+ domain_name = domain_match.group(1).strip()
323
+ # Strip trailing dots (FQDN format) and trailing digits that are nmap artifacts
324
+ # Pattern: strip trailing digits followed by optional dot (e.g., "baby.vl0." → "baby.vl")
325
+ domain_name = re.sub(r"\d+\.?$", "", domain_name).rstrip(".")
326
+ if domain_name and not any(d["domain"] == domain_name for d in domains):
327
+ domains.append(
328
+ {
329
+ "domain": domain_name,
330
+ "ip": current_host.get("ip") if current_host else None,
331
+ "source": "nmap_banner",
332
+ }
333
+ )
334
+
288
335
  # Parse host line: "Nmap scan report for 10.0.0.5" or "Nmap scan report for example.com (10.0.0.5)"
289
336
  # Also handles: "Nmap scan report for 10.0.0.5 [host down, received no-response]"
290
337
  if line.startswith("Nmap scan report for"):
@@ -293,16 +340,16 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
293
340
 
294
341
  # Extract IP and hostname, removing any trailing status info like [host down...]
295
342
  # Remove the [host down...] part first
296
- clean_line = re.sub(r'\s*\[host.*?\].*$', '', line)
343
+ clean_line = re.sub(r"\s*\[host.*?\].*$", "", line)
297
344
 
298
345
  # Extract IP and hostname
299
- match = re.search(r'for (.+?)(?:\s+\((.+?)\))?$', clean_line)
346
+ match = re.search(r"for (.+?)(?:\s+\((.+?)\))?$", clean_line)
300
347
  if match:
301
348
  target = match.group(1)
302
349
  paren_content = match.group(2)
303
350
 
304
351
  # Determine if target is IP or hostname
305
- if re.match(r'^\d+\.\d+\.\d+\.\d+$', target):
352
+ if re.match(r"^\d+\.\d+\.\d+\.\d+$", target):
306
353
  ip = target
307
354
  hostname = paren_content if paren_content else None
308
355
  else:
@@ -312,15 +359,15 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
312
359
  # Check if this host already exists (e.g., from "Discovered open port" lines)
313
360
  existing_host = None
314
361
  for h in hosts:
315
- if h.get('ip') == ip:
362
+ if h.get("ip") == ip:
316
363
  existing_host = h
317
364
  break
318
365
 
319
366
  if existing_host:
320
367
  # Use existing host and update hostname if needed
321
368
  current_host = existing_host
322
- if hostname and not current_host.get('hostname'):
323
- current_host['hostname'] = hostname
369
+ if hostname and not current_host.get("hostname"):
370
+ current_host["hostname"] = hostname
324
371
  else:
325
372
  current_host = {
326
373
  "ip": ip,
@@ -329,11 +376,11 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
329
376
  "os": None,
330
377
  "mac_address": None,
331
378
  "os_accuracy": None,
332
- "services": []
379
+ "services": [],
333
380
  }
334
381
 
335
382
  # Check if the line indicates host is down
336
- if '[host down' in line and current_host:
383
+ if "[host down" in line and current_host:
337
384
  current_host["status"] = "down"
338
385
 
339
386
  # Parse host status
@@ -346,21 +393,23 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
346
393
  # Parse "Skipping host" (host timeout) - still mark as up since we got some response
347
394
  elif "Skipping host" in line and "due to host timeout" in line:
348
395
  # Extract the IP from the line
349
- match = re.search(r'Skipping host (\d+\.\d+\.\d+\.\d+)', line)
396
+ match = re.search(r"Skipping host (\d+\.\d+\.\d+\.\d+)", line)
350
397
  if match:
351
398
  skip_ip = match.group(1)
352
399
  # Find the host with this IP and mark it (it may have discovered ports)
353
400
  for h in hosts:
354
- if h.get('ip') == skip_ip:
355
- h['status'] = 'up' # Host responded, just timed out
401
+ if h.get("ip") == skip_ip:
402
+ h["status"] = "up" # Host responded, just timed out
356
403
  break
357
- if current_host and current_host.get('ip') == skip_ip:
358
- current_host['status'] = 'up'
404
+ if current_host and current_host.get("ip") == skip_ip:
405
+ current_host["status"] = "up"
359
406
 
360
407
  # Parse "Discovered open port" lines (shown during scan, before timeout)
361
408
  # Format: "Discovered open port 443/tcp on 10.0.0.48"
362
409
  elif "Discovered open port" in line:
363
- match = re.search(r'Discovered open port (\d+)/(tcp|udp) on (\d+\.\d+\.\d+\.\d+)', line)
410
+ match = re.search(
411
+ r"Discovered open port (\d+)/(tcp|udp) on (\d+\.\d+\.\d+\.\d+)", line
412
+ )
364
413
  if match:
365
414
  port = int(match.group(1))
366
415
  protocol = match.group(2)
@@ -369,11 +418,15 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
369
418
  # Find or create host entry for this IP
370
419
  target_host = None
371
420
  for h in hosts:
372
- if h.get('ip') == host_ip:
421
+ if h.get("ip") == host_ip:
373
422
  target_host = h
374
423
  break
375
424
 
376
- if not target_host and current_host and current_host.get('ip') == host_ip:
425
+ if (
426
+ not target_host
427
+ and current_host
428
+ and current_host.get("ip") == host_ip
429
+ ):
377
430
  target_host = current_host
378
431
  elif not target_host:
379
432
  # Create a new host entry
@@ -384,7 +437,7 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
384
437
  "os": None,
385
438
  "mac_address": None,
386
439
  "os_accuracy": None,
387
- "services": []
440
+ "services": [],
388
441
  }
389
442
  hosts.append(target_host)
390
443
 
@@ -399,73 +452,86 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
399
452
  # Infer service name from common ports
400
453
  service_name = None
401
454
  if port == 80:
402
- service_name = 'http'
455
+ service_name = "http"
403
456
  elif port == 443:
404
- service_name = 'https'
457
+ service_name = "https"
405
458
  elif port == 22:
406
- service_name = 'ssh'
459
+ service_name = "ssh"
407
460
  elif port == 21:
408
- service_name = 'ftp'
461
+ service_name = "ftp"
409
462
  elif port == 23:
410
- service_name = 'telnet'
463
+ service_name = "telnet"
411
464
  elif port == 25:
412
- service_name = 'smtp'
465
+ service_name = "smtp"
413
466
  elif port == 53:
414
- service_name = 'domain'
467
+ service_name = "domain"
415
468
  elif port == 445:
416
- service_name = 'microsoft-ds'
469
+ service_name = "microsoft-ds"
417
470
  elif port == 139:
418
- service_name = 'netbios-ssn'
471
+ service_name = "netbios-ssn"
419
472
  elif port == 3306:
420
- service_name = 'mysql'
473
+ service_name = "mysql"
421
474
  elif port == 5432:
422
- service_name = 'postgresql'
423
-
424
- target_host["services"].append({
425
- "port": port,
426
- "protocol": protocol,
427
- "state": "open",
428
- "service": service_name,
429
- "product": None,
430
- "version": None,
431
- "raw_version": None
432
- })
475
+ service_name = "postgresql"
476
+
477
+ target_host["services"].append(
478
+ {
479
+ "port": port,
480
+ "protocol": protocol,
481
+ "state": "open",
482
+ "service": service_name,
483
+ "product": None,
484
+ "version": None,
485
+ "raw_version": None,
486
+ }
487
+ )
433
488
 
434
489
  # Parse service line: "22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1"
435
- elif re.match(r'^\d+/(tcp|udp)', line) and current_host:
490
+ elif re.match(r"^\d+/(tcp|udp)", line) and current_host:
436
491
  parts = line.split(None, 4) # Split on whitespace, max 5 parts
437
492
  if len(parts) >= 3:
438
- port_proto = parts[0].split('/')
493
+ port_proto = parts[0].split("/")
439
494
  port = int(port_proto[0])
440
- protocol = port_proto[1] if len(port_proto) > 1 else 'tcp'
495
+ protocol = port_proto[1] if len(port_proto) > 1 else "tcp"
441
496
  state = parts[1]
442
497
  service_name = parts[2] if len(parts) > 2 else None
443
498
 
444
499
  # Everything after service name is version info
445
- raw_version = ' '.join(parts[3:]) if len(parts) > 3 else None
446
-
500
+ raw_version = " ".join(parts[3:]) if len(parts) > 3 else None
501
+
447
502
  # Parse product and version from raw version string
448
503
  product = None
449
504
  version = None
450
-
505
+
451
506
  if raw_version:
452
507
  try:
453
508
  # Remove nmap metadata: "syn-ack ttl XX", "reset ttl XX", etc.
454
509
  cleaned = raw_version
455
510
  # Handle various nmap scan type prefixes
456
- metadata_prefixes = ['syn-ack', 'reset', 'conn-refused', 'no-response']
511
+ metadata_prefixes = [
512
+ "syn-ack",
513
+ "reset",
514
+ "conn-refused",
515
+ "no-response",
516
+ ]
457
517
  for prefix in metadata_prefixes:
458
518
  if cleaned.lower().startswith(prefix):
459
519
  parts_ver = cleaned.split()
460
520
  # Skip prefix and "ttl XX" if present
461
- if len(parts_ver) > 1 and 'ttl' in parts_ver:
521
+ if len(parts_ver) > 1 and "ttl" in parts_ver:
462
522
  try:
463
- ttl_idx = parts_ver.index('ttl')
464
- cleaned = ' '.join(parts_ver[ttl_idx+2:]) # Skip "ttl XX"
523
+ ttl_idx = parts_ver.index("ttl")
524
+ cleaned = " ".join(
525
+ parts_ver[ttl_idx + 2 :]
526
+ ) # Skip "ttl XX"
465
527
  except (ValueError, IndexError):
466
- cleaned = ' '.join(parts_ver[1:]) # Skip just prefix
528
+ cleaned = " ".join(
529
+ parts_ver[1:]
530
+ ) # Skip just prefix
467
531
  else:
468
- cleaned = ' '.join(parts_ver[1:]) # Skip just prefix
532
+ cleaned = " ".join(
533
+ parts_ver[1:]
534
+ ) # Skip just prefix
469
535
  break
470
536
 
471
537
  # Extract product and version with multiple patterns
@@ -476,9 +542,9 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
476
542
  # "OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13" → product="OpenSSH", version="6.6.1p1"
477
543
 
478
544
  version_patterns = [
479
- r'([A-Za-z][\w\s\-\.]+?)\s+(v?\d+[\.\d]+[\w\-\.]*)', # Standard
480
- r'^([A-Za-z][\w\-]+)\s+(\d[\w\.\-]+)', # ProductName vX.Y.Z
481
- r'^([A-Za-z][\w\s]+?)\s+v?(\d+(?:\.\d+)+)', # "Product Name 1.2.3"
545
+ r"([A-Za-z][\w\s\-\.]+?)\s+(v?\d+[\.\d]+[\w\-\.]*)", # Standard
546
+ r"^([A-Za-z][\w\-]+)\s+(\d[\w\.\-]+)", # ProductName vX.Y.Z
547
+ r"^([A-Za-z][\w\s]+?)\s+v?(\d+(?:\.\d+)+)", # "Product Name 1.2.3"
482
548
  ]
483
549
 
484
550
  matched = False
@@ -503,11 +569,13 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
503
569
  # This handles cases where nmap misidentifies or can't fingerprint web apps
504
570
  # Common misidentifications: ppp (port 3000), upnp (port 8000), tcpwrapped
505
571
  common_web_ports = [3000, 8080, 8000, 8888, 9090]
506
- misidentified_services = ['unknown', 'tcpwrapped', 'ppp', 'upnp', None]
507
- if (service_name in misidentified_services or not service_name) and port in common_web_ports:
508
- service_name = 'http'
572
+ misidentified_services = ["unknown", "tcpwrapped", "ppp", "upnp", None]
573
+ if (
574
+ service_name in misidentified_services or not service_name
575
+ ) and port in common_web_ports:
576
+ service_name = "http"
509
577
  if not product:
510
- product = 'http'
578
+ product = "http"
511
579
 
512
580
  # Check if we already have this port (from "Discovered open port" lines)
513
581
  existing_svc = None
@@ -528,37 +596,39 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
528
596
  if raw_version:
529
597
  existing_svc["raw_version"] = raw_version
530
598
  else:
531
- current_host["services"].append({
532
- "port": port,
533
- "protocol": protocol,
534
- "state": state,
535
- "service": service_name,
536
- "product": product,
537
- "version": version,
538
- "raw_version": raw_version
539
- })
599
+ current_host["services"].append(
600
+ {
601
+ "port": port,
602
+ "protocol": protocol,
603
+ "state": state,
604
+ "service": service_name,
605
+ "product": product,
606
+ "version": version,
607
+ "raw_version": raw_version,
608
+ }
609
+ )
540
610
 
541
611
  # Parse OS detection: "Running: Linux 4.X|5.X" or "OS details: Linux 5.4"
542
612
  elif ("Running:" in line or "OS details:" in line) and current_host:
543
- os_info = line.split(':', 1)[1].strip()
613
+ os_info = line.split(":", 1)[1].strip()
544
614
  current_host["os"] = os_info
545
615
 
546
616
  # Parse MAC address: "MAC Address: 00:11:22:33:44:55 (Vendor)"
547
617
  elif "MAC Address:" in line and current_host:
548
- match = re.search(r'MAC Address:\s+([0-9A-Fa-f:]{17})', line)
618
+ match = re.search(r"MAC Address:\s+([0-9A-Fa-f:]{17})", line)
549
619
  if match:
550
620
  current_host["mac_address"] = match.group(1)
551
621
 
552
622
  # Parse OS with confidence: "OS: Linux 5.x (95%)"
553
623
  elif line.startswith("OS:") and current_host:
554
- match = re.search(r'OS:\s+(.+?)\s+\((\d+)%\)', line)
624
+ match = re.search(r"OS:\s+(.+?)\s+\((\d+)%\)", line)
555
625
  if match:
556
626
  current_host["os"] = match.group(1).strip()
557
627
  current_host["os_accuracy"] = int(match.group(2))
558
628
 
559
629
  # Parse OS from Service Info line: "Service Info: OSs: Windows, Windows Server 2008 R2 - 2012"
560
630
  elif "Service Info:" in line and "OSs:" in line and current_host:
561
- match = re.search(r'OSs?:\s+([^;]+)', line)
631
+ match = re.search(r"OSs?:\s+([^;]+)", line)
562
632
  if match:
563
633
  os_info = match.group(1).strip()
564
634
  # Only update if we don't already have more specific OS info
@@ -566,8 +636,13 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
566
636
  current_host["os"] = os_info
567
637
 
568
638
  # Parse OS from Service Info line: "Service Info: OS: Linux"
569
- elif "Service Info:" in line and "OS:" in line and "OSs:" not in line and current_host:
570
- match = re.search(r'OS:\s+([^;,]+)', line)
639
+ elif (
640
+ "Service Info:" in line
641
+ and "OS:" in line
642
+ and "OSs:" not in line
643
+ and current_host
644
+ ):
645
+ match = re.search(r"OS:\s+([^;,]+)", line)
571
646
  if match:
572
647
  os_info = match.group(1).strip()
573
648
  # Only update if we don't already have more specific OS info
@@ -576,7 +651,7 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
576
651
 
577
652
  # Parse hostname from Service Info: "Service Info: Host: VAGRANT-2008R2"
578
653
  elif "Service Info:" in line and "Host:" in line and current_host:
579
- match = re.search(r'Host:\s+([^\s;,]+)', line)
654
+ match = re.search(r"Host:\s+([^\s;,]+)", line)
580
655
  if match:
581
656
  hostname = match.group(1).strip()
582
657
  # Only update if we don't already have a hostname
@@ -586,13 +661,17 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
586
661
  # Parse OS from SMB script: "| OS: Windows Server 2008 R2 Standard 7601 Service Pack 1"
587
662
  elif line.strip().startswith("|") and "OS:" in line and current_host:
588
663
  # This is from smb-os-discovery script - more detailed than Service Info
589
- match = re.search(r'\|\s+OS:\s+(.+?)(?:\s+\(|$)', line)
664
+ match = re.search(r"\|\s+OS:\s+(.+?)(?:\s+\(|$)", line)
590
665
  if match:
591
666
  os_info = match.group(1).strip()
592
667
  # SMB OS discovery is very accurate, so always update
593
- if os_info and os_info != "Windows": # Don't overwrite with generic "Windows"
668
+ if (
669
+ os_info and os_info != "Windows"
670
+ ): # Don't overwrite with generic "Windows"
594
671
  current_host["os"] = os_info
595
- current_host["os_accuracy"] = 95 # High confidence for SMB detection
672
+ current_host["os_accuracy"] = (
673
+ 95 # High confidence for SMB detection
674
+ )
596
675
 
597
676
  # Don't forget the last host (if not already added)
598
677
  if current_host and current_host not in hosts:
@@ -607,7 +686,8 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
607
686
  return {
608
687
  "hosts": hosts,
609
688
  "vulnerabilities": vulnerabilities,
610
- "info_scripts": info_scripts
689
+ "info_scripts": info_scripts,
690
+ "domains": domains,
611
691
  }
612
692
 
613
693
 
@@ -631,18 +711,18 @@ def parse_nmap_info_scripts(output: str) -> List[Dict[str, Any]]:
631
711
  current_host_ip = None
632
712
  current_port = None
633
713
 
634
- lines = output.split('\n')
714
+ lines = output.split("\n")
635
715
  i = 0
636
716
 
637
717
  # Info scripts to capture (add more as needed)
638
718
  info_scripts = {
639
- 'vnc-info': 'VNC Server Information',
640
- 'ssh-hostkey': 'SSH Host Key',
641
- 'http-server-header': 'HTTP Server Header',
642
- 'ssl-cert': 'SSL Certificate',
643
- 'http-title': 'HTTP Page Title',
644
- 'smb-os-discovery': 'SMB OS Discovery',
645
- 'rdp-ntlm-info': 'RDP NTLM Information',
719
+ "vnc-info": "VNC Server Information",
720
+ "ssh-hostkey": "SSH Host Key",
721
+ "http-server-header": "HTTP Server Header",
722
+ "ssl-cert": "SSL Certificate",
723
+ "http-title": "HTTP Page Title",
724
+ "smb-os-discovery": "SMB OS Discovery",
725
+ "rdp-ntlm-info": "RDP NTLM Information",
646
726
  }
647
727
 
648
728
  while i < len(lines):
@@ -650,26 +730,26 @@ def parse_nmap_info_scripts(output: str) -> List[Dict[str, Any]]:
650
730
 
651
731
  # Track current host - "Nmap scan report for 10.0.0.73"
652
732
  if line.startswith("Nmap scan report for"):
653
- match = re.search(r'for (\d+\.\d+\.\d+\.\d+)', line)
733
+ match = re.search(r"for (\d+\.\d+\.\d+\.\d+)", line)
654
734
  if match:
655
735
  current_host_ip = match.group(1)
656
736
  else:
657
737
  # Try hostname (IP in parens)
658
- match = re.search(r'\((\d+\.\d+\.\d+\.\d+)\)', line)
738
+ match = re.search(r"\((\d+\.\d+\.\d+\.\d+)\)", line)
659
739
  if match:
660
740
  current_host_ip = match.group(1)
661
741
 
662
742
  # Track current port - "80/tcp open http"
663
- elif re.match(r'^\d+/(tcp|udp)', line):
743
+ elif re.match(r"^\d+/(tcp|udp)", line):
664
744
  parts = line.split()
665
745
  if parts:
666
- port_proto = parts[0].split('/')
746
+ port_proto = parts[0].split("/")
667
747
  current_port = int(port_proto[0])
668
748
 
669
749
  # Parse info script blocks
670
- elif line.startswith('| ') and ':' in line and current_host_ip:
750
+ elif line.startswith("| ") and ":" in line and current_host_ip:
671
751
  # Could be start of a script block like "| vnc-info:"
672
- script_match = re.match(r'\|\s*([a-zA-Z0-9_-]+):\s*$', line)
752
+ script_match = re.match(r"\|\s*([a-zA-Z0-9_-]+):\s*$", line)
673
753
  if script_match:
674
754
  script_name = script_match.group(1)
675
755
 
@@ -678,24 +758,28 @@ def parse_nmap_info_scripts(output: str) -> List[Dict[str, Any]]:
678
758
  # Collect all lines of this script block
679
759
  script_lines = []
680
760
  i += 1
681
- while i < len(lines) and (lines[i].startswith('|') or lines[i].startswith('|_')):
761
+ while i < len(lines) and (
762
+ lines[i].startswith("|") or lines[i].startswith("|_")
763
+ ):
682
764
  # Clean up the line
683
- clean_line = lines[i].lstrip('|').lstrip('_').strip()
765
+ clean_line = lines[i].lstrip("|").lstrip("_").strip()
684
766
  if clean_line:
685
767
  script_lines.append(clean_line)
686
- if lines[i].startswith('|_'):
768
+ if lines[i].startswith("|_"):
687
769
  break
688
770
  i += 1
689
771
 
690
772
  if script_lines:
691
- findings.append({
692
- 'host_ip': current_host_ip,
693
- 'port': current_port,
694
- 'script': script_name,
695
- 'title': info_scripts[script_name],
696
- 'severity': 'info',
697
- 'description': '\n'.join(script_lines)
698
- })
773
+ findings.append(
774
+ {
775
+ "host_ip": current_host_ip,
776
+ "port": current_port,
777
+ "script": script_name,
778
+ "title": info_scripts[script_name],
779
+ "severity": "info",
780
+ "description": "\n".join(script_lines),
781
+ }
782
+ )
699
783
  continue
700
784
 
701
785
  i += 1
@@ -728,11 +812,15 @@ def parse_nmap_log(log_path: str) -> Dict[str, Any]:
728
812
  Parsed nmap data with hosts, services, and vulnerabilities
729
813
  """
730
814
  try:
731
- with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
815
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
732
816
  content = f.read()
733
817
 
734
818
  return parse_nmap_text(content)
735
819
  except FileNotFoundError:
736
- return {"hosts": [], "vulnerabilities": [], "error": f"File not found: {log_path}"}
820
+ return {
821
+ "hosts": [],
822
+ "vulnerabilities": [],
823
+ "error": f"File not found: {log_path}",
824
+ }
737
825
  except Exception as e:
738
826
  return {"hosts": [], "vulnerabilities": [], "error": str(e)}