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.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23434 -10286
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.26.dist-info/RECORD +0 -379
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.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()
|