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
@@ -6,6 +6,7 @@ Permission levels:
6
6
  - editor: Can run scans, add findings, but can't delete engagement
7
7
  - viewer: Read-only access to engagement data
8
8
  """
9
+
9
10
  import sqlite3
10
11
  from enum import Enum
11
12
  from typing import Optional, List, Dict, Any
@@ -17,6 +18,7 @@ from souleyez.auth import get_current_user, Role
17
18
 
18
19
  class EngagementPermission(Enum):
19
20
  """Permission levels for engagement access."""
21
+
20
22
  OWNER = "owner"
21
23
  EDITOR = "editor"
22
24
  VIEWER = "viewer"
@@ -25,6 +27,7 @@ class EngagementPermission(Enum):
25
27
  @dataclass
26
28
  class EngagementAccess:
27
29
  """Represents a user's access to an engagement."""
30
+
28
31
  engagement_id: int
29
32
  user_id: str
30
33
  permission_level: EngagementPermission
@@ -48,9 +51,7 @@ class EngagementAccessManager:
48
51
  # =========================================================================
49
52
 
50
53
  def get_user_permission(
51
- self,
52
- engagement_id: int,
53
- user_id: str
54
+ self, engagement_id: int, user_id: str
54
55
  ) -> Optional[EngagementPermission]:
55
56
  """
56
57
  Get user's permission level for an engagement.
@@ -61,23 +62,22 @@ class EngagementAccessManager:
61
62
 
62
63
  # First check if user is the owner
63
64
  row = conn.execute(
64
- "SELECT owner_id FROM engagements WHERE id = ?",
65
- (engagement_id,)
65
+ "SELECT owner_id FROM engagements WHERE id = ?", (engagement_id,)
66
66
  ).fetchone()
67
67
 
68
- if row and row['owner_id'] == user_id:
68
+ if row and row["owner_id"] == user_id:
69
69
  conn.close()
70
70
  return EngagementPermission.OWNER
71
71
 
72
72
  # Check engagement_permissions table
73
73
  row = conn.execute(
74
74
  "SELECT permission_level FROM engagement_permissions WHERE engagement_id = ? AND user_id = ?",
75
- (engagement_id, user_id)
75
+ (engagement_id, user_id),
76
76
  ).fetchone()
77
77
  conn.close()
78
78
 
79
79
  if row:
80
- return EngagementPermission(row['permission_level'])
80
+ return EngagementPermission(row["permission_level"])
81
81
 
82
82
  return None
83
83
 
@@ -105,7 +105,9 @@ class EngagementAccessManager:
105
105
  perm = self.get_user_permission(engagement_id, user_id)
106
106
  return perm == EngagementPermission.OWNER
107
107
 
108
- def can_manage_team(self, engagement_id: int, user_id: str, user_role: Role) -> bool:
108
+ def can_manage_team(
109
+ self, engagement_id: int, user_id: str, user_role: Role
110
+ ) -> bool:
109
111
  """Check if user can add/remove team members (owner only, or admin)."""
110
112
  if user_role == Role.ADMIN:
111
113
  return True
@@ -117,7 +119,9 @@ class EngagementAccessManager:
117
119
  # Engagement Queries (Filtered by Access)
118
120
  # =========================================================================
119
121
 
120
- def get_accessible_engagements(self, user_id: str, user_role: Role) -> List[Dict[str, Any]]:
122
+ def get_accessible_engagements(
123
+ self, user_id: str, user_role: Role
124
+ ) -> List[Dict[str, Any]]:
121
125
  """
122
126
  Get all engagements the user can access.
123
127
 
@@ -127,14 +131,17 @@ class EngagementAccessManager:
127
131
 
128
132
  if user_role == Role.ADMIN:
129
133
  # Admins see everything
130
- rows = conn.execute("""
134
+ rows = conn.execute(
135
+ """
131
136
  SELECT e.*, 'admin' as permission_level
132
137
  FROM engagements e
133
138
  ORDER BY e.created_at DESC
134
- """).fetchall()
139
+ """
140
+ ).fetchall()
135
141
  else:
136
142
  # Non-admins see owned + shared engagements
137
- rows = conn.execute("""
143
+ rows = conn.execute(
144
+ """
138
145
  SELECT e.*,
139
146
  CASE
140
147
  WHEN e.owner_id = ? THEN 'owner'
@@ -147,7 +154,9 @@ class EngagementAccessManager:
147
154
  OR ep.user_id = ?
148
155
  OR e.owner_id IS NULL
149
156
  ORDER BY e.created_at DESC
150
- """, (user_id, user_id, user_id, user_id)).fetchall()
157
+ """,
158
+ (user_id, user_id, user_id, user_id),
159
+ ).fetchall()
151
160
 
152
161
  conn.close()
153
162
  return [dict(row) for row in rows]
@@ -161,7 +170,7 @@ class EngagementAccessManager:
161
170
  engagement_id: int,
162
171
  user_id: str,
163
172
  permission_level: EngagementPermission,
164
- granted_by: str
173
+ granted_by: str,
165
174
  ) -> tuple[bool, str]:
166
175
  """
167
176
  Add a user to an engagement's team.
@@ -174,45 +183,43 @@ class EngagementAccessManager:
174
183
 
175
184
  try:
176
185
  conn = self._get_conn()
177
- conn.execute("""
186
+ conn.execute(
187
+ """
178
188
  INSERT OR REPLACE INTO engagement_permissions
179
189
  (engagement_id, user_id, permission_level, granted_by, granted_at)
180
190
  VALUES (?, ?, ?, ?, ?)
181
- """, (
182
- engagement_id,
183
- user_id,
184
- permission_level.value,
185
- granted_by,
186
- datetime.now().isoformat()
187
- ))
191
+ """,
192
+ (
193
+ engagement_id,
194
+ user_id,
195
+ permission_level.value,
196
+ granted_by,
197
+ datetime.now().isoformat(),
198
+ ),
199
+ )
188
200
  conn.commit()
189
201
  conn.close()
190
202
  return True, ""
191
203
  except Exception as e:
192
204
  return False, str(e)
193
205
 
194
- def remove_team_member(
195
- self,
196
- engagement_id: int,
197
- user_id: str
198
- ) -> tuple[bool, str]:
206
+ def remove_team_member(self, engagement_id: int, user_id: str) -> tuple[bool, str]:
199
207
  """Remove a user from an engagement's team."""
200
208
  try:
201
209
  conn = self._get_conn()
202
210
 
203
211
  # Can't remove the owner via this method
204
212
  row = conn.execute(
205
- "SELECT owner_id FROM engagements WHERE id = ?",
206
- (engagement_id,)
213
+ "SELECT owner_id FROM engagements WHERE id = ?", (engagement_id,)
207
214
  ).fetchone()
208
215
 
209
- if row and row['owner_id'] == user_id:
216
+ if row and row["owner_id"] == user_id:
210
217
  conn.close()
211
218
  return False, "Cannot remove the owner. Transfer ownership first."
212
219
 
213
220
  result = conn.execute(
214
221
  "DELETE FROM engagement_permissions WHERE engagement_id = ? AND user_id = ?",
215
- (engagement_id, user_id)
222
+ (engagement_id, user_id),
216
223
  )
217
224
  conn.commit()
218
225
  conn.close()
@@ -229,23 +236,29 @@ class EngagementAccessManager:
229
236
  conn = self._get_conn()
230
237
 
231
238
  # Get owner
232
- owner_row = conn.execute("""
239
+ owner_row = conn.execute(
240
+ """
233
241
  SELECT u.id, u.username, u.email, 'owner' as permission_level,
234
242
  e.created_at as granted_at, NULL as granted_by
235
243
  FROM engagements e
236
244
  JOIN users u ON e.owner_id = u.id
237
245
  WHERE e.id = ?
238
- """, (engagement_id,)).fetchone()
246
+ """,
247
+ (engagement_id,),
248
+ ).fetchone()
239
249
 
240
250
  # Get other team members
241
- member_rows = conn.execute("""
251
+ member_rows = conn.execute(
252
+ """
242
253
  SELECT u.id, u.username, u.email, ep.permission_level,
243
254
  ep.granted_at, ep.granted_by
244
255
  FROM engagement_permissions ep
245
256
  JOIN users u ON ep.user_id = u.id
246
257
  WHERE ep.engagement_id = ?
247
258
  ORDER BY ep.granted_at
248
- """, (engagement_id,)).fetchall()
259
+ """,
260
+ (engagement_id,),
261
+ ).fetchall()
249
262
 
250
263
  conn.close()
251
264
 
@@ -257,10 +270,7 @@ class EngagementAccessManager:
257
270
  return members
258
271
 
259
272
  def transfer_ownership(
260
- self,
261
- engagement_id: int,
262
- new_owner_id: str,
263
- transferred_by: str
273
+ self, engagement_id: int, new_owner_id: str, transferred_by: str
264
274
  ) -> tuple[bool, str]:
265
275
  """Transfer engagement ownership to another user."""
266
276
  try:
@@ -268,35 +278,42 @@ class EngagementAccessManager:
268
278
 
269
279
  # Get current owner
270
280
  row = conn.execute(
271
- "SELECT owner_id FROM engagements WHERE id = ?",
272
- (engagement_id,)
281
+ "SELECT owner_id FROM engagements WHERE id = ?", (engagement_id,)
273
282
  ).fetchone()
274
283
 
275
284
  if not row:
276
285
  conn.close()
277
286
  return False, "Engagement not found"
278
287
 
279
- old_owner_id = row['owner_id']
288
+ old_owner_id = row["owner_id"]
280
289
 
281
290
  # Update owner
282
291
  conn.execute(
283
292
  "UPDATE engagements SET owner_id = ? WHERE id = ?",
284
- (new_owner_id, engagement_id)
293
+ (new_owner_id, engagement_id),
285
294
  )
286
295
 
287
296
  # Remove new owner from permissions table (they're now owner)
288
297
  conn.execute(
289
298
  "DELETE FROM engagement_permissions WHERE engagement_id = ? AND user_id = ?",
290
- (engagement_id, new_owner_id)
299
+ (engagement_id, new_owner_id),
291
300
  )
292
301
 
293
302
  # Add old owner as editor (so they don't lose access)
294
303
  if old_owner_id:
295
- conn.execute("""
304
+ conn.execute(
305
+ """
296
306
  INSERT OR REPLACE INTO engagement_permissions
297
307
  (engagement_id, user_id, permission_level, granted_by, granted_at)
298
308
  VALUES (?, ?, 'editor', ?, ?)
299
- """, (engagement_id, old_owner_id, transferred_by, datetime.now().isoformat()))
309
+ """,
310
+ (
311
+ engagement_id,
312
+ old_owner_id,
313
+ transferred_by,
314
+ datetime.now().isoformat(),
315
+ ),
316
+ )
300
317
 
301
318
  conn.commit()
302
319
  conn.close()
@@ -318,7 +335,7 @@ class EngagementAccessManager:
318
335
  conn = self._get_conn()
319
336
  result = conn.execute(
320
337
  "UPDATE engagements SET owner_id = ? WHERE owner_id IS NULL",
321
- (admin_user_id,)
338
+ (admin_user_id,),
322
339
  )
323
340
  conn.commit()
324
341
  count = result.rowcount
@@ -4,6 +4,7 @@ souleyez.auth.permissions - Role-based access control definitions
4
4
  Roles: Admin, Lead, Analyst, Viewer
5
5
  Tiers: FREE, PRO (for licensing)
6
6
  """
7
+
7
8
  from enum import Enum, auto
8
9
  from typing import Set, Optional
9
10
  from functools import wraps
@@ -11,6 +12,7 @@ from functools import wraps
11
12
 
12
13
  class Role(Enum):
13
14
  """User roles with hierarchical permissions."""
15
+
14
16
  ADMIN = "admin"
15
17
  LEAD = "lead"
16
18
  ANALYST = "analyst"
@@ -19,12 +21,14 @@ class Role(Enum):
19
21
 
20
22
  class Tier(Enum):
21
23
  """Licensing tiers."""
24
+
22
25
  FREE = "FREE"
23
26
  PRO = "PRO"
24
27
 
25
28
 
26
29
  class Permission(Enum):
27
30
  """Individual permissions that can be checked."""
31
+
28
32
  # User management
29
33
  USER_CREATE = auto()
30
34
  USER_UPDATE = auto()
@@ -86,7 +90,6 @@ class Permission(Enum):
86
90
  # Role -> Permissions mapping
87
91
  ROLE_PERMISSIONS: dict[Role, Set[Permission]] = {
88
92
  Role.ADMIN: set(Permission), # All permissions
89
-
90
93
  Role.LEAD: {
91
94
  Permission.ENGAGEMENT_CREATE,
92
95
  Permission.ENGAGEMENT_UPDATE,
@@ -116,7 +119,6 @@ ROLE_PERMISSIONS: dict[Role, Set[Permission]] = {
116
119
  Permission.AUDIT_VIEW,
117
120
  Permission.USER_LIST,
118
121
  },
119
-
120
122
  Role.ANALYST: {
121
123
  Permission.ENGAGEMENT_VIEW,
122
124
  Permission.SCAN_RUN,
@@ -136,7 +138,6 @@ ROLE_PERMISSIONS: dict[Role, Set[Permission]] = {
136
138
  Permission.AUTOMATION_MANAGE,
137
139
  Permission.MSF_INTEGRATION,
138
140
  },
139
-
140
141
  Role.VIEWER: {
141
142
  Permission.ENGAGEMENT_VIEW,
142
143
  Permission.FINDING_VIEW,
@@ -195,11 +196,13 @@ class PermissionChecker:
195
196
 
196
197
  def requires_permission(permission: Permission):
197
198
  """Decorator to require a permission for a function."""
199
+
198
200
  def decorator(func):
199
201
  @wraps(func)
200
202
  def wrapper(*args, **kwargs):
201
203
  # Get current user from context (implementation depends on how session is managed)
202
204
  from souleyez.auth import get_current_user
205
+
203
206
  user = get_current_user()
204
207
 
205
208
  if user is None:
@@ -212,15 +215,19 @@ def requires_permission(permission: Permission):
212
215
  raise PermissionError(f"Permission denied: {permission.name}")
213
216
 
214
217
  return func(*args, **kwargs)
218
+
215
219
  return wrapper
220
+
216
221
  return decorator
217
222
 
218
223
 
219
224
  def requires_pro(func):
220
225
  """Decorator to require Pro tier for a function."""
226
+
221
227
  @wraps(func)
222
228
  def wrapper(*args, **kwargs):
223
229
  from souleyez.auth import get_current_user
230
+
224
231
  user = get_current_user()
225
232
 
226
233
  if user is None:
@@ -230,6 +237,7 @@ def requires_pro(func):
230
237
  raise PermissionError("Pro license required")
231
238
 
232
239
  return func(*args, **kwargs)
240
+
233
241
  return wrapper
234
242
 
235
243
 
@@ -241,6 +249,7 @@ def requires_role(min_role: Role):
241
249
  @wraps(func)
242
250
  def wrapper(*args, **kwargs):
243
251
  from souleyez.auth import get_current_user
252
+
244
253
  user = get_current_user()
245
254
 
246
255
  if user is None:
@@ -253,5 +262,7 @@ def requires_role(min_role: Role):
253
262
  raise PermissionError(f"Requires {min_role.value} role or higher")
254
263
 
255
264
  return func(*args, **kwargs)
265
+
256
266
  return wrapper
267
+
257
268
  return decorator
@@ -6,6 +6,7 @@ Handles:
6
6
  - Session storage and cleanup
7
7
  - Current user context
8
8
  """
9
+
9
10
  import sqlite3
10
11
  import secrets
11
12
  import hashlib
@@ -28,6 +29,7 @@ SESSION_FILE_NAME = "session.json"
28
29
  @dataclass
29
30
  class Session:
30
31
  """Active user session."""
32
+
31
33
  id: str
32
34
  user_id: str
33
35
  token: str # Only available at creation time
@@ -78,7 +80,7 @@ class SessionManager:
78
80
  ip_address: Optional[str] = None,
79
81
  user_agent: Optional[str] = None,
80
82
  duration_hours: int = DEFAULT_SESSION_HOURS,
81
- vault_unlocked_at: Optional[datetime] = None
83
+ vault_unlocked_at: Optional[datetime] = None,
82
84
  ) -> Session:
83
85
  """
84
86
  Create a new session for a user.
@@ -101,18 +103,21 @@ class SessionManager:
101
103
  expires_at = now + timedelta(hours=duration_hours)
102
104
 
103
105
  conn = self._get_conn()
104
- conn.execute("""
106
+ conn.execute(
107
+ """
105
108
  INSERT INTO sessions (id, user_id, token_hash, expires_at, created_at, ip_address, user_agent)
106
109
  VALUES (?, ?, ?, ?, ?, ?, ?)
107
- """, (
108
- session_id,
109
- user.id,
110
- token_hash,
111
- expires_at.isoformat(),
112
- now.isoformat(),
113
- ip_address,
114
- user_agent
115
- ))
110
+ """,
111
+ (
112
+ session_id,
113
+ user.id,
114
+ token_hash,
115
+ expires_at.isoformat(),
116
+ now.isoformat(),
117
+ ip_address,
118
+ user_agent,
119
+ ),
120
+ )
116
121
  conn.commit()
117
122
  conn.close()
118
123
 
@@ -124,7 +129,7 @@ class SessionManager:
124
129
  created_at=now,
125
130
  ip_address=ip_address,
126
131
  user_agent=user_agent,
127
- vault_unlocked_at=vault_unlocked_at or now
132
+ vault_unlocked_at=vault_unlocked_at or now,
128
133
  )
129
134
 
130
135
  # Save session to local file
@@ -142,12 +147,15 @@ class SessionManager:
142
147
  token_hash = self._hash_token(token)
143
148
 
144
149
  conn = self._get_conn()
145
- row = conn.execute("""
150
+ row = conn.execute(
151
+ """
146
152
  SELECT s.*, u.*
147
153
  FROM sessions s
148
154
  JOIN users u ON s.user_id = u.id
149
155
  WHERE s.token_hash = ? AND s.expires_at > ?
150
- """, (token_hash, datetime.now().isoformat())).fetchone()
156
+ """,
157
+ (token_hash, datetime.now().isoformat()),
158
+ ).fetchone()
151
159
  conn.close()
152
160
 
153
161
  if row is None:
@@ -155,7 +163,7 @@ class SessionManager:
155
163
 
156
164
  # Build user from row
157
165
  user_manager = UserManager(self.db_path)
158
- return user_manager.get_user_by_id(row['user_id'])
166
+ return user_manager.get_user_by_id(row["user_id"])
159
167
 
160
168
  def invalidate_session(self, session_id: str) -> bool:
161
169
  """Invalidate (logout) a specific session."""
@@ -187,8 +195,7 @@ class SessionManager:
187
195
  """Remove all expired sessions from the database."""
188
196
  conn = self._get_conn()
189
197
  result = conn.execute(
190
- "DELETE FROM sessions WHERE expires_at < ?",
191
- (datetime.now().isoformat(),)
198
+ "DELETE FROM sessions WHERE expires_at < ?", (datetime.now().isoformat(),)
192
199
  )
193
200
  conn.commit()
194
201
  count = result.rowcount
@@ -202,63 +209,70 @@ class SessionManager:
202
209
  def is_vault_session_valid(self) -> bool:
203
210
  """
204
211
  Check if vault unlock is still valid (30 min inactivity).
205
-
212
+
206
213
  Returns:
207
214
  True if vault session is still valid
208
215
  """
209
- if self._current_session is None or self._current_session.vault_unlocked_at is None:
216
+ if (
217
+ self._current_session is None
218
+ or self._current_session.vault_unlocked_at is None
219
+ ):
210
220
  return False
211
-
221
+
212
222
  elapsed = datetime.now() - self._current_session.vault_unlocked_at
213
223
  return elapsed < timedelta(minutes=self._vault_timeout_minutes)
214
224
 
215
225
  def require_full_reauth(self) -> bool:
216
226
  """
217
227
  Check if both layers need re-authentication.
218
-
228
+
219
229
  Returns:
220
230
  True if full re-auth (vault + user) is required
221
231
  """
222
232
  if self._current_session is None:
223
233
  return True
224
-
234
+
225
235
  # Check if vault session has timed out
226
236
  if not self.is_vault_session_valid():
227
237
  return True
228
-
238
+
229
239
  # Check if user session has expired
230
240
  if datetime.now() >= self._current_session.expires_at:
231
241
  return True
232
-
242
+
233
243
  return False
234
244
 
235
245
  def apply_cross_layer_delay(self, vault_failures: int):
236
246
  """
237
247
  Apply delay based on vault failures before allowing user login.
238
-
248
+
239
249
  Args:
240
250
  vault_failures: Number of recent vault unlock failures
241
251
  """
242
252
  import time
243
253
  import click
244
-
254
+
245
255
  if vault_failures >= 2:
246
256
  delay = 30 * (vault_failures - 1) # 30s for 2, 60s for 3+
247
-
257
+
248
258
  # Avoid re-applying same delay multiple times
249
259
  if self._cross_layer_delay_applied_at:
250
- since_last = (datetime.now() - self._cross_layer_delay_applied_at).total_seconds()
260
+ since_last = (
261
+ datetime.now() - self._cross_layer_delay_applied_at
262
+ ).total_seconds()
251
263
  if since_last < delay:
252
264
  return
253
-
254
- click.echo(f"\n⚠️ Security delay: {delay} seconds (vault failures detected)")
255
-
265
+
266
+ click.echo(
267
+ f"\n⚠️ Security delay: {delay} seconds (vault failures detected)"
268
+ )
269
+
256
270
  # Show countdown
257
271
  for remaining in range(delay, 0, -1):
258
272
  click.echo(f"\r Continuing in {remaining}s... ", nl=False)
259
273
  time.sleep(1)
260
274
  click.echo("\r Continuing... ")
261
-
275
+
262
276
  self._cross_layer_delay_applied_at = datetime.now()
263
277
 
264
278
  def _save_session_to_file(self, session: Session):
@@ -270,7 +284,11 @@ class SessionManager:
270
284
  "token": session.token,
271
285
  "expires_at": session.expires_at.isoformat(),
272
286
  "user_id": session.user_id,
273
- "vault_unlocked_at": session.vault_unlocked_at.isoformat() if session.vault_unlocked_at else None
287
+ "vault_unlocked_at": (
288
+ session.vault_unlocked_at.isoformat()
289
+ if session.vault_unlocked_at
290
+ else None
291
+ ),
274
292
  }
275
293
 
276
294
  self.session_file.write_text(json.dumps(data, indent=2))
@@ -285,7 +303,7 @@ class SessionManager:
285
303
  try:
286
304
  data = json.loads(self.session_file.read_text())
287
305
  # Check expiration
288
- expires_at = datetime.fromisoformat(data['expires_at'])
306
+ expires_at = datetime.fromisoformat(data["expires_at"])
289
307
  if expires_at < datetime.now():
290
308
  self._clear_session_file()
291
309
  return None
@@ -317,7 +335,7 @@ class SessionManager:
317
335
  if session_data is None:
318
336
  return None
319
337
 
320
- user = self.validate_token(session_data['token'])
338
+ user = self.validate_token(session_data["token"])
321
339
  if user is None:
322
340
  self._clear_session_file()
323
341
  return None
@@ -337,9 +355,8 @@ class SessionManager:
337
355
  """Log out the current user."""
338
356
  session_data = self._load_session_from_file()
339
357
  if session_data:
340
- self.invalidate_session(session_data['session_id'])
358
+ self.invalidate_session(session_data["session_id"])
341
359
 
342
360
  self._clear_session_file()
343
361
  self._current_user = None
344
362
  return True
345
-