souleyez 2.43.29__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of souleyez might be problematic. Click here for more details.
- 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 +9564 -2881
- 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 +564 -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 +409 -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 +417 -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 +913 -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 +219 -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 +237 -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 +23034 -10679
- 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.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
- souleyez-3.0.0.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
souleyez/ui.py
CHANGED
|
@@ -17,32 +17,44 @@ from .utils import nmap_installed, detect_local_subnet
|
|
|
17
17
|
try:
|
|
18
18
|
from .engine.background import enqueue_job, start_worker, stop_worker
|
|
19
19
|
except Exception:
|
|
20
|
+
|
|
20
21
|
def enqueue_job(tool, target, args=None, label=None):
|
|
21
|
-
raise RuntimeError(
|
|
22
|
+
raise RuntimeError(
|
|
23
|
+
"enqueue_job not available in this environment (background engine missing)."
|
|
24
|
+
)
|
|
25
|
+
|
|
22
26
|
def start_worker(detach=False):
|
|
23
|
-
raise RuntimeError(
|
|
27
|
+
raise RuntimeError(
|
|
28
|
+
"start_worker not available in this environment (background engine missing)."
|
|
29
|
+
)
|
|
30
|
+
|
|
24
31
|
def stop_worker():
|
|
25
|
-
raise RuntimeError(
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
"stop_worker not available in this environment (background engine missing)."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
26
37
|
from .engine.background import list_jobs, get_job
|
|
27
38
|
from .engine.loader import discover_plugins
|
|
28
39
|
from .engine.manager import run_scan_sync
|
|
29
40
|
from .scanner import run_nmap
|
|
30
41
|
from .storage.db import get_scans, get_scan
|
|
42
|
+
|
|
31
43
|
# (history.py kept for export helpers if needed in future, not used for reads)
|
|
32
44
|
|
|
33
45
|
VERSION = "0.5.0"
|
|
34
46
|
|
|
35
47
|
# ANSI colors
|
|
36
|
-
CSI =
|
|
37
|
-
RESET = CSI +
|
|
38
|
-
BOLD = CSI +
|
|
39
|
-
GREEN = CSI +
|
|
40
|
-
RED = CSI +
|
|
41
|
-
CYAN = CSI +
|
|
42
|
-
MAG = CSI +
|
|
48
|
+
CSI = "\033["
|
|
49
|
+
RESET = CSI + "0m"
|
|
50
|
+
BOLD = CSI + "1m"
|
|
51
|
+
GREEN = CSI + "32m"
|
|
52
|
+
RED = CSI + "31m"
|
|
53
|
+
CYAN = CSI + "36m"
|
|
54
|
+
MAG = CSI + "35m"
|
|
43
55
|
|
|
44
56
|
# === Branding ===
|
|
45
|
-
BANNER = r
|
|
57
|
+
BANNER = r"""
|
|
46
58
|
____ ____ _ _ _ ____ _ _ ____ _ _ ____ ____
|
|
47
59
|
/ ___| / ___| | | | | / \ / ___|| | | |/ ___|| | | |/ ___|| _ \
|
|
48
60
|
\___ \| | | |_| | / _ \ \___ \| | | | | _ | | | | | _ | |_) |
|
|
@@ -50,65 +62,81 @@ BANNER = r'''
|
|
|
50
62
|
|____/ \____| |_| |_/_/ \_\____/ \___/ \____| \___/ \____||_| \_\
|
|
51
63
|
|
|
52
64
|
$0u! H@cK3R$
|
|
53
|
-
|
|
54
|
-
FOOTER =
|
|
65
|
+
"""
|
|
66
|
+
FOOTER = "y0d8 & CyberSoul SecurITy"
|
|
67
|
+
|
|
55
68
|
|
|
56
69
|
def print_banner():
|
|
57
|
-
subprocess.run([
|
|
70
|
+
subprocess.run(["clear" if os.name == "posix" else "cls"], shell=False)
|
|
58
71
|
print(MAG + BANNER + RESET)
|
|
59
72
|
print(f"{BOLD}souleyez v{VERSION}{RESET}")
|
|
60
73
|
print(FOOTER)
|
|
61
74
|
print()
|
|
62
|
-
print(
|
|
75
|
+
print(
|
|
76
|
+
RED
|
|
77
|
+
+ "LEGAL:"
|
|
78
|
+
+ RESET
|
|
79
|
+
+ " Use only on systems you own or have explicit permission to test."
|
|
80
|
+
)
|
|
63
81
|
print()
|
|
64
82
|
|
|
83
|
+
|
|
65
84
|
def print_header():
|
|
66
|
-
print(
|
|
67
|
-
print(
|
|
68
|
-
print(
|
|
85
|
+
print("=" * 60)
|
|
86
|
+
print(" souleyez — quick nmap launcher")
|
|
87
|
+
print("=" * 60)
|
|
69
88
|
if not nmap_installed():
|
|
70
|
-
print(
|
|
89
|
+
print("WARNING: nmap not found on PATH. Install nmap to run scans.")
|
|
71
90
|
print()
|
|
72
91
|
|
|
92
|
+
|
|
73
93
|
def prompt(text: str) -> str:
|
|
74
94
|
try:
|
|
75
95
|
return input(text).strip()
|
|
76
96
|
except KeyboardInterrupt:
|
|
77
|
-
print(
|
|
78
|
-
return
|
|
97
|
+
print("\nReturning to menu.")
|
|
98
|
+
return ""
|
|
79
99
|
except EOFError:
|
|
80
|
-
return
|
|
100
|
+
return ""
|
|
101
|
+
|
|
81
102
|
|
|
82
103
|
# ---- Nmap run & helpers ----
|
|
83
104
|
|
|
105
|
+
|
|
84
106
|
def _render_table(per_host: List[Dict[str, Any]]):
|
|
85
107
|
"""Pretty-print discovery results (optional table)."""
|
|
86
108
|
rows = []
|
|
87
109
|
for h in per_host or []:
|
|
88
|
-
addr = h.get(
|
|
89
|
-
state =
|
|
90
|
-
openp = str(h.get(
|
|
110
|
+
addr = h.get("addr") or ""
|
|
111
|
+
state = "Up" if h.get("up") else "Down"
|
|
112
|
+
openp = str(h.get("open") or 0)
|
|
91
113
|
rows.append((addr, state, openp))
|
|
92
114
|
if not rows:
|
|
93
|
-
print(
|
|
115
|
+
print("No hosts to display.")
|
|
94
116
|
return
|
|
95
117
|
col1 = max([len(r[0]) for r in rows] + [4])
|
|
96
118
|
col2 = max([len(r[1]) for r in rows] + [5])
|
|
97
119
|
col3 = max([len(r[2]) for r in rows] + [10])
|
|
98
|
-
sep =
|
|
99
|
-
header =
|
|
100
|
-
|
|
120
|
+
sep = "+" + "-" * (col1 + 2) + "+" + "-" * (col2 + 2) + "+" + "-" * (col3 + 2) + "+"
|
|
121
|
+
header = (
|
|
122
|
+
f'| {"Host".ljust(col1)} | {"State".ljust(col2)} | {"Open Ports".ljust(col3)} |'
|
|
123
|
+
)
|
|
124
|
+
print(sep)
|
|
125
|
+
print(header)
|
|
126
|
+
print(sep)
|
|
101
127
|
for addr, state, openp in rows:
|
|
102
|
-
color = GREEN if state ==
|
|
103
|
-
line = f
|
|
128
|
+
color = GREEN if state == "Up" else RED
|
|
129
|
+
line = f"| {addr.ljust(col1)} | {state.ljust(col2)} | {openp.rjust(col3)} |"
|
|
104
130
|
print(color + line + RESET)
|
|
105
131
|
print(sep)
|
|
106
132
|
|
|
133
|
+
|
|
107
134
|
def _ask_show_table(summary: Dict[str, Any]):
|
|
108
|
-
if summary and isinstance(summary, dict) and
|
|
109
|
-
ans = prompt(
|
|
110
|
-
if ans in (
|
|
111
|
-
_render_table(summary.get(
|
|
135
|
+
if summary and isinstance(summary, dict) and "per_host" in summary:
|
|
136
|
+
ans = prompt("Show discovery table now? (Y/n) > ").lower()
|
|
137
|
+
if ans in ("", "y", "yes"):
|
|
138
|
+
_render_table(summary.get("per_host"))
|
|
139
|
+
|
|
112
140
|
|
|
113
141
|
def _run_and_record_legacy(target: str, args: List[str], label: str, tool="nmap"):
|
|
114
142
|
"""
|
|
@@ -116,73 +144,83 @@ def _run_and_record_legacy(target: str, args: List[str], label: str, tool="nmap"
|
|
|
116
144
|
Results are written to legacy history.json by scanner.add_history_entry (if used in scanner),
|
|
117
145
|
but the UI reads history from SQLite (manager/db handles DB writes for plugin era).
|
|
118
146
|
"""
|
|
119
|
-
xml_choice = prompt(
|
|
120
|
-
print(
|
|
147
|
+
xml_choice = prompt("Save XML output? (y/N) > ").lower() in ("y", "yes")
|
|
148
|
+
print("\nStarting scan — live output below (ctrl+C to cancel)\n")
|
|
121
149
|
try:
|
|
122
|
-
logpath, rc, xmlpath, summary = run_nmap(
|
|
150
|
+
logpath, rc, xmlpath, summary = run_nmap(
|
|
151
|
+
target, args, label, save_xml=xml_choice
|
|
152
|
+
)
|
|
123
153
|
except EnvironmentError as e:
|
|
124
|
-
print(f
|
|
154
|
+
print(f"Error: {e}")
|
|
125
155
|
return
|
|
126
|
-
print(f
|
|
156
|
+
print(f"\nScan finished: log saved to {logpath} (rc={rc})")
|
|
127
157
|
if xml_choice and xmlpath:
|
|
128
|
-
print(f
|
|
158
|
+
print(f"XML saved to: {xmlpath}")
|
|
129
159
|
if summary:
|
|
130
|
-
print(
|
|
160
|
+
print("XML Summary:")
|
|
131
161
|
print(f' Hosts total: {summary.get("hosts_total")}')
|
|
132
162
|
print(f' Hosts up: {summary.get("hosts_up")}')
|
|
133
163
|
print(f' Open ports: {summary.get("open_ports")}')
|
|
134
164
|
_ask_show_table(summary)
|
|
135
165
|
else:
|
|
136
|
-
print(
|
|
166
|
+
print("No XML summary available (parsing failed).")
|
|
137
167
|
print()
|
|
138
168
|
|
|
169
|
+
|
|
139
170
|
PRESETS = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
171
|
+
"1": ("Discovery Scan (ping only)", ["-sn"]),
|
|
172
|
+
"2": ("Fast Scan (-F)", ["-v", "-PS", "-F"]),
|
|
173
|
+
"3": ("Service & OS (full)", ["-sV", "-O", "-p1-65535"]),
|
|
143
174
|
}
|
|
144
175
|
|
|
176
|
+
|
|
145
177
|
def handle_preset_choice(choice: str):
|
|
146
178
|
preset = PRESETS.get(choice)
|
|
147
179
|
if not preset:
|
|
148
|
-
print(
|
|
180
|
+
print("Invalid preset.")
|
|
149
181
|
return
|
|
150
182
|
desc, args = preset
|
|
151
183
|
print(f'\nPreset: {desc}\nArgs: {" ".join(args)}')
|
|
152
|
-
target = prompt(
|
|
184
|
+
target = prompt("Target (IP, host, CIDR) > ")
|
|
153
185
|
if not target:
|
|
154
|
-
print(
|
|
186
|
+
print("No target provided. Returning to menu.")
|
|
155
187
|
return
|
|
156
|
-
label = prompt(
|
|
188
|
+
label = prompt("Label for this scan (optional) > ")
|
|
157
189
|
_run_and_record_legacy(target, args, label, tool="nmap")
|
|
158
190
|
|
|
191
|
+
|
|
159
192
|
def handle_custom():
|
|
160
|
-
raw = prompt(
|
|
193
|
+
raw = prompt("Enter custom nmap args (e.g. -sV -p22-80) > ")
|
|
161
194
|
if not raw:
|
|
162
|
-
print(
|
|
195
|
+
print("No args, returning.")
|
|
163
196
|
return
|
|
164
197
|
args = raw.split()
|
|
165
|
-
target = prompt(
|
|
198
|
+
target = prompt("Target (IP, host, CIDR) > ")
|
|
166
199
|
if not target:
|
|
167
|
-
print(
|
|
200
|
+
print("No target provided. Returning to menu.")
|
|
168
201
|
return
|
|
169
|
-
label = prompt(
|
|
202
|
+
label = prompt("Label for this scan (optional) > ")
|
|
170
203
|
_run_and_record_legacy(target, args, label, tool="nmap")
|
|
171
204
|
|
|
205
|
+
|
|
172
206
|
def handle_scan_my_lan():
|
|
173
207
|
subnet = detect_local_subnet()
|
|
174
208
|
if not subnet:
|
|
175
|
-
print(
|
|
176
|
-
|
|
209
|
+
print(
|
|
210
|
+
"Could not auto-detect your local subnet. Enter it manually (e.g. 192.168.1.0/24)."
|
|
211
|
+
)
|
|
212
|
+
subnet = prompt("Subnet > ")
|
|
177
213
|
if not subnet:
|
|
178
|
-
print(
|
|
214
|
+
print("No subnet provided. Returning to menu.")
|
|
179
215
|
return
|
|
180
|
-
print(f
|
|
181
|
-
label = prompt(
|
|
182
|
-
_run_and_record_legacy(subnet, [
|
|
216
|
+
print(f"Using subnet: {subnet}")
|
|
217
|
+
label = prompt("Label for this scan (optional) > ")
|
|
218
|
+
_run_and_record_legacy(subnet, ["-sn"], label or "lan", tool="nmap")
|
|
219
|
+
|
|
183
220
|
|
|
184
221
|
# ---- DB-backed History (H2) ----
|
|
185
222
|
|
|
223
|
+
|
|
186
224
|
def _group_by_tool(scans: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
187
225
|
groups = {}
|
|
188
226
|
for s in scans:
|
|
@@ -193,66 +231,76 @@ def _group_by_tool(scans: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]
|
|
|
193
231
|
groups[k].sort(key=lambda x: x.get("id", 0), reverse=True)
|
|
194
232
|
return dict(sorted(groups.items(), key=lambda kv: kv[0])) # alphabetical tool order
|
|
195
233
|
|
|
234
|
+
|
|
196
235
|
def _print_history_list(scans: List[Dict[str, Any]]):
|
|
197
236
|
"""Render a flat list of scans with 1-based indices."""
|
|
198
237
|
if not scans:
|
|
199
|
-
print(
|
|
238
|
+
print("No history yet.")
|
|
200
239
|
return
|
|
201
240
|
for i, e in enumerate(scans, start=1):
|
|
202
|
-
args_str =
|
|
203
|
-
xml_mark =
|
|
204
|
-
print(
|
|
205
|
-
|
|
241
|
+
args_str = " ".join(e.get("args") or [])
|
|
242
|
+
xml_mark = " [xml]" if e.get("xml") else ""
|
|
243
|
+
print(
|
|
244
|
+
f"{i}) {e.get('ts')} | {e.get('tool')} | {e.get('target')} | {args_str} | label={e.get('label')} | log={e.get('log')}{xml_mark}"
|
|
245
|
+
)
|
|
246
|
+
print(
|
|
247
|
+
f"\nShowing {len(scans)} results | Storage: DB mode | souleyez v{VERSION}\n"
|
|
248
|
+
)
|
|
249
|
+
|
|
206
250
|
|
|
207
251
|
def history_view_all() -> List[Dict[str, Any]]:
|
|
208
252
|
scans = get_scans(limit=100000, tool=None) # H-on: show all
|
|
209
253
|
_print_history_list(scans)
|
|
210
254
|
return scans
|
|
211
255
|
|
|
256
|
+
|
|
212
257
|
def history_view_by_tool():
|
|
213
258
|
scans_all = get_scans(limit=100000)
|
|
214
259
|
groups = _group_by_tool(scans_all)
|
|
215
260
|
if not groups:
|
|
216
|
-
print(
|
|
261
|
+
print("No history yet.")
|
|
217
262
|
return
|
|
218
263
|
print("\nTools in history:")
|
|
219
264
|
tools = list(groups.keys())
|
|
220
265
|
for i, t in enumerate(tools, start=1):
|
|
221
266
|
print(f" {i}) {t} ({len(groups[t])})")
|
|
222
267
|
print(" 0) Back")
|
|
223
|
-
sel = prompt(
|
|
268
|
+
sel = prompt("Choose tool > ")
|
|
224
269
|
if not sel.isdigit():
|
|
225
|
-
print(
|
|
270
|
+
print("Invalid choice.")
|
|
226
271
|
return
|
|
227
272
|
idx = int(sel)
|
|
228
273
|
if idx == 0 or not (1 <= idx <= len(tools)):
|
|
229
274
|
return
|
|
230
|
-
chosen = tools[idx-1]
|
|
275
|
+
chosen = tools[idx - 1]
|
|
231
276
|
print(f"\n=== {chosen} history ===")
|
|
232
277
|
_print_history_list(groups[chosen])
|
|
233
278
|
|
|
279
|
+
|
|
234
280
|
def history_rerun(scans: List[Dict[str, Any]]):
|
|
235
281
|
if not scans:
|
|
236
|
-
print(
|
|
282
|
+
print("No history to re-run.")
|
|
237
283
|
return
|
|
238
|
-
sel = prompt(
|
|
284
|
+
sel = prompt("Entry number to re-run (1-based) > ")
|
|
239
285
|
if not sel.isdigit():
|
|
240
|
-
print(
|
|
286
|
+
print("Invalid index.")
|
|
241
287
|
return
|
|
242
288
|
idx = int(sel)
|
|
243
289
|
if idx < 1 or idx > len(scans):
|
|
244
|
-
print(
|
|
245
|
-
return
|
|
246
|
-
e = scans[idx-1]
|
|
247
|
-
target = e.get(
|
|
248
|
-
args = e.get(
|
|
249
|
-
label = e.get(
|
|
250
|
-
tool = (e.get(
|
|
251
|
-
print(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if action ==
|
|
290
|
+
print("Out of range.")
|
|
291
|
+
return
|
|
292
|
+
e = scans[idx - 1]
|
|
293
|
+
target = e.get("target") or ""
|
|
294
|
+
args = e.get("args") or []
|
|
295
|
+
label = e.get("label")
|
|
296
|
+
tool = (e.get("tool") or "nmap").lower()
|
|
297
|
+
print(
|
|
298
|
+
f"\nSelected: target={target}, args={' '.join(args)}, label={label}, tool={tool}"
|
|
299
|
+
)
|
|
300
|
+
action = prompt("Run now (r), Edit first (e), Back (b) > ").lower()
|
|
301
|
+
if action == "q":
|
|
302
|
+
return
|
|
303
|
+
if action == "e":
|
|
256
304
|
new_args_raw = prompt(f"Args [{ ' '.join(args) or '-sn' } ] > ").strip()
|
|
257
305
|
if new_args_raw:
|
|
258
306
|
args = new_args_raw.split()
|
|
@@ -260,93 +308,122 @@ def history_rerun(scans: List[Dict[str, Any]]):
|
|
|
260
308
|
if new_target:
|
|
261
309
|
target = new_target
|
|
262
310
|
label = prompt(f"Label [{ label or '' } ] > ").strip() or label
|
|
263
|
-
elif action !=
|
|
264
|
-
print(
|
|
311
|
+
elif action != "r":
|
|
312
|
+
print("Unknown choice.")
|
|
265
313
|
return
|
|
266
314
|
# For now, we re-use legacy runner; plugin engine will hook here next
|
|
267
315
|
_run_and_record_legacy(target, args, label, tool=tool)
|
|
268
316
|
|
|
317
|
+
|
|
269
318
|
def history_export(scans: List[Dict[str, Any]]):
|
|
270
319
|
if not scans:
|
|
271
|
-
print(
|
|
320
|
+
print("No history to export.")
|
|
272
321
|
return
|
|
273
|
-
fmt = prompt(
|
|
274
|
-
if fmt not in (
|
|
275
|
-
print(
|
|
322
|
+
fmt = prompt("Format (json/csv) > ").lower()
|
|
323
|
+
if fmt not in ("json", "csv"):
|
|
324
|
+
print("Unknown format.")
|
|
276
325
|
return
|
|
277
|
-
sel = prompt(
|
|
326
|
+
sel = prompt("Entry number to export (1-based, ENTER for latest) > ")
|
|
278
327
|
if not sel:
|
|
279
328
|
entry = scans[0]
|
|
280
329
|
elif sel.isdigit():
|
|
281
330
|
idx = int(sel)
|
|
282
331
|
if idx < 1 or idx > len(scans):
|
|
283
|
-
print(
|
|
332
|
+
print("Out of range.")
|
|
284
333
|
return
|
|
285
|
-
entry = scans[idx-1]
|
|
334
|
+
entry = scans[idx - 1]
|
|
286
335
|
else:
|
|
287
|
-
print(
|
|
336
|
+
print("Invalid index.")
|
|
288
337
|
return
|
|
289
338
|
# Export using a simple inline writer (avoid legacy)
|
|
290
339
|
from pathlib import Path
|
|
291
340
|
import json, csv
|
|
341
|
+
|
|
292
342
|
export_dir = Path.home() / ".souleyez" / "exports"
|
|
293
343
|
export_dir.mkdir(parents=True, exist_ok=True)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
344
|
+
|
|
345
|
+
def safe(s):
|
|
346
|
+
return "".join(c if (c.isalnum() or c in "._-") else "_" for c in str(s or ""))
|
|
347
|
+
|
|
348
|
+
base = (
|
|
349
|
+
f"{safe(entry.get('ts'))}_{safe(entry.get('tool'))}_{safe(entry.get('target'))}"
|
|
350
|
+
)
|
|
351
|
+
if entry.get("label"):
|
|
297
352
|
base += f"_{safe(entry.get('label'))}"
|
|
298
|
-
if fmt ==
|
|
353
|
+
if fmt == "json":
|
|
299
354
|
path = export_dir / f"{base}.json"
|
|
300
|
-
with path.open(
|
|
355
|
+
with path.open("w", encoding="utf-8") as f:
|
|
301
356
|
json.dump(entry, f, indent=2)
|
|
302
357
|
else:
|
|
303
358
|
path = export_dir / f"{base}.csv"
|
|
304
359
|
per_host = entry.get("per_host") or []
|
|
305
360
|
if not per_host:
|
|
306
361
|
# minimal CSV row when no per_host breakdown
|
|
307
|
-
per_host = [
|
|
308
|
-
|
|
362
|
+
per_host = [
|
|
363
|
+
{
|
|
364
|
+
"addr": "",
|
|
365
|
+
"up": "",
|
|
366
|
+
"open": (entry.get("summary") or {}).get("open_ports", 0),
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
with path.open("w", newline="", encoding="utf-8") as f:
|
|
309
370
|
w = csv.writer(f)
|
|
310
|
-
w.writerow(
|
|
371
|
+
w.writerow(
|
|
372
|
+
[
|
|
373
|
+
"timestamp",
|
|
374
|
+
"tool",
|
|
375
|
+
"target",
|
|
376
|
+
"label",
|
|
377
|
+
"host",
|
|
378
|
+
"up",
|
|
379
|
+
"open_ports",
|
|
380
|
+
"log",
|
|
381
|
+
"xml",
|
|
382
|
+
]
|
|
383
|
+
)
|
|
311
384
|
for h in per_host:
|
|
312
|
-
w.writerow(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
385
|
+
w.writerow(
|
|
386
|
+
[
|
|
387
|
+
entry.get("ts", ""),
|
|
388
|
+
entry.get("tool", ""),
|
|
389
|
+
entry.get("target", ""),
|
|
390
|
+
entry.get("label", ""),
|
|
391
|
+
h.get("addr", ""),
|
|
392
|
+
"" if h.get("up") is None else bool(h.get("up")),
|
|
393
|
+
h.get("open", 0),
|
|
394
|
+
entry.get("log", ""),
|
|
395
|
+
entry.get("xml", ""),
|
|
396
|
+
]
|
|
397
|
+
)
|
|
323
398
|
print(f"Exported {fmt.upper()} to: {path}")
|
|
324
399
|
|
|
400
|
+
|
|
325
401
|
def history_menu():
|
|
326
402
|
while True:
|
|
327
|
-
print(
|
|
328
|
-
print(
|
|
329
|
-
print(
|
|
330
|
-
print(
|
|
331
|
-
print(
|
|
332
|
-
print(
|
|
403
|
+
print("\nHistory Menu:")
|
|
404
|
+
print(" 1) View all history")
|
|
405
|
+
print(" 2) View by tool")
|
|
406
|
+
print(" 3) Re-run scan")
|
|
407
|
+
print(" 4) Export scan")
|
|
408
|
+
print(" 5) Back")
|
|
333
409
|
print()
|
|
334
|
-
choice = prompt(
|
|
335
|
-
if choice ==
|
|
410
|
+
choice = prompt("Choice > ")
|
|
411
|
+
if choice == "1":
|
|
336
412
|
scans = history_view_all()
|
|
337
|
-
elif choice ==
|
|
413
|
+
elif choice == "2":
|
|
338
414
|
history_view_by_tool()
|
|
339
415
|
scans = None
|
|
340
|
-
elif choice ==
|
|
416
|
+
elif choice == "3":
|
|
341
417
|
scans = history_view_all()
|
|
342
418
|
history_rerun(scans)
|
|
343
|
-
elif choice ==
|
|
419
|
+
elif choice == "4":
|
|
344
420
|
scans = history_view_all()
|
|
345
421
|
history_export(scans)
|
|
346
|
-
elif choice ==
|
|
422
|
+
elif choice == "5" or choice.lower() == "q":
|
|
347
423
|
return
|
|
348
424
|
else:
|
|
349
|
-
print(
|
|
425
|
+
print("Unknown choice.")
|
|
426
|
+
|
|
350
427
|
|
|
351
428
|
# ---- Main menu ----
|
|
352
429
|
|
|
@@ -357,75 +434,92 @@ def handle_gobuster():
|
|
|
357
434
|
Prompts for target (URL), extra args (user-provided), and an optional label.
|
|
358
435
|
Requires the user to supply -w <wordlist> in args for Gobuster to run.
|
|
359
436
|
"""
|
|
360
|
-
print(
|
|
361
|
-
target = prompt(
|
|
437
|
+
print("\n--- Gobuster (Web Plugin) ---")
|
|
438
|
+
target = prompt("Target (URL) > ")
|
|
362
439
|
if not target:
|
|
363
|
-
print(
|
|
440
|
+
print("No target provided; returning.")
|
|
364
441
|
return
|
|
365
|
-
print(
|
|
366
|
-
|
|
442
|
+
print(
|
|
443
|
+
"\n"
|
|
444
|
+
+ "\033[31m\033[1m"
|
|
445
|
+
+ "REMINDER: Gobuster will run next — make sure you provided -w <wordlist>. Example: data/wordlists/web_dirs_common.txt"
|
|
446
|
+
+ "\033[0m"
|
|
447
|
+
)
|
|
448
|
+
args_raw = prompt(
|
|
449
|
+
"Gobuster args (e.g. dir -u http://example.com -w /path/wordlist -t 10) > "
|
|
450
|
+
)
|
|
367
451
|
if not args_raw:
|
|
368
|
-
print(
|
|
452
|
+
print(
|
|
453
|
+
"No args provided. You must include ' -w <wordlist>' for Gobuster to run."
|
|
454
|
+
)
|
|
369
455
|
return
|
|
370
456
|
args = args_raw.split()
|
|
371
457
|
# Basic safety check: require -w (wordlist) in args
|
|
372
|
-
if not any(a ==
|
|
373
|
-
print(
|
|
458
|
+
if not any(a == "-w" for a in args):
|
|
459
|
+
print("Gobuster requires a wordlist (-w). Please provide -w <path>. Aborting.")
|
|
374
460
|
return
|
|
375
|
-
label = prompt(
|
|
376
|
-
print(
|
|
461
|
+
label = prompt("Label for this scan (optional) > ")
|
|
462
|
+
print(
|
|
463
|
+
"\\nStarting Gobuster scan — this will run synchronously (wait until it completes)."
|
|
464
|
+
)
|
|
377
465
|
sid = None
|
|
378
466
|
try:
|
|
379
|
-
sid = run_scan_sync(
|
|
467
|
+
sid = run_scan_sync("gobuster", target, args, label, save_xml=False)
|
|
380
468
|
except Exception as e:
|
|
381
|
-
print(f
|
|
469
|
+
print(f"Error launching gobuster: {e}")
|
|
382
470
|
return
|
|
383
|
-
print(f
|
|
471
|
+
print(f"Scan scheduled/completed with id: {sid}")
|
|
384
472
|
print()
|
|
385
473
|
|
|
474
|
+
|
|
386
475
|
def handle_web_plugins():
|
|
387
|
-
print(
|
|
476
|
+
print(
|
|
477
|
+
"\n"
|
|
478
|
+
+ "\033[31m\033[1m"
|
|
479
|
+
+ "NOTE: Gobuster requires -w <wordlist> to run. Example: data/wordlists/web_dirs_common.txt"
|
|
480
|
+
+ "\033[0m"
|
|
481
|
+
)
|
|
388
482
|
"""
|
|
389
483
|
Web Plugins submenu (TUI). Keep this minimal: user chooses a plugin to run or Back.
|
|
390
484
|
"""
|
|
391
485
|
while True:
|
|
392
486
|
print()
|
|
393
|
-
print(
|
|
394
|
-
print(
|
|
395
|
-
print(
|
|
487
|
+
print("--- Web Plugins ---")
|
|
488
|
+
print(" 1) Gobuster")
|
|
489
|
+
print(" b) Back")
|
|
396
490
|
print()
|
|
397
|
-
ch = prompt(
|
|
398
|
-
if ch in (
|
|
491
|
+
ch = prompt("Choice > ").strip().lower()
|
|
492
|
+
if ch in ("1", "gobuster"):
|
|
399
493
|
handle_gobuster()
|
|
400
|
-
elif ch ==
|
|
494
|
+
elif ch == "q":
|
|
401
495
|
return
|
|
402
496
|
else:
|
|
403
|
-
print(
|
|
497
|
+
print("Unknown choice. Use 1 or b (back).")
|
|
404
498
|
|
|
405
499
|
|
|
406
500
|
def show_menu():
|
|
407
501
|
detected = detect_local_subnet()
|
|
408
502
|
print_header()
|
|
409
503
|
if detected:
|
|
410
|
-
print(f
|
|
504
|
+
print(f" 0) Scan my LAN (auto-detect) [{detected}]")
|
|
411
505
|
else:
|
|
412
|
-
print(
|
|
506
|
+
print(" 0) Scan my LAN (auto-detect) [no subnet detected]")
|
|
413
507
|
print()
|
|
414
|
-
print(
|
|
415
|
-
print(
|
|
416
|
-
print(
|
|
417
|
-
print(
|
|
508
|
+
print("Recon Plugins (Network)")
|
|
509
|
+
print(" 1) Discovery Scan (ping only)")
|
|
510
|
+
print(" 2) Fast Scan")
|
|
511
|
+
print(" 3) Full Service/OS Scan")
|
|
418
512
|
print()
|
|
419
|
-
print(
|
|
420
|
-
print(
|
|
513
|
+
print("Web Plugins")
|
|
514
|
+
print(" 7) Web Plugins (Gobuster, etc.)")
|
|
421
515
|
print()
|
|
422
|
-
print(
|
|
423
|
-
print(
|
|
424
|
-
print(
|
|
516
|
+
print(" 4) Custom Scan")
|
|
517
|
+
print(" 5) History")
|
|
518
|
+
print(" 6) Exit")
|
|
425
519
|
print()
|
|
426
|
-
print(
|
|
427
|
-
print(
|
|
428
|
-
print(
|
|
520
|
+
print("System")
|
|
521
|
+
print(" 8) Background Jobs")
|
|
522
|
+
print(" 9) Network Plugins")
|
|
429
523
|
print()
|
|
430
524
|
|
|
431
525
|
|
|
@@ -433,57 +527,71 @@ def run_menu_loop():
|
|
|
433
527
|
print_banner()
|
|
434
528
|
while True:
|
|
435
529
|
show_menu()
|
|
436
|
-
choice = prompt(
|
|
437
|
-
if choice ==
|
|
530
|
+
choice = prompt("Choice > ").strip().lower()
|
|
531
|
+
if choice == "0":
|
|
438
532
|
handle_scan_my_lan()
|
|
439
|
-
elif choice in (
|
|
533
|
+
elif choice in ("1", "2", "3"):
|
|
440
534
|
handle_preset_choice(choice)
|
|
441
|
-
elif choice ==
|
|
535
|
+
elif choice == "4":
|
|
442
536
|
handle_custom()
|
|
443
|
-
elif choice ==
|
|
537
|
+
elif choice == "5":
|
|
444
538
|
history_menu()
|
|
445
|
-
elif choice ==
|
|
539
|
+
elif choice == "8":
|
|
446
540
|
background_jobs_menu()
|
|
447
|
-
elif choice ==
|
|
448
|
-
print(
|
|
541
|
+
elif choice == "6" or choice.lower() in ("q", "quit", "exit"):
|
|
542
|
+
print("Goodbye!")
|
|
449
543
|
sys.exit(0)
|
|
450
544
|
# web plugins (if present) — typically printed as 7 in the menu
|
|
451
|
-
elif choice ==
|
|
545
|
+
elif choice == "7":
|
|
452
546
|
try:
|
|
453
547
|
from .ui import handle_web_plugins
|
|
548
|
+
|
|
454
549
|
handle_web_plugins()
|
|
455
550
|
except Exception:
|
|
456
551
|
# if not present, show message and continue
|
|
457
|
-
print(
|
|
552
|
+
print("Web Plugins are not available.")
|
|
458
553
|
# background jobs (system) — printed as 8
|
|
459
|
-
elif choice ==
|
|
554
|
+
elif choice == "8":
|
|
460
555
|
try:
|
|
461
556
|
# reuse CLI job menu if exposed; fallback to _cmd_jobs via main
|
|
462
557
|
from .main import _cmd_jobs
|
|
463
|
-
|
|
558
|
+
|
|
559
|
+
_cmd_jobs(["list"])
|
|
464
560
|
except Exception:
|
|
465
561
|
# if a background jobs TUI exists, call it; else notify
|
|
466
|
-
print(
|
|
562
|
+
print("Background jobs command not available from TUI.")
|
|
467
563
|
# network plugins (printed as 9)
|
|
468
|
-
elif choice ==
|
|
564
|
+
elif choice == "9":
|
|
469
565
|
try:
|
|
470
566
|
handle_network_plugins()
|
|
471
567
|
except NameError:
|
|
472
|
-
print(
|
|
568
|
+
print("Network plugins are not available.")
|
|
473
569
|
else:
|
|
474
|
-
print(
|
|
475
|
-
|
|
570
|
+
print("Invalid choice. Try again.\\n")
|
|
476
571
|
|
|
477
572
|
|
|
478
573
|
def _status_icon_txt(s: str) -> str:
|
|
479
|
-
m = {
|
|
574
|
+
m = {
|
|
575
|
+
"queued": "● queued",
|
|
576
|
+
"running": "▶ running",
|
|
577
|
+
"done": "✔ done",
|
|
578
|
+
"failed": "✖ failed",
|
|
579
|
+
}
|
|
480
580
|
return m.get((s or "").lower(), s or "?")
|
|
481
581
|
|
|
582
|
+
|
|
482
583
|
def _print_jobs_grouped():
|
|
483
584
|
# Group order: running -> queued -> done -> failed
|
|
484
|
-
groups = [
|
|
585
|
+
groups = [
|
|
586
|
+
("running", "RUNNING"),
|
|
587
|
+
("queued", "QUEUED"),
|
|
588
|
+
("done", "DONE"),
|
|
589
|
+
("failed", "FAILED"),
|
|
590
|
+
]
|
|
485
591
|
print(" BACKGROUND JOBS")
|
|
486
|
-
print(
|
|
592
|
+
print(
|
|
593
|
+
"──────────────────────────────────────────────────────────────────────────────"
|
|
594
|
+
)
|
|
487
595
|
jobs = list_jobs(limit=200)
|
|
488
596
|
if not jobs:
|
|
489
597
|
print(" (no jobs)")
|
|
@@ -494,77 +602,86 @@ def _print_jobs_grouped():
|
|
|
494
602
|
continue
|
|
495
603
|
print(f" {title}")
|
|
496
604
|
print(" ID TOOL STATUS TARGET CREATED")
|
|
497
|
-
print(
|
|
605
|
+
print(
|
|
606
|
+
" ───────────────────────────────────────────────────────────────────────────"
|
|
607
|
+
)
|
|
498
608
|
for j in subset:
|
|
499
609
|
st = (j.get("status") or "").lower()
|
|
500
610
|
icon = _status_icon_txt(st)
|
|
501
|
-
print(
|
|
611
|
+
print(
|
|
612
|
+
f"{str(j['id']).ljust(4)} {j['tool'][:10].ljust(10)} {icon[:10].ljust(10)} {j['target'][:30].ljust(30)} {j.get('created_at','')}"
|
|
613
|
+
)
|
|
502
614
|
print()
|
|
503
615
|
|
|
616
|
+
|
|
504
617
|
def _jobs_tail_prompt():
|
|
505
|
-
jid = prompt(
|
|
618
|
+
jid = prompt("Job id to tail > ")
|
|
506
619
|
if not jid or not jid.isdigit():
|
|
507
|
-
print(
|
|
620
|
+
print("Invalid job id.")
|
|
508
621
|
return
|
|
509
622
|
j = get_job(int(jid))
|
|
510
623
|
if not j:
|
|
511
|
-
print(
|
|
624
|
+
print("Job not found.")
|
|
512
625
|
return
|
|
513
|
-
scan_id = j.get(
|
|
626
|
+
scan_id = j.get("result_scan_id")
|
|
514
627
|
if not scan_id:
|
|
515
|
-
print(
|
|
628
|
+
print("Job has no scan result yet.")
|
|
516
629
|
return
|
|
517
630
|
try:
|
|
518
631
|
from .storage.db import get_scan
|
|
632
|
+
|
|
519
633
|
rec = get_scan(scan_id)
|
|
520
|
-
if rec and rec.get(
|
|
634
|
+
if rec and rec.get("log"):
|
|
521
635
|
try:
|
|
522
|
-
with open(rec[
|
|
636
|
+
with open(rec["log"], "r", encoding="utf-8", errors="ignore") as fh:
|
|
523
637
|
lines = fh.readlines()
|
|
524
|
-
print(
|
|
638
|
+
print("".join(lines[-200:]))
|
|
525
639
|
except Exception as e:
|
|
526
|
-
print(
|
|
640
|
+
print("Could not read log file:", e)
|
|
527
641
|
else:
|
|
528
|
-
print(
|
|
642
|
+
print("No log path recorded for scan.")
|
|
529
643
|
except Exception as e:
|
|
530
|
-
print(
|
|
644
|
+
print("Cannot load scan record:", e)
|
|
645
|
+
|
|
531
646
|
|
|
532
647
|
def handle_background_jobs():
|
|
533
648
|
while True:
|
|
534
|
-
print(
|
|
535
|
-
print(
|
|
536
|
-
print(
|
|
537
|
-
print(
|
|
538
|
-
print(
|
|
539
|
-
print(
|
|
540
|
-
print(
|
|
541
|
-
print(
|
|
542
|
-
print(
|
|
543
|
-
print(
|
|
649
|
+
print("\\n--- Background Jobs ---")
|
|
650
|
+
print(" 1) View queued jobs")
|
|
651
|
+
print(" 2) View running jobs")
|
|
652
|
+
print(" 3) View completed jobs")
|
|
653
|
+
print(" 4) Tail job log")
|
|
654
|
+
print(" v) View live output")
|
|
655
|
+
print(" 5) Start worker")
|
|
656
|
+
print(" 6) Stop worker")
|
|
657
|
+
print(" r) Refresh")
|
|
658
|
+
print(" b) Back")
|
|
544
659
|
print()
|
|
545
|
-
ch = prompt(
|
|
546
|
-
if ch in (
|
|
660
|
+
ch = prompt("Choice > ").strip().lower()
|
|
661
|
+
if ch in ("1", "2", "3", "r"):
|
|
547
662
|
_print_jobs_grouped()
|
|
548
|
-
elif ch ==
|
|
663
|
+
elif ch == "4":
|
|
549
664
|
_jobs_tail_prompt()
|
|
550
|
-
elif ch ==
|
|
665
|
+
elif ch == "v":
|
|
551
666
|
view_job_live_prompt()
|
|
552
|
-
elif ch ==
|
|
553
|
-
print(
|
|
554
|
-
print(
|
|
555
|
-
print(
|
|
556
|
-
sel = prompt(
|
|
557
|
-
fg =
|
|
667
|
+
elif ch == "5":
|
|
668
|
+
print("Run worker:")
|
|
669
|
+
print(" 1) Foreground (monitor in this terminal)")
|
|
670
|
+
print(" 2) Background (detach)")
|
|
671
|
+
sel = prompt("Choice > ").strip()
|
|
672
|
+
fg = sel == "1"
|
|
558
673
|
start_worker(detach=(not fg))
|
|
559
|
-
print(
|
|
560
|
-
|
|
674
|
+
print(
|
|
675
|
+
"Worker started "
|
|
676
|
+
+ ("(foreground thread)." if fg else "(daemon background thread).")
|
|
677
|
+
)
|
|
678
|
+
elif ch == "6":
|
|
561
679
|
stop_worker()
|
|
562
|
-
print(
|
|
563
|
-
elif ch ==
|
|
680
|
+
print("Worker stop signal sent.")
|
|
681
|
+
elif ch == "q":
|
|
564
682
|
return
|
|
565
683
|
else:
|
|
566
|
-
print(
|
|
567
|
-
|
|
684
|
+
print("Unknown choice.")
|
|
568
685
|
|
|
569
686
|
|
|
570
687
|
# ----------------------------
|
|
@@ -572,12 +689,13 @@ def handle_background_jobs():
|
|
|
572
689
|
# ----------------------------
|
|
573
690
|
def view_job_live_prompt():
|
|
574
691
|
"""Prompt for a job id and launch the live viewer."""
|
|
575
|
-
jid = prompt(
|
|
692
|
+
jid = prompt("Job id to view live > ")
|
|
576
693
|
if not jid or not jid.isdigit():
|
|
577
|
-
print(
|
|
694
|
+
print("Invalid job id.")
|
|
578
695
|
return
|
|
579
696
|
view_job_live(int(jid))
|
|
580
697
|
|
|
698
|
+
|
|
581
699
|
def view_job_live(job_id: int, refresh_interval: float = 1.0, max_lines: int = 300):
|
|
582
700
|
"""
|
|
583
701
|
Framed live tail viewer (T3).
|
|
@@ -590,85 +708,99 @@ def view_job_live(job_id: int, refresh_interval: float = 1.0, max_lines: int = 3
|
|
|
590
708
|
- falls back to ~/.souleyez/artifacts/<job_id>.log when missing
|
|
591
709
|
"""
|
|
592
710
|
import os, sys, time, json, select
|
|
711
|
+
|
|
593
712
|
# lookup job record
|
|
594
713
|
j = None
|
|
595
714
|
try:
|
|
596
715
|
j = get_job(job_id)
|
|
597
716
|
except Exception as _e:
|
|
598
|
-
print(
|
|
717
|
+
print("Could not load job:", _e)
|
|
599
718
|
return
|
|
600
719
|
if not j:
|
|
601
|
-
print(
|
|
720
|
+
print("Job not found.")
|
|
602
721
|
return
|
|
603
722
|
# attempt to find log path
|
|
604
723
|
log_path = None
|
|
605
|
-
scan_id = j.get(
|
|
724
|
+
scan_id = j.get("result_scan_id")
|
|
606
725
|
if scan_id:
|
|
607
726
|
try:
|
|
608
727
|
from .storage.db import get_scan
|
|
728
|
+
|
|
609
729
|
rec = get_scan(scan_id)
|
|
610
|
-
if rec and rec.get(
|
|
611
|
-
log_path = rec.get(
|
|
730
|
+
if rec and rec.get("log"):
|
|
731
|
+
log_path = rec.get("log")
|
|
612
732
|
except Exception:
|
|
613
733
|
# ignore, fallback later
|
|
614
734
|
pass
|
|
615
735
|
if not log_path:
|
|
616
736
|
# default fallback
|
|
617
|
-
log_dir = os.path.join(os.path.expanduser(
|
|
737
|
+
log_dir = os.path.join(os.path.expanduser("~"), ".souleyez", "artifacts")
|
|
618
738
|
if not os.path.isdir(log_dir):
|
|
619
739
|
os.makedirs(log_dir, exist_ok=True)
|
|
620
|
-
log_path = os.path.join(log_dir, f
|
|
740
|
+
log_path = os.path.join(log_dir, f"{job_id}.log")
|
|
621
741
|
# viewer loop
|
|
622
|
-
print(
|
|
742
|
+
print(
|
|
743
|
+
f"Opening live view for job {job_id} (log: {log_path}) — press q then ENTER to quit"
|
|
744
|
+
)
|
|
623
745
|
last_size = 0
|
|
624
746
|
try:
|
|
625
747
|
while True:
|
|
626
748
|
# clear screen
|
|
627
|
-
subprocess.run([
|
|
749
|
+
subprocess.run(["clear" if os.name == "posix" else "cls"], shell=False)
|
|
628
750
|
# header frame
|
|
629
|
-
print(
|
|
630
|
-
title = f
|
|
631
|
-
print(
|
|
632
|
-
print(
|
|
751
|
+
print("╭" + "─" * 74 + "╮")
|
|
752
|
+
title = f" Live Output — Job {job_id} "
|
|
753
|
+
print("│" + title.center(74) + "│")
|
|
754
|
+
print("├" + "─" * 74 + "┤")
|
|
633
755
|
# read last lines safely
|
|
634
756
|
lines = []
|
|
635
757
|
try:
|
|
636
758
|
if os.path.exists(log_path):
|
|
637
|
-
with open(log_path,
|
|
759
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as fh:
|
|
638
760
|
all_lines = fh.readlines()
|
|
639
761
|
if len(all_lines) > max_lines:
|
|
640
762
|
lines = all_lines[-max_lines:]
|
|
641
763
|
else:
|
|
642
764
|
lines = all_lines
|
|
643
765
|
else:
|
|
644
|
-
lines = [
|
|
766
|
+
lines = ["(log file not found yet — waiting for output...)"]
|
|
645
767
|
except Exception as e:
|
|
646
|
-
lines = [f
|
|
768
|
+
lines = [f"(error reading log: {e})"]
|
|
647
769
|
# print content with side padding
|
|
648
770
|
for L in lines:
|
|
649
771
|
# ensure single-line prints (no embedded control sequences)
|
|
650
|
-
L = L.rstrip(
|
|
772
|
+
L = L.rstrip("\n")
|
|
651
773
|
# clamp to width 74
|
|
652
774
|
if len(L) > 74:
|
|
653
775
|
L = L[-74:]
|
|
654
|
-
print(
|
|
776
|
+
print("│" + L.ljust(74) + "│")
|
|
655
777
|
# footer
|
|
656
|
-
print(
|
|
657
|
-
print(
|
|
658
|
-
|
|
778
|
+
print("├" + "─" * 74 + "┤")
|
|
779
|
+
print(
|
|
780
|
+
"│"
|
|
781
|
+
+ f" Press q + ENTER to quit | Refresh every {refresh_interval}s ".ljust(
|
|
782
|
+
74
|
|
783
|
+
)
|
|
784
|
+
+ "│"
|
|
785
|
+
)
|
|
786
|
+
print("╰" + "─" * 74 + "╯")
|
|
659
787
|
# wait with non-blocking check for input
|
|
660
788
|
# select.select works on POSIX; on Windows this might behave differently.
|
|
661
789
|
sys.stdout.flush()
|
|
662
790
|
rlist, _, _ = select.select([sys.stdin], [], [], refresh_interval)
|
|
663
791
|
if rlist:
|
|
664
792
|
inp = sys.stdin.readline().strip().lower()
|
|
665
|
-
if inp ==
|
|
793
|
+
if inp == "q":
|
|
666
794
|
break
|
|
667
795
|
# check job status and exit when done (optional)
|
|
668
796
|
try:
|
|
669
797
|
j = get_job(job_id)
|
|
670
|
-
if j and (j.get(
|
|
671
|
-
print(
|
|
798
|
+
if j and (j.get("status") in ("done", "failed")):
|
|
799
|
+
print(
|
|
800
|
+
"\nJob finished (status: {}). Press ENTER to return.".format(
|
|
801
|
+
j.get("status")
|
|
802
|
+
)
|
|
803
|
+
)
|
|
672
804
|
# wait for user to press enter
|
|
673
805
|
_ = sys.stdin.readline()
|
|
674
806
|
break
|
|
@@ -680,48 +812,45 @@ def view_job_live(job_id: int, refresh_interval: float = 1.0, max_lines: int = 3
|
|
|
680
812
|
pass
|
|
681
813
|
finally:
|
|
682
814
|
# small tidy
|
|
683
|
-
print(
|
|
815
|
+
print("Exiting live view.")
|
|
684
816
|
print(" 8) Background Jobs")
|
|
685
817
|
|
|
686
818
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
819
|
def _print_plugin_help_block(helpdata: dict):
|
|
692
820
|
"""Pretty-print H2 style help for plugin HELP dict."""
|
|
693
821
|
if not helpdata:
|
|
694
822
|
print("No help available for this plugin.")
|
|
695
823
|
return
|
|
696
824
|
print()
|
|
697
|
-
print(helpdata.get("name", "Plugin")
|
|
825
|
+
print(helpdata.get("name", "Plugin"))
|
|
698
826
|
print("─" * max(10, len(helpdata.get("name", ""))))
|
|
699
827
|
print("Description:")
|
|
700
|
-
for line in helpdata.get("description","").splitlines():
|
|
828
|
+
for line in helpdata.get("description", "").splitlines():
|
|
701
829
|
print(" " + line)
|
|
702
830
|
print()
|
|
703
831
|
print("Usage:")
|
|
704
|
-
print(" " + helpdata.get("usage",""))
|
|
832
|
+
print(" " + helpdata.get("usage", ""))
|
|
705
833
|
print()
|
|
706
834
|
if helpdata.get("examples"):
|
|
707
835
|
print("Examples:")
|
|
708
|
-
for ex in helpdata.get("examples",[]):
|
|
836
|
+
for ex in helpdata.get("examples", []):
|
|
709
837
|
print(" " + ex)
|
|
710
838
|
print()
|
|
711
839
|
if helpdata.get("flags"):
|
|
712
840
|
print("Useful Flags:")
|
|
713
|
-
for flag, desc in helpdata.get("flags",[]):
|
|
841
|
+
for flag, desc in helpdata.get("flags", []):
|
|
714
842
|
print(f" {flag.ljust(18)} {desc}")
|
|
715
843
|
print()
|
|
716
844
|
if helpdata.get("presets"):
|
|
717
845
|
print("Presets:")
|
|
718
|
-
for i, p in enumerate(helpdata.get("presets",[]), start=1):
|
|
846
|
+
for i, p in enumerate(helpdata.get("presets", []), start=1):
|
|
719
847
|
print(f" {i}) {p.get('name')} - {p.get('desc')}")
|
|
720
848
|
print()
|
|
721
849
|
print("Legal:")
|
|
722
850
|
print(" Use only on systems you own or have explicit permission to test.")
|
|
723
851
|
print()
|
|
724
852
|
|
|
853
|
+
|
|
725
854
|
def handle_network_plugins():
|
|
726
855
|
"""
|
|
727
856
|
TUI submenu with H2 help + presets + run/enqueue flow.
|
|
@@ -744,7 +873,7 @@ def handle_network_plugins():
|
|
|
744
873
|
choice = prompt("Choice > ").strip().lower()
|
|
745
874
|
if not choice:
|
|
746
875
|
continue
|
|
747
|
-
if choice in ("b","back"):
|
|
876
|
+
if choice in ("b", "back"):
|
|
748
877
|
return
|
|
749
878
|
if choice == "h":
|
|
750
879
|
print("Choose a plugin number to show detailed help.")
|
|
@@ -771,7 +900,7 @@ def handle_network_plugins():
|
|
|
771
900
|
ch = prompt("Choice > ").strip().lower()
|
|
772
901
|
if not ch:
|
|
773
902
|
continue
|
|
774
|
-
if ch in ("b","back"):
|
|
903
|
+
if ch in ("b", "back"):
|
|
775
904
|
break
|
|
776
905
|
if ch == "h":
|
|
777
906
|
_print_plugin_help_block(helpdata or {})
|
|
@@ -780,7 +909,7 @@ def handle_network_plugins():
|
|
|
780
909
|
args_raw = prompt("Custom args (e.g. -Tuning 9 -ssl) > ").strip()
|
|
781
910
|
args = args_raw.split() if args_raw else []
|
|
782
911
|
elif ch.isdigit() and presets and (1 <= int(ch) <= len(presets)):
|
|
783
|
-
args = presets[int(ch)-1].get("args",[])
|
|
912
|
+
args = presets[int(ch) - 1].get("args", [])
|
|
784
913
|
print("Selected preset args:", " ".join(args))
|
|
785
914
|
else:
|
|
786
915
|
print("Invalid choice.")
|
|
@@ -796,7 +925,10 @@ def handle_network_plugins():
|
|
|
796
925
|
print(f"Run completed rc={rc} log={logp}")
|
|
797
926
|
try:
|
|
798
927
|
from .history import add_history_entry
|
|
799
|
-
|
|
928
|
+
|
|
929
|
+
add_history_entry(
|
|
930
|
+
target, args, label or "", logp, "", tool=plugin.tool
|
|
931
|
+
)
|
|
800
932
|
except Exception:
|
|
801
933
|
pass
|
|
802
934
|
except Exception as e:
|
|
@@ -814,111 +946,131 @@ def handle_network_plugins():
|
|
|
814
946
|
# after run/enqueue return to plugin submenu
|
|
815
947
|
# end while
|
|
816
948
|
|
|
949
|
+
|
|
817
950
|
# ---- Background Jobs TUI (auto-added) ----
|
|
818
951
|
# Provides: list jobs, view details, tail log, start worker
|
|
819
952
|
def _format_status(s):
|
|
820
953
|
try:
|
|
821
|
-
if s ==
|
|
822
|
-
return GREEN +
|
|
823
|
-
if s ==
|
|
824
|
-
return CYAN +
|
|
825
|
-
if s ==
|
|
826
|
-
return MAG +
|
|
954
|
+
if s == "done":
|
|
955
|
+
return GREEN + "done" + RESET
|
|
956
|
+
if s == "running":
|
|
957
|
+
return CYAN + "running" + RESET
|
|
958
|
+
if s == "queued":
|
|
959
|
+
return MAG + "queued" + RESET
|
|
827
960
|
return RED + str(s) + RESET
|
|
828
961
|
except Exception:
|
|
829
962
|
return str(s)
|
|
830
963
|
|
|
964
|
+
|
|
831
965
|
def _print_job_row(j):
|
|
832
|
-
jid = j.get(
|
|
833
|
-
tool = j.get(
|
|
834
|
-
target = j.get(
|
|
835
|
-
status = _format_status(j.get(
|
|
836
|
-
created = j.get(
|
|
837
|
-
print(
|
|
966
|
+
jid = j.get("id")
|
|
967
|
+
tool = j.get("tool") or ""
|
|
968
|
+
target = j.get("target") or ""
|
|
969
|
+
status = _format_status(j.get("status"))
|
|
970
|
+
created = j.get("created_at") or ""
|
|
971
|
+
print(
|
|
972
|
+
f"{str(jid).ljust(4)} {tool.ljust(10)} {target.ljust(30)} {status.ljust(10)} {created}"
|
|
973
|
+
)
|
|
974
|
+
|
|
838
975
|
|
|
839
976
|
def background_jobs_menu():
|
|
840
977
|
while True:
|
|
841
978
|
print()
|
|
842
|
-
print(
|
|
843
|
-
print(
|
|
844
|
-
print(
|
|
845
|
-
print(
|
|
846
|
-
print(
|
|
847
|
-
print(
|
|
848
|
-
print(
|
|
849
|
-
print(
|
|
850
|
-
ch = prompt(
|
|
851
|
-
if ch in (
|
|
979
|
+
print("=" * 60)
|
|
980
|
+
print("--- Background Jobs ---")
|
|
981
|
+
print(" 1) List jobs")
|
|
982
|
+
print(" 2) View job details")
|
|
983
|
+
print(" 3) Tail job log")
|
|
984
|
+
print(" 4) Start worker (background)")
|
|
985
|
+
print(" 5) Start worker (foreground)")
|
|
986
|
+
print(" b) Back")
|
|
987
|
+
ch = prompt("Choice > ").strip().lower()
|
|
988
|
+
if ch in ("1", "list"):
|
|
852
989
|
jobs = []
|
|
853
990
|
try:
|
|
854
991
|
jobs = list_jobs(limit=200)
|
|
855
992
|
except Exception as e:
|
|
856
|
-
print(
|
|
993
|
+
print("Could not load jobs:", e)
|
|
857
994
|
jobs = []
|
|
858
995
|
if not jobs:
|
|
859
|
-
print(
|
|
996
|
+
print("No jobs.")
|
|
860
997
|
else:
|
|
861
998
|
print()
|
|
862
|
-
print(
|
|
863
|
-
|
|
999
|
+
print(
|
|
1000
|
+
"ID Tool Target Status Created"
|
|
1001
|
+
)
|
|
1002
|
+
print("-" * 80)
|
|
864
1003
|
for j in jobs:
|
|
865
1004
|
_print_job_row(j)
|
|
866
|
-
print(
|
|
867
|
-
elif ch in (
|
|
868
|
-
jid = prompt(
|
|
1005
|
+
print("-" * 80)
|
|
1006
|
+
elif ch in ("2", "view"):
|
|
1007
|
+
jid = prompt("Job ID > ")
|
|
869
1008
|
if not jid:
|
|
870
1009
|
continue
|
|
871
1010
|
try:
|
|
872
1011
|
jidn = int(jid)
|
|
873
1012
|
except Exception:
|
|
874
|
-
print(
|
|
1013
|
+
print("Invalid id.")
|
|
875
1014
|
continue
|
|
876
1015
|
rec = get_job(jidn)
|
|
877
1016
|
if not rec:
|
|
878
|
-
print(
|
|
1017
|
+
print("Job not found.")
|
|
879
1018
|
continue
|
|
880
1019
|
# pretty print limited fields
|
|
881
|
-
for k in (
|
|
1020
|
+
for k in (
|
|
1021
|
+
"id",
|
|
1022
|
+
"tool",
|
|
1023
|
+
"target",
|
|
1024
|
+
"args",
|
|
1025
|
+
"label",
|
|
1026
|
+
"status",
|
|
1027
|
+
"created_at",
|
|
1028
|
+
"started_at",
|
|
1029
|
+
"finished_at",
|
|
1030
|
+
"error",
|
|
1031
|
+
"log",
|
|
1032
|
+
):
|
|
882
1033
|
print(f"{k}: {rec.get(k)}")
|
|
883
|
-
elif ch in (
|
|
884
|
-
jid = prompt(
|
|
1034
|
+
elif ch in ("3", "tail"):
|
|
1035
|
+
jid = prompt("Job ID > ")
|
|
885
1036
|
if not jid:
|
|
886
1037
|
continue
|
|
887
1038
|
try:
|
|
888
1039
|
jidn = int(jid)
|
|
889
1040
|
except Exception:
|
|
890
|
-
print(
|
|
1041
|
+
print("Invalid id.")
|
|
891
1042
|
continue
|
|
892
1043
|
rec = get_job(jidn)
|
|
893
1044
|
if not rec:
|
|
894
|
-
print(
|
|
1045
|
+
print("Job not found.")
|
|
895
1046
|
continue
|
|
896
|
-
logp = rec.get(
|
|
1047
|
+
logp = rec.get("log") or ""
|
|
897
1048
|
if not logp or not os.path.exists(logp):
|
|
898
|
-
print(
|
|
1049
|
+
print("Log not found:", logp)
|
|
899
1050
|
continue
|
|
900
1051
|
try:
|
|
901
|
-
with open(logp,
|
|
1052
|
+
with open(logp, "r", encoding="utf-8", errors="replace") as fh:
|
|
902
1053
|
lines = fh.readlines()[-200:]
|
|
903
|
-
print(
|
|
1054
|
+
print("".join(lines))
|
|
904
1055
|
except Exception as e:
|
|
905
|
-
print(
|
|
906
|
-
elif ch ==
|
|
1056
|
+
print("Could not read log:", e)
|
|
1057
|
+
elif ch == "4":
|
|
907
1058
|
try:
|
|
908
1059
|
start_worker(detach=True)
|
|
909
|
-
print(
|
|
1060
|
+
print("Worker started (background). Check data/logs/worker.log")
|
|
910
1061
|
except Exception as e:
|
|
911
|
-
print(
|
|
912
|
-
elif ch ==
|
|
1062
|
+
print("Could not start worker:", e)
|
|
1063
|
+
elif ch == "5":
|
|
913
1064
|
try:
|
|
914
1065
|
start_worker(detach=False, fg=True)
|
|
915
1066
|
except KeyboardInterrupt:
|
|
916
|
-
print(
|
|
1067
|
+
print("\nWorker stopped (foreground).")
|
|
917
1068
|
except Exception as e:
|
|
918
|
-
print(
|
|
919
|
-
elif ch ==
|
|
1069
|
+
print("Could not start worker:", e)
|
|
1070
|
+
elif ch == "q":
|
|
920
1071
|
return
|
|
921
1072
|
else:
|
|
922
|
-
print(
|
|
923
|
-
|
|
1073
|
+
print("Unknown choice.")
|
|
1074
|
+
|
|
924
1075
|
|
|
1076
|
+
# ---- end Background Jobs TUI ----
|