souleyez 2.43.26__py3-none-any.whl → 2.43.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23434 -10286
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.26.dist-info/RECORD +0 -379
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/ui/evidence_vault.py
CHANGED
|
@@ -17,16 +17,18 @@ console = Console()
|
|
|
17
17
|
|
|
18
18
|
# Phase display info
|
|
19
19
|
PHASE_DISPLAY = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
"reconnaissance": ("📡 RECON", "cyan"),
|
|
21
|
+
"enumeration": ("🔍 ENUM", "blue"),
|
|
22
|
+
"exploitation": ("💥 EXPLOIT", "red"),
|
|
23
|
+
"post_exploitation": ("🎯 POST", "magenta"),
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
PHASE_ORDER = [
|
|
26
|
+
PHASE_ORDER = ["reconnaissance", "enumeration", "exploitation", "post_exploitation"]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def _flatten_evidence(
|
|
29
|
+
def _flatten_evidence(
|
|
30
|
+
evidence: Dict[str, List[Dict]], phase_filter: Optional[str] = None
|
|
31
|
+
) -> List[Dict]:
|
|
30
32
|
"""Flatten evidence dict into a single list with phase info."""
|
|
31
33
|
flat = []
|
|
32
34
|
for phase_key in PHASE_ORDER:
|
|
@@ -34,17 +36,13 @@ def _flatten_evidence(evidence: Dict[str, List[Dict]], phase_filter: Optional[st
|
|
|
34
36
|
continue
|
|
35
37
|
for item in evidence.get(phase_key, []):
|
|
36
38
|
item_copy = item.copy()
|
|
37
|
-
item_copy[
|
|
39
|
+
item_copy["phase"] = phase_key
|
|
38
40
|
flat.append(item_copy)
|
|
39
41
|
return flat
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def _build_evidence_table(
|
|
43
|
-
items: List[Dict],
|
|
44
|
-
selected_ids: Set[int],
|
|
45
|
-
page: int,
|
|
46
|
-
page_size: int,
|
|
47
|
-
view_all: bool
|
|
45
|
+
items: List[Dict], selected_ids: Set[int], page: int, page_size: int, view_all: bool
|
|
48
46
|
) -> tuple:
|
|
49
47
|
"""Build Rich table for evidence items.
|
|
50
48
|
|
|
@@ -67,7 +65,7 @@ def _build_evidence_table(
|
|
|
67
65
|
header_style="bold cyan",
|
|
68
66
|
box=DesignSystem.TABLE_BOX,
|
|
69
67
|
padding=(0, 1),
|
|
70
|
-
expand=True
|
|
68
|
+
expand=True,
|
|
71
69
|
)
|
|
72
70
|
|
|
73
71
|
table.add_column("○", width=3, justify="center")
|
|
@@ -87,40 +85,40 @@ def _build_evidence_table(
|
|
|
87
85
|
row_num = (page * page_size) + idx + 1
|
|
88
86
|
|
|
89
87
|
# Checkbox
|
|
90
|
-
item_id = item.get(
|
|
91
|
-
checkbox =
|
|
88
|
+
item_id = item.get("id", idx)
|
|
89
|
+
checkbox = "●" if item_id in selected_ids else "○"
|
|
92
90
|
|
|
93
91
|
# Phase with color
|
|
94
|
-
phase_key = item.get(
|
|
95
|
-
phase_label, phase_color = PHASE_DISPLAY.get(phase_key, (
|
|
92
|
+
phase_key = item.get("phase", "reconnaissance")
|
|
93
|
+
phase_label, phase_color = PHASE_DISPLAY.get(phase_key, ("?", "white"))
|
|
96
94
|
phase_display = f"[{phase_color}]{phase_label}[/{phase_color}]"
|
|
97
95
|
|
|
98
96
|
# Type
|
|
99
|
-
item_type = item.get(
|
|
97
|
+
item_type = item.get("type", "job").capitalize()
|
|
100
98
|
|
|
101
99
|
# Tool
|
|
102
|
-
tool = item.get(
|
|
100
|
+
tool = item.get("tool", "-").upper()
|
|
103
101
|
|
|
104
102
|
# Title - use label for jobs, title for findings
|
|
105
|
-
if item_type.lower() ==
|
|
106
|
-
title = item.get(
|
|
103
|
+
if item_type.lower() == "job":
|
|
104
|
+
title = item.get("label") or item.get("title", "-")
|
|
107
105
|
else:
|
|
108
|
-
title = item.get(
|
|
106
|
+
title = item.get("title", "-")
|
|
109
107
|
|
|
110
108
|
# Description - actual description field
|
|
111
|
-
desc = item.get(
|
|
112
|
-
if not desc or desc ==
|
|
113
|
-
desc =
|
|
109
|
+
desc = item.get("description", "-")
|
|
110
|
+
if not desc or desc == "None":
|
|
111
|
+
desc = "-"
|
|
114
112
|
if len(str(desc)) > 28:
|
|
115
|
-
desc = str(desc)[:28] +
|
|
113
|
+
desc = str(desc)[:28] + "…"
|
|
116
114
|
|
|
117
115
|
# Severity with color
|
|
118
|
-
severity = item.get(
|
|
119
|
-
if severity in [
|
|
116
|
+
severity = item.get("severity", "")
|
|
117
|
+
if severity in ["critical", "high"]:
|
|
120
118
|
sev_display = f"[red]🔴 HIGH[/red]"
|
|
121
|
-
elif severity ==
|
|
119
|
+
elif severity == "medium":
|
|
122
120
|
sev_display = f"[yellow]🟡 MED[/yellow]"
|
|
123
|
-
elif severity in [
|
|
121
|
+
elif severity in ["low", "info"]:
|
|
124
122
|
sev_display = f"[green]🟢 LOW[/green]"
|
|
125
123
|
else:
|
|
126
124
|
sev_display = "[dim]--[/dim]"
|
|
@@ -133,7 +131,7 @@ def _build_evidence_table(
|
|
|
133
131
|
tool,
|
|
134
132
|
title,
|
|
135
133
|
desc,
|
|
136
|
-
sev_display
|
|
134
|
+
sev_display,
|
|
137
135
|
)
|
|
138
136
|
|
|
139
137
|
return table, total_pages, displayed
|
|
@@ -177,7 +175,13 @@ def view_evidence_vault(engagement_id: int):
|
|
|
177
175
|
|
|
178
176
|
# Header
|
|
179
177
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
180
|
-
click.echo(
|
|
178
|
+
click.echo(
|
|
179
|
+
"│"
|
|
180
|
+
+ click.style(
|
|
181
|
+
" EVIDENCE & ARTIFACTS ".center(width - 2), bold=True, fg="cyan"
|
|
182
|
+
)
|
|
183
|
+
+ "│"
|
|
184
|
+
)
|
|
181
185
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
182
186
|
click.echo()
|
|
183
187
|
|
|
@@ -188,30 +192,43 @@ def view_evidence_vault(engagement_id: int):
|
|
|
188
192
|
|
|
189
193
|
# Calculate stats
|
|
190
194
|
total_count = sum(len(items) for items in evidence.values())
|
|
191
|
-
credentials_count = sum(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
credentials_count = sum(
|
|
196
|
+
1
|
|
197
|
+
for phase_items in evidence.values()
|
|
198
|
+
for item in phase_items
|
|
199
|
+
if item.get("type") == "credential"
|
|
200
|
+
)
|
|
201
|
+
findings_count = sum(
|
|
202
|
+
1
|
|
203
|
+
for phase_items in evidence.values()
|
|
204
|
+
for item in phase_items
|
|
205
|
+
if item.get("type") == "finding"
|
|
206
|
+
)
|
|
207
|
+
high_value = sum(
|
|
208
|
+
1
|
|
209
|
+
for phase_items in evidence.values()
|
|
210
|
+
for item in phase_items
|
|
211
|
+
if item.get("severity") in ["critical", "high"]
|
|
212
|
+
)
|
|
198
213
|
|
|
199
214
|
# Summary line
|
|
200
215
|
click.echo(click.style("📊 SUMMARY", bold=True))
|
|
201
|
-
click.echo(
|
|
216
|
+
click.echo(
|
|
217
|
+
f" Total: {total_count} │ 🔑 Credentials: {credentials_count} │ 🔍 Findings: {findings_count} │ 🔴 High-Value: {high_value} │ 📸 Screenshots: {screenshot_count}"
|
|
218
|
+
)
|
|
202
219
|
click.echo()
|
|
203
220
|
|
|
204
221
|
# Engagement and filter info
|
|
205
222
|
filter_parts = [f"Engagement: {engagement['name']}"]
|
|
206
223
|
if phase_filter:
|
|
207
|
-
phase_label, _ = PHASE_DISPLAY.get(phase_filter, (phase_filter,
|
|
224
|
+
phase_label, _ = PHASE_DISPLAY.get(phase_filter, (phase_filter, "white"))
|
|
208
225
|
filter_parts.append(f"Phase: {phase_label}")
|
|
209
226
|
if filters:
|
|
210
|
-
if
|
|
227
|
+
if "tool" in filters:
|
|
211
228
|
filter_parts.append(f"Tool: {filters['tool']}")
|
|
212
|
-
if
|
|
229
|
+
if "host" in filters:
|
|
213
230
|
filter_parts.append(f"Host: {filters['host']}")
|
|
214
|
-
if
|
|
231
|
+
if "days" in filters:
|
|
215
232
|
filter_parts.append(f"Last {filters['days']} days")
|
|
216
233
|
|
|
217
234
|
# Flatten evidence for table display
|
|
@@ -229,7 +246,7 @@ def view_evidence_vault(engagement_id: int):
|
|
|
229
246
|
|
|
230
247
|
# Build and display table
|
|
231
248
|
if total_items == 0:
|
|
232
|
-
click.echo(click.style(" No evidence collected yet", fg=
|
|
249
|
+
click.echo(click.style(" No evidence collected yet", fg="bright_black"))
|
|
233
250
|
else:
|
|
234
251
|
table, _, displayed_items = _build_evidence_table(
|
|
235
252
|
flat_evidence, selected_ids, page, page_size, view_all
|
|
@@ -242,7 +259,11 @@ def view_evidence_vault(engagement_id: int):
|
|
|
242
259
|
|
|
243
260
|
# Selection count
|
|
244
261
|
if selected_ids:
|
|
245
|
-
click.echo(
|
|
262
|
+
click.echo(
|
|
263
|
+
click.style(
|
|
264
|
+
f" Selected: {len(selected_ids)} item(s)", fg="cyan", bold=True
|
|
265
|
+
)
|
|
266
|
+
)
|
|
246
267
|
|
|
247
268
|
# Menu
|
|
248
269
|
click.echo()
|
|
@@ -250,11 +271,21 @@ def view_evidence_vault(engagement_id: int):
|
|
|
250
271
|
click.echo()
|
|
251
272
|
click.echo(" [#] View evidence details")
|
|
252
273
|
click.echo(" [t] Toggle pagination" + (" (showing all)" if view_all else ""))
|
|
253
|
-
click.echo(
|
|
274
|
+
click.echo(
|
|
275
|
+
" [g] Filter by phase"
|
|
276
|
+
+ (
|
|
277
|
+
f" ({PHASE_DISPLAY.get(phase_filter, ('All', ''))[0]})"
|
|
278
|
+
if phase_filter
|
|
279
|
+
else " (All)"
|
|
280
|
+
)
|
|
281
|
+
)
|
|
254
282
|
click.echo(" [c] Screenshots" + f" ({screenshot_count})")
|
|
255
283
|
click.echo(" [s] Search evidence")
|
|
256
284
|
click.echo(" [f] Filter by tool/host/date")
|
|
257
|
-
click.echo(
|
|
285
|
+
click.echo(
|
|
286
|
+
" [x] Export"
|
|
287
|
+
+ (f" ({len(selected_ids)} selected)" if selected_ids else " all")
|
|
288
|
+
)
|
|
258
289
|
click.echo(" [?] Help")
|
|
259
290
|
click.echo(" [q] Back")
|
|
260
291
|
click.echo()
|
|
@@ -262,18 +293,18 @@ def view_evidence_vault(engagement_id: int):
|
|
|
262
293
|
try:
|
|
263
294
|
choice = input(" Select option: ").strip().lower()
|
|
264
295
|
|
|
265
|
-
if choice ==
|
|
296
|
+
if choice == "q":
|
|
266
297
|
return
|
|
267
|
-
elif choice ==
|
|
298
|
+
elif choice == "t":
|
|
268
299
|
view_all = not view_all
|
|
269
300
|
page = 0
|
|
270
|
-
elif choice ==
|
|
301
|
+
elif choice == "n" and not view_all:
|
|
271
302
|
if page < total_pages - 1:
|
|
272
303
|
page += 1
|
|
273
|
-
elif choice ==
|
|
304
|
+
elif choice == "p" and not view_all:
|
|
274
305
|
if page > 0:
|
|
275
306
|
page -= 1
|
|
276
|
-
elif choice ==
|
|
307
|
+
elif choice == "g":
|
|
277
308
|
# Inline phase filter submenu
|
|
278
309
|
click.echo("\n Filter by phase:")
|
|
279
310
|
click.echo(" [0] All phases")
|
|
@@ -284,32 +315,37 @@ def view_evidence_vault(engagement_id: int):
|
|
|
284
315
|
click.echo()
|
|
285
316
|
phase_choice = input(" Select option: ").strip()
|
|
286
317
|
phase_map = {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
318
|
+
"0": None,
|
|
319
|
+
"1": "reconnaissance",
|
|
320
|
+
"2": "enumeration",
|
|
321
|
+
"3": "exploitation",
|
|
322
|
+
"4": "post_exploitation",
|
|
292
323
|
}
|
|
293
324
|
if phase_choice in phase_map:
|
|
294
325
|
phase_filter = phase_map[phase_choice]
|
|
295
326
|
page = 0
|
|
296
|
-
elif choice ==
|
|
327
|
+
elif choice == "c":
|
|
297
328
|
view_screenshots(engagement_id, screenshots, sm)
|
|
298
|
-
elif choice ==
|
|
329
|
+
elif choice == "s":
|
|
299
330
|
search_evidence(engagement_id, evidence)
|
|
300
|
-
elif choice ==
|
|
331
|
+
elif choice == "f":
|
|
301
332
|
filters = apply_filters()
|
|
302
333
|
page = 0
|
|
303
|
-
elif choice ==
|
|
334
|
+
elif choice == "x":
|
|
304
335
|
if selected_ids:
|
|
305
336
|
# Export selected items
|
|
306
|
-
click.echo(
|
|
337
|
+
click.echo(
|
|
338
|
+
click.style(
|
|
339
|
+
f"\nExporting {len(selected_ids)} selected items...",
|
|
340
|
+
fg="yellow",
|
|
341
|
+
)
|
|
342
|
+
)
|
|
307
343
|
export_evidence_bundle(engagement_id, engagement, evidence)
|
|
308
344
|
else:
|
|
309
345
|
export_evidence_bundle(engagement_id, engagement, evidence)
|
|
310
|
-
elif choice ==
|
|
346
|
+
elif choice == "?":
|
|
311
347
|
_show_evidence_help()
|
|
312
|
-
elif choice ==
|
|
348
|
+
elif choice == "i":
|
|
313
349
|
# Interactive mode
|
|
314
350
|
_interactive_evidence_select(flat_evidence, selected_ids, engagement_id)
|
|
315
351
|
elif choice.isdigit():
|
|
@@ -319,79 +355,85 @@ def view_evidence_vault(engagement_id: int):
|
|
|
319
355
|
item = flat_evidence[row_num - 1]
|
|
320
356
|
_view_evidence_detail(item)
|
|
321
357
|
else:
|
|
322
|
-
click.echo(
|
|
358
|
+
click.echo(
|
|
359
|
+
click.style(f"Invalid row number (1-{total_items})", fg="red")
|
|
360
|
+
)
|
|
323
361
|
click.pause()
|
|
324
|
-
elif choice.startswith(
|
|
362
|
+
elif choice.startswith(" ") and choice.strip().isdigit():
|
|
325
363
|
# Toggle selection with space+number
|
|
326
364
|
row_num = int(choice.strip())
|
|
327
365
|
if 1 <= row_num <= total_items:
|
|
328
366
|
item = flat_evidence[row_num - 1]
|
|
329
|
-
item_id = item.get(
|
|
367
|
+
item_id = item.get("id", row_num - 1)
|
|
330
368
|
if item_id in selected_ids:
|
|
331
369
|
selected_ids.discard(item_id)
|
|
332
370
|
else:
|
|
333
371
|
selected_ids.add(item_id)
|
|
334
|
-
elif choice ==
|
|
372
|
+
elif choice == "":
|
|
335
373
|
pass # Just refresh
|
|
336
374
|
else:
|
|
337
|
-
click.echo(click.style("Invalid option. Press ? for help.", fg=
|
|
375
|
+
click.echo(click.style("Invalid option. Press ? for help.", fg="red"))
|
|
338
376
|
click.pause()
|
|
339
377
|
|
|
340
378
|
except (KeyboardInterrupt, EOFError):
|
|
341
379
|
return
|
|
342
380
|
except ValueError:
|
|
343
|
-
click.echo(click.style("Invalid input", fg=
|
|
381
|
+
click.echo(click.style("Invalid input", fg="red"))
|
|
344
382
|
click.pause()
|
|
345
383
|
|
|
346
384
|
|
|
347
|
-
def _interactive_evidence_select(
|
|
385
|
+
def _interactive_evidence_select(
|
|
386
|
+
items: List[Dict], selected_ids: Set[int], engagement_id: int
|
|
387
|
+
):
|
|
348
388
|
"""Interactive mode for evidence selection with arrow keys."""
|
|
349
389
|
from souleyez.ui.interactive_selector import interactive_select
|
|
350
390
|
|
|
351
391
|
if not items:
|
|
352
|
-
click.echo(click.style("\nNo evidence to display", fg=
|
|
392
|
+
click.echo(click.style("\nNo evidence to display", fg="yellow"))
|
|
353
393
|
click.pause()
|
|
354
394
|
return
|
|
355
395
|
|
|
356
396
|
# Build item dicts for selector
|
|
357
397
|
item_dicts = []
|
|
358
398
|
for item in items:
|
|
359
|
-
item_type = item.get(
|
|
360
|
-
if item_type ==
|
|
361
|
-
title = item.get(
|
|
399
|
+
item_type = item.get("type", "job")
|
|
400
|
+
if item_type == "job":
|
|
401
|
+
title = item.get("label") or item.get("title", "-")
|
|
362
402
|
else:
|
|
363
|
-
title = item.get(
|
|
403
|
+
title = item.get("title", "-")
|
|
364
404
|
if len(str(title)) > 50:
|
|
365
|
-
title = str(title)[:50] +
|
|
366
|
-
|
|
367
|
-
phase_key = item.get(
|
|
368
|
-
phase_label, _ = PHASE_DISPLAY.get(phase_key, (
|
|
369
|
-
|
|
370
|
-
severity = item.get(
|
|
371
|
-
if severity in [
|
|
372
|
-
sev_display =
|
|
373
|
-
elif severity ==
|
|
374
|
-
sev_display =
|
|
375
|
-
elif severity in [
|
|
376
|
-
sev_display =
|
|
405
|
+
title = str(title)[:50] + "…"
|
|
406
|
+
|
|
407
|
+
phase_key = item.get("phase", "reconnaissance")
|
|
408
|
+
phase_label, _ = PHASE_DISPLAY.get(phase_key, ("?", "white"))
|
|
409
|
+
|
|
410
|
+
severity = item.get("severity", "")
|
|
411
|
+
if severity in ["critical", "high"]:
|
|
412
|
+
sev_display = "🔴 HIGH"
|
|
413
|
+
elif severity == "medium":
|
|
414
|
+
sev_display = "🟡 MED"
|
|
415
|
+
elif severity in ["low", "info"]:
|
|
416
|
+
sev_display = "🟢 LOW"
|
|
377
417
|
else:
|
|
378
|
-
sev_display =
|
|
379
|
-
|
|
380
|
-
item_dicts.append(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
418
|
+
sev_display = "--"
|
|
419
|
+
|
|
420
|
+
item_dicts.append(
|
|
421
|
+
{
|
|
422
|
+
"id": item.get("id", 0),
|
|
423
|
+
"phase": phase_label,
|
|
424
|
+
"type": item.get("type", "job").capitalize(),
|
|
425
|
+
"tool": item.get("tool", "-").upper(),
|
|
426
|
+
"title": title,
|
|
427
|
+
"severity": sev_display,
|
|
428
|
+
}
|
|
429
|
+
)
|
|
388
430
|
|
|
389
431
|
def on_action(action: str, selected: set, current_item: dict):
|
|
390
|
-
if action ==
|
|
432
|
+
if action == "v" and current_item:
|
|
391
433
|
# View details
|
|
392
|
-
item_id = current_item.get(
|
|
434
|
+
item_id = current_item.get("id")
|
|
393
435
|
for item in items:
|
|
394
|
-
if item.get(
|
|
436
|
+
if item.get("id") == item_id:
|
|
395
437
|
_view_evidence_detail(item)
|
|
396
438
|
break
|
|
397
439
|
|
|
@@ -399,31 +441,33 @@ def _interactive_evidence_select(items: List[Dict], selected_ids: Set[int], enga
|
|
|
399
441
|
interactive_select(
|
|
400
442
|
items=item_dicts,
|
|
401
443
|
columns=[
|
|
402
|
-
{
|
|
403
|
-
{
|
|
404
|
-
{
|
|
405
|
-
{
|
|
406
|
-
{
|
|
444
|
+
{"name": "Phase", "width": 12, "key": "phase"},
|
|
445
|
+
{"name": "Type", "width": 10, "key": "type"},
|
|
446
|
+
{"name": "Tool", "width": 10, "key": "tool"},
|
|
447
|
+
{"name": "Title", "key": "title"},
|
|
448
|
+
{"name": "Severity", "width": 10, "key": "severity"},
|
|
407
449
|
],
|
|
408
450
|
selected_ids=selected_ids,
|
|
409
|
-
get_id=lambda x: x.get(
|
|
410
|
-
title="SELECT EVIDENCE"
|
|
451
|
+
get_id=lambda x: x.get("id"),
|
|
452
|
+
title="SELECT EVIDENCE",
|
|
411
453
|
)
|
|
412
454
|
|
|
413
455
|
if not selected_ids:
|
|
414
456
|
return
|
|
415
457
|
|
|
416
458
|
result = _evidence_bulk_action_menu(items, selected_ids, engagement_id)
|
|
417
|
-
if result ==
|
|
459
|
+
if result == "back":
|
|
418
460
|
return
|
|
419
461
|
|
|
420
462
|
|
|
421
|
-
def _evidence_bulk_action_menu(
|
|
463
|
+
def _evidence_bulk_action_menu(
|
|
464
|
+
items: List[Dict], selected_ids: Set[int], engagement_id: int
|
|
465
|
+
) -> str:
|
|
422
466
|
"""Show action menu for selected evidence items."""
|
|
423
|
-
selected_items = [item for item in items if item.get(
|
|
467
|
+
selected_items = [item for item in items if item.get("id") in selected_ids]
|
|
424
468
|
|
|
425
469
|
if not selected_items:
|
|
426
|
-
return
|
|
470
|
+
return "continue"
|
|
427
471
|
|
|
428
472
|
click.echo()
|
|
429
473
|
click.echo(f" Selected: {len(selected_items)} item(s)")
|
|
@@ -434,25 +478,29 @@ def _evidence_bulk_action_menu(items: List[Dict], selected_ids: Set[int], engage
|
|
|
434
478
|
click.echo()
|
|
435
479
|
|
|
436
480
|
try:
|
|
437
|
-
choice =
|
|
481
|
+
choice = (
|
|
482
|
+
click.prompt(" Select option", default="q", show_default=False)
|
|
483
|
+
.strip()
|
|
484
|
+
.lower()
|
|
485
|
+
)
|
|
438
486
|
|
|
439
|
-
if choice ==
|
|
440
|
-
return
|
|
441
|
-
elif choice ==
|
|
487
|
+
if choice == "q":
|
|
488
|
+
return "back"
|
|
489
|
+
elif choice == "v" and selected_items:
|
|
442
490
|
_view_evidence_detail(selected_items[0])
|
|
443
|
-
return
|
|
444
|
-
elif choice ==
|
|
491
|
+
return "continue"
|
|
492
|
+
elif choice == "x":
|
|
445
493
|
_export_selected_evidence(selected_items, engagement_id)
|
|
446
|
-
return
|
|
447
|
-
elif choice ==
|
|
494
|
+
return "continue"
|
|
495
|
+
elif choice == "c":
|
|
448
496
|
selected_ids.clear()
|
|
449
|
-
click.echo(click.style(" ✓ Selection cleared", fg=
|
|
450
|
-
return
|
|
497
|
+
click.echo(click.style(" ✓ Selection cleared", fg="green"))
|
|
498
|
+
return "continue"
|
|
451
499
|
|
|
452
500
|
except (KeyboardInterrupt, EOFError):
|
|
453
501
|
pass
|
|
454
502
|
|
|
455
|
-
return
|
|
503
|
+
return "continue"
|
|
456
504
|
|
|
457
505
|
|
|
458
506
|
def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
|
|
@@ -462,13 +510,15 @@ def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
|
|
|
462
510
|
from datetime import datetime
|
|
463
511
|
from souleyez.storage.engagement import EngagementManager
|
|
464
512
|
|
|
465
|
-
click.echo(
|
|
513
|
+
click.echo(
|
|
514
|
+
click.style(f"\n Exporting {len(selected_items)} item(s)...", fg="yellow")
|
|
515
|
+
)
|
|
466
516
|
|
|
467
517
|
try:
|
|
468
518
|
# Get engagement info
|
|
469
519
|
em = EngagementManager()
|
|
470
520
|
engagement = em.get_by_id(engagement_id)
|
|
471
|
-
eng_name = engagement[
|
|
521
|
+
eng_name = engagement["name"] if engagement else f"engagement_{engagement_id}"
|
|
472
522
|
|
|
473
523
|
# Create output directory
|
|
474
524
|
output_dir = os.path.expanduser("~/.souleyez/exports")
|
|
@@ -476,12 +526,12 @@ def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
|
|
|
476
526
|
|
|
477
527
|
# Generate filename
|
|
478
528
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
479
|
-
safe_name = eng_name.replace(
|
|
529
|
+
safe_name = eng_name.replace(" ", "_").replace("/", "_").replace("\\", "_")
|
|
480
530
|
zip_filename = f"{safe_name}_selected_evidence_{timestamp}.zip"
|
|
481
531
|
zip_path = os.path.join(output_dir, zip_filename)
|
|
482
532
|
|
|
483
533
|
# Create ZIP
|
|
484
|
-
with zipfile.ZipFile(zip_path,
|
|
534
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
485
535
|
# Add README
|
|
486
536
|
readme_lines = [
|
|
487
537
|
"=" * 70,
|
|
@@ -496,18 +546,20 @@ def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
|
|
|
496
546
|
]
|
|
497
547
|
|
|
498
548
|
for idx, item in enumerate(selected_items, 1):
|
|
499
|
-
item_type = item.get(
|
|
500
|
-
tool = item.get(
|
|
501
|
-
target = item.get(
|
|
549
|
+
item_type = item.get("type", "unknown")
|
|
550
|
+
tool = item.get("tool", "N/A")
|
|
551
|
+
target = item.get("target", "N/A")
|
|
502
552
|
readme_lines.append(f" {idx}. [{item_type}] {tool} -> {target}")
|
|
503
553
|
|
|
504
554
|
# Export job logs
|
|
505
|
-
if item_type ==
|
|
506
|
-
log_path = item.get(
|
|
555
|
+
if item_type == "job":
|
|
556
|
+
log_path = item.get("log_path")
|
|
507
557
|
if log_path and os.path.exists(log_path):
|
|
508
|
-
safe_tool = tool.replace(
|
|
509
|
-
safe_target =
|
|
510
|
-
|
|
558
|
+
safe_tool = tool.replace("/", "_").replace("\\", "_")
|
|
559
|
+
safe_target = (
|
|
560
|
+
str(target).replace("/", "_").replace(":", "_")[:50]
|
|
561
|
+
)
|
|
562
|
+
phase = item.get("phase", "general").replace("_", "-")
|
|
511
563
|
arcname = f"{phase}/{idx:03d}_{safe_tool}_{safe_target}.log"
|
|
512
564
|
zipf.write(log_path, arcname)
|
|
513
565
|
readme_lines.append(f" -> {arcname}")
|
|
@@ -515,11 +567,11 @@ def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
|
|
|
515
567
|
readme_lines.extend(["", "=" * 70])
|
|
516
568
|
zipf.writestr("README.txt", "\n".join(readme_lines))
|
|
517
569
|
|
|
518
|
-
click.echo(click.style(f"\n ✓ Exported to:", fg=
|
|
570
|
+
click.echo(click.style(f"\n ✓ Exported to:", fg="green"))
|
|
519
571
|
click.echo(f" {zip_path}")
|
|
520
572
|
|
|
521
573
|
except Exception as e:
|
|
522
|
-
click.echo(click.style(f"\n ✗ Export failed: {e}", fg=
|
|
574
|
+
click.echo(click.style(f"\n ✗ Export failed: {e}", fg="red"))
|
|
523
575
|
|
|
524
576
|
click.pause()
|
|
525
577
|
|
|
@@ -530,32 +582,38 @@ def _show_evidence_help():
|
|
|
530
582
|
width = get_terminal_width()
|
|
531
583
|
|
|
532
584
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
533
|
-
click.echo(
|
|
585
|
+
click.echo(
|
|
586
|
+
"│"
|
|
587
|
+
+ click.style(
|
|
588
|
+
" EVIDENCE & ARTIFACTS - HELP ".center(width - 2), bold=True, fg="cyan"
|
|
589
|
+
)
|
|
590
|
+
+ "│"
|
|
591
|
+
)
|
|
534
592
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
535
593
|
click.echo()
|
|
536
594
|
|
|
537
|
-
click.echo(click.style(" NAVIGATION", bold=True, fg=
|
|
595
|
+
click.echo(click.style(" NAVIGATION", bold=True, fg="yellow"))
|
|
538
596
|
click.echo(" ───────────────────────────────────────")
|
|
539
597
|
click.echo(" [#] Enter a row number to view details")
|
|
540
598
|
click.echo(" [n/p] Next/Previous page (when paginated)")
|
|
541
599
|
click.echo(" [t] Toggle between paginated and full view")
|
|
542
600
|
click.echo()
|
|
543
601
|
|
|
544
|
-
click.echo(click.style(" FILTERING", bold=True, fg=
|
|
602
|
+
click.echo(click.style(" FILTERING", bold=True, fg="yellow"))
|
|
545
603
|
click.echo(" ───────────────────────────────────────")
|
|
546
604
|
click.echo(" [g] Filter by phase (shows submenu)")
|
|
547
605
|
click.echo(" [f] Apply filters by tool, host, or date range")
|
|
548
606
|
click.echo(" [s] Full-text search across all evidence")
|
|
549
607
|
click.echo()
|
|
550
608
|
|
|
551
|
-
click.echo(click.style(" ACTIONS", bold=True, fg=
|
|
609
|
+
click.echo(click.style(" ACTIONS", bold=True, fg="yellow"))
|
|
552
610
|
click.echo(" ───────────────────────────────────────")
|
|
553
611
|
click.echo(" [space #] Toggle selection for row # (e.g., ' 3' selects row 3)")
|
|
554
612
|
click.echo(" [x] Export evidence (selected items or all)")
|
|
555
613
|
click.echo(" [c] View and manage screenshots")
|
|
556
614
|
click.echo()
|
|
557
615
|
|
|
558
|
-
click.echo(click.style(" OTHER", bold=True, fg=
|
|
616
|
+
click.echo(click.style(" OTHER", bold=True, fg="yellow"))
|
|
559
617
|
click.echo(" ───────────────────────────────────────")
|
|
560
618
|
click.echo(" [?] Show this help")
|
|
561
619
|
click.echo(" [q] Return to main menu")
|
|
@@ -570,15 +628,21 @@ def _view_evidence_detail(item: Dict):
|
|
|
570
628
|
width = get_terminal_width()
|
|
571
629
|
|
|
572
630
|
# Header
|
|
573
|
-
item_type = item.get(
|
|
631
|
+
item_type = item.get("type", "item").upper()
|
|
574
632
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
575
|
-
click.echo(
|
|
633
|
+
click.echo(
|
|
634
|
+
"│"
|
|
635
|
+
+ click.style(
|
|
636
|
+
f" EVIDENCE DETAIL - {item_type} ".center(width - 2), bold=True, fg="cyan"
|
|
637
|
+
)
|
|
638
|
+
+ "│"
|
|
639
|
+
)
|
|
576
640
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
577
641
|
click.echo()
|
|
578
642
|
|
|
579
643
|
# Phase badge
|
|
580
|
-
phase_key = item.get(
|
|
581
|
-
phase_label, phase_color = PHASE_DISPLAY.get(phase_key, (
|
|
644
|
+
phase_key = item.get("phase", "reconnaissance")
|
|
645
|
+
phase_label, phase_color = PHASE_DISPLAY.get(phase_key, ("?", "white"))
|
|
582
646
|
click.echo(f" {click.style(phase_label, fg=phase_color, bold=True)}")
|
|
583
647
|
click.echo()
|
|
584
648
|
|
|
@@ -588,52 +652,60 @@ def _view_evidence_detail(item: Dict):
|
|
|
588
652
|
click.echo()
|
|
589
653
|
|
|
590
654
|
click.echo(click.style(" DESCRIPTION", bold=True))
|
|
591
|
-
desc = item.get(
|
|
655
|
+
desc = item.get("description", "No description")
|
|
592
656
|
# Word wrap description
|
|
593
|
-
for line in desc.split(
|
|
657
|
+
for line in desc.split("\n"):
|
|
594
658
|
click.echo(f" {line}")
|
|
595
659
|
click.echo()
|
|
596
660
|
|
|
597
661
|
# Tool and target
|
|
598
662
|
click.echo(click.style(" DETAILS", bold=True))
|
|
599
663
|
click.echo(f" Tool: {item.get('tool', '-').upper()}")
|
|
600
|
-
target = item.get(
|
|
664
|
+
target = item.get("target", item.get("host", "-"))
|
|
601
665
|
if isinstance(target, list):
|
|
602
|
-
target =
|
|
666
|
+
target = ", ".join(target) if target else "-"
|
|
603
667
|
click.echo(f" Target: {target}")
|
|
604
668
|
|
|
605
669
|
# Severity for findings
|
|
606
|
-
if item.get(
|
|
607
|
-
severity = item[
|
|
608
|
-
if severity in [
|
|
609
|
-
sev_str = click.style(f"🔴 {severity}", fg=
|
|
610
|
-
elif severity ==
|
|
611
|
-
sev_str = click.style(f"🟡 {severity}", fg=
|
|
670
|
+
if item.get("severity"):
|
|
671
|
+
severity = item["severity"].upper()
|
|
672
|
+
if severity in ["CRITICAL", "HIGH"]:
|
|
673
|
+
sev_str = click.style(f"🔴 {severity}", fg="red", bold=True)
|
|
674
|
+
elif severity == "MEDIUM":
|
|
675
|
+
sev_str = click.style(f"🟡 {severity}", fg="yellow")
|
|
612
676
|
else:
|
|
613
|
-
sev_str = click.style(f"🟢 {severity}", fg=
|
|
677
|
+
sev_str = click.style(f"🟢 {severity}", fg="green")
|
|
614
678
|
click.echo(f" Severity: {sev_str}")
|
|
615
679
|
|
|
616
680
|
# Status for jobs
|
|
617
|
-
if item.get(
|
|
618
|
-
status = item[
|
|
619
|
-
if status ==
|
|
620
|
-
status_str = click.style("✓ Completed", fg=
|
|
621
|
-
elif status ==
|
|
622
|
-
status_str = click.style("✗ Error", fg=
|
|
623
|
-
elif status ==
|
|
624
|
-
status_str = click.style("⟳ Running", fg=
|
|
681
|
+
if item.get("status"):
|
|
682
|
+
status = item["status"]
|
|
683
|
+
if status == "done":
|
|
684
|
+
status_str = click.style("✓ Completed", fg="green")
|
|
685
|
+
elif status == "error":
|
|
686
|
+
status_str = click.style("✗ Error", fg="red")
|
|
687
|
+
elif status == "running":
|
|
688
|
+
status_str = click.style("⟳ Running", fg="yellow")
|
|
625
689
|
else:
|
|
626
690
|
status_str = status
|
|
627
691
|
click.echo(f" Status: {status_str}")
|
|
628
692
|
|
|
629
693
|
# Date
|
|
630
|
-
if item.get(
|
|
694
|
+
if item.get("created_at"):
|
|
631
695
|
click.echo(f" Date: {item['created_at'][:19].replace('T', ' ')}")
|
|
632
696
|
|
|
633
697
|
click.echo()
|
|
634
698
|
|
|
635
699
|
# Additional metadata if present
|
|
636
|
-
metadata_keys = [
|
|
700
|
+
metadata_keys = [
|
|
701
|
+
"port",
|
|
702
|
+
"service",
|
|
703
|
+
"version",
|
|
704
|
+
"cve",
|
|
705
|
+
"cvss",
|
|
706
|
+
"username",
|
|
707
|
+
"raw_output",
|
|
708
|
+
]
|
|
637
709
|
has_metadata = any(item.get(k) for k in metadata_keys)
|
|
638
710
|
|
|
639
711
|
if has_metadata:
|
|
@@ -641,9 +713,9 @@ def _view_evidence_detail(item: Dict):
|
|
|
641
713
|
for key in metadata_keys:
|
|
642
714
|
if item.get(key):
|
|
643
715
|
value = item[key]
|
|
644
|
-
if key ==
|
|
716
|
+
if key == "raw_output":
|
|
645
717
|
click.echo(f" {key}:")
|
|
646
|
-
for line in str(value)[:500].split(
|
|
718
|
+
for line in str(value)[:500].split("\n")[:10]:
|
|
647
719
|
click.echo(f" {line}")
|
|
648
720
|
if len(str(value)) > 500:
|
|
649
721
|
click.echo(" ... (truncated)")
|
|
@@ -662,46 +734,47 @@ def _view_evidence_detail(item: Dict):
|
|
|
662
734
|
def display_evidence_item(item: Dict):
|
|
663
735
|
"""Display a single evidence item."""
|
|
664
736
|
# Icon based on type
|
|
665
|
-
icons = {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
'credential': '🔑',
|
|
669
|
-
'file': '📁'
|
|
670
|
-
}
|
|
671
|
-
icon = icons.get(item['type'], '•')
|
|
672
|
-
|
|
737
|
+
icons = {"job": "📄", "finding": "🔍", "credential": "🔑", "file": "📁"}
|
|
738
|
+
icon = icons.get(item["type"], "•")
|
|
739
|
+
|
|
673
740
|
# Format date
|
|
674
741
|
try:
|
|
675
|
-
date_str = item[
|
|
742
|
+
date_str = item["created_at"][:16].replace("T", " ")
|
|
676
743
|
except:
|
|
677
|
-
date_str =
|
|
678
|
-
|
|
744
|
+
date_str = "Unknown date"
|
|
745
|
+
|
|
679
746
|
# Add severity icon for findings
|
|
680
747
|
severity_icon = ""
|
|
681
|
-
if item.get(
|
|
748
|
+
if item.get("severity") in ["critical", "high"]:
|
|
682
749
|
severity_icon = " 🔴"
|
|
683
|
-
elif item.get(
|
|
750
|
+
elif item.get("severity") == "medium":
|
|
684
751
|
severity_icon = " 🟡"
|
|
685
|
-
|
|
752
|
+
|
|
686
753
|
# Main line
|
|
687
|
-
tool_upper = item[
|
|
688
|
-
click.echo(
|
|
754
|
+
tool_upper = item["tool"].upper()
|
|
755
|
+
click.echo(
|
|
756
|
+
f" {icon} [{click.style(tool_upper, fg='cyan')}]{severity_icon} {item['title']}"
|
|
757
|
+
)
|
|
689
758
|
click.echo(f" → {item['description']}")
|
|
690
759
|
click.echo(f" {click.style(date_str, fg='bright_black')}", nl=False)
|
|
691
|
-
|
|
760
|
+
|
|
692
761
|
# Type-specific info
|
|
693
|
-
if item[
|
|
694
|
-
if item.get(
|
|
762
|
+
if item["type"] == "job":
|
|
763
|
+
if item.get("status") == "done":
|
|
695
764
|
click.echo(f" | {click.style('Completed', fg='green')}", nl=False)
|
|
696
|
-
elif item.get(
|
|
765
|
+
elif item.get("status") == "error":
|
|
697
766
|
click.echo(f" | {click.style('Error', fg='red')}", nl=False)
|
|
698
|
-
elif item[
|
|
699
|
-
severity = item.get(
|
|
700
|
-
sev_color =
|
|
767
|
+
elif item["type"] == "finding":
|
|
768
|
+
severity = item.get("severity", "info").upper()
|
|
769
|
+
sev_color = (
|
|
770
|
+
"red"
|
|
771
|
+
if severity in ["CRITICAL", "HIGH"]
|
|
772
|
+
else "yellow" if severity == "MEDIUM" else "blue"
|
|
773
|
+
)
|
|
701
774
|
click.echo(f" | {click.style(f'Severity: {severity}', fg=sev_color)}", nl=False)
|
|
702
|
-
elif item[
|
|
775
|
+
elif item["type"] == "credential":
|
|
703
776
|
click.echo(f" | {click.style('Credential Found', fg='green')}", nl=False)
|
|
704
|
-
|
|
777
|
+
|
|
705
778
|
click.echo() # New line
|
|
706
779
|
click.echo() # Spacing
|
|
707
780
|
|
|
@@ -715,90 +788,90 @@ def view_phase_details(engagement_id: int, evidence: Dict[str, List[Dict]]):
|
|
|
715
788
|
click.echo(" [4] Post-Exploitation")
|
|
716
789
|
click.echo(" [q] Cancel")
|
|
717
790
|
click.echo()
|
|
718
|
-
|
|
791
|
+
|
|
719
792
|
choice = input(" Select option: ").strip()
|
|
720
|
-
|
|
793
|
+
|
|
721
794
|
phase_map = {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
795
|
+
"1": "reconnaissance",
|
|
796
|
+
"2": "enumeration",
|
|
797
|
+
"3": "exploitation",
|
|
798
|
+
"4": "post_exploitation",
|
|
726
799
|
}
|
|
727
|
-
|
|
728
|
-
if choice ==
|
|
800
|
+
|
|
801
|
+
if choice == "q":
|
|
729
802
|
return
|
|
730
|
-
|
|
803
|
+
|
|
731
804
|
phase_key = phase_map.get(choice)
|
|
732
805
|
if not phase_key:
|
|
733
|
-
click.echo(click.style("Invalid selection", fg=
|
|
806
|
+
click.echo(click.style("Invalid selection", fg="red"))
|
|
734
807
|
click.pause()
|
|
735
808
|
return
|
|
736
|
-
|
|
809
|
+
|
|
737
810
|
# Show all items in this phase
|
|
738
811
|
DesignSystem.clear_screen()
|
|
739
812
|
width = get_terminal_width()
|
|
740
|
-
|
|
813
|
+
|
|
741
814
|
phase_names = {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
815
|
+
"reconnaissance": "RECONNAISSANCE",
|
|
816
|
+
"enumeration": "ENUMERATION",
|
|
817
|
+
"exploitation": "EXPLOITATION",
|
|
818
|
+
"post_exploitation": "POST-EXPLOITATION",
|
|
746
819
|
}
|
|
747
|
-
|
|
820
|
+
|
|
748
821
|
click.echo("\n" + "=" * width)
|
|
749
|
-
click.echo(click.style(phase_names[phase_key], bold=True, fg=
|
|
822
|
+
click.echo(click.style(phase_names[phase_key], bold=True, fg="cyan").center(width))
|
|
750
823
|
click.echo("=" * width + "\n")
|
|
751
|
-
|
|
824
|
+
|
|
752
825
|
items = evidence.get(phase_key, [])
|
|
753
|
-
|
|
826
|
+
|
|
754
827
|
if not items:
|
|
755
|
-
click.echo(click.style("No evidence in this phase", fg=
|
|
828
|
+
click.echo(click.style("No evidence in this phase", fg="yellow"))
|
|
756
829
|
else:
|
|
757
830
|
for idx, item in enumerate(items, 1):
|
|
758
831
|
click.echo(f"\n{click.style(f'[{idx}]', fg='cyan', bold=True)} ", nl=False)
|
|
759
832
|
display_evidence_item(item)
|
|
760
|
-
|
|
833
|
+
|
|
761
834
|
click.pause("\nPress any key to return...")
|
|
762
835
|
|
|
763
836
|
|
|
764
837
|
def apply_filters() -> Dict:
|
|
765
838
|
"""Apply filters to evidence view."""
|
|
766
|
-
click.echo("\n" + click.style("Filter Evidence", bold=True, fg=
|
|
839
|
+
click.echo("\n" + click.style("Filter Evidence", bold=True, fg="cyan"))
|
|
767
840
|
click.echo("─" * 40)
|
|
768
841
|
click.echo()
|
|
769
|
-
|
|
842
|
+
|
|
770
843
|
filters = {}
|
|
771
|
-
|
|
844
|
+
|
|
772
845
|
# Tool filter
|
|
773
846
|
tool = input("Filter by tool (or 'all'): ").strip()
|
|
774
|
-
if tool and tool.lower() !=
|
|
775
|
-
filters[
|
|
776
|
-
|
|
847
|
+
if tool and tool.lower() != "all":
|
|
848
|
+
filters["tool"] = tool
|
|
849
|
+
|
|
777
850
|
# Host filter
|
|
778
851
|
host = input("Filter by host/target (or 'all'): ").strip()
|
|
779
|
-
if host and host.lower() !=
|
|
780
|
-
filters[
|
|
781
|
-
|
|
852
|
+
if host and host.lower() != "all":
|
|
853
|
+
filters["host"] = host
|
|
854
|
+
|
|
782
855
|
# Date filter
|
|
783
856
|
click.echo("\nFilter by date:")
|
|
784
857
|
click.echo(" [1] Last 24 hours")
|
|
785
858
|
click.echo(" [2] Last 7 days")
|
|
786
859
|
click.echo(" [3] Last 30 days")
|
|
787
860
|
click.echo(" [4] All time")
|
|
788
|
-
|
|
861
|
+
|
|
789
862
|
date_choice = input(" Select option: ").strip()
|
|
790
|
-
|
|
791
|
-
days_map = {
|
|
863
|
+
|
|
864
|
+
days_map = {"1": 1, "2": 7, "3": 30, "4": None}
|
|
792
865
|
days = days_map.get(date_choice)
|
|
793
|
-
|
|
866
|
+
|
|
794
867
|
if days:
|
|
795
|
-
filters[
|
|
796
|
-
|
|
868
|
+
filters["days"] = days
|
|
869
|
+
|
|
797
870
|
if filters:
|
|
798
|
-
click.echo("\n" + click.style("✓ Filters applied", fg=
|
|
871
|
+
click.echo("\n" + click.style("✓ Filters applied", fg="green"))
|
|
799
872
|
else:
|
|
800
|
-
click.echo("\n" + click.style("No filters applied", fg=
|
|
801
|
-
|
|
873
|
+
click.echo("\n" + click.style("No filters applied", fg="yellow"))
|
|
874
|
+
|
|
802
875
|
click.pause()
|
|
803
876
|
return filters
|
|
804
877
|
|
|
@@ -807,13 +880,13 @@ def search_evidence(engagement_id: int, evidence: Dict[str, List[Dict]]):
|
|
|
807
880
|
"""Search evidence with full-text search."""
|
|
808
881
|
click.echo()
|
|
809
882
|
search_term = click.prompt("Enter search term", default="").strip()
|
|
810
|
-
|
|
883
|
+
|
|
811
884
|
if not search_term:
|
|
812
885
|
return
|
|
813
|
-
|
|
886
|
+
|
|
814
887
|
search_lower = search_term.lower()
|
|
815
888
|
results = []
|
|
816
|
-
|
|
889
|
+
|
|
817
890
|
# Search across all evidence
|
|
818
891
|
for phase, items in evidence.items():
|
|
819
892
|
for item in items:
|
|
@@ -821,50 +894,56 @@ def search_evidence(engagement_id: int, evidence: Dict[str, List[Dict]]):
|
|
|
821
894
|
searchable = f"{item.get('title', '')} {item.get('description', '')} {item.get('tool', '')}".lower()
|
|
822
895
|
if search_lower in searchable:
|
|
823
896
|
results.append((phase, item))
|
|
824
|
-
|
|
897
|
+
|
|
825
898
|
# Display results
|
|
826
899
|
DesignSystem.clear_screen()
|
|
827
900
|
click.echo()
|
|
828
|
-
click.echo(
|
|
901
|
+
click.echo(
|
|
902
|
+
click.style(f"🔍 SEARCH RESULTS FOR: {search_term}", bold=True, fg="cyan")
|
|
903
|
+
)
|
|
829
904
|
click.echo("=" * 80)
|
|
830
905
|
click.echo()
|
|
831
|
-
|
|
906
|
+
|
|
832
907
|
if not results:
|
|
833
|
-
click.echo(click.style(" No matches found", fg=
|
|
908
|
+
click.echo(click.style(" No matches found", fg="yellow"))
|
|
834
909
|
else:
|
|
835
910
|
click.echo(f"Found {len(results)} match(es):\n")
|
|
836
911
|
for phase, item in results[:20]: # Show first 20
|
|
837
912
|
severity_icon = ""
|
|
838
|
-
if item.get(
|
|
913
|
+
if item.get("severity") in ["critical", "high"]:
|
|
839
914
|
severity_icon = " 🔴"
|
|
840
|
-
elif item.get(
|
|
915
|
+
elif item.get("severity") == "medium":
|
|
841
916
|
severity_icon = " 🟡"
|
|
842
|
-
|
|
843
|
-
icon = {
|
|
917
|
+
|
|
918
|
+
icon = {"job": "📄", "finding": "🔍", "credential": "🔑", "file": "📁"}.get(
|
|
919
|
+
item["type"], "•"
|
|
920
|
+
)
|
|
844
921
|
click.echo(f" {icon} [{phase.upper()}]{severity_icon} {item['title']}")
|
|
845
922
|
click.echo(f" → {item['description'][:80]}")
|
|
846
923
|
click.echo()
|
|
847
|
-
|
|
924
|
+
|
|
848
925
|
if len(results) > 20:
|
|
849
926
|
click.echo(f" ... and {len(results) - 20} more matches")
|
|
850
|
-
|
|
927
|
+
|
|
851
928
|
click.echo()
|
|
852
929
|
click.pause()
|
|
853
930
|
|
|
854
931
|
|
|
855
|
-
def export_evidence_bundle(
|
|
932
|
+
def export_evidence_bundle(
|
|
933
|
+
engagement_id: int, engagement: Dict, evidence: Dict[str, List[Dict]]
|
|
934
|
+
):
|
|
856
935
|
"""Export all evidence as ZIP bundle."""
|
|
857
|
-
click.echo("\n" + click.style("Exporting evidence bundle...", fg=
|
|
936
|
+
click.echo("\n" + click.style("Exporting evidence bundle...", fg="yellow"))
|
|
858
937
|
|
|
859
938
|
try:
|
|
860
939
|
from souleyez.export.evidence_bundle import create_evidence_bundle
|
|
861
940
|
|
|
862
941
|
zip_path = create_evidence_bundle(engagement_id, engagement, evidence)
|
|
863
|
-
click.echo(click.style(f"\n✓ Evidence bundle created:", fg=
|
|
942
|
+
click.echo(click.style(f"\n✓ Evidence bundle created:", fg="green"))
|
|
864
943
|
click.echo(f" {zip_path}")
|
|
865
944
|
|
|
866
945
|
except Exception as e:
|
|
867
|
-
click.echo(click.style(f"\n✗ Export failed: {e}", fg=
|
|
946
|
+
click.echo(click.style(f"\n✗ Export failed: {e}", fg="red"))
|
|
868
947
|
|
|
869
948
|
click.pause()
|
|
870
949
|
|
|
@@ -885,7 +964,11 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
|
|
|
885
964
|
width = get_terminal_width()
|
|
886
965
|
|
|
887
966
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
888
|
-
click.echo(
|
|
967
|
+
click.echo(
|
|
968
|
+
"│"
|
|
969
|
+
+ click.style(" SCREENSHOTS ".center(width - 2), bold=True, fg="cyan")
|
|
970
|
+
+ "│"
|
|
971
|
+
)
|
|
889
972
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
890
973
|
click.echo()
|
|
891
974
|
|
|
@@ -893,15 +976,19 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
|
|
|
893
976
|
screenshots = sm.list_screenshots(engagement_id)
|
|
894
977
|
|
|
895
978
|
if not screenshots:
|
|
896
|
-
click.echo(click.style(" No screenshots found", fg=
|
|
979
|
+
click.echo(click.style(" No screenshots found", fg="yellow"))
|
|
897
980
|
click.echo()
|
|
898
|
-
click.echo(
|
|
981
|
+
click.echo(
|
|
982
|
+
" 💡 Add screenshots with: souleyez screenshots add /path/to/image.png"
|
|
983
|
+
)
|
|
899
984
|
click.echo()
|
|
900
985
|
click.echo(" [q] ← Back")
|
|
901
986
|
click.echo()
|
|
902
987
|
|
|
903
|
-
choice = click.prompt(
|
|
904
|
-
|
|
988
|
+
choice = click.prompt(
|
|
989
|
+
"Select option", type=str, default="q", show_default=False
|
|
990
|
+
)
|
|
991
|
+
if choice == "q":
|
|
905
992
|
return
|
|
906
993
|
continue
|
|
907
994
|
|
|
@@ -910,7 +997,12 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
|
|
|
910
997
|
|
|
911
998
|
# Display table
|
|
912
999
|
console = Console()
|
|
913
|
-
table = Table(
|
|
1000
|
+
table = Table(
|
|
1001
|
+
show_header=True,
|
|
1002
|
+
header_style="bold",
|
|
1003
|
+
box=DesignSystem.TABLE_BOX,
|
|
1004
|
+
expand=True,
|
|
1005
|
+
)
|
|
914
1006
|
table.add_column("#", width=4)
|
|
915
1007
|
table.add_column("ID", width=6)
|
|
916
1008
|
table.add_column("Title", width=30)
|
|
@@ -920,7 +1012,7 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
|
|
|
920
1012
|
|
|
921
1013
|
for idx, s in enumerate(screenshots, 1):
|
|
922
1014
|
# Format size
|
|
923
|
-
size = s[
|
|
1015
|
+
size = s["file_size"]
|
|
924
1016
|
if size < 1024:
|
|
925
1017
|
size_str = f"{size} B"
|
|
926
1018
|
elif size < 1024 * 1024:
|
|
@@ -930,25 +1022,25 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
|
|
|
930
1022
|
|
|
931
1023
|
# Format links
|
|
932
1024
|
links = []
|
|
933
|
-
if s[
|
|
1025
|
+
if s["host_id"]:
|
|
934
1026
|
links.append(f"Host:{s['host_id']}")
|
|
935
|
-
if s[
|
|
1027
|
+
if s["finding_id"]:
|
|
936
1028
|
links.append(f"Finding:{s['finding_id']}")
|
|
937
|
-
if s[
|
|
1029
|
+
if s["job_id"]:
|
|
938
1030
|
links.append(f"Job:{s['job_id']}")
|
|
939
1031
|
links_str = ", ".join(links) if links else "-"
|
|
940
1032
|
|
|
941
|
-
title = s[
|
|
1033
|
+
title = s["title"] or s["filename"]
|
|
942
1034
|
if len(title) > 30:
|
|
943
1035
|
title = title[:27] + "..."
|
|
944
1036
|
|
|
945
1037
|
table.add_row(
|
|
946
1038
|
str(idx),
|
|
947
|
-
str(s[
|
|
1039
|
+
str(s["id"]),
|
|
948
1040
|
title,
|
|
949
1041
|
size_str,
|
|
950
1042
|
links_str,
|
|
951
|
-
s[
|
|
1043
|
+
s["created_at"][:10] if s["created_at"] else "N/A",
|
|
952
1044
|
)
|
|
953
1045
|
|
|
954
1046
|
console.print(table)
|
|
@@ -959,54 +1051,70 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
|
|
|
959
1051
|
click.echo(" [q] ← Back")
|
|
960
1052
|
click.echo()
|
|
961
1053
|
|
|
962
|
-
choice =
|
|
1054
|
+
choice = (
|
|
1055
|
+
click.prompt("Select option", type=str, default="q", show_default=False)
|
|
1056
|
+
.strip()
|
|
1057
|
+
.lower()
|
|
1058
|
+
)
|
|
963
1059
|
|
|
964
|
-
if choice ==
|
|
1060
|
+
if choice == "q":
|
|
965
1061
|
return
|
|
966
|
-
elif choice ==
|
|
1062
|
+
elif choice == "d":
|
|
967
1063
|
# Delete screenshot
|
|
968
1064
|
try:
|
|
969
|
-
screenshot_id = click.prompt(
|
|
1065
|
+
screenshot_id = click.prompt(
|
|
1066
|
+
" Enter screenshot ID to delete", type=int
|
|
1067
|
+
)
|
|
970
1068
|
screenshot = sm.get_screenshot(screenshot_id)
|
|
971
1069
|
if screenshot:
|
|
972
|
-
if click.confirm(
|
|
1070
|
+
if click.confirm(
|
|
1071
|
+
f" Delete screenshot '{screenshot['title']}'?", default=False
|
|
1072
|
+
):
|
|
973
1073
|
sm.delete_screenshot(screenshot_id)
|
|
974
|
-
click.echo(click.style(" ✓ Screenshot deleted", fg=
|
|
1074
|
+
click.echo(click.style(" ✓ Screenshot deleted", fg="green"))
|
|
975
1075
|
else:
|
|
976
|
-
click.echo(
|
|
1076
|
+
click.echo(
|
|
1077
|
+
click.style(f" Screenshot {screenshot_id} not found", fg="red")
|
|
1078
|
+
)
|
|
977
1079
|
except (ValueError, KeyboardInterrupt):
|
|
978
1080
|
pass
|
|
979
1081
|
click.pause()
|
|
980
|
-
elif choice ==
|
|
1082
|
+
elif choice == "v":
|
|
981
1083
|
# View screenshot by number
|
|
982
1084
|
try:
|
|
983
1085
|
idx = click.prompt(" Enter screenshot # to view", type=int) - 1
|
|
984
1086
|
if 0 <= idx < len(screenshots):
|
|
985
1087
|
s = screenshots[idx]
|
|
986
|
-
filepath = Path(s[
|
|
1088
|
+
filepath = Path(s["filepath"])
|
|
987
1089
|
if filepath.exists():
|
|
988
1090
|
click.echo()
|
|
989
|
-
click.echo(click.style(f" Opening: {s['title']}", fg=
|
|
1091
|
+
click.echo(click.style(f" Opening: {s['title']}", fg="cyan"))
|
|
990
1092
|
click.echo(f" Location: {filepath}")
|
|
991
1093
|
# Try to open with default viewer
|
|
992
1094
|
try:
|
|
993
|
-
if sys.platform ==
|
|
994
|
-
subprocess.run([
|
|
995
|
-
elif sys.platform.startswith(
|
|
996
|
-
subprocess.run([
|
|
1095
|
+
if sys.platform == "darwin": # macOS
|
|
1096
|
+
subprocess.run(["open", str(filepath)])
|
|
1097
|
+
elif sys.platform.startswith("linux"): # Linux
|
|
1098
|
+
subprocess.run(["xdg-open", str(filepath)])
|
|
997
1099
|
else: # Windows
|
|
998
|
-
subprocess.run([
|
|
999
|
-
click.echo(click.style(" ✓ Screenshot opened", fg=
|
|
1100
|
+
subprocess.run(["start", str(filepath)], shell=True)
|
|
1101
|
+
click.echo(click.style(" ✓ Screenshot opened", fg="green"))
|
|
1000
1102
|
except Exception as e:
|
|
1001
|
-
click.echo(
|
|
1103
|
+
click.echo(
|
|
1104
|
+
click.style(
|
|
1105
|
+
f" Could not open screenshot: {e}", fg="yellow"
|
|
1106
|
+
)
|
|
1107
|
+
)
|
|
1002
1108
|
click.echo(f" Manual path: {filepath}")
|
|
1003
1109
|
else:
|
|
1004
|
-
click.echo(
|
|
1110
|
+
click.echo(
|
|
1111
|
+
click.style(" Screenshot file not found!", fg="red")
|
|
1112
|
+
)
|
|
1005
1113
|
else:
|
|
1006
|
-
click.echo(click.style(" Invalid screenshot number", fg=
|
|
1114
|
+
click.echo(click.style(" Invalid screenshot number", fg="red"))
|
|
1007
1115
|
except (ValueError, click.Abort):
|
|
1008
1116
|
pass
|
|
1009
1117
|
click.pause()
|
|
1010
1118
|
else:
|
|
1011
|
-
click.echo(click.style(" Invalid option", fg=
|
|
1119
|
+
click.echo(click.style(" Invalid option", fg="red"))
|
|
1012
1120
|
click.pause()
|