souleyez 2.43.28__py3-none-any.whl → 2.43.32__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 (356) 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 +9592 -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 +1238 -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 +2198 -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 +288 -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/whois_handler.py +277 -0
  126. souleyez/handlers/wpscan_handler.py +554 -0
  127. souleyez/history.py +32 -16
  128. souleyez/importers/msf_importer.py +106 -75
  129. souleyez/importers/smart_importer.py +208 -147
  130. souleyez/integrations/siem/__init__.py +10 -10
  131. souleyez/integrations/siem/base.py +17 -18
  132. souleyez/integrations/siem/elastic.py +108 -122
  133. souleyez/integrations/siem/factory.py +207 -80
  134. souleyez/integrations/siem/googlesecops.py +146 -154
  135. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  136. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  137. souleyez/integrations/siem/sentinel.py +107 -109
  138. souleyez/integrations/siem/splunk.py +246 -212
  139. souleyez/integrations/siem/wazuh.py +65 -71
  140. souleyez/integrations/wazuh/__init__.py +5 -5
  141. souleyez/integrations/wazuh/client.py +70 -93
  142. souleyez/integrations/wazuh/config.py +85 -57
  143. souleyez/integrations/wazuh/host_mapper.py +28 -36
  144. souleyez/integrations/wazuh/sync.py +78 -68
  145. souleyez/intelligence/__init__.py +4 -5
  146. souleyez/intelligence/correlation_analyzer.py +309 -295
  147. souleyez/intelligence/exploit_knowledge.py +661 -623
  148. souleyez/intelligence/exploit_suggestions.py +159 -139
  149. souleyez/intelligence/gap_analyzer.py +132 -97
  150. souleyez/intelligence/gap_detector.py +251 -214
  151. souleyez/intelligence/sensitive_tables.py +266 -129
  152. souleyez/intelligence/service_parser.py +137 -123
  153. souleyez/intelligence/surface_analyzer.py +407 -268
  154. souleyez/intelligence/target_parser.py +159 -162
  155. souleyez/licensing/__init__.py +6 -6
  156. souleyez/licensing/validator.py +17 -19
  157. souleyez/log_config.py +79 -54
  158. souleyez/main.py +1505 -687
  159. souleyez/migrations/fix_job_counter.py +16 -14
  160. souleyez/parsers/bloodhound_parser.py +41 -39
  161. souleyez/parsers/crackmapexec_parser.py +178 -111
  162. souleyez/parsers/dalfox_parser.py +72 -77
  163. souleyez/parsers/dnsrecon_parser.py +103 -91
  164. souleyez/parsers/enum4linux_parser.py +183 -153
  165. souleyez/parsers/ffuf_parser.py +29 -25
  166. souleyez/parsers/gobuster_parser.py +301 -41
  167. souleyez/parsers/hashcat_parser.py +324 -79
  168. souleyez/parsers/http_fingerprint_parser.py +350 -103
  169. souleyez/parsers/hydra_parser.py +131 -111
  170. souleyez/parsers/impacket_parser.py +231 -178
  171. souleyez/parsers/john_parser.py +98 -86
  172. souleyez/parsers/katana_parser.py +316 -0
  173. souleyez/parsers/msf_parser.py +943 -498
  174. souleyez/parsers/nikto_parser.py +346 -65
  175. souleyez/parsers/nmap_parser.py +262 -174
  176. souleyez/parsers/nuclei_parser.py +40 -44
  177. souleyez/parsers/responder_parser.py +26 -26
  178. souleyez/parsers/searchsploit_parser.py +74 -74
  179. souleyez/parsers/service_explorer_parser.py +279 -0
  180. souleyez/parsers/smbmap_parser.py +180 -124
  181. souleyez/parsers/sqlmap_parser.py +434 -308
  182. souleyez/parsers/theharvester_parser.py +75 -57
  183. souleyez/parsers/whois_parser.py +135 -94
  184. souleyez/parsers/wpscan_parser.py +278 -190
  185. souleyez/plugins/afp.py +44 -36
  186. souleyez/plugins/afp_brute.py +114 -46
  187. souleyez/plugins/ard.py +48 -37
  188. souleyez/plugins/bloodhound.py +95 -61
  189. souleyez/plugins/certipy.py +303 -0
  190. souleyez/plugins/crackmapexec.py +186 -85
  191. souleyez/plugins/dalfox.py +120 -59
  192. souleyez/plugins/dns_hijack.py +146 -41
  193. souleyez/plugins/dnsrecon.py +97 -61
  194. souleyez/plugins/enum4linux.py +91 -66
  195. souleyez/plugins/evil_winrm.py +291 -0
  196. souleyez/plugins/ffuf.py +166 -90
  197. souleyez/plugins/firmware_extract.py +133 -29
  198. souleyez/plugins/gobuster.py +387 -190
  199. souleyez/plugins/gpp_extract.py +393 -0
  200. souleyez/plugins/hashcat.py +100 -73
  201. souleyez/plugins/http_fingerprint.py +854 -267
  202. souleyez/plugins/hydra.py +566 -200
  203. souleyez/plugins/impacket_getnpusers.py +117 -69
  204. souleyez/plugins/impacket_psexec.py +84 -64
  205. souleyez/plugins/impacket_secretsdump.py +103 -69
  206. souleyez/plugins/impacket_smbclient.py +89 -75
  207. souleyez/plugins/john.py +86 -69
  208. souleyez/plugins/katana.py +313 -0
  209. souleyez/plugins/kerbrute.py +237 -0
  210. souleyez/plugins/lfi_extract.py +541 -0
  211. souleyez/plugins/macos_ssh.py +117 -48
  212. souleyez/plugins/mdns.py +35 -30
  213. souleyez/plugins/msf_auxiliary.py +253 -130
  214. souleyez/plugins/msf_exploit.py +239 -161
  215. souleyez/plugins/nikto.py +134 -78
  216. souleyez/plugins/nmap.py +275 -91
  217. souleyez/plugins/nuclei.py +180 -89
  218. souleyez/plugins/nxc.py +285 -0
  219. souleyez/plugins/plugin_base.py +35 -36
  220. souleyez/plugins/plugin_template.py +13 -5
  221. souleyez/plugins/rdp_sec_check.py +130 -0
  222. souleyez/plugins/responder.py +112 -71
  223. souleyez/plugins/router_http_brute.py +76 -65
  224. souleyez/plugins/router_ssh_brute.py +118 -41
  225. souleyez/plugins/router_telnet_brute.py +124 -42
  226. souleyez/plugins/routersploit.py +91 -59
  227. souleyez/plugins/routersploit_exploit.py +77 -55
  228. souleyez/plugins/searchsploit.py +91 -77
  229. souleyez/plugins/service_explorer.py +1160 -0
  230. souleyez/plugins/smbmap.py +122 -72
  231. souleyez/plugins/smbpasswd.py +215 -0
  232. souleyez/plugins/sqlmap.py +301 -113
  233. souleyez/plugins/theharvester.py +127 -75
  234. souleyez/plugins/tr069.py +79 -57
  235. souleyez/plugins/upnp.py +65 -47
  236. souleyez/plugins/upnp_abuse.py +73 -55
  237. souleyez/plugins/vnc_access.py +129 -42
  238. souleyez/plugins/vnc_brute.py +109 -38
  239. souleyez/plugins/whois.py +77 -58
  240. souleyez/plugins/wpscan.py +173 -69
  241. souleyez/reporting/__init__.py +2 -1
  242. souleyez/reporting/attack_chain.py +411 -346
  243. souleyez/reporting/charts.py +436 -501
  244. souleyez/reporting/compliance_mappings.py +334 -201
  245. souleyez/reporting/detection_report.py +126 -125
  246. souleyez/reporting/formatters.py +828 -591
  247. souleyez/reporting/generator.py +386 -302
  248. souleyez/reporting/metrics.py +72 -75
  249. souleyez/scanner.py +35 -29
  250. souleyez/security/__init__.py +37 -11
  251. souleyez/security/scope_validator.py +175 -106
  252. souleyez/security/validation.py +223 -149
  253. souleyez/security.py +22 -6
  254. souleyez/storage/credentials.py +247 -186
  255. souleyez/storage/crypto.py +296 -129
  256. souleyez/storage/database.py +73 -50
  257. souleyez/storage/db.py +58 -36
  258. souleyez/storage/deliverable_evidence.py +177 -128
  259. souleyez/storage/deliverable_exporter.py +282 -246
  260. souleyez/storage/deliverable_templates.py +134 -116
  261. souleyez/storage/deliverables.py +135 -130
  262. souleyez/storage/engagements.py +109 -56
  263. souleyez/storage/evidence.py +181 -152
  264. souleyez/storage/execution_log.py +31 -17
  265. souleyez/storage/exploit_attempts.py +93 -57
  266. souleyez/storage/exploits.py +67 -36
  267. souleyez/storage/findings.py +48 -61
  268. souleyez/storage/hosts.py +176 -144
  269. souleyez/storage/migrate_to_engagements.py +43 -19
  270. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  271. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  272. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  273. souleyez/storage/migrations/_005_screenshots.py +13 -5
  274. souleyez/storage/migrations/_006_deliverables.py +13 -5
  275. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  276. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  277. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  278. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  279. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  280. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  281. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  282. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  283. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  284. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  285. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  286. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  287. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  288. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  289. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  290. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  291. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  292. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  293. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  294. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  295. souleyez/storage/migrations/__init__.py +26 -26
  296. souleyez/storage/migrations/migration_manager.py +19 -19
  297. souleyez/storage/msf_sessions.py +100 -65
  298. souleyez/storage/osint.py +17 -24
  299. souleyez/storage/recommendation_engine.py +269 -235
  300. souleyez/storage/screenshots.py +33 -32
  301. souleyez/storage/smb_shares.py +136 -92
  302. souleyez/storage/sqlmap_data.py +183 -128
  303. souleyez/storage/team_collaboration.py +135 -141
  304. souleyez/storage/timeline_tracker.py +122 -94
  305. souleyez/storage/wazuh_vulns.py +64 -66
  306. souleyez/storage/web_paths.py +33 -37
  307. souleyez/testing/credential_tester.py +221 -205
  308. souleyez/ui/__init__.py +1 -1
  309. souleyez/ui/ai_quotes.py +12 -12
  310. souleyez/ui/attack_surface.py +2439 -1516
  311. souleyez/ui/chain_rules_view.py +914 -382
  312. souleyez/ui/correlation_view.py +312 -230
  313. souleyez/ui/dashboard.py +2382 -1130
  314. souleyez/ui/deliverables_view.py +148 -62
  315. souleyez/ui/design_system.py +13 -13
  316. souleyez/ui/errors.py +49 -49
  317. souleyez/ui/evidence_linking_view.py +284 -179
  318. souleyez/ui/evidence_vault.py +393 -285
  319. souleyez/ui/exploit_suggestions_view.py +555 -349
  320. souleyez/ui/export_view.py +100 -66
  321. souleyez/ui/gap_analysis_view.py +315 -171
  322. souleyez/ui/help_system.py +105 -97
  323. souleyez/ui/intelligence_view.py +436 -293
  324. souleyez/ui/interactive.py +23142 -10430
  325. souleyez/ui/interactive_selector.py +75 -68
  326. souleyez/ui/log_formatter.py +47 -39
  327. souleyez/ui/menu_components.py +22 -13
  328. souleyez/ui/msf_auxiliary_menu.py +184 -133
  329. souleyez/ui/pending_chains_view.py +336 -172
  330. souleyez/ui/progress_indicators.py +5 -3
  331. souleyez/ui/recommendations_view.py +195 -137
  332. souleyez/ui/rule_builder.py +343 -225
  333. souleyez/ui/setup_wizard.py +678 -284
  334. souleyez/ui/shortcuts.py +217 -165
  335. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  336. souleyez/ui/splunk_vulns_view.py +139 -86
  337. souleyez/ui/team_dashboard.py +498 -335
  338. souleyez/ui/template_selector.py +196 -105
  339. souleyez/ui/terminal.py +6 -6
  340. souleyez/ui/timeline_view.py +198 -127
  341. souleyez/ui/tool_setup.py +264 -164
  342. souleyez/ui/tutorial.py +202 -72
  343. souleyez/ui/tutorial_state.py +40 -40
  344. souleyez/ui/wazuh_vulns_view.py +235 -141
  345. souleyez/ui/wordlist_browser.py +260 -107
  346. souleyez/ui.py +464 -312
  347. souleyez/utils/tool_checker.py +427 -367
  348. souleyez/utils.py +33 -29
  349. souleyez/wordlists.py +134 -167
  350. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/METADATA +1 -1
  351. souleyez-2.43.32.dist-info/RECORD +441 -0
  352. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/WHEEL +1 -1
  353. souleyez-2.43.28.dist-info/RECORD +0 -379
  354. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/entry_points.txt +0 -0
  355. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/licenses/LICENSE +0 -0
  356. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,821 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Nmap handler.
4
+
5
+ Consolidates parsing and display logic for nmap and ARD (which uses nmap) jobs.
6
+ """
7
+ import logging
8
+ import os
9
+ import re
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import click
13
+
14
+ from souleyez.engine.job_status import STATUS_DONE, STATUS_ERROR, STATUS_NO_RESULTS
15
+ from souleyez.handlers.base import BaseToolHandler
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class NmapHandler(BaseToolHandler):
21
+ """Handler for nmap and nmap-based (ARD) jobs."""
22
+
23
+ tool_name = "nmap"
24
+ display_name = "Nmap"
25
+
26
+ # All handlers enabled
27
+ has_error_handler = True
28
+ has_warning_handler = True
29
+ has_no_results_handler = True
30
+ has_done_handler = True
31
+
32
+ # Risky ports that warrant security concerns
33
+ RISKY_PORTS = {
34
+ 21: ("FTP", "Cleartext file transfer - check for anonymous access"),
35
+ 23: ("Telnet", "Cleartext remote access - highly insecure"),
36
+ 25: ("SMTP", "Mail relay - check for open relay"),
37
+ 69: ("TFTP", "Trivial FTP - no authentication"),
38
+ 111: ("RPC", "Remote procedure call - can expose NFS/services"),
39
+ 135: ("MSRPC", "Windows RPC - often targeted"),
40
+ 139: ("NetBIOS", "Legacy Windows networking"),
41
+ 445: ("SMB", "File sharing - frequent attack target"),
42
+ 512: ("rexec", "Remote execution - cleartext"),
43
+ 513: ("rlogin", "Remote login - cleartext, no auth"),
44
+ 514: ("rsh", "Remote shell - cleartext, no auth"),
45
+ 1433: ("MSSQL", "Database exposed - should not be public"),
46
+ 1521: ("Oracle", "Database exposed - should not be public"),
47
+ 2049: ("NFS", "Network file system - check exports"),
48
+ 3306: ("MySQL", "Database exposed - should not be public"),
49
+ 3389: ("RDP", "Remote desktop - brute forceable"),
50
+ 5432: ("PostgreSQL", "Database exposed - should not be public"),
51
+ 5900: ("VNC", "Remote desktop - often weak auth"),
52
+ 5901: ("VNC", "Remote desktop - often weak auth"),
53
+ 6000: ("X11", "Remote display - unencrypted"),
54
+ 6379: ("Redis", "Database/cache - often no auth"),
55
+ 27017: ("MongoDB", "Database exposed - often no auth"),
56
+ }
57
+
58
+ def parse_job(
59
+ self,
60
+ engagement_id: int,
61
+ log_path: str,
62
+ job: Dict[str, Any],
63
+ host_manager: Optional[Any] = None,
64
+ findings_manager: Optional[Any] = None,
65
+ credentials_manager: Optional[Any] = None,
66
+ ) -> Dict[str, Any]:
67
+ """
68
+ Parse nmap job results.
69
+
70
+ Imports hosts/services into database and creates findings for CVEs.
71
+ """
72
+ try:
73
+ from souleyez.parsers.nmap_parser import parse_nmap_log
74
+ from souleyez.core.cve_matcher import CVEMatcher
75
+ from souleyez.engine.result_handler import detect_tool_error
76
+
77
+ # Import managers if not provided
78
+ if host_manager is None:
79
+ from souleyez.storage.hosts import HostManager
80
+
81
+ host_manager = HostManager()
82
+ if findings_manager is None:
83
+ from souleyez.storage.findings import FindingsManager
84
+
85
+ findings_manager = FindingsManager()
86
+
87
+ # Parse the log file
88
+ parsed = parse_nmap_log(log_path)
89
+
90
+ if "error" in parsed:
91
+ return {"error": parsed["error"]}
92
+
93
+ # Import into database
94
+ result = host_manager.import_nmap_results(engagement_id, parsed)
95
+ logger.info(
96
+ f"Nmap import: {result['hosts_added']} hosts, "
97
+ f"{result['services_added']} services in engagement {engagement_id}"
98
+ )
99
+ logger.debug(
100
+ f"Info scripts to process: {len(parsed.get('info_scripts', []))}"
101
+ )
102
+
103
+ # Check for CVEs and common issues
104
+ cve_matcher = CVEMatcher()
105
+ findings_added = 0
106
+
107
+ # First, store any script-detected vulnerabilities (from --script vuln)
108
+ findings_added += self._store_vulnerabilities(
109
+ parsed, engagement_id, host_manager, findings_manager
110
+ )
111
+
112
+ # Then check for inferred CVEs based on service versions
113
+ findings_added += self._check_service_cves(
114
+ parsed, engagement_id, host_manager, findings_manager, cve_matcher
115
+ )
116
+
117
+ # Store info script findings
118
+ findings_added += self._store_info_scripts(
119
+ parsed, engagement_id, host_manager, findings_manager
120
+ )
121
+
122
+ # Build host details list for summary
123
+ host_details = self._build_host_details(parsed)
124
+
125
+ # Determine scan type based on job args
126
+ args = job.get("args", [])
127
+ is_discovery = "-sn" in args or "--discovery" in args
128
+ is_full_scan = any(x in args for x in ["-sV", "-O", "-A", "-p1-65535"])
129
+
130
+ # Collect all services for tool chaining
131
+ all_services = self._collect_chainable_services(parsed)
132
+
133
+ # Check for nmap errors before determining status
134
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
135
+ log_content = f.read()
136
+ nmap_error = detect_tool_error(log_content, "nmap")
137
+
138
+ # Determine status based on results
139
+ hosts_up = len(
140
+ [h for h in parsed.get("hosts", []) if h.get("status") == "up"]
141
+ )
142
+ if nmap_error:
143
+ status = STATUS_ERROR
144
+ elif hosts_up > 0:
145
+ status = STATUS_DONE
146
+ else:
147
+ status = STATUS_NO_RESULTS
148
+
149
+ # Build summary for job queue display
150
+ summary_parts = []
151
+ if hosts_up > 0:
152
+ summary_parts.append(f"{hosts_up} host(s) up")
153
+ if result["services_added"] > 0:
154
+ summary_parts.append(f"{result['services_added']} service(s)")
155
+ if findings_added > 0:
156
+ summary_parts.append(f"{findings_added} finding(s)")
157
+ summary = " | ".join(summary_parts) if summary_parts else "No hosts found"
158
+
159
+ return {
160
+ "tool": "nmap",
161
+ "status": status,
162
+ "summary": summary,
163
+ "hosts_added": result["hosts_added"],
164
+ "services_added": result["services_added"],
165
+ "findings_added": findings_added,
166
+ "host_details": host_details,
167
+ "is_discovery": is_discovery,
168
+ "is_full_scan": is_full_scan,
169
+ "services": all_services,
170
+ "hosts": parsed.get("hosts", []),
171
+ "domains": parsed.get(
172
+ "domains", []
173
+ ), # AD domains from LDAP/SMB banners
174
+ }
175
+
176
+ except Exception as e:
177
+ logger.error(f"Error parsing nmap job: {e}")
178
+ return {"error": str(e)}
179
+
180
+ def _store_vulnerabilities(
181
+ self, parsed: Dict, engagement_id: int, host_manager: Any, findings_manager: Any
182
+ ) -> int:
183
+ """Store script-detected vulnerabilities (from --script vuln)."""
184
+ findings_added = 0
185
+
186
+ for vuln in parsed.get("vulnerabilities", []):
187
+ host_ip = vuln.get("host_ip")
188
+ if not host_ip:
189
+ continue
190
+
191
+ host = host_manager.get_host_by_ip(engagement_id, host_ip)
192
+ if not host:
193
+ continue
194
+
195
+ host_id = host["id"]
196
+
197
+ # Determine severity from CVSS score
198
+ cvss = vuln.get("cvss_score")
199
+ if cvss:
200
+ if cvss >= 9.0:
201
+ severity = "critical"
202
+ elif cvss >= 7.0:
203
+ severity = "high"
204
+ elif cvss >= 4.0:
205
+ severity = "medium"
206
+ else:
207
+ severity = "low"
208
+ else:
209
+ severity = "high" if vuln.get("state") == "VULNERABLE" else "medium"
210
+
211
+ # Build references string from CVE IDs
212
+ cve_ids = vuln.get("cve_ids", [])
213
+ refs = None
214
+ if cve_ids:
215
+ cve_refs = [
216
+ f"https://nvd.nist.gov/vuln/detail/{cve}" for cve in cve_ids[:3]
217
+ ]
218
+ refs = ", ".join(cve_refs)
219
+ elif vuln.get("references"):
220
+ refs = ", ".join(vuln.get("references", [])[:3])
221
+
222
+ # Build description with CVSS and CVE info
223
+ description = vuln.get(
224
+ "description", f"Detected by nmap script: {vuln.get('script')}"
225
+ )
226
+ if cvss:
227
+ description += f"\n\nCVSS Score: {cvss}"
228
+ if cve_ids:
229
+ description += f"\nCVE IDs: {', '.join(cve_ids[:5])}"
230
+
231
+ # Build title - include CVE if available
232
+ title = vuln.get("title", vuln.get("script", "Unknown Vulnerability"))
233
+ if cve_ids and cve_ids[0] not in title:
234
+ title = f"{cve_ids[0]}: {title}"
235
+
236
+ findings_manager.add_finding(
237
+ engagement_id=engagement_id,
238
+ host_id=host_id,
239
+ title=title,
240
+ finding_type="vulnerability",
241
+ severity=severity,
242
+ description=description,
243
+ port=vuln.get("port"),
244
+ tool="nmap",
245
+ refs=refs,
246
+ evidence=f"Host: {vuln.get('host_ip', 'unknown')}:{vuln.get('port', 'N/A')}\nScript: {vuln.get('script', 'nmap')}",
247
+ )
248
+ findings_added += 1
249
+
250
+ return findings_added
251
+
252
+ def _check_service_cves(
253
+ self,
254
+ parsed: Dict,
255
+ engagement_id: int,
256
+ host_manager: Any,
257
+ findings_manager: Any,
258
+ cve_matcher: Any,
259
+ ) -> int:
260
+ """Check for inferred CVEs based on service versions."""
261
+ findings_added = 0
262
+
263
+ for host_data in parsed.get("hosts", []):
264
+ if host_data.get("status") != "up":
265
+ continue
266
+
267
+ host = host_manager.get_host_by_ip(engagement_id, host_data.get("ip"))
268
+ if not host:
269
+ continue
270
+
271
+ host_id = host["id"]
272
+
273
+ for svc in host_data.get("services", []):
274
+ service_info = {
275
+ "service_name": svc.get("service") or "",
276
+ "version": svc.get("version") or "",
277
+ "port": svc.get("port"),
278
+ "protocol": svc.get("protocol") or "tcp",
279
+ }
280
+
281
+ # Also check database for stored version if not in parsed data
282
+ if not service_info["version"]:
283
+ services = host_manager.get_host_services(host_id)
284
+ for stored_svc in services:
285
+ if stored_svc["port"] == svc.get("port"):
286
+ service_info["version"] = stored_svc.get(
287
+ "service_version", ""
288
+ )
289
+ break
290
+
291
+ # Check for CVEs
292
+ cve_findings = cve_matcher.parse_nmap_service(service_info)
293
+ for finding in cve_findings:
294
+ findings_manager.add_finding(
295
+ engagement_id=engagement_id,
296
+ host_id=host_id,
297
+ title=finding["title"],
298
+ finding_type="vulnerability",
299
+ severity=finding["severity"],
300
+ description=finding["description"],
301
+ port=finding.get("port"),
302
+ tool="nmap",
303
+ refs=f"https://nvd.nist.gov/vuln/detail/{finding.get('cve_id')}",
304
+ )
305
+ findings_added += 1
306
+
307
+ # Check for common issues
308
+ issue_findings = cve_matcher.scan_for_common_issues(service_info)
309
+ for finding in issue_findings:
310
+ findings_manager.add_finding(
311
+ engagement_id=engagement_id,
312
+ host_id=host_id,
313
+ title=finding["title"],
314
+ finding_type="misconfiguration",
315
+ severity=finding["severity"],
316
+ description=finding["description"],
317
+ port=finding.get("port"),
318
+ tool="nmap",
319
+ )
320
+ findings_added += 1
321
+
322
+ return findings_added
323
+
324
+ def _store_info_scripts(
325
+ self, parsed: Dict, engagement_id: int, host_manager: Any, findings_manager: Any
326
+ ) -> int:
327
+ """Store info script findings (vnc-info, ssh-hostkey, etc.)."""
328
+ findings_added = 0
329
+
330
+ for info in parsed.get("info_scripts", []):
331
+ host_ip = info.get("host_ip")
332
+ if not host_ip:
333
+ logger.warning(f"Info script missing host_ip: {info.get('script')}")
334
+ continue
335
+
336
+ host = host_manager.get_host_by_ip(engagement_id, host_ip)
337
+ if not host:
338
+ logger.warning(
339
+ f"Host not found for info script: {host_ip} in engagement {engagement_id}"
340
+ )
341
+ continue
342
+
343
+ host_id = host["id"]
344
+
345
+ script_name = info.get("script", "unknown")
346
+ title = info.get("title", script_name)
347
+ description = info.get("description", "")
348
+
349
+ port = info.get("port")
350
+ if port:
351
+ title = f"{title} (port {port})"
352
+
353
+ findings_manager.add_finding(
354
+ engagement_id=engagement_id,
355
+ host_id=host_id,
356
+ title=title,
357
+ finding_type="info",
358
+ severity="info",
359
+ description=description,
360
+ port=port,
361
+ tool="nmap",
362
+ evidence=f"Host: {host_ip}:{port if port else 'N/A'}\nScript: {script_name}",
363
+ )
364
+ findings_added += 1
365
+
366
+ return findings_added
367
+
368
+ def _build_host_details(self, parsed: Dict) -> List[Dict]:
369
+ """Build host details list for summary."""
370
+ host_details = []
371
+
372
+ for host_data in parsed.get("hosts", []):
373
+ if host_data.get("status") == "up":
374
+ services = host_data.get("services", [])
375
+ service_count = len(services)
376
+
377
+ # Get top ports for detailed scans
378
+ top_ports = []
379
+ for svc in services[:5]:
380
+ port = svc.get("port")
381
+ service_name = svc.get("service", "unknown")
382
+ top_ports.append(f"{port}/{service_name}")
383
+
384
+ host_details.append(
385
+ {
386
+ "ip": host_data.get("ip"),
387
+ "hostname": host_data.get("hostname"),
388
+ "os": host_data.get("os"),
389
+ "service_count": service_count,
390
+ "top_ports": top_ports,
391
+ }
392
+ )
393
+
394
+ return host_details
395
+
396
+ def _collect_chainable_services(self, parsed: Dict) -> List[Dict]:
397
+ """Collect all services for tool chaining."""
398
+ chainable_states = {"open", "filtered", "open|filtered"}
399
+ all_services = []
400
+
401
+ for host_data in parsed.get("hosts", []):
402
+ if host_data.get("status") == "up":
403
+ for svc in host_data.get("services", []):
404
+ port_state = svc.get("state", "open").lower()
405
+ if port_state in chainable_states:
406
+ all_services.append(
407
+ {
408
+ "ip": host_data.get("ip"),
409
+ "port": svc.get("port"),
410
+ "protocol": svc.get("protocol", "tcp"),
411
+ "state": port_state,
412
+ "service_name": svc.get("service") or "",
413
+ "version": svc.get("version") or "",
414
+ }
415
+ )
416
+
417
+ return all_services
418
+
419
+ def _identify_security_concerns(self, hosts: List[Dict]) -> List[Dict]:
420
+ """Identify risky services in discovered hosts."""
421
+ security_concerns = []
422
+
423
+ for host in hosts:
424
+ ip = host.get("ip", "unknown")
425
+ hostname = host.get("hostname", "")
426
+
427
+ for svc in host.get("services", []):
428
+ port = svc.get("port")
429
+ state = svc.get("state") or ""
430
+ service_name = svc.get("service") or ""
431
+
432
+ if state != "open":
433
+ continue
434
+
435
+ try:
436
+ port_num = int(port)
437
+ if port_num in self.RISKY_PORTS:
438
+ name, desc = self.RISKY_PORTS[port_num]
439
+ host_display = f"{ip}:{port}"
440
+ if hostname:
441
+ host_display += f" ({hostname})"
442
+ security_concerns.append(
443
+ {
444
+ "host": host_display,
445
+ "port": port_num,
446
+ "service": name,
447
+ "description": desc,
448
+ }
449
+ )
450
+ elif "vnc" in service_name.lower():
451
+ host_display = f"{ip}:{port}"
452
+ security_concerns.append(
453
+ {
454
+ "host": host_display,
455
+ "port": port_num,
456
+ "service": "VNC",
457
+ "description": "Remote desktop - often weak auth",
458
+ }
459
+ )
460
+ except (ValueError, TypeError):
461
+ pass
462
+
463
+ return security_concerns
464
+
465
+ def display_done(
466
+ self,
467
+ job: Dict[str, Any],
468
+ log_path: str,
469
+ show_all: bool = False,
470
+ show_passwords: bool = False,
471
+ ) -> None:
472
+ """Display successful nmap scan results."""
473
+ try:
474
+ from souleyez.parsers.nmap_parser import parse_nmap_output
475
+
476
+ if not log_path or not os.path.exists(log_path):
477
+ return
478
+
479
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
480
+ log_content = f.read()
481
+ parsed = parse_nmap_output(log_content, job.get("target", ""))
482
+
483
+ vulnerabilities = parsed.get("vulnerabilities", [])
484
+ hosts = parsed.get("hosts", [])
485
+
486
+ # If vulnerabilities found, show vuln-focused view
487
+ if vulnerabilities:
488
+ self._display_vulnerabilities(vulnerabilities, show_all)
489
+ elif hosts:
490
+ self._display_services(hosts, show_all)
491
+ else:
492
+ self.display_no_results(job, log_path)
493
+
494
+ except Exception as e:
495
+ logger.debug(f"Error in display_done: {e}")
496
+
497
+ def _display_vulnerabilities(
498
+ self, vulnerabilities: List[Dict], show_all: bool
499
+ ) -> None:
500
+ """Display vulnerability-focused view."""
501
+ # Group by severity
502
+ by_severity = {"critical": [], "high": [], "medium": [], "low": []}
503
+ for vuln in vulnerabilities:
504
+ cvss = vuln.get("cvss_score")
505
+ if cvss and cvss >= 9.0:
506
+ sev = "critical"
507
+ elif cvss and cvss >= 7.0:
508
+ sev = "high"
509
+ elif cvss and cvss >= 4.0:
510
+ sev = "medium"
511
+ elif vuln.get("state") == "VULNERABLE":
512
+ sev = "high"
513
+ else:
514
+ sev = "medium"
515
+ by_severity[sev].append(vuln)
516
+
517
+ # Count unique hosts scanned
518
+ unique_hosts = set(
519
+ v.get("host_ip") for v in vulnerabilities if v.get("host_ip")
520
+ )
521
+
522
+ click.echo(click.style("=" * 70, fg="red"))
523
+ click.echo(click.style("VULNERABILITY SCAN RESULTS", bold=True, fg="red"))
524
+ click.echo(click.style("=" * 70, fg="red"))
525
+ click.echo()
526
+
527
+ # Summary line
528
+ crit_count = len(by_severity["critical"])
529
+ high_count = len(by_severity["high"])
530
+ med_count = len(by_severity["medium"])
531
+ low_count = len(by_severity["low"])
532
+
533
+ summary_parts = []
534
+ if crit_count:
535
+ summary_parts.append(
536
+ click.style(f"CRITICAL: {crit_count}", fg="red", bold=True)
537
+ )
538
+ if high_count:
539
+ summary_parts.append(click.style(f"HIGH: {high_count}", fg="red"))
540
+ if med_count:
541
+ summary_parts.append(click.style(f"MEDIUM: {med_count}", fg="yellow"))
542
+ if low_count:
543
+ summary_parts.append(click.style(f"LOW: {low_count}", fg="blue"))
544
+
545
+ click.echo(
546
+ f" Hosts Scanned: {len(unique_hosts)} Total Findings: {len(vulnerabilities)}"
547
+ )
548
+ click.echo(f" {' | '.join(summary_parts)}")
549
+ click.echo()
550
+
551
+ # Display each severity section with interactive pagination
552
+ items_per_page = 15
553
+ done_viewing = False
554
+
555
+ for severity in ["critical", "high", "medium", "low"]:
556
+ if done_viewing:
557
+ break
558
+
559
+ items = by_severity[severity]
560
+ if not items:
561
+ continue
562
+
563
+ # Section header with color
564
+ if severity == "critical":
565
+ header = click.style(
566
+ f"-- CRITICAL ({len(items)}) ", fg="red", bold=True
567
+ )
568
+ elif severity == "high":
569
+ header = click.style(f"-- HIGH ({len(items)}) ", fg="red")
570
+ elif severity == "medium":
571
+ header = click.style(f"-- MEDIUM ({len(items)}) ", fg="yellow")
572
+ else:
573
+ header = click.style(f"-- LOW ({len(items)}) ", fg="blue")
574
+
575
+ click.echo(header + click.style("-" * 50, dim=True))
576
+
577
+ # Paginate through items
578
+ current_idx = 0
579
+ while current_idx < len(items):
580
+ end_idx = min(current_idx + items_per_page, len(items))
581
+ for vuln in items[current_idx:end_idx]:
582
+ title = vuln.get("title", vuln.get("script", "Unknown"))
583
+ host_ip = vuln.get("host_ip", "")
584
+ port = vuln.get("port", "")
585
+
586
+ location = f"{host_ip}:{port}" if port else host_ip
587
+ click.echo(f" {location:<18} {title[:55]}")
588
+
589
+ current_idx = end_idx
590
+ remaining = len(items) - current_idx
591
+
592
+ if remaining > 0 and not show_all:
593
+ try:
594
+ prompt_text = click.style(
595
+ f" ({current_idx}/{len(items)}) [Enter]=more [s]=skip [d]=done: ",
596
+ dim=True,
597
+ )
598
+ choice = (
599
+ click.prompt(prompt_text, default="", show_default=False)
600
+ .lower()
601
+ .strip()
602
+ )
603
+
604
+ if choice == "d":
605
+ done_viewing = True
606
+ break
607
+ elif choice == "s":
608
+ click.echo(
609
+ click.style(
610
+ f" ... {remaining} more not shown", dim=True
611
+ )
612
+ )
613
+ break
614
+ except (KeyboardInterrupt, EOFError):
615
+ done_viewing = True
616
+ break
617
+
618
+ click.echo()
619
+
620
+ click.echo(click.style("=" * 70, fg="red"))
621
+ click.echo()
622
+
623
+ def _display_services(self, hosts: List[Dict], show_all: bool) -> None:
624
+ """Display discovered services view."""
625
+ has_services = any(host.get("services", []) for host in hosts)
626
+
627
+ # Security Concerns Analysis
628
+ security_concerns = self._identify_security_concerns(hosts)
629
+
630
+ # Display security concerns section if any found
631
+ if security_concerns:
632
+ click.echo(click.style("=" * 70, fg="yellow"))
633
+ click.echo(click.style("SECURITY CONCERNS", bold=True, fg="yellow"))
634
+ click.echo(click.style("=" * 70, fg="yellow"))
635
+ click.echo()
636
+
637
+ # Group by service type
638
+ by_service = {}
639
+ for concern in security_concerns:
640
+ svc = concern["service"]
641
+ if svc not in by_service:
642
+ by_service[svc] = []
643
+ by_service[svc].append(concern)
644
+
645
+ for service, concerns in sorted(by_service.items()):
646
+ click.echo(
647
+ click.style(f" {service}", bold=True, fg="yellow")
648
+ + click.style(f" - {concerns[0]['description']}", fg="bright_black")
649
+ )
650
+ for c in concerns:
651
+ click.echo(f" - {c['host']}")
652
+ click.echo()
653
+
654
+ click.echo(click.style("=" * 70, fg="yellow"))
655
+ click.echo()
656
+
657
+ click.echo(click.style("=" * 70, fg="cyan"))
658
+ if has_services:
659
+ click.echo(click.style("DISCOVERED SERVICES", bold=True, fg="cyan"))
660
+ else:
661
+ click.echo(click.style("DISCOVERED HOSTS", bold=True, fg="cyan"))
662
+ click.echo(click.style("=" * 70, fg="cyan"))
663
+ click.echo()
664
+
665
+ for host in hosts:
666
+ ip = host.get("ip", "unknown")
667
+ hostname = host.get("hostname")
668
+ status = host.get("status", "unknown")
669
+ services = host.get("services", [])
670
+
671
+ if services:
672
+ # Show host header
673
+ click.echo(click.style(f"Host: {ip}", bold=True))
674
+ if hostname:
675
+ click.echo(f" Hostname: {hostname}")
676
+ # Show each service
677
+ for svc in services:
678
+ port = svc.get("port", "?")
679
+ protocol = svc.get("protocol", "tcp")
680
+ state = svc.get("state", "unknown")
681
+ service = svc.get("service", "unknown")
682
+ version = svc.get("version", "")
683
+
684
+ state_color = (
685
+ "green"
686
+ if state == "open"
687
+ else ("yellow" if state == "filtered" else None)
688
+ )
689
+ state_display = (
690
+ click.style(state, fg=state_color) if state_color else state
691
+ )
692
+ version_str = f" ({version})" if version else ""
693
+ click.echo(
694
+ f" {port}/{protocol} {state_display} {service}{version_str}"
695
+ )
696
+ click.echo()
697
+ elif status == "up":
698
+ host_display = f" {ip}"
699
+ if hostname:
700
+ host_display += f" ({hostname})"
701
+ click.echo(host_display)
702
+
703
+ click.echo(click.style("=" * 70, fg="cyan"))
704
+ click.echo()
705
+
706
+ def display_warning(
707
+ self,
708
+ job: Dict[str, Any],
709
+ log_path: str,
710
+ log_content: Optional[str] = None,
711
+ ) -> None:
712
+ """Display warning status for nmap scan."""
713
+ click.echo(click.style("=" * 70, fg="yellow"))
714
+ click.echo(click.style("[WARNING] NMAP SCAN", bold=True, fg="yellow"))
715
+ click.echo(click.style("=" * 70, fg="yellow"))
716
+ click.echo()
717
+ click.echo(" Scan completed with warnings. Check raw logs for details.")
718
+ click.echo(" Press [r] to view raw logs.")
719
+ click.echo()
720
+ click.echo(click.style("=" * 70, fg="yellow"))
721
+ click.echo()
722
+
723
+ def display_error(
724
+ self,
725
+ job: Dict[str, Any],
726
+ log_path: str,
727
+ log_content: Optional[str] = None,
728
+ ) -> None:
729
+ """Display error status for nmap scan."""
730
+ # Read log if not provided
731
+ if log_content is None and log_path and os.path.exists(log_path):
732
+ try:
733
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
734
+ log_content = f.read()
735
+ except Exception:
736
+ log_content = ""
737
+
738
+ click.echo(click.style("=" * 70, fg="red"))
739
+ click.echo(click.style("[ERROR] NMAP SCAN FAILED", bold=True, fg="red"))
740
+ click.echo(click.style("=" * 70, fg="red"))
741
+ click.echo()
742
+
743
+ # Check for common nmap errors
744
+ error_msg = None
745
+ if log_content:
746
+ if "Failed to resolve" in log_content or "Failed to open" in log_content:
747
+ error_msg = "Failed to resolve target hostname"
748
+ elif "No targets were specified" in log_content:
749
+ error_msg = "No valid targets specified"
750
+ elif (
751
+ "requires root privileges" in log_content
752
+ or "Operation not permitted" in log_content
753
+ ):
754
+ error_msg = "Scan type requires root privileges (try sudo)"
755
+ elif "Host seems down" in log_content:
756
+ error_msg = "Host appears to be down or blocking probes"
757
+ elif "timed out" in log_content.lower() or "timeout" in log_content.lower():
758
+ error_msg = "Scan timed out - target may be slow or filtering"
759
+ elif "Connection refused" in log_content:
760
+ error_msg = "Connection refused - no services on target ports"
761
+
762
+ if error_msg:
763
+ click.echo(f" {error_msg}")
764
+ else:
765
+ click.echo(" Scan failed - see raw logs for details (press 'r')")
766
+
767
+ click.echo()
768
+ click.echo(click.style("=" * 70, fg="red"))
769
+ click.echo()
770
+
771
+ def display_no_results(
772
+ self,
773
+ job: Dict[str, Any],
774
+ log_path: str,
775
+ ) -> None:
776
+ """Display no_results status for nmap scan."""
777
+ # Try to read log for additional context
778
+ log_text = ""
779
+ if log_path and os.path.exists(log_path):
780
+ try:
781
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
782
+ log_text = f.read()
783
+ except Exception:
784
+ pass
785
+
786
+ click.echo(click.style("=" * 70, fg="cyan"))
787
+ click.echo(click.style("NMAP SCAN RESULTS", bold=True, fg="cyan"))
788
+ click.echo(click.style("=" * 70, fg="cyan"))
789
+ click.echo()
790
+ click.echo(" No open ports or services discovered.")
791
+ click.echo()
792
+
793
+ # Check for additional context
794
+ if "Host seems down" in log_text:
795
+ click.echo(
796
+ click.style(
797
+ " Note: Host appears to be down or blocking probes", fg="yellow"
798
+ )
799
+ )
800
+ elif "filtered" in log_text.lower():
801
+ click.echo(
802
+ click.style(" Note: Ports may be filtered by firewall", fg="yellow")
803
+ )
804
+
805
+ click.echo()
806
+ click.echo(click.style(" This could mean:", fg="bright_black"))
807
+ click.echo(
808
+ click.style(" - All ports are closed or filtered", fg="bright_black")
809
+ )
810
+ click.echo(click.style(" - Host is behind a firewall", fg="bright_black"))
811
+ click.echo(
812
+ click.style(
813
+ " - Try different scan types (-sS, -sT, -sU)", fg="bright_black"
814
+ )
815
+ )
816
+ click.echo(
817
+ click.style(" - Try scanning more ports (-p-)", fg="bright_black")
818
+ )
819
+ click.echo()
820
+ click.echo(click.style("=" * 70, fg="cyan"))
821
+ click.echo()