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
@@ -4,6 +4,7 @@ Network utilities for security validation.
4
4
  Provides functions for detecting VM host (gateway) and validating
5
5
  that Ollama connections only go to trusted destinations.
6
6
  """
7
+
7
8
  import socket
8
9
  import subprocess
9
10
  import re
@@ -23,14 +24,14 @@ def get_default_gateway() -> Optional[str]:
23
24
  try:
24
25
  # Try using 'ip route' (Linux)
25
26
  result = subprocess.run(
26
- ['ip', 'route', 'show', 'default'],
27
+ ["ip", "route", "show", "default"],
27
28
  capture_output=True,
28
29
  text=True,
29
- timeout=5
30
+ timeout=5,
30
31
  )
31
32
  if result.returncode == 0:
32
33
  # Parse: "default via 10.0.0.1 dev eth0 ..."
33
- match = re.search(r'default via (\d+\.\d+\.\d+\.\d+)', result.stdout)
34
+ match = re.search(r"default via (\d+\.\d+\.\d+\.\d+)", result.stdout)
34
35
  if match:
35
36
  return match.group(1)
36
37
  except (subprocess.TimeoutExpired, FileNotFoundError):
@@ -39,14 +40,13 @@ def get_default_gateway() -> Optional[str]:
39
40
  try:
40
41
  # Fallback: 'route -n' (older Linux)
41
42
  result = subprocess.run(
42
- ['route', '-n'],
43
- capture_output=True,
44
- text=True,
45
- timeout=5
43
+ ["route", "-n"], capture_output=True, text=True, timeout=5
46
44
  )
47
45
  if result.returncode == 0:
48
- for line in result.stdout.split('\n'):
49
- if line.startswith('0.0.0.0'): # nosec B104 - parsing route output, not binding
46
+ for line in result.stdout.split("\n"):
47
+ if line.startswith(
48
+ "0.0.0.0"
49
+ ): # nosec B104 - parsing route output, not binding
50
50
  parts = line.split()
51
51
  if len(parts) >= 2:
52
52
  return parts[1]
@@ -56,8 +56,9 @@ def get_default_gateway() -> Optional[str]:
56
56
  try:
57
57
  # Fallback: netifaces if available
58
58
  import netifaces
59
+
59
60
  gateways = netifaces.gateways()
60
- default = gateways.get('default', {})
61
+ default = gateways.get("default", {})
61
62
  if netifaces.AF_INET in default:
62
63
  return default[netifaces.AF_INET][0]
63
64
  except ImportError:
@@ -87,7 +88,7 @@ def is_localhost(host: str) -> bool:
87
88
  """Check if host is localhost."""
88
89
  if not host:
89
90
  return False
90
- return host in ('localhost', '127.0.0.1', '::1')
91
+ return host in ("localhost", "127.0.0.1", "::1")
91
92
 
92
93
 
93
94
  def is_private_ip(ip: str) -> bool:
@@ -109,7 +110,7 @@ def is_private_ip(ip: str) -> bool:
109
110
  return False
110
111
 
111
112
  try:
112
- parts = ip.split('.')
113
+ parts = ip.split(".")
113
114
  if len(parts) != 4:
114
115
  return False
115
116
 
@@ -173,7 +174,13 @@ def get_ollama_host_info() -> dict:
173
174
  Dict with allowed host info
174
175
  """
175
176
  return {
176
- 'allowed_hosts': ['localhost', '127.0.0.1', '10.x.x.x', '172.16-31.x.x', '192.168.x.x'],
177
- 'description': 'Localhost and private network IPs (RFC 1918)',
178
- 'blocked': 'Public internet IPs and hostnames'
177
+ "allowed_hosts": [
178
+ "localhost",
179
+ "127.0.0.1",
180
+ "10.x.x.x",
181
+ "172.16-31.x.x",
182
+ "192.168.x.x",
183
+ ],
184
+ "description": "Localhost and private network IPs (RFC 1918)",
185
+ "blocked": "Public internet IPs and hostnames",
179
186
  }
@@ -33,37 +33,40 @@ class ParserHandler:
33
33
  def _register_parsers(self):
34
34
  """Register all available parsers."""
35
35
  parser_modules = {
36
- 'nmap': 'souleyez.parsers.nmap_parser',
37
- 'enum4linux': 'souleyez.parsers.enum4linux_parser',
38
- 'gobuster': 'souleyez.parsers.gobuster_parser',
39
- 'sqlmap': 'souleyez.parsers.sqlmap_parser',
40
- 'smbmap': 'souleyez.parsers.smbmap_parser',
41
- 'theharvester': 'souleyez.parsers.theharvester_parser',
42
- 'msf': 'souleyez.parsers.msf_parser',
43
- 'whois': 'souleyez.parsers.whois_parser',
44
-
45
- 'wpscan': 'souleyez.parsers.wpscan_parser',
46
- 'hydra': 'souleyez.parsers.hydra_parser',
47
- 'dnsrecon': 'souleyez.parsers.dnsrecon_parser',
48
- 'nikto': 'souleyez.parsers.nikto_parser',
49
- 'dalfox': 'souleyez.parsers.dalfox_parser',
36
+ "nmap": "souleyez.parsers.nmap_parser",
37
+ "enum4linux": "souleyez.parsers.enum4linux_parser",
38
+ "gobuster": "souleyez.parsers.gobuster_parser",
39
+ "sqlmap": "souleyez.parsers.sqlmap_parser",
40
+ "smbmap": "souleyez.parsers.smbmap_parser",
41
+ "theharvester": "souleyez.parsers.theharvester_parser",
42
+ "msf": "souleyez.parsers.msf_parser",
43
+ "whois": "souleyez.parsers.whois_parser",
44
+ "wpscan": "souleyez.parsers.wpscan_parser",
45
+ "hydra": "souleyez.parsers.hydra_parser",
46
+ "dnsrecon": "souleyez.parsers.dnsrecon_parser",
47
+ "nikto": "souleyez.parsers.nikto_parser",
48
+ "dalfox": "souleyez.parsers.dalfox_parser",
50
49
  }
51
50
 
52
51
  for tool, module_path in parser_modules.items():
53
52
  try:
54
- module = __import__(module_path, fromlist=[''])
53
+ module = __import__(module_path, fromlist=[""])
55
54
  parse_func_name = f"parse_{tool}_output"
56
55
  if hasattr(module, parse_func_name):
57
56
  self.parsers[tool] = getattr(module, parse_func_name)
58
57
  logger.debug(f"Registered parser for {tool}")
59
58
  else:
60
- logger.warning(f"Parser module {module_path} missing {parse_func_name}")
59
+ logger.warning(
60
+ f"Parser module {module_path} missing {parse_func_name}"
61
+ )
61
62
  except ImportError as e:
62
63
  logger.warning(f"Could not import parser for {tool}: {e}")
63
64
  except Exception as e:
64
65
  logger.error(f"Error registering parser for {tool}: {e}")
65
66
 
66
- def parse(self, tool: str, output: str, target: str = "", **kwargs) -> Optional[Dict[str, Any]]:
67
+ def parse(
68
+ self, tool: str, output: str, target: str = "", **kwargs
69
+ ) -> Optional[Dict[str, Any]]:
67
70
  """
68
71
  Parse tool output with error handling.
69
72
 
@@ -92,13 +95,15 @@ class ParserHandler:
92
95
  logger.error(f"Parser error for {tool}: {e}", exc_info=True)
93
96
  # Return basic structure with error information
94
97
  return {
95
- 'error': str(e),
96
- 'tool': tool,
97
- 'target': target,
98
- 'raw_output': output[:500] # First 500 chars for debugging
98
+ "error": str(e),
99
+ "tool": tool,
100
+ "target": target,
101
+ "raw_output": output[:500], # First 500 chars for debugging
99
102
  }
100
103
 
101
- def parse_file(self, tool: str, log_path: str, target: str = "", **kwargs) -> Optional[Dict[str, Any]]:
104
+ def parse_file(
105
+ self, tool: str, log_path: str, target: str = "", **kwargs
106
+ ) -> Optional[Dict[str, Any]]:
102
107
  """
103
108
  Parse tool output from file with error handling.
104
109
 
@@ -117,7 +122,7 @@ class ParserHandler:
117
122
  logger.error(f"Log file does not exist: {log_path}")
118
123
  return None
119
124
 
120
- with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
125
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
121
126
  output = f.read()
122
127
 
123
128
  return self.parse(tool, output, target, **kwargs)
@@ -148,13 +153,17 @@ def get_parser_handler() -> ParserHandler:
148
153
 
149
154
 
150
155
  # Convenience functions
151
- def parse_output(tool: str, output: str, target: str = "", **kwargs) -> Optional[Dict[str, Any]]:
156
+ def parse_output(
157
+ tool: str, output: str, target: str = "", **kwargs
158
+ ) -> Optional[Dict[str, Any]]:
152
159
  """Parse tool output using the global parser handler."""
153
160
  handler = get_parser_handler()
154
161
  return handler.parse(tool, output, target, **kwargs)
155
162
 
156
163
 
157
- def parse_file(tool: str, log_path: str, target: str = "", **kwargs) -> Optional[Dict[str, Any]]:
164
+ def parse_file(
165
+ tool: str, log_path: str, target: str = "", **kwargs
166
+ ) -> Optional[Dict[str, Any]]:
158
167
  """Parse tool output file using the global parser handler."""
159
168
  handler = get_parser_handler()
160
169
  return handler.parse_file(tool, log_path, target, **kwargs)
@@ -22,10 +22,10 @@ _lock = threading.RLock()
22
22
 
23
23
 
24
24
  # Chain status constants
25
- CHAIN_PENDING = 'pending' # Awaiting user decision
26
- CHAIN_APPROVED = 'approved' # User approved, ready to execute
27
- CHAIN_REJECTED = 'rejected' # User rejected, will not execute
28
- CHAIN_EXECUTED = 'executed' # Approved and job created
25
+ CHAIN_PENDING = "pending" # Awaiting user decision
26
+ CHAIN_APPROVED = "approved" # User approved, ready to execute
27
+ CHAIN_REJECTED = "rejected" # User rejected, will not execute
28
+ CHAIN_EXECUTED = "executed" # Approved and job created
29
29
 
30
30
 
31
31
  def _ensure_dirs():
@@ -48,7 +48,9 @@ def _read_chains() -> List[Dict[str, Any]]:
48
48
  def _write_chains(chains: List[Dict[str, Any]]):
49
49
  """Write chains to storage atomically."""
50
50
  _ensure_dirs()
51
- tmp = tempfile.NamedTemporaryFile("w", delete=False, dir=CHAINS_DIR, encoding="utf-8")
51
+ tmp = tempfile.NamedTemporaryFile(
52
+ "w", delete=False, dir=CHAINS_DIR, encoding="utf-8"
53
+ )
52
54
  try:
53
55
  json.dump(chains, tmp, indent=2, ensure_ascii=False)
54
56
  tmp.flush()
@@ -69,7 +71,7 @@ def _next_chain_id(chains: List[Dict[str, Any]]) -> int:
69
71
 
70
72
  try:
71
73
  if os.path.exists(counter_file):
72
- with open(counter_file, 'r') as f:
74
+ with open(counter_file, "r") as f:
73
75
  next_id = int(f.read().strip())
74
76
  else:
75
77
  maxid = 0
@@ -78,7 +80,7 @@ def _next_chain_id(chains: List[Dict[str, Any]]) -> int:
78
80
  maxid = c["id"]
79
81
  next_id = maxid + 1
80
82
 
81
- with open(counter_file, 'w') as f:
83
+ with open(counter_file, "w") as f:
82
84
  f.write(str(next_id + 1))
83
85
 
84
86
  return next_id
@@ -98,7 +100,7 @@ def add_pending_chain(
98
100
  args: List[str],
99
101
  priority: int,
100
102
  engagement_id: Optional[int] = None,
101
- metadata: Optional[Dict[str, Any]] = None
103
+ metadata: Optional[Dict[str, Any]] = None,
102
104
  ) -> int:
103
105
  """
104
106
  Add a chain to the pending approval queue.
@@ -122,20 +124,20 @@ def add_pending_chain(
122
124
  now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
123
125
 
124
126
  chain = {
125
- 'id': chain_id,
126
- 'parent_job_id': parent_job_id,
127
- 'rule_description': rule_description,
128
- 'tool': tool,
129
- 'target': target,
130
- 'args': args or [],
131
- 'priority': priority,
132
- 'status': CHAIN_PENDING,
133
- 'created_at': now,
134
- 'decided_at': None,
135
- 'executed_at': None,
136
- 'job_id': None, # Set when executed
137
- 'engagement_id': engagement_id,
138
- 'metadata': metadata or {}
127
+ "id": chain_id,
128
+ "parent_job_id": parent_job_id,
129
+ "rule_description": rule_description,
130
+ "tool": tool,
131
+ "target": target,
132
+ "args": args or [],
133
+ "priority": priority,
134
+ "status": CHAIN_PENDING,
135
+ "created_at": now,
136
+ "decided_at": None,
137
+ "executed_at": None,
138
+ "job_id": None, # Set when executed
139
+ "engagement_id": engagement_id,
140
+ "metadata": metadata or {},
139
141
  }
140
142
 
141
143
  chains.append(chain)
@@ -148,7 +150,7 @@ def list_pending_chains(
148
150
  status: Optional[str] = None,
149
151
  engagement_id: Optional[int] = None,
150
152
  limit: int = 100,
151
- offset: int = 0
153
+ offset: int = 0,
152
154
  ) -> List[Dict[str, Any]]:
153
155
  """
154
156
  List chains, optionally filtered by status and engagement.
@@ -166,21 +168,21 @@ def list_pending_chains(
166
168
 
167
169
  # Apply filters
168
170
  if status:
169
- chains = [c for c in chains if c.get('status') == status]
171
+ chains = [c for c in chains if c.get("status") == status]
170
172
  if engagement_id is not None:
171
- chains = [c for c in chains if c.get('engagement_id') == engagement_id]
173
+ chains = [c for c in chains if c.get("engagement_id") == engagement_id]
172
174
 
173
175
  # Sort by priority (desc) then created_at (asc)
174
- chains.sort(key=lambda c: (-c.get('priority', 5), c.get('created_at', '')))
176
+ chains.sort(key=lambda c: (-c.get("priority", 5), c.get("created_at", "")))
175
177
 
176
- return chains[offset:offset + limit]
178
+ return chains[offset : offset + limit]
177
179
 
178
180
 
179
181
  def get_pending_chain(chain_id: int) -> Optional[Dict[str, Any]]:
180
182
  """Get a specific chain by ID."""
181
183
  chains = _read_chains()
182
184
  for c in chains:
183
- if c.get('id') == chain_id:
185
+ if c.get("id") == chain_id:
184
186
  return c
185
187
  return None
186
188
 
@@ -195,11 +197,11 @@ def approve_chain(chain_id: int) -> bool:
195
197
  with _lock:
196
198
  chains = _read_chains()
197
199
  for c in chains:
198
- if c.get('id') == chain_id:
199
- if c.get('status') != CHAIN_PENDING:
200
+ if c.get("id") == chain_id:
201
+ if c.get("status") != CHAIN_PENDING:
200
202
  return False # Already decided
201
- c['status'] = CHAIN_APPROVED
202
- c['decided_at'] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
203
+ c["status"] = CHAIN_APPROVED
204
+ c["decided_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
203
205
  _write_chains(chains)
204
206
  return True
205
207
  return False
@@ -215,11 +217,11 @@ def reject_chain(chain_id: int) -> bool:
215
217
  with _lock:
216
218
  chains = _read_chains()
217
219
  for c in chains:
218
- if c.get('id') == chain_id:
219
- if c.get('status') != CHAIN_PENDING:
220
+ if c.get("id") == chain_id:
221
+ if c.get("status") != CHAIN_PENDING:
220
222
  return False
221
- c['status'] = CHAIN_REJECTED
222
- c['decided_at'] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
223
+ c["status"] = CHAIN_REJECTED
224
+ c["decided_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
223
225
  _write_chains(chains)
224
226
  return True
225
227
  return False
@@ -241,10 +243,10 @@ def approve_all_pending(engagement_id: Optional[int] = None) -> int:
241
243
  approved = 0
242
244
 
243
245
  for c in chains:
244
- if c.get('status') == CHAIN_PENDING:
245
- if engagement_id is None or c.get('engagement_id') == engagement_id:
246
- c['status'] = CHAIN_APPROVED
247
- c['decided_at'] = now
246
+ if c.get("status") == CHAIN_PENDING:
247
+ if engagement_id is None or c.get("engagement_id") == engagement_id:
248
+ c["status"] = CHAIN_APPROVED
249
+ c["decided_at"] = now
248
250
  approved += 1
249
251
 
250
252
  if approved > 0:
@@ -269,10 +271,10 @@ def reject_all_pending(engagement_id: Optional[int] = None) -> int:
269
271
  rejected = 0
270
272
 
271
273
  for c in chains:
272
- if c.get('status') == CHAIN_PENDING:
273
- if engagement_id is None or c.get('engagement_id') == engagement_id:
274
- c['status'] = CHAIN_REJECTED
275
- c['decided_at'] = now
274
+ if c.get("status") == CHAIN_PENDING:
275
+ if engagement_id is None or c.get("engagement_id") == engagement_id:
276
+ c["status"] = CHAIN_REJECTED
277
+ c["decided_at"] = now
276
278
  rejected += 1
277
279
 
278
280
  if rejected > 0:
@@ -295,16 +297,18 @@ def mark_chain_executed(chain_id: int, job_id: int) -> bool:
295
297
  with _lock:
296
298
  chains = _read_chains()
297
299
  for c in chains:
298
- if c.get('id') == chain_id:
299
- c['status'] = CHAIN_EXECUTED
300
- c['executed_at'] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
301
- c['job_id'] = job_id
300
+ if c.get("id") == chain_id:
301
+ c["status"] = CHAIN_EXECUTED
302
+ c["executed_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
303
+ c["job_id"] = job_id
302
304
  _write_chains(chains)
303
305
  return True
304
306
  return False
305
307
 
306
308
 
307
- def get_approved_chains(engagement_id: Optional[int] = None, limit: int = 50) -> List[Dict[str, Any]]:
309
+ def get_approved_chains(
310
+ engagement_id: Optional[int] = None, limit: int = 50
311
+ ) -> List[Dict[str, Any]]:
308
312
  """
309
313
  Get approved chains ready for execution.
310
314
 
@@ -315,7 +319,9 @@ def get_approved_chains(engagement_id: Optional[int] = None, limit: int = 50) ->
315
319
  Returns:
316
320
  List of approved chains, sorted by priority
317
321
  """
318
- return list_pending_chains(status=CHAIN_APPROVED, engagement_id=engagement_id, limit=limit)
322
+ return list_pending_chains(
323
+ status=CHAIN_APPROVED, engagement_id=engagement_id, limit=limit
324
+ )
319
325
 
320
326
 
321
327
  def purge_old_chains(days: int = 7) -> int:
@@ -337,10 +343,11 @@ def purge_old_chains(days: int = 7) -> int:
337
343
 
338
344
  original_count = len(chains)
339
345
  chains = [
340
- c for c in chains
341
- if c.get('status') == CHAIN_PENDING or # Keep pending
342
- c.get('status') == CHAIN_APPROVED or # Keep approved (not yet executed)
343
- c.get('created_at', '') > cutoff_str # Keep recent
346
+ c
347
+ for c in chains
348
+ if c.get("status") == CHAIN_PENDING # Keep pending
349
+ or c.get("status") == CHAIN_APPROVED # Keep approved (not yet executed)
350
+ or c.get("created_at", "") > cutoff_str # Keep recent
344
351
  ]
345
352
 
346
353
  purged = original_count - len(chains)
@@ -353,9 +360,9 @@ def purge_old_chains(days: int = 7) -> int:
353
360
  def get_pending_count(engagement_id: Optional[int] = None) -> int:
354
361
  """Get count of pending chains awaiting approval."""
355
362
  chains = _read_chains()
356
- pending = [c for c in chains if c.get('status') == CHAIN_PENDING]
363
+ pending = [c for c in chains if c.get("status") == CHAIN_PENDING]
357
364
  if engagement_id is not None:
358
- pending = [c for c in pending if c.get('engagement_id') == engagement_id]
365
+ pending = [c for c in pending if c.get("engagement_id") == engagement_id]
359
366
  return len(pending)
360
367
 
361
368
 
@@ -369,19 +376,63 @@ def get_chain_stats(engagement_id: Optional[int] = None) -> Dict[str, int]:
369
376
  chains = _read_chains()
370
377
 
371
378
  if engagement_id is not None:
372
- chains = [c for c in chains if c.get('engagement_id') == engagement_id]
379
+ chains = [c for c in chains if c.get("engagement_id") == engagement_id]
373
380
 
374
381
  stats = {
375
- 'pending': 0,
376
- 'approved': 0,
377
- 'rejected': 0,
378
- 'executed': 0,
379
- 'total': len(chains)
382
+ "pending": 0,
383
+ "approved": 0,
384
+ "rejected": 0,
385
+ "executed": 0,
386
+ "total": len(chains),
380
387
  }
381
388
 
382
389
  for c in chains:
383
- status = c.get('status', '')
390
+ status = c.get("status", "")
384
391
  if status in stats:
385
392
  stats[status] += 1
386
393
 
387
394
  return stats
395
+
396
+
397
+ def purge_orphaned_chains() -> int:
398
+ """
399
+ Remove chains that reference non-existent engagements.
400
+
401
+ This can happen when engagements are deleted but their pending
402
+ chains weren't cleaned up. Orphaned chains waste resources and
403
+ will never be processed.
404
+
405
+ Returns:
406
+ Number of orphaned chains removed
407
+ """
408
+ import sqlite3
409
+
410
+ with _lock:
411
+ chains = _read_chains()
412
+ if not chains:
413
+ return 0
414
+
415
+ # Get all valid engagement IDs from database
416
+ try:
417
+ db_path = os.path.join(os.path.expanduser("~"), ".souleyez", "souleyez.db")
418
+ conn = sqlite3.connect(db_path)
419
+ cursor = conn.execute("SELECT id FROM engagements")
420
+ valid_ids = {row[0] for row in cursor.fetchall()}
421
+ conn.close()
422
+ except Exception:
423
+ return 0 # Can't verify, don't purge
424
+
425
+ original_count = len(chains)
426
+
427
+ # Keep only chains with valid engagement IDs (or None)
428
+ chains = [
429
+ c
430
+ for c in chains
431
+ if c.get("engagement_id") is None or c.get("engagement_id") in valid_ids
432
+ ]
433
+
434
+ purged = original_count - len(chains)
435
+ if purged > 0:
436
+ _write_chains(chains)
437
+
438
+ return purged