souleyez 2.43.28__py3-none-any.whl → 2.43.32__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 (356) 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 +9592 -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 +1238 -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 +2198 -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 +288 -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/whois_handler.py +277 -0
  126. souleyez/handlers/wpscan_handler.py +554 -0
  127. souleyez/history.py +32 -16
  128. souleyez/importers/msf_importer.py +106 -75
  129. souleyez/importers/smart_importer.py +208 -147
  130. souleyez/integrations/siem/__init__.py +10 -10
  131. souleyez/integrations/siem/base.py +17 -18
  132. souleyez/integrations/siem/elastic.py +108 -122
  133. souleyez/integrations/siem/factory.py +207 -80
  134. souleyez/integrations/siem/googlesecops.py +146 -154
  135. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  136. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  137. souleyez/integrations/siem/sentinel.py +107 -109
  138. souleyez/integrations/siem/splunk.py +246 -212
  139. souleyez/integrations/siem/wazuh.py +65 -71
  140. souleyez/integrations/wazuh/__init__.py +5 -5
  141. souleyez/integrations/wazuh/client.py +70 -93
  142. souleyez/integrations/wazuh/config.py +85 -57
  143. souleyez/integrations/wazuh/host_mapper.py +28 -36
  144. souleyez/integrations/wazuh/sync.py +78 -68
  145. souleyez/intelligence/__init__.py +4 -5
  146. souleyez/intelligence/correlation_analyzer.py +309 -295
  147. souleyez/intelligence/exploit_knowledge.py +661 -623
  148. souleyez/intelligence/exploit_suggestions.py +159 -139
  149. souleyez/intelligence/gap_analyzer.py +132 -97
  150. souleyez/intelligence/gap_detector.py +251 -214
  151. souleyez/intelligence/sensitive_tables.py +266 -129
  152. souleyez/intelligence/service_parser.py +137 -123
  153. souleyez/intelligence/surface_analyzer.py +407 -268
  154. souleyez/intelligence/target_parser.py +159 -162
  155. souleyez/licensing/__init__.py +6 -6
  156. souleyez/licensing/validator.py +17 -19
  157. souleyez/log_config.py +79 -54
  158. souleyez/main.py +1505 -687
  159. souleyez/migrations/fix_job_counter.py +16 -14
  160. souleyez/parsers/bloodhound_parser.py +41 -39
  161. souleyez/parsers/crackmapexec_parser.py +178 -111
  162. souleyez/parsers/dalfox_parser.py +72 -77
  163. souleyez/parsers/dnsrecon_parser.py +103 -91
  164. souleyez/parsers/enum4linux_parser.py +183 -153
  165. souleyez/parsers/ffuf_parser.py +29 -25
  166. souleyez/parsers/gobuster_parser.py +301 -41
  167. souleyez/parsers/hashcat_parser.py +324 -79
  168. souleyez/parsers/http_fingerprint_parser.py +350 -103
  169. souleyez/parsers/hydra_parser.py +131 -111
  170. souleyez/parsers/impacket_parser.py +231 -178
  171. souleyez/parsers/john_parser.py +98 -86
  172. souleyez/parsers/katana_parser.py +316 -0
  173. souleyez/parsers/msf_parser.py +943 -498
  174. souleyez/parsers/nikto_parser.py +346 -65
  175. souleyez/parsers/nmap_parser.py +262 -174
  176. souleyez/parsers/nuclei_parser.py +40 -44
  177. souleyez/parsers/responder_parser.py +26 -26
  178. souleyez/parsers/searchsploit_parser.py +74 -74
  179. souleyez/parsers/service_explorer_parser.py +279 -0
  180. souleyez/parsers/smbmap_parser.py +180 -124
  181. souleyez/parsers/sqlmap_parser.py +434 -308
  182. souleyez/parsers/theharvester_parser.py +75 -57
  183. souleyez/parsers/whois_parser.py +135 -94
  184. souleyez/parsers/wpscan_parser.py +278 -190
  185. souleyez/plugins/afp.py +44 -36
  186. souleyez/plugins/afp_brute.py +114 -46
  187. souleyez/plugins/ard.py +48 -37
  188. souleyez/plugins/bloodhound.py +95 -61
  189. souleyez/plugins/certipy.py +303 -0
  190. souleyez/plugins/crackmapexec.py +186 -85
  191. souleyez/plugins/dalfox.py +120 -59
  192. souleyez/plugins/dns_hijack.py +146 -41
  193. souleyez/plugins/dnsrecon.py +97 -61
  194. souleyez/plugins/enum4linux.py +91 -66
  195. souleyez/plugins/evil_winrm.py +291 -0
  196. souleyez/plugins/ffuf.py +166 -90
  197. souleyez/plugins/firmware_extract.py +133 -29
  198. souleyez/plugins/gobuster.py +387 -190
  199. souleyez/plugins/gpp_extract.py +393 -0
  200. souleyez/plugins/hashcat.py +100 -73
  201. souleyez/plugins/http_fingerprint.py +854 -267
  202. souleyez/plugins/hydra.py +566 -200
  203. souleyez/plugins/impacket_getnpusers.py +117 -69
  204. souleyez/plugins/impacket_psexec.py +84 -64
  205. souleyez/plugins/impacket_secretsdump.py +103 -69
  206. souleyez/plugins/impacket_smbclient.py +89 -75
  207. souleyez/plugins/john.py +86 -69
  208. souleyez/plugins/katana.py +313 -0
  209. souleyez/plugins/kerbrute.py +237 -0
  210. souleyez/plugins/lfi_extract.py +541 -0
  211. souleyez/plugins/macos_ssh.py +117 -48
  212. souleyez/plugins/mdns.py +35 -30
  213. souleyez/plugins/msf_auxiliary.py +253 -130
  214. souleyez/plugins/msf_exploit.py +239 -161
  215. souleyez/plugins/nikto.py +134 -78
  216. souleyez/plugins/nmap.py +275 -91
  217. souleyez/plugins/nuclei.py +180 -89
  218. souleyez/plugins/nxc.py +285 -0
  219. souleyez/plugins/plugin_base.py +35 -36
  220. souleyez/plugins/plugin_template.py +13 -5
  221. souleyez/plugins/rdp_sec_check.py +130 -0
  222. souleyez/plugins/responder.py +112 -71
  223. souleyez/plugins/router_http_brute.py +76 -65
  224. souleyez/plugins/router_ssh_brute.py +118 -41
  225. souleyez/plugins/router_telnet_brute.py +124 -42
  226. souleyez/plugins/routersploit.py +91 -59
  227. souleyez/plugins/routersploit_exploit.py +77 -55
  228. souleyez/plugins/searchsploit.py +91 -77
  229. souleyez/plugins/service_explorer.py +1160 -0
  230. souleyez/plugins/smbmap.py +122 -72
  231. souleyez/plugins/smbpasswd.py +215 -0
  232. souleyez/plugins/sqlmap.py +301 -113
  233. souleyez/plugins/theharvester.py +127 -75
  234. souleyez/plugins/tr069.py +79 -57
  235. souleyez/plugins/upnp.py +65 -47
  236. souleyez/plugins/upnp_abuse.py +73 -55
  237. souleyez/plugins/vnc_access.py +129 -42
  238. souleyez/plugins/vnc_brute.py +109 -38
  239. souleyez/plugins/whois.py +77 -58
  240. souleyez/plugins/wpscan.py +173 -69
  241. souleyez/reporting/__init__.py +2 -1
  242. souleyez/reporting/attack_chain.py +411 -346
  243. souleyez/reporting/charts.py +436 -501
  244. souleyez/reporting/compliance_mappings.py +334 -201
  245. souleyez/reporting/detection_report.py +126 -125
  246. souleyez/reporting/formatters.py +828 -591
  247. souleyez/reporting/generator.py +386 -302
  248. souleyez/reporting/metrics.py +72 -75
  249. souleyez/scanner.py +35 -29
  250. souleyez/security/__init__.py +37 -11
  251. souleyez/security/scope_validator.py +175 -106
  252. souleyez/security/validation.py +223 -149
  253. souleyez/security.py +22 -6
  254. souleyez/storage/credentials.py +247 -186
  255. souleyez/storage/crypto.py +296 -129
  256. souleyez/storage/database.py +73 -50
  257. souleyez/storage/db.py +58 -36
  258. souleyez/storage/deliverable_evidence.py +177 -128
  259. souleyez/storage/deliverable_exporter.py +282 -246
  260. souleyez/storage/deliverable_templates.py +134 -116
  261. souleyez/storage/deliverables.py +135 -130
  262. souleyez/storage/engagements.py +109 -56
  263. souleyez/storage/evidence.py +181 -152
  264. souleyez/storage/execution_log.py +31 -17
  265. souleyez/storage/exploit_attempts.py +93 -57
  266. souleyez/storage/exploits.py +67 -36
  267. souleyez/storage/findings.py +48 -61
  268. souleyez/storage/hosts.py +176 -144
  269. souleyez/storage/migrate_to_engagements.py +43 -19
  270. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  271. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  272. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  273. souleyez/storage/migrations/_005_screenshots.py +13 -5
  274. souleyez/storage/migrations/_006_deliverables.py +13 -5
  275. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  276. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  277. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  278. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  279. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  280. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  281. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  282. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  283. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  284. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  285. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  286. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  287. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  288. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  289. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  290. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  291. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  292. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  293. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  294. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  295. souleyez/storage/migrations/__init__.py +26 -26
  296. souleyez/storage/migrations/migration_manager.py +19 -19
  297. souleyez/storage/msf_sessions.py +100 -65
  298. souleyez/storage/osint.py +17 -24
  299. souleyez/storage/recommendation_engine.py +269 -235
  300. souleyez/storage/screenshots.py +33 -32
  301. souleyez/storage/smb_shares.py +136 -92
  302. souleyez/storage/sqlmap_data.py +183 -128
  303. souleyez/storage/team_collaboration.py +135 -141
  304. souleyez/storage/timeline_tracker.py +122 -94
  305. souleyez/storage/wazuh_vulns.py +64 -66
  306. souleyez/storage/web_paths.py +33 -37
  307. souleyez/testing/credential_tester.py +221 -205
  308. souleyez/ui/__init__.py +1 -1
  309. souleyez/ui/ai_quotes.py +12 -12
  310. souleyez/ui/attack_surface.py +2439 -1516
  311. souleyez/ui/chain_rules_view.py +914 -382
  312. souleyez/ui/correlation_view.py +312 -230
  313. souleyez/ui/dashboard.py +2382 -1130
  314. souleyez/ui/deliverables_view.py +148 -62
  315. souleyez/ui/design_system.py +13 -13
  316. souleyez/ui/errors.py +49 -49
  317. souleyez/ui/evidence_linking_view.py +284 -179
  318. souleyez/ui/evidence_vault.py +393 -285
  319. souleyez/ui/exploit_suggestions_view.py +555 -349
  320. souleyez/ui/export_view.py +100 -66
  321. souleyez/ui/gap_analysis_view.py +315 -171
  322. souleyez/ui/help_system.py +105 -97
  323. souleyez/ui/intelligence_view.py +436 -293
  324. souleyez/ui/interactive.py +23142 -10430
  325. souleyez/ui/interactive_selector.py +75 -68
  326. souleyez/ui/log_formatter.py +47 -39
  327. souleyez/ui/menu_components.py +22 -13
  328. souleyez/ui/msf_auxiliary_menu.py +184 -133
  329. souleyez/ui/pending_chains_view.py +336 -172
  330. souleyez/ui/progress_indicators.py +5 -3
  331. souleyez/ui/recommendations_view.py +195 -137
  332. souleyez/ui/rule_builder.py +343 -225
  333. souleyez/ui/setup_wizard.py +678 -284
  334. souleyez/ui/shortcuts.py +217 -165
  335. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  336. souleyez/ui/splunk_vulns_view.py +139 -86
  337. souleyez/ui/team_dashboard.py +498 -335
  338. souleyez/ui/template_selector.py +196 -105
  339. souleyez/ui/terminal.py +6 -6
  340. souleyez/ui/timeline_view.py +198 -127
  341. souleyez/ui/tool_setup.py +264 -164
  342. souleyez/ui/tutorial.py +202 -72
  343. souleyez/ui/tutorial_state.py +40 -40
  344. souleyez/ui/wazuh_vulns_view.py +235 -141
  345. souleyez/ui/wordlist_browser.py +260 -107
  346. souleyez/ui.py +464 -312
  347. souleyez/utils/tool_checker.py +427 -367
  348. souleyez/utils.py +33 -29
  349. souleyez/wordlists.py +134 -167
  350. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/METADATA +1 -1
  351. souleyez-2.43.32.dist-info/RECORD +441 -0
  352. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/WHEEL +1 -1
  353. souleyez-2.43.28.dist-info/RECORD +0 -379
  354. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/entry_points.txt +0 -0
  355. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/licenses/LICENSE +0 -0
  356. {souleyez-2.43.28.dist-info → souleyez-2.43.32.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)