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.

Potentially problematic release.


This version of souleyez might be problematic. Click here for more details.

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
@@ -20,12 +20,14 @@ logger = get_logger(__name__)
20
20
 
21
21
  class ScopeViolationError(Exception):
22
22
  """Raised when a target is out of scope and enforcement is 'block'."""
23
+
23
24
  pass
24
25
 
25
26
 
26
27
  @dataclass
27
28
  class ScopeValidationResult:
28
29
  """Result of scope validation check."""
30
+
29
31
  is_in_scope: bool
30
32
  matched_entry: Optional[Dict[str, Any]]
31
33
  reason: str
@@ -71,15 +73,15 @@ class ScopeValidator:
71
73
  FROM engagement_scope
72
74
  WHERE engagement_id = ?
73
75
  ORDER BY is_excluded ASC, scope_type ASC""",
74
- (self.engagement_id,)
76
+ (self.engagement_id,),
75
77
  )
76
78
  self._scope_cache = entries
77
79
  return entries
78
80
  except Exception as e:
79
- logger.warning("Failed to get scope entries", extra={
80
- "engagement_id": self.engagement_id,
81
- "error": str(e)
82
- })
81
+ logger.warning(
82
+ "Failed to get scope entries",
83
+ extra={"engagement_id": self.engagement_id, "error": str(e)},
84
+ )
83
85
  return []
84
86
 
85
87
  def has_scope_defined(self) -> bool:
@@ -91,7 +93,7 @@ class ScopeValidator:
91
93
  """
92
94
  entries = self.get_scope_entries()
93
95
  # Only count inclusion entries (not exclusions)
94
- inclusions = [e for e in entries if not e.get('is_excluded')]
96
+ inclusions = [e for e in entries if not e.get("is_excluded")]
95
97
  return len(inclusions) > 0
96
98
 
97
99
  def get_enforcement_mode(self) -> str:
@@ -107,17 +109,17 @@ class ScopeValidator:
107
109
  try:
108
110
  result = self.db.execute_one(
109
111
  "SELECT scope_enforcement FROM engagements WHERE id = ?",
110
- (self.engagement_id,)
112
+ (self.engagement_id,),
111
113
  )
112
- mode = result.get('scope_enforcement', 'off') if result else 'off'
113
- self._enforcement_cache = mode or 'off'
114
+ mode = result.get("scope_enforcement", "off") if result else "off"
115
+ self._enforcement_cache = mode or "off"
114
116
  return self._enforcement_cache
115
117
  except Exception as e:
116
- logger.warning("Failed to get enforcement mode", extra={
117
- "engagement_id": self.engagement_id,
118
- "error": str(e)
119
- })
120
- return 'off'
118
+ logger.warning(
119
+ "Failed to get enforcement mode",
120
+ extra={"engagement_id": self.engagement_id, "error": str(e)},
121
+ )
122
+ return "off"
121
123
 
122
124
  def validate_target(self, target: str) -> ScopeValidationResult:
123
125
  """
@@ -128,6 +130,7 @@ class ScopeValidator:
128
130
  - IP addresses
129
131
  - CIDR ranges
130
132
  - Hostnames/domains
133
+ - Space-separated multiple targets (validates each, all must be in scope)
131
134
 
132
135
  Args:
133
136
  target: The target to validate (IP, URL, hostname, etc.)
@@ -140,18 +143,48 @@ class ScopeValidator:
140
143
  is_in_scope=False,
141
144
  matched_entry=None,
142
145
  reason="Empty target",
143
- scope_type=None
146
+ scope_type=None,
144
147
  )
145
148
 
146
149
  target = target.strip()
147
150
 
151
+ # Handle space-separated multiple targets
152
+ # Check if this looks like multiple targets (space-separated, not a URL with spaces)
153
+ if " " in target and not target.startswith(("http://", "https://")):
154
+ targets = target.split()
155
+ # Validate each target - all must be in scope
156
+ for t in targets:
157
+ result = self._validate_single_target(t)
158
+ if not result.is_in_scope:
159
+ return result # Return first out-of-scope result
160
+ # All targets are in scope
161
+ return ScopeValidationResult(
162
+ is_in_scope=True,
163
+ matched_entry=None,
164
+ reason=f"All {len(targets)} targets are in scope",
165
+ scope_type=None,
166
+ )
167
+
168
+ return self._validate_single_target(target)
169
+
170
+ def _validate_single_target(self, target: str) -> ScopeValidationResult:
171
+ """
172
+ Validate a single target against the engagement scope.
173
+
174
+ Args:
175
+ target: Single target to validate (IP, URL, hostname, etc.)
176
+
177
+ Returns:
178
+ ScopeValidationResult with validation outcome
179
+ """
180
+
148
181
  # If no scope defined, everything is in scope (permissive default)
149
182
  if not self.has_scope_defined():
150
183
  return ScopeValidationResult(
151
184
  is_in_scope=True,
152
185
  matched_entry=None,
153
186
  reason="No scope defined (permissive)",
154
- scope_type=None
187
+ scope_type=None,
155
188
  )
156
189
 
157
190
  # Determine target type and extract relevant part
@@ -162,26 +195,26 @@ class ScopeValidator:
162
195
 
163
196
  # First check exclusions (deny rules take precedence)
164
197
  for entry in entries:
165
- if not entry.get('is_excluded'):
198
+ if not entry.get("is_excluded"):
166
199
  continue
167
200
  if self._matches_entry(normalized, target_type, entry):
168
201
  return ScopeValidationResult(
169
202
  is_in_scope=False,
170
203
  matched_entry=entry,
171
204
  reason=f"Explicitly excluded by scope entry: {entry['value']}",
172
- scope_type=entry['scope_type']
205
+ scope_type=entry["scope_type"],
173
206
  )
174
207
 
175
208
  # Then check inclusions
176
209
  for entry in entries:
177
- if entry.get('is_excluded'):
210
+ if entry.get("is_excluded"):
178
211
  continue
179
212
  if self._matches_entry(normalized, target_type, entry):
180
213
  return ScopeValidationResult(
181
214
  is_in_scope=True,
182
215
  matched_entry=entry,
183
216
  reason=f"Matched scope entry: {entry['value']}",
184
- scope_type=entry['scope_type']
217
+ scope_type=entry["scope_type"],
185
218
  )
186
219
 
187
220
  # No match found - out of scope
@@ -189,7 +222,7 @@ class ScopeValidator:
189
222
  is_in_scope=False,
190
223
  matched_entry=None,
191
224
  reason=f"Target '{target}' does not match any scope entry",
192
- scope_type=None
225
+ scope_type=None,
193
226
  )
194
227
 
195
228
  def validate_ip(self, ip: str) -> ScopeValidationResult:
@@ -228,8 +261,13 @@ class ScopeValidator:
228
261
  """
229
262
  return self.validate_target(url)
230
263
 
231
- def log_validation(self, target: str, result: ScopeValidationResult,
232
- action: str, job_id: int = None) -> None:
264
+ def log_validation(
265
+ self,
266
+ target: str,
267
+ result: ScopeValidationResult,
268
+ action: str,
269
+ job_id: int = None,
270
+ ) -> None:
233
271
  """
234
272
  Log a validation result to the audit trail.
235
273
 
@@ -241,31 +279,40 @@ class ScopeValidator:
241
279
  """
242
280
  try:
243
281
  from souleyez.auth import get_current_user
282
+
244
283
  user = get_current_user()
245
284
  user_id = user.id if user else None
246
285
  except Exception:
247
286
  user_id = None
248
287
 
249
- validation_result = 'in_scope' if result.is_in_scope else 'out_of_scope'
288
+ validation_result = "in_scope" if result.is_in_scope else "out_of_scope"
250
289
  if not self.has_scope_defined():
251
- validation_result = 'no_scope_defined'
290
+ validation_result = "no_scope_defined"
252
291
 
253
292
  try:
254
- self.db.insert('scope_validation_log', {
255
- 'engagement_id': self.engagement_id,
256
- 'job_id': job_id,
257
- 'target': target,
258
- 'validation_result': validation_result,
259
- 'action_taken': action,
260
- 'matched_scope_id': result.matched_entry.get('id') if result.matched_entry else None,
261
- 'user_id': user_id
262
- })
293
+ self.db.insert(
294
+ "scope_validation_log",
295
+ {
296
+ "engagement_id": self.engagement_id,
297
+ "job_id": job_id,
298
+ "target": target,
299
+ "validation_result": validation_result,
300
+ "action_taken": action,
301
+ "matched_scope_id": (
302
+ result.matched_entry.get("id") if result.matched_entry else None
303
+ ),
304
+ "user_id": user_id,
305
+ },
306
+ )
263
307
  except Exception as e:
264
- logger.warning("Failed to log scope validation", extra={
265
- "engagement_id": self.engagement_id,
266
- "target": target,
267
- "error": str(e)
268
- })
308
+ logger.warning(
309
+ "Failed to log scope validation",
310
+ extra={
311
+ "engagement_id": self.engagement_id,
312
+ "target": target,
313
+ "error": str(e),
314
+ },
315
+ )
269
316
 
270
317
  def _parse_target(self, target: str) -> tuple:
271
318
  """
@@ -276,35 +323,37 @@ class ScopeValidator:
276
323
  target_type is one of: 'ip', 'cidr', 'domain', 'url'
277
324
  """
278
325
  # Check if URL
279
- if target.startswith(('http://', 'https://')):
326
+ if target.startswith(("http://", "https://")):
280
327
  parsed = urlparse(target)
281
- host = parsed.netloc.split(':')[0] # Remove port
328
+ host = parsed.netloc.split(":")[0] # Remove port
282
329
  # Check if host part is IP
283
330
  try:
284
331
  ipaddress.ip_address(host)
285
- return ('ip', host)
332
+ return ("ip", host)
286
333
  except ValueError:
287
- return ('domain', host.lower())
334
+ return ("domain", host.lower())
288
335
 
289
336
  # Check if IP address
290
337
  try:
291
338
  ipaddress.ip_address(target)
292
- return ('ip', target)
339
+ return ("ip", target)
293
340
  except ValueError:
294
341
  pass
295
342
 
296
343
  # Check if CIDR notation
297
- if '/' in target:
344
+ if "/" in target:
298
345
  try:
299
346
  ipaddress.ip_network(target, strict=False)
300
- return ('cidr', target)
347
+ return ("cidr", target)
301
348
  except ValueError:
302
349
  pass
303
350
 
304
351
  # Assume domain/hostname
305
- return ('domain', target.lower())
352
+ return ("domain", target.lower())
306
353
 
307
- def _matches_entry(self, target: str, target_type: str, entry: Dict[str, Any]) -> bool:
354
+ def _matches_entry(
355
+ self, target: str, target_type: str, entry: Dict[str, Any]
356
+ ) -> bool:
308
357
  """
309
358
  Check if a target matches a scope entry.
310
359
 
@@ -316,46 +365,48 @@ class ScopeValidator:
316
365
  Returns:
317
366
  True if matches, False otherwise
318
367
  """
319
- entry_type = entry['scope_type']
320
- entry_value = entry['value']
368
+ entry_type = entry["scope_type"]
369
+ entry_value = entry["value"]
321
370
 
322
371
  # IP target
323
- if target_type == 'ip':
324
- if entry_type == 'cidr':
372
+ if target_type == "ip":
373
+ if entry_type == "cidr":
325
374
  return self._ip_in_cidr(target, entry_value)
326
- elif entry_type == 'hostname':
375
+ elif entry_type == "hostname":
327
376
  # Exact IP match
328
377
  return target == entry_value
329
- elif entry_type == 'domain':
378
+ elif entry_type == "domain":
330
379
  # IP doesn't match domain patterns
331
380
  return False
332
- elif entry_type == 'url':
381
+ elif entry_type == "url":
333
382
  # Extract host from URL entry
334
383
  try:
335
384
  parsed = urlparse(entry_value)
336
- return target == parsed.netloc.split(':')[0]
385
+ return target == parsed.netloc.split(":")[0]
337
386
  except Exception:
338
387
  return False
339
388
 
340
389
  # CIDR target (less common - check containment)
341
- elif target_type == 'cidr':
342
- if entry_type == 'cidr':
390
+ elif target_type == "cidr":
391
+ if entry_type == "cidr":
343
392
  return self._cidr_overlaps(target, entry_value)
344
393
  return False
345
394
 
346
395
  # Domain target
347
- elif target_type == 'domain':
348
- if entry_type == 'domain':
396
+ elif target_type == "domain":
397
+ if entry_type == "domain":
349
398
  return self._domain_matches(target, entry_value)
350
- elif entry_type == 'hostname':
399
+ elif entry_type == "hostname":
351
400
  # Exact hostname match
352
401
  return target.lower() == entry_value.lower()
353
- elif entry_type == 'url':
402
+ elif entry_type == "url":
354
403
  # Extract host from URL entry
355
404
  try:
356
405
  parsed = urlparse(entry_value)
357
- entry_host = parsed.netloc.split(':')[0].lower()
358
- return target == entry_host or self._domain_matches(target, entry_host)
406
+ entry_host = parsed.netloc.split(":")[0].lower()
407
+ return target == entry_host or self._domain_matches(
408
+ target, entry_host
409
+ )
359
410
  except Exception:
360
411
  return False
361
412
  return False
@@ -400,14 +451,14 @@ class ScopeValidator:
400
451
  target = target.lower()
401
452
 
402
453
  # Handle wildcard patterns
403
- if pattern.startswith('*.'):
454
+ if pattern.startswith("*."):
404
455
  # Remove the *. prefix for suffix matching
405
456
  suffix = pattern[2:]
406
457
  # Match exact suffix or .suffix
407
- return target == suffix or target.endswith('.' + suffix)
458
+ return target == suffix or target.endswith("." + suffix)
408
459
 
409
460
  # Handle wildcards in other positions using fnmatch
410
- if '*' in pattern or '?' in pattern:
461
+ if "*" in pattern or "?" in pattern:
411
462
  return fnmatch.fnmatch(target, pattern)
412
463
 
413
464
  # Exact match
@@ -428,8 +479,14 @@ class ScopeManager:
428
479
  def __init__(self):
429
480
  self.db = get_db()
430
481
 
431
- def add_scope(self, engagement_id: int, scope_type: str, value: str,
432
- is_excluded: bool = False, description: str = None) -> int:
482
+ def add_scope(
483
+ self,
484
+ engagement_id: int,
485
+ scope_type: str,
486
+ value: str,
487
+ is_excluded: bool = False,
488
+ description: str = None,
489
+ ) -> int:
433
490
  """
434
491
  Add a scope entry for an engagement.
435
492
 
@@ -446,28 +503,34 @@ class ScopeManager:
446
503
  Raises:
447
504
  ValueError: If scope_type or value is invalid
448
505
  """
449
- valid_types = ['cidr', 'domain', 'url', 'hostname']
506
+ valid_types = ["cidr", "domain", "url", "hostname"]
450
507
  if scope_type not in valid_types:
451
- raise ValueError(f"Invalid scope_type: {scope_type}. Must be one of: {valid_types}")
508
+ raise ValueError(
509
+ f"Invalid scope_type: {scope_type}. Must be one of: {valid_types}"
510
+ )
452
511
 
453
512
  # Validate the value based on type
454
513
  self._validate_scope_value(scope_type, value)
455
514
 
456
515
  try:
457
516
  from souleyez.auth import get_current_user
517
+
458
518
  user = get_current_user()
459
519
  added_by = user.id if user else None
460
520
  except Exception:
461
521
  added_by = None
462
522
 
463
- return self.db.insert('engagement_scope', {
464
- 'engagement_id': engagement_id,
465
- 'scope_type': scope_type,
466
- 'value': value,
467
- 'is_excluded': is_excluded,
468
- 'description': description,
469
- 'added_by': added_by
470
- })
523
+ return self.db.insert(
524
+ "engagement_scope",
525
+ {
526
+ "engagement_id": engagement_id,
527
+ "scope_type": scope_type,
528
+ "value": value,
529
+ "is_excluded": is_excluded,
530
+ "description": description,
531
+ "added_by": added_by,
532
+ },
533
+ )
471
534
 
472
535
  def remove_scope(self, scope_id: int) -> bool:
473
536
  """
@@ -480,16 +543,13 @@ class ScopeManager:
480
543
  True if removed, False if not found
481
544
  """
482
545
  try:
483
- self.db.execute(
484
- "DELETE FROM engagement_scope WHERE id = ?",
485
- (scope_id,)
486
- )
546
+ self.db.execute("DELETE FROM engagement_scope WHERE id = ?", (scope_id,))
487
547
  return True
488
548
  except Exception as e:
489
- logger.warning("Failed to remove scope entry", extra={
490
- "scope_id": scope_id,
491
- "error": str(e)
492
- })
549
+ logger.warning(
550
+ "Failed to remove scope entry",
551
+ extra={"scope_id": scope_id, "error": str(e)},
552
+ )
493
553
  return False
494
554
 
495
555
  def list_scope(self, engagement_id: int) -> List[Dict[str, Any]]:
@@ -507,7 +567,7 @@ class ScopeManager:
507
567
  FROM engagement_scope
508
568
  WHERE engagement_id = ?
509
569
  ORDER BY is_excluded ASC, scope_type ASC, value ASC""",
510
- (engagement_id,)
570
+ (engagement_id,),
511
571
  )
512
572
 
513
573
  def set_enforcement(self, engagement_id: int, mode: str) -> bool:
@@ -524,25 +584,28 @@ class ScopeManager:
524
584
  Raises:
525
585
  ValueError: If mode is invalid
526
586
  """
527
- valid_modes = ['off', 'warn', 'block']
587
+ valid_modes = ["off", "warn", "block"]
528
588
  if mode not in valid_modes:
529
- raise ValueError(f"Invalid enforcement mode: {mode}. Must be one of: {valid_modes}")
589
+ raise ValueError(
590
+ f"Invalid enforcement mode: {mode}. Must be one of: {valid_modes}"
591
+ )
530
592
 
531
593
  try:
532
594
  self.db.execute(
533
595
  "UPDATE engagements SET scope_enforcement = ? WHERE id = ?",
534
- (mode, engagement_id)
596
+ (mode, engagement_id),
535
597
  )
536
598
  return True
537
599
  except Exception as e:
538
- logger.warning("Failed to set enforcement mode", extra={
539
- "engagement_id": engagement_id,
540
- "mode": mode,
541
- "error": str(e)
542
- })
600
+ logger.warning(
601
+ "Failed to set enforcement mode",
602
+ extra={"engagement_id": engagement_id, "mode": mode, "error": str(e)},
603
+ )
543
604
  return False
544
605
 
545
- def get_validation_log(self, engagement_id: int, limit: int = 100) -> List[Dict[str, Any]]:
606
+ def get_validation_log(
607
+ self, engagement_id: int, limit: int = 100
608
+ ) -> List[Dict[str, Any]]:
546
609
  """
547
610
  Get scope validation log for an engagement.
548
611
 
@@ -560,7 +623,7 @@ class ScopeManager:
560
623
  WHERE engagement_id = ?
561
624
  ORDER BY created_at DESC
562
625
  LIMIT ?""",
563
- (engagement_id, limit)
626
+ (engagement_id, limit),
564
627
  )
565
628
 
566
629
  def _validate_scope_value(self, scope_type: str, value: str) -> None:
@@ -575,13 +638,13 @@ class ScopeManager:
575
638
 
576
639
  value = value.strip()
577
640
 
578
- if scope_type == 'cidr':
641
+ if scope_type == "cidr":
579
642
  try:
580
643
  ipaddress.ip_network(value, strict=False)
581
644
  except ValueError:
582
645
  raise ValueError(f"Invalid CIDR notation: {value}")
583
646
 
584
- elif scope_type == 'hostname':
647
+ elif scope_type == "hostname":
585
648
  # Basic hostname validation (can be IP or hostname)
586
649
  try:
587
650
  ipaddress.ip_address(value)
@@ -589,23 +652,29 @@ class ScopeManager:
589
652
  # Not an IP, validate as hostname
590
653
  if len(value) > 253:
591
654
  raise ValueError("Hostname too long (max 253 characters)")
592
- if not re.match(r'^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$', value):
655
+ if not re.match(
656
+ r"^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$",
657
+ value,
658
+ ):
593
659
  raise ValueError(f"Invalid hostname format: {value}")
594
660
 
595
- elif scope_type == 'domain':
661
+ elif scope_type == "domain":
596
662
  # Allow wildcards like *.example.com
597
- if value.startswith('*.'):
663
+ if value.startswith("*."):
598
664
  domain_part = value[2:]
599
665
  else:
600
666
  domain_part = value
601
667
 
602
668
  if len(domain_part) > 253:
603
669
  raise ValueError("Domain too long (max 253 characters)")
604
- if not re.match(r'^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$', domain_part):
670
+ if not re.match(
671
+ r"^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$",
672
+ domain_part,
673
+ ):
605
674
  raise ValueError(f"Invalid domain format: {value}")
606
675
 
607
- elif scope_type == 'url':
608
- if not value.startswith(('http://', 'https://')):
676
+ elif scope_type == "url":
677
+ if not value.startswith(("http://", "https://")):
609
678
  raise ValueError("URL must start with http:// or https://")
610
679
  try:
611
680
  parsed = urlparse(value)