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
@@ -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'[A-Z]', password):
36
+ if not re.search(r"[A-Z]", password):
37
37
  raise ValueError("Password must contain uppercase letter")
38
- if not re.search(r'[a-z]', password):
38
+ if not re.search(r"[a-z]", password):
39
39
  raise ValueError("Password must contain lowercase letter")
40
- if not re.search(r'[0-9]', password):
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(f"Storing credential field with UNENCRYPTED: marker - crypto locked")
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("Credentials are encrypted but crypto manager is locked. Call unlock() first.")
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
- conn.execute("""
106
+ """
107
+ )
108
+ conn.execute(
109
+ """
101
110
  CREATE INDEX IF NOT EXISTS idx_credentials_host
102
111
  ON credentials(host_id)
103
- """)
104
- conn.execute("""
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 = 'tcp',
121
- credential_type: str = 'user',
122
- status: str = 'untested',
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, host_id, encrypted_username, encrypted_password, service, port
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 == 'valid' and existing['status'] != 'valid':
153
- self._update_status(existing['id'], status, tool)
154
- return existing['id']
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 == 'valid':
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(username_only['id'], password=encrypted_password, status=status, tool=tool)
165
- return username_only['id']
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
- 'engagement_id': engagement_id,
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
- "tool": tool
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("Credential accessed", extra={
238
- "engagement": self.engagement or engagement_id,
239
- "cred_id": result.get('id'),
240
- "username": username if username else "<none>",
241
- "host_id": host_id
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("Credential status updated", extra={
263
- "engagement": self.engagement,
264
- "cred_id": credential_id,
265
- "new_status": status,
266
- "tool": tool
267
- })
268
-
269
- def _update_credential(self, credential_id: int, password: str = None, status: str = None, tool: str = None, notes: str = None, last_tested: str = None):
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(self, credential_id: int, status: str = None, notes: str = None):
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(credential_id, status=status, notes=notes, last_tested=last_tested)
322
-
323
- logger.info("Credential status updated", extra={
324
- "engagement": self.engagement,
325
- "cred_id": credential_id,
326
- "new_status": status
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("SELECT username FROM credentials WHERE id = ?", (credential_id,))
352
- username = cred.get('username') if cred else "<unknown>"
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("Credential deleted", extra={
360
- "engagement": self.engagement,
361
- "cred_id": credential_id,
362
- "username": username
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("Encryption enable failed", extra={
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("Credentials unlocked", extra={
405
- "engagement": self.engagement,
406
- "success": True
407
- })
462
+ logger.info(
463
+ "Credentials unlocked",
464
+ extra={"engagement": self.engagement, "success": True},
465
+ )
408
466
  else:
409
- logger.warning("Credentials unlock failed", extra={
410
- "engagement": self.engagement,
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('username'):
468
- row['username'] = self._decrypt_field(row['username'])
469
- if row.get('password'):
470
- row['password'] = self._decrypt_field(row['password'])
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('username'):
475
- row['username'] = '••••••••'
476
- if row.get('password'):
477
- row['password'] = '••••••••'
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(self, engagement_id: int, decrypt: bool = True) -> List[Dict[str, Any]]:
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()['count']
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()['count']
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()['count']
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()['count']
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()['count']
585
+ (engagement_id,),
586
+ ).fetchone()["count"]
527
587
 
528
588
  conn.close()
529
589
 
530
590
  return {
531
- 'total': total,
532
- 'valid': valid,
533
- 'users_only': users_only,
534
- 'passwords_only': passwords_only,
535
- 'pairs': pairs
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 {'error': 'Encryption not enabled'}
550
-
609
+ return {"error": "Encryption not enabled"}
610
+
551
611
  if not self.crypto.is_unlocked():
552
- return {'error': 'Crypto manager is locked - cannot encrypt'}
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['id']
564
- username = row['username']
565
- password = row['password']
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(f"Encrypted {encrypted_count} credentials, skipped {skipped_count} already encrypted")
618
-
676
+
677
+ logger.info(
678
+ f"Encrypted {encrypted_count} credentials, skipped {skipped_count} already encrypted"
679
+ )
680
+
619
681
  return {
620
- 'encrypted': encrypted_count,
621
- 'skipped': skipped_count,
622
- 'total': encrypted_count + skipped_count
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['host_id'],
657
- cred['username'],
658
- cred['password'],
659
- cred['service'],
660
- cred['port']
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['id'])
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 = ','.join(['?'] * len(to_remove))
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(f"Removed {len(to_remove)} duplicate credentials, kept {kept_count}")
680
-
681
- return {
682
- 'removed': len(to_remove),
683
- 'kept': kept_count
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}