souleyez 2.43.29__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of souleyez might be problematic. Click here for more details.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9564 -2881
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +564 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +409 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +417 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +913 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +219 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +237 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23034 -10679
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
- souleyez-3.0.0.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
souleyez/storage/credentials.py
CHANGED
|
@@ -23,7 +23,7 @@ class CredentialsManager:
|
|
|
23
23
|
def validate_master_password(self, password: str):
|
|
24
24
|
"""
|
|
25
25
|
Validate master password meets security requirements.
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
Requirements:
|
|
28
28
|
- At least 12 characters
|
|
29
29
|
- Contains uppercase letter
|
|
@@ -33,11 +33,11 @@ class CredentialsManager:
|
|
|
33
33
|
"""
|
|
34
34
|
if len(password) < 12:
|
|
35
35
|
raise ValueError("Password must be at least 12 characters")
|
|
36
|
-
if not re.search(r
|
|
36
|
+
if not re.search(r"[A-Z]", password):
|
|
37
37
|
raise ValueError("Password must contain uppercase letter")
|
|
38
|
-
if not re.search(r
|
|
38
|
+
if not re.search(r"[a-z]", password):
|
|
39
39
|
raise ValueError("Password must contain lowercase letter")
|
|
40
|
-
if not re.search(r
|
|
40
|
+
if not re.search(r"[0-9]", password):
|
|
41
41
|
raise ValueError("Password must contain number")
|
|
42
42
|
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
|
43
43
|
raise ValueError("Password must contain special character")
|
|
@@ -51,7 +51,9 @@ class CredentialsManager:
|
|
|
51
51
|
# Store with a marker prefix to indicate it needs encryption
|
|
52
52
|
# This allows background workers to store discovered credentials
|
|
53
53
|
# They will be encrypted when the user next unlocks the database
|
|
54
|
-
logger.warning(
|
|
54
|
+
logger.warning(
|
|
55
|
+
f"Storing credential field with UNENCRYPTED: marker - crypto locked"
|
|
56
|
+
)
|
|
55
57
|
return f"UNENCRYPTED:{value}"
|
|
56
58
|
return self.crypto.encrypt(value)
|
|
57
59
|
return value
|
|
@@ -66,14 +68,17 @@ class CredentialsManager:
|
|
|
66
68
|
# Strip marker and return plaintext
|
|
67
69
|
return value[12:] # len("UNENCRYPTED:") = 12
|
|
68
70
|
if not self.crypto.is_unlocked():
|
|
69
|
-
raise RuntimeError(
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
"Credentials are encrypted but crypto manager is locked. Call unlock() first."
|
|
73
|
+
)
|
|
70
74
|
return self.crypto.decrypt(value)
|
|
71
75
|
return value
|
|
72
76
|
|
|
73
77
|
def _ensure_table(self):
|
|
74
78
|
"""Ensure credentials table exists."""
|
|
75
79
|
conn = self.db.get_connection()
|
|
76
|
-
conn.execute(
|
|
80
|
+
conn.execute(
|
|
81
|
+
"""
|
|
77
82
|
CREATE TABLE IF NOT EXISTS credentials (
|
|
78
83
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
79
84
|
engagement_id INTEGER NOT NULL,
|
|
@@ -90,21 +95,28 @@ class CredentialsManager:
|
|
|
90
95
|
FOREIGN KEY (engagement_id) REFERENCES engagements(id),
|
|
91
96
|
FOREIGN KEY (host_id) REFERENCES hosts(id)
|
|
92
97
|
)
|
|
93
|
-
"""
|
|
98
|
+
"""
|
|
99
|
+
)
|
|
94
100
|
|
|
95
101
|
# Create index for faster lookups
|
|
96
|
-
conn.execute(
|
|
102
|
+
conn.execute(
|
|
103
|
+
"""
|
|
97
104
|
CREATE INDEX IF NOT EXISTS idx_credentials_engagement
|
|
98
105
|
ON credentials(engagement_id)
|
|
99
|
-
"""
|
|
100
|
-
|
|
106
|
+
"""
|
|
107
|
+
)
|
|
108
|
+
conn.execute(
|
|
109
|
+
"""
|
|
101
110
|
CREATE INDEX IF NOT EXISTS idx_credentials_host
|
|
102
111
|
ON credentials(host_id)
|
|
103
|
-
"""
|
|
104
|
-
|
|
112
|
+
"""
|
|
113
|
+
)
|
|
114
|
+
conn.execute(
|
|
115
|
+
"""
|
|
105
116
|
CREATE INDEX IF NOT EXISTS idx_credentials_status
|
|
106
117
|
ON credentials(status)
|
|
107
|
-
"""
|
|
118
|
+
"""
|
|
119
|
+
)
|
|
108
120
|
|
|
109
121
|
conn.commit()
|
|
110
122
|
conn.close()
|
|
@@ -117,10 +129,12 @@ class CredentialsManager:
|
|
|
117
129
|
password: str = None,
|
|
118
130
|
service: str = None,
|
|
119
131
|
port: int = None,
|
|
120
|
-
protocol: str =
|
|
121
|
-
credential_type: str =
|
|
122
|
-
status: str =
|
|
123
|
-
tool: str = None
|
|
132
|
+
protocol: str = "tcp",
|
|
133
|
+
credential_type: str = "user",
|
|
134
|
+
status: str = "untested",
|
|
135
|
+
tool: str = None,
|
|
136
|
+
notes: str = None,
|
|
137
|
+
source: str = None, # Legacy alias for tool
|
|
124
138
|
) -> int:
|
|
125
139
|
"""
|
|
126
140
|
Add a credential to the database.
|
|
@@ -136,60 +150,79 @@ class CredentialsManager:
|
|
|
136
150
|
credential_type: Type of credential (user, password, hash, key)
|
|
137
151
|
status: Status (untested, valid, invalid)
|
|
138
152
|
tool: Tool that discovered this credential
|
|
153
|
+
notes: Additional notes about the credential
|
|
154
|
+
source: Legacy alias for tool parameter
|
|
139
155
|
|
|
140
156
|
Returns:
|
|
141
157
|
Credential ID
|
|
142
158
|
"""
|
|
159
|
+
# Handle legacy 'source' parameter
|
|
160
|
+
if source and not tool:
|
|
161
|
+
tool = source
|
|
143
162
|
# Check for duplicates (must encrypt before comparing)
|
|
144
163
|
encrypted_username = self._encrypt_field(username) if username else None
|
|
145
164
|
encrypted_password = self._encrypt_field(password) if password else None
|
|
146
|
-
|
|
165
|
+
|
|
147
166
|
existing = self.get_credential(
|
|
148
|
-
engagement_id,
|
|
167
|
+
engagement_id,
|
|
168
|
+
host_id,
|
|
169
|
+
encrypted_username,
|
|
170
|
+
encrypted_password,
|
|
171
|
+
service,
|
|
172
|
+
port,
|
|
149
173
|
)
|
|
150
174
|
if existing:
|
|
151
175
|
# Update status if this one is more definitive
|
|
152
|
-
if status ==
|
|
153
|
-
self._update_status(existing[
|
|
154
|
-
return existing[
|
|
176
|
+
if status == "valid" and existing["status"] != "valid":
|
|
177
|
+
self._update_status(existing["id"], status, tool)
|
|
178
|
+
return existing["id"]
|
|
155
179
|
|
|
156
180
|
# Special case: If adding a valid username:password pair, check if we have
|
|
157
181
|
# a username-only entry that should be upgraded instead of creating duplicate
|
|
158
|
-
if username and password and status ==
|
|
182
|
+
if username and password and status == "valid":
|
|
159
183
|
username_only = self.get_credential(
|
|
160
184
|
engagement_id, host_id, encrypted_username, None, service, port
|
|
161
185
|
)
|
|
162
186
|
if username_only:
|
|
163
187
|
# Upgrade the existing entry with the password
|
|
164
|
-
self._update_credential(
|
|
165
|
-
|
|
188
|
+
self._update_credential(
|
|
189
|
+
username_only["id"],
|
|
190
|
+
password=encrypted_password,
|
|
191
|
+
status=status,
|
|
192
|
+
tool=tool,
|
|
193
|
+
)
|
|
194
|
+
return username_only["id"]
|
|
166
195
|
|
|
167
196
|
data = {
|
|
168
|
-
|
|
169
|
-
'host_id': host_id,
|
|
170
|
-
'service': service,
|
|
171
|
-
'port': port,
|
|
172
|
-
'protocol': protocol,
|
|
173
|
-
'username': encrypted_username,
|
|
174
|
-
'password': encrypted_password,
|
|
175
|
-
'credential_type': credential_type,
|
|
176
|
-
'status': status,
|
|
177
|
-
'tool': tool
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
cred_id = self.db.insert('credentials', data)
|
|
181
|
-
|
|
182
|
-
# Audit log (never log actual password)
|
|
183
|
-
logger.info("Credential added", extra={
|
|
184
|
-
"engagement": self.engagement or engagement_id,
|
|
185
|
-
"cred_id": cred_id,
|
|
186
|
-
"username": username if username else "<none>",
|
|
197
|
+
"engagement_id": engagement_id,
|
|
187
198
|
"host_id": host_id,
|
|
188
199
|
"service": service,
|
|
189
200
|
"port": port,
|
|
190
|
-
"
|
|
191
|
-
|
|
192
|
-
|
|
201
|
+
"protocol": protocol,
|
|
202
|
+
"username": encrypted_username,
|
|
203
|
+
"password": encrypted_password,
|
|
204
|
+
"credential_type": credential_type,
|
|
205
|
+
"status": status,
|
|
206
|
+
"tool": tool,
|
|
207
|
+
"notes": notes,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
cred_id = self.db.insert("credentials", data)
|
|
211
|
+
|
|
212
|
+
# Audit log (never log actual password)
|
|
213
|
+
logger.info(
|
|
214
|
+
"Credential added",
|
|
215
|
+
extra={
|
|
216
|
+
"engagement": self.engagement or engagement_id,
|
|
217
|
+
"cred_id": cred_id,
|
|
218
|
+
"username": username if username else "<none>",
|
|
219
|
+
"host_id": host_id,
|
|
220
|
+
"service": service,
|
|
221
|
+
"port": port,
|
|
222
|
+
"tool": tool,
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
|
|
193
226
|
return cred_id
|
|
194
227
|
|
|
195
228
|
def get_credential(
|
|
@@ -199,7 +232,7 @@ class CredentialsManager:
|
|
|
199
232
|
username: str = None,
|
|
200
233
|
password: str = None,
|
|
201
234
|
service: str = None,
|
|
202
|
-
port: int = None
|
|
235
|
+
port: int = None,
|
|
203
236
|
) -> Optional[Dict[str, Any]]:
|
|
204
237
|
"""Check if credential already exists."""
|
|
205
238
|
query = """
|
|
@@ -231,16 +264,19 @@ class CredentialsManager:
|
|
|
231
264
|
query += " LIMIT 1"
|
|
232
265
|
|
|
233
266
|
result = self.db.execute_one(query, tuple(params))
|
|
234
|
-
|
|
267
|
+
|
|
235
268
|
# Audit log credential access (never log password)
|
|
236
269
|
if result:
|
|
237
|
-
logger.info(
|
|
238
|
-
"
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
270
|
+
logger.info(
|
|
271
|
+
"Credential accessed",
|
|
272
|
+
extra={
|
|
273
|
+
"engagement": self.engagement or engagement_id,
|
|
274
|
+
"cred_id": result.get("id"),
|
|
275
|
+
"username": username if username else "<none>",
|
|
276
|
+
"host_id": host_id,
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
|
|
244
280
|
return result
|
|
245
281
|
|
|
246
282
|
def _update_status(self, credential_id: int, status: str, tool: str = None):
|
|
@@ -249,24 +285,35 @@ class CredentialsManager:
|
|
|
249
285
|
if tool:
|
|
250
286
|
conn.execute(
|
|
251
287
|
"UPDATE credentials SET status = ?, tool = ? WHERE id = ?",
|
|
252
|
-
(status, tool, credential_id)
|
|
288
|
+
(status, tool, credential_id),
|
|
253
289
|
)
|
|
254
290
|
else:
|
|
255
291
|
conn.execute(
|
|
256
292
|
"UPDATE credentials SET status = ? WHERE id = ?",
|
|
257
|
-
(status, credential_id)
|
|
293
|
+
(status, credential_id),
|
|
258
294
|
)
|
|
259
295
|
conn.commit()
|
|
260
296
|
conn.close()
|
|
261
|
-
|
|
262
|
-
logger.info(
|
|
263
|
-
"
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
297
|
+
|
|
298
|
+
logger.info(
|
|
299
|
+
"Credential status updated",
|
|
300
|
+
extra={
|
|
301
|
+
"engagement": self.engagement,
|
|
302
|
+
"cred_id": credential_id,
|
|
303
|
+
"new_status": status,
|
|
304
|
+
"tool": tool,
|
|
305
|
+
},
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def _update_credential(
|
|
309
|
+
self,
|
|
310
|
+
credential_id: int,
|
|
311
|
+
password: str = None,
|
|
312
|
+
status: str = None,
|
|
313
|
+
tool: str = None,
|
|
314
|
+
notes: str = None,
|
|
315
|
+
last_tested: str = None,
|
|
316
|
+
):
|
|
270
317
|
"""Update credential with password and/or status."""
|
|
271
318
|
conn = self.db.get_connection()
|
|
272
319
|
|
|
@@ -301,7 +348,9 @@ class CredentialsManager:
|
|
|
301
348
|
conn.commit()
|
|
302
349
|
conn.close()
|
|
303
350
|
|
|
304
|
-
def update_credential_status(
|
|
351
|
+
def update_credential_status(
|
|
352
|
+
self, credential_id: int, status: str = None, notes: str = None
|
|
353
|
+
):
|
|
305
354
|
"""
|
|
306
355
|
Update the status and/or notes of a credential.
|
|
307
356
|
|
|
@@ -309,25 +358,30 @@ class CredentialsManager:
|
|
|
309
358
|
credential_id: Credential ID
|
|
310
359
|
status: New status (valid, invalid, untested, discovered, etc.)
|
|
311
360
|
notes: Optional notes about the credential
|
|
312
|
-
|
|
361
|
+
|
|
313
362
|
Returns:
|
|
314
363
|
bool: True if successful
|
|
315
364
|
"""
|
|
316
365
|
from datetime import datetime
|
|
317
|
-
|
|
366
|
+
|
|
318
367
|
# Update last_tested if status is changing
|
|
319
368
|
last_tested = datetime.now().isoformat() if status else None
|
|
320
|
-
|
|
321
|
-
self._update_credential(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
"
|
|
327
|
-
|
|
328
|
-
|
|
369
|
+
|
|
370
|
+
self._update_credential(
|
|
371
|
+
credential_id, status=status, notes=notes, last_tested=last_tested
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
logger.info(
|
|
375
|
+
"Credential status updated",
|
|
376
|
+
extra={
|
|
377
|
+
"engagement": self.engagement,
|
|
378
|
+
"cred_id": credential_id,
|
|
379
|
+
"new_status": status,
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
|
|
329
383
|
return True
|
|
330
|
-
|
|
384
|
+
|
|
331
385
|
def delete_credential(self, credential_id: int):
|
|
332
386
|
"""
|
|
333
387
|
Delete a credential.
|
|
@@ -341,6 +395,7 @@ class CredentialsManager:
|
|
|
341
395
|
# Check permission
|
|
342
396
|
from souleyez.auth import get_current_user
|
|
343
397
|
from souleyez.auth.permissions import Permission, PermissionChecker
|
|
398
|
+
|
|
344
399
|
user = get_current_user()
|
|
345
400
|
if user:
|
|
346
401
|
checker = PermissionChecker(user.role, user.tier)
|
|
@@ -348,69 +403,72 @@ class CredentialsManager:
|
|
|
348
403
|
raise PermissionError("Permission denied: CREDENTIAL_DELETE required")
|
|
349
404
|
|
|
350
405
|
# Get username for audit log before deleting
|
|
351
|
-
cred = self.db.execute_one(
|
|
352
|
-
|
|
353
|
-
|
|
406
|
+
cred = self.db.execute_one(
|
|
407
|
+
"SELECT username FROM credentials WHERE id = ?", (credential_id,)
|
|
408
|
+
)
|
|
409
|
+
username = cred.get("username") if cred else "<unknown>"
|
|
410
|
+
|
|
354
411
|
conn = self.db.get_connection()
|
|
355
412
|
conn.execute("DELETE FROM credentials WHERE id = ?", (credential_id,))
|
|
356
413
|
conn.commit()
|
|
357
414
|
conn.close()
|
|
358
|
-
|
|
359
|
-
logger.info(
|
|
360
|
-
"
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
415
|
+
|
|
416
|
+
logger.info(
|
|
417
|
+
"Credential deleted",
|
|
418
|
+
extra={
|
|
419
|
+
"engagement": self.engagement,
|
|
420
|
+
"cred_id": credential_id,
|
|
421
|
+
"username": username,
|
|
422
|
+
},
|
|
423
|
+
)
|
|
424
|
+
|
|
365
425
|
def enable_encryption(self, password: str) -> bool:
|
|
366
426
|
"""
|
|
367
427
|
Enable encryption for credentials.
|
|
368
|
-
|
|
428
|
+
|
|
369
429
|
Args:
|
|
370
430
|
password: Master password
|
|
371
|
-
|
|
431
|
+
|
|
372
432
|
Returns:
|
|
373
433
|
True if encryption enabled successfully
|
|
374
434
|
"""
|
|
375
435
|
# Validate password complexity
|
|
376
436
|
self.validate_master_password(password)
|
|
377
|
-
|
|
437
|
+
|
|
378
438
|
result = self.crypto.enable_encryption(password)
|
|
379
|
-
|
|
439
|
+
|
|
380
440
|
if result:
|
|
381
|
-
logger.info("Encryption enabled", extra={
|
|
382
|
-
"engagement": self.engagement
|
|
383
|
-
})
|
|
441
|
+
logger.info("Encryption enabled", extra={"engagement": self.engagement})
|
|
384
442
|
else:
|
|
385
|
-
logger.warning(
|
|
386
|
-
"engagement": self.engagement
|
|
387
|
-
|
|
388
|
-
|
|
443
|
+
logger.warning(
|
|
444
|
+
"Encryption enable failed", extra={"engagement": self.engagement}
|
|
445
|
+
)
|
|
446
|
+
|
|
389
447
|
return result
|
|
390
|
-
|
|
448
|
+
|
|
391
449
|
def unlock(self, password: str) -> bool:
|
|
392
450
|
"""
|
|
393
451
|
Unlock encrypted credentials.
|
|
394
|
-
|
|
452
|
+
|
|
395
453
|
Args:
|
|
396
454
|
password: Master password
|
|
397
|
-
|
|
455
|
+
|
|
398
456
|
Returns:
|
|
399
457
|
True if unlock successful
|
|
400
458
|
"""
|
|
401
459
|
result = self.crypto.unlock(password)
|
|
402
|
-
|
|
460
|
+
|
|
403
461
|
if result:
|
|
404
|
-
logger.info(
|
|
405
|
-
"
|
|
406
|
-
"success": True
|
|
407
|
-
|
|
462
|
+
logger.info(
|
|
463
|
+
"Credentials unlocked",
|
|
464
|
+
extra={"engagement": self.engagement, "success": True},
|
|
465
|
+
)
|
|
408
466
|
else:
|
|
409
|
-
logger.warning(
|
|
410
|
-
"
|
|
411
|
-
"success": False
|
|
412
|
-
|
|
413
|
-
|
|
467
|
+
logger.warning(
|
|
468
|
+
"Credentials unlock failed",
|
|
469
|
+
extra={"engagement": self.engagement, "success": False},
|
|
470
|
+
)
|
|
471
|
+
|
|
414
472
|
return result
|
|
415
473
|
|
|
416
474
|
def list_credentials(
|
|
@@ -419,7 +477,7 @@ class CredentialsManager:
|
|
|
419
477
|
host_id: int = None,
|
|
420
478
|
service: str = None,
|
|
421
479
|
status: str = None,
|
|
422
|
-
decrypt: bool = True
|
|
480
|
+
decrypt: bool = True,
|
|
423
481
|
) -> List[Dict[str, Any]]:
|
|
424
482
|
"""
|
|
425
483
|
List credentials for an engagement.
|
|
@@ -464,21 +522,23 @@ class CredentialsManager:
|
|
|
464
522
|
# Decrypt username and password fields if requested
|
|
465
523
|
if decrypt:
|
|
466
524
|
for row in results:
|
|
467
|
-
if row.get(
|
|
468
|
-
row[
|
|
469
|
-
if row.get(
|
|
470
|
-
row[
|
|
525
|
+
if row.get("username"):
|
|
526
|
+
row["username"] = self._decrypt_field(row["username"])
|
|
527
|
+
if row.get("password"):
|
|
528
|
+
row["password"] = self._decrypt_field(row["password"])
|
|
471
529
|
else:
|
|
472
530
|
# Mask credentials when not decrypting (for display purposes)
|
|
473
531
|
for row in results:
|
|
474
|
-
if row.get(
|
|
475
|
-
row[
|
|
476
|
-
if row.get(
|
|
477
|
-
row[
|
|
532
|
+
if row.get("username"):
|
|
533
|
+
row["username"] = "••••••••"
|
|
534
|
+
if row.get("password"):
|
|
535
|
+
row["password"] = "••••••••"
|
|
478
536
|
|
|
479
537
|
return results
|
|
480
538
|
|
|
481
|
-
def list_credentials_for_engagement(
|
|
539
|
+
def list_credentials_for_engagement(
|
|
540
|
+
self, engagement_id: int, decrypt: bool = True
|
|
541
|
+
) -> List[Dict[str, Any]]:
|
|
482
542
|
"""
|
|
483
543
|
List all credentials for an engagement (alias method for compatibility).
|
|
484
544
|
|
|
@@ -498,76 +558,76 @@ class CredentialsManager:
|
|
|
498
558
|
# Total credentials
|
|
499
559
|
total = conn.execute(
|
|
500
560
|
"SELECT COUNT(*) as count FROM credentials WHERE engagement_id = ?",
|
|
501
|
-
(engagement_id,)
|
|
502
|
-
).fetchone()[
|
|
561
|
+
(engagement_id,),
|
|
562
|
+
).fetchone()["count"]
|
|
503
563
|
|
|
504
564
|
# Valid credentials (confirmed working)
|
|
505
565
|
valid = conn.execute(
|
|
506
566
|
"SELECT COUNT(*) as count FROM credentials WHERE engagement_id = ? AND status = 'valid'",
|
|
507
|
-
(engagement_id,)
|
|
508
|
-
).fetchone()[
|
|
567
|
+
(engagement_id,),
|
|
568
|
+
).fetchone()["count"]
|
|
509
569
|
|
|
510
570
|
# Username-only (enumerated users)
|
|
511
571
|
users_only = conn.execute(
|
|
512
572
|
"SELECT COUNT(*) as count FROM credentials WHERE engagement_id = ? AND username IS NOT NULL AND password IS NULL",
|
|
513
|
-
(engagement_id,)
|
|
514
|
-
).fetchone()[
|
|
573
|
+
(engagement_id,),
|
|
574
|
+
).fetchone()["count"]
|
|
515
575
|
|
|
516
576
|
# Password-only
|
|
517
577
|
passwords_only = conn.execute(
|
|
518
578
|
"SELECT COUNT(*) as count FROM credentials WHERE engagement_id = ? AND username IS NULL AND password IS NOT NULL",
|
|
519
|
-
(engagement_id,)
|
|
520
|
-
).fetchone()[
|
|
579
|
+
(engagement_id,),
|
|
580
|
+
).fetchone()["count"]
|
|
521
581
|
|
|
522
582
|
# Username:password pairs
|
|
523
583
|
pairs = conn.execute(
|
|
524
584
|
"SELECT COUNT(*) as count FROM credentials WHERE engagement_id = ? AND username IS NOT NULL AND password IS NOT NULL",
|
|
525
|
-
(engagement_id,)
|
|
526
|
-
).fetchone()[
|
|
585
|
+
(engagement_id,),
|
|
586
|
+
).fetchone()["count"]
|
|
527
587
|
|
|
528
588
|
conn.close()
|
|
529
589
|
|
|
530
590
|
return {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
591
|
+
"total": total,
|
|
592
|
+
"valid": valid,
|
|
593
|
+
"users_only": users_only,
|
|
594
|
+
"passwords_only": passwords_only,
|
|
595
|
+
"pairs": pairs,
|
|
536
596
|
}
|
|
537
597
|
|
|
538
598
|
def encrypt_all_unencrypted(self) -> Dict[str, int]:
|
|
539
599
|
"""
|
|
540
600
|
Encrypt all plaintext credentials in the database.
|
|
541
|
-
|
|
601
|
+
|
|
542
602
|
This should be called after unlocking crypto to encrypt credentials
|
|
543
603
|
that were stored while the worker was running without the master password.
|
|
544
|
-
|
|
604
|
+
|
|
545
605
|
Returns:
|
|
546
606
|
Dict with counts of encrypted credentials
|
|
547
607
|
"""
|
|
548
608
|
if not self.crypto.is_encryption_enabled():
|
|
549
|
-
return {
|
|
550
|
-
|
|
609
|
+
return {"error": "Encryption not enabled"}
|
|
610
|
+
|
|
551
611
|
if not self.crypto.is_unlocked():
|
|
552
|
-
return {
|
|
553
|
-
|
|
612
|
+
return {"error": "Crypto manager is locked - cannot encrypt"}
|
|
613
|
+
|
|
554
614
|
conn = self.db.get_connection()
|
|
555
|
-
|
|
615
|
+
|
|
556
616
|
# Get all credentials
|
|
557
617
|
rows = conn.execute("SELECT id, username, password FROM credentials").fetchall()
|
|
558
|
-
|
|
618
|
+
|
|
559
619
|
encrypted_count = 0
|
|
560
620
|
skipped_count = 0
|
|
561
|
-
|
|
621
|
+
|
|
562
622
|
for row in rows:
|
|
563
|
-
cred_id = row[
|
|
564
|
-
username = row[
|
|
565
|
-
password = row[
|
|
566
|
-
|
|
623
|
+
cred_id = row["id"]
|
|
624
|
+
username = row["username"]
|
|
625
|
+
password = row["password"]
|
|
626
|
+
|
|
567
627
|
needs_update = False
|
|
568
628
|
new_username = username
|
|
569
629
|
new_password = password
|
|
570
|
-
|
|
630
|
+
|
|
571
631
|
# Check if username needs encryption
|
|
572
632
|
if username and isinstance(username, str):
|
|
573
633
|
if username.startswith("UNENCRYPTED:"):
|
|
@@ -584,7 +644,7 @@ class CredentialsManager:
|
|
|
584
644
|
# Not encrypted and no marker - encrypt as-is (legacy data)
|
|
585
645
|
new_username = self.crypto.encrypt(username)
|
|
586
646
|
needs_update = True
|
|
587
|
-
|
|
647
|
+
|
|
588
648
|
# Check if password needs encryption
|
|
589
649
|
if password and isinstance(password, str):
|
|
590
650
|
if password.startswith("UNENCRYPTED:"):
|
|
@@ -601,32 +661,34 @@ class CredentialsManager:
|
|
|
601
661
|
# Not encrypted and no marker - encrypt as-is (legacy data)
|
|
602
662
|
new_password = self.crypto.encrypt(password)
|
|
603
663
|
needs_update = True
|
|
604
|
-
|
|
664
|
+
|
|
605
665
|
if needs_update:
|
|
606
666
|
conn.execute(
|
|
607
667
|
"UPDATE credentials SET username = ?, password = ? WHERE id = ?",
|
|
608
|
-
(new_username, new_password, cred_id)
|
|
668
|
+
(new_username, new_password, cred_id),
|
|
609
669
|
)
|
|
610
670
|
encrypted_count += 1
|
|
611
671
|
else:
|
|
612
672
|
skipped_count += 1
|
|
613
|
-
|
|
673
|
+
|
|
614
674
|
conn.commit()
|
|
615
675
|
conn.close()
|
|
616
|
-
|
|
617
|
-
logger.info(
|
|
618
|
-
|
|
676
|
+
|
|
677
|
+
logger.info(
|
|
678
|
+
f"Encrypted {encrypted_count} credentials, skipped {skipped_count} already encrypted"
|
|
679
|
+
)
|
|
680
|
+
|
|
619
681
|
return {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
682
|
+
"encrypted": encrypted_count,
|
|
683
|
+
"skipped": skipped_count,
|
|
684
|
+
"total": encrypted_count + skipped_count,
|
|
623
685
|
}
|
|
624
|
-
|
|
686
|
+
|
|
625
687
|
def remove_duplicates(self, engagement_id: int) -> Dict[str, int]:
|
|
626
688
|
"""
|
|
627
689
|
Remove duplicate credentials from the database.
|
|
628
690
|
Keeps the one with the most definitive status (valid > untested).
|
|
629
|
-
|
|
691
|
+
|
|
630
692
|
Returns:
|
|
631
693
|
dict: {'removed': count, 'kept': count}
|
|
632
694
|
"""
|
|
@@ -643,42 +705,41 @@ class CredentialsManager:
|
|
|
643
705
|
END,
|
|
644
706
|
created_at DESC
|
|
645
707
|
"""
|
|
646
|
-
|
|
708
|
+
|
|
647
709
|
all_creds = self.db.execute(query, (engagement_id,))
|
|
648
|
-
|
|
710
|
+
|
|
649
711
|
seen = set()
|
|
650
712
|
to_remove = []
|
|
651
713
|
kept_count = 0
|
|
652
|
-
|
|
714
|
+
|
|
653
715
|
for cred in all_creds:
|
|
654
716
|
# Create a unique key for this credential
|
|
655
717
|
key = (
|
|
656
|
-
cred[
|
|
657
|
-
cred[
|
|
658
|
-
cred[
|
|
659
|
-
cred[
|
|
660
|
-
cred[
|
|
718
|
+
cred["host_id"],
|
|
719
|
+
cred["username"],
|
|
720
|
+
cred["password"],
|
|
721
|
+
cred["service"],
|
|
722
|
+
cred["port"],
|
|
661
723
|
)
|
|
662
|
-
|
|
724
|
+
|
|
663
725
|
if key in seen:
|
|
664
726
|
# Duplicate found
|
|
665
|
-
to_remove.append(cred[
|
|
727
|
+
to_remove.append(cred["id"])
|
|
666
728
|
else:
|
|
667
729
|
# First occurrence, keep it
|
|
668
730
|
seen.add(key)
|
|
669
731
|
kept_count += 1
|
|
670
|
-
|
|
732
|
+
|
|
671
733
|
# Remove duplicates
|
|
672
734
|
if to_remove:
|
|
673
|
-
placeholders =
|
|
735
|
+
placeholders = ",".join(["?"] * len(to_remove))
|
|
674
736
|
self.db.execute(
|
|
675
737
|
f"DELETE FROM credentials WHERE id IN ({placeholders})",
|
|
676
|
-
tuple(to_remove)
|
|
738
|
+
tuple(to_remove),
|
|
677
739
|
)
|
|
678
|
-
|
|
679
|
-
logger.info(
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
740
|
+
|
|
741
|
+
logger.info(
|
|
742
|
+
f"Removed {len(to_remove)} duplicate credentials, kept {kept_count}"
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
return {"removed": len(to_remove), "kept": kept_count}
|