souleyez 3.0.0__py3-none-any.whl → 3.0.7__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 (325) hide show
  1. souleyez/__init__.py +1 -1
  2. souleyez/ai/__init__.py +7 -7
  3. souleyez/ai/action_mapper.py +3 -2
  4. souleyez/ai/chain_advisor.py +2 -1
  5. souleyez/ai/claude_provider.py +2 -2
  6. souleyez/ai/context_builder.py +4 -2
  7. souleyez/ai/executor.py +9 -6
  8. souleyez/ai/feedback_handler.py +4 -2
  9. souleyez/ai/llm_provider.py +2 -2
  10. souleyez/ai/ollama_provider.py +2 -2
  11. souleyez/ai/ollama_service.py +10 -26
  12. souleyez/ai/path_scorer.py +2 -1
  13. souleyez/ai/recommender.py +6 -4
  14. souleyez/ai/report_context.py +2 -2
  15. souleyez/ai/report_service.py +5 -5
  16. souleyez/ai/result_parser.py +3 -2
  17. souleyez/ai/safety.py +5 -2
  18. souleyez/auth/__init__.py +6 -6
  19. souleyez/auth/audit.py +2 -2
  20. souleyez/auth/engagement_access.py +5 -7
  21. souleyez/auth/permissions.py +1 -1
  22. souleyez/auth/session_manager.py +5 -5
  23. souleyez/auth/user_manager.py +4 -5
  24. souleyez/commands/audit.py +6 -5
  25. souleyez/commands/auth.py +6 -5
  26. souleyez/commands/deliverables.py +2 -3
  27. souleyez/commands/engagement.py +3 -3
  28. souleyez/commands/license.py +3 -2
  29. souleyez/commands/screenshots.py +5 -4
  30. souleyez/commands/user.py +10 -8
  31. souleyez/config.py +4 -2
  32. souleyez/core/credential_tester.py +4 -2
  33. souleyez/core/cve_mappings.py +2 -1
  34. souleyez/core/cve_matcher.py +2 -1
  35. souleyez/core/msf_auto_mapper.py +2 -0
  36. souleyez/core/msf_chain_engine.py +3 -1
  37. souleyez/core/msf_database.py +7 -13
  38. souleyez/core/msf_integration.py +2 -2
  39. souleyez/core/msf_rpc_client.py +3 -2
  40. souleyez/core/msf_rpc_manager.py +4 -4
  41. souleyez/core/msf_sync_manager.py +7 -7
  42. souleyez/core/network_utils.py +1 -1
  43. souleyez/core/parser_handler.py +2 -1
  44. souleyez/core/pending_chains.py +4 -3
  45. souleyez/core/templates.py +5 -2
  46. souleyez/core/tool_chaining.py +101 -70
  47. souleyez/core/version_utils.py +1 -0
  48. souleyez/core/vuln_correlation.py +3 -2
  49. souleyez/core/web_utils.py +2 -1
  50. souleyez/detection/__init__.py +1 -1
  51. souleyez/detection/attack_signatures.py +1 -1
  52. souleyez/detection/mitre_mappings.py +1 -2
  53. souleyez/detection/validator.py +5 -4
  54. souleyez/devtools.py +4 -2
  55. souleyez/docs/README.md +2 -2
  56. souleyez/engine/background.py +168 -7
  57. souleyez/engine/base.py +2 -1
  58. souleyez/engine/loader.py +4 -2
  59. souleyez/engine/log_sanitizer.py +1 -0
  60. souleyez/engine/manager.py +3 -1
  61. souleyez/engine/result_handler.py +50 -67
  62. souleyez/engine/worker_manager.py +6 -4
  63. souleyez/export/evidence_bundle.py +1 -0
  64. souleyez/handlers/base.py +1 -0
  65. souleyez/handlers/bash_handler.py +1 -0
  66. souleyez/handlers/bloodhound_handler.py +1 -0
  67. souleyez/handlers/certipy_handler.py +1 -0
  68. souleyez/handlers/crackmapexec_handler.py +2 -20
  69. souleyez/handlers/dnsrecon_handler.py +2 -1
  70. souleyez/handlers/enum4linux_handler.py +65 -37
  71. souleyez/handlers/evil_winrm_handler.py +1 -0
  72. souleyez/handlers/ffuf_handler.py +3 -1
  73. souleyez/handlers/gobuster_handler.py +7 -6
  74. souleyez/handlers/gpp_extract_handler.py +1 -0
  75. souleyez/handlers/hashcat_handler.py +1 -0
  76. souleyez/handlers/hydra_handler.py +5 -2
  77. souleyez/handlers/impacket_getuserspns_handler.py +1 -0
  78. souleyez/handlers/impacket_psexec_handler.py +1 -0
  79. souleyez/handlers/impacket_secretsdump_handler.py +1 -0
  80. souleyez/handlers/john_handler.py +1 -0
  81. souleyez/handlers/katana_handler.py +39 -2
  82. souleyez/handlers/kerbrute_handler.py +1 -0
  83. souleyez/handlers/ldapsearch_handler.py +90 -17
  84. souleyez/handlers/lfi_extract_handler.py +1 -0
  85. souleyez/handlers/msf_auxiliary_handler.py +1 -0
  86. souleyez/handlers/msf_exploit_handler.py +1 -0
  87. souleyez/handlers/nikto_handler.py +2 -1
  88. souleyez/handlers/nmap_handler.py +2 -1
  89. souleyez/handlers/nuclei_handler.py +2 -1
  90. souleyez/handlers/nxc_handler.py +3 -18
  91. souleyez/handlers/rdp_sec_check_handler.py +1 -0
  92. souleyez/handlers/registry.py +1 -0
  93. souleyez/handlers/responder_handler.py +1 -0
  94. souleyez/handlers/service_explorer_handler.py +2 -1
  95. souleyez/handlers/smbclient_handler.py +1 -0
  96. souleyez/handlers/smbmap_handler.py +3 -2
  97. souleyez/handlers/sqlmap_handler.py +6 -4
  98. souleyez/handlers/theharvester_handler.py +2 -1
  99. souleyez/handlers/web_login_test_handler.py +1 -0
  100. souleyez/handlers/whois_handler.py +3 -2
  101. souleyez/handlers/wpscan_handler.py +2 -1
  102. souleyez/history.py +4 -3
  103. souleyez/importers/msf_importer.py +5 -3
  104. souleyez/importers/smart_importer.py +6 -4
  105. souleyez/integrations/siem/__init__.py +6 -6
  106. souleyez/integrations/siem/base.py +1 -1
  107. souleyez/integrations/siem/elastic.py +3 -3
  108. souleyez/integrations/siem/factory.py +1 -2
  109. souleyez/integrations/siem/googlesecops.py +4 -4
  110. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +1 -1
  111. souleyez/integrations/siem/sentinel.py +3 -3
  112. souleyez/integrations/siem/splunk.py +3 -3
  113. souleyez/integrations/siem/wazuh.py +4 -4
  114. souleyez/integrations/wazuh/__init__.py +1 -1
  115. souleyez/integrations/wazuh/client.py +3 -2
  116. souleyez/integrations/wazuh/config.py +3 -2
  117. souleyez/integrations/wazuh/host_mapper.py +3 -1
  118. souleyez/integrations/wazuh/sync.py +4 -1
  119. souleyez/intelligence/__init__.py +1 -1
  120. souleyez/intelligence/correlation_analyzer.py +6 -5
  121. souleyez/intelligence/exploit_knowledge.py +4 -4
  122. souleyez/intelligence/exploit_suggestions.py +4 -3
  123. souleyez/intelligence/gap_analyzer.py +5 -3
  124. souleyez/intelligence/gap_detector.py +2 -0
  125. souleyez/intelligence/sensitive_tables.py +1 -1
  126. souleyez/intelligence/service_parser.py +1 -0
  127. souleyez/intelligence/surface_analyzer.py +9 -9
  128. souleyez/intelligence/target_parser.py +1 -0
  129. souleyez/licensing/__init__.py +3 -3
  130. souleyez/main.py +25 -18
  131. souleyez/migrations/fix_job_counter.py +2 -1
  132. souleyez/parsers/bloodhound_parser.py +1 -0
  133. souleyez/parsers/crackmapexec_parser.py +2 -1
  134. souleyez/parsers/dalfox_parser.py +3 -2
  135. souleyez/parsers/dnsrecon_parser.py +2 -1
  136. souleyez/parsers/enum4linux_parser.py +2 -1
  137. souleyez/parsers/ffuf_parser.py +2 -1
  138. souleyez/parsers/gobuster_parser.py +2 -1
  139. souleyez/parsers/hashcat_parser.py +3 -2
  140. souleyez/parsers/http_fingerprint_parser.py +2 -1
  141. souleyez/parsers/hydra_parser.py +2 -1
  142. souleyez/parsers/impacket_parser.py +2 -1
  143. souleyez/parsers/john_parser.py +4 -3
  144. souleyez/parsers/katana_parser.py +134 -2
  145. souleyez/parsers/msf_parser.py +2 -1
  146. souleyez/parsers/nikto_parser.py +2 -1
  147. souleyez/parsers/nmap_parser.py +14 -3
  148. souleyez/parsers/nuclei_parser.py +3 -2
  149. souleyez/parsers/responder_parser.py +1 -0
  150. souleyez/parsers/searchsploit_parser.py +3 -2
  151. souleyez/parsers/service_explorer_parser.py +1 -0
  152. souleyez/parsers/smbmap_parser.py +2 -1
  153. souleyez/parsers/sqlmap_parser.py +36 -2
  154. souleyez/parsers/theharvester_parser.py +2 -1
  155. souleyez/parsers/whois_parser.py +2 -1
  156. souleyez/parsers/wpscan_parser.py +3 -2
  157. souleyez/plugins/afp.py +3 -1
  158. souleyez/plugins/afp_brute.py +3 -1
  159. souleyez/plugins/ard.py +3 -1
  160. souleyez/plugins/bloodhound.py +3 -2
  161. souleyez/plugins/certipy.py +1 -0
  162. souleyez/plugins/crackmapexec.py +11 -7
  163. souleyez/plugins/dalfox.py +5 -2
  164. souleyez/plugins/dns_hijack.py +3 -1
  165. souleyez/plugins/dnsrecon.py +3 -1
  166. souleyez/plugins/enum4linux.py +3 -1
  167. souleyez/plugins/evil_winrm.py +1 -0
  168. souleyez/plugins/ffuf.py +3 -1
  169. souleyez/plugins/firmware_extract.py +3 -2
  170. souleyez/plugins/gobuster.py +6 -3
  171. souleyez/plugins/gpp_extract.py +1 -0
  172. souleyez/plugins/hashcat.py +2 -1
  173. souleyez/plugins/http_fingerprint.py +57 -7
  174. souleyez/plugins/hydra.py +5 -3
  175. souleyez/plugins/impacket_common.py +40 -0
  176. souleyez/plugins/impacket_getnpusers.py +19 -2
  177. souleyez/plugins/impacket_getuserspns.py +158 -0
  178. souleyez/plugins/impacket_psexec.py +19 -2
  179. souleyez/plugins/impacket_secretsdump.py +19 -2
  180. souleyez/plugins/impacket_smbclient.py +19 -2
  181. souleyez/plugins/john.py +2 -1
  182. souleyez/plugins/katana.py +48 -6
  183. souleyez/plugins/kerbrute.py +1 -0
  184. souleyez/plugins/lfi_extract.py +1 -0
  185. souleyez/plugins/macos_ssh.py +3 -1
  186. souleyez/plugins/mdns.py +3 -1
  187. souleyez/plugins/msf_auxiliary.py +3 -2
  188. souleyez/plugins/msf_exploit.py +6 -5
  189. souleyez/plugins/nikto.py +5 -2
  190. souleyez/plugins/nmap.py +6 -4
  191. souleyez/plugins/nuclei.py +3 -1
  192. souleyez/plugins/nxc.py +1 -0
  193. souleyez/plugins/plugin_base.py +3 -2
  194. souleyez/plugins/plugin_template.py +3 -2
  195. souleyez/plugins/rdp_sec_check.py +1 -0
  196. souleyez/plugins/responder.py +2 -1
  197. souleyez/plugins/router_http_brute.py +3 -1
  198. souleyez/plugins/router_ssh_brute.py +3 -1
  199. souleyez/plugins/router_telnet_brute.py +3 -1
  200. souleyez/plugins/routersploit.py +5 -3
  201. souleyez/plugins/routersploit_exploit.py +5 -3
  202. souleyez/plugins/searchsploit.py +1 -0
  203. souleyez/plugins/service_explorer.py +2 -1
  204. souleyez/plugins/smbmap.py +3 -1
  205. souleyez/plugins/smbpasswd.py +1 -0
  206. souleyez/plugins/sqlmap.py +3 -1
  207. souleyez/plugins/theharvester.py +3 -1
  208. souleyez/plugins/tr069.py +3 -1
  209. souleyez/plugins/upnp.py +3 -1
  210. souleyez/plugins/upnp_abuse.py +4 -2
  211. souleyez/plugins/vnc_access.py +4 -2
  212. souleyez/plugins/vnc_brute.py +3 -1
  213. souleyez/plugins/web_login_test.py +1 -0
  214. souleyez/plugins/whois.py +3 -1
  215. souleyez/plugins/wpscan.py +3 -1
  216. souleyez/reporting/attack_chain.py +2 -1
  217. souleyez/reporting/charts.py +1 -0
  218. souleyez/reporting/compliance_mappings.py +1 -0
  219. souleyez/reporting/detection_report.py +10 -10
  220. souleyez/reporting/formatters.py +7 -12
  221. souleyez/reporting/generator.py +34 -46
  222. souleyez/reporting/metrics.py +2 -1
  223. souleyez/scanner.py +6 -3
  224. souleyez/security/__init__.py +7 -5
  225. souleyez/security/scope_validator.py +5 -4
  226. souleyez/security.py +5 -2
  227. souleyez/storage/credentials.py +14 -19
  228. souleyez/storage/crypto.py +7 -4
  229. souleyez/storage/database.py +6 -6
  230. souleyez/storage/db.py +8 -8
  231. souleyez/storage/deliverable_evidence.py +2 -1
  232. souleyez/storage/deliverable_exporter.py +3 -2
  233. souleyez/storage/deliverable_templates.py +2 -1
  234. souleyez/storage/deliverables.py +2 -1
  235. souleyez/storage/engagements.py +6 -4
  236. souleyez/storage/evidence.py +5 -4
  237. souleyez/storage/execution_log.py +4 -2
  238. souleyez/storage/exploit_attempts.py +3 -2
  239. souleyez/storage/exploits.py +3 -1
  240. souleyez/storage/findings.py +3 -1
  241. souleyez/storage/hosts.py +5 -2
  242. souleyez/storage/migrate_to_engagements.py +14 -24
  243. souleyez/storage/migrations/_001_add_credential_enhancements.py +12 -21
  244. souleyez/storage/migrations/_003_add_execution_log.py +8 -13
  245. souleyez/storage/migrations/_005_screenshots.py +2 -4
  246. souleyez/storage/migrations/_006_deliverables.py +2 -4
  247. souleyez/storage/migrations/_007_deliverable_templates.py +4 -8
  248. souleyez/storage/migrations/_008_add_nuclei_table.py +2 -4
  249. souleyez/storage/migrations/_010_evidence_linking.py +6 -12
  250. souleyez/storage/migrations/_012_team_collaboration.py +12 -24
  251. souleyez/storage/migrations/_013_add_host_tags.py +2 -4
  252. souleyez/storage/migrations/_014_exploit_attempts.py +10 -20
  253. souleyez/storage/migrations/_015_add_mac_os_fields.py +4 -8
  254. souleyez/storage/migrations/_016_add_domain_field.py +2 -4
  255. souleyez/storage/migrations/_017_msf_sessions.py +8 -16
  256. souleyez/storage/migrations/_018_add_osint_target.py +4 -8
  257. souleyez/storage/migrations/_019_add_engagement_type.py +4 -8
  258. souleyez/storage/migrations/_020_add_rbac.py +9 -17
  259. souleyez/storage/migrations/_021_wazuh_integration.py +4 -8
  260. souleyez/storage/migrations/_023_fix_detection_results_fk.py +2 -4
  261. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +4 -8
  262. souleyez/storage/migrations/_026_add_engagement_scope.py +4 -8
  263. souleyez/storage/migrations/_027_multi_siem_persistence.py +8 -16
  264. souleyez/storage/migrations/__init__.py +1 -4
  265. souleyez/storage/migrations/migration_manager.py +6 -9
  266. souleyez/storage/msf_sessions.py +1 -1
  267. souleyez/storage/osint.py +3 -1
  268. souleyez/storage/recommendation_engine.py +3 -2
  269. souleyez/storage/screenshots.py +2 -1
  270. souleyez/storage/smb_shares.py +3 -1
  271. souleyez/storage/sqlmap_data.py +6 -4
  272. souleyez/storage/team_collaboration.py +3 -2
  273. souleyez/storage/timeline_tracker.py +2 -1
  274. souleyez/storage/wazuh_vulns.py +3 -1
  275. souleyez/storage/web_paths.py +3 -1
  276. souleyez/testing/credential_tester.py +2 -0
  277. souleyez/ui/__init__.py +2 -1
  278. souleyez/ui/ai_quotes.py +1 -1
  279. souleyez/ui/attack_surface.py +50 -28
  280. souleyez/ui/chain_rules_view.py +6 -3
  281. souleyez/ui/correlation_view.py +3 -2
  282. souleyez/ui/dashboard.py +85 -139
  283. souleyez/ui/deliverables_view.py +1 -1
  284. souleyez/ui/design_system.py +5 -3
  285. souleyez/ui/errors.py +3 -1
  286. souleyez/ui/evidence_linking_view.py +2 -1
  287. souleyez/ui/evidence_vault.py +11 -6
  288. souleyez/ui/exploit_suggestions_view.py +11 -7
  289. souleyez/ui/export_view.py +3 -1
  290. souleyez/ui/gap_analysis_view.py +6 -3
  291. souleyez/ui/help_system.py +4 -1
  292. souleyez/ui/intelligence_view.py +7 -3
  293. souleyez/ui/interactive.py +1280 -558
  294. souleyez/ui/interactive_selector.py +3 -2
  295. souleyez/ui/log_formatter.py +1 -0
  296. souleyez/ui/menu_components.py +3 -1
  297. souleyez/ui/msf_auxiliary_menu.py +4 -1
  298. souleyez/ui/pending_chains_view.py +15 -12
  299. souleyez/ui/progress_indicators.py +5 -2
  300. souleyez/ui/recommendations_view.py +4 -2
  301. souleyez/ui/rule_builder.py +4 -1
  302. souleyez/ui/setup_wizard.py +10 -8
  303. souleyez/ui/shortcuts.py +1 -1
  304. souleyez/ui/splunk_gap_analysis_view.py +7 -4
  305. souleyez/ui/splunk_vulns_view.py +4 -1
  306. souleyez/ui/team_dashboard.py +7 -5
  307. souleyez/ui/template_selector.py +2 -1
  308. souleyez/ui/terminal.py +3 -2
  309. souleyez/ui/timeline_view.py +2 -1
  310. souleyez/ui/tool_setup.py +92 -31
  311. souleyez/ui/tutorial.py +7 -4
  312. souleyez/ui/tutorial_state.py +3 -2
  313. souleyez/ui/wazuh_vulns_view.py +5 -2
  314. souleyez/ui/wordlist_browser.py +4 -3
  315. souleyez/ui.py +13 -7
  316. souleyez/utils/tool_checker.py +61 -12
  317. souleyez/utils.py +4 -4
  318. souleyez/wordlists.py +1 -0
  319. {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/METADATA +1 -1
  320. souleyez-3.0.7.dist-info/RECORD +445 -0
  321. souleyez-3.0.0.dist-info/RECORD +0 -443
  322. {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/WHEEL +0 -0
  323. {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/entry_points.txt +0 -0
  324. {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/licenses/LICENSE +0 -0
  325. {souleyez-3.0.0.dist-info → souleyez-3.0.7.dist-info}/top_level.txt +0 -0
@@ -73,6 +73,10 @@ TRANSIENT_ERROR_PATTERNS = [
73
73
  "Resource temporarily unavailable",
74
74
  "SMBTimeout",
75
75
  "timed out while waiting",
76
+ # Impacket-specific timeout patterns
77
+ "] timed out", # Matches: [Errno Connection error (IP:port)] timed out
78
+ "RemoteOperations failed", # Impacket remote operation failures
79
+ "Errno Connection error", # Generic Impacket connection errors
76
80
  ]
77
81
 
78
82
  _lock = threading.RLock() # Reentrant lock allows nested acquisition by same thread
@@ -88,6 +92,40 @@ def _is_transient_error(log_content: str) -> bool:
88
92
  return False
89
93
 
90
94
 
95
+ def _is_netexec_flaky_empty(log_content: str, job: dict) -> bool:
96
+ """
97
+ Check if netexec/crackmapexec produced no output (flaky ARM behavior).
98
+
99
+ On ARM, netexec is ~20% flaky - it exits 0 but sometimes produces
100
+ zero output. This detects ONLY that case.
101
+
102
+ DOES NOT retry when:
103
+ - Access denied (has "[-]" error output)
104
+ - Connected but no shares (legitimate result)
105
+ - Any netexec output exists
106
+ """
107
+ tool = job.get("tool", "").lower()
108
+ args = job.get("args", [])
109
+
110
+ if tool not in ("crackmapexec", "nxc", "netexec"):
111
+ return False
112
+
113
+ args_str = " ".join(args).lower() if args else ""
114
+ if "--shares" not in args_str and "smb" not in args_str:
115
+ return False
116
+
117
+ lower = log_content.lower()
118
+
119
+ # If we got ANY netexec output, it's a real response - don't retry
120
+ # This includes error output like "[-]" which indicates access denied
121
+ has_any_output = any(
122
+ x in lower for x in ["smb", "[*]", "[+]", "[-]", "445", "signing"]
123
+ )
124
+
125
+ # Only retry if truly zero output (ARM flakiness)
126
+ return not has_any_output
127
+
128
+
91
129
  class _CrossProcessLock:
92
130
  """
93
131
  Cross-process file lock using fcntl.flock().
@@ -1516,6 +1554,78 @@ def _is_stdbuf_available() -> bool:
1516
1554
  return _stdbuf_available
1517
1555
 
1518
1556
 
1557
+ def _is_python_tool(cmd: List[str]) -> bool:
1558
+ """
1559
+ Check if a command is a Python-based tool.
1560
+
1561
+ stdbuf doesn't work well with Python scripts because Python manages its own
1562
+ buffering internally. On some systems (especially ARM), stdbuf can prevent
1563
+ Python tools from producing any output at all.
1564
+
1565
+ Args:
1566
+ cmd: Command array to check
1567
+
1568
+ Returns:
1569
+ True if the command is a Python tool that should skip stdbuf
1570
+ """
1571
+ if not cmd:
1572
+ return False
1573
+
1574
+ executable = cmd[0]
1575
+
1576
+ # Direct Python invocation
1577
+ if executable in ("python", "python3") or executable.startswith("python"):
1578
+ return True
1579
+
1580
+ # .py extension
1581
+ if executable.endswith(".py"):
1582
+ return True
1583
+
1584
+ # Known Python tools that don't work with stdbuf
1585
+ # These are tools installed via pip/pipx that are Python scripts
1586
+ python_tools = {
1587
+ "netexec",
1588
+ "nxc",
1589
+ "crackmapexec",
1590
+ "cme",
1591
+ "smbmap",
1592
+ "impacket-GetNPUsers",
1593
+ "impacket-GetUserSPNs",
1594
+ "impacket-secretsdump",
1595
+ "impacket-psexec",
1596
+ "impacket-smbclient",
1597
+ "impacket-wmiexec",
1598
+ "impacket-dcomexec",
1599
+ "impacket-atexec",
1600
+ "GetNPUsers.py",
1601
+ "GetUserSPNs.py",
1602
+ "secretsdump.py",
1603
+ "psexec.py",
1604
+ "smbclient.py",
1605
+ "certipy",
1606
+ "bloodhound-python",
1607
+ "ldapdomaindump",
1608
+ }
1609
+
1610
+ # Check base name (handle full paths)
1611
+ base_name = os.path.basename(executable)
1612
+ if base_name in python_tools:
1613
+ return True
1614
+
1615
+ # Check shebang for Python interpreter
1616
+ exe_path = shutil.which(executable)
1617
+ if exe_path:
1618
+ try:
1619
+ with open(exe_path, "rb") as f:
1620
+ first_bytes = f.read(100)
1621
+ if first_bytes.startswith(b"#!") and b"python" in first_bytes:
1622
+ return True
1623
+ except (IOError, OSError):
1624
+ pass
1625
+
1626
+ return False
1627
+
1628
+
1519
1629
  def _wrap_cmd_for_line_buffering(cmd: List[str]) -> List[str]:
1520
1630
  """
1521
1631
  Wrap a command with stdbuf for line-buffered output when available.
@@ -1524,6 +1634,9 @@ def _wrap_cmd_for_line_buffering(cmd: List[str]) -> List[str]:
1524
1634
  improving real-time log monitoring and ensuring output is captured
1525
1635
  before process termination.
1526
1636
 
1637
+ Note: Python tools are excluded because stdbuf interferes with Python's
1638
+ internal buffering and can cause zero output on some systems (Ubuntu ARM).
1639
+
1527
1640
  Args:
1528
1641
  cmd: Command to wrap
1529
1642
 
@@ -1533,6 +1646,10 @@ def _wrap_cmd_for_line_buffering(cmd: List[str]) -> List[str]:
1533
1646
  if not cmd:
1534
1647
  return cmd
1535
1648
 
1649
+ # Skip stdbuf for Python tools - causes output capture failures on ARM
1650
+ if _is_python_tool(cmd):
1651
+ return cmd
1652
+
1536
1653
  if _is_stdbuf_available():
1537
1654
  # stdbuf -oL = line-buffered stdout, -eL = line-buffered stderr
1538
1655
  return ["stdbuf", "-oL", "-eL"] + cmd
@@ -2064,21 +2181,30 @@ def run_job(jid: int) -> None:
2064
2181
  job = get_job(jid)
2065
2182
  retry_count = job.get("metadata", {}).get("retry_count", 0)
2066
2183
  if retry_count < MAX_RETRIES:
2067
- # Read log to check for transient errors
2068
- # Note: Check even when rc==0 because tools like nxc may exit 0 but log errors
2184
+ # Read log to check for transient errors or flaky netexec
2069
2185
  log_path = job.get("log", "")
2070
2186
  if log_path and os.path.exists(log_path):
2071
2187
  try:
2072
2188
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
2073
2189
  log_content = f.read()
2074
- if _is_transient_error(log_content):
2075
- # Transient error detected - auto-retry
2190
+
2191
+ # Check for transient errors OR flaky netexec (connected but no shares)
2192
+ is_transient = _is_transient_error(log_content)
2193
+ is_netexec_flaky = _is_netexec_flaky_empty(log_content, job)
2194
+ retry_reason = None
2195
+
2196
+ if is_transient:
2197
+ retry_reason = "transient error"
2198
+ elif is_netexec_flaky:
2199
+ retry_reason = "netexec connected but no shares"
2200
+
2201
+ if retry_reason:
2076
2202
  logger.info(
2077
- "Transient error detected, auto-retrying job",
2203
+ f"Auto-retrying job: {retry_reason}",
2078
2204
  extra={"job_id": jid, "retry_count": retry_count + 1},
2079
2205
  )
2080
2206
  _append_worker_log(
2081
- f"job {jid}: transient error detected, auto-retry {retry_count + 1}/{MAX_RETRIES}"
2207
+ f"job {jid}: {retry_reason}, auto-retry {retry_count + 1}/{MAX_RETRIES}"
2082
2208
  )
2083
2209
 
2084
2210
  # Build new job metadata with incremented retry count
@@ -2095,7 +2221,7 @@ def run_job(jid: int) -> None:
2095
2221
  engagement_id=job.get("engagement_id"),
2096
2222
  metadata=new_metadata,
2097
2223
  parent_id=job.get("metadata", {}).get("parent_id"),
2098
- reason=f"Auto-retry {retry_count + 1}/{MAX_RETRIES} (transient error)",
2224
+ reason=f"Auto-retry {retry_count + 1}/{MAX_RETRIES} ({retry_reason})",
2099
2225
  rule_id=job.get("metadata", {}).get("rule_id"),
2100
2226
  skip_scope_check=True, # Already validated on first run
2101
2227
  )
@@ -2120,6 +2246,41 @@ def run_job(jid: int) -> None:
2120
2246
 
2121
2247
  # Re-fetch job to get updated data
2122
2248
  job = get_job(jid)
2249
+
2250
+ # Ensure log file is fully flushed to disk before parsing
2251
+ # Some tools (especially Python-based like impacket) may have buffered output
2252
+ log_path = job.get("log", "")
2253
+ if log_path and os.path.exists(log_path):
2254
+ # Wait for completion marker with retries
2255
+ # Log files end with "Exit Code:" or "=== Completed"
2256
+ max_retries = 5
2257
+ retry_delay = 0.2 # 200ms between retries
2258
+ log_complete = False
2259
+
2260
+ for attempt in range(max_retries):
2261
+ try:
2262
+ with open(
2263
+ log_path, "r", encoding="utf-8", errors="replace"
2264
+ ) as f:
2265
+ content = f.read()
2266
+ # Check for completion markers
2267
+ if "Exit Code:" in content or "=== Completed" in content:
2268
+ log_complete = True
2269
+ break
2270
+ except Exception:
2271
+ pass
2272
+
2273
+ if attempt < max_retries - 1:
2274
+ time.sleep(retry_delay)
2275
+
2276
+ if not log_complete:
2277
+ # Last resort: force filesystem sync and proceed anyway
2278
+ try:
2279
+ with open(log_path, "a") as f:
2280
+ os.fsync(f.fileno())
2281
+ except Exception:
2282
+ pass
2283
+
2123
2284
  parse_result = handle_job_result(job)
2124
2285
 
2125
2286
  # Handle parse failure cases
souleyez/engine/base.py CHANGED
@@ -3,7 +3,8 @@
3
3
  Simple plugin base classes and type hints for souleyez.
4
4
  Plugins should implement ScannerPlugin.run(prepared) -> dict (ScanResult).
5
5
  """
6
- from typing import Dict, Any, Optional
6
+
7
+ from typing import Any, Dict, Optional
7
8
 
8
9
  ScanResult = Dict[str, Any]
9
10
 
souleyez/engine/loader.py CHANGED
@@ -2,10 +2,12 @@
2
2
  """
3
3
  Simple plugin loader for souleyez (L1).
4
4
  """
5
+
5
6
  from __future__ import annotations
6
- import pkgutil
7
+
7
8
  import importlib
8
- from typing import Dict, Any
9
+ import pkgutil
10
+ from typing import Any, Dict
9
11
 
10
12
 
11
13
  def _safe_import_module(fullname: str):
@@ -5,6 +5,7 @@ souleyez.engine.log_sanitizer - Sanitize sensitive data from logs
5
5
  Redacts credentials and sensitive information from job logs to prevent
6
6
  plaintext password exposure, while maintaining log readability.
7
7
  """
8
+
8
9
  import re
9
10
  from typing import Optional
10
11
 
@@ -6,11 +6,13 @@ This manager contains a small built-in adapter for "nmap" that uses your existin
6
6
  souleyez.scanner.run_nmap function. Later plugins can be added by implementing
7
7
  ScannerPlugin in their own modules and registering them here.
8
8
  """
9
+
10
+ import importlib
9
11
  import threading
10
12
  from typing import Optional
13
+
11
14
  from ..storage.db import init_db, insert_scan, update_scan
12
15
  from ..utils import timestamp_str
13
- import importlib
14
16
 
15
17
  # lazy import existing run_nmap (your project has souleyez/scanner.py)
16
18
 
@@ -2,10 +2,12 @@
2
2
  """
3
3
  souleyez.engine.result_handler - Auto-parse job results
4
4
  """
5
- import os
5
+
6
6
  import logging
7
- from typing import Optional, Dict, Any
8
- from .job_status import STATUS_DONE, STATUS_NO_RESULTS, STATUS_WARNING, STATUS_ERROR
7
+ import os
8
+ from typing import Any, Dict, Optional
9
+
10
+ from .job_status import STATUS_DONE, STATUS_ERROR, STATUS_NO_RESULTS, STATUS_WARNING
9
11
 
10
12
  logger = logging.getLogger(__name__)
11
13
 
@@ -49,7 +51,8 @@ TOOL_ERROR_PATTERNS = {
49
51
  "nuclei": [
50
52
  "could not connect",
51
53
  "context deadline exceeded",
52
- "no address found",
54
+ # Note: "no address found" removed - nuclei reports this as [INF] when
55
+ # templates try alternate ports, not a real error. Exit code 0 = success.
53
56
  ],
54
57
  "ffuf": [
55
58
  "error making request",
@@ -129,36 +132,11 @@ def handle_job_result(job: Dict[str, Any]) -> Optional[Dict[str, Any]]:
129
132
  """
130
133
  tool = job.get("tool", "").lower()
131
134
  log_path = job.get("log")
132
- status = job.get("status")
133
-
134
- # Some tools return non-zero exit codes even on success
135
- # Parse 'done' jobs and 'error' jobs for certain tools
136
- # gobuster: exits 1 for wildcard detection (but we can still parse and retry)
137
- # enum4linux: exits non-zero when some queries fail, even with partial results
138
- # msf_exploit: exits 0 even on failure, but we want to parse either way
139
- # hashcat: exits 1 when exhausted (no passwords cracked) - not an error
140
- # web_login_test: exits 1 when credentials fail - not an error, just invalid creds
141
- tools_with_nonzero_success = [
142
- "gobuster",
143
- "msf_auxiliary",
144
- "msfconsole",
145
- "enum4linux",
146
- "msf_exploit",
147
- "wpscan",
148
- "hashcat",
149
- "web_login_test",
150
- ]
151
-
152
- if status == "done":
153
- # Always parse successful jobs
154
- pass
155
- elif status == "error" and tool in tools_with_nonzero_success:
156
- # Parse error jobs for tools that can succeed with non-zero exit codes
157
- pass
158
- else:
159
- # Skip other error/failed jobs
160
- return None
161
135
 
136
+ # Always try to parse if log file exists
137
+ # Status checks were causing race conditions where jobs showed "done"
138
+ # but get_job() returned stale status, causing parse to be skipped
139
+ # The caller (background.py) only calls us for completed jobs anyway
162
140
  if not log_path or not os.path.exists(log_path):
163
141
  logger.error(
164
142
  f"Job {job.get('id')} parse failed: log file missing or does not exist (path={log_path})"
@@ -193,9 +171,9 @@ def handle_job_result(job: Dict[str, Any]) -> Optional[Dict[str, Any]]:
193
171
 
194
172
  try:
195
173
  from souleyez.handlers.registry import get_handler
196
- from souleyez.storage.hosts import HostManager
197
- from souleyez.storage.findings import FindingsManager
198
174
  from souleyez.storage.credentials import CredentialsManager
175
+ from souleyez.storage.findings import FindingsManager
176
+ from souleyez.storage.hosts import HostManager
199
177
 
200
178
  handler = get_handler(tool)
201
179
  if handler:
@@ -306,7 +284,7 @@ def reparse_job(job_id: int) -> Dict[str, Any]:
306
284
  Returns:
307
285
  Dict with 'success', 'message', and optionally 'parse_result'
308
286
  """
309
- from .background import get_job, _read_jobs, _write_jobs
287
+ from .background import _read_jobs, _write_jobs, get_job
310
288
 
311
289
  # Get the job
312
290
  job = get_job(job_id)
@@ -345,9 +323,9 @@ def reparse_job(job_id: int) -> Dict[str, Any]:
345
323
 
346
324
  try:
347
325
  from souleyez.handlers.registry import get_handler
348
- from souleyez.storage.hosts import HostManager
349
- from souleyez.storage.findings import FindingsManager
350
326
  from souleyez.storage.credentials import CredentialsManager
327
+ from souleyez.storage.findings import FindingsManager
328
+ from souleyez.storage.hosts import HostManager
351
329
 
352
330
  handler = get_handler(tool)
353
331
  if handler:
@@ -527,10 +505,10 @@ def parse_nmap_job(
527
505
  ) -> Dict[str, Any]:
528
506
  """Parse nmap job results."""
529
507
  try:
508
+ from souleyez.core.cve_matcher import CVEMatcher
530
509
  from souleyez.parsers.nmap_parser import parse_nmap_log
531
- from souleyez.storage.hosts import HostManager
532
510
  from souleyez.storage.findings import FindingsManager
533
- from souleyez.core.cve_matcher import CVEMatcher
511
+ from souleyez.storage.hosts import HostManager
534
512
 
535
513
  # Parse the log file
536
514
  parsed = parse_nmap_log(log_path)
@@ -804,11 +782,11 @@ def parse_theharvester_job(
804
782
  """Parse theHarvester job results."""
805
783
  try:
806
784
  from souleyez.parsers.theharvester_parser import (
807
- parse_theharvester_output,
808
785
  get_osint_stats,
786
+ parse_theharvester_output,
809
787
  )
810
- from souleyez.storage.osint import OsintManager
811
788
  from souleyez.storage.hosts import HostManager
789
+ from souleyez.storage.osint import OsintManager
812
790
 
813
791
  # Read the log file
814
792
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
@@ -1301,14 +1279,15 @@ def parse_gobuster_job(
1301
1279
  ) -> Dict[str, Any]:
1302
1280
  """Parse gobuster job results."""
1303
1281
  try:
1282
+ from urllib.parse import urlparse
1283
+
1304
1284
  from souleyez.parsers.gobuster_parser import (
1305
- parse_gobuster_output,
1306
- get_paths_stats,
1307
1285
  generate_next_steps,
1286
+ get_paths_stats,
1287
+ parse_gobuster_output,
1308
1288
  )
1309
- from souleyez.storage.web_paths import WebPathsManager
1310
1289
  from souleyez.storage.hosts import HostManager
1311
- from urllib.parse import urlparse
1290
+ from souleyez.storage.web_paths import WebPathsManager
1312
1291
 
1313
1292
  # Read the log file
1314
1293
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
@@ -1532,11 +1511,12 @@ def parse_sqlmap_job(
1532
1511
  ) -> Dict[str, Any]:
1533
1512
  """Parse sqlmap job results."""
1534
1513
  try:
1535
- from souleyez.parsers.sqlmap_parser import parse_sqlmap_output, get_sqli_stats
1514
+ from urllib.parse import urlparse
1515
+
1516
+ from souleyez.parsers.sqlmap_parser import get_sqli_stats, parse_sqlmap_output
1536
1517
  from souleyez.storage.findings import FindingsManager
1537
1518
  from souleyez.storage.hosts import HostManager
1538
1519
  from souleyez.storage.sqlmap_data import SQLMapDataManager
1539
- from urllib.parse import urlparse
1540
1520
 
1541
1521
  # Read the log file
1542
1522
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
@@ -1973,9 +1953,9 @@ def _extract_credentials_from_dump(
1973
1953
  tuple: (count of credentials added, list of plaintext credential dicts for chaining)
1974
1954
  """
1975
1955
  from souleyez.intelligence.sensitive_tables import is_sensitive_table
1956
+ from souleyez.log_config import get_logger
1976
1957
  from souleyez.storage.credentials import CredentialsManager
1977
1958
  from souleyez.storage.hosts import HostManager
1978
- from souleyez.log_config import get_logger
1979
1959
 
1980
1960
  logger = get_logger(__name__)
1981
1961
  cred_manager = CredentialsManager()
@@ -2189,10 +2169,11 @@ def parse_msf_auxiliary_job(
2189
2169
  """Parse MSF auxiliary module job results."""
2190
2170
  try:
2191
2171
  import re
2172
+
2192
2173
  from souleyez.parsers.msf_parser import parse_msf_log
2193
- from souleyez.storage.hosts import HostManager
2194
- from souleyez.storage.findings import FindingsManager
2195
2174
  from souleyez.storage.credentials import CredentialsManager
2175
+ from souleyez.storage.findings import FindingsManager
2176
+ from souleyez.storage.hosts import HostManager
2196
2177
 
2197
2178
  # Read raw log for connection failure detection
2198
2179
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
@@ -2348,8 +2329,9 @@ def parse_msf_exploit_job(
2348
2329
  """Parse MSF exploit module job results."""
2349
2330
  try:
2350
2331
  import re
2351
- from souleyez.storage.hosts import HostManager
2332
+
2352
2333
  from souleyez.storage.findings import FindingsManager
2334
+ from souleyez.storage.hosts import HostManager
2353
2335
 
2354
2336
  # Read the log file
2355
2337
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
@@ -2501,10 +2483,10 @@ def parse_smbmap_job(
2501
2483
  ) -> Dict[str, Any]:
2502
2484
  """Parse smbmap job results."""
2503
2485
  try:
2504
- from souleyez.parsers.smbmap_parser import parse_smbmap_output, extract_findings
2505
- from souleyez.storage.smb_shares import SMBSharesManager
2486
+ from souleyez.parsers.smbmap_parser import extract_findings, parse_smbmap_output
2506
2487
  from souleyez.storage.findings import FindingsManager
2507
2488
  from souleyez.storage.hosts import HostManager
2489
+ from souleyez.storage.smb_shares import SMBSharesManager
2508
2490
 
2509
2491
  # Read the log file
2510
2492
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
@@ -2604,9 +2586,9 @@ def parse_whois_job(
2604
2586
  """Parse WHOIS job results."""
2605
2587
  try:
2606
2588
  from souleyez.parsers.whois_parser import (
2607
- parse_whois_output,
2608
- map_to_osint_data,
2609
2589
  extract_emails,
2590
+ map_to_osint_data,
2591
+ parse_whois_output,
2610
2592
  )
2611
2593
  from souleyez.storage.osint import OsintManager
2612
2594
 
@@ -2666,8 +2648,8 @@ def parse_dnsrecon_job(
2666
2648
  """Parse dnsrecon job results."""
2667
2649
  try:
2668
2650
  from souleyez.parsers.dnsrecon_parser import parse_dnsrecon_output
2669
- from souleyez.storage.osint import OsintManager
2670
2651
  from souleyez.storage.hosts import HostManager
2652
+ from souleyez.storage.osint import OsintManager
2671
2653
 
2672
2654
  # Read the log file
2673
2655
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
@@ -2818,8 +2800,8 @@ def parse_hydra_job(
2818
2800
  try:
2819
2801
  from souleyez.parsers.hydra_parser import parse_hydra_output
2820
2802
  from souleyez.storage.credentials import CredentialsManager
2821
- from souleyez.storage.hosts import HostManager
2822
2803
  from souleyez.storage.findings import FindingsManager
2804
+ from souleyez.storage.hosts import HostManager
2823
2805
 
2824
2806
  # Read the log file
2825
2807
  with open(log_path, "r", encoding="utf-8", errors="replace") as f:
@@ -3165,9 +3147,9 @@ def parse_enum4linux_job(
3165
3147
  """Parse enum4linux job results."""
3166
3148
  try:
3167
3149
  from souleyez.parsers.enum4linux_parser import (
3168
- parse_enum4linux_output,
3169
- get_smb_stats,
3170
3150
  categorize_share,
3151
+ get_smb_stats,
3152
+ parse_enum4linux_output,
3171
3153
  )
3172
3154
  from souleyez.storage.findings import FindingsManager
3173
3155
  from souleyez.storage.hosts import HostManager
@@ -3310,8 +3292,8 @@ def parse_crackmapexec_job(
3310
3292
  """Parse CrackMapExec job results."""
3311
3293
  try:
3312
3294
  from souleyez.parsers.crackmapexec_parser import parse_crackmapexec
3313
- from souleyez.storage.hosts import HostManager
3314
3295
  from souleyez.storage.credentials import CredentialsManager
3296
+ from souleyez.storage.hosts import HostManager
3315
3297
 
3316
3298
  target = job.get("target", "")
3317
3299
  parsed = parse_crackmapexec(log_path, target)
@@ -3379,8 +3361,8 @@ def parse_ffuf_job(
3379
3361
  """Parse ffuf job results."""
3380
3362
  try:
3381
3363
  from souleyez.parsers.ffuf_parser import parse_ffuf
3382
- from souleyez.storage.hosts import HostManager
3383
3364
  from souleyez.storage.findings import FindingsManager
3365
+ from souleyez.storage.hosts import HostManager
3384
3366
 
3385
3367
  target = job.get("target", "")
3386
3368
  parsed = parse_ffuf(log_path, target)
@@ -3709,8 +3691,8 @@ def parse_nikto_job(
3709
3691
  """Parse nikto web server scanner results."""
3710
3692
  try:
3711
3693
  from souleyez.parsers.nikto_parser import (
3712
- parse_nikto_output,
3713
3694
  generate_next_steps,
3695
+ parse_nikto_output,
3714
3696
  )
3715
3697
  from souleyez.storage.findings import FindingsManager
3716
3698
  from souleyez.storage.hosts import HostManager
@@ -3834,14 +3816,15 @@ def parse_http_fingerprint_job(
3834
3816
  based on detected WAF, CDN, or managed hosting platform.
3835
3817
  """
3836
3818
  try:
3819
+ from urllib.parse import urlparse
3820
+
3837
3821
  from souleyez.parsers.http_fingerprint_parser import (
3838
- parse_http_fingerprint_output,
3839
3822
  build_fingerprint_context,
3840
- get_tool_recommendations,
3841
3823
  generate_next_steps,
3824
+ get_tool_recommendations,
3825
+ parse_http_fingerprint_output,
3842
3826
  )
3843
3827
  from souleyez.storage.hosts import HostManager
3844
- from urllib.parse import urlparse
3845
3828
 
3846
3829
  target = job.get("target", "")
3847
3830
 
@@ -4084,8 +4067,8 @@ def parse_hashcat_job(
4084
4067
  """Parse hashcat job results and extract cracked passwords."""
4085
4068
  try:
4086
4069
  from souleyez.parsers.hashcat_parser import (
4087
- parse_hashcat_output,
4088
4070
  map_to_credentials,
4071
+ parse_hashcat_output,
4089
4072
  )
4090
4073
  from souleyez.storage.credentials import CredentialsManager
4091
4074
  from souleyez.storage.findings import FindingsManager
@@ -2,9 +2,11 @@
2
2
  """
3
3
  Worker health check and management utilities
4
4
  """
5
- import psutil
5
+
6
6
  import time
7
- from typing import Optional, Tuple, Dict, Any
7
+ from typing import Any, Dict, Optional, Tuple
8
+
9
+ import psutil
8
10
 
9
11
 
10
12
  def is_worker_running() -> Tuple[bool, Optional[int]]:
@@ -49,7 +51,7 @@ def is_worker_healthy() -> Tuple[bool, Optional[int], Optional[str]]:
49
51
  - pid: Worker PID if found, None otherwise
50
52
  - issue: Description of issue if not healthy, None otherwise
51
53
  """
52
- from souleyez.engine.background import get_heartbeat_age, HEARTBEAT_STALE_THRESHOLD
54
+ from souleyez.engine.background import HEARTBEAT_STALE_THRESHOLD, get_heartbeat_age
53
55
 
54
56
  is_running, pid = is_worker_running()
55
57
 
@@ -169,7 +171,7 @@ def get_worker_health() -> Dict[str, Any]:
169
171
  - cpu_percent: CPU usage percentage
170
172
  - memory_mb: Memory usage in MB
171
173
  """
172
- from souleyez.engine.background import get_heartbeat_age, HEARTBEAT_STALE_THRESHOLD
174
+ from souleyez.engine.background import HEARTBEAT_STALE_THRESHOLD, get_heartbeat_age
173
175
 
174
176
  is_running, pid = is_worker_running()
175
177
  heartbeat_age = get_heartbeat_age()
@@ -3,6 +3,7 @@
3
3
  Evidence bundle export to ZIP.
4
4
  Creates organized ZIP archive of all engagement evidence.
5
5
  """
6
+
6
7
  import os
7
8
  import zipfile
8
9
  from datetime import datetime
souleyez/handlers/base.py CHANGED
@@ -5,6 +5,7 @@ Base handler class for tool result parsing and display.
5
5
  All tool handlers inherit from BaseToolHandler and implement
6
6
  the required methods for their specific tool.
7
7
  """
8
+
8
9
  import logging
9
10
  from abc import ABC, abstractmethod
10
11
  from typing import Any, Dict, Optional
@@ -5,6 +5,7 @@ Bash handler.
5
5
  Handles bash script execution results, particularly for auto-generated
6
6
  scripts like web login credential tests.
7
7
  """
8
+
8
9
  import logging
9
10
  import os
10
11
  import re
@@ -3,6 +3,7 @@
3
3
  Handler for BloodHound - Active Directory attack path mapping.
4
4
  Parses bloodhound-python collection results.
5
5
  """
6
+
6
7
  import logging
7
8
  import os
8
9
  import re
@@ -3,6 +3,7 @@
3
3
  Handler for Certipy - Active Directory Certificate Services (ADCS) enumeration.
4
4
  Parses vulnerable certificate templates (ESC1-ESC8).
5
5
  """
6
+
6
7
  import logging
7
8
  import os
8
9
  import re