souleyez 2.43.29__py3-none-any.whl → 2.43.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +22827 -10678
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -45,8 +45,8 @@ class CryptoManager:
45
45
  self._salt: Optional[bytes] = None
46
46
  self._last_access: Optional[datetime] = None
47
47
  self._last_unlock_time: Optional[datetime] = None
48
- self._timeout_minutes = config.get('security.session_timeout_minutes', 30)
49
-
48
+ self._timeout_minutes = config.get("security.session_timeout_minutes", 30)
49
+
50
50
  # Vault lockout tracking
51
51
  self._failed_attempts = 0
52
52
  self._lockout_until: Optional[datetime] = None
@@ -61,13 +61,13 @@ class CryptoManager:
61
61
  """Load existing crypto config or create new one."""
62
62
  if self.crypto_config_path.exists():
63
63
  try:
64
- with open(self.crypto_config_path, 'r') as f:
64
+ with open(self.crypto_config_path, "r") as f:
65
65
  cfg = json.load(f)
66
- self._salt = base64.b64decode(cfg.get('salt', ''))
67
- self._encryption_enabled = cfg.get('encryption_enabled', False)
66
+ self._salt = base64.b64decode(cfg.get("salt", ""))
67
+ self._encryption_enabled = cfg.get("encryption_enabled", False)
68
68
 
69
69
  # Load persisted lockout state
70
- lockout_str = cfg.get('lockout_until')
70
+ lockout_str = cfg.get("lockout_until")
71
71
  if lockout_str:
72
72
  self._lockout_until = datetime.fromisoformat(lockout_str)
73
73
  # Check if lockout has expired
@@ -77,20 +77,21 @@ class CryptoManager:
77
77
  self._failed_attempts = 0
78
78
  else:
79
79
  # Active lockout - load the failed attempts count
80
- self._failed_attempts = cfg.get('failed_attempts', 0)
80
+ self._failed_attempts = cfg.get("failed_attempts", 0)
81
81
  else:
82
82
  # No lockout - reset failed attempts (fresh start)
83
83
  self._failed_attempts = 0
84
84
 
85
- logger.info("Crypto config loaded", extra={
86
- "encryption_enabled": self._encryption_enabled
87
- })
85
+ logger.info(
86
+ "Crypto config loaded",
87
+ extra={"encryption_enabled": self._encryption_enabled},
88
+ )
88
89
  except (json.JSONDecodeError, KeyError) as e:
89
90
  # Corrupted config, regenerate
90
- logger.warning("Crypto config corrupted, regenerating", extra={
91
- "error": str(e),
92
- "traceback": traceback.format_exc()
93
- })
91
+ logger.warning(
92
+ "Crypto config corrupted, regenerating",
93
+ extra={"error": str(e), "traceback": traceback.format_exc()},
94
+ )
94
95
  self._initialize_config()
95
96
  else:
96
97
  logger.info("Initializing new crypto config")
@@ -101,10 +102,10 @@ class CryptoManager:
101
102
  self._salt = os.urandom(32) # 256-bit salt
102
103
  self._encryption_enabled = False
103
104
  self._save_config()
104
- logger.info("Crypto config initialized", extra={
105
- "salt_generated": True,
106
- "encryption_enabled": False
107
- })
105
+ logger.info(
106
+ "Crypto config initialized",
107
+ extra={"salt_generated": True, "encryption_enabled": False},
108
+ )
108
109
 
109
110
  def _save_config(self):
110
111
  """Save crypto configuration to disk."""
@@ -114,29 +115,33 @@ class CryptoManager:
114
115
  existing_verification = None
115
116
  if self.crypto_config_path.exists():
116
117
  try:
117
- with open(self.crypto_config_path, 'r') as f:
118
+ with open(self.crypto_config_path, "r") as f:
118
119
  existing_config = json.load(f)
119
- existing_verification = existing_config.get('password_verification')
120
+ existing_verification = existing_config.get("password_verification")
120
121
  except (json.JSONDecodeError, IOError):
121
122
  pass
122
123
 
123
124
  config = {
124
- 'salt': base64.b64encode(self._salt).decode('utf-8'),
125
- 'encryption_enabled': self._encryption_enabled,
126
- 'failed_attempts': self._failed_attempts,
127
- 'lockout_until': self._lockout_until.isoformat() if self._lockout_until else None
125
+ "salt": base64.b64encode(self._salt).decode("utf-8"),
126
+ "encryption_enabled": self._encryption_enabled,
127
+ "failed_attempts": self._failed_attempts,
128
+ "lockout_until": (
129
+ self._lockout_until.isoformat() if self._lockout_until else None
130
+ ),
128
131
  }
129
132
 
130
133
  # Add password verification token
131
134
  if self._fernet:
132
135
  # Create new verification token with current fernet
133
136
  verification_data = b"SOULEYEZ_PASSWORD_VERIFICATION_TOKEN"
134
- config['password_verification'] = self._fernet.encrypt(verification_data).decode('utf-8')
137
+ config["password_verification"] = self._fernet.encrypt(
138
+ verification_data
139
+ ).decode("utf-8")
135
140
  elif existing_verification:
136
141
  # Preserve existing verification token
137
- config['password_verification'] = existing_verification
142
+ config["password_verification"] = existing_verification
138
143
 
139
- with open(self.crypto_config_path, 'w') as f:
144
+ with open(self.crypto_config_path, "w") as f:
140
145
  json.dump(config, f, indent=2)
141
146
  # Secure permissions (readable only by owner)
142
147
  os.chmod(self.crypto_config_path, 0o600)
@@ -148,18 +153,23 @@ class CryptoManager:
148
153
  def is_unlocked(self) -> bool:
149
154
  """Check if crypto manager is unlocked (key loaded)."""
150
155
  return self._fernet is not None
151
-
156
+
152
157
  def _check_timeout(self):
153
158
  """Check if session has timed out and lock if necessary."""
154
159
  if self._last_access and self._fernet:
155
160
  elapsed = (datetime.now() - self._last_access).total_seconds() / 60
156
161
  if elapsed > self._timeout_minutes:
157
- logger.warning("Session expired due to inactivity", extra={
158
- "elapsed_minutes": round(elapsed, 2),
159
- "timeout_minutes": self._timeout_minutes
160
- })
162
+ logger.warning(
163
+ "Session expired due to inactivity",
164
+ extra={
165
+ "elapsed_minutes": round(elapsed, 2),
166
+ "timeout_minutes": self._timeout_minutes,
167
+ },
168
+ )
161
169
  self.lock()
162
- raise RuntimeError(f"Session expired after {self._timeout_minutes} minutes of inactivity. Please unlock again.")
170
+ raise RuntimeError(
171
+ f"Session expired after {self._timeout_minutes} minutes of inactivity. Please unlock again."
172
+ )
163
173
  self._last_access = datetime.now()
164
174
 
165
175
  def derive_key_from_password(self, password: str) -> bytes:
@@ -179,10 +189,10 @@ class CryptoManager:
179
189
  algorithm=hashes.SHA256(),
180
190
  length=32,
181
191
  salt=self._salt,
182
- iterations=config.get('crypto.iterations', 600000),
183
- backend=default_backend()
192
+ iterations=config.get("crypto.iterations", 600000),
193
+ backend=default_backend(),
184
194
  )
185
- key = base64.urlsafe_b64encode(kdf.derive(password.encode('utf-8')))
195
+ key = base64.urlsafe_b64encode(kdf.derive(password.encode("utf-8")))
186
196
  return key
187
197
 
188
198
  def is_locked_out(self) -> bool:
@@ -209,19 +219,27 @@ class CryptoManager:
209
219
  remaining = self._max_attempts - self._failed_attempts
210
220
 
211
221
  if self._failed_attempts >= self._max_attempts:
212
- self._lockout_until = datetime.now() + timedelta(minutes=self._lockout_minutes)
213
- logger.warning("Vault locked out", extra={
214
- "failed_attempts": self._failed_attempts,
215
- "lockout_minutes": self._lockout_minutes
216
- })
222
+ self._lockout_until = datetime.now() + timedelta(
223
+ minutes=self._lockout_minutes
224
+ )
225
+ logger.warning(
226
+ "Vault locked out",
227
+ extra={
228
+ "failed_attempts": self._failed_attempts,
229
+ "lockout_minutes": self._lockout_minutes,
230
+ },
231
+ )
217
232
  # Persist lockout state to survive app restarts
218
233
  self._save_config()
219
234
  return (0, True)
220
235
 
221
- logger.info("Failed vault unlock attempt", extra={
222
- "failed_attempts": self._failed_attempts,
223
- "remaining_attempts": remaining
224
- })
236
+ logger.info(
237
+ "Failed vault unlock attempt",
238
+ extra={
239
+ "failed_attempts": self._failed_attempts,
240
+ "remaining_attempts": remaining,
241
+ },
242
+ )
225
243
  # Persist failed attempt count
226
244
  self._save_config()
227
245
  return (remaining, False)
@@ -255,16 +273,16 @@ class CryptoManager:
255
273
  # Check if locked out
256
274
  if self.is_locked_out():
257
275
  remaining = self.get_lockout_remaining()
258
- logger.warning("Unlock attempt during lockout", extra={
259
- "remaining_seconds": remaining
260
- })
276
+ logger.warning(
277
+ "Unlock attempt during lockout", extra={"remaining_seconds": remaining}
278
+ )
261
279
  return False
262
-
280
+
263
281
  # Reject empty passwords immediately
264
282
  if not password or not password.strip():
265
283
  logger.warning("Unlock failed - empty password not allowed")
266
284
  return False
267
-
285
+
268
286
  try:
269
287
  key = self.derive_key_from_password(password)
270
288
  self._fernet = Fernet(key)
@@ -272,21 +290,27 @@ class CryptoManager:
272
290
  # Validate password by trying to decrypt the verification token
273
291
  # Load current config to get verification token
274
292
  if self.crypto_config_path.exists():
275
- with open(self.crypto_config_path, 'r') as f:
293
+ with open(self.crypto_config_path, "r") as f:
276
294
  cfg = json.load(f)
277
295
 
278
- verification_token = cfg.get('password_verification')
296
+ verification_token = cfg.get("password_verification")
279
297
  if verification_token:
280
298
  # Try to decrypt the verification token
281
299
  try:
282
- decrypted = self._fernet.decrypt(verification_token.encode('utf-8'))
300
+ decrypted = self._fernet.decrypt(
301
+ verification_token.encode("utf-8")
302
+ )
283
303
  if decrypted != b"SOULEYEZ_PASSWORD_VERIFICATION_TOKEN":
284
304
  self._fernet = None
285
- logger.warning("Unlock failed - password verification failed")
305
+ logger.warning(
306
+ "Unlock failed - password verification failed"
307
+ )
286
308
  return False
287
309
  except Exception as decrypt_error:
288
310
  self._fernet = None
289
- logger.warning(f"Unlock failed - incorrect password: {decrypt_error}")
311
+ logger.warning(
312
+ f"Unlock failed - incorrect password: {decrypt_error}"
313
+ )
290
314
  return False
291
315
  else:
292
316
  # No verification token yet (old version or first unlock after upgrade)
@@ -298,9 +322,10 @@ class CryptoManager:
298
322
  self.reset_failed_attempts()
299
323
  self._last_access = datetime.now()
300
324
  self._last_unlock_time = datetime.now()
301
- logger.info("Crypto manager unlocked", extra={
302
- "encryption_enabled": self._encryption_enabled
303
- })
325
+ logger.info(
326
+ "Crypto manager unlocked",
327
+ extra={"encryption_enabled": self._encryption_enabled},
328
+ )
304
329
 
305
330
  # Write msfrpc session file if configured (enables background worker RPC access)
306
331
  self._write_msfrpc_session_if_configured()
@@ -308,10 +333,10 @@ class CryptoManager:
308
333
  return True
309
334
  except Exception as e:
310
335
  self._fernet = None
311
- logger.error("Unlock failed", extra={
312
- "error_type": type(e).__name__,
313
- "error_message": "<redacted>"
314
- })
336
+ logger.error(
337
+ "Unlock failed",
338
+ extra={"error_type": type(e).__name__, "error_message": "<redacted>"},
339
+ )
315
340
  return False
316
341
 
317
342
  def lock(self):
@@ -334,11 +359,11 @@ class CryptoManager:
334
359
  from souleyez import config as app_config
335
360
 
336
361
  # Only write if msfrpc is enabled
337
- if not app_config.get('msfrpc.enabled', False):
362
+ if not app_config.get("msfrpc.enabled", False):
338
363
  return
339
364
 
340
365
  # Get encrypted password
341
- encrypted = app_config.get('msfrpc.password')
366
+ encrypted = app_config.get("msfrpc.password")
342
367
  if not encrypted:
343
368
  return
344
369
 
@@ -349,6 +374,7 @@ class CryptoManager:
349
374
 
350
375
  # Write to session file
351
376
  from souleyez.core.msf_rpc_manager import write_msfrpc_session
377
+
352
378
  write_msfrpc_session(decrypted)
353
379
  logger.debug("MSF RPC session file written for background worker")
354
380
 
@@ -359,6 +385,7 @@ class CryptoManager:
359
385
  """Clear the msfrpc session file on lock."""
360
386
  try:
361
387
  from souleyez.core.msf_rpc_manager import clear_msfrpc_session
388
+
362
389
  clear_msfrpc_session()
363
390
  except Exception:
364
391
  pass # Best effort cleanup
@@ -377,71 +404,99 @@ class CryptoManager:
377
404
  logger.info("Encryption already enabled")
378
405
  return True
379
406
 
380
- # Unlock with the provided password
381
- if not self.unlock(password):
382
- logger.warning("Enable encryption failed - unlock unsuccessful")
407
+ try:
408
+ # When enabling encryption (fresh or after disable), we need to:
409
+ # 1. Generate new salt (or use existing if valid)
410
+ # 2. Derive key from password
411
+ # 3. Create new fernet
412
+ # 4. Save config with new verification token
413
+
414
+ # Generate fresh salt if needed
415
+ if not self._salt:
416
+ self._salt = os.urandom(32)
417
+
418
+ # Derive key and create fernet
419
+ key = self.derive_key_from_password(password)
420
+ self._fernet = Fernet(key)
421
+
422
+ # Enable encryption and save (this creates new verification token)
423
+ self._encryption_enabled = True
424
+ self._save_config()
425
+
426
+ # Reset lockout state on successful enable
427
+ self.reset_failed_attempts()
428
+ self._last_access = datetime.now()
429
+ self._last_unlock_time = datetime.now()
430
+
431
+ # Migrate existing plaintext credentials
432
+ migrated = self._migrate_plaintext_credentials()
433
+ logger.info(
434
+ "Encryption enabled successfully",
435
+ extra={"credentials_migrated": migrated},
436
+ )
437
+
438
+ return True
439
+ except Exception as e:
440
+ logger.error(
441
+ "Failed to enable encryption",
442
+ extra={"error_type": type(e).__name__, "error": str(e)},
443
+ )
444
+ self._fernet = None
383
445
  return False
384
446
 
385
- self._encryption_enabled = True
386
- self._save_config()
387
-
388
- # Migrate existing plaintext credentials
389
- migrated = self._migrate_plaintext_credentials()
390
- logger.info("Encryption enabled successfully", extra={"credentials_migrated": migrated})
391
-
392
- return True
393
-
394
447
  def _migrate_plaintext_credentials(self) -> int:
395
448
  """
396
449
  Migrate existing plaintext credentials to encrypted format.
397
-
450
+
398
451
  Returns:
399
452
  Number of credentials migrated
400
453
  """
401
454
  from souleyez.storage.database import Database
402
-
455
+
403
456
  if not self._fernet:
404
457
  logger.error("Cannot migrate credentials - crypto manager not unlocked")
405
458
  return 0
406
-
459
+
407
460
  db = Database()
408
461
  credentials = db.execute("SELECT id, username, password FROM credentials")
409
-
462
+
410
463
  migrated = 0
411
464
  for cred in credentials:
412
- cred_id = cred['id']
413
- username = cred.get('username')
414
- password = cred.get('password')
415
-
465
+ cred_id = cred["id"]
466
+ username = cred.get("username")
467
+ password = cred.get("password")
468
+
416
469
  updated = False
417
470
  update_data = {}
418
-
471
+
419
472
  # Check if username is plaintext (not encrypted)
420
473
  if username and not self._is_encrypted(username):
421
474
  encrypted_username = self.encrypt(username)
422
475
  if encrypted_username:
423
- update_data['username'] = encrypted_username
476
+ update_data["username"] = encrypted_username
424
477
  updated = True
425
-
478
+
426
479
  # Check if password is plaintext (not encrypted)
427
480
  if password and not self._is_encrypted(password):
428
481
  encrypted_password = self.encrypt(password)
429
482
  if encrypted_password:
430
- update_data['password'] = encrypted_password
483
+ update_data["password"] = encrypted_password
431
484
  updated = True
432
-
485
+
433
486
  if updated:
434
487
  db.execute(
435
488
  f"UPDATE credentials SET {', '.join([f'{k}=?' for k in update_data.keys()])} WHERE id = ?",
436
- tuple(list(update_data.values()) + [cred_id])
489
+ tuple(list(update_data.values()) + [cred_id]),
437
490
  )
438
491
  migrated += 1
439
-
492
+
440
493
  if migrated > 0:
441
- logger.info(f"Migrated {migrated} plaintext credentials to encrypted format")
442
-
494
+ logger.info(
495
+ f"Migrated {migrated} plaintext credentials to encrypted format"
496
+ )
497
+
443
498
  return migrated
444
-
499
+
445
500
  def _is_encrypted(self, value: str) -> bool:
446
501
  """
447
502
  Check if a value appears to be encrypted (simple heuristic).
@@ -451,7 +506,7 @@ class CryptoManager:
451
506
  return False
452
507
  try:
453
508
  # Fernet tokens always start with version byte (0x80 = gA in base64)
454
- return value.startswith('gA') and len(value) > 50
509
+ return value.startswith("gA") and len(value) > 50
455
510
  except:
456
511
  return False
457
512
 
@@ -496,17 +551,17 @@ class CryptoManager:
496
551
  return None
497
552
 
498
553
  try:
499
- encrypted_bytes = self._fernet.encrypt(plaintext.encode('utf-8'))
500
- logger.debug("Data encrypted", extra={
501
- "data_type": "credential_field",
502
- "size_bytes": len(plaintext)
503
- })
504
- return encrypted_bytes.decode('utf-8')
554
+ encrypted_bytes = self._fernet.encrypt(plaintext.encode("utf-8"))
555
+ logger.debug(
556
+ "Data encrypted",
557
+ extra={"data_type": "credential_field", "size_bytes": len(plaintext)},
558
+ )
559
+ return encrypted_bytes.decode("utf-8")
505
560
  except Exception as e:
506
- logger.error("Encryption failed", extra={
507
- "error_type": type(e).__name__,
508
- "error_message": "<redacted>"
509
- })
561
+ logger.error(
562
+ "Encryption failed",
563
+ extra={"error_type": type(e).__name__, "error_message": "<redacted>"},
564
+ )
510
565
  raise RuntimeError("Encryption failed")
511
566
 
512
567
  def decrypt(self, ciphertext: str) -> Optional[str]:
@@ -533,52 +588,164 @@ class CryptoManager:
533
588
  return None
534
589
 
535
590
  try:
536
- decrypted_bytes = self._fernet.decrypt(ciphertext.encode('utf-8'))
537
- logger.debug("Data decrypted", extra={
538
- "data_type": "credential_field",
539
- "success": True
540
- })
541
- return decrypted_bytes.decode('utf-8')
591
+ decrypted_bytes = self._fernet.decrypt(ciphertext.encode("utf-8"))
592
+ logger.debug(
593
+ "Data decrypted",
594
+ extra={"data_type": "credential_field", "success": True},
595
+ )
596
+ return decrypted_bytes.decode("utf-8")
542
597
  except InvalidToken:
543
598
  # Data might be plaintext (migration scenario)
544
599
  logger.warning("Decryption failed - data may be plaintext")
545
600
  return ciphertext
546
601
  except Exception as e:
547
- logger.error("Decryption failed", extra={
548
- "error_type": type(e).__name__,
549
- "error_message": "<redacted>"
550
- })
602
+ logger.error(
603
+ "Decryption failed",
604
+ extra={"error_type": type(e).__name__, "error_message": "<redacted>"},
605
+ )
551
606
  raise RuntimeError("Decryption failed")
552
607
 
553
- def change_password(self, old_password: str, new_password: str) -> bool:
608
+ def change_password(self, old_password: str, new_password: str) -> tuple:
554
609
  """
555
- Change master password.
610
+ Change master password with atomic credential re-encryption.
556
611
 
557
- Note: This requires re-encrypting all credentials with the new key.
558
- Use the migration script for this operation.
612
+ Safely re-encrypts all credentials with the new password. If any step
613
+ fails, the old password remains valid and no data is lost.
559
614
 
560
615
  Args:
561
616
  old_password: Current password
562
617
  new_password: New password
563
618
 
564
619
  Returns:
565
- True if password changed successfully
620
+ Tuple of (success: bool, error_message: str or None, credentials_migrated: int)
566
621
  """
567
- # Verify old password
622
+ from souleyez.storage.database import Database
623
+
624
+ # Step 1: Verify old password
568
625
  if not self.unlock(old_password):
569
626
  logger.warning("Password change failed - old password incorrect")
570
- return False
627
+ return (False, "Incorrect current password", 0)
571
628
 
572
- # Generate new salt and create new fernet with new password
573
- self._salt = os.urandom(32)
574
- key = self.derive_key_from_password(new_password)
575
- self._fernet = Fernet(key)
629
+ old_fernet = self._fernet
630
+ old_salt = self._salt
576
631
 
577
- # Save config with new salt and new verification token
632
+ # Step 2: Read all encrypted credentials BEFORE changing anything
633
+ db = Database()
634
+ credentials = db.execute("SELECT id, username, password FROM credentials")
635
+
636
+ # Step 3: Decrypt all credentials with old key
637
+ decrypted_creds = []
638
+ for cred in credentials:
639
+ cred_id = cred["id"]
640
+ username = cred.get("username")
641
+ password = cred.get("password")
642
+
643
+ try:
644
+ decrypted_username = None
645
+ decrypted_password = None
646
+
647
+ if username and self._is_encrypted(username):
648
+ decrypted_username = old_fernet.decrypt(
649
+ username.encode("utf-8")
650
+ ).decode("utf-8")
651
+ else:
652
+ decrypted_username = username
653
+
654
+ if password and self._is_encrypted(password):
655
+ decrypted_password = old_fernet.decrypt(
656
+ password.encode("utf-8")
657
+ ).decode("utf-8")
658
+ else:
659
+ decrypted_password = password
660
+
661
+ decrypted_creds.append(
662
+ {
663
+ "id": cred_id,
664
+ "username": decrypted_username,
665
+ "password": decrypted_password,
666
+ }
667
+ )
668
+ except Exception as e:
669
+ # Rollback - don't change anything
670
+ logger.error(
671
+ "Password change failed - could not decrypt credential",
672
+ extra={"cred_id": cred_id, "error": str(e)},
673
+ )
674
+ return (False, f"Failed to decrypt credential ID {cred_id}", 0)
675
+
676
+ # Step 4: Generate new salt and key
677
+ new_salt = os.urandom(32)
678
+ self._salt = new_salt
679
+ new_key = self.derive_key_from_password(new_password)
680
+ new_fernet = Fernet(new_key)
681
+
682
+ # Step 5: Re-encrypt all credentials with new key
683
+ re_encrypted = []
684
+ for cred in decrypted_creds:
685
+ try:
686
+ encrypted_username = None
687
+ encrypted_password = None
688
+
689
+ if cred["username"]:
690
+ encrypted_username = new_fernet.encrypt(
691
+ cred["username"].encode("utf-8")
692
+ ).decode("utf-8")
693
+
694
+ if cred["password"]:
695
+ encrypted_password = new_fernet.encrypt(
696
+ cred["password"].encode("utf-8")
697
+ ).decode("utf-8")
698
+
699
+ re_encrypted.append(
700
+ {
701
+ "id": cred["id"],
702
+ "username": encrypted_username,
703
+ "password": encrypted_password,
704
+ }
705
+ )
706
+ except Exception as e:
707
+ # Rollback - restore old salt and don't update anything
708
+ self._salt = old_salt
709
+ self._fernet = old_fernet
710
+ logger.error(
711
+ "Password change failed - could not re-encrypt credential",
712
+ extra={"cred_id": cred["id"], "error": str(e)},
713
+ )
714
+ return (False, f"Failed to re-encrypt credential ID {cred['id']}", 0)
715
+
716
+ # Step 6: Update database with re-encrypted credentials
717
+ migrated = 0
718
+ try:
719
+ for cred in re_encrypted:
720
+ db.execute(
721
+ "UPDATE credentials SET username = ?, password = ? WHERE id = ?",
722
+ (cred["username"], cred["password"], cred["id"]),
723
+ )
724
+ migrated += 1
725
+ except Exception as e:
726
+ # Database update failed - this is problematic but old salt is still in config
727
+ # The safest thing is to not update the config file
728
+ self._salt = old_salt
729
+ self._fernet = old_fernet
730
+ logger.error(
731
+ "Password change failed - database update error",
732
+ extra={"error": str(e), "credentials_updated": migrated},
733
+ )
734
+ return (
735
+ False,
736
+ f"Database error after updating {migrated} credentials",
737
+ migrated,
738
+ )
739
+
740
+ # Step 7: Only NOW update the fernet and save config
741
+ self._fernet = new_fernet
578
742
  self._save_config()
579
743
 
580
- logger.info("Master password changed successfully")
581
- return True
744
+ logger.info(
745
+ "Master password changed successfully",
746
+ extra={"credentials_migrated": migrated},
747
+ )
748
+ return (True, None, migrated)
582
749
 
583
750
 
584
751
  # Singleton accessor