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.
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 +9564 -2881
  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 +564 -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 +409 -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 +417 -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 +913 -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 +219 -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 +237 -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 +23034 -10679
  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-3.0.0.dist-info}/METADATA +2 -2
  353. souleyez-3.0.0.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  """
2
2
  CLI commands for screenshot management.
3
3
  """
4
+
4
5
  import click
5
6
  from pathlib import Path
6
7
  from rich.console import Console
@@ -21,12 +22,12 @@ def screenshots():
21
22
 
22
23
 
23
24
  @screenshots.command()
24
- @click.argument('source', type=click.Path(exists=True))
25
- @click.option('--title', '-t', help='Screenshot title')
26
- @click.option('--description', '-d', help='Screenshot description')
27
- @click.option('--host', '-h', type=int, help='Link to host ID')
28
- @click.option('--finding', '-f', type=int, help='Link to finding ID')
29
- @click.option('--job', '-j', type=int, help='Link to job ID')
25
+ @click.argument("source", type=click.Path(exists=True))
26
+ @click.option("--title", "-t", help="Screenshot title")
27
+ @click.option("--description", "-d", help="Screenshot description")
28
+ @click.option("--host", "-h", type=int, help="Link to host ID")
29
+ @click.option("--finding", "-f", type=int, help="Link to finding ID")
30
+ @click.option("--job", "-j", type=int, help="Link to job ID")
30
31
  def add(source, title, description, host, finding, job):
31
32
  """Add a screenshot to current engagement."""
32
33
  em = EngagementManager()
@@ -39,13 +40,13 @@ def add(source, title, description, host, finding, job):
39
40
 
40
41
  try:
41
42
  screenshot_id = sm.add_screenshot(
42
- engagement_id=current['id'],
43
+ engagement_id=current["id"],
43
44
  source_path=source,
44
45
  title=title,
45
46
  description=description,
46
47
  host_id=host,
47
48
  finding_id=finding,
48
- job_id=job
49
+ job_id=job,
49
50
  )
50
51
 
51
52
  console.print(f"[green]✓ Screenshot added: ID {screenshot_id}[/green]")
@@ -64,9 +65,9 @@ def add(source, title, description, host, finding, job):
64
65
 
65
66
 
66
67
  @screenshots.command()
67
- @click.option('--host', '-h', type=int, help='Filter by host ID')
68
- @click.option('--finding', '-f', type=int, help='Filter by finding ID')
69
- @click.option('--job', '-j', type=int, help='Filter by job ID')
68
+ @click.option("--host", "-h", type=int, help="Filter by host ID")
69
+ @click.option("--finding", "-f", type=int, help="Filter by finding ID")
70
+ @click.option("--job", "-j", type=int, help="Filter by job ID")
70
71
  def list(host, finding, job):
71
72
  """List screenshots for current engagement."""
72
73
  em = EngagementManager()
@@ -78,10 +79,7 @@ def list(host, finding, job):
78
79
  return
79
80
 
80
81
  screenshots = sm.list_screenshots(
81
- engagement_id=current['id'],
82
- host_id=host,
83
- finding_id=finding,
84
- job_id=job
82
+ engagement_id=current["id"], host_id=host, finding_id=finding, job_id=job
85
83
  )
86
84
 
87
85
  if not screenshots:
@@ -97,7 +95,7 @@ def list(host, finding, job):
97
95
  table.add_column("Created", style="blue")
98
96
 
99
97
  for s in screenshots:
100
- size = s['file_size']
98
+ size = s["file_size"]
101
99
  if size < 1024:
102
100
  size_str = f"{size} B"
103
101
  elif size < 1024 * 1024:
@@ -106,21 +104,21 @@ def list(host, finding, job):
106
104
  size_str = f"{size / (1024 * 1024):.1f} MB"
107
105
 
108
106
  links = []
109
- if s['host_id']:
107
+ if s["host_id"]:
110
108
  links.append(f"Host:{s['host_id']}")
111
- if s['finding_id']:
109
+ if s["finding_id"]:
112
110
  links.append(f"Finding:{s['finding_id']}")
113
- if s['job_id']:
111
+ if s["job_id"]:
114
112
  links.append(f"Job:{s['job_id']}")
115
113
  links_str = ", ".join(links) if links else "None"
116
114
 
117
115
  table.add_row(
118
- str(s['id']),
119
- s['title'] or s['filename'],
120
- s['filename'][:30] + "..." if len(s['filename']) > 30 else s['filename'],
116
+ str(s["id"]),
117
+ s["title"] or s["filename"],
118
+ s["filename"][:30] + "..." if len(s["filename"]) > 30 else s["filename"],
121
119
  size_str,
122
120
  links_str,
123
- s['created_at'][:10]
121
+ s["created_at"][:10],
124
122
  )
125
123
 
126
124
  console.print(table)
@@ -128,30 +126,36 @@ def list(host, finding, job):
128
126
 
129
127
 
130
128
  @screenshots.command()
131
- @click.argument('screenshot_id', type=int)
132
- @click.option('--host', '-h', type=int, help='Link to host ID')
133
- @click.option('--finding', '-f', type=int, help='Link to finding ID')
134
- @click.option('--job', '-j', type=int, help='Link to job ID')
129
+ @click.argument("screenshot_id", type=int)
130
+ @click.option("--host", "-h", type=int, help="Link to host ID")
131
+ @click.option("--finding", "-f", type=int, help="Link to finding ID")
132
+ @click.option("--job", "-j", type=int, help="Link to job ID")
135
133
  def link(screenshot_id, host, finding, job):
136
134
  """Link screenshot to host/finding/job."""
137
135
  sm = ScreenshotManager()
138
136
 
139
137
  if host:
140
138
  sm.link_to_host(screenshot_id, host)
141
- console.print(f"[green]✓ Linked screenshot {screenshot_id} to host {host}[/green]")
139
+ console.print(
140
+ f"[green]✓ Linked screenshot {screenshot_id} to host {host}[/green]"
141
+ )
142
142
 
143
143
  if finding:
144
144
  sm.link_to_finding(screenshot_id, finding)
145
- console.print(f"[green]✓ Linked screenshot {screenshot_id} to finding {finding}[/green]")
145
+ console.print(
146
+ f"[green]✓ Linked screenshot {screenshot_id} to finding {finding}[/green]"
147
+ )
146
148
 
147
149
  if job:
148
150
  sm.link_to_job(screenshot_id, job)
149
- console.print(f"[green]✓ Linked screenshot {screenshot_id} to job {job}[/green]")
151
+ console.print(
152
+ f"[green]✓ Linked screenshot {screenshot_id} to job {job}[/green]"
153
+ )
150
154
 
151
155
 
152
156
  @screenshots.command()
153
- @click.argument('screenshot_id', type=int)
154
- @click.confirmation_option(prompt='Are you sure you want to delete this screenshot?')
157
+ @click.argument("screenshot_id", type=int)
158
+ @click.confirmation_option(prompt="Are you sure you want to delete this screenshot?")
155
159
  def delete(screenshot_id):
156
160
  """Delete a screenshot."""
157
161
  sm = ScreenshotManager()
souleyez/commands/user.py CHANGED
@@ -8,6 +8,7 @@ Commands (admin only):
8
8
  - souleyez user delete <username> - Delete user
9
9
  - souleyez user passwd [username] - Change password
10
10
  """
11
+
11
12
  import click
12
13
  import getpass
13
14
  from rich.console import Console
@@ -45,14 +46,20 @@ def user():
45
46
  @require_admin
46
47
  @click.argument("username")
47
48
  @click.option("--email", "-e", help="User email address")
48
- @click.option("--role", "-r",
49
- type=click.Choice(["admin", "lead", "analyst", "viewer"]),
50
- default="analyst",
51
- help="User role (default: analyst)")
52
- @click.option("--tier", "-t",
53
- type=click.Choice(["FREE", "PRO"]),
54
- default=None,
55
- help="License tier (auto-detects from active license)")
49
+ @click.option(
50
+ "--role",
51
+ "-r",
52
+ type=click.Choice(["admin", "lead", "analyst", "viewer"]),
53
+ default="analyst",
54
+ help="User role (default: analyst)",
55
+ )
56
+ @click.option(
57
+ "--tier",
58
+ "-t",
59
+ type=click.Choice(["FREE", "PRO"]),
60
+ default=None,
61
+ help="License tier (auto-detects from active license)",
62
+ )
56
63
  def user_create(username, email, role, tier):
57
64
  """Create a new user account."""
58
65
  import secrets
@@ -77,7 +84,7 @@ def user_create(username, email, role, tier):
77
84
  email=email,
78
85
  role=Role(role),
79
86
  tier=Tier(tier),
80
- skip_password_validation=True # Generated password is secure
87
+ skip_password_validation=True, # Generated password is secure
81
88
  )
82
89
 
83
90
  if new_user is None:
@@ -86,8 +93,12 @@ def user_create(username, email, role, tier):
86
93
 
87
94
  # Log the action
88
95
  current = get_current_user()
89
- _log_audit("user.created", current.id, current.username,
90
- f"Created user: {username} (role={role}, tier={tier})")
96
+ _log_audit(
97
+ "user.created",
98
+ current.id,
99
+ current.username,
100
+ f"Created user: {username} (role={role}, tier={tier})",
101
+ )
91
102
 
92
103
  # Display credentials in a nice panel
93
104
  tier_display = "💎 PRO" if tier == "PRO" else "FREE"
@@ -101,7 +112,9 @@ def user_create(username, email, role, tier):
101
112
  f"[dim]To change password after login:[/dim]\n"
102
113
  f"[dim] • souleyez user passwd {username}[/dim]"
103
114
  )
104
- console.print(Panel(panel_content, title="🔐 New User Created", border_style="green"))
115
+ console.print(
116
+ Panel(panel_content, title="🔐 New User Created", border_style="green")
117
+ )
105
118
 
106
119
 
107
120
  @user.command("list")
@@ -130,7 +143,11 @@ def user_list(show_all):
130
143
  status = "[green]Active[/green]" if u.is_active else "[red]Disabled[/red]"
131
144
  if u.is_locked:
132
145
  status = "[yellow]Locked[/yellow]"
133
- last_login = u.last_login.strftime('%Y-%m-%d %H:%M') if u.last_login else "[dim]Never[/dim]"
146
+ last_login = (
147
+ u.last_login.strftime("%Y-%m-%d %H:%M")
148
+ if u.last_login
149
+ else "[dim]Never[/dim]"
150
+ )
134
151
 
135
152
  table.add_row(
136
153
  u.username,
@@ -138,7 +155,7 @@ def user_list(show_all):
138
155
  tier_badge,
139
156
  u.email or "[dim]-[/dim]",
140
157
  status,
141
- last_login
158
+ last_login,
142
159
  )
143
160
 
144
161
  console.print(table)
@@ -149,14 +166,17 @@ def user_list(show_all):
149
166
  @require_login
150
167
  @require_admin
151
168
  @click.argument("username")
152
- @click.option("--role", "-r",
153
- type=click.Choice(["admin", "lead", "analyst", "viewer"]),
154
- help="New role")
155
- @click.option("--tier", "-t",
156
- type=click.Choice(["FREE", "PRO"]),
157
- help="New tier")
169
+ @click.option(
170
+ "--role",
171
+ "-r",
172
+ type=click.Choice(["admin", "lead", "analyst", "viewer"]),
173
+ help="New role",
174
+ )
175
+ @click.option("--tier", "-t", type=click.Choice(["FREE", "PRO"]), help="New tier")
158
176
  @click.option("--email", "-e", help="New email")
159
- @click.option("--activate/--deactivate", default=None, help="Activate or deactivate account")
177
+ @click.option(
178
+ "--activate/--deactivate", default=None, help="Activate or deactivate account"
179
+ )
160
180
  def user_update(username, role, tier, email, activate):
161
181
  """Update a user's role, tier, or status."""
162
182
  user_mgr = _get_user_manager()
@@ -178,7 +198,9 @@ def user_update(username, role, tier, email, activate):
178
198
  updates["is_active"] = activate
179
199
 
180
200
  if not updates:
181
- console.print("[yellow]No changes specified. Use --role, --tier, --email, or --activate/--deactivate[/yellow]")
201
+ console.print(
202
+ "[yellow]No changes specified. Use --role, --tier, --email, or --activate/--deactivate[/yellow]"
203
+ )
182
204
  return
183
205
 
184
206
  success, error = user_mgr.update_user(target.id, **updates)
@@ -189,8 +211,12 @@ def user_update(username, role, tier, email, activate):
189
211
 
190
212
  # Log the action
191
213
  current = get_current_user()
192
- _log_audit("user.updated", current.id, current.username,
193
- f"Updated user: {username} ({updates})")
214
+ _log_audit(
215
+ "user.updated",
216
+ current.id,
217
+ current.username,
218
+ f"Updated user: {username} ({updates})",
219
+ )
194
220
 
195
221
  console.print(f"[green]✅ User '{username}' updated successfully![/green]")
196
222
 
@@ -227,8 +253,9 @@ def user_delete(username, force):
227
253
  console.print(f"[red]❌ {error}[/red]")
228
254
  return
229
255
 
230
- _log_audit("user.deleted", current.id, current.username,
231
- f"Deleted user: {username}")
256
+ _log_audit(
257
+ "user.deleted", current.id, current.username, f"Deleted user: {username}"
258
+ )
232
259
 
233
260
  console.print(f"[green]✅ User '{username}' deleted[/green]")
234
261
 
@@ -245,7 +272,9 @@ def user_passwd(username):
245
272
  if username:
246
273
  # Changing another user's password - requires admin
247
274
  if current.role != Role.ADMIN:
248
- console.print("[red]❌ Admin privileges required to change another user's password[/red]")
275
+ console.print(
276
+ "[red]❌ Admin privileges required to change another user's password[/red]"
277
+ )
249
278
  return
250
279
  target = user_mgr.get_user_by_username(username)
251
280
  if target is None:
@@ -283,8 +312,12 @@ def user_passwd(username):
283
312
  console.print(f"[red]❌ {error}[/red]")
284
313
  return
285
314
 
286
- _log_audit("user.password_changed", current.id, current.username,
287
- f"Password changed for: {username}")
315
+ _log_audit(
316
+ "user.password_changed",
317
+ current.id,
318
+ current.username,
319
+ f"Password changed for: {username}",
320
+ )
288
321
 
289
322
  console.print(f"[green]✅ Password changed successfully![/green]")
290
323
 
@@ -315,8 +348,12 @@ def user_upgrade(username, reason):
315
348
  console.print(f"[red]❌ {error}[/red]")
316
349
  return
317
350
 
318
- _log_audit("user.tier_upgraded", current.id, current.username,
319
- f"Upgraded '{username}' to PRO. Reason: {reason}")
351
+ _log_audit(
352
+ "user.tier_upgraded",
353
+ current.id,
354
+ current.username,
355
+ f"Upgraded '{username}' to PRO. Reason: {reason}",
356
+ )
320
357
 
321
358
  console.print(f"[green]✅ User '{username}' upgraded to 💎 PRO[/green]")
322
359
  console.print(f" Reason: {reason}")
@@ -344,7 +381,9 @@ def user_downgrade(username, reason, force):
344
381
 
345
382
  # Confirm downgrade
346
383
  if not force:
347
- console.print(f"\n[yellow]⚠️ This will remove Pro features for '{username}'[/yellow]")
384
+ console.print(
385
+ f"\n[yellow]⚠️ This will remove Pro features for '{username}'[/yellow]"
386
+ )
348
387
  if not click.confirm("Continue?"):
349
388
  console.print("[dim]Cancelled[/dim]")
350
389
  return
@@ -356,8 +395,12 @@ def user_downgrade(username, reason, force):
356
395
  console.print(f"[red]❌ {error}[/red]")
357
396
  return
358
397
 
359
- _log_audit("user.tier_downgraded", current.id, current.username,
360
- f"Downgraded '{username}' to FREE. Reason: {reason}")
398
+ _log_audit(
399
+ "user.tier_downgraded",
400
+ current.id,
401
+ current.username,
402
+ f"Downgraded '{username}' to FREE. Reason: {reason}",
403
+ )
361
404
 
362
405
  console.print(f"[green]✅ User '{username}' downgraded to FREE[/green]")
363
406
  console.print(f" Reason: {reason}")
@@ -371,10 +414,13 @@ def _log_audit(action: str, user_id: str, username: str, details: str = None):
371
414
 
372
415
  try:
373
416
  conn = sqlite3.connect(get_db().db_path)
374
- conn.execute("""
417
+ conn.execute(
418
+ """
375
419
  INSERT INTO audit_log (user_id, username, action, details, timestamp)
376
420
  VALUES (?, ?, ?, ?, ?)
377
- """, (user_id, username, action, details, datetime.now().isoformat()))
421
+ """,
422
+ (user_id, username, action, details, datetime.now().isoformat()),
423
+ )
378
424
  conn.commit()
379
425
  conn.close()
380
426
  except Exception:
souleyez/config.py CHANGED
@@ -28,29 +28,34 @@ CONFIG_PATH = Path.home() / ".souleyez" / "config.json"
28
28
 
29
29
  DEFAULT_CONFIG = {
30
30
  "plugins": {"enabled": [], "disabled": []},
31
- "settings": {"wordlists": None, "proxy": None, "threads": 10, "ollama_model": "llama3.1:8b"},
31
+ "settings": {
32
+ "wordlists": None,
33
+ "proxy": None,
34
+ "threads": 10,
35
+ "ollama_model": "llama3.1:8b",
36
+ },
32
37
  "database": {
33
38
  "path": "~/.souleyez/souleyez.db",
34
39
  "backup_enabled": True,
35
- "backup_interval_hours": 24
40
+ "backup_interval_hours": 24,
36
41
  },
37
42
  "crypto": {
38
43
  "algorithm": "AES-256-GCM",
39
44
  "iterations": 600000,
40
- "key_derivation": "PBKDF2"
45
+ "key_derivation": "PBKDF2",
41
46
  },
42
47
  "logging": {
43
48
  "level": "INFO",
44
49
  "format": "json",
45
50
  "file": "~/.souleyez/souleyez.log",
46
51
  "max_bytes": 10485760,
47
- "backup_count": 5
52
+ "backup_count": 5,
48
53
  },
49
54
  "security": {
50
55
  "session_timeout_minutes": 30,
51
56
  "max_login_attempts": 5,
52
57
  "lockout_duration_minutes": 15,
53
- "min_password_length": 12
58
+ "min_password_length": 12,
54
59
  },
55
60
  "ai": {
56
61
  "provider": "ollama", # "ollama" or "claude"
@@ -65,7 +70,7 @@ DEFAULT_CONFIG = {
65
70
  # AI Chain Advisor settings
66
71
  "chain_mode": "suggest", # "off", "suggest", or "auto"
67
72
  "chain_min_confidence": 0.6, # Minimum confidence for AI recommendations
68
- "chain_max_recommendations": 5 # Max AI suggestions per analysis
73
+ "chain_max_recommendations": 5, # Max AI suggestions per analysis
69
74
  },
70
75
  # MSF RPC Configuration (Pro feature)
71
76
  "msfrpc": {
@@ -78,8 +83,8 @@ DEFAULT_CONFIG = {
78
83
  "timeout": 30, # Connection timeout in seconds
79
84
  "poll_interval": 2, # Seconds between session polls
80
85
  "max_poll_time": 300, # Max time to wait for session (5 min)
81
- "fallback_to_console": True # Use msfconsole if RPC unavailable
82
- }
86
+ "fallback_to_console": True, # Use msfconsole if RPC unavailable
87
+ },
83
88
  }
84
89
 
85
90
  CONFIG_SCHEMA = {
@@ -87,87 +92,86 @@ CONFIG_SCHEMA = {
87
92
  "type": int,
88
93
  "min": 100000,
89
94
  "max": 10000000,
90
- "error": "Iterations must be between 100k and 10M for security"
95
+ "error": "Iterations must be between 100k and 10M for security",
91
96
  },
92
97
  "database.path": {
93
98
  "type": str,
94
- "validator": lambda p: not p.startswith(
95
- ("http://", "https://", "ftp://")
96
- ),
97
- "error": "Database path must be local filesystem"
99
+ "validator": lambda p: not p.startswith(("http://", "https://", "ftp://")),
100
+ "error": "Database path must be local filesystem",
98
101
  },
99
102
  "logging.level": {
100
103
  "type": str,
101
104
  "allowed": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
102
- "error": "Invalid log level"
105
+ "error": "Invalid log level",
103
106
  },
104
107
  "security.max_login_attempts": {
105
108
  "type": int,
106
109
  "min": 1,
107
110
  "max": 10,
108
- "error": "Max login attempts must be 1-10"
111
+ "error": "Max login attempts must be 1-10",
109
112
  },
110
113
  "security.session_timeout_minutes": {
111
114
  "type": int,
112
115
  "min": 5,
113
116
  "max": 1440,
114
- "error": "Session timeout must be 5-1440 minutes"
117
+ "error": "Session timeout must be 5-1440 minutes",
115
118
  },
116
119
  "settings.threads": {
117
120
  "type": int,
118
121
  "min": 1,
119
122
  "max": 100,
120
- "error": "Threads must be 1-100"
123
+ "error": "Threads must be 1-100",
121
124
  },
122
125
  "ai.ollama_mode": {
123
126
  "type": str,
124
127
  "allowed": ["local", "remote"],
125
- "error": "Ollama mode must be 'local' or 'remote'"
128
+ "error": "Ollama mode must be 'local' or 'remote'",
126
129
  },
127
130
  "ai.chain_mode": {
128
131
  "type": str,
129
132
  "allowed": ["off", "suggest", "auto"],
130
- "error": "AI chain mode must be 'off', 'suggest', or 'auto'"
133
+ "error": "AI chain mode must be 'off', 'suggest', or 'auto'",
131
134
  },
132
135
  "ai.chain_min_confidence": {
133
136
  "type": float,
134
137
  "min": 0.0,
135
138
  "max": 1.0,
136
- "error": "AI chain confidence must be 0.0-1.0"
139
+ "error": "AI chain confidence must be 0.0-1.0",
137
140
  },
138
141
  "ai.chain_max_recommendations": {
139
142
  "type": int,
140
143
  "min": 1,
141
144
  "max": 20,
142
- "error": "AI chain max recommendations must be 1-20"
145
+ "error": "AI chain max recommendations must be 1-20",
143
146
  },
144
147
  # MSF RPC validation
145
148
  "msfrpc.port": {
146
149
  "type": int,
147
150
  "min": 1,
148
151
  "max": 65535,
149
- "error": "MSF RPC port must be 1-65535"
152
+ "error": "MSF RPC port must be 1-65535",
150
153
  },
151
154
  "msfrpc.timeout": {
152
155
  "type": int,
153
156
  "min": 5,
154
157
  "max": 120,
155
- "error": "MSF RPC timeout must be 5-120 seconds"
158
+ "error": "MSF RPC timeout must be 5-120 seconds",
156
159
  },
157
160
  "msfrpc.poll_interval": {
158
161
  "type": int,
159
162
  "min": 1,
160
163
  "max": 30,
161
- "error": "Poll interval must be 1-30 seconds"
164
+ "error": "Poll interval must be 1-30 seconds",
162
165
  },
163
166
  "msfrpc.max_poll_time": {
164
167
  "type": int,
165
168
  "min": 30,
166
169
  "max": 600,
167
- "error": "Max poll time must be 30-600 seconds"
168
- }
170
+ "error": "Max poll time must be 30-600 seconds",
171
+ },
169
172
  }
170
173
 
174
+
171
175
  def _ensure_dir():
172
176
  CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
173
177
 
@@ -177,7 +181,7 @@ def _get_nested(data: dict, key: str, default=None):
177
181
  Get nested dict value using dotted notation.
178
182
  Example: _get_nested(cfg, 'database.path') -> cfg['database']['path']
179
183
  """
180
- parts = key.split('.')
184
+ parts = key.split(".")
181
185
  current = data
182
186
  for part in parts:
183
187
  if isinstance(current, dict) and part in current:
@@ -193,7 +197,7 @@ def _set_nested(data: dict, key: str, value):
193
197
  Example: _set_nested(cfg, 'database.path', '/foo')
194
198
  -> cfg['database']['path'] = '/foo'
195
199
  """
196
- parts = key.split('.')
200
+ parts = key.split(".")
197
201
  current = data
198
202
  for part in parts[:-1]:
199
203
  if part not in current:
@@ -237,12 +241,14 @@ def validate_config(cfg: dict) -> tuple[bool, list[str]]:
237
241
  def _merge_with_defaults(cfg: dict) -> dict:
238
242
  """Deep merge user config with defaults."""
239
243
  import copy
244
+
240
245
  merged = copy.deepcopy(DEFAULT_CONFIG)
241
246
 
242
247
  def deep_merge(base, updates):
243
248
  for key, value in updates.items():
244
- is_dict_merge = (key in base and isinstance(base[key], dict)
245
- and isinstance(value, dict))
249
+ is_dict_merge = (
250
+ key in base and isinstance(base[key], dict) and isinstance(value, dict)
251
+ )
246
252
  if is_dict_merge:
247
253
  deep_merge(base[key], value)
248
254
  else:
@@ -252,7 +258,6 @@ def _merge_with_defaults(cfg: dict) -> dict:
252
258
  return merged
253
259
 
254
260
 
255
-
256
261
  def _normalize(data: dict) -> dict:
257
262
  # Accept both new {"plugins":{...}}
258
263
  # and old flat {"enabled":[], "disabled":[]}
@@ -264,7 +269,12 @@ def _normalize(data: dict) -> dict:
264
269
  plugins.setdefault("disabled", [])
265
270
  data.setdefault(
266
271
  "settings",
267
- {"wordlists": None, "proxy": None, "threads": 10, "ollama_model": "llama3.1:8b"}
272
+ {
273
+ "wordlists": None,
274
+ "proxy": None,
275
+ "threads": 10,
276
+ "ollama_model": "llama3.1:8b",
277
+ },
268
278
  )
269
279
  return data
270
280
  # old flat form
@@ -272,11 +282,15 @@ def _normalize(data: dict) -> dict:
272
282
  disabled = data.get("disabled", []) or []
273
283
  return {
274
284
  "plugins": {"enabled": enabled, "disabled": disabled},
275
- "settings": {"wordlists": None, "proxy": None, "threads": 10, "ollama_model": "llama3.1:8b"}
285
+ "settings": {
286
+ "wordlists": None,
287
+ "proxy": None,
288
+ "threads": 10,
289
+ "ollama_model": "llama3.1:8b",
290
+ },
276
291
  }
277
292
 
278
293
 
279
-
280
294
  def read_config() -> dict:
281
295
  """
282
296
  Read and validate config from file.
@@ -292,8 +306,7 @@ def read_config() -> dict:
292
306
  return DEFAULT_CONFIG.copy()
293
307
  except Exception as e:
294
308
  logging.warning(
295
- f"Cannot create config file: {e}. "
296
- "Using defaults in memory."
309
+ f"Cannot create config file: {e}. " "Using defaults in memory."
297
310
  )
298
311
  return DEFAULT_CONFIG.copy()
299
312
 
@@ -312,12 +325,8 @@ def read_config() -> dict:
312
325
  return normalized
313
326
 
314
327
  except json.JSONDecodeError as e:
315
- logging.error(
316
- f"Corrupted config file at {CONFIG_PATH}: {e}"
317
- )
318
- logging.warning(
319
- "Using default config. Fix or delete config file to reset."
320
- )
328
+ logging.error(f"Corrupted config file at {CONFIG_PATH}: {e}")
329
+ logging.warning("Using default config. Fix or delete config file to reset.")
321
330
  return DEFAULT_CONFIG.copy()
322
331
  except Exception as e:
323
332
  logging.error(f"Error reading config: {e}")
@@ -339,7 +348,7 @@ def get(key: str, default=None):
339
348
  get('database.path') -> checks SOULEYEZ_DATABASE_PATH env,
340
349
  then config file, then default
341
350
  """
342
- env_key = "SOULEYEZ_" + key.upper().replace('.', '_')
351
+ env_key = "SOULEYEZ_" + key.upper().replace(".", "_")
343
352
  env_val = os.getenv(env_key)
344
353
  if env_val is not None:
345
354
  return env_val
@@ -352,7 +361,6 @@ def get(key: str, default=None):
352
361
  return _get_nested(DEFAULT_CONFIG, key, default)
353
362
 
354
363
 
355
-
356
364
  def list_plugins_config() -> tuple[list[str], list[str]]:
357
365
  cfg = read_config()
358
366
  e = [x.lower() for x in cfg["plugins"]["enabled"]]