souleyez 2.43.29__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 +22827 -10678
- 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-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/main.py
CHANGED
|
@@ -16,7 +16,13 @@ from rich.console import Console
|
|
|
16
16
|
from rich.table import Table
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
|
-
from souleyez.engine.background import
|
|
19
|
+
from souleyez.engine.background import (
|
|
20
|
+
enqueue_job,
|
|
21
|
+
list_jobs,
|
|
22
|
+
get_job,
|
|
23
|
+
start_worker,
|
|
24
|
+
worker_loop,
|
|
25
|
+
)
|
|
20
26
|
from souleyez.storage.engagements import EngagementManager
|
|
21
27
|
from souleyez.ui.interactive import run_interactive_menu
|
|
22
28
|
from souleyez.ui.dashboard import run_dashboard
|
|
@@ -29,10 +35,10 @@ def _check_prerequisites():
|
|
|
29
35
|
"""Check for required system tools and warn if missing."""
|
|
30
36
|
missing = []
|
|
31
37
|
|
|
32
|
-
if not shutil.which(
|
|
33
|
-
missing.append(
|
|
34
|
-
if not shutil.which(
|
|
35
|
-
missing.append(
|
|
38
|
+
if not shutil.which("curl"):
|
|
39
|
+
missing.append("curl")
|
|
40
|
+
if not shutil.which("pip3"):
|
|
41
|
+
missing.append("python3-pip")
|
|
36
42
|
|
|
37
43
|
if missing:
|
|
38
44
|
click.secho("⚠️ Missing prerequisites:", fg="yellow", bold=True)
|
|
@@ -54,11 +60,12 @@ def _check_first_run_setup():
|
|
|
54
60
|
return
|
|
55
61
|
|
|
56
62
|
# Skip if running the setup command itself
|
|
57
|
-
if len(sys.argv) > 1 and sys.argv[1] in (
|
|
63
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("setup", "--version", "--help"):
|
|
58
64
|
return
|
|
59
65
|
|
|
60
66
|
try:
|
|
61
67
|
from souleyez.utils.tool_checker import get_tool_stats
|
|
68
|
+
|
|
62
69
|
installed, total = get_tool_stats()
|
|
63
70
|
|
|
64
71
|
# If less than 3 tools installed, prompt user
|
|
@@ -68,7 +75,9 @@ def _check_first_run_setup():
|
|
|
68
75
|
click.secho(" FIRST RUN DETECTED", fg="cyan", bold=True)
|
|
69
76
|
click.secho("=" * 60, fg="cyan")
|
|
70
77
|
click.echo()
|
|
71
|
-
click.echo(
|
|
78
|
+
click.echo(
|
|
79
|
+
f" SoulEyez wraps 40+ pentesting tools, but only {installed}/{total}"
|
|
80
|
+
)
|
|
72
81
|
click.echo(" tools are currently installed on your system.")
|
|
73
82
|
click.echo()
|
|
74
83
|
click.echo(" Run the setup wizard to install tools like nmap, sqlmap,")
|
|
@@ -83,6 +92,7 @@ def _check_first_run_setup():
|
|
|
83
92
|
|
|
84
93
|
# Import and run setup
|
|
85
94
|
from souleyez.ui.tool_setup import run_tool_setup
|
|
95
|
+
|
|
86
96
|
run_tool_setup(check_only=False, install_all=False)
|
|
87
97
|
sys.exit(0)
|
|
88
98
|
else:
|
|
@@ -104,7 +114,7 @@ def _check_privileged_tools():
|
|
|
104
114
|
import subprocess
|
|
105
115
|
|
|
106
116
|
# Skip if running setup or help commands
|
|
107
|
-
if len(sys.argv) > 1 and sys.argv[1] in (
|
|
117
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("setup", "--version", "--help"):
|
|
108
118
|
return
|
|
109
119
|
|
|
110
120
|
marker_file = Path.home() / ".souleyez" / ".sudoers_declined"
|
|
@@ -114,18 +124,16 @@ def _check_privileged_tools():
|
|
|
114
124
|
return
|
|
115
125
|
|
|
116
126
|
# Check if nmap is installed and needs configuration
|
|
117
|
-
nmap_path = shutil.which(
|
|
127
|
+
nmap_path = shutil.which("nmap")
|
|
118
128
|
if not nmap_path:
|
|
119
129
|
return
|
|
120
130
|
|
|
121
131
|
# Check if passwordless sudo already works
|
|
122
132
|
# First clear any cached sudo credentials so we test the actual config
|
|
123
133
|
try:
|
|
124
|
-
subprocess.run([
|
|
134
|
+
subprocess.run(["sudo", "-k"], capture_output=True, timeout=5)
|
|
125
135
|
result = subprocess.run(
|
|
126
|
-
[
|
|
127
|
-
capture_output=True,
|
|
128
|
-
timeout=5
|
|
136
|
+
["sudo", "-n", nmap_path, "--version"], capture_output=True, timeout=5
|
|
129
137
|
)
|
|
130
138
|
if result.returncode == 0:
|
|
131
139
|
return # Already configured
|
|
@@ -151,6 +159,7 @@ def _check_privileged_tools():
|
|
|
151
159
|
|
|
152
160
|
try:
|
|
153
161
|
import subprocess
|
|
162
|
+
|
|
154
163
|
# Use printf to ensure newline at end (required by sudoers parser)
|
|
155
164
|
# echo should add newline but some environments strip it
|
|
156
165
|
cmd = f"printf '%s\\n' '{sudoers_line}' | sudo tee {sudoers_file} > /dev/null && sudo chmod 0440 {sudoers_file}"
|
|
@@ -159,7 +168,10 @@ def _check_privileged_tools():
|
|
|
159
168
|
if proc.returncode == 0:
|
|
160
169
|
click.secho(" ✓ Configured! Privileged scans now work.", fg="green")
|
|
161
170
|
else:
|
|
162
|
-
click.secho(
|
|
171
|
+
click.secho(
|
|
172
|
+
" ✗ Failed to configure. Run 'souleyez setup --fix-permissions'",
|
|
173
|
+
fg="red",
|
|
174
|
+
)
|
|
163
175
|
except Exception as e:
|
|
164
176
|
click.secho(f" ✗ Error: {e}", fg="red")
|
|
165
177
|
click.echo()
|
|
@@ -173,16 +185,18 @@ def _check_privileged_tools():
|
|
|
173
185
|
|
|
174
186
|
|
|
175
187
|
@click.group()
|
|
176
|
-
@click.version_option(version=
|
|
188
|
+
@click.version_option(version="2.43.34")
|
|
177
189
|
def cli():
|
|
178
190
|
"""SoulEyez - AI-Powered Pentesting Platform by CyberSoul Security"""
|
|
179
191
|
from souleyez.log_config import init_logging
|
|
192
|
+
|
|
180
193
|
init_logging()
|
|
181
194
|
|
|
182
195
|
# Initialize auth system for CLI commands
|
|
183
196
|
try:
|
|
184
197
|
from souleyez.auth import init_auth
|
|
185
198
|
from souleyez.storage.database import get_db
|
|
199
|
+
|
|
186
200
|
init_auth(get_db().db_path)
|
|
187
201
|
except Exception:
|
|
188
202
|
pass # Auth not required for all commands (e.g., --help, --version)
|
|
@@ -199,6 +213,7 @@ def cli():
|
|
|
199
213
|
# Ensure user has local copy of wordlists
|
|
200
214
|
try:
|
|
201
215
|
from souleyez.wordlists import ensure_user_wordlists
|
|
216
|
+
|
|
202
217
|
ensure_user_wordlists()
|
|
203
218
|
except ImportError:
|
|
204
219
|
pass
|
|
@@ -211,9 +226,17 @@ def interactive():
|
|
|
211
226
|
|
|
212
227
|
|
|
213
228
|
@cli.command()
|
|
214
|
-
@click.option(
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
@click.option(
|
|
230
|
+
"--check", "-c", is_flag=True, help="Only check tool status, don't install"
|
|
231
|
+
)
|
|
232
|
+
@click.option(
|
|
233
|
+
"--install-all", "-a", is_flag=True, help="Install all missing tools automatically"
|
|
234
|
+
)
|
|
235
|
+
@click.option(
|
|
236
|
+
"--fix-permissions",
|
|
237
|
+
is_flag=True,
|
|
238
|
+
help="Configure tools for privileged operations (nmap, responder)",
|
|
239
|
+
)
|
|
217
240
|
def setup(check, install_all, fix_permissions):
|
|
218
241
|
"""Install and configure pentesting tools for your system.
|
|
219
242
|
|
|
@@ -236,22 +259,23 @@ def setup(check, install_all, fix_permissions):
|
|
|
236
259
|
_fix_tool_permissions()
|
|
237
260
|
return
|
|
238
261
|
from souleyez.ui.tool_setup import run_tool_setup
|
|
262
|
+
|
|
239
263
|
run_tool_setup(check_only=check, install_all=install_all)
|
|
240
264
|
|
|
241
265
|
|
|
242
266
|
# Privileged tools configuration - binary tools
|
|
243
|
-
PRIVILEGED_BINARY_TOOLS = [
|
|
267
|
+
PRIVILEGED_BINARY_TOOLS = ["nmap"]
|
|
244
268
|
|
|
245
269
|
# Privileged tools configuration - script-based tools
|
|
246
270
|
PRIVILEGED_SCRIPT_TOOLS = {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
str(Path.home() /
|
|
271
|
+
"responder": {
|
|
272
|
+
"interpreter": "/usr/bin/python3",
|
|
273
|
+
"script_paths": [
|
|
274
|
+
"/usr/share/responder/Responder.py",
|
|
275
|
+
"/opt/Responder/Responder.py",
|
|
276
|
+
str(Path.home() / "tools/Responder/Responder.py"),
|
|
253
277
|
],
|
|
254
|
-
|
|
278
|
+
"description": "LLMNR/NBT-NS credential capture",
|
|
255
279
|
}
|
|
256
280
|
}
|
|
257
281
|
|
|
@@ -269,7 +293,7 @@ def _fix_tool_permissions():
|
|
|
269
293
|
import subprocess
|
|
270
294
|
|
|
271
295
|
click.echo()
|
|
272
|
-
click.echo(click.style(" PRIVILEGED TOOL SETUP", bold=True, fg=
|
|
296
|
+
click.echo(click.style(" PRIVILEGED TOOL SETUP", bold=True, fg="cyan"))
|
|
273
297
|
click.echo(" " + "─" * 50)
|
|
274
298
|
click.echo()
|
|
275
299
|
click.echo(" Configuring passwordless sudo for security tools.")
|
|
@@ -293,11 +317,9 @@ def _fix_tool_permissions():
|
|
|
293
317
|
|
|
294
318
|
# Check if passwordless sudo already works
|
|
295
319
|
try:
|
|
296
|
-
subprocess.run([
|
|
320
|
+
subprocess.run(["sudo", "-k"], capture_output=True, timeout=5)
|
|
297
321
|
result = subprocess.run(
|
|
298
|
-
[
|
|
299
|
-
capture_output=True,
|
|
300
|
-
timeout=5
|
|
322
|
+
["sudo", "-n", tool_path, "--version"], capture_output=True, timeout=5
|
|
301
323
|
)
|
|
302
324
|
if result.returncode == 0:
|
|
303
325
|
binary_tools_already_fixed.append(tool_name)
|
|
@@ -308,25 +330,29 @@ def _fix_tool_permissions():
|
|
|
308
330
|
|
|
309
331
|
# Check script-based tools
|
|
310
332
|
for tool_name, tool_info in PRIVILEGED_SCRIPT_TOOLS.items():
|
|
311
|
-
script_path = _find_script_path(tool_info[
|
|
333
|
+
script_path = _find_script_path(tool_info["script_paths"])
|
|
312
334
|
if not script_path:
|
|
313
335
|
script_tools_not_installed.append(tool_name)
|
|
314
336
|
continue
|
|
315
337
|
|
|
316
|
-
interpreter = tool_info[
|
|
338
|
+
interpreter = tool_info["interpreter"]
|
|
317
339
|
try:
|
|
318
|
-
subprocess.run([
|
|
340
|
+
subprocess.run(["sudo", "-k"], capture_output=True, timeout=5)
|
|
319
341
|
result = subprocess.run(
|
|
320
|
-
[
|
|
342
|
+
["sudo", "-n", interpreter, script_path, "--help"],
|
|
321
343
|
capture_output=True,
|
|
322
|
-
timeout=5
|
|
344
|
+
timeout=5,
|
|
323
345
|
)
|
|
324
346
|
if result.returncode == 0:
|
|
325
347
|
script_tools_already_fixed.append(tool_name)
|
|
326
348
|
else:
|
|
327
|
-
script_tools_to_fix.append(
|
|
349
|
+
script_tools_to_fix.append(
|
|
350
|
+
(tool_name, interpreter, script_path, tool_info["description"])
|
|
351
|
+
)
|
|
328
352
|
except Exception:
|
|
329
|
-
script_tools_to_fix.append(
|
|
353
|
+
script_tools_to_fix.append(
|
|
354
|
+
(tool_name, interpreter, script_path, tool_info["description"])
|
|
355
|
+
)
|
|
330
356
|
|
|
331
357
|
# Show status
|
|
332
358
|
click.echo(" " + click.style("STATUS:", bold=True))
|
|
@@ -339,10 +365,14 @@ def _fix_tool_permissions():
|
|
|
339
365
|
click.echo(f" {click.style('✓', fg='green')} {name} - already configured")
|
|
340
366
|
|
|
341
367
|
for name in binary_tools_not_installed:
|
|
342
|
-
click.echo(
|
|
368
|
+
click.echo(
|
|
369
|
+
f" {click.style('○', fg='yellow')} {name} - not installed (skipping)"
|
|
370
|
+
)
|
|
343
371
|
|
|
344
372
|
for name in script_tools_not_installed:
|
|
345
|
-
click.echo(
|
|
373
|
+
click.echo(
|
|
374
|
+
f" {click.style('○', fg='yellow')} {name} - not installed (skipping)"
|
|
375
|
+
)
|
|
346
376
|
|
|
347
377
|
for name, _ in binary_tools_to_fix:
|
|
348
378
|
click.echo(f" {click.style('✗', fg='red')} {name} - needs configuration")
|
|
@@ -353,7 +383,9 @@ def _fix_tool_permissions():
|
|
|
353
383
|
click.echo()
|
|
354
384
|
|
|
355
385
|
if not binary_tools_to_fix and not script_tools_to_fix:
|
|
356
|
-
click.echo(
|
|
386
|
+
click.echo(
|
|
387
|
+
click.style(" All installed tools are already configured!", fg="green")
|
|
388
|
+
)
|
|
357
389
|
return
|
|
358
390
|
|
|
359
391
|
click.echo(" This requires sudo to configure /etc/sudoers.d/")
|
|
@@ -406,12 +438,22 @@ def _fix_tool_permissions():
|
|
|
406
438
|
click.echo(f" {click.style('✗', fg='red')} Error: {e}")
|
|
407
439
|
|
|
408
440
|
click.echo()
|
|
409
|
-
click.echo(
|
|
441
|
+
click.echo(
|
|
442
|
+
click.style(" Done! Privileged scans now work automatically.", fg="green")
|
|
443
|
+
)
|
|
410
444
|
|
|
411
445
|
|
|
412
446
|
@cli.command()
|
|
413
|
-
@click.option(
|
|
414
|
-
|
|
447
|
+
@click.option(
|
|
448
|
+
"--follow", "-f", type=int, default=None, help="Follow live output of job ID"
|
|
449
|
+
)
|
|
450
|
+
@click.option(
|
|
451
|
+
"--refresh",
|
|
452
|
+
"-r",
|
|
453
|
+
type=int,
|
|
454
|
+
default=15,
|
|
455
|
+
help="Refresh interval in seconds (default: 15)",
|
|
456
|
+
)
|
|
415
457
|
@require_password
|
|
416
458
|
def dashboard(follow, refresh):
|
|
417
459
|
"""Launch live dashboard with real-time job status and findings."""
|
|
@@ -443,22 +485,26 @@ def engagement_list():
|
|
|
443
485
|
em = EngagementManager()
|
|
444
486
|
engagements = em.list()
|
|
445
487
|
current = em.get_current()
|
|
446
|
-
|
|
488
|
+
|
|
447
489
|
if not engagements:
|
|
448
|
-
click.echo(
|
|
490
|
+
click.echo(
|
|
491
|
+
"No engagements found. Create one with: souleyez engagement create <name>"
|
|
492
|
+
)
|
|
449
493
|
return
|
|
450
|
-
|
|
494
|
+
|
|
451
495
|
click.echo("\n" + "=" * 80)
|
|
452
496
|
click.echo("ENGAGEMENTS")
|
|
453
497
|
click.echo("=" * 80)
|
|
454
|
-
|
|
498
|
+
|
|
455
499
|
for eng in engagements:
|
|
456
|
-
marker = "* " if current and eng[
|
|
457
|
-
stats = em.stats(eng[
|
|
458
|
-
click.echo(
|
|
459
|
-
|
|
500
|
+
marker = "* " if current and eng["id"] == current["id"] else " "
|
|
501
|
+
stats = em.stats(eng["id"])
|
|
502
|
+
click.echo(
|
|
503
|
+
f"{marker}{eng['name']:<20} | Hosts: {stats['hosts']:>3} | Services: {stats['services']:>3} | Findings: {stats['findings']:>3}"
|
|
504
|
+
)
|
|
505
|
+
if eng.get("description"):
|
|
460
506
|
click.echo(f" └─ {eng['description']}")
|
|
461
|
-
|
|
507
|
+
|
|
462
508
|
click.echo("=" * 80)
|
|
463
509
|
if current:
|
|
464
510
|
click.echo(f"Current: {current['name']}")
|
|
@@ -484,13 +530,13 @@ def engagement_current():
|
|
|
484
530
|
"""Show current engagement."""
|
|
485
531
|
em = EngagementManager()
|
|
486
532
|
current = em.get_current()
|
|
487
|
-
|
|
533
|
+
|
|
488
534
|
if not current:
|
|
489
535
|
click.echo("No engagement selected")
|
|
490
536
|
return
|
|
491
|
-
|
|
492
|
-
stats = em.stats(current[
|
|
493
|
-
|
|
537
|
+
|
|
538
|
+
stats = em.stats(current["id"])
|
|
539
|
+
|
|
494
540
|
click.echo("\n" + "=" * 60)
|
|
495
541
|
click.echo(f"Current Engagement: {current['name']}")
|
|
496
542
|
click.echo("=" * 60)
|
|
@@ -511,22 +557,22 @@ def engagement_delete(name, force):
|
|
|
511
557
|
"""Delete an engagement and all its data."""
|
|
512
558
|
em = EngagementManager()
|
|
513
559
|
eng = em.get(name)
|
|
514
|
-
|
|
560
|
+
|
|
515
561
|
if not eng:
|
|
516
562
|
click.echo(f"✗ Workspace '{name}' not found", err=True)
|
|
517
563
|
return
|
|
518
|
-
|
|
564
|
+
|
|
519
565
|
if not force:
|
|
520
|
-
stats = em.stats(eng[
|
|
566
|
+
stats = em.stats(eng["id"])
|
|
521
567
|
click.echo(f"\nWarning: This will delete engagement '{name}' and:")
|
|
522
568
|
click.echo(f" - {stats['hosts']} hosts")
|
|
523
569
|
click.echo(f" - {stats['services']} services")
|
|
524
570
|
click.echo(f" - {stats['findings']} findings")
|
|
525
|
-
|
|
571
|
+
|
|
526
572
|
if not click.confirm("\nAre you sure?"):
|
|
527
573
|
click.echo("Cancelled")
|
|
528
574
|
return
|
|
529
|
-
|
|
575
|
+
|
|
530
576
|
if em.delete(name):
|
|
531
577
|
click.echo(f"✓ Deleted workspace '{name}'")
|
|
532
578
|
else:
|
|
@@ -535,60 +581,62 @@ def engagement_delete(name, force):
|
|
|
535
581
|
|
|
536
582
|
@engagement.command("delete-all")
|
|
537
583
|
@click.option("--force", "-f", is_flag=True, help="Skip confirmation")
|
|
538
|
-
@click.option(
|
|
584
|
+
@click.option(
|
|
585
|
+
"--keep-current", is_flag=True, help="Keep the currently active engagement"
|
|
586
|
+
)
|
|
539
587
|
def engagement_delete_all(force, keep_current):
|
|
540
588
|
"""Delete all engagements (except optionally the current one)."""
|
|
541
589
|
em = EngagementManager()
|
|
542
590
|
all_engagements = em.list()
|
|
543
|
-
|
|
591
|
+
|
|
544
592
|
if not all_engagements:
|
|
545
593
|
click.echo("No engagements to delete")
|
|
546
594
|
return
|
|
547
|
-
|
|
595
|
+
|
|
548
596
|
# Get current engagement if we're keeping it
|
|
549
597
|
current = em.get_current() if keep_current else None
|
|
550
|
-
|
|
598
|
+
|
|
551
599
|
# Filter engagements to delete
|
|
552
600
|
to_delete = []
|
|
553
601
|
for eng in all_engagements:
|
|
554
|
-
if keep_current and current and eng[
|
|
602
|
+
if keep_current and current and eng["id"] == current["id"]:
|
|
555
603
|
continue
|
|
556
604
|
to_delete.append(eng)
|
|
557
|
-
|
|
605
|
+
|
|
558
606
|
if not to_delete:
|
|
559
607
|
click.echo("No engagements to delete")
|
|
560
608
|
return
|
|
561
|
-
|
|
609
|
+
|
|
562
610
|
# Show what will be deleted
|
|
563
611
|
click.echo(f"\n⚠️ Warning: This will delete {len(to_delete)} engagement(s):")
|
|
564
612
|
for eng in to_delete[:10]: # Show first 10
|
|
565
613
|
click.echo(f" - {eng['name']}")
|
|
566
|
-
|
|
614
|
+
|
|
567
615
|
if len(to_delete) > 10:
|
|
568
616
|
click.echo(f" ... and {len(to_delete) - 10} more")
|
|
569
|
-
|
|
617
|
+
|
|
570
618
|
if keep_current and current:
|
|
571
619
|
click.echo(f"\n✓ Will keep current engagement: {current['name']}")
|
|
572
|
-
|
|
620
|
+
|
|
573
621
|
# Confirmation
|
|
574
622
|
if not force:
|
|
575
623
|
click.echo()
|
|
576
624
|
if not click.confirm(f"Delete {len(to_delete)} engagement(s)?", default=False):
|
|
577
625
|
click.echo("Cancelled")
|
|
578
626
|
return
|
|
579
|
-
|
|
627
|
+
|
|
580
628
|
# Delete engagements
|
|
581
629
|
deleted = 0
|
|
582
630
|
failed = 0
|
|
583
|
-
|
|
631
|
+
|
|
584
632
|
click.echo()
|
|
585
|
-
with click.progressbar(to_delete, label=
|
|
633
|
+
with click.progressbar(to_delete, label="Deleting engagements") as bar:
|
|
586
634
|
for eng in bar:
|
|
587
|
-
if em.delete(eng[
|
|
635
|
+
if em.delete(eng["name"]):
|
|
588
636
|
deleted += 1
|
|
589
637
|
else:
|
|
590
638
|
failed += 1
|
|
591
|
-
|
|
639
|
+
|
|
592
640
|
click.echo()
|
|
593
641
|
click.echo(f"✓ Deleted {deleted} engagement(s)")
|
|
594
642
|
if failed > 0:
|
|
@@ -597,10 +645,12 @@ def engagement_delete_all(force, keep_current):
|
|
|
597
645
|
|
|
598
646
|
# Register engagement team subcommand
|
|
599
647
|
from souleyez.commands.engagement import team
|
|
648
|
+
|
|
600
649
|
engagement.add_command(team)
|
|
601
650
|
|
|
602
651
|
# Register audit commands
|
|
603
652
|
from souleyez.commands.audit import audit
|
|
653
|
+
|
|
604
654
|
cli.add_command(audit)
|
|
605
655
|
|
|
606
656
|
|
|
@@ -608,6 +658,7 @@ cli.add_command(audit)
|
|
|
608
658
|
# SCOPE MANAGEMENT
|
|
609
659
|
# ============================================================================
|
|
610
660
|
|
|
661
|
+
|
|
611
662
|
@cli.group()
|
|
612
663
|
def scope():
|
|
613
664
|
"""Engagement scope management - define and enforce target boundaries."""
|
|
@@ -621,7 +672,9 @@ def scope():
|
|
|
621
672
|
@click.option("--url", help="Add URL (e.g., https://app.example.com)")
|
|
622
673
|
@click.option("--hostname", help="Add specific hostname or IP")
|
|
623
674
|
@click.option("--exclude", is_flag=True, help="Add as exclusion (deny rule)")
|
|
624
|
-
@click.option(
|
|
675
|
+
@click.option(
|
|
676
|
+
"--description", "-d", default="", help="Description for this scope entry"
|
|
677
|
+
)
|
|
625
678
|
def scope_add(engagement_name, cidr, domain, url, hostname, exclude, description):
|
|
626
679
|
"""Add a scope entry to an engagement."""
|
|
627
680
|
from souleyez.security.scope_validator import ScopeManager
|
|
@@ -636,24 +689,27 @@ def scope_add(engagement_name, cidr, domain, url, hostname, exclude, description
|
|
|
636
689
|
|
|
637
690
|
# Determine scope type and value
|
|
638
691
|
if cidr:
|
|
639
|
-
scope_type, value =
|
|
692
|
+
scope_type, value = "cidr", cidr
|
|
640
693
|
elif domain:
|
|
641
|
-
scope_type, value =
|
|
694
|
+
scope_type, value = "domain", domain
|
|
642
695
|
elif url:
|
|
643
|
-
scope_type, value =
|
|
696
|
+
scope_type, value = "url", url
|
|
644
697
|
elif hostname:
|
|
645
|
-
scope_type, value =
|
|
698
|
+
scope_type, value = "hostname", hostname
|
|
646
699
|
else:
|
|
647
|
-
click.echo(
|
|
700
|
+
click.echo(
|
|
701
|
+
"Error: Must specify one of --cidr, --domain, --url, or --hostname",
|
|
702
|
+
err=True,
|
|
703
|
+
)
|
|
648
704
|
return
|
|
649
705
|
|
|
650
706
|
try:
|
|
651
707
|
scope_id = manager.add_scope(
|
|
652
|
-
engagement_id=eng[
|
|
708
|
+
engagement_id=eng["id"],
|
|
653
709
|
scope_type=scope_type,
|
|
654
710
|
value=value,
|
|
655
711
|
is_excluded=exclude,
|
|
656
|
-
description=description
|
|
712
|
+
description=description,
|
|
657
713
|
)
|
|
658
714
|
action = "exclusion" if exclude else "scope entry"
|
|
659
715
|
click.echo(f"Added {action}: {scope_type}={value} (id={scope_id})")
|
|
@@ -674,8 +730,8 @@ def scope_list(engagement_name):
|
|
|
674
730
|
return
|
|
675
731
|
|
|
676
732
|
manager = ScopeManager()
|
|
677
|
-
validator = ScopeValidator(eng[
|
|
678
|
-
entries = manager.list_scope(eng[
|
|
733
|
+
validator = ScopeValidator(eng["id"])
|
|
734
|
+
entries = manager.list_scope(eng["id"])
|
|
679
735
|
enforcement = validator.get_enforcement_mode()
|
|
680
736
|
|
|
681
737
|
click.echo(f"\nScope for '{engagement_name}' (enforcement: {enforcement})")
|
|
@@ -689,9 +745,11 @@ def scope_list(engagement_name):
|
|
|
689
745
|
click.echo("-" * 70)
|
|
690
746
|
|
|
691
747
|
for entry in entries:
|
|
692
|
-
excluded = "EXCLUDE" if entry.get(
|
|
693
|
-
click.echo(
|
|
694
|
-
|
|
748
|
+
excluded = "EXCLUDE" if entry.get("is_excluded") else ""
|
|
749
|
+
click.echo(
|
|
750
|
+
f"{entry['id']:<5} {entry['scope_type']:<10} {entry['value']:<35} {excluded:<10}"
|
|
751
|
+
)
|
|
752
|
+
if entry.get("description"):
|
|
695
753
|
click.echo(f" {entry['description']}")
|
|
696
754
|
|
|
697
755
|
click.echo()
|
|
@@ -719,7 +777,7 @@ def scope_remove(engagement_name, scope_id):
|
|
|
719
777
|
|
|
720
778
|
@scope.command("enforcement")
|
|
721
779
|
@click.argument("engagement_name")
|
|
722
|
-
@click.argument("mode", type=click.Choice([
|
|
780
|
+
@click.argument("mode", type=click.Choice(["off", "warn", "block"]))
|
|
723
781
|
def scope_enforcement(engagement_name, mode):
|
|
724
782
|
"""Set enforcement mode for an engagement.
|
|
725
783
|
|
|
@@ -737,7 +795,7 @@ def scope_enforcement(engagement_name, mode):
|
|
|
737
795
|
return
|
|
738
796
|
|
|
739
797
|
manager = ScopeManager()
|
|
740
|
-
if manager.set_enforcement(eng[
|
|
798
|
+
if manager.set_enforcement(eng["id"], mode):
|
|
741
799
|
click.echo(f"Enforcement mode set to '{mode}' for '{engagement_name}'")
|
|
742
800
|
else:
|
|
743
801
|
click.echo("Error: Failed to set enforcement mode", err=True)
|
|
@@ -756,7 +814,7 @@ def scope_validate(engagement_name, target):
|
|
|
756
814
|
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
757
815
|
return
|
|
758
816
|
|
|
759
|
-
validator = ScopeValidator(eng[
|
|
817
|
+
validator = ScopeValidator(eng["id"])
|
|
760
818
|
result = validator.validate_target(target)
|
|
761
819
|
|
|
762
820
|
if result.is_in_scope:
|
|
@@ -783,7 +841,7 @@ def scope_revalidate(engagement_name):
|
|
|
783
841
|
return
|
|
784
842
|
|
|
785
843
|
hm = HostManager()
|
|
786
|
-
result = hm.revalidate_scope_status(eng[
|
|
844
|
+
result = hm.revalidate_scope_status(eng["id"])
|
|
787
845
|
|
|
788
846
|
click.echo(f"Revalidated hosts for '{engagement_name}':")
|
|
789
847
|
click.echo(f" Updated: {result['updated']}")
|
|
@@ -805,7 +863,7 @@ def scope_log(engagement_name, limit):
|
|
|
805
863
|
return
|
|
806
864
|
|
|
807
865
|
manager = ScopeManager()
|
|
808
|
-
log_entries = manager.get_validation_log(eng[
|
|
866
|
+
log_entries = manager.get_validation_log(eng["id"], limit)
|
|
809
867
|
|
|
810
868
|
click.echo(f"\nScope validation log for '{engagement_name}' (last {limit})")
|
|
811
869
|
click.echo("=" * 80)
|
|
@@ -818,10 +876,10 @@ def scope_log(engagement_name, limit):
|
|
|
818
876
|
click.echo("-" * 80)
|
|
819
877
|
|
|
820
878
|
for entry in log_entries:
|
|
821
|
-
timestamp = entry.get(
|
|
822
|
-
target = entry.get(
|
|
823
|
-
result = entry.get(
|
|
824
|
-
action = entry.get(
|
|
879
|
+
timestamp = entry.get("created_at", "")[:19] # Trim to datetime
|
|
880
|
+
target = entry.get("target", "")[:24]
|
|
881
|
+
result = entry.get("validation_result", "")
|
|
882
|
+
action = entry.get("action_taken", "")
|
|
825
883
|
click.echo(f"{timestamp:<20} {target:<25} {result:<12} {action:<10}")
|
|
826
884
|
|
|
827
885
|
|
|
@@ -839,7 +897,7 @@ def jobs():
|
|
|
839
897
|
def jobs_enqueue(tool, target, args, label):
|
|
840
898
|
"""Enqueue a background job."""
|
|
841
899
|
args_list = args.split() if args else []
|
|
842
|
-
|
|
900
|
+
|
|
843
901
|
try:
|
|
844
902
|
job_id = enqueue_job(tool, target, args_list, label)
|
|
845
903
|
click.echo(f"✓ Enqueued job {job_id}: {tool} {target}")
|
|
@@ -855,30 +913,32 @@ def jobs_enqueue(tool, target, args, label):
|
|
|
855
913
|
def jobs_list(limit, status):
|
|
856
914
|
"""List background jobs."""
|
|
857
915
|
jobs_data = list_jobs(limit=limit)
|
|
858
|
-
|
|
916
|
+
|
|
859
917
|
if status:
|
|
860
|
-
jobs_data = [j for j in jobs_data if j.get(
|
|
861
|
-
|
|
918
|
+
jobs_data = [j for j in jobs_data if j.get("status") == status]
|
|
919
|
+
|
|
862
920
|
if not jobs_data:
|
|
863
921
|
click.echo("No jobs found")
|
|
864
922
|
return
|
|
865
|
-
|
|
923
|
+
|
|
866
924
|
click.echo("\n" + "=" * 100)
|
|
867
|
-
click.echo(
|
|
925
|
+
click.echo(
|
|
926
|
+
f"{'ID':<5} {'Tool':<12} {'Target':<25} {'Status':<10} {'Label':<20} {'Created':<20}"
|
|
927
|
+
)
|
|
868
928
|
click.echo("=" * 100)
|
|
869
|
-
|
|
929
|
+
|
|
870
930
|
for job in jobs_data:
|
|
871
|
-
status_val = job.get(
|
|
931
|
+
status_val = job.get("status", "N/A")
|
|
872
932
|
|
|
873
933
|
# Color code status
|
|
874
|
-
if status_val ==
|
|
875
|
-
status_str = click.style(f"{status_val:<10}", fg=
|
|
876
|
-
elif status_val ==
|
|
877
|
-
status_str = click.style(f"{status_val:<10}", fg=
|
|
878
|
-
elif status_val in (
|
|
879
|
-
status_str = click.style(f"{status_val:<10}", fg=
|
|
880
|
-
elif status_val ==
|
|
881
|
-
status_str = click.style(f"{status_val:<10}", fg=
|
|
934
|
+
if status_val == "done":
|
|
935
|
+
status_str = click.style(f"{status_val:<10}", fg="green")
|
|
936
|
+
elif status_val == "running":
|
|
937
|
+
status_str = click.style(f"{status_val:<10}", fg="yellow")
|
|
938
|
+
elif status_val in ("error", "failed"):
|
|
939
|
+
status_str = click.style(f"{status_val:<10}", fg="red")
|
|
940
|
+
elif status_val == "killed":
|
|
941
|
+
status_str = click.style(f"{status_val:<10}", fg="magenta")
|
|
882
942
|
else:
|
|
883
943
|
status_str = f"{status_val:<10}"
|
|
884
944
|
|
|
@@ -890,7 +950,7 @@ def jobs_list(limit, status):
|
|
|
890
950
|
f"{job.get('label', '')[:19]:<20} "
|
|
891
951
|
f"{job.get('created_at', 'N/A'):<20}"
|
|
892
952
|
)
|
|
893
|
-
|
|
953
|
+
|
|
894
954
|
click.echo("=" * 100 + "\n")
|
|
895
955
|
|
|
896
956
|
|
|
@@ -899,11 +959,11 @@ def jobs_list(limit, status):
|
|
|
899
959
|
def jobs_get(job_id):
|
|
900
960
|
"""Get job details."""
|
|
901
961
|
job = get_job(job_id)
|
|
902
|
-
|
|
962
|
+
|
|
903
963
|
if not job:
|
|
904
964
|
click.echo(f"✗ Job {job_id} not found", err=True)
|
|
905
965
|
return
|
|
906
|
-
|
|
966
|
+
|
|
907
967
|
click.echo("\n" + "=" * 60)
|
|
908
968
|
click.echo(f"Job {job_id}")
|
|
909
969
|
click.echo("=" * 60)
|
|
@@ -916,10 +976,10 @@ def jobs_get(job_id):
|
|
|
916
976
|
click.echo(f"Started: {job.get('started_at', 'N/A')}")
|
|
917
977
|
click.echo(f"Finished: {job.get('finished_at', 'N/A')}")
|
|
918
978
|
click.echo(f"Log: {job.get('log', 'N/A')}")
|
|
919
|
-
|
|
920
|
-
if job.get(
|
|
979
|
+
|
|
980
|
+
if job.get("error"):
|
|
921
981
|
click.echo(f"Error: {job['error']}")
|
|
922
|
-
|
|
982
|
+
|
|
923
983
|
click.echo("=" * 60 + "\n")
|
|
924
984
|
|
|
925
985
|
|
|
@@ -942,32 +1002,32 @@ def jobs_show(job_id):
|
|
|
942
1002
|
click.echo(f"Tool: {job.get('tool', 'N/A')}")
|
|
943
1003
|
click.echo(f"Target: {job.get('target', 'N/A')}")
|
|
944
1004
|
click.echo(f"Args: {' '.join(job.get('args', []))}")
|
|
945
|
-
if job.get(
|
|
1005
|
+
if job.get("label"):
|
|
946
1006
|
click.echo(f"Label: {job['label']}")
|
|
947
1007
|
click.echo(f"Status: {job.get('status', 'N/A')}")
|
|
948
1008
|
click.echo(f"Created: {job.get('created_at', 'N/A')}")
|
|
949
|
-
if job.get(
|
|
1009
|
+
if job.get("started_at"):
|
|
950
1010
|
click.echo(f"Started: {job['started_at']}")
|
|
951
|
-
if job.get(
|
|
1011
|
+
if job.get("finished_at"):
|
|
952
1012
|
click.echo(f"Finished: {job['finished_at']}")
|
|
953
1013
|
|
|
954
|
-
if job.get(
|
|
1014
|
+
if job.get("error"):
|
|
955
1015
|
click.echo(f"Error: {job['error']}")
|
|
956
1016
|
|
|
957
1017
|
click.echo()
|
|
958
1018
|
|
|
959
1019
|
# Show log output
|
|
960
|
-
log_path = job.get(
|
|
1020
|
+
log_path = job.get("log")
|
|
961
1021
|
if log_path and os.path.exists(log_path):
|
|
962
|
-
click.echo(click.style("LOG OUTPUT:", bold=True, fg=
|
|
1022
|
+
click.echo(click.style("LOG OUTPUT:", bold=True, fg="cyan"))
|
|
963
1023
|
click.echo("-" * 70)
|
|
964
1024
|
|
|
965
1025
|
try:
|
|
966
|
-
with open(log_path,
|
|
1026
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
967
1027
|
content = f.read()
|
|
968
1028
|
|
|
969
1029
|
# Show last 100 lines
|
|
970
|
-
lines = content.split(
|
|
1030
|
+
lines = content.split("\n")
|
|
971
1031
|
if len(lines) > 100:
|
|
972
1032
|
click.echo(f"... (showing last 100 of {len(lines)} lines)\n")
|
|
973
1033
|
lines = lines[-100:]
|
|
@@ -976,9 +1036,9 @@ def jobs_show(job_id):
|
|
|
976
1036
|
click.echo(line)
|
|
977
1037
|
|
|
978
1038
|
except Exception as e:
|
|
979
|
-
click.echo(click.style(f"Error reading log: {e}", fg=
|
|
1039
|
+
click.echo(click.style(f"Error reading log: {e}", fg="red"))
|
|
980
1040
|
else:
|
|
981
|
-
click.echo(click.style("No log file available", fg=
|
|
1041
|
+
click.echo(click.style("No log file available", fg="yellow"))
|
|
982
1042
|
|
|
983
1043
|
click.echo("\n" + "=" * 70 + "\n")
|
|
984
1044
|
|
|
@@ -989,19 +1049,19 @@ def jobs_show(job_id):
|
|
|
989
1049
|
def jobs_tail(job_id, follow):
|
|
990
1050
|
"""Tail job log file."""
|
|
991
1051
|
import subprocess
|
|
992
|
-
|
|
1052
|
+
|
|
993
1053
|
job = get_job(job_id)
|
|
994
|
-
|
|
1054
|
+
|
|
995
1055
|
if not job:
|
|
996
1056
|
click.echo(f"✗ Job {job_id} not found", err=True)
|
|
997
1057
|
return
|
|
998
|
-
|
|
999
|
-
log_path = job.get(
|
|
1000
|
-
|
|
1058
|
+
|
|
1059
|
+
log_path = job.get("log")
|
|
1060
|
+
|
|
1001
1061
|
if not log_path or not os.path.exists(log_path):
|
|
1002
1062
|
click.echo(f"✗ Log file not found: {log_path}", err=True)
|
|
1003
1063
|
return
|
|
1004
|
-
|
|
1064
|
+
|
|
1005
1065
|
try:
|
|
1006
1066
|
if follow:
|
|
1007
1067
|
subprocess.run(["tail", "-f", log_path])
|
|
@@ -1026,20 +1086,20 @@ def jobs_kill(job_id, force):
|
|
|
1026
1086
|
click.echo(f"✗ Job {job_id} not found", err=True)
|
|
1027
1087
|
return
|
|
1028
1088
|
|
|
1029
|
-
status = job.get(
|
|
1030
|
-
|
|
1089
|
+
status = job.get("status")
|
|
1090
|
+
|
|
1031
1091
|
# Allow killing queued, running, and error jobs
|
|
1032
|
-
if status not in [
|
|
1092
|
+
if status not in ["queued", "running", "error"]:
|
|
1033
1093
|
click.echo(f"✗ Job {job_id} cannot be killed (status: {status})", err=True)
|
|
1034
1094
|
return
|
|
1035
1095
|
|
|
1036
1096
|
if kill_job(job_id):
|
|
1037
|
-
if status ==
|
|
1038
|
-
click.secho(f"✓ Job {job_id} removed from queue", fg=
|
|
1039
|
-
elif status ==
|
|
1040
|
-
click.secho(f"✓ Job {job_id} marked as killed", fg=
|
|
1097
|
+
if status == "queued":
|
|
1098
|
+
click.secho(f"✓ Job {job_id} removed from queue", fg="green")
|
|
1099
|
+
elif status == "error":
|
|
1100
|
+
click.secho(f"✓ Job {job_id} marked as killed", fg="green")
|
|
1041
1101
|
else:
|
|
1042
|
-
click.secho(f"✓ Job {job_id} killed successfully", fg=
|
|
1102
|
+
click.secho(f"✓ Job {job_id} killed successfully", fg="green")
|
|
1043
1103
|
else:
|
|
1044
1104
|
click.echo(f"✗ Failed to kill job {job_id}", err=True)
|
|
1045
1105
|
|
|
@@ -1047,29 +1107,35 @@ def jobs_kill(job_id, force):
|
|
|
1047
1107
|
@jobs.command("sanitize")
|
|
1048
1108
|
@click.argument("job_id", type=int, required=False)
|
|
1049
1109
|
@click.option("--all", is_flag=True, help="Sanitize all job logs")
|
|
1050
|
-
@click.option(
|
|
1110
|
+
@click.option(
|
|
1111
|
+
"--dry-run", is_flag=True, help="Show what would be redacted without modifying logs"
|
|
1112
|
+
)
|
|
1051
1113
|
def jobs_sanitize(job_id, all, dry_run):
|
|
1052
1114
|
"""Sanitize job logs by redacting credentials."""
|
|
1053
1115
|
from souleyez.engine.log_sanitizer import LogSanitizer
|
|
1054
1116
|
from souleyez.storage.crypto import CryptoManager
|
|
1055
|
-
|
|
1117
|
+
|
|
1056
1118
|
crypto_mgr = CryptoManager()
|
|
1057
1119
|
if not crypto_mgr.is_enabled():
|
|
1058
|
-
click.secho("⚠️ Warning: Encryption is not enabled.", fg=
|
|
1059
|
-
click.echo(
|
|
1120
|
+
click.secho("⚠️ Warning: Encryption is not enabled.", fg="yellow")
|
|
1121
|
+
click.echo(
|
|
1122
|
+
" Sanitization is primarily useful when encryption is enabled to prevent"
|
|
1123
|
+
)
|
|
1060
1124
|
click.echo(" plaintext credentials in logs while encrypted in database.")
|
|
1061
1125
|
if not click.confirm("Continue anyway?"):
|
|
1062
1126
|
return
|
|
1063
|
-
|
|
1127
|
+
|
|
1064
1128
|
if all:
|
|
1065
1129
|
jobs_to_sanitize = list_jobs(limit=10000)
|
|
1066
1130
|
if not jobs_to_sanitize:
|
|
1067
1131
|
click.echo("No jobs found")
|
|
1068
1132
|
return
|
|
1069
|
-
|
|
1133
|
+
|
|
1070
1134
|
click.echo(f"Found {len(jobs_to_sanitize)} job(s)")
|
|
1071
|
-
|
|
1072
|
-
if not dry_run and not click.confirm(
|
|
1135
|
+
|
|
1136
|
+
if not dry_run and not click.confirm(
|
|
1137
|
+
f"Sanitize logs for all {len(jobs_to_sanitize)} jobs?"
|
|
1138
|
+
):
|
|
1073
1139
|
return
|
|
1074
1140
|
elif job_id:
|
|
1075
1141
|
job = get_job(job_id)
|
|
@@ -1082,60 +1148,62 @@ def jobs_sanitize(job_id, all, dry_run):
|
|
|
1082
1148
|
click.echo("Usage: souleyez jobs sanitize <job_id>")
|
|
1083
1149
|
click.echo(" souleyez jobs sanitize --all")
|
|
1084
1150
|
return
|
|
1085
|
-
|
|
1151
|
+
|
|
1086
1152
|
sanitized_count = 0
|
|
1087
1153
|
redacted_count = 0
|
|
1088
|
-
|
|
1154
|
+
|
|
1089
1155
|
for job in jobs_to_sanitize:
|
|
1090
|
-
jid = job[
|
|
1091
|
-
log_path = job.get(
|
|
1092
|
-
|
|
1156
|
+
jid = job["id"]
|
|
1157
|
+
log_path = job.get("log")
|
|
1158
|
+
|
|
1093
1159
|
if not log_path or not os.path.exists(log_path):
|
|
1094
1160
|
continue
|
|
1095
|
-
|
|
1161
|
+
|
|
1096
1162
|
try:
|
|
1097
|
-
with open(log_path,
|
|
1163
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
1098
1164
|
original = f.read()
|
|
1099
|
-
|
|
1165
|
+
|
|
1100
1166
|
if not LogSanitizer.contains_credentials(original):
|
|
1101
1167
|
continue
|
|
1102
|
-
|
|
1168
|
+
|
|
1103
1169
|
sanitized = LogSanitizer.sanitize(original)
|
|
1104
|
-
|
|
1170
|
+
|
|
1105
1171
|
if original == sanitized:
|
|
1106
1172
|
continue
|
|
1107
|
-
|
|
1173
|
+
|
|
1108
1174
|
summary = LogSanitizer.get_redaction_summary(original, sanitized)
|
|
1109
|
-
|
|
1175
|
+
|
|
1110
1176
|
if dry_run:
|
|
1111
1177
|
click.echo(f"Job {jid}: {summary}")
|
|
1112
1178
|
redacted_count += 1
|
|
1113
1179
|
else:
|
|
1114
|
-
with open(log_path,
|
|
1180
|
+
with open(log_path, "w", encoding="utf-8") as f:
|
|
1115
1181
|
f.write(sanitized)
|
|
1116
|
-
click.secho(f"✓ Job {jid}: {summary}", fg=
|
|
1182
|
+
click.secho(f"✓ Job {jid}: {summary}", fg="green")
|
|
1117
1183
|
sanitized_count += 1
|
|
1118
|
-
|
|
1184
|
+
|
|
1119
1185
|
except Exception as e:
|
|
1120
1186
|
click.echo(f"✗ Job {jid}: Failed - {e}", err=True)
|
|
1121
|
-
|
|
1187
|
+
|
|
1122
1188
|
if dry_run:
|
|
1123
1189
|
click.echo(f"\n{redacted_count} job log(s) contain credentials (dry-run)")
|
|
1124
1190
|
else:
|
|
1125
|
-
click.secho(f"\n✓ Sanitized {sanitized_count} job log(s)", fg=
|
|
1191
|
+
click.secho(f"\n✓ Sanitized {sanitized_count} job log(s)", fg="green")
|
|
1126
1192
|
|
|
1127
1193
|
|
|
1128
1194
|
@jobs.command("reparse")
|
|
1129
1195
|
@click.argument("job_id", type=int, required=False)
|
|
1130
1196
|
@click.option("--all", is_flag=True, help="Reparse all completed jobs")
|
|
1131
1197
|
@click.option("--tool", "-t", default=None, help="Filter by tool type")
|
|
1132
|
-
|
|
1198
|
+
@click.option("--chain", is_flag=True, help="Re-evaluate chain rules after reparsing")
|
|
1199
|
+
def jobs_reparse(job_id, all, tool, chain):
|
|
1133
1200
|
"""Reparse completed job results to update database and status.
|
|
1134
1201
|
|
|
1135
1202
|
Useful for applying new parsing logic to old jobs. This will:
|
|
1136
1203
|
- Re-run the parser on existing log files
|
|
1137
1204
|
- Update findings/credentials in database
|
|
1138
1205
|
- Update job status (e.g., no_results -> done if data found)
|
|
1206
|
+
- Optionally re-trigger chain rules with --chain
|
|
1139
1207
|
"""
|
|
1140
1208
|
from souleyez.engine.result_handler import reparse_job
|
|
1141
1209
|
|
|
@@ -1147,9 +1215,13 @@ def jobs_reparse(job_id, all, tool):
|
|
|
1147
1215
|
|
|
1148
1216
|
# Filter by status and optionally by tool
|
|
1149
1217
|
# Include no_results since that's what we're trying to fix
|
|
1150
|
-
jobs_to_reparse = [
|
|
1218
|
+
jobs_to_reparse = [
|
|
1219
|
+
j
|
|
1220
|
+
for j in jobs_to_reparse
|
|
1221
|
+
if j.get("status") in ("done", "error", "no_results")
|
|
1222
|
+
]
|
|
1151
1223
|
if tool:
|
|
1152
|
-
jobs_to_reparse = [j for j in jobs_to_reparse if j.get(
|
|
1224
|
+
jobs_to_reparse = [j for j in jobs_to_reparse if j.get("tool") == tool]
|
|
1153
1225
|
|
|
1154
1226
|
if not jobs_to_reparse:
|
|
1155
1227
|
filter_msg = f" for tool '{tool}'" if tool else ""
|
|
@@ -1166,8 +1238,11 @@ def jobs_reparse(job_id, all, tool):
|
|
|
1166
1238
|
click.echo(f"✗ Job {job_id} not found", err=True)
|
|
1167
1239
|
return
|
|
1168
1240
|
|
|
1169
|
-
if job.get(
|
|
1170
|
-
click.echo(
|
|
1241
|
+
if job.get("status") not in ("done", "error", "no_results"):
|
|
1242
|
+
click.echo(
|
|
1243
|
+
f"✗ Job {job_id} is not completed (status: {job.get('status')})",
|
|
1244
|
+
err=True,
|
|
1245
|
+
)
|
|
1171
1246
|
return
|
|
1172
1247
|
|
|
1173
1248
|
jobs_to_reparse = [job]
|
|
@@ -1184,62 +1259,82 @@ def jobs_reparse(job_id, all, tool):
|
|
|
1184
1259
|
error_count = 0
|
|
1185
1260
|
|
|
1186
1261
|
for job in jobs_to_reparse:
|
|
1187
|
-
jid = job[
|
|
1188
|
-
tool_name = job.get(
|
|
1189
|
-
old_status = job.get(
|
|
1262
|
+
jid = job["id"]
|
|
1263
|
+
tool_name = job.get("tool", "unknown")
|
|
1264
|
+
old_status = job.get("status")
|
|
1190
1265
|
|
|
1191
1266
|
try:
|
|
1192
1267
|
result = reparse_job(jid)
|
|
1193
1268
|
|
|
1194
|
-
if not result.get(
|
|
1195
|
-
msg = result.get(
|
|
1196
|
-
if
|
|
1269
|
+
if not result.get("success"):
|
|
1270
|
+
msg = result.get("message", "Unknown error")
|
|
1271
|
+
if "No parser" in msg:
|
|
1197
1272
|
click.echo(f" Job {jid} ({tool_name}): No parser available")
|
|
1198
1273
|
skipped_count += 1
|
|
1199
1274
|
else:
|
|
1200
|
-
click.secho(f"✗ Job {jid} ({tool_name}): {msg}", fg=
|
|
1275
|
+
click.secho(f"✗ Job {jid} ({tool_name}): {msg}", fg="red")
|
|
1201
1276
|
error_count += 1
|
|
1202
1277
|
else:
|
|
1203
1278
|
# Show what was parsed
|
|
1204
|
-
parse_result = result.get(
|
|
1205
|
-
new_status = result.get(
|
|
1279
|
+
parse_result = result.get("parse_result", {})
|
|
1280
|
+
new_status = result.get("new_status")
|
|
1206
1281
|
summary = []
|
|
1207
1282
|
|
|
1208
|
-
if parse_result.get(
|
|
1283
|
+
if parse_result.get("hosts_added", 0) > 0:
|
|
1209
1284
|
summary.append(f"{parse_result['hosts_added']} hosts")
|
|
1210
|
-
if parse_result.get(
|
|
1285
|
+
if parse_result.get("osint_added", 0) > 0:
|
|
1211
1286
|
summary.append(f"{parse_result['osint_added']} OSINT records")
|
|
1212
|
-
if parse_result.get(
|
|
1287
|
+
if parse_result.get("findings_added", 0) > 0:
|
|
1213
1288
|
summary.append(f"{parse_result['findings_added']} findings")
|
|
1214
|
-
if parse_result.get(
|
|
1289
|
+
if parse_result.get("credentials_added", 0) > 0:
|
|
1215
1290
|
summary.append(f"{parse_result['credentials_added']} credentials")
|
|
1216
|
-
if parse_result.get(
|
|
1291
|
+
if parse_result.get("users_found", 0) > 0:
|
|
1217
1292
|
summary.append(f"{parse_result['users_found']} users")
|
|
1218
|
-
if parse_result.get(
|
|
1293
|
+
if parse_result.get("shares_found", 0) > 0:
|
|
1219
1294
|
summary.append(f"{parse_result['shares_found']} shares")
|
|
1220
1295
|
|
|
1221
1296
|
summary_str = ", ".join(summary) if summary else "parsed"
|
|
1222
1297
|
|
|
1223
1298
|
# Highlight status changes
|
|
1224
1299
|
if old_status != new_status:
|
|
1225
|
-
click.secho(
|
|
1300
|
+
click.secho(
|
|
1301
|
+
f"✓ Job {jid} ({tool_name}): {summary_str} [{old_status} → {new_status}]",
|
|
1302
|
+
fg="green",
|
|
1303
|
+
)
|
|
1226
1304
|
updated_count += 1
|
|
1227
1305
|
else:
|
|
1228
|
-
click.secho(f"✓ Job {jid} ({tool_name}): {summary_str}", fg=
|
|
1306
|
+
click.secho(f"✓ Job {jid} ({tool_name}): {summary_str}", fg="green")
|
|
1229
1307
|
parsed_count += 1
|
|
1230
1308
|
|
|
1309
|
+
# Re-evaluate chain rules if requested
|
|
1310
|
+
if chain and parse_result:
|
|
1311
|
+
try:
|
|
1312
|
+
from souleyez.core.tool_chaining import ToolChaining
|
|
1313
|
+
|
|
1314
|
+
crm = ToolChaining()
|
|
1315
|
+
chain_job_ids = crm.auto_chain(
|
|
1316
|
+
job=job, parse_results=parse_result
|
|
1317
|
+
)
|
|
1318
|
+
if chain_job_ids:
|
|
1319
|
+
click.secho(
|
|
1320
|
+
f" → Chained {len(chain_job_ids)} job(s): {chain_job_ids}",
|
|
1321
|
+
fg="cyan",
|
|
1322
|
+
)
|
|
1323
|
+
except Exception as chain_err:
|
|
1324
|
+
click.secho(f" → Chain error: {chain_err}", fg="yellow")
|
|
1325
|
+
|
|
1231
1326
|
except Exception as e:
|
|
1232
|
-
click.secho(f"✗ Job {jid} ({tool_name}): Failed - {e}", fg=
|
|
1327
|
+
click.secho(f"✗ Job {jid} ({tool_name}): Failed - {e}", fg="red")
|
|
1233
1328
|
error_count += 1
|
|
1234
1329
|
|
|
1235
1330
|
click.echo(f"\n{'=' * 60}")
|
|
1236
|
-
click.secho(f"✓ Reparsed: {parsed_count}", fg=
|
|
1331
|
+
click.secho(f"✓ Reparsed: {parsed_count}", fg="green")
|
|
1237
1332
|
if updated_count > 0:
|
|
1238
|
-
click.secho(f" Status updated: {updated_count}", fg=
|
|
1333
|
+
click.secho(f" Status updated: {updated_count}", fg="cyan")
|
|
1239
1334
|
if skipped_count > 0:
|
|
1240
1335
|
click.echo(f" Skipped: {skipped_count}")
|
|
1241
1336
|
if error_count > 0:
|
|
1242
|
-
click.secho(f"✗ Errors: {error_count}", fg=
|
|
1337
|
+
click.secho(f"✗ Errors: {error_count}", fg="red")
|
|
1243
1338
|
click.echo("=" * 60)
|
|
1244
1339
|
|
|
1245
1340
|
|
|
@@ -1269,19 +1364,20 @@ def worker_start(fg):
|
|
|
1269
1364
|
def worker_status():
|
|
1270
1365
|
"""Check worker status."""
|
|
1271
1366
|
import subprocess
|
|
1272
|
-
|
|
1367
|
+
|
|
1273
1368
|
try:
|
|
1274
1369
|
# Use pgrep to find worker_loop processes (more reliable than ps + grep)
|
|
1275
1370
|
result = subprocess.run(
|
|
1276
|
-
["pgrep", "-f", "worker_loop"],
|
|
1277
|
-
capture_output=True,
|
|
1278
|
-
text=True
|
|
1371
|
+
["pgrep", "-f", "worker_loop"], capture_output=True, text=True
|
|
1279
1372
|
)
|
|
1280
|
-
|
|
1373
|
+
|
|
1281
1374
|
if result.returncode == 0 and result.stdout.strip():
|
|
1282
1375
|
# Found worker processes
|
|
1283
|
-
pids = result.stdout.strip().split(
|
|
1284
|
-
click.secho(
|
|
1376
|
+
pids = result.stdout.strip().split("\n")
|
|
1377
|
+
click.secho(
|
|
1378
|
+
f"✓ Worker is running ({len(pids)} process{'es' if len(pids) > 1 else ''}):",
|
|
1379
|
+
fg="green",
|
|
1380
|
+
)
|
|
1285
1381
|
for pid in pids:
|
|
1286
1382
|
click.echo(f" PID {pid}: background worker")
|
|
1287
1383
|
else:
|
|
@@ -1292,12 +1388,12 @@ def worker_status():
|
|
|
1292
1388
|
try:
|
|
1293
1389
|
result = subprocess.run(["ps", "aux"], capture_output=True, text=True)
|
|
1294
1390
|
worker_procs = []
|
|
1295
|
-
for line in result.stdout.split(
|
|
1296
|
-
if
|
|
1391
|
+
for line in result.stdout.split("\n"):
|
|
1392
|
+
if "worker_loop" in line and "grep" not in line:
|
|
1297
1393
|
worker_procs.append(line)
|
|
1298
|
-
|
|
1394
|
+
|
|
1299
1395
|
if worker_procs:
|
|
1300
|
-
click.secho("✓ Worker is running:", fg=
|
|
1396
|
+
click.secho("✓ Worker is running:", fg="green")
|
|
1301
1397
|
for proc in worker_procs:
|
|
1302
1398
|
parts = proc.split()
|
|
1303
1399
|
if len(parts) >= 2:
|
|
@@ -1316,22 +1412,22 @@ def list_plugins():
|
|
|
1316
1412
|
"""List available plugins."""
|
|
1317
1413
|
try:
|
|
1318
1414
|
from souleyez.engine.loader import discover_plugins
|
|
1319
|
-
|
|
1415
|
+
|
|
1320
1416
|
plugins = discover_plugins()
|
|
1321
|
-
|
|
1417
|
+
|
|
1322
1418
|
if not plugins:
|
|
1323
1419
|
click.echo("No plugins found")
|
|
1324
1420
|
return
|
|
1325
|
-
|
|
1421
|
+
|
|
1326
1422
|
click.echo("\n" + "=" * 80)
|
|
1327
1423
|
click.echo("AVAILABLE PLUGINS")
|
|
1328
1424
|
click.echo("=" * 80)
|
|
1329
|
-
|
|
1425
|
+
|
|
1330
1426
|
for key, plugin in sorted(plugins.items()):
|
|
1331
|
-
name = getattr(plugin,
|
|
1332
|
-
category = getattr(plugin,
|
|
1427
|
+
name = getattr(plugin, "name", "Unknown")
|
|
1428
|
+
category = getattr(plugin, "category", "misc")
|
|
1333
1429
|
click.echo(f"{key:<15} | {name:<30} | {category}")
|
|
1334
|
-
|
|
1430
|
+
|
|
1335
1431
|
click.echo("=" * 80)
|
|
1336
1432
|
click.echo(f"Total: {len(plugins)} plugins\n")
|
|
1337
1433
|
except Exception as e:
|
|
@@ -1340,6 +1436,7 @@ def list_plugins():
|
|
|
1340
1436
|
|
|
1341
1437
|
# ==================== DATABASE COMMANDS ====================
|
|
1342
1438
|
|
|
1439
|
+
|
|
1343
1440
|
@cli.group()
|
|
1344
1441
|
def db():
|
|
1345
1442
|
"""Database management commands."""
|
|
@@ -1351,19 +1448,19 @@ def db_migrate():
|
|
|
1351
1448
|
"""Apply pending database migrations."""
|
|
1352
1449
|
from souleyez.storage.migrations.migration_manager import MigrationManager
|
|
1353
1450
|
from souleyez.storage.database import get_db
|
|
1354
|
-
|
|
1451
|
+
|
|
1355
1452
|
click.echo()
|
|
1356
|
-
click.echo(click.style("🔄 DATABASE MIGRATION", bold=True, fg=
|
|
1453
|
+
click.echo(click.style("🔄 DATABASE MIGRATION", bold=True, fg="cyan"))
|
|
1357
1454
|
click.echo("=" * 80)
|
|
1358
|
-
|
|
1455
|
+
|
|
1359
1456
|
# Get database path
|
|
1360
1457
|
db_instance = get_db()
|
|
1361
1458
|
db_path = db_instance.db_path
|
|
1362
|
-
|
|
1459
|
+
|
|
1363
1460
|
# Run migrations
|
|
1364
1461
|
manager = MigrationManager(db_path)
|
|
1365
1462
|
applied = manager.migrate()
|
|
1366
|
-
|
|
1463
|
+
|
|
1367
1464
|
click.echo()
|
|
1368
1465
|
|
|
1369
1466
|
|
|
@@ -1372,11 +1469,11 @@ def db_status():
|
|
|
1372
1469
|
"""Show database migration status."""
|
|
1373
1470
|
from souleyez.storage.migrations.migration_manager import MigrationManager
|
|
1374
1471
|
from souleyez.storage.database import get_db
|
|
1375
|
-
|
|
1472
|
+
|
|
1376
1473
|
# Get database path
|
|
1377
1474
|
db_instance = get_db()
|
|
1378
1475
|
db_path = db_instance.db_path
|
|
1379
|
-
|
|
1476
|
+
|
|
1380
1477
|
# Show status
|
|
1381
1478
|
manager = MigrationManager(db_path)
|
|
1382
1479
|
manager.status()
|
|
@@ -1388,36 +1485,36 @@ def db_encrypt():
|
|
|
1388
1485
|
import getpass
|
|
1389
1486
|
from souleyez.storage.credentials import CredentialsManager
|
|
1390
1487
|
from souleyez.storage.crypto import get_crypto_manager
|
|
1391
|
-
|
|
1488
|
+
|
|
1392
1489
|
crypto = get_crypto_manager()
|
|
1393
|
-
|
|
1490
|
+
|
|
1394
1491
|
# Check if encryption is enabled
|
|
1395
1492
|
if not crypto.is_encryption_enabled():
|
|
1396
|
-
click.echo(click.style("✗ Encryption is not enabled", fg=
|
|
1493
|
+
click.echo(click.style("✗ Encryption is not enabled", fg="red"))
|
|
1397
1494
|
click.echo("Run 'souleyez interactive' to set up encryption first")
|
|
1398
1495
|
return
|
|
1399
|
-
|
|
1496
|
+
|
|
1400
1497
|
# Unlock crypto if needed
|
|
1401
1498
|
if not crypto.is_unlocked():
|
|
1402
|
-
click.echo(click.style("🔐 Master password required", fg=
|
|
1499
|
+
click.echo(click.style("🔐 Master password required", fg="cyan"))
|
|
1403
1500
|
password = getpass.getpass("Enter master password: ")
|
|
1404
1501
|
try:
|
|
1405
1502
|
crypto.unlock(password)
|
|
1406
|
-
click.echo(click.style("✓ Unlocked", fg=
|
|
1503
|
+
click.echo(click.style("✓ Unlocked", fg="green"))
|
|
1407
1504
|
except Exception as e:
|
|
1408
|
-
click.echo(click.style(f"✗ Failed to unlock: {e}", fg=
|
|
1505
|
+
click.echo(click.style(f"✗ Failed to unlock: {e}", fg="red"))
|
|
1409
1506
|
return
|
|
1410
|
-
|
|
1507
|
+
|
|
1411
1508
|
# Encrypt all credentials
|
|
1412
1509
|
cm = CredentialsManager()
|
|
1413
1510
|
result = cm.encrypt_all_unencrypted()
|
|
1414
|
-
|
|
1415
|
-
if
|
|
1416
|
-
click.echo(click.style(f"✗ {result['error']}", fg=
|
|
1511
|
+
|
|
1512
|
+
if "error" in result:
|
|
1513
|
+
click.echo(click.style(f"✗ {result['error']}", fg="red"))
|
|
1417
1514
|
return
|
|
1418
|
-
|
|
1515
|
+
|
|
1419
1516
|
click.echo()
|
|
1420
|
-
click.echo(click.style("✓ Encryption complete", fg=
|
|
1517
|
+
click.echo(click.style("✓ Encryption complete", fg="green", bold=True))
|
|
1421
1518
|
click.echo(f" • Encrypted: {result.get('encrypted', 0)} credentials")
|
|
1422
1519
|
click.echo(f" • Already encrypted: {result.get('skipped', 0)} credentials")
|
|
1423
1520
|
click.echo(f" • Total: {result.get('total', 0)} credentials")
|
|
@@ -1430,35 +1527,40 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1430
1527
|
import sqlite3
|
|
1431
1528
|
|
|
1432
1529
|
click.echo()
|
|
1433
|
-
click.echo(click.style("🩺 SoulEyez Doctor", fg=
|
|
1434
|
-
click.echo(click.style("=" * 50, fg=
|
|
1530
|
+
click.echo(click.style("🩺 SoulEyez Doctor", fg="cyan", bold=True))
|
|
1531
|
+
click.echo(click.style("=" * 50, fg="cyan"))
|
|
1435
1532
|
click.echo()
|
|
1436
1533
|
|
|
1437
1534
|
issues = []
|
|
1438
1535
|
warnings = []
|
|
1439
1536
|
|
|
1440
1537
|
def check_pass(msg):
|
|
1441
|
-
click.echo(click.style(f" ✓ {msg}", fg=
|
|
1538
|
+
click.echo(click.style(f" ✓ {msg}", fg="green"))
|
|
1442
1539
|
|
|
1443
1540
|
def check_fail(msg, fix_cmd=None):
|
|
1444
|
-
click.echo(click.style(f" ✗ {msg}", fg=
|
|
1541
|
+
click.echo(click.style(f" ✗ {msg}", fg="red"))
|
|
1445
1542
|
issues.append((msg, fix_cmd))
|
|
1446
1543
|
|
|
1447
1544
|
def check_warn(msg, suggestion=None):
|
|
1448
|
-
click.echo(click.style(f" ⚠ {msg}", fg=
|
|
1545
|
+
click.echo(click.style(f" ⚠ {msg}", fg="yellow"))
|
|
1449
1546
|
warnings.append((msg, suggestion))
|
|
1450
1547
|
|
|
1451
1548
|
# Section 1: Python Environment
|
|
1452
1549
|
click.echo(click.style("Python Environment", bold=True))
|
|
1453
1550
|
import sys
|
|
1454
|
-
|
|
1551
|
+
|
|
1552
|
+
python_version = (
|
|
1553
|
+
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
1554
|
+
)
|
|
1455
1555
|
if sys.version_info >= (3, 8):
|
|
1456
1556
|
check_pass(f"Python {python_version}")
|
|
1457
1557
|
else:
|
|
1458
|
-
check_fail(
|
|
1558
|
+
check_fail(
|
|
1559
|
+
f"Python {python_version} (need 3.8+)", "Install Python 3.8 or newer"
|
|
1560
|
+
)
|
|
1459
1561
|
|
|
1460
1562
|
# Check required packages
|
|
1461
|
-
required_packages = [
|
|
1563
|
+
required_packages = ["click", "rich", "wcwidth"]
|
|
1462
1564
|
for pkg in required_packages:
|
|
1463
1565
|
try:
|
|
1464
1566
|
__import__(pkg)
|
|
@@ -1471,14 +1573,16 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1471
1573
|
|
|
1472
1574
|
# Section 2: Data Directory
|
|
1473
1575
|
click.echo(click.style("Data Directory", bold=True))
|
|
1474
|
-
data_dir = Path.home() /
|
|
1576
|
+
data_dir = Path.home() / ".souleyez"
|
|
1475
1577
|
if data_dir.exists():
|
|
1476
1578
|
check_pass(f"Data directory: {data_dir}")
|
|
1477
1579
|
else:
|
|
1478
|
-
check_warn(
|
|
1580
|
+
check_warn(
|
|
1581
|
+
f"Data directory missing: {data_dir}", "Will be created on first run"
|
|
1582
|
+
)
|
|
1479
1583
|
|
|
1480
1584
|
# Check database
|
|
1481
|
-
db_path = data_dir /
|
|
1585
|
+
db_path = data_dir / "souleyez.db"
|
|
1482
1586
|
if db_path.exists():
|
|
1483
1587
|
try:
|
|
1484
1588
|
conn = sqlite3.connect(str(db_path))
|
|
@@ -1488,7 +1592,9 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1488
1592
|
conn.close()
|
|
1489
1593
|
check_pass(f"Database OK ({count} engagements)")
|
|
1490
1594
|
except Exception as e:
|
|
1491
|
-
check_fail(
|
|
1595
|
+
check_fail(
|
|
1596
|
+
f"Database error: {str(e)[:40]}", "Run: souleyez setup --repair-db"
|
|
1597
|
+
)
|
|
1492
1598
|
else:
|
|
1493
1599
|
check_warn("No database yet", "Will be created on first run")
|
|
1494
1600
|
|
|
@@ -1497,9 +1603,9 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1497
1603
|
# Section 3: Essential Tools
|
|
1498
1604
|
click.echo(click.style("Essential Tools", bold=True))
|
|
1499
1605
|
essential_tools = {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1606
|
+
"nmap": "sudo apt install nmap",
|
|
1607
|
+
"git": "sudo apt install git",
|
|
1608
|
+
"curl": "sudo apt install curl",
|
|
1503
1609
|
}
|
|
1504
1610
|
|
|
1505
1611
|
for tool, install_cmd in essential_tools.items():
|
|
@@ -1514,6 +1620,7 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1514
1620
|
click.echo(click.style("Pentesting Tools", bold=True))
|
|
1515
1621
|
# Use the same tool checker as the setup wizard for consistency
|
|
1516
1622
|
from souleyez.utils.tool_checker import get_tool_stats
|
|
1623
|
+
|
|
1517
1624
|
installed, total = get_tool_stats()
|
|
1518
1625
|
|
|
1519
1626
|
if installed == total:
|
|
@@ -1527,22 +1634,27 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1527
1634
|
|
|
1528
1635
|
# Section 5: Configuration
|
|
1529
1636
|
click.echo(click.style("Configuration", bold=True))
|
|
1530
|
-
config_file = data_dir /
|
|
1637
|
+
config_file = data_dir / "config.json"
|
|
1531
1638
|
if config_file.exists():
|
|
1532
1639
|
try:
|
|
1533
1640
|
import json
|
|
1641
|
+
|
|
1534
1642
|
with open(config_file) as f:
|
|
1535
1643
|
config = json.load(f)
|
|
1536
1644
|
check_pass("Config file valid")
|
|
1537
1645
|
|
|
1538
1646
|
# Check AI providers
|
|
1539
|
-
ai_config = config.get(
|
|
1540
|
-
provider = ai_config.get(
|
|
1541
|
-
has_ollama = provider ==
|
|
1542
|
-
has_claude = ai_config.get(
|
|
1543
|
-
|
|
1647
|
+
ai_config = config.get("ai", {})
|
|
1648
|
+
provider = ai_config.get("provider", "")
|
|
1649
|
+
has_ollama = provider == "ollama" or ai_config.get("ollama_model")
|
|
1650
|
+
has_claude = ai_config.get("claude_api_key") or ai_config.get(
|
|
1651
|
+
"anthropic_api_key"
|
|
1652
|
+
)
|
|
1653
|
+
has_openai = ai_config.get("openai_api_key")
|
|
1544
1654
|
if has_ollama or has_claude or has_openai:
|
|
1545
|
-
provider_name = provider or (
|
|
1655
|
+
provider_name = provider or (
|
|
1656
|
+
"ollama" if has_ollama else "claude" if has_claude else "openai"
|
|
1657
|
+
)
|
|
1546
1658
|
check_pass(f"AI provider: {provider_name}")
|
|
1547
1659
|
except Exception as e:
|
|
1548
1660
|
check_fail(f"Config error: {str(e)[:30]}", "Check ~/.souleyez/config.json")
|
|
@@ -1555,45 +1667,58 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1555
1667
|
click.echo(click.style("System Configuration", bold=True))
|
|
1556
1668
|
|
|
1557
1669
|
# Check sudoers files for proper format (trailing newline)
|
|
1558
|
-
sudoers_dir = Path(
|
|
1559
|
-
sudoers_tools = [
|
|
1670
|
+
sudoers_dir = Path("/etc/sudoers.d")
|
|
1671
|
+
sudoers_tools = ["nmap", "souleyez-responder"] # Tools we configure in sudoers
|
|
1560
1672
|
for tool in sudoers_tools:
|
|
1561
1673
|
sudoers_file = sudoers_dir / tool
|
|
1562
1674
|
if sudoers_file.exists():
|
|
1563
1675
|
try:
|
|
1564
1676
|
# Check file size vs expected content
|
|
1565
1677
|
content = sudoers_file.read_bytes()
|
|
1566
|
-
if content and not content.endswith(b
|
|
1567
|
-
check_fail(
|
|
1568
|
-
|
|
1678
|
+
if content and not content.endswith(b"\n"):
|
|
1679
|
+
check_fail(
|
|
1680
|
+
f"Sudoers {tool}: missing newline",
|
|
1681
|
+
f"echo '' | sudo tee -a /etc/sudoers.d/{tool}",
|
|
1682
|
+
)
|
|
1569
1683
|
elif verbose:
|
|
1570
1684
|
check_pass(f"Sudoers {tool}: OK")
|
|
1571
1685
|
except PermissionError:
|
|
1572
1686
|
if verbose:
|
|
1573
|
-
click.echo(
|
|
1687
|
+
click.echo(
|
|
1688
|
+
click.style(
|
|
1689
|
+
f" - Sudoers {tool}: need sudo to verify",
|
|
1690
|
+
fg="bright_black",
|
|
1691
|
+
)
|
|
1692
|
+
)
|
|
1574
1693
|
|
|
1575
1694
|
# Check PATH for common tool directories
|
|
1576
|
-
path_dirs = os.environ.get(
|
|
1577
|
-
pipx_bin = str(Path.home() /
|
|
1578
|
-
go_bin = str(Path.home() /
|
|
1695
|
+
path_dirs = os.environ.get("PATH", "").split(":")
|
|
1696
|
+
pipx_bin = str(Path.home() / ".local" / "bin")
|
|
1697
|
+
go_bin = str(Path.home() / "go" / "bin")
|
|
1579
1698
|
|
|
1580
1699
|
# Detect shell config file (zsh for Kali, bash for others)
|
|
1581
|
-
shell = os.environ.get(
|
|
1582
|
-
shell_rc =
|
|
1700
|
+
shell = os.environ.get("SHELL", "/bin/bash")
|
|
1701
|
+
shell_rc = "~/.zshrc" if "zsh" in shell else "~/.bashrc"
|
|
1583
1702
|
|
|
1584
1703
|
if pipx_bin in path_dirs:
|
|
1585
1704
|
if verbose:
|
|
1586
1705
|
check_pass("PATH includes ~/.local/bin (pipx)")
|
|
1587
1706
|
else:
|
|
1588
1707
|
if Path(pipx_bin).exists() and any(Path(pipx_bin).iterdir()):
|
|
1589
|
-
check_warn(
|
|
1708
|
+
check_warn(
|
|
1709
|
+
"~/.local/bin not in PATH",
|
|
1710
|
+
f'Add to {shell_rc}: export PATH="$HOME/.local/bin:$PATH"',
|
|
1711
|
+
)
|
|
1590
1712
|
|
|
1591
1713
|
if go_bin in path_dirs:
|
|
1592
1714
|
if verbose:
|
|
1593
1715
|
check_pass("PATH includes ~/go/bin")
|
|
1594
1716
|
else:
|
|
1595
1717
|
if Path(go_bin).exists() and any(Path(go_bin).iterdir()):
|
|
1596
|
-
check_warn(
|
|
1718
|
+
check_warn(
|
|
1719
|
+
"~/go/bin not in PATH",
|
|
1720
|
+
f'Add to {shell_rc}: export PATH="$HOME/go/bin:$PATH"',
|
|
1721
|
+
)
|
|
1597
1722
|
|
|
1598
1723
|
# Check database is writable
|
|
1599
1724
|
if db_path.exists():
|
|
@@ -1609,69 +1734,183 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1609
1734
|
# Check background worker
|
|
1610
1735
|
try:
|
|
1611
1736
|
import subprocess
|
|
1612
|
-
|
|
1613
|
-
|
|
1737
|
+
|
|
1738
|
+
result = subprocess.run(
|
|
1739
|
+
["pgrep", "-f", "souleyez worker"], capture_output=True, timeout=5
|
|
1740
|
+
)
|
|
1614
1741
|
if result.returncode == 0:
|
|
1615
1742
|
check_pass("Background worker running")
|
|
1616
1743
|
else:
|
|
1617
|
-
check_warn(
|
|
1744
|
+
check_warn(
|
|
1745
|
+
"Background worker not running",
|
|
1746
|
+
"souleyez dashboard (starts automatically)",
|
|
1747
|
+
)
|
|
1618
1748
|
except Exception:
|
|
1619
1749
|
pass
|
|
1620
1750
|
|
|
1751
|
+
# Check for orphaned pending chains
|
|
1752
|
+
try:
|
|
1753
|
+
from souleyez.core.pending_chains import _read_chains, purge_orphaned_chains
|
|
1754
|
+
|
|
1755
|
+
chains = _read_chains()
|
|
1756
|
+
if chains:
|
|
1757
|
+
# Check which are orphaned
|
|
1758
|
+
db_path = os.path.join(data_dir, "souleyez.db")
|
|
1759
|
+
conn = sqlite3.connect(db_path)
|
|
1760
|
+
cursor = conn.execute("SELECT id FROM engagements")
|
|
1761
|
+
valid_ids = {row[0] for row in cursor.fetchall()}
|
|
1762
|
+
conn.close()
|
|
1763
|
+
|
|
1764
|
+
orphaned = [
|
|
1765
|
+
c
|
|
1766
|
+
for c in chains
|
|
1767
|
+
if c.get("engagement_id") not in valid_ids
|
|
1768
|
+
and c.get("engagement_id") is not None
|
|
1769
|
+
]
|
|
1770
|
+
if orphaned:
|
|
1771
|
+
check_warn(
|
|
1772
|
+
f"Orphaned pending chains: {len(orphaned)}",
|
|
1773
|
+
"Run with --fix to clean up",
|
|
1774
|
+
)
|
|
1775
|
+
if fix:
|
|
1776
|
+
purged = purge_orphaned_chains()
|
|
1777
|
+
if purged > 0:
|
|
1778
|
+
check_pass(f"Cleaned up {purged} orphaned chains")
|
|
1779
|
+
elif verbose:
|
|
1780
|
+
check_pass(f"Pending chains OK ({len(chains)} active)")
|
|
1781
|
+
elif verbose:
|
|
1782
|
+
check_pass("No pending chains")
|
|
1783
|
+
except Exception:
|
|
1784
|
+
pass
|
|
1785
|
+
|
|
1786
|
+
# Check dashboard cache status
|
|
1787
|
+
if verbose:
|
|
1788
|
+
try:
|
|
1789
|
+
from souleyez.ui.dashboard import _header_cache, _HEADER_CACHE_TTL
|
|
1790
|
+
|
|
1791
|
+
cache_entries = len(_header_cache)
|
|
1792
|
+
if cache_entries > 0:
|
|
1793
|
+
check_pass(
|
|
1794
|
+
f"Dashboard cache: {cache_entries} entries (TTL: {_HEADER_CACHE_TTL}s)"
|
|
1795
|
+
)
|
|
1796
|
+
else:
|
|
1797
|
+
check_pass("Dashboard cache: empty (will populate on first load)")
|
|
1798
|
+
except Exception:
|
|
1799
|
+
pass
|
|
1800
|
+
|
|
1801
|
+
try:
|
|
1802
|
+
from souleyez.intelligence.exploit_suggestions import (
|
|
1803
|
+
_SUGGESTION_CACHE,
|
|
1804
|
+
_CACHE_TIMEOUT,
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
cache_entries = len(_SUGGESTION_CACHE)
|
|
1808
|
+
if cache_entries > 0:
|
|
1809
|
+
check_pass(
|
|
1810
|
+
f"Exploit suggestions cache: {cache_entries} entries (TTL: {_CACHE_TIMEOUT}s)"
|
|
1811
|
+
)
|
|
1812
|
+
else:
|
|
1813
|
+
check_pass(
|
|
1814
|
+
"Exploit suggestions cache: empty (will populate on first load)"
|
|
1815
|
+
)
|
|
1816
|
+
except Exception:
|
|
1817
|
+
pass
|
|
1818
|
+
|
|
1621
1819
|
click.echo()
|
|
1622
1820
|
|
|
1623
1821
|
# Section 7: MSF Database (if msfconsole available)
|
|
1624
|
-
if shutil.which(
|
|
1822
|
+
if shutil.which("msfconsole"):
|
|
1625
1823
|
click.echo(click.style("Metasploit", bold=True))
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1824
|
+
|
|
1825
|
+
# Use comprehensive msfdb status check
|
|
1826
|
+
from souleyez.utils.tool_checker import check_msfdb_status
|
|
1827
|
+
|
|
1828
|
+
db_status = check_msfdb_status()
|
|
1829
|
+
|
|
1830
|
+
if db_status["initialized"] and db_status["running"]:
|
|
1831
|
+
check_pass("MSF database initialized and running")
|
|
1832
|
+
elif db_status["initialized"] and not db_status["running"]:
|
|
1833
|
+
check_warn(
|
|
1834
|
+
"MSF database initialized but PostgreSQL not running",
|
|
1835
|
+
"sudo systemctl start postgresql",
|
|
1836
|
+
)
|
|
1837
|
+
elif not db_status["initialized"]:
|
|
1838
|
+
# Fall back to file check in case msfdb status failed
|
|
1839
|
+
msf_db = Path.home() / ".msf4" / "database.yml"
|
|
1840
|
+
system_msf_db = Path("/usr/share/metasploit-framework/config/database.yml")
|
|
1841
|
+
if msf_db.exists() or system_msf_db.exists():
|
|
1842
|
+
check_warn(
|
|
1843
|
+
"MSF database config exists but status unclear",
|
|
1844
|
+
db_status.get("message", "Run: sudo msfdb status"),
|
|
1845
|
+
)
|
|
1846
|
+
else:
|
|
1847
|
+
check_fail("MSF database not initialized", "msfdb init")
|
|
1848
|
+
|
|
1849
|
+
if verbose:
|
|
1850
|
+
click.echo(
|
|
1851
|
+
click.style(
|
|
1852
|
+
f" Status: {db_status.get('message', 'Unknown')}",
|
|
1853
|
+
fg="bright_black",
|
|
1854
|
+
)
|
|
1855
|
+
)
|
|
1633
1856
|
|
|
1634
1857
|
# Check if root has access (for sudo msfconsole)
|
|
1635
1858
|
# Skip if we can't access /root (not an error, just needs sudo to check)
|
|
1636
|
-
root_msf_db = Path(
|
|
1859
|
+
root_msf_db = Path("/root/.msf4/database.yml")
|
|
1637
1860
|
try:
|
|
1638
1861
|
if root_msf_db.exists():
|
|
1639
1862
|
check_pass("Sudo MSF access configured")
|
|
1640
1863
|
else:
|
|
1641
|
-
check_warn(
|
|
1642
|
-
|
|
1864
|
+
check_warn(
|
|
1865
|
+
"Sudo MSF may not connect to DB",
|
|
1866
|
+
"sudo cp ~/.msf4/database.yml /root/.msf4/",
|
|
1867
|
+
)
|
|
1643
1868
|
except PermissionError:
|
|
1644
1869
|
# Can't check without sudo - not a problem, just skip
|
|
1645
1870
|
if verbose:
|
|
1646
|
-
click.echo(
|
|
1871
|
+
click.echo(
|
|
1872
|
+
click.style(
|
|
1873
|
+
" - Sudo MSF config: need sudo to verify", fg="bright_black"
|
|
1874
|
+
)
|
|
1875
|
+
)
|
|
1647
1876
|
|
|
1648
1877
|
click.echo()
|
|
1649
1878
|
|
|
1650
1879
|
# Summary
|
|
1651
|
-
click.echo(click.style("=" * 50, fg=
|
|
1880
|
+
click.echo(click.style("=" * 50, fg="cyan"))
|
|
1652
1881
|
if not issues and not warnings:
|
|
1653
|
-
click.echo(
|
|
1882
|
+
click.echo(
|
|
1883
|
+
click.style(
|
|
1884
|
+
"✓ All checks passed! SoulEyez is healthy.", fg="green", bold=True
|
|
1885
|
+
)
|
|
1886
|
+
)
|
|
1654
1887
|
else:
|
|
1655
1888
|
if issues:
|
|
1656
|
-
click.echo(
|
|
1889
|
+
click.echo(
|
|
1890
|
+
click.style(f"\n{len(issues)} issue(s) found:", fg="red", bold=True)
|
|
1891
|
+
)
|
|
1657
1892
|
for issue, fix_cmd in issues:
|
|
1658
1893
|
click.echo(f" • {issue}")
|
|
1659
1894
|
if fix_cmd:
|
|
1660
|
-
click.echo(click.style(f" Fix: {fix_cmd}", fg=
|
|
1895
|
+
click.echo(click.style(f" Fix: {fix_cmd}", fg="cyan"))
|
|
1661
1896
|
|
|
1662
1897
|
if warnings:
|
|
1663
|
-
click.echo(
|
|
1898
|
+
click.echo(
|
|
1899
|
+
click.style(f"\n{len(warnings)} warning(s):", fg="yellow", bold=True)
|
|
1900
|
+
)
|
|
1664
1901
|
for warning, suggestion in warnings:
|
|
1665
1902
|
click.echo(f" • {warning}")
|
|
1666
1903
|
if suggestion:
|
|
1667
|
-
click.echo(click.style(f" Suggestion: {suggestion}", fg=
|
|
1904
|
+
click.echo(click.style(f" Suggestion: {suggestion}", fg="cyan"))
|
|
1668
1905
|
|
|
1669
1906
|
# Auto-fix if requested
|
|
1670
1907
|
if fix and issues:
|
|
1671
1908
|
click.echo()
|
|
1672
|
-
click.echo(click.style("Attempting auto-fix...", fg=
|
|
1909
|
+
click.echo(click.style("Attempting auto-fix...", fg="cyan", bold=True))
|
|
1673
1910
|
# TODO: Implement auto-fix for common issues
|
|
1674
|
-
click.echo(
|
|
1911
|
+
click.echo(
|
|
1912
|
+
"Auto-fix not yet implemented. Please run the suggested commands manually."
|
|
1913
|
+
)
|
|
1675
1914
|
|
|
1676
1915
|
click.echo()
|
|
1677
1916
|
|
|
@@ -1679,8 +1918,8 @@ def _run_doctor(fix=False, verbose=False):
|
|
|
1679
1918
|
|
|
1680
1919
|
|
|
1681
1920
|
@cli.command()
|
|
1682
|
-
@click.option(
|
|
1683
|
-
@click.option(
|
|
1921
|
+
@click.option("--fix", is_flag=True, help="Attempt to automatically fix issues")
|
|
1922
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed diagnostic info")
|
|
1684
1923
|
def doctor(fix, verbose):
|
|
1685
1924
|
"""Diagnose and fix common SoulEyez issues.
|
|
1686
1925
|
|
|
@@ -1705,11 +1944,12 @@ def tutorial():
|
|
|
1705
1944
|
- Understanding the dashboard
|
|
1706
1945
|
"""
|
|
1707
1946
|
from souleyez.ui.tutorial import run_tutorial
|
|
1947
|
+
|
|
1708
1948
|
run_tutorial()
|
|
1709
1949
|
|
|
1710
1950
|
|
|
1711
|
-
@cli.command(
|
|
1712
|
-
@click.option(
|
|
1951
|
+
@cli.command("install-desktop")
|
|
1952
|
+
@click.option("--remove", is_flag=True, help="Remove the desktop shortcut")
|
|
1713
1953
|
def install_desktop(remove):
|
|
1714
1954
|
"""Install SoulEyez desktop shortcut in Applications menu.
|
|
1715
1955
|
|
|
@@ -1719,29 +1959,33 @@ def install_desktop(remove):
|
|
|
1719
1959
|
import shutil
|
|
1720
1960
|
from importlib import resources
|
|
1721
1961
|
|
|
1722
|
-
applications_dir = Path.home() /
|
|
1723
|
-
icons_dir = Path.home() /
|
|
1724
|
-
desktop_file = applications_dir /
|
|
1725
|
-
icon_dest = icons_dir /
|
|
1962
|
+
applications_dir = Path.home() / ".local" / "share" / "applications"
|
|
1963
|
+
icons_dir = Path.home() / ".local" / "share" / "icons"
|
|
1964
|
+
desktop_file = applications_dir / "souleyez.desktop"
|
|
1965
|
+
icon_dest = icons_dir / "souleyez.png"
|
|
1726
1966
|
|
|
1727
1967
|
if remove:
|
|
1728
1968
|
# Remove desktop shortcut
|
|
1729
1969
|
removed = False
|
|
1730
1970
|
if desktop_file.exists():
|
|
1731
1971
|
desktop_file.unlink()
|
|
1732
|
-
click.echo(click.style(" Removed desktop shortcut", fg=
|
|
1972
|
+
click.echo(click.style(" Removed desktop shortcut", fg="green"))
|
|
1733
1973
|
removed = True
|
|
1734
1974
|
if icon_dest.exists():
|
|
1735
1975
|
icon_dest.unlink()
|
|
1736
|
-
click.echo(click.style(" Removed icon", fg=
|
|
1976
|
+
click.echo(click.style(" Removed icon", fg="green"))
|
|
1737
1977
|
removed = True
|
|
1738
1978
|
if removed:
|
|
1739
|
-
click.echo(
|
|
1979
|
+
click.echo(
|
|
1980
|
+
click.style("\nSoulEyez removed from Applications menu.", fg="cyan")
|
|
1981
|
+
)
|
|
1740
1982
|
else:
|
|
1741
|
-
click.echo(click.style("No desktop shortcut found.", fg=
|
|
1983
|
+
click.echo(click.style("No desktop shortcut found.", fg="yellow"))
|
|
1742
1984
|
return
|
|
1743
1985
|
|
|
1744
|
-
click.echo(
|
|
1986
|
+
click.echo(
|
|
1987
|
+
click.style("\nInstalling SoulEyez desktop shortcut...\n", fg="cyan", bold=True)
|
|
1988
|
+
)
|
|
1745
1989
|
|
|
1746
1990
|
# Create directories
|
|
1747
1991
|
applications_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1752,20 +1996,21 @@ def install_desktop(remove):
|
|
|
1752
1996
|
# Try importlib.resources first (Python 3.9+)
|
|
1753
1997
|
try:
|
|
1754
1998
|
from importlib.resources import files
|
|
1755
|
-
|
|
1756
|
-
|
|
1999
|
+
|
|
2000
|
+
icon_source = files("souleyez.assets").joinpath("souleyez-icon.png")
|
|
2001
|
+
with open(icon_source, "rb") as src:
|
|
1757
2002
|
icon_data = src.read()
|
|
1758
2003
|
except (ImportError, TypeError, FileNotFoundError):
|
|
1759
2004
|
# Fallback: find icon relative to this file
|
|
1760
|
-
icon_source = Path(__file__).parent /
|
|
1761
|
-
with open(icon_source,
|
|
2005
|
+
icon_source = Path(__file__).parent / "assets" / "souleyez-icon.png"
|
|
2006
|
+
with open(icon_source, "rb") as src:
|
|
1762
2007
|
icon_data = src.read()
|
|
1763
2008
|
|
|
1764
|
-
with open(icon_dest,
|
|
2009
|
+
with open(icon_dest, "wb") as dst:
|
|
1765
2010
|
dst.write(icon_data)
|
|
1766
|
-
click.echo(click.style(" Installed icon", fg=
|
|
2011
|
+
click.echo(click.style(" Installed icon", fg="green"))
|
|
1767
2012
|
except Exception as e:
|
|
1768
|
-
click.echo(click.style(f" Warning: Could not copy icon: {e}", fg=
|
|
2013
|
+
click.echo(click.style(f" Warning: Could not copy icon: {e}", fg="yellow"))
|
|
1769
2014
|
icon_dest = "utilities-terminal" # Fallback to system icon
|
|
1770
2015
|
|
|
1771
2016
|
# Create .desktop file
|
|
@@ -1781,21 +2026,27 @@ Keywords=pentest;security;hacking;nmap;metasploit;
|
|
|
1781
2026
|
"""
|
|
1782
2027
|
|
|
1783
2028
|
desktop_file.write_text(desktop_content)
|
|
1784
|
-
click.echo(click.style(" Created desktop entry", fg=
|
|
2029
|
+
click.echo(click.style(" Created desktop entry", fg="green"))
|
|
1785
2030
|
|
|
1786
2031
|
# Update desktop database (optional, may not be available)
|
|
1787
2032
|
try:
|
|
1788
2033
|
import subprocess
|
|
1789
|
-
|
|
1790
|
-
|
|
2034
|
+
|
|
2035
|
+
subprocess.run(
|
|
2036
|
+
["update-desktop-database", str(applications_dir)],
|
|
2037
|
+
capture_output=True,
|
|
2038
|
+
check=False,
|
|
2039
|
+
)
|
|
1791
2040
|
except Exception:
|
|
1792
2041
|
pass # Not critical if this fails
|
|
1793
2042
|
|
|
1794
2043
|
click.echo()
|
|
1795
|
-
click.echo(
|
|
2044
|
+
click.echo(
|
|
2045
|
+
click.style("SoulEyez added to Applications menu!", fg="green", bold=True)
|
|
2046
|
+
)
|
|
1796
2047
|
click.echo()
|
|
1797
2048
|
click.echo("You can find it under:")
|
|
1798
|
-
click.echo(click.style(" Applications > Security > SoulEyez", fg=
|
|
2049
|
+
click.echo(click.style(" Applications > Security > SoulEyez", fg="cyan"))
|
|
1799
2050
|
click.echo()
|
|
1800
2051
|
click.echo("To remove: souleyez install-desktop --remove")
|
|
1801
2052
|
|
|
@@ -1805,106 +2056,129 @@ def main():
|
|
|
1805
2056
|
cli()
|
|
1806
2057
|
|
|
1807
2058
|
|
|
1808
|
-
|
|
1809
2059
|
@cli.command()
|
|
1810
|
-
@click.option(
|
|
1811
|
-
@click.confirmation_option(prompt=
|
|
2060
|
+
@click.option("--purge-data", is_flag=True, help="Remove all user data (~/.souleyez)")
|
|
2061
|
+
@click.confirmation_option(prompt="Are you sure you want to uninstall SoulEyez?")
|
|
1812
2062
|
def uninstall(purge_data):
|
|
1813
2063
|
"""Uninstall SoulEyez and optionally remove all user data."""
|
|
1814
2064
|
import subprocess
|
|
1815
2065
|
import shutil
|
|
1816
2066
|
import signal
|
|
1817
|
-
|
|
1818
|
-
click.echo(click.style("\n🗑️ Uninstalling SoulEyez...", fg=
|
|
2067
|
+
|
|
2068
|
+
click.echo(click.style("\n🗑️ Uninstalling SoulEyez...", fg="yellow", bold=True))
|
|
1819
2069
|
click.echo()
|
|
1820
|
-
|
|
2070
|
+
|
|
1821
2071
|
# Stop background worker (both dev mode and CLI mode)
|
|
1822
2072
|
click.echo("1. Stopping background worker...")
|
|
1823
2073
|
try:
|
|
1824
|
-
subprocess.run(
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2074
|
+
subprocess.run(
|
|
2075
|
+
["pkill", "-f", "souleyez.engine.background"],
|
|
2076
|
+
capture_output=True,
|
|
2077
|
+
check=False,
|
|
2078
|
+
)
|
|
2079
|
+
subprocess.run(
|
|
2080
|
+
["pkill", "-f", "souleyez worker"], capture_output=True, check=False
|
|
2081
|
+
)
|
|
2082
|
+
click.echo(click.style(" ✓ Worker stopped", fg="green"))
|
|
1829
2083
|
except Exception as e:
|
|
1830
|
-
click.echo(click.style(f" ⚠ Could not stop worker: {e}", fg=
|
|
1831
|
-
|
|
2084
|
+
click.echo(click.style(f" ⚠ Could not stop worker: {e}", fg="yellow"))
|
|
2085
|
+
|
|
1832
2086
|
# Remove user data if requested
|
|
1833
2087
|
if purge_data:
|
|
1834
2088
|
click.echo("\n2. Removing user data...")
|
|
1835
|
-
data_dir = Path.home() /
|
|
2089
|
+
data_dir = Path.home() / ".souleyez"
|
|
1836
2090
|
if data_dir.exists():
|
|
1837
|
-
click.echo(
|
|
2091
|
+
click.echo(
|
|
2092
|
+
click.style(f" ⚠ WARNING: This will delete:", fg="yellow", bold=True)
|
|
2093
|
+
)
|
|
1838
2094
|
click.echo(f" • Database: {data_dir / 'souleyez.db'}")
|
|
1839
2095
|
click.echo(f" • Crypto keys: {data_dir / 'crypto.json'}")
|
|
1840
2096
|
click.echo(f" • Logs: {data_dir / 'souleyez.log'}")
|
|
1841
2097
|
click.echo(f" • All engagement data")
|
|
1842
2098
|
click.echo()
|
|
1843
|
-
|
|
1844
|
-
if click.confirm(
|
|
2099
|
+
|
|
2100
|
+
if click.confirm(
|
|
2101
|
+
click.style(" Delete ALL user data?", fg="red", bold=True)
|
|
2102
|
+
):
|
|
1845
2103
|
try:
|
|
1846
2104
|
shutil.rmtree(data_dir)
|
|
1847
|
-
click.echo(click.style(f" ✓ Removed {data_dir}", fg=
|
|
2105
|
+
click.echo(click.style(f" ✓ Removed {data_dir}", fg="green"))
|
|
1848
2106
|
except Exception as e:
|
|
1849
|
-
click.echo(click.style(f" ✗ Error: {e}", fg=
|
|
2107
|
+
click.echo(click.style(f" ✗ Error: {e}", fg="red"))
|
|
1850
2108
|
else:
|
|
1851
|
-
click.echo(click.style(" → User data preserved", fg=
|
|
2109
|
+
click.echo(click.style(" → User data preserved", fg="cyan"))
|
|
1852
2110
|
else:
|
|
1853
|
-
click.echo(click.style(" ℹ No user data found", fg=
|
|
2111
|
+
click.echo(click.style(" ℹ No user data found", fg="cyan"))
|
|
1854
2112
|
else:
|
|
1855
|
-
click.echo(click.style("\n2. User data preserved in ~/.souleyez", fg=
|
|
2113
|
+
click.echo(click.style("\n2. User data preserved in ~/.souleyez", fg="cyan"))
|
|
1856
2114
|
click.echo(" • Your engagements and database are safe")
|
|
1857
2115
|
click.echo(" • Reinstalling will restore access to your data")
|
|
1858
2116
|
click.echo(" • To remove data: souleyez uninstall --purge-data")
|
|
1859
|
-
|
|
2117
|
+
|
|
1860
2118
|
# Remove application with pipx
|
|
1861
2119
|
click.echo("\n3. Removing application...")
|
|
1862
2120
|
click.echo(" Run: pipx uninstall souleyez")
|
|
1863
2121
|
click.echo()
|
|
1864
|
-
|
|
2122
|
+
|
|
1865
2123
|
try:
|
|
1866
|
-
result = subprocess.run(
|
|
1867
|
-
|
|
2124
|
+
result = subprocess.run(
|
|
2125
|
+
["pipx", "uninstall", "souleyez"],
|
|
2126
|
+
capture_output=True,
|
|
2127
|
+
text=True,
|
|
2128
|
+
check=False,
|
|
2129
|
+
)
|
|
1868
2130
|
if result.returncode == 0:
|
|
1869
|
-
click.echo(
|
|
2131
|
+
click.echo(
|
|
2132
|
+
click.style(" ✓ SoulEyez uninstalled successfully", fg="green")
|
|
2133
|
+
)
|
|
1870
2134
|
else:
|
|
1871
|
-
click.echo(
|
|
2135
|
+
click.echo(
|
|
2136
|
+
click.style(
|
|
2137
|
+
f" ⚠ pipx uninstall returned: {result.stderr}", fg="yellow"
|
|
2138
|
+
)
|
|
2139
|
+
)
|
|
1872
2140
|
click.echo(" You may need to run manually: pipx uninstall souleyez")
|
|
1873
2141
|
except FileNotFoundError:
|
|
1874
|
-
click.echo(click.style(" ℹ pipx not found - uninstall manually", fg=
|
|
2142
|
+
click.echo(click.style(" ℹ pipx not found - uninstall manually", fg="cyan"))
|
|
1875
2143
|
click.echo(" Run: pip uninstall souleyez")
|
|
1876
2144
|
except Exception as e:
|
|
1877
|
-
click.echo(click.style(f" ✗ Error: {e}", fg=
|
|
1878
|
-
|
|
2145
|
+
click.echo(click.style(f" ✗ Error: {e}", fg="red"))
|
|
2146
|
+
|
|
1879
2147
|
click.echo()
|
|
1880
2148
|
if purge_data:
|
|
1881
|
-
click.echo(click.style("✓ SoulEyez completely removed", fg=
|
|
2149
|
+
click.echo(click.style("✓ SoulEyez completely removed", fg="green", bold=True))
|
|
1882
2150
|
else:
|
|
1883
|
-
click.echo(
|
|
2151
|
+
click.echo(
|
|
2152
|
+
click.style("✓ SoulEyez removed (data preserved)", fg="green", bold=True)
|
|
2153
|
+
)
|
|
1884
2154
|
click.echo()
|
|
1885
2155
|
|
|
1886
2156
|
|
|
1887
2157
|
# Import and register auth commands (must be before if __name__ block)
|
|
1888
2158
|
from souleyez.commands.auth import login, logout, whoami
|
|
2159
|
+
|
|
1889
2160
|
cli.add_command(login)
|
|
1890
2161
|
cli.add_command(logout)
|
|
1891
2162
|
cli.add_command(whoami)
|
|
1892
2163
|
|
|
1893
2164
|
# Import and register user management commands
|
|
1894
2165
|
from souleyez.commands.user import user
|
|
2166
|
+
|
|
1895
2167
|
cli.add_command(user)
|
|
1896
2168
|
|
|
1897
2169
|
# Import and register license management commands
|
|
1898
2170
|
from souleyez.commands.license import license
|
|
2171
|
+
|
|
1899
2172
|
cli.add_command(license)
|
|
1900
2173
|
|
|
1901
2174
|
|
|
1902
|
-
if __name__ ==
|
|
2175
|
+
if __name__ == "__main__":
|
|
1903
2176
|
main()
|
|
1904
2177
|
|
|
1905
2178
|
|
|
1906
2179
|
# ==================== HOST COMMANDS ====================
|
|
1907
2180
|
|
|
2181
|
+
|
|
1908
2182
|
@cli.group()
|
|
1909
2183
|
def hosts():
|
|
1910
2184
|
"""Host management commands."""
|
|
@@ -1912,7 +2186,9 @@ def hosts():
|
|
|
1912
2186
|
|
|
1913
2187
|
|
|
1914
2188
|
@hosts.command("list")
|
|
1915
|
-
@click.option(
|
|
2189
|
+
@click.option(
|
|
2190
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2191
|
+
)
|
|
1916
2192
|
@click.option("--all", "-a", is_flag=True, help="Show all hosts (including down hosts)")
|
|
1917
2193
|
@click.option("--status", "-s", default=None, help="Filter by status (up/down/unknown)")
|
|
1918
2194
|
def hosts_list(engagement, all, status):
|
|
@@ -1929,23 +2205,28 @@ def hosts_list(engagement, all, status):
|
|
|
1929
2205
|
else:
|
|
1930
2206
|
eng = em.get_current()
|
|
1931
2207
|
if not eng:
|
|
1932
|
-
click.echo(
|
|
2208
|
+
click.echo(
|
|
2209
|
+
"✗ No engagement selected. Use: souleyez engagement use <name>",
|
|
2210
|
+
err=True,
|
|
2211
|
+
)
|
|
1933
2212
|
return
|
|
1934
2213
|
|
|
1935
2214
|
hm = HostManager()
|
|
1936
|
-
all_hosts = hm.list_hosts(eng[
|
|
2215
|
+
all_hosts = hm.list_hosts(eng["id"])
|
|
1937
2216
|
|
|
1938
2217
|
# Filter hosts
|
|
1939
2218
|
if status:
|
|
1940
|
-
hosts = [h for h in all_hosts if h.get(
|
|
2219
|
+
hosts = [h for h in all_hosts if h.get("status", "unknown") == status]
|
|
1941
2220
|
elif not all:
|
|
1942
2221
|
# Default: only show 'up' hosts
|
|
1943
|
-
hosts = [h for h in all_hosts if h.get(
|
|
2222
|
+
hosts = [h for h in all_hosts if h.get("status", "unknown") == "up"]
|
|
1944
2223
|
else:
|
|
1945
2224
|
hosts = all_hosts
|
|
1946
2225
|
|
|
1947
2226
|
if not hosts:
|
|
1948
|
-
filter_msg =
|
|
2227
|
+
filter_msg = (
|
|
2228
|
+
f" with status='{status}'" if status else " (live only)" if not all else ""
|
|
2229
|
+
)
|
|
1949
2230
|
click.echo(f"No hosts found in workspace '{eng['name']}'{filter_msg}")
|
|
1950
2231
|
return
|
|
1951
2232
|
|
|
@@ -1978,8 +2259,16 @@ def hosts_list(engagement, all, status):
|
|
|
1978
2259
|
@click.argument("ip_address")
|
|
1979
2260
|
@click.option("--hostname", "-n", default=None, help="Hostname")
|
|
1980
2261
|
@click.option("--os", default=None, help="Operating system")
|
|
1981
|
-
@click.option(
|
|
1982
|
-
|
|
2262
|
+
@click.option(
|
|
2263
|
+
"--status",
|
|
2264
|
+
"-s",
|
|
2265
|
+
default="up",
|
|
2266
|
+
type=click.Choice(["up", "down", "unknown"]),
|
|
2267
|
+
help="Host status",
|
|
2268
|
+
)
|
|
2269
|
+
@click.option(
|
|
2270
|
+
"--engagement", "-e", default=None, help="Engagement name (default: current)"
|
|
2271
|
+
)
|
|
1983
2272
|
def hosts_add(ip_address, hostname, os, status, engagement):
|
|
1984
2273
|
"""Manually add a host to engagement."""
|
|
1985
2274
|
from souleyez.storage.hosts import HostManager
|
|
@@ -1994,20 +2283,20 @@ def hosts_add(ip_address, hostname, os, status, engagement):
|
|
|
1994
2283
|
else:
|
|
1995
2284
|
eng = em.get_current()
|
|
1996
2285
|
if not eng:
|
|
1997
|
-
click.echo(
|
|
2286
|
+
click.echo(
|
|
2287
|
+
"✗ No engagement selected. Use: souleyez engagement use <name>",
|
|
2288
|
+
err=True,
|
|
2289
|
+
)
|
|
1998
2290
|
return
|
|
1999
2291
|
|
|
2000
2292
|
hm = HostManager()
|
|
2001
2293
|
|
|
2002
2294
|
try:
|
|
2003
|
-
host_data = {
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
'
|
|
2007
|
-
|
|
2008
|
-
}
|
|
2009
|
-
host_id = hm.add_or_update_host(eng['id'], host_data)
|
|
2010
|
-
click.echo(f"✓ Added host {ip_address} to engagement '{eng['name']}' (id={host_id})")
|
|
2295
|
+
host_data = {"ip": ip_address, "hostname": hostname, "os": os, "status": status}
|
|
2296
|
+
host_id = hm.add_or_update_host(eng["id"], host_data)
|
|
2297
|
+
click.echo(
|
|
2298
|
+
f"✓ Added host {ip_address} to engagement '{eng['name']}' (id={host_id})"
|
|
2299
|
+
)
|
|
2011
2300
|
if hostname:
|
|
2012
2301
|
click.echo(f" Hostname: {hostname}")
|
|
2013
2302
|
if os:
|
|
@@ -2019,11 +2308,13 @@ def hosts_add(ip_address, hostname, os, status, engagement):
|
|
|
2019
2308
|
|
|
2020
2309
|
@hosts.command("show")
|
|
2021
2310
|
@click.argument("ip_address")
|
|
2022
|
-
@click.option(
|
|
2311
|
+
@click.option(
|
|
2312
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2313
|
+
)
|
|
2023
2314
|
def hosts_show(ip_address, engagement):
|
|
2024
2315
|
"""Show detailed host information."""
|
|
2025
2316
|
from souleyez.storage.hosts import HostManager
|
|
2026
|
-
|
|
2317
|
+
|
|
2027
2318
|
em = EngagementManager()
|
|
2028
2319
|
|
|
2029
2320
|
if engagement:
|
|
@@ -2036,16 +2327,18 @@ def hosts_show(ip_address, engagement):
|
|
|
2036
2327
|
if not eng:
|
|
2037
2328
|
click.echo("✗ No engagement selected", err=True)
|
|
2038
2329
|
return
|
|
2039
|
-
|
|
2330
|
+
|
|
2040
2331
|
hm = HostManager()
|
|
2041
|
-
host = hm.get_host_by_ip(eng[
|
|
2042
|
-
|
|
2332
|
+
host = hm.get_host_by_ip(eng["id"], ip_address)
|
|
2333
|
+
|
|
2043
2334
|
if not host:
|
|
2044
|
-
click.echo(
|
|
2335
|
+
click.echo(
|
|
2336
|
+
f"✗ Host {ip_address} not found in workspace '{eng['name']}'", err=True
|
|
2337
|
+
)
|
|
2045
2338
|
return
|
|
2046
|
-
|
|
2047
|
-
services = hm.get_host_services(host[
|
|
2048
|
-
|
|
2339
|
+
|
|
2340
|
+
services = hm.get_host_services(host["id"])
|
|
2341
|
+
|
|
2049
2342
|
click.echo("\n" + "=" * 80)
|
|
2050
2343
|
click.echo(f"HOST: {host['ip_address']}")
|
|
2051
2344
|
click.echo("=" * 80)
|
|
@@ -2055,13 +2348,15 @@ def hosts_show(ip_address, engagement):
|
|
|
2055
2348
|
click.echo(f"MAC: {host.get('mac_address') or 'N/A'}")
|
|
2056
2349
|
click.echo(f"First seen: {host.get('created_at', 'N/A')}")
|
|
2057
2350
|
click.echo(f"Last updated: {host.get('updated_at', 'N/A')}")
|
|
2058
|
-
|
|
2351
|
+
|
|
2059
2352
|
click.echo("\n" + "-" * 80)
|
|
2060
2353
|
click.echo(f"SERVICES ({len(services)})")
|
|
2061
2354
|
click.echo("-" * 80)
|
|
2062
|
-
|
|
2355
|
+
|
|
2063
2356
|
if services:
|
|
2064
|
-
click.echo(
|
|
2357
|
+
click.echo(
|
|
2358
|
+
f"{'Port':<10} {'Protocol':<10} {'State':<10} {'Service':<20} {'Version':<30}"
|
|
2359
|
+
)
|
|
2065
2360
|
click.echo("-" * 80)
|
|
2066
2361
|
for svc in services:
|
|
2067
2362
|
click.echo(
|
|
@@ -2073,24 +2368,37 @@ def hosts_show(ip_address, engagement):
|
|
|
2073
2368
|
)
|
|
2074
2369
|
else:
|
|
2075
2370
|
click.echo("No services found")
|
|
2076
|
-
|
|
2371
|
+
|
|
2077
2372
|
click.echo("=" * 80 + "\n")
|
|
2078
2373
|
|
|
2079
2374
|
|
|
2080
2375
|
@hosts.command("update")
|
|
2081
2376
|
@click.argument("ip_address")
|
|
2082
|
-
@click.option(
|
|
2083
|
-
|
|
2377
|
+
@click.option(
|
|
2378
|
+
"--status",
|
|
2379
|
+
type=click.Choice(["active", "compromised", "offline", "up", "down"]),
|
|
2380
|
+
help="Host status",
|
|
2381
|
+
)
|
|
2382
|
+
@click.option(
|
|
2383
|
+
"--access-level",
|
|
2384
|
+
type=click.Choice(["none", "user", "admin", "root"]),
|
|
2385
|
+
help="Access level gained",
|
|
2386
|
+
)
|
|
2084
2387
|
@click.option("--notes", help="Additional notes about the host")
|
|
2085
|
-
@click.option(
|
|
2388
|
+
@click.option(
|
|
2389
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2390
|
+
)
|
|
2086
2391
|
def hosts_update(ip_address, status, access_level, notes, engagement):
|
|
2087
2392
|
"""Update host status, access level, and notes."""
|
|
2088
2393
|
from souleyez.storage.hosts import HostManager
|
|
2089
|
-
|
|
2394
|
+
|
|
2090
2395
|
if not status and not access_level and notes is None:
|
|
2091
|
-
click.echo(
|
|
2396
|
+
click.echo(
|
|
2397
|
+
"✗ Must provide at least one of --status, --access-level, or --notes",
|
|
2398
|
+
err=True,
|
|
2399
|
+
)
|
|
2092
2400
|
return
|
|
2093
|
-
|
|
2401
|
+
|
|
2094
2402
|
em = EngagementManager()
|
|
2095
2403
|
|
|
2096
2404
|
if engagement:
|
|
@@ -2103,18 +2411,22 @@ def hosts_update(ip_address, status, access_level, notes, engagement):
|
|
|
2103
2411
|
if not eng:
|
|
2104
2412
|
click.echo("✗ No engagement selected", err=True)
|
|
2105
2413
|
return
|
|
2106
|
-
|
|
2414
|
+
|
|
2107
2415
|
hm = HostManager()
|
|
2108
|
-
host = hm.get_host_by_ip(eng[
|
|
2109
|
-
|
|
2416
|
+
host = hm.get_host_by_ip(eng["id"], ip_address)
|
|
2417
|
+
|
|
2110
2418
|
if not host:
|
|
2111
|
-
click.echo(
|
|
2419
|
+
click.echo(
|
|
2420
|
+
f"✗ Host {ip_address} not found in workspace '{eng['name']}'", err=True
|
|
2421
|
+
)
|
|
2112
2422
|
return
|
|
2113
|
-
|
|
2114
|
-
success = hm.update_host_status(
|
|
2115
|
-
|
|
2423
|
+
|
|
2424
|
+
success = hm.update_host_status(
|
|
2425
|
+
host["id"], status=status, access_level=access_level, notes=notes
|
|
2426
|
+
)
|
|
2427
|
+
|
|
2116
2428
|
if success:
|
|
2117
|
-
click.echo(click.style(f"✓ Host {ip_address} updated", fg=
|
|
2429
|
+
click.echo(click.style(f"✓ Host {ip_address} updated", fg="green"))
|
|
2118
2430
|
if status:
|
|
2119
2431
|
click.echo(f" Status: {status}")
|
|
2120
2432
|
if access_level:
|
|
@@ -2122,11 +2434,12 @@ def hosts_update(ip_address, status, access_level, notes, engagement):
|
|
|
2122
2434
|
if notes:
|
|
2123
2435
|
click.echo(f" Notes: {notes}")
|
|
2124
2436
|
else:
|
|
2125
|
-
click.echo(click.style(f"✗ Failed to update host {ip_address}", fg=
|
|
2437
|
+
click.echo(click.style(f"✗ Failed to update host {ip_address}", fg="red"))
|
|
2126
2438
|
|
|
2127
2439
|
|
|
2128
2440
|
# ==================== SERVICE COMMANDS ====================
|
|
2129
2441
|
|
|
2442
|
+
|
|
2130
2443
|
@cli.group()
|
|
2131
2444
|
def services():
|
|
2132
2445
|
"""Service management commands."""
|
|
@@ -2136,11 +2449,18 @@ def services():
|
|
|
2136
2449
|
@services.command("add")
|
|
2137
2450
|
@click.argument("ip_address")
|
|
2138
2451
|
@click.argument("port", type=int)
|
|
2139
|
-
@click.argument("protocol", type=click.Choice([
|
|
2452
|
+
@click.argument("protocol", type=click.Choice(["tcp", "udp"]))
|
|
2140
2453
|
@click.option("--service", "-s", default=None, help="Service name (e.g., ssh, http)")
|
|
2141
2454
|
@click.option("--version", "-v", default=None, help="Service version")
|
|
2142
|
-
@click.option(
|
|
2143
|
-
|
|
2455
|
+
@click.option(
|
|
2456
|
+
"--state",
|
|
2457
|
+
default="open",
|
|
2458
|
+
type=click.Choice(["open", "closed", "filtered"]),
|
|
2459
|
+
help="Service state",
|
|
2460
|
+
)
|
|
2461
|
+
@click.option(
|
|
2462
|
+
"--engagement", "-e", default=None, help="Engagement name (default: current)"
|
|
2463
|
+
)
|
|
2144
2464
|
def services_add(ip_address, port, protocol, service, version, state, engagement):
|
|
2145
2465
|
"""Manually add a service to a host."""
|
|
2146
2466
|
from souleyez.storage.hosts import HostManager
|
|
@@ -2155,35 +2475,37 @@ def services_add(ip_address, port, protocol, service, version, state, engagement
|
|
|
2155
2475
|
else:
|
|
2156
2476
|
eng = em.get_current()
|
|
2157
2477
|
if not eng:
|
|
2158
|
-
click.echo(
|
|
2478
|
+
click.echo(
|
|
2479
|
+
"✗ No engagement selected. Use: souleyez engagement use <name>",
|
|
2480
|
+
err=True,
|
|
2481
|
+
)
|
|
2159
2482
|
return
|
|
2160
2483
|
|
|
2161
2484
|
hm = HostManager()
|
|
2162
2485
|
|
|
2163
2486
|
# Get or create host
|
|
2164
|
-
host = hm.get_host_by_ip(eng[
|
|
2487
|
+
host = hm.get_host_by_ip(eng["id"], ip_address)
|
|
2165
2488
|
if not host:
|
|
2166
2489
|
# Create host first
|
|
2167
2490
|
click.echo(f"Host {ip_address} not found, creating it...")
|
|
2168
|
-
host_data = {
|
|
2169
|
-
|
|
2170
|
-
'status': 'up'
|
|
2171
|
-
}
|
|
2172
|
-
host_id = hm.add_or_update_host(eng['id'], host_data)
|
|
2491
|
+
host_data = {"ip": ip_address, "status": "up"}
|
|
2492
|
+
host_id = hm.add_or_update_host(eng["id"], host_data)
|
|
2173
2493
|
else:
|
|
2174
|
-
host_id = host[
|
|
2494
|
+
host_id = host["id"]
|
|
2175
2495
|
|
|
2176
2496
|
# Add service
|
|
2177
2497
|
try:
|
|
2178
2498
|
service_data = {
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2499
|
+
"port": port,
|
|
2500
|
+
"protocol": protocol,
|
|
2501
|
+
"state": state,
|
|
2502
|
+
"service": service,
|
|
2503
|
+
"version": version,
|
|
2184
2504
|
}
|
|
2185
2505
|
service_id = hm.add_service(host_id, service_data)
|
|
2186
|
-
click.echo(
|
|
2506
|
+
click.echo(
|
|
2507
|
+
f"✓ Added service {ip_address}:{port}/{protocol} to engagement '{eng['name']}' (id={service_id})"
|
|
2508
|
+
)
|
|
2187
2509
|
if service:
|
|
2188
2510
|
click.echo(f" Service: {service}")
|
|
2189
2511
|
if version:
|
|
@@ -2194,14 +2516,16 @@ def services_add(ip_address, port, protocol, service, version, state, engagement
|
|
|
2194
2516
|
|
|
2195
2517
|
|
|
2196
2518
|
@services.command("list")
|
|
2197
|
-
@click.option(
|
|
2519
|
+
@click.option(
|
|
2520
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2521
|
+
)
|
|
2198
2522
|
@click.option("--port", "-p", type=int, default=None, help="Filter by port")
|
|
2199
2523
|
def services_list(engagement, port):
|
|
2200
2524
|
"""List all services across all hosts."""
|
|
2201
2525
|
from souleyez.storage.hosts import HostManager
|
|
2202
|
-
|
|
2526
|
+
|
|
2203
2527
|
em = EngagementManager()
|
|
2204
|
-
|
|
2528
|
+
|
|
2205
2529
|
if engagement:
|
|
2206
2530
|
eng = em.get(engagement)
|
|
2207
2531
|
if not eng:
|
|
@@ -2212,36 +2536,40 @@ def services_list(engagement, port):
|
|
|
2212
2536
|
if not eng:
|
|
2213
2537
|
click.echo("✗ No engagement selected", err=True)
|
|
2214
2538
|
return
|
|
2215
|
-
|
|
2539
|
+
|
|
2216
2540
|
hm = HostManager()
|
|
2217
|
-
|
|
2541
|
+
|
|
2218
2542
|
# Get all hosts and their services
|
|
2219
|
-
hosts = hm.list_hosts(eng[
|
|
2220
|
-
|
|
2543
|
+
hosts = hm.list_hosts(eng["id"])
|
|
2544
|
+
|
|
2221
2545
|
all_services = []
|
|
2222
2546
|
for host in hosts:
|
|
2223
|
-
services = hm.get_host_services(host[
|
|
2547
|
+
services = hm.get_host_services(host["id"])
|
|
2224
2548
|
for svc in services:
|
|
2225
|
-
if port is None or svc[
|
|
2226
|
-
all_services.append(
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2549
|
+
if port is None or svc["port"] == port:
|
|
2550
|
+
all_services.append(
|
|
2551
|
+
{
|
|
2552
|
+
"host_ip": host["ip_address"],
|
|
2553
|
+
"host_name": host.get("hostname"),
|
|
2554
|
+
**svc,
|
|
2555
|
+
}
|
|
2556
|
+
)
|
|
2557
|
+
|
|
2232
2558
|
if not all_services:
|
|
2233
2559
|
click.echo(f"No services found in workspace '{eng['name']}'")
|
|
2234
2560
|
return
|
|
2235
|
-
|
|
2561
|
+
|
|
2236
2562
|
click.echo("\n" + "=" * 120)
|
|
2237
2563
|
click.echo(f"SERVICES - Engagement: {eng['name']}")
|
|
2238
2564
|
if port:
|
|
2239
2565
|
click.echo(f"Filtered by port: {port}")
|
|
2240
2566
|
click.echo("=" * 120)
|
|
2241
|
-
click.echo(
|
|
2567
|
+
click.echo(
|
|
2568
|
+
f"{'Host':<18} {'Port':<8} {'Proto':<8} {'State':<10} {'Service':<20} {'Version':<40}"
|
|
2569
|
+
)
|
|
2242
2570
|
click.echo("=" * 120)
|
|
2243
|
-
|
|
2244
|
-
for svc in sorted(all_services, key=lambda x: (x[
|
|
2571
|
+
|
|
2572
|
+
for svc in sorted(all_services, key=lambda x: (x["host_ip"], x["port"])):
|
|
2245
2573
|
click.echo(
|
|
2246
2574
|
f"{svc['host_ip']:<18} "
|
|
2247
2575
|
f"{svc['port']:<8} "
|
|
@@ -2250,13 +2578,14 @@ def services_list(engagement, port):
|
|
|
2250
2578
|
f"{(svc.get('service_name') or 'unknown')[:19]:<20} "
|
|
2251
2579
|
f"{(svc.get('service_version') or 'N/A')[:39]:<40}"
|
|
2252
2580
|
)
|
|
2253
|
-
|
|
2581
|
+
|
|
2254
2582
|
click.echo("=" * 120)
|
|
2255
2583
|
click.echo(f"Total: {len(all_services)} services\n")
|
|
2256
2584
|
|
|
2257
2585
|
|
|
2258
2586
|
# ==================== FINDINGS COMMANDS ====================
|
|
2259
2587
|
|
|
2588
|
+
|
|
2260
2589
|
@cli.group()
|
|
2261
2590
|
@require_password
|
|
2262
2591
|
def findings():
|
|
@@ -2265,8 +2594,15 @@ def findings():
|
|
|
2265
2594
|
|
|
2266
2595
|
|
|
2267
2596
|
@findings.command("list")
|
|
2268
|
-
@click.option(
|
|
2269
|
-
|
|
2597
|
+
@click.option(
|
|
2598
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2599
|
+
)
|
|
2600
|
+
@click.option(
|
|
2601
|
+
"--severity",
|
|
2602
|
+
"-s",
|
|
2603
|
+
default=None,
|
|
2604
|
+
help="Filter by severity (critical, high, medium, low, info)",
|
|
2605
|
+
)
|
|
2270
2606
|
@click.option("--tool", "-t", default=None, help="Filter by tool")
|
|
2271
2607
|
@click.option("--host", "-h", default=None, help="Filter by host IP")
|
|
2272
2608
|
def findings_list(engagement, severity, tool, host):
|
|
@@ -2292,14 +2628,17 @@ def findings_list(engagement, severity, tool, host):
|
|
|
2292
2628
|
host_id = None
|
|
2293
2629
|
if host:
|
|
2294
2630
|
from souleyez.storage.hosts import HostManager
|
|
2631
|
+
|
|
2295
2632
|
hm = HostManager()
|
|
2296
|
-
host_obj = hm.get_host_by_ip(eng[
|
|
2633
|
+
host_obj = hm.get_host_by_ip(eng["id"], host)
|
|
2297
2634
|
if not host_obj:
|
|
2298
2635
|
click.echo(f"✗ Host {host} not found", err=True)
|
|
2299
2636
|
return
|
|
2300
|
-
host_id = host_obj[
|
|
2637
|
+
host_id = host_obj["id"]
|
|
2301
2638
|
|
|
2302
|
-
findings = fm.list_findings(
|
|
2639
|
+
findings = fm.list_findings(
|
|
2640
|
+
eng["id"], host_id=host_id, severity=severity, tool=tool
|
|
2641
|
+
)
|
|
2303
2642
|
|
|
2304
2643
|
if not findings:
|
|
2305
2644
|
click.echo(f"No findings found in workspace '{eng['name']}'")
|
|
@@ -2307,11 +2646,11 @@ def findings_list(engagement, severity, tool, host):
|
|
|
2307
2646
|
|
|
2308
2647
|
# Get severity color mapping
|
|
2309
2648
|
severity_colors = {
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2649
|
+
"critical": "red",
|
|
2650
|
+
"high": "red",
|
|
2651
|
+
"medium": "yellow",
|
|
2652
|
+
"low": "blue",
|
|
2653
|
+
"info": "white",
|
|
2315
2654
|
}
|
|
2316
2655
|
|
|
2317
2656
|
click.echo("\n" + "=" * 140)
|
|
@@ -2321,11 +2660,13 @@ def findings_list(engagement, severity, tool, host):
|
|
|
2321
2660
|
if tool:
|
|
2322
2661
|
click.echo(f"Filtered by tool: {tool}")
|
|
2323
2662
|
click.echo("=" * 140)
|
|
2324
|
-
click.echo(
|
|
2663
|
+
click.echo(
|
|
2664
|
+
f"{'ID':<6} {'Severity':<10} {'Host':<18} {'Port':<6} {'Tool':<10} {'Title':<80}"
|
|
2665
|
+
)
|
|
2325
2666
|
click.echo("=" * 140)
|
|
2326
2667
|
|
|
2327
2668
|
for finding in findings:
|
|
2328
|
-
sev_color = severity_colors.get(finding.get(
|
|
2669
|
+
sev_color = severity_colors.get(finding.get("severity", "info"), "white")
|
|
2329
2670
|
click.echo(
|
|
2330
2671
|
f"{finding['id']:<6} "
|
|
2331
2672
|
f"{click.style(finding.get('severity', 'info').upper()[:9], fg=sev_color):<19} "
|
|
@@ -2362,13 +2703,13 @@ def findings_show(finding_id):
|
|
|
2362
2703
|
click.echo(f"\nDescription:")
|
|
2363
2704
|
click.echo(f" {finding.get('description', 'N/A')}")
|
|
2364
2705
|
|
|
2365
|
-
if finding.get(
|
|
2706
|
+
if finding.get("path"):
|
|
2366
2707
|
click.echo(f"\nPath: {finding['path']}")
|
|
2367
2708
|
|
|
2368
|
-
if finding.get(
|
|
2709
|
+
if finding.get("port"):
|
|
2369
2710
|
click.echo(f"Port: {finding['port']}")
|
|
2370
2711
|
|
|
2371
|
-
if finding.get(
|
|
2712
|
+
if finding.get("refs"):
|
|
2372
2713
|
click.echo(f"\nReference: {finding['refs']}")
|
|
2373
2714
|
|
|
2374
2715
|
click.echo(f"\nDiscovered: {finding.get('created_at', 'N/A')}")
|
|
@@ -2376,7 +2717,9 @@ def findings_show(finding_id):
|
|
|
2376
2717
|
|
|
2377
2718
|
|
|
2378
2719
|
@findings.command("summary")
|
|
2379
|
-
@click.option(
|
|
2720
|
+
@click.option(
|
|
2721
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2722
|
+
)
|
|
2380
2723
|
def findings_summary(engagement):
|
|
2381
2724
|
"""Show findings summary by severity."""
|
|
2382
2725
|
from souleyez.storage.findings import FindingsManager
|
|
@@ -2395,7 +2738,7 @@ def findings_summary(engagement):
|
|
|
2395
2738
|
return
|
|
2396
2739
|
|
|
2397
2740
|
fm = FindingsManager()
|
|
2398
|
-
summary = fm.get_findings_summary(eng[
|
|
2741
|
+
summary = fm.get_findings_summary(eng["id"])
|
|
2399
2742
|
|
|
2400
2743
|
total = sum(summary.values())
|
|
2401
2744
|
|
|
@@ -2405,17 +2748,17 @@ def findings_summary(engagement):
|
|
|
2405
2748
|
click.echo(f"{'Severity':<15} {'Count':<10} {'Percentage':<15}")
|
|
2406
2749
|
click.echo("=" * 60)
|
|
2407
2750
|
|
|
2408
|
-
for severity in [
|
|
2751
|
+
for severity in ["critical", "high", "medium", "low", "info"]:
|
|
2409
2752
|
count = summary.get(severity, 0)
|
|
2410
2753
|
pct = (count / total * 100) if total > 0 else 0
|
|
2411
2754
|
|
|
2412
2755
|
color = {
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
}.get(severity,
|
|
2756
|
+
"critical": "red",
|
|
2757
|
+
"high": "red",
|
|
2758
|
+
"medium": "yellow",
|
|
2759
|
+
"low": "blue",
|
|
2760
|
+
"info": "white",
|
|
2761
|
+
}.get(severity, "white")
|
|
2419
2762
|
|
|
2420
2763
|
click.echo(
|
|
2421
2764
|
f"{click.style(severity.upper(), fg=color):<24} "
|
|
@@ -2430,6 +2773,7 @@ def findings_summary(engagement):
|
|
|
2430
2773
|
|
|
2431
2774
|
# ==================== OSINT COMMANDS ====================
|
|
2432
2775
|
|
|
2776
|
+
|
|
2433
2777
|
@cli.group()
|
|
2434
2778
|
@require_password
|
|
2435
2779
|
def osint():
|
|
@@ -2438,8 +2782,12 @@ def osint():
|
|
|
2438
2782
|
|
|
2439
2783
|
|
|
2440
2784
|
@osint.command("list")
|
|
2441
|
-
@click.option(
|
|
2442
|
-
|
|
2785
|
+
@click.option(
|
|
2786
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2787
|
+
)
|
|
2788
|
+
@click.option(
|
|
2789
|
+
"--type", "-t", default=None, help="Filter by data type (email, host, ip, url, asn)"
|
|
2790
|
+
)
|
|
2443
2791
|
@click.option("--source", "-s", default=None, help="Filter by source tool")
|
|
2444
2792
|
def osint_list(engagement, type, source):
|
|
2445
2793
|
"""List all OSINT data in engagement."""
|
|
@@ -2459,7 +2807,7 @@ def osint_list(engagement, type, source):
|
|
|
2459
2807
|
return
|
|
2460
2808
|
|
|
2461
2809
|
om = OsintManager()
|
|
2462
|
-
osint_data = om.list_osint_data(eng[
|
|
2810
|
+
osint_data = om.list_osint_data(eng["id"], data_type=type, source=source)
|
|
2463
2811
|
|
|
2464
2812
|
if not osint_data:
|
|
2465
2813
|
click.echo(f"No OSINT data found in workspace '{eng['name']}'")
|
|
@@ -2472,7 +2820,9 @@ def osint_list(engagement, type, source):
|
|
|
2472
2820
|
if source:
|
|
2473
2821
|
click.echo(f"Filtered by source: {source}")
|
|
2474
2822
|
click.echo("=" * 120)
|
|
2475
|
-
click.echo(
|
|
2823
|
+
click.echo(
|
|
2824
|
+
f"{'ID':<6} {'Type':<12} {'Source':<15} {'Value':<70} {'Discovered':<20}"
|
|
2825
|
+
)
|
|
2476
2826
|
click.echo("=" * 120)
|
|
2477
2827
|
|
|
2478
2828
|
for item in osint_data:
|
|
@@ -2489,7 +2839,9 @@ def osint_list(engagement, type, source):
|
|
|
2489
2839
|
|
|
2490
2840
|
|
|
2491
2841
|
@osint.command("summary")
|
|
2492
|
-
@click.option(
|
|
2842
|
+
@click.option(
|
|
2843
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2844
|
+
)
|
|
2493
2845
|
def osint_summary(engagement):
|
|
2494
2846
|
"""Show OSINT data summary by type."""
|
|
2495
2847
|
from souleyez.storage.osint import OsintManager
|
|
@@ -2508,7 +2860,7 @@ def osint_summary(engagement):
|
|
|
2508
2860
|
return
|
|
2509
2861
|
|
|
2510
2862
|
om = OsintManager()
|
|
2511
|
-
summary = om.get_osint_summary(eng[
|
|
2863
|
+
summary = om.get_osint_summary(eng["id"])
|
|
2512
2864
|
|
|
2513
2865
|
total = sum(summary.values())
|
|
2514
2866
|
|
|
@@ -2526,20 +2878,16 @@ def osint_summary(engagement):
|
|
|
2526
2878
|
count = summary[data_type]
|
|
2527
2879
|
pct = (count / total * 100) if total > 0 else 0
|
|
2528
2880
|
|
|
2529
|
-
click.echo(
|
|
2530
|
-
f"{data_type:<15} "
|
|
2531
|
-
f"{count:<10} "
|
|
2532
|
-
f"{pct:.1f}%"
|
|
2533
|
-
)
|
|
2881
|
+
click.echo(f"{data_type:<15} " f"{count:<10} " f"{pct:.1f}%")
|
|
2534
2882
|
|
|
2535
2883
|
click.echo("=" * 60)
|
|
2536
2884
|
click.echo(f"{'TOTAL':<15} {total}")
|
|
2537
2885
|
click.echo("=" * 60 + "\n")
|
|
2538
2886
|
|
|
2539
2887
|
|
|
2540
|
-
|
|
2541
2888
|
# ==================== WEB PATHS COMMANDS ====================
|
|
2542
2889
|
|
|
2890
|
+
|
|
2543
2891
|
@cli.group()
|
|
2544
2892
|
def paths():
|
|
2545
2893
|
"""Web paths/directories management commands."""
|
|
@@ -2547,8 +2895,12 @@ def paths():
|
|
|
2547
2895
|
|
|
2548
2896
|
|
|
2549
2897
|
@paths.command("list")
|
|
2550
|
-
@click.option(
|
|
2551
|
-
|
|
2898
|
+
@click.option(
|
|
2899
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2900
|
+
)
|
|
2901
|
+
@click.option(
|
|
2902
|
+
"--status", "-s", type=int, default=None, help="Filter by HTTP status code"
|
|
2903
|
+
)
|
|
2552
2904
|
@click.option("--host", "-h", default=None, help="Filter by host IP or hostname")
|
|
2553
2905
|
def paths_list(engagement, status, host):
|
|
2554
2906
|
"""List discovered web paths."""
|
|
@@ -2574,10 +2926,10 @@ def paths_list(engagement, status, host):
|
|
|
2574
2926
|
host_id = None
|
|
2575
2927
|
if host:
|
|
2576
2928
|
hm = HostManager()
|
|
2577
|
-
hosts = hm.list_hosts(eng[
|
|
2929
|
+
hosts = hm.list_hosts(eng["id"])
|
|
2578
2930
|
for h in hosts:
|
|
2579
|
-
if h.get(
|
|
2580
|
-
host_id = h[
|
|
2931
|
+
if h.get("hostname") == host or h.get("ip_address") == host:
|
|
2932
|
+
host_id = h["id"]
|
|
2581
2933
|
break
|
|
2582
2934
|
if not host_id:
|
|
2583
2935
|
click.echo(f"✗ Host {host} not found", err=True)
|
|
@@ -2587,7 +2939,7 @@ def paths_list(engagement, status, host):
|
|
|
2587
2939
|
if host_id:
|
|
2588
2940
|
paths = wpm.list_web_paths(host_id=host_id, status_code=status)
|
|
2589
2941
|
else:
|
|
2590
|
-
paths = wpm.list_web_paths(engagement_id=eng[
|
|
2942
|
+
paths = wpm.list_web_paths(engagement_id=eng["id"], status_code=status)
|
|
2591
2943
|
|
|
2592
2944
|
if not paths:
|
|
2593
2945
|
click.echo(f"No web paths found in workspace '{eng['name']}'")
|
|
@@ -2604,18 +2956,18 @@ def paths_list(engagement, status, host):
|
|
|
2604
2956
|
click.echo("=" * 140)
|
|
2605
2957
|
|
|
2606
2958
|
for path in paths:
|
|
2607
|
-
status_code = path.get(
|
|
2959
|
+
status_code = path.get("status_code", "N/A")
|
|
2608
2960
|
# Color code status
|
|
2609
2961
|
if status_code == 200:
|
|
2610
|
-
status_str = click.style(str(status_code), fg=
|
|
2962
|
+
status_str = click.style(str(status_code), fg="green")
|
|
2611
2963
|
elif 300 <= status_code < 400:
|
|
2612
|
-
status_str = click.style(str(status_code), fg=
|
|
2964
|
+
status_str = click.style(str(status_code), fg="yellow")
|
|
2613
2965
|
elif 400 <= status_code < 500:
|
|
2614
|
-
status_str = click.style(str(status_code), fg=
|
|
2966
|
+
status_str = click.style(str(status_code), fg="red")
|
|
2615
2967
|
else:
|
|
2616
2968
|
status_str = str(status_code)
|
|
2617
2969
|
|
|
2618
|
-
host_info = path.get(
|
|
2970
|
+
host_info = path.get("hostname") or path.get("ip_address") or "N/A"
|
|
2619
2971
|
|
|
2620
2972
|
click.echo(
|
|
2621
2973
|
f"{path['id']:<6} "
|
|
@@ -2630,7 +2982,9 @@ def paths_list(engagement, status, host):
|
|
|
2630
2982
|
|
|
2631
2983
|
|
|
2632
2984
|
@paths.command("summary")
|
|
2633
|
-
@click.option(
|
|
2985
|
+
@click.option(
|
|
2986
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
2987
|
+
)
|
|
2634
2988
|
def paths_summary(engagement):
|
|
2635
2989
|
"""Show web paths summary by status code."""
|
|
2636
2990
|
from souleyez.storage.web_paths import WebPathsManager
|
|
@@ -2649,7 +3003,7 @@ def paths_summary(engagement):
|
|
|
2649
3003
|
return
|
|
2650
3004
|
|
|
2651
3005
|
wpm = WebPathsManager()
|
|
2652
|
-
summary = wpm.get_paths_summary(eng[
|
|
3006
|
+
summary = wpm.get_paths_summary(eng["id"])
|
|
2653
3007
|
|
|
2654
3008
|
total = sum(summary.values())
|
|
2655
3009
|
|
|
@@ -2663,18 +3017,20 @@ def paths_summary(engagement):
|
|
|
2663
3017
|
click.echo(f"{'Status Code':<15} {'Count':<10} {'Percentage':<15}")
|
|
2664
3018
|
click.echo("=" * 60)
|
|
2665
3019
|
|
|
2666
|
-
for status_code in sorted(
|
|
3020
|
+
for status_code in sorted(
|
|
3021
|
+
summary.keys(), key=lambda x: int(x) if x.isdigit() else 999
|
|
3022
|
+
):
|
|
2667
3023
|
count = summary[status_code]
|
|
2668
3024
|
pct = (count / total * 100) if total > 0 else 0
|
|
2669
3025
|
|
|
2670
3026
|
# Color code
|
|
2671
3027
|
status_int = int(status_code) if status_code.isdigit() else 0
|
|
2672
3028
|
if status_int == 200:
|
|
2673
|
-
status_display = click.style(status_code, fg=
|
|
3029
|
+
status_display = click.style(status_code, fg="green")
|
|
2674
3030
|
elif 300 <= status_int < 400:
|
|
2675
|
-
status_display = click.style(status_code, fg=
|
|
3031
|
+
status_display = click.style(status_code, fg="yellow")
|
|
2676
3032
|
elif 400 <= status_int < 500:
|
|
2677
|
-
status_display = click.style(status_code, fg=
|
|
3033
|
+
status_display = click.style(status_code, fg="red")
|
|
2678
3034
|
else:
|
|
2679
3035
|
status_display = status_code
|
|
2680
3036
|
|
|
@@ -2691,6 +3047,7 @@ def paths_summary(engagement):
|
|
|
2691
3047
|
|
|
2692
3048
|
# ==================== CREDENTIALS COMMANDS ====================
|
|
2693
3049
|
|
|
3050
|
+
|
|
2694
3051
|
@cli.group()
|
|
2695
3052
|
def creds():
|
|
2696
3053
|
"""Credentials management - similar to MSF's creds command."""
|
|
@@ -2700,11 +3057,20 @@ def creds():
|
|
|
2700
3057
|
@creds.command("add")
|
|
2701
3058
|
@click.argument("username")
|
|
2702
3059
|
@click.argument("password")
|
|
2703
|
-
@click.option(
|
|
3060
|
+
@click.option(
|
|
3061
|
+
"--service", "-s", default=None, help="Service type (ssh, smb, mysql, etc.)"
|
|
3062
|
+
)
|
|
2704
3063
|
@click.option("--host", "-h", default=None, help="Host IP address")
|
|
2705
3064
|
@click.option("--port", "-p", type=int, default=None, help="Port number")
|
|
2706
|
-
@click.option(
|
|
2707
|
-
|
|
3065
|
+
@click.option(
|
|
3066
|
+
"--status",
|
|
3067
|
+
default="untested",
|
|
3068
|
+
type=click.Choice(["valid", "invalid", "untested"]),
|
|
3069
|
+
help="Credential status",
|
|
3070
|
+
)
|
|
3071
|
+
@click.option(
|
|
3072
|
+
"--engagement", "-e", default=None, help="Engagement name (default: current)"
|
|
3073
|
+
)
|
|
2708
3074
|
def creds_add(username, password, service, host, port, status, engagement):
|
|
2709
3075
|
"""Manually add credentials to engagement."""
|
|
2710
3076
|
from souleyez.storage.credentials import CredentialsManager
|
|
@@ -2720,34 +3086,37 @@ def creds_add(username, password, service, host, port, status, engagement):
|
|
|
2720
3086
|
else:
|
|
2721
3087
|
eng = em.get_current()
|
|
2722
3088
|
if not eng:
|
|
2723
|
-
click.echo(
|
|
3089
|
+
click.echo(
|
|
3090
|
+
"✗ No engagement selected. Use: souleyez engagement use <name>",
|
|
3091
|
+
err=True,
|
|
3092
|
+
)
|
|
2724
3093
|
return
|
|
2725
3094
|
|
|
2726
3095
|
# Get host_id if host specified
|
|
2727
3096
|
host_id = None
|
|
2728
3097
|
if host:
|
|
2729
3098
|
hm = HostManager()
|
|
2730
|
-
host_obj = hm.get_host_by_ip(eng[
|
|
3099
|
+
host_obj = hm.get_host_by_ip(eng["id"], host)
|
|
2731
3100
|
if not host_obj:
|
|
2732
3101
|
# Create host if it doesn't exist
|
|
2733
3102
|
click.echo(f"Host {host} not found, creating it...")
|
|
2734
|
-
host_data = {
|
|
2735
|
-
host_id = hm.add_or_update_host(eng[
|
|
3103
|
+
host_data = {"ip": host, "status": "up"}
|
|
3104
|
+
host_id = hm.add_or_update_host(eng["id"], host_data)
|
|
2736
3105
|
else:
|
|
2737
|
-
host_id = host_obj[
|
|
3106
|
+
host_id = host_obj["id"]
|
|
2738
3107
|
|
|
2739
3108
|
cm = CredentialsManager()
|
|
2740
3109
|
|
|
2741
3110
|
try:
|
|
2742
3111
|
cred_id = cm.add_credential(
|
|
2743
|
-
engagement_id=eng[
|
|
3112
|
+
engagement_id=eng["id"],
|
|
2744
3113
|
host_id=host_id,
|
|
2745
3114
|
username=username,
|
|
2746
3115
|
password=password,
|
|
2747
3116
|
service=service,
|
|
2748
3117
|
port=port,
|
|
2749
3118
|
status=status,
|
|
2750
|
-
tool=
|
|
3119
|
+
tool="manual",
|
|
2751
3120
|
)
|
|
2752
3121
|
click.echo(f"✓ Added credential to engagement '{eng['name']}' (id={cred_id})")
|
|
2753
3122
|
click.echo(f" Username: {username}")
|
|
@@ -2764,8 +3133,12 @@ def creds_add(username, password, service, host, port, status, engagement):
|
|
|
2764
3133
|
|
|
2765
3134
|
|
|
2766
3135
|
@creds.command("list")
|
|
2767
|
-
@click.option(
|
|
2768
|
-
|
|
3136
|
+
@click.option(
|
|
3137
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
3138
|
+
)
|
|
3139
|
+
@click.option(
|
|
3140
|
+
"--service", "-s", default=None, help="Filter by service (ssh, smb, mysql, etc.)"
|
|
3141
|
+
)
|
|
2769
3142
|
@click.option("--status", "-t", default=None, help="Filter by status (valid, untested)")
|
|
2770
3143
|
@click.option("--host", "-h", default=None, help="Filter by host IP")
|
|
2771
3144
|
def creds_list(engagement, service, status, host):
|
|
@@ -2796,13 +3169,15 @@ def creds_list(engagement, service, status, host):
|
|
|
2796
3169
|
host_id = None
|
|
2797
3170
|
if host:
|
|
2798
3171
|
hm = HostManager()
|
|
2799
|
-
host_obj = hm.get_host_by_ip(eng[
|
|
3172
|
+
host_obj = hm.get_host_by_ip(eng["id"], host)
|
|
2800
3173
|
if not host_obj:
|
|
2801
3174
|
click.echo(f"✗ Host {host} not found", err=True)
|
|
2802
3175
|
return
|
|
2803
|
-
host_id = host_obj[
|
|
3176
|
+
host_id = host_obj["id"]
|
|
2804
3177
|
|
|
2805
|
-
creds = cm.list_credentials(
|
|
3178
|
+
creds = cm.list_credentials(
|
|
3179
|
+
eng["id"], host_id=host_id, service=service, status=status
|
|
3180
|
+
)
|
|
2806
3181
|
|
|
2807
3182
|
if not creds:
|
|
2808
3183
|
filter_msg = ""
|
|
@@ -2814,7 +3189,7 @@ def creds_list(engagement, service, status, host):
|
|
|
2814
3189
|
return
|
|
2815
3190
|
|
|
2816
3191
|
# Get stats
|
|
2817
|
-
stats = cm.get_stats(eng[
|
|
3192
|
+
stats = cm.get_stats(eng["id"])
|
|
2818
3193
|
|
|
2819
3194
|
click.echo("\n" + "=" * 100)
|
|
2820
3195
|
click.echo(f"CREDENTIALS - Engagement: {eng['name']}")
|
|
@@ -2830,18 +3205,22 @@ def creds_list(engagement, service, status, host):
|
|
|
2830
3205
|
click.echo("=" * 100)
|
|
2831
3206
|
|
|
2832
3207
|
# Summary line
|
|
2833
|
-
click.echo(
|
|
2834
|
-
|
|
2835
|
-
|
|
3208
|
+
click.echo(
|
|
3209
|
+
f"Total: {stats['total']} | "
|
|
3210
|
+
+ click.style(f"Valid: {stats['valid']}", fg="green", bold=True)
|
|
3211
|
+
+ f" | Usernames: {stats['users_only']} | Pairs: {stats['pairs']}"
|
|
3212
|
+
)
|
|
2836
3213
|
click.echo()
|
|
2837
3214
|
|
|
2838
3215
|
# Separate valid and untested
|
|
2839
|
-
valid_creds = [c for c in creds if c.get(
|
|
2840
|
-
untested_creds = [c for c in creds if c.get(
|
|
3216
|
+
valid_creds = [c for c in creds if c.get("status") == "valid"]
|
|
3217
|
+
untested_creds = [c for c in creds if c.get("status") != "valid"]
|
|
2841
3218
|
|
|
2842
3219
|
# Show valid credentials
|
|
2843
3220
|
if valid_creds:
|
|
2844
|
-
click.echo(
|
|
3221
|
+
click.echo(
|
|
3222
|
+
click.style("VALID CREDENTIALS (Confirmed Working)", bold=True, fg="green")
|
|
3223
|
+
)
|
|
2845
3224
|
click.echo()
|
|
2846
3225
|
|
|
2847
3226
|
# Create Rich Table
|
|
@@ -2856,12 +3235,12 @@ def creds_list(engagement, service, status, host):
|
|
|
2856
3235
|
table.add_column("Tool", width=15)
|
|
2857
3236
|
|
|
2858
3237
|
for cred in valid_creds:
|
|
2859
|
-
username = cred.get(
|
|
2860
|
-
password = cred.get(
|
|
2861
|
-
service_name = cred.get(
|
|
2862
|
-
ip = cred.get(
|
|
2863
|
-
port = str(cred.get(
|
|
2864
|
-
tool_name = cred.get(
|
|
3238
|
+
username = cred.get("username", "")[:19]
|
|
3239
|
+
password = cred.get("password", "")[:19]
|
|
3240
|
+
service_name = cred.get("service", "N/A")[:9]
|
|
3241
|
+
ip = cred.get("ip_address", "N/A")[:17]
|
|
3242
|
+
port = str(cred.get("port", "N/A"))[:5]
|
|
3243
|
+
tool_name = cred.get("tool", "N/A")[:14]
|
|
2865
3244
|
|
|
2866
3245
|
table.add_row("✓", username, password, service_name, ip, port, tool_name)
|
|
2867
3246
|
|
|
@@ -2870,19 +3249,25 @@ def creds_list(engagement, service, status, host):
|
|
|
2870
3249
|
|
|
2871
3250
|
# Show discovered usernames
|
|
2872
3251
|
if untested_creds:
|
|
2873
|
-
click.echo(
|
|
3252
|
+
click.echo(
|
|
3253
|
+
click.style(
|
|
3254
|
+
f"DISCOVERED USERNAMES ({len(untested_creds)} untested)",
|
|
3255
|
+
bold=True,
|
|
3256
|
+
fg="cyan",
|
|
3257
|
+
)
|
|
3258
|
+
)
|
|
2874
3259
|
click.echo(DesignSystem.separator())
|
|
2875
3260
|
|
|
2876
3261
|
# Group by service
|
|
2877
3262
|
by_service = {}
|
|
2878
3263
|
for cred in untested_creds:
|
|
2879
|
-
svc = cred.get(
|
|
3264
|
+
svc = cred.get("service", "unknown")
|
|
2880
3265
|
if svc not in by_service:
|
|
2881
3266
|
by_service[svc] = []
|
|
2882
|
-
by_service[svc].append(cred.get(
|
|
3267
|
+
by_service[svc].append(cred.get("username", ""))
|
|
2883
3268
|
|
|
2884
3269
|
for svc, usernames in sorted(by_service.items()):
|
|
2885
|
-
user_list =
|
|
3270
|
+
user_list = ", ".join(sorted(usernames))
|
|
2886
3271
|
click.echo(f"{svc.upper():<8} ({len(usernames):2}): {user_list}")
|
|
2887
3272
|
|
|
2888
3273
|
click.echo(DesignSystem.separator())
|
|
@@ -2891,7 +3276,9 @@ def creds_list(engagement, service, status, host):
|
|
|
2891
3276
|
|
|
2892
3277
|
|
|
2893
3278
|
@creds.command("stats")
|
|
2894
|
-
@click.option(
|
|
3279
|
+
@click.option(
|
|
3280
|
+
"--engagement", "-w", default=None, help="Engagement name (default: current)"
|
|
3281
|
+
)
|
|
2895
3282
|
def creds_stats(engagement):
|
|
2896
3283
|
"""Show credentials statistics."""
|
|
2897
3284
|
from souleyez.storage.credentials import CredentialsManager
|
|
@@ -2910,13 +3297,15 @@ def creds_stats(engagement):
|
|
|
2910
3297
|
return
|
|
2911
3298
|
|
|
2912
3299
|
cm = CredentialsManager()
|
|
2913
|
-
stats = cm.get_stats(eng[
|
|
3300
|
+
stats = cm.get_stats(eng["id"])
|
|
2914
3301
|
|
|
2915
3302
|
click.echo("\n" + "=" * 60)
|
|
2916
3303
|
click.echo(f"CREDENTIALS STATISTICS - Engagement: {eng['name']}")
|
|
2917
3304
|
click.echo("=" * 60)
|
|
2918
3305
|
click.echo(f"Total Credentials: {stats['total']}")
|
|
2919
|
-
click.echo(
|
|
3306
|
+
click.echo(
|
|
3307
|
+
f"Valid (confirmed): {click.style(str(stats['valid']), fg='green')}"
|
|
3308
|
+
)
|
|
2920
3309
|
click.echo(f"Username-only: {stats['users_only']}")
|
|
2921
3310
|
click.echo(f"Password-only: {stats['passwords_only']}")
|
|
2922
3311
|
click.echo(f"Username:Password pairs: {stats['pairs']}")
|
|
@@ -2925,26 +3314,294 @@ def creds_stats(engagement):
|
|
|
2925
3314
|
|
|
2926
3315
|
@creds.command("update")
|
|
2927
3316
|
@click.argument("credential_id", type=int)
|
|
2928
|
-
@click.option(
|
|
3317
|
+
@click.option(
|
|
3318
|
+
"--status",
|
|
3319
|
+
type=click.Choice(["untested", "valid", "invalid"]),
|
|
3320
|
+
help="Credential status",
|
|
3321
|
+
)
|
|
2929
3322
|
@click.option("--notes", help="Additional notes")
|
|
2930
3323
|
def creds_update(credential_id, status, notes):
|
|
2931
3324
|
"""Update credential status and notes."""
|
|
2932
3325
|
from souleyez.storage.credentials import CredentialsManager
|
|
2933
|
-
|
|
3326
|
+
|
|
2934
3327
|
if not status and notes is None:
|
|
2935
3328
|
click.echo("✗ Must provide --status or --notes", err=True)
|
|
2936
3329
|
return
|
|
2937
|
-
|
|
3330
|
+
|
|
2938
3331
|
cm = CredentialsManager()
|
|
2939
|
-
|
|
3332
|
+
|
|
2940
3333
|
try:
|
|
2941
3334
|
success = cm.update_credential_status(credential_id, status=status, notes=notes)
|
|
2942
3335
|
if success:
|
|
2943
|
-
click.echo(click.style(f"✓ Credential {credential_id} updated", fg=
|
|
3336
|
+
click.echo(click.style(f"✓ Credential {credential_id} updated", fg="green"))
|
|
2944
3337
|
else:
|
|
2945
|
-
click.echo(click.style(f"✗ Credential {credential_id} not found", fg=
|
|
3338
|
+
click.echo(click.style(f"✗ Credential {credential_id} not found", fg="red"))
|
|
2946
3339
|
except Exception as e:
|
|
2947
|
-
click.echo(click.style(f"✗ Error: {e}", fg=
|
|
3340
|
+
click.echo(click.style(f"✗ Error: {e}", fg="red"))
|
|
3341
|
+
|
|
3342
|
+
|
|
3343
|
+
@creds.command("cleanup")
|
|
3344
|
+
@require_password
|
|
3345
|
+
@click.option("--engagement", "-e", type=int, help="Engagement ID (default: all)")
|
|
3346
|
+
@click.option(
|
|
3347
|
+
"--confirm",
|
|
3348
|
+
is_flag=True,
|
|
3349
|
+
default=False,
|
|
3350
|
+
help="Actually delete the garbage credentials",
|
|
3351
|
+
)
|
|
3352
|
+
@click.option(
|
|
3353
|
+
"--all",
|
|
3354
|
+
"-a",
|
|
3355
|
+
"show_all",
|
|
3356
|
+
is_flag=True,
|
|
3357
|
+
default=False,
|
|
3358
|
+
help="Show all garbage credentials (default: first 20)",
|
|
3359
|
+
)
|
|
3360
|
+
def creds_cleanup(engagement, confirm, show_all):
|
|
3361
|
+
"""Remove garbage credentials (scanner artifacts, injection payloads).
|
|
3362
|
+
|
|
3363
|
+
By default runs in dry-run mode to show what would be deleted.
|
|
3364
|
+
Use --confirm to actually delete the garbage entries.
|
|
3365
|
+
|
|
3366
|
+
Examples:
|
|
3367
|
+
souleyez creds cleanup # Preview what would be deleted
|
|
3368
|
+
souleyez creds cleanup --all # Show all garbage credentials
|
|
3369
|
+
souleyez creds cleanup --confirm # Actually delete garbage
|
|
3370
|
+
souleyez creds cleanup -e 5 # Preview for engagement 5 only
|
|
3371
|
+
"""
|
|
3372
|
+
import re
|
|
3373
|
+
from souleyez.storage.credentials import CredentialsManager
|
|
3374
|
+
from souleyez.storage.engagements import EngagementManager
|
|
3375
|
+
|
|
3376
|
+
def is_garbage_username(username: str) -> bool:
|
|
3377
|
+
"""Check if username is scanner garbage."""
|
|
3378
|
+
if not username or len(username) > 100:
|
|
3379
|
+
return True
|
|
3380
|
+
|
|
3381
|
+
username_lower = username.lower()
|
|
3382
|
+
|
|
3383
|
+
# Scanner tool signatures
|
|
3384
|
+
scanner_patterns = [
|
|
3385
|
+
"netsparker",
|
|
3386
|
+
"burpsuite",
|
|
3387
|
+
"burp",
|
|
3388
|
+
"acunetix",
|
|
3389
|
+
"nikto",
|
|
3390
|
+
"sqlmap",
|
|
3391
|
+
"havij",
|
|
3392
|
+
"w3af",
|
|
3393
|
+
"owasp",
|
|
3394
|
+
"zap",
|
|
3395
|
+
"wvs",
|
|
3396
|
+
]
|
|
3397
|
+
for pattern in scanner_patterns:
|
|
3398
|
+
if pattern in username_lower:
|
|
3399
|
+
return True
|
|
3400
|
+
|
|
3401
|
+
# Template injection patterns
|
|
3402
|
+
injection_patterns = [
|
|
3403
|
+
"{{",
|
|
3404
|
+
"}}",
|
|
3405
|
+
"${",
|
|
3406
|
+
"}$",
|
|
3407
|
+
"<%",
|
|
3408
|
+
"%>",
|
|
3409
|
+
"{%",
|
|
3410
|
+
"%}",
|
|
3411
|
+
"sleep(",
|
|
3412
|
+
"benchmark(",
|
|
3413
|
+
"waitfor delay",
|
|
3414
|
+
"pg_sleep",
|
|
3415
|
+
]
|
|
3416
|
+
for pattern in injection_patterns:
|
|
3417
|
+
if pattern in username_lower:
|
|
3418
|
+
return True
|
|
3419
|
+
|
|
3420
|
+
# Path traversal patterns
|
|
3421
|
+
path_patterns = [
|
|
3422
|
+
"/etc/",
|
|
3423
|
+
"\\etc\\",
|
|
3424
|
+
"/passwd",
|
|
3425
|
+
"/shadow",
|
|
3426
|
+
"/windows/",
|
|
3427
|
+
"c:\\",
|
|
3428
|
+
".asp",
|
|
3429
|
+
".aspx",
|
|
3430
|
+
".axd",
|
|
3431
|
+
".php",
|
|
3432
|
+
".jsp",
|
|
3433
|
+
".pl",
|
|
3434
|
+
"../",
|
|
3435
|
+
"..\\",
|
|
3436
|
+
"file://",
|
|
3437
|
+
"php://",
|
|
3438
|
+
"data://",
|
|
3439
|
+
"::1/",
|
|
3440
|
+
"[::1]",
|
|
3441
|
+
"/elmah",
|
|
3442
|
+
"/trace",
|
|
3443
|
+
"127.0.0.1/",
|
|
3444
|
+
]
|
|
3445
|
+
for pattern in path_patterns:
|
|
3446
|
+
if pattern in username_lower:
|
|
3447
|
+
return True
|
|
3448
|
+
|
|
3449
|
+
# Command injection patterns
|
|
3450
|
+
cmd_patterns = [
|
|
3451
|
+
"& ping ",
|
|
3452
|
+
"| ping ",
|
|
3453
|
+
"; ping ",
|
|
3454
|
+
"ping -",
|
|
3455
|
+
"& whoami",
|
|
3456
|
+
"| whoami",
|
|
3457
|
+
"; whoami",
|
|
3458
|
+
"`whoami`",
|
|
3459
|
+
"$(whoami)",
|
|
3460
|
+
"cmd.exe",
|
|
3461
|
+
"/bin/sh",
|
|
3462
|
+
"& dir",
|
|
3463
|
+
"| dir",
|
|
3464
|
+
"; dir",
|
|
3465
|
+
"& ls",
|
|
3466
|
+
"| ls",
|
|
3467
|
+
"; ls",
|
|
3468
|
+
"nc -",
|
|
3469
|
+
"ncat ",
|
|
3470
|
+
"netcat ",
|
|
3471
|
+
]
|
|
3472
|
+
for pattern in cmd_patterns:
|
|
3473
|
+
if pattern in username_lower:
|
|
3474
|
+
return True
|
|
3475
|
+
|
|
3476
|
+
# SQL injection patterns
|
|
3477
|
+
sql_patterns = [
|
|
3478
|
+
"' or ",
|
|
3479
|
+
"' and ",
|
|
3480
|
+
"1=1",
|
|
3481
|
+
"1'='1",
|
|
3482
|
+
"' union ",
|
|
3483
|
+
"select ",
|
|
3484
|
+
"insert ",
|
|
3485
|
+
"update ",
|
|
3486
|
+
"delete ",
|
|
3487
|
+
"drop ",
|
|
3488
|
+
"concat(",
|
|
3489
|
+
"char(",
|
|
3490
|
+
"chr(",
|
|
3491
|
+
"0x00",
|
|
3492
|
+
"@@version",
|
|
3493
|
+
]
|
|
3494
|
+
for pattern in sql_patterns:
|
|
3495
|
+
if pattern in username_lower:
|
|
3496
|
+
return True
|
|
3497
|
+
|
|
3498
|
+
# URL encoding patterns
|
|
3499
|
+
if "%27" in username or "%22" in username or "%3c" in username_lower:
|
|
3500
|
+
return True
|
|
3501
|
+
|
|
3502
|
+
# Hex patterns
|
|
3503
|
+
if re.search(r"0x[0-9a-f]{4,}", username_lower):
|
|
3504
|
+
return True
|
|
3505
|
+
|
|
3506
|
+
# Starts/ends with injection chars
|
|
3507
|
+
injection_chars = ['"', "'", ";", "|", "&", "`", "(", ")", "<", ">"]
|
|
3508
|
+
if username[0] in injection_chars or username[-1] in injection_chars:
|
|
3509
|
+
return True
|
|
3510
|
+
|
|
3511
|
+
# Too many special characters
|
|
3512
|
+
special_count = sum(1 for c in username if c in "{}[]()$%^&*|\\/<>\"`'")
|
|
3513
|
+
if special_count > 3:
|
|
3514
|
+
return True
|
|
3515
|
+
|
|
3516
|
+
# Mostly digits and long
|
|
3517
|
+
if len(username) > 20:
|
|
3518
|
+
alnum_only = re.sub(r"[^a-zA-Z0-9]", "", username)
|
|
3519
|
+
if len(alnum_only) > 0:
|
|
3520
|
+
digit_ratio = sum(1 for c in alnum_only if c.isdigit()) / len(
|
|
3521
|
+
alnum_only
|
|
3522
|
+
)
|
|
3523
|
+
if digit_ratio > 0.7:
|
|
3524
|
+
return True
|
|
3525
|
+
|
|
3526
|
+
return False
|
|
3527
|
+
|
|
3528
|
+
cm = CredentialsManager()
|
|
3529
|
+
em = EngagementManager()
|
|
3530
|
+
|
|
3531
|
+
# Get credentials (must decrypt to check actual usernames)
|
|
3532
|
+
if engagement:
|
|
3533
|
+
creds_list = cm.list_credentials_for_engagement(engagement, decrypt=True)
|
|
3534
|
+
eng_name = f"engagement {engagement}"
|
|
3535
|
+
else:
|
|
3536
|
+
# Get all engagements (unfiltered for cleanup operation)
|
|
3537
|
+
all_creds = []
|
|
3538
|
+
for eng in em.list(user_filtered=False):
|
|
3539
|
+
eng_creds = cm.list_credentials_for_engagement(eng["id"], decrypt=True)
|
|
3540
|
+
all_creds.extend(eng_creds)
|
|
3541
|
+
creds_list = all_creds
|
|
3542
|
+
eng_name = "all engagements"
|
|
3543
|
+
|
|
3544
|
+
# Find garbage
|
|
3545
|
+
garbage = []
|
|
3546
|
+
for cred in creds_list:
|
|
3547
|
+
username = cred.get("username", "")
|
|
3548
|
+
if is_garbage_username(username):
|
|
3549
|
+
garbage.append(cred)
|
|
3550
|
+
|
|
3551
|
+
if not garbage:
|
|
3552
|
+
click.echo(
|
|
3553
|
+
click.style(f"✓ No garbage credentials found in {eng_name}", fg="green")
|
|
3554
|
+
)
|
|
3555
|
+
return
|
|
3556
|
+
|
|
3557
|
+
# Display what we found
|
|
3558
|
+
click.echo(
|
|
3559
|
+
click.style(
|
|
3560
|
+
f"\nFound {len(garbage)} garbage credential(s) in {eng_name}:\n",
|
|
3561
|
+
fg="yellow",
|
|
3562
|
+
bold=True,
|
|
3563
|
+
)
|
|
3564
|
+
)
|
|
3565
|
+
|
|
3566
|
+
# Table header
|
|
3567
|
+
click.echo(f" {'ID':<6} {'Service':<18} {'Username':<50}")
|
|
3568
|
+
click.echo("-" * 80)
|
|
3569
|
+
|
|
3570
|
+
display_list = garbage if show_all else garbage[:20]
|
|
3571
|
+
for cred in display_list:
|
|
3572
|
+
username = (cred.get("username") or "<empty>")[:48]
|
|
3573
|
+
cred_id = str(cred.get("id", "?"))
|
|
3574
|
+
service = (cred.get("service") or "unknown")[:16]
|
|
3575
|
+
click.echo(f" {cred_id:<6} {service:<18} {username}")
|
|
3576
|
+
|
|
3577
|
+
if not show_all and len(garbage) > 20:
|
|
3578
|
+
click.echo(f"\n ... and {len(garbage) - 20} more (use --all to see all)")
|
|
3579
|
+
|
|
3580
|
+
click.echo("-" * 80)
|
|
3581
|
+
|
|
3582
|
+
if confirm:
|
|
3583
|
+
# Actually delete
|
|
3584
|
+
click.echo(
|
|
3585
|
+
click.style(f"\nDeleting {len(garbage)} garbage credential(s)...", fg="red")
|
|
3586
|
+
)
|
|
3587
|
+
deleted = 0
|
|
3588
|
+
for cred in garbage:
|
|
3589
|
+
try:
|
|
3590
|
+
cm.delete_credential(cred["id"])
|
|
3591
|
+
deleted += 1
|
|
3592
|
+
except Exception as e:
|
|
3593
|
+
click.echo(f" Failed to delete {cred['id']}: {e}")
|
|
3594
|
+
|
|
3595
|
+
click.echo(
|
|
3596
|
+
click.style(f"✓ Deleted {deleted} garbage credential(s)", fg="green")
|
|
3597
|
+
)
|
|
3598
|
+
else:
|
|
3599
|
+
click.echo(
|
|
3600
|
+
click.style(
|
|
3601
|
+
f"\n[DRY RUN] Would delete {len(garbage)} credential(s)", fg="yellow"
|
|
3602
|
+
)
|
|
3603
|
+
)
|
|
3604
|
+
click.echo("Run with --confirm to actually delete them.")
|
|
2948
3605
|
|
|
2949
3606
|
|
|
2950
3607
|
@cli.group()
|
|
@@ -2955,11 +3612,37 @@ def report():
|
|
|
2955
3612
|
|
|
2956
3613
|
|
|
2957
3614
|
@report.command("generate")
|
|
2958
|
-
@click.option(
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3615
|
+
@click.option(
|
|
3616
|
+
"--format",
|
|
3617
|
+
"-f",
|
|
3618
|
+
type=click.Choice(["markdown", "html", "json"], case_sensitive=False),
|
|
3619
|
+
default="html",
|
|
3620
|
+
help="Report format",
|
|
3621
|
+
)
|
|
3622
|
+
@click.option(
|
|
3623
|
+
"--type",
|
|
3624
|
+
"-t",
|
|
3625
|
+
type=click.Choice(
|
|
3626
|
+
["executive", "technical", "summary", "detection"], case_sensitive=False
|
|
3627
|
+
),
|
|
3628
|
+
default="technical",
|
|
3629
|
+
help="Report type (executive, technical, summary, detection)",
|
|
3630
|
+
)
|
|
3631
|
+
@click.option(
|
|
3632
|
+
"--output",
|
|
3633
|
+
"-o",
|
|
3634
|
+
type=str,
|
|
3635
|
+
help="Output file path (default: reports/<engagement>_<timestamp>.<ext>)",
|
|
3636
|
+
)
|
|
3637
|
+
@click.option(
|
|
3638
|
+
"--engagement", "-e", type=int, help="Engagement ID (default: current engagement)"
|
|
3639
|
+
)
|
|
3640
|
+
@click.option(
|
|
3641
|
+
"--ai",
|
|
3642
|
+
is_flag=True,
|
|
3643
|
+
default=False,
|
|
3644
|
+
help="Enable AI-enhanced report (PRO tier, requires Claude API or Ollama)",
|
|
3645
|
+
)
|
|
2963
3646
|
def report_generate(format, type, output, engagement, ai):
|
|
2964
3647
|
"""Generate a penetration test report.
|
|
2965
3648
|
|
|
@@ -2985,54 +3668,61 @@ def report_generate(format, type, output, engagement, ai):
|
|
|
2985
3668
|
if engagement:
|
|
2986
3669
|
eng = em.get_by_id(engagement)
|
|
2987
3670
|
if not eng:
|
|
2988
|
-
click.echo(click.style(f"✗ Engagement {engagement} not found", fg=
|
|
3671
|
+
click.echo(click.style(f"✗ Engagement {engagement} not found", fg="red"))
|
|
2989
3672
|
return
|
|
2990
3673
|
else:
|
|
2991
3674
|
eng = em.get_current()
|
|
2992
3675
|
if not eng:
|
|
2993
|
-
click.echo(
|
|
3676
|
+
click.echo(
|
|
3677
|
+
click.style(
|
|
3678
|
+
"✗ No current engagement. Use 'souleyez engagement list' to see available engagements.",
|
|
3679
|
+
fg="red",
|
|
3680
|
+
)
|
|
3681
|
+
)
|
|
2994
3682
|
return
|
|
2995
3683
|
|
|
2996
|
-
engagement_id = eng[
|
|
2997
|
-
engagement_name = eng[
|
|
3684
|
+
engagement_id = eng["id"]
|
|
3685
|
+
engagement_name = eng["name"]
|
|
2998
3686
|
|
|
2999
3687
|
# Detection reports require Wazuh integration
|
|
3000
|
-
if type ==
|
|
3688
|
+
if type == "detection":
|
|
3001
3689
|
from souleyez.integrations.wazuh.config import WazuhConfig
|
|
3690
|
+
|
|
3002
3691
|
wazuh_config = WazuhConfig.get_config(engagement_id)
|
|
3003
|
-
if not wazuh_config or not wazuh_config.get(
|
|
3004
|
-
click.echo(
|
|
3005
|
-
"✗ Detection reports require Wazuh integration.",
|
|
3006
|
-
|
|
3007
|
-
))
|
|
3692
|
+
if not wazuh_config or not wazuh_config.get("enabled"):
|
|
3693
|
+
click.echo(
|
|
3694
|
+
click.style("✗ Detection reports require Wazuh integration.", fg="red")
|
|
3695
|
+
)
|
|
3008
3696
|
click.echo(" Configure Wazuh first: souleyez wazuh configure")
|
|
3009
3697
|
return
|
|
3010
3698
|
|
|
3011
3699
|
# Check for detection results
|
|
3012
3700
|
from souleyez.storage.database import get_db
|
|
3701
|
+
|
|
3013
3702
|
db = get_db()
|
|
3014
3703
|
results = db.execute(
|
|
3015
3704
|
"SELECT COUNT(*) FROM detection_results WHERE engagement_id = ?",
|
|
3016
|
-
(engagement_id,)
|
|
3705
|
+
(engagement_id,),
|
|
3017
3706
|
).fetchone()[0]
|
|
3018
3707
|
if results == 0:
|
|
3019
|
-
click.echo(
|
|
3020
|
-
"✗ No detection validation results found.",
|
|
3021
|
-
|
|
3022
|
-
))
|
|
3708
|
+
click.echo(
|
|
3709
|
+
click.style("✗ No detection validation results found.", fg="red")
|
|
3710
|
+
)
|
|
3023
3711
|
click.echo(" Run attacks and validate detections first.")
|
|
3024
3712
|
click.echo(" Use: souleyez detection validate")
|
|
3025
3713
|
return
|
|
3026
3714
|
|
|
3027
3715
|
# Show report type being generated
|
|
3028
3716
|
type_label = {
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3717
|
+
"executive": "EXECUTIVE (C-Level)",
|
|
3718
|
+
"technical": "TECHNICAL (Full Details)",
|
|
3719
|
+
"summary": "SUMMARY (Quick Status)",
|
|
3720
|
+
"detection": "DETECTION COVERAGE (SIEM Analysis)",
|
|
3033
3721
|
}
|
|
3034
3722
|
ai_label = " + AI Enhanced" if ai else ""
|
|
3035
|
-
click.echo(
|
|
3723
|
+
click.echo(
|
|
3724
|
+
f"Generating {click.style(type_label[type] + ai_label, fg='yellow', bold=True)} {format.upper()} report"
|
|
3725
|
+
)
|
|
3036
3726
|
click.echo(f"Engagement: {click.style(engagement_name, fg='cyan', bold=True)}")
|
|
3037
3727
|
|
|
3038
3728
|
# Check AI availability if requested
|
|
@@ -3040,26 +3730,38 @@ def report_generate(format, type, output, engagement, ai):
|
|
|
3040
3730
|
if ai:
|
|
3041
3731
|
from souleyez.ai import AIReportService
|
|
3042
3732
|
from souleyez.ai.llm_factory import LLMFactory
|
|
3733
|
+
|
|
3043
3734
|
ai_provider = LLMFactory.get_available_provider()
|
|
3044
3735
|
if ai_provider and ai_provider.is_available():
|
|
3045
3736
|
provider_info = ai_provider.get_status()
|
|
3046
|
-
provider_name = provider_info.get(
|
|
3047
|
-
click.echo(
|
|
3737
|
+
provider_name = provider_info.get("provider", "Unknown")
|
|
3738
|
+
click.echo(
|
|
3739
|
+
f"AI Provider: {click.style(provider_name, fg='magenta', bold=True)}"
|
|
3740
|
+
)
|
|
3048
3741
|
|
|
3049
3742
|
# Show privacy warning for cloud providers
|
|
3050
|
-
if provider_name.lower() ==
|
|
3051
|
-
click.echo(
|
|
3743
|
+
if provider_name.lower() == "claude":
|
|
3744
|
+
click.echo(
|
|
3745
|
+
click.style(
|
|
3746
|
+
"⚠ PRIVACY: Engagement data will be sent to Anthropic's servers.",
|
|
3747
|
+
fg="yellow",
|
|
3748
|
+
)
|
|
3749
|
+
)
|
|
3052
3750
|
else:
|
|
3053
|
-
click.echo(
|
|
3751
|
+
click.echo(
|
|
3752
|
+
click.style(
|
|
3753
|
+
"⚠ AI not available. Falling back to standard report.", fg="yellow"
|
|
3754
|
+
)
|
|
3755
|
+
)
|
|
3054
3756
|
click.echo(" Configure Claude API key or ensure Ollama is running.")
|
|
3055
3757
|
ai = False
|
|
3056
3758
|
ai_provider = None
|
|
3057
3759
|
|
|
3058
3760
|
# Generate output filename if not specified
|
|
3059
3761
|
if not output:
|
|
3060
|
-
timestamp = datetime.datetime.now().strftime(
|
|
3061
|
-
safe_name = engagement_name.replace(
|
|
3062
|
-
ext = format if format !=
|
|
3762
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
3763
|
+
safe_name = engagement_name.replace(" ", "_").replace("/", "_")
|
|
3764
|
+
ext = format if format != "markdown" else "md"
|
|
3063
3765
|
# Include report type and AI in filename
|
|
3064
3766
|
ai_suffix = "_ai" if ai else ""
|
|
3065
3767
|
output = f"reports/{safe_name}_{type}{ai_suffix}_{timestamp}.{ext}"
|
|
@@ -3071,6 +3773,7 @@ def report_generate(format, type, output, engagement, ai):
|
|
|
3071
3773
|
# Set explicit AI provider to avoid factory fallback issues
|
|
3072
3774
|
if ai_provider:
|
|
3073
3775
|
from souleyez.ai import AIReportService
|
|
3776
|
+
|
|
3074
3777
|
rg._ai_service = AIReportService(provider=ai_provider)
|
|
3075
3778
|
|
|
3076
3779
|
# Generate report with specified type and AI enhancement
|
|
@@ -3079,17 +3782,18 @@ def report_generate(format, type, output, engagement, ai):
|
|
|
3079
3782
|
format=format,
|
|
3080
3783
|
output_path=output,
|
|
3081
3784
|
report_type=type,
|
|
3082
|
-
ai_enhanced=ai
|
|
3785
|
+
ai_enhanced=ai,
|
|
3083
3786
|
)
|
|
3084
3787
|
|
|
3085
|
-
click.echo(click.style(f"✓ Report generated successfully!", fg=
|
|
3788
|
+
click.echo(click.style(f"✓ Report generated successfully!", fg="green"))
|
|
3086
3789
|
click.echo(f" Type: {type_label[type]}{' + AI' if ai else ''}")
|
|
3087
3790
|
click.echo(f" Format: {format.upper()}")
|
|
3088
3791
|
click.echo(f" File: {report_path}")
|
|
3089
3792
|
|
|
3090
3793
|
# Show summary (different for detection reports)
|
|
3091
|
-
if type ==
|
|
3794
|
+
if type == "detection":
|
|
3092
3795
|
from souleyez.reporting.detection_report import DetectionReportGatherer
|
|
3796
|
+
|
|
3093
3797
|
gatherer = DetectionReportGatherer(engagement_id)
|
|
3094
3798
|
stats = gatherer.get_summary_stats()
|
|
3095
3799
|
click.echo(f"\nDetection Summary:")
|
|
@@ -3106,8 +3810,9 @@ def report_generate(format, type, output, engagement, ai):
|
|
|
3106
3810
|
click.echo(f" Credentials: {len(data['credentials'])}")
|
|
3107
3811
|
|
|
3108
3812
|
except Exception as e:
|
|
3109
|
-
click.echo(click.style(f"✗ Error generating report: {e}", fg=
|
|
3813
|
+
click.echo(click.style(f"✗ Error generating report: {e}", fg="red"))
|
|
3110
3814
|
import traceback
|
|
3815
|
+
|
|
3111
3816
|
traceback.print_exc()
|
|
3112
3817
|
|
|
3113
3818
|
|
|
@@ -3115,13 +3820,16 @@ def report_generate(format, type, output, engagement, ai):
|
|
|
3115
3820
|
def report_list():
|
|
3116
3821
|
"""List generated reports."""
|
|
3117
3822
|
import os
|
|
3823
|
+
|
|
3118
3824
|
reports_dir = Path("reports")
|
|
3119
3825
|
|
|
3120
3826
|
if not reports_dir.exists():
|
|
3121
3827
|
click.echo("No reports directory found.")
|
|
3122
3828
|
return
|
|
3123
3829
|
|
|
3124
|
-
reports = sorted(
|
|
3830
|
+
reports = sorted(
|
|
3831
|
+
reports_dir.glob("*.*"), key=lambda p: p.stat().st_mtime, reverse=True
|
|
3832
|
+
)
|
|
3125
3833
|
|
|
3126
3834
|
if not reports:
|
|
3127
3835
|
click.echo("No reports found.")
|
|
@@ -3135,7 +3843,9 @@ def report_list():
|
|
|
3135
3843
|
size = rpt.stat().st_size
|
|
3136
3844
|
mtime = datetime.datetime.fromtimestamp(rpt.stat().st_mtime)
|
|
3137
3845
|
click.echo(f"{rpt.name}")
|
|
3138
|
-
click.echo(
|
|
3846
|
+
click.echo(
|
|
3847
|
+
f" Size: {size:,} bytes | Modified: {mtime.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
3848
|
+
)
|
|
3139
3849
|
|
|
3140
3850
|
click.echo("=" * 70 + "\n")
|
|
3141
3851
|
|
|
@@ -3144,6 +3854,7 @@ def report_list():
|
|
|
3144
3854
|
# AI Commands
|
|
3145
3855
|
# ============================================================================
|
|
3146
3856
|
|
|
3857
|
+
|
|
3147
3858
|
@cli.group()
|
|
3148
3859
|
def ai():
|
|
3149
3860
|
"""AI-powered attack path recommendations (Pro feature)."""
|
|
@@ -3163,27 +3874,31 @@ def ai_status():
|
|
|
3163
3874
|
status = service.get_status()
|
|
3164
3875
|
|
|
3165
3876
|
# Display connection status
|
|
3166
|
-
if status[
|
|
3167
|
-
click.echo(click.style("✓ Ollama Connection: ", fg=
|
|
3877
|
+
if status["connected"]:
|
|
3878
|
+
click.echo(click.style("✓ Ollama Connection: ", fg="green") + "Connected")
|
|
3168
3879
|
click.echo(f" Endpoint: {status['endpoint']}")
|
|
3169
3880
|
else:
|
|
3170
|
-
click.echo(click.style("✗ Ollama Connection: ", fg=
|
|
3881
|
+
click.echo(click.style("✗ Ollama Connection: ", fg="red") + "Not connected")
|
|
3171
3882
|
click.echo(f" Endpoint: {status['endpoint']}")
|
|
3172
|
-
if status.get(
|
|
3883
|
+
if status.get("error"):
|
|
3173
3884
|
click.echo(f" Error: {status['error']}")
|
|
3174
|
-
click.echo(
|
|
3885
|
+
click.echo(
|
|
3886
|
+
"\n"
|
|
3887
|
+
+ click.style("💡 TIP:", fg="yellow")
|
|
3888
|
+
+ " Install Ollama from https://ollama.ai"
|
|
3889
|
+
)
|
|
3175
3890
|
click.echo(" Then run: ollama serve")
|
|
3176
3891
|
click.echo("=" * 70 + "\n")
|
|
3177
3892
|
return
|
|
3178
3893
|
|
|
3179
3894
|
# Display available models
|
|
3180
3895
|
click.echo(f"\nAvailable Models: {len(status['models'])}")
|
|
3181
|
-
if status[
|
|
3182
|
-
for model in status[
|
|
3183
|
-
if status[
|
|
3184
|
-
click.echo(f" • {model} " + click.style("(configured)", fg=
|
|
3185
|
-
elif status[
|
|
3186
|
-
click.echo(f" • {model} " + click.style("(default)", fg=
|
|
3896
|
+
if status["models"]:
|
|
3897
|
+
for model in status["models"]:
|
|
3898
|
+
if status["configured_model"] in model:
|
|
3899
|
+
click.echo(f" • {model} " + click.style("(configured)", fg="cyan"))
|
|
3900
|
+
elif status["default_model"] in model:
|
|
3901
|
+
click.echo(f" • {model} " + click.style("(default)", fg="yellow"))
|
|
3187
3902
|
else:
|
|
3188
3903
|
click.echo(f" • {model}")
|
|
3189
3904
|
else:
|
|
@@ -3191,11 +3906,13 @@ def ai_status():
|
|
|
3191
3906
|
|
|
3192
3907
|
# Display configured model status
|
|
3193
3908
|
click.echo(f"\nConfigured Model: {status['configured_model']}")
|
|
3194
|
-
if status[
|
|
3195
|
-
click.echo(click.style("✓ Status: ", fg=
|
|
3909
|
+
if status["model_available"]:
|
|
3910
|
+
click.echo(click.style("✓ Status: ", fg="green") + "Ready")
|
|
3196
3911
|
else:
|
|
3197
|
-
click.echo(click.style("✗ Status: ", fg=
|
|
3198
|
-
click.echo(
|
|
3912
|
+
click.echo(click.style("✗ Status: ", fg="red") + "Not available")
|
|
3913
|
+
click.echo(
|
|
3914
|
+
f"\n" + click.style("💡 TIP:", fg="yellow") + f" Pull the model with:"
|
|
3915
|
+
)
|
|
3199
3916
|
click.echo(f" ollama pull {status['configured_model']}")
|
|
3200
3917
|
click.echo(f" Or change the model in Settings & Security → AI Settings")
|
|
3201
3918
|
|
|
@@ -3217,39 +3934,41 @@ def ai_init():
|
|
|
3217
3934
|
# Check connection
|
|
3218
3935
|
click.echo("🔄 Checking Ollama connection...")
|
|
3219
3936
|
if not service.check_connection():
|
|
3220
|
-
click.echo(click.style("✗ Failed:", fg=
|
|
3221
|
-
click.echo(
|
|
3937
|
+
click.echo(click.style("✗ Failed:", fg="red") + " Cannot connect to Ollama")
|
|
3938
|
+
click.echo(
|
|
3939
|
+
"\n" + click.style("💡 TIP:", fg="yellow") + " Install and start Ollama:"
|
|
3940
|
+
)
|
|
3222
3941
|
click.echo(" 1. Download from https://ollama.ai")
|
|
3223
3942
|
click.echo(" 2. Run: ollama serve")
|
|
3224
3943
|
click.echo("=" * 70 + "\n")
|
|
3225
3944
|
sys.exit(1)
|
|
3226
3945
|
|
|
3227
|
-
click.echo(click.style("✓ Connected", fg=
|
|
3946
|
+
click.echo(click.style("✓ Connected", fg="green") + " to Ollama\n")
|
|
3228
3947
|
|
|
3229
3948
|
# Check if model exists
|
|
3230
3949
|
configured_model = service.model
|
|
3231
3950
|
click.echo(f"🔍 Checking for model: {configured_model}...")
|
|
3232
3951
|
|
|
3233
3952
|
if service.check_model():
|
|
3234
|
-
click.echo(click.style("✓ Model already available", fg=
|
|
3235
|
-
click.echo("\n" + click.style("✓ AI features ready!", fg=
|
|
3953
|
+
click.echo(click.style("✓ Model already available", fg="green"))
|
|
3954
|
+
click.echo("\n" + click.style("✓ AI features ready!", fg="green", bold=True))
|
|
3236
3955
|
click.echo("=" * 70 + "\n")
|
|
3237
3956
|
return
|
|
3238
3957
|
|
|
3239
3958
|
# Model doesn't exist, pull it
|
|
3240
|
-
click.echo(click.style("⬇️ Model not found, pulling now...", fg=
|
|
3959
|
+
click.echo(click.style("⬇️ Model not found, pulling now...", fg="yellow"))
|
|
3241
3960
|
click.echo(f" This may take a few minutes depending on your connection.\n")
|
|
3242
3961
|
|
|
3243
3962
|
try:
|
|
3244
3963
|
if service.pull_model():
|
|
3245
|
-
click.echo(click.style("\n✓ Model pulled successfully!", fg=
|
|
3246
|
-
click.echo(click.style("✓ AI features ready!", fg=
|
|
3964
|
+
click.echo(click.style("\n✓ Model pulled successfully!", fg="green"))
|
|
3965
|
+
click.echo(click.style("✓ AI features ready!", fg="green", bold=True))
|
|
3247
3966
|
else:
|
|
3248
|
-
click.echo(click.style("\n✗ Failed to pull model", fg=
|
|
3967
|
+
click.echo(click.style("\n✗ Failed to pull model", fg="red"))
|
|
3249
3968
|
click.echo(" Please try manually: ollama pull " + configured_model)
|
|
3250
3969
|
sys.exit(1)
|
|
3251
3970
|
except Exception as e:
|
|
3252
|
-
click.echo(click.style(f"\n✗ Error pulling model: {e}", fg=
|
|
3971
|
+
click.echo(click.style(f"\n✗ Error pulling model: {e}", fg="red"))
|
|
3253
3972
|
click.echo(" Please try manually: ollama pull " + configured_model)
|
|
3254
3973
|
sys.exit(1)
|
|
3255
3974
|
|
|
@@ -3257,8 +3976,20 @@ def ai_init():
|
|
|
3257
3976
|
|
|
3258
3977
|
|
|
3259
3978
|
@ai.command("recommend")
|
|
3260
|
-
@click.option(
|
|
3261
|
-
|
|
3979
|
+
@click.option(
|
|
3980
|
+
"--engagement",
|
|
3981
|
+
"-e",
|
|
3982
|
+
type=int,
|
|
3983
|
+
default=None,
|
|
3984
|
+
help="Engagement ID to analyze (default: current)",
|
|
3985
|
+
)
|
|
3986
|
+
@click.option(
|
|
3987
|
+
"--steps",
|
|
3988
|
+
"-s",
|
|
3989
|
+
type=int,
|
|
3990
|
+
default=1,
|
|
3991
|
+
help="Number of attack steps to generate (default: 1)",
|
|
3992
|
+
)
|
|
3262
3993
|
def ai_recommend(engagement, steps):
|
|
3263
3994
|
"""Generate AI-powered attack path recommendation."""
|
|
3264
3995
|
from souleyez.ai.recommender import AttackRecommender
|
|
@@ -3271,16 +4002,21 @@ def ai_recommend(engagement, steps):
|
|
|
3271
4002
|
# Use specified engagement ID
|
|
3272
4003
|
engagement_data = eng_mgr.get_by_id(engagement)
|
|
3273
4004
|
if not engagement_data:
|
|
3274
|
-
click.echo(
|
|
4005
|
+
click.echo(
|
|
4006
|
+
click.style(f"✗ Error:", fg="red")
|
|
4007
|
+
+ f" Engagement ID {engagement} not found"
|
|
4008
|
+
)
|
|
3275
4009
|
sys.exit(1)
|
|
3276
4010
|
else:
|
|
3277
4011
|
# Use current engagement
|
|
3278
4012
|
engagement_data = eng_mgr.get_current()
|
|
3279
4013
|
if not engagement_data:
|
|
3280
|
-
click.echo(
|
|
4014
|
+
click.echo(
|
|
4015
|
+
click.style("✗ Error:", fg="red") + " No current engagement selected"
|
|
4016
|
+
)
|
|
3281
4017
|
click.echo(" Use: souleyez engagement use <name>")
|
|
3282
4018
|
sys.exit(1)
|
|
3283
|
-
engagement = engagement_data[
|
|
4019
|
+
engagement = engagement_data["id"]
|
|
3284
4020
|
|
|
3285
4021
|
click.echo("\n" + "=" * 70)
|
|
3286
4022
|
if steps > 1:
|
|
@@ -3293,65 +4029,82 @@ def ai_recommend(engagement, steps):
|
|
|
3293
4029
|
# Generate recommendation
|
|
3294
4030
|
click.echo("🤔 Analyzing engagement data...")
|
|
3295
4031
|
recommender = AttackRecommender()
|
|
3296
|
-
|
|
4032
|
+
|
|
3297
4033
|
if steps > 1:
|
|
3298
4034
|
result = recommender.generate_chain(engagement, num_steps=steps)
|
|
3299
4035
|
else:
|
|
3300
4036
|
result = recommender.suggest_next_step(engagement)
|
|
3301
4037
|
|
|
3302
4038
|
# Check for errors
|
|
3303
|
-
if result.get(
|
|
3304
|
-
click.echo(click.style("✗ Failed:", fg=
|
|
3305
|
-
if
|
|
3306
|
-
click.echo(
|
|
4039
|
+
if result.get("error"):
|
|
4040
|
+
click.echo(click.style("✗ Failed:", fg="red") + f" {result['error']}")
|
|
4041
|
+
if "ai init" in result["error"]:
|
|
4042
|
+
click.echo(
|
|
4043
|
+
"\n" + click.style("💡 TIP:", fg="yellow") + " Run: souleyez ai init"
|
|
4044
|
+
)
|
|
3307
4045
|
click.echo("=" * 70 + "\n")
|
|
3308
4046
|
sys.exit(1)
|
|
3309
4047
|
|
|
3310
4048
|
# Display recommendation
|
|
3311
4049
|
if steps > 1:
|
|
3312
4050
|
# Display multi-step chain
|
|
3313
|
-
click.echo(click.style("✓ Attack chain generated!", fg=
|
|
3314
|
-
|
|
3315
|
-
for step in result[
|
|
3316
|
-
click.echo(
|
|
4051
|
+
click.echo(click.style("✓ Attack chain generated!", fg="green") + "\n")
|
|
4052
|
+
|
|
4053
|
+
for step in result["steps"]:
|
|
4054
|
+
click.echo(
|
|
4055
|
+
click.style(f"STEP {step['step_number']}:", fg="yellow", bold=True)
|
|
4056
|
+
+ f" {step['action']}"
|
|
4057
|
+
)
|
|
3317
4058
|
click.echo(f" {click.style('TARGET:', fg='cyan')} {step['target']}")
|
|
3318
4059
|
click.echo(f" {click.style('RATIONALE:', fg='white')} {step['rationale']}")
|
|
3319
4060
|
click.echo(f" {click.style('EXPECTED:', fg='green')} {step['expected']}")
|
|
3320
|
-
|
|
3321
|
-
risk_color = {
|
|
3322
|
-
|
|
3323
|
-
|
|
4061
|
+
|
|
4062
|
+
risk_color = {"LOW": "green", "MEDIUM": "yellow", "HIGH": "red"}.get(
|
|
4063
|
+
step["risk"], "white"
|
|
4064
|
+
)
|
|
4065
|
+
click.echo(
|
|
4066
|
+
f" {click.style('RISK:', fg='cyan')} {click.style(step['risk'], fg=risk_color)}"
|
|
4067
|
+
)
|
|
4068
|
+
click.echo(
|
|
4069
|
+
f" {click.style('DEPENDENCIES:', fg='cyan')} {step['dependencies']}"
|
|
4070
|
+
)
|
|
3324
4071
|
click.echo()
|
|
3325
4072
|
else:
|
|
3326
4073
|
# Display single-step recommendation
|
|
3327
|
-
click.echo(click.style("✓ Recommendation generated!", fg=
|
|
4074
|
+
click.echo(click.style("✓ Recommendation generated!", fg="green") + "\n")
|
|
3328
4075
|
|
|
3329
|
-
click.echo(click.style("NEXT ACTION:", fg=
|
|
4076
|
+
click.echo(click.style("NEXT ACTION:", fg="cyan", bold=True))
|
|
3330
4077
|
click.echo(f" {result['action']}\n")
|
|
3331
4078
|
|
|
3332
|
-
click.echo(click.style("TARGET:", fg=
|
|
4079
|
+
click.echo(click.style("TARGET:", fg="cyan", bold=True))
|
|
3333
4080
|
click.echo(f" {result['target']}\n")
|
|
3334
4081
|
|
|
3335
|
-
click.echo(click.style("RATIONALE:", fg=
|
|
4082
|
+
click.echo(click.style("RATIONALE:", fg="cyan", bold=True))
|
|
3336
4083
|
click.echo(f" {result['rationale']}\n")
|
|
3337
4084
|
|
|
3338
|
-
click.echo(click.style("EXPECTED OUTCOME:", fg=
|
|
4085
|
+
click.echo(click.style("EXPECTED OUTCOME:", fg="cyan", bold=True))
|
|
3339
4086
|
click.echo(f" {result['expected_outcome']}\n")
|
|
3340
4087
|
|
|
3341
4088
|
# Color-code risk level
|
|
3342
|
-
risk = result[
|
|
3343
|
-
risk_colors = {
|
|
3344
|
-
risk_color = risk_colors.get(risk,
|
|
4089
|
+
risk = result["risk_level"]
|
|
4090
|
+
risk_colors = {"low": "green", "medium": "yellow", "high": "red"}
|
|
4091
|
+
risk_color = risk_colors.get(risk, "white")
|
|
3345
4092
|
|
|
3346
|
-
click.echo(click.style("RISK LEVEL:", fg=
|
|
4093
|
+
click.echo(click.style("RISK LEVEL:", fg="cyan", bold=True))
|
|
3347
4094
|
click.echo(" " + click.style(risk.upper(), fg=risk_color, bold=True))
|
|
3348
4095
|
|
|
3349
4096
|
click.echo("\n" + "=" * 70 + "\n")
|
|
3350
4097
|
|
|
3351
4098
|
|
|
3352
|
-
@ai.command(
|
|
3353
|
-
@click.option(
|
|
3354
|
-
@click.option(
|
|
4099
|
+
@ai.command("paths")
|
|
4100
|
+
@click.option("--engagement", "-e", type=int, help="Engagement ID (default: current)")
|
|
4101
|
+
@click.option(
|
|
4102
|
+
"--number",
|
|
4103
|
+
"-n",
|
|
4104
|
+
type=int,
|
|
4105
|
+
default=3,
|
|
4106
|
+
help="Number of paths to generate (default: 3)",
|
|
4107
|
+
)
|
|
3355
4108
|
def ai_paths(engagement, number):
|
|
3356
4109
|
"""Generate and rank multiple attack paths."""
|
|
3357
4110
|
from souleyez.storage.engagements import EngagementManager
|
|
@@ -3363,64 +4116,88 @@ def ai_paths(engagement, number):
|
|
|
3363
4116
|
if engagement:
|
|
3364
4117
|
eng = em.get_by_id(engagement)
|
|
3365
4118
|
if not eng:
|
|
3366
|
-
click.echo(click.style(f"✗ Engagement {engagement} not found", fg=
|
|
4119
|
+
click.echo(click.style(f"✗ Engagement {engagement} not found", fg="red"))
|
|
3367
4120
|
return
|
|
3368
4121
|
else:
|
|
3369
4122
|
eng = em.get_current()
|
|
3370
4123
|
if not eng:
|
|
3371
|
-
click.echo(
|
|
4124
|
+
click.echo(
|
|
4125
|
+
click.style(
|
|
4126
|
+
"✗ No active engagement. Use: souleyez engagement use <name>",
|
|
4127
|
+
fg="red",
|
|
4128
|
+
)
|
|
4129
|
+
)
|
|
3372
4130
|
return
|
|
3373
4131
|
|
|
3374
|
-
click.echo(
|
|
3375
|
-
|
|
4132
|
+
click.echo(
|
|
4133
|
+
click.style(
|
|
4134
|
+
f"\n🤖 Generating {number} alternative attack paths...",
|
|
4135
|
+
fg="cyan",
|
|
4136
|
+
bold=True,
|
|
4137
|
+
)
|
|
4138
|
+
)
|
|
4139
|
+
click.echo(
|
|
4140
|
+
click.style(
|
|
4141
|
+
"⏳ This may take 1-3 minutes depending on AI model speed...\n", fg="yellow"
|
|
4142
|
+
)
|
|
4143
|
+
)
|
|
3376
4144
|
|
|
3377
4145
|
# Generate paths
|
|
3378
4146
|
recommender = AttackRecommender()
|
|
3379
|
-
result = recommender.suggest_multiple_paths(eng[
|
|
4147
|
+
result = recommender.suggest_multiple_paths(eng["id"], num_paths=number)
|
|
3380
4148
|
|
|
3381
4149
|
# Handle errors
|
|
3382
|
-
if result.get(
|
|
3383
|
-
click.echo(click.style(f"✗ Error: {result['error']}", fg=
|
|
4150
|
+
if result.get("error"):
|
|
4151
|
+
click.echo(click.style(f"✗ Error: {result['error']}", fg="red"))
|
|
3384
4152
|
return
|
|
3385
4153
|
|
|
3386
|
-
paths = result.get(
|
|
4154
|
+
paths = result.get("paths", [])
|
|
3387
4155
|
if not paths:
|
|
3388
|
-
click.echo(click.style("No attack paths generated", fg=
|
|
4156
|
+
click.echo(click.style("No attack paths generated", fg="yellow"))
|
|
3389
4157
|
return
|
|
3390
4158
|
|
|
3391
4159
|
# Display ranked paths
|
|
3392
4160
|
click.echo("=" * 80)
|
|
3393
|
-
click.echo(
|
|
4161
|
+
click.echo(
|
|
4162
|
+
click.style(f"RANKED ATTACK PATHS (Top {len(paths)})", bold=True, fg="cyan")
|
|
4163
|
+
)
|
|
3394
4164
|
click.echo("=" * 80)
|
|
3395
4165
|
|
|
3396
4166
|
for scored_path in paths:
|
|
3397
|
-
rank = scored_path[
|
|
3398
|
-
path = scored_path[
|
|
3399
|
-
scores = scored_path[
|
|
3400
|
-
total = scored_path[
|
|
4167
|
+
rank = scored_path["rank"]
|
|
4168
|
+
path = scored_path["path"]
|
|
4169
|
+
scores = scored_path["scores"]
|
|
4170
|
+
total = scored_path["total_score"]
|
|
3401
4171
|
|
|
3402
4172
|
# Rank header with score
|
|
3403
4173
|
if rank == 1:
|
|
3404
|
-
rank_color =
|
|
3405
|
-
rank_icon =
|
|
4174
|
+
rank_color = "green"
|
|
4175
|
+
rank_icon = "🥇"
|
|
3406
4176
|
elif rank == 2:
|
|
3407
|
-
rank_color =
|
|
3408
|
-
rank_icon =
|
|
4177
|
+
rank_color = "yellow"
|
|
4178
|
+
rank_icon = "🥈"
|
|
3409
4179
|
elif rank == 3:
|
|
3410
|
-
rank_color =
|
|
3411
|
-
rank_icon =
|
|
4180
|
+
rank_color = "cyan"
|
|
4181
|
+
rank_icon = "🥉"
|
|
3412
4182
|
else:
|
|
3413
|
-
rank_color =
|
|
3414
|
-
rank_icon = f
|
|
4183
|
+
rank_color = "white"
|
|
4184
|
+
rank_icon = f"#{rank}"
|
|
3415
4185
|
|
|
3416
|
-
click.echo(
|
|
3417
|
-
|
|
4186
|
+
click.echo(
|
|
4187
|
+
f"\n{rank_icon} "
|
|
4188
|
+
+ click.style(f"PATH {rank}", bold=True, fg=rank_color)
|
|
4189
|
+
+ click.style(f" (Score: {total}/100)", fg=rank_color)
|
|
4190
|
+
)
|
|
3418
4191
|
click.echo("-" * 80)
|
|
3419
4192
|
|
|
3420
4193
|
# Path details
|
|
3421
4194
|
click.echo(f"ACTION: {path['action']}")
|
|
3422
4195
|
click.echo(f"TARGET: {path['target']}")
|
|
3423
|
-
risk_color =
|
|
4196
|
+
risk_color = (
|
|
4197
|
+
"green"
|
|
4198
|
+
if path["risk_level"] == "LOW"
|
|
4199
|
+
else ("yellow" if path["risk_level"] == "MEDIUM" else "red")
|
|
4200
|
+
)
|
|
3424
4201
|
click.echo(f"RISK: {click.style(path['risk_level'], fg=risk_color)}")
|
|
3425
4202
|
click.echo(f"\nRATIONALE: {path['rationale']}")
|
|
3426
4203
|
click.echo(f"EXPECTED: {path['expected']}")
|
|
@@ -3433,38 +4210,55 @@ def ai_paths(engagement, number):
|
|
|
3433
4210
|
click.echo(f" Complexity: -{scores['complexity']} (penalty)")
|
|
3434
4211
|
|
|
3435
4212
|
click.echo("\n" + "=" * 80)
|
|
3436
|
-
click.echo(
|
|
4213
|
+
click.echo(
|
|
4214
|
+
click.style(f"\n💡 Tip: Execute top path with: souleyez ai execute", fg="cyan")
|
|
4215
|
+
)
|
|
3437
4216
|
|
|
3438
4217
|
|
|
3439
4218
|
@ai.command("execute")
|
|
3440
|
-
@click.option(
|
|
4219
|
+
@click.option(
|
|
4220
|
+
"--engagement",
|
|
4221
|
+
"-e",
|
|
4222
|
+
type=int,
|
|
4223
|
+
default=None,
|
|
4224
|
+
help="Engagement ID (default: current)",
|
|
4225
|
+
)
|
|
3441
4226
|
@click.option("--once", is_flag=True, help="Run only one iteration then stop")
|
|
3442
4227
|
@click.option("--dry-run", is_flag=True, help="Show commands but don't execute")
|
|
3443
4228
|
@click.option("--auto-low", is_flag=True, help="Auto-approve LOW risk commands")
|
|
3444
|
-
@click.option(
|
|
3445
|
-
|
|
4229
|
+
@click.option(
|
|
4230
|
+
"--auto-medium", is_flag=True, help="Auto-approve LOW and MEDIUM risk commands"
|
|
4231
|
+
)
|
|
4232
|
+
@click.option(
|
|
4233
|
+
"--max-iterations", "-n", type=int, default=None, help="Maximum iterations to run"
|
|
4234
|
+
)
|
|
3446
4235
|
def ai_execute(engagement, once, dry_run, auto_low, auto_medium, max_iterations):
|
|
3447
4236
|
"""Execute AI-driven attack recommendations interactively."""
|
|
3448
4237
|
from souleyez.ai.executor import InteractiveExecutor
|
|
3449
4238
|
from souleyez.ai.safety import ApprovalMode
|
|
3450
4239
|
from souleyez.storage.engagements import EngagementManager
|
|
3451
|
-
|
|
4240
|
+
|
|
3452
4241
|
# Get engagement
|
|
3453
4242
|
eng_mgr = EngagementManager()
|
|
3454
|
-
|
|
4243
|
+
|
|
3455
4244
|
if engagement:
|
|
3456
4245
|
engagement_data = eng_mgr.get_by_id(engagement)
|
|
3457
4246
|
if not engagement_data:
|
|
3458
|
-
click.echo(
|
|
4247
|
+
click.echo(
|
|
4248
|
+
click.style(f"✗ Error:", fg="red")
|
|
4249
|
+
+ f" Engagement ID {engagement} not found"
|
|
4250
|
+
)
|
|
3459
4251
|
sys.exit(1)
|
|
3460
4252
|
else:
|
|
3461
4253
|
engagement_data = eng_mgr.get_current()
|
|
3462
4254
|
if not engagement_data:
|
|
3463
|
-
click.echo(
|
|
4255
|
+
click.echo(
|
|
4256
|
+
click.style("✗ Error:", fg="red") + " No current engagement selected"
|
|
4257
|
+
)
|
|
3464
4258
|
click.echo(" Use: souleyez engagement use <name>")
|
|
3465
4259
|
sys.exit(1)
|
|
3466
|
-
engagement = engagement_data[
|
|
3467
|
-
|
|
4260
|
+
engagement = engagement_data["id"]
|
|
4261
|
+
|
|
3468
4262
|
# Determine approval mode
|
|
3469
4263
|
if dry_run:
|
|
3470
4264
|
approval_mode = ApprovalMode.DRY_RUN
|
|
@@ -3474,20 +4268,18 @@ def ai_execute(engagement, once, dry_run, auto_low, auto_medium, max_iterations)
|
|
|
3474
4268
|
approval_mode = ApprovalMode.AUTO_LOW
|
|
3475
4269
|
else:
|
|
3476
4270
|
approval_mode = ApprovalMode.MANUAL
|
|
3477
|
-
|
|
4271
|
+
|
|
3478
4272
|
# Create executor and run
|
|
3479
4273
|
try:
|
|
3480
4274
|
executor = InteractiveExecutor(approval_mode=approval_mode)
|
|
3481
4275
|
executor.execute_loop(
|
|
3482
|
-
engagement_id=engagement,
|
|
3483
|
-
max_iterations=max_iterations,
|
|
3484
|
-
once=once
|
|
4276
|
+
engagement_id=engagement, max_iterations=max_iterations, once=once
|
|
3485
4277
|
)
|
|
3486
4278
|
except KeyboardInterrupt:
|
|
3487
|
-
click.echo(click.style("\n\n🛑 Execution stopped by user", fg=
|
|
4279
|
+
click.echo(click.style("\n\n🛑 Execution stopped by user", fg="yellow"))
|
|
3488
4280
|
sys.exit(0)
|
|
3489
4281
|
except Exception as e:
|
|
3490
|
-
click.echo(click.style(f"\n✗ Fatal error: {e}", fg=
|
|
4282
|
+
click.echo(click.style(f"\n✗ Fatal error: {e}", fg="red"))
|
|
3491
4283
|
sys.exit(1)
|
|
3492
4284
|
|
|
3493
4285
|
|
|
@@ -3495,6 +4287,7 @@ def ai_execute(engagement, once, dry_run, auto_low, auto_medium, max_iterations)
|
|
|
3495
4287
|
# Import Commands
|
|
3496
4288
|
# ============================================================================
|
|
3497
4289
|
|
|
4290
|
+
|
|
3498
4291
|
@cli.group()
|
|
3499
4292
|
def import_data():
|
|
3500
4293
|
"""Import data from external sources."""
|
|
@@ -3502,8 +4295,8 @@ def import_data():
|
|
|
3502
4295
|
|
|
3503
4296
|
|
|
3504
4297
|
@import_data.command("msf")
|
|
3505
|
-
@click.argument(
|
|
3506
|
-
@click.option(
|
|
4298
|
+
@click.argument("xml_file", type=click.Path(exists=True))
|
|
4299
|
+
@click.option("-v", "--verbose", is_flag=True, help="Show detailed import progress")
|
|
3507
4300
|
def import_msf(xml_file, verbose):
|
|
3508
4301
|
"""
|
|
3509
4302
|
Import data from Metasploit Framework XML export.
|
|
@@ -3520,13 +4313,24 @@ def import_msf(xml_file, verbose):
|
|
|
3520
4313
|
current_ws = em.get_current()
|
|
3521
4314
|
|
|
3522
4315
|
if not current_ws:
|
|
3523
|
-
click.echo(
|
|
4316
|
+
click.echo(
|
|
4317
|
+
click.style(
|
|
4318
|
+
"✗ No engagement selected! Use 'souleyez engagement use <name>'",
|
|
4319
|
+
fg="red",
|
|
4320
|
+
)
|
|
4321
|
+
)
|
|
3524
4322
|
return
|
|
3525
4323
|
|
|
3526
|
-
engagement_id = current_ws[
|
|
3527
|
-
engagement_name = current_ws[
|
|
4324
|
+
engagement_id = current_ws["id"]
|
|
4325
|
+
engagement_name = current_ws["name"]
|
|
3528
4326
|
|
|
3529
|
-
click.echo(
|
|
4327
|
+
click.echo(
|
|
4328
|
+
click.style(
|
|
4329
|
+
f"\n🔄 Importing Metasploit data into engagement: {engagement_name}",
|
|
4330
|
+
fg="cyan",
|
|
4331
|
+
bold=True,
|
|
4332
|
+
)
|
|
4333
|
+
)
|
|
3530
4334
|
click.echo()
|
|
3531
4335
|
|
|
3532
4336
|
importer = MSFImporter(engagement_id)
|
|
@@ -3535,7 +4339,9 @@ def import_msf(xml_file, verbose):
|
|
|
3535
4339
|
stats = importer.import_xml(xml_file, verbose=verbose)
|
|
3536
4340
|
|
|
3537
4341
|
click.echo()
|
|
3538
|
-
click.echo(
|
|
4342
|
+
click.echo(
|
|
4343
|
+
click.style("✓ Import completed successfully!", fg="green", bold=True)
|
|
4344
|
+
)
|
|
3539
4345
|
click.echo()
|
|
3540
4346
|
click.echo("Import Summary:")
|
|
3541
4347
|
click.echo(f" • Hosts: {stats['hosts']}")
|
|
@@ -3543,20 +4349,23 @@ def import_msf(xml_file, verbose):
|
|
|
3543
4349
|
click.echo(f" • Credentials: {stats['credentials']}")
|
|
3544
4350
|
click.echo(f" • Vulnerabilities: {stats['vulnerabilities']}")
|
|
3545
4351
|
|
|
3546
|
-
if stats[
|
|
4352
|
+
if stats["skipped"] > 0:
|
|
3547
4353
|
click.echo(f" • Skipped: {stats['skipped']}")
|
|
3548
4354
|
|
|
3549
4355
|
click.echo()
|
|
3550
|
-
click.echo(
|
|
4356
|
+
click.echo(
|
|
4357
|
+
click.style("💡 TIP:", fg="yellow", bold=True) + " View imported data with:"
|
|
4358
|
+
)
|
|
3551
4359
|
click.echo(" • souleyez dashboard")
|
|
3552
4360
|
click.echo(" • souleyez interactive")
|
|
3553
4361
|
click.echo(" • souleyez report generate")
|
|
3554
4362
|
click.echo()
|
|
3555
4363
|
|
|
3556
4364
|
except Exception as e:
|
|
3557
|
-
click.echo(click.style(f"\n✗ Import failed: {e}", fg=
|
|
4365
|
+
click.echo(click.style(f"\n✗ Import failed: {e}", fg="red"))
|
|
3558
4366
|
if verbose:
|
|
3559
4367
|
import traceback
|
|
4368
|
+
|
|
3560
4369
|
traceback.print_exc()
|
|
3561
4370
|
return
|
|
3562
4371
|
|
|
@@ -3568,7 +4377,7 @@ def config():
|
|
|
3568
4377
|
|
|
3569
4378
|
|
|
3570
4379
|
@config.command("get")
|
|
3571
|
-
@click.argument(
|
|
4380
|
+
@click.argument("key")
|
|
3572
4381
|
def config_get(key):
|
|
3573
4382
|
"""
|
|
3574
4383
|
Get a configuration value.
|
|
@@ -3590,8 +4399,8 @@ def config_get(key):
|
|
|
3590
4399
|
|
|
3591
4400
|
|
|
3592
4401
|
@config.command("set")
|
|
3593
|
-
@click.argument(
|
|
3594
|
-
@click.argument(
|
|
4402
|
+
@click.argument("key")
|
|
4403
|
+
@click.argument("value")
|
|
3595
4404
|
def config_set(key, value):
|
|
3596
4405
|
"""
|
|
3597
4406
|
Set a configuration value.
|
|
@@ -3610,26 +4419,33 @@ def config_set(key, value):
|
|
|
3610
4419
|
cfg = read_config()
|
|
3611
4420
|
|
|
3612
4421
|
# Convert value types for common settings
|
|
3613
|
-
if key in (
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
4422
|
+
if key in (
|
|
4423
|
+
"settings.threads",
|
|
4424
|
+
"security.session_timeout_minutes",
|
|
4425
|
+
"security.max_login_attempts",
|
|
4426
|
+
"security.lockout_duration_minutes",
|
|
4427
|
+
"security.min_password_length",
|
|
4428
|
+
"crypto.iterations",
|
|
4429
|
+
"database.backup_interval_hours",
|
|
4430
|
+
"ai.max_tokens",
|
|
4431
|
+
"logging.max_bytes",
|
|
4432
|
+
"logging.backup_count",
|
|
4433
|
+
):
|
|
3618
4434
|
try:
|
|
3619
4435
|
value = int(value)
|
|
3620
4436
|
except ValueError:
|
|
3621
|
-
click.echo(click.style(f"Error: {key} requires an integer value", fg=
|
|
4437
|
+
click.echo(click.style(f"Error: {key} requires an integer value", fg="red"))
|
|
3622
4438
|
return
|
|
3623
4439
|
|
|
3624
|
-
if key in (
|
|
4440
|
+
if key in ("ai.temperature",):
|
|
3625
4441
|
try:
|
|
3626
4442
|
value = float(value)
|
|
3627
4443
|
except ValueError:
|
|
3628
|
-
click.echo(click.style(f"Error: {key} requires a numeric value", fg=
|
|
4444
|
+
click.echo(click.style(f"Error: {key} requires a numeric value", fg="red"))
|
|
3629
4445
|
return
|
|
3630
4446
|
|
|
3631
|
-
if key in (
|
|
3632
|
-
value = value.lower() in (
|
|
4447
|
+
if key in ("database.backup_enabled",):
|
|
4448
|
+
value = value.lower() in ("true", "1", "yes")
|
|
3633
4449
|
|
|
3634
4450
|
# Set the value
|
|
3635
4451
|
_set_nested(cfg, key, value)
|
|
@@ -3637,7 +4453,7 @@ def config_set(key, value):
|
|
|
3637
4453
|
# Write config
|
|
3638
4454
|
write_config(cfg)
|
|
3639
4455
|
|
|
3640
|
-
click.echo(click.style(f"✓ Set {key} = {value}", fg=
|
|
4456
|
+
click.echo(click.style(f"✓ Set {key} = {value}", fg="green"))
|
|
3641
4457
|
|
|
3642
4458
|
|
|
3643
4459
|
@config.command("list")
|
|
@@ -3654,8 +4470,10 @@ def config_list():
|
|
|
3654
4470
|
|
|
3655
4471
|
# Import and register screenshot commands
|
|
3656
4472
|
from souleyez.commands.screenshots import screenshots
|
|
4473
|
+
|
|
3657
4474
|
cli.add_command(screenshots)
|
|
3658
4475
|
|
|
3659
4476
|
# Import and register deliverable commands
|
|
3660
4477
|
from souleyez.commands.deliverables import deliverables
|
|
4478
|
+
|
|
3661
4479
|
cli.add_command(deliverables)
|