souleyez 2.43.26__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 +23434 -10286
  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.26.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.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.26.dist-info/RECORD +0 -379
  356. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -52,26 +52,28 @@ def parse_enum4linux_output(output: str, target: str = "") -> Dict[str, Any]:
52
52
  def _is_enum4linux_ng_output(output: str) -> bool:
53
53
  """Detect if output is from enum4linux-ng (YAML-style format)."""
54
54
  # Primary indicator - explicit version string (most reliable)
55
- if re.search(r'ENUM4LINUX\s*-\s*next\s*generation', output, re.IGNORECASE):
55
+ if re.search(r"ENUM4LINUX\s*-\s*next\s*generation", output, re.IGNORECASE):
56
56
  return True
57
- if re.search(r'enum4linux-ng', output, re.IGNORECASE):
57
+ if re.search(r"enum4linux-ng", output, re.IGNORECASE):
58
58
  return True
59
59
 
60
60
  # Secondary indicators - look for YAML-style patterns unique to ng
61
61
  ng_indicators = [
62
- re.search(r'After merging (user|share|group) results', output, re.IGNORECASE),
63
- re.search(r'^\s{2,}username:\s+', output, re.MULTILINE), # Indented YAML-style
64
- re.search(r"^'?\d+'?:\s*$", output, re.MULTILINE), # RID entries: '1000': or 1000:
65
- re.search(r'^\s{2,}(groupname|name|type|comment):\s+', output, re.MULTILINE),
66
- re.search(r'Trying to get SID from lsaquery', output, re.IGNORECASE),
62
+ re.search(r"After merging (user|share|group) results", output, re.IGNORECASE),
63
+ re.search(r"^\s{2,}username:\s+", output, re.MULTILINE), # Indented YAML-style
64
+ re.search(
65
+ r"^'?\d+'?:\s*$", output, re.MULTILINE
66
+ ), # RID entries: '1000': or 1000:
67
+ re.search(r"^\s{2,}(groupname|name|type|comment):\s+", output, re.MULTILINE),
68
+ re.search(r"Trying to get SID from lsaquery", output, re.IGNORECASE),
67
69
  ]
68
70
 
69
71
  # Classic enum4linux indicators (to confirm it's NOT ng)
70
72
  classic_indicators = [
71
- re.search(r'enum4linux v\d', output, re.IGNORECASE),
72
- re.search(r'Starting enum4linux v', output, re.IGNORECASE),
73
- re.search(r'Sharename\s+Type\s+Comment', output), # Table header
74
- re.search(r'\|\s+Users on', output),
73
+ re.search(r"enum4linux v\d", output, re.IGNORECASE),
74
+ re.search(r"Starting enum4linux v", output, re.IGNORECASE),
75
+ re.search(r"Sharename\s+Type\s+Comment", output), # Table header
76
+ re.search(r"\|\s+Users on", output),
75
77
  ]
76
78
 
77
79
  ng_count = sum(1 for ind in ng_indicators if ind)
@@ -102,17 +104,17 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
102
104
  type: ...
103
105
  """
104
106
  result = {
105
- 'target': target,
106
- 'workgroup': None,
107
- 'domain_sid': None,
108
- 'shares': [],
109
- 'users': [],
110
- 'groups': []
107
+ "target": target,
108
+ "workgroup": None,
109
+ "domain_sid": None,
110
+ "shares": [],
111
+ "users": [],
112
+ "groups": [],
111
113
  }
112
114
 
113
115
  # Remove ANSI color codes
114
- output = re.sub(r'\x1b\[[0-9;]*m', '', output)
115
- lines = output.split('\n')
116
+ output = re.sub(r"\x1b\[[0-9;]*m", "", output)
117
+ lines = output.split("\n")
116
118
 
117
119
  current_section = None
118
120
  current_item = {}
@@ -121,16 +123,16 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
121
123
  def save_current_item():
122
124
  """Save the current item to the appropriate result list."""
123
125
  nonlocal current_item, in_item
124
- if current_section == 'users' and current_item.get('username'):
125
- username = current_item['username']
126
- if username not in result['users']:
127
- result['users'].append(username)
128
- elif current_section == 'shares' and current_item.get('name'):
129
- result['shares'].append(current_item.copy())
130
- elif current_section == 'groups' and current_item.get('groupname'):
131
- groupname = current_item['groupname']
132
- if groupname not in result['groups']:
133
- result['groups'].append(groupname)
126
+ if current_section == "users" and current_item.get("username"):
127
+ username = current_item["username"]
128
+ if username not in result["users"]:
129
+ result["users"].append(username)
130
+ elif current_section == "shares" and current_item.get("name"):
131
+ result["shares"].append(current_item.copy())
132
+ elif current_section == "groups" and current_item.get("groupname"):
133
+ groupname = current_item["groupname"]
134
+ if groupname not in result["groups"]:
135
+ result["groups"].append(groupname)
134
136
  current_item = {}
135
137
  in_item = False
136
138
 
@@ -138,31 +140,33 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
138
140
  stripped = line.rstrip()
139
141
 
140
142
  # Detect section headers from enum4linux-ng output
141
- if 'After merging user results' in stripped:
143
+ if "After merging user results" in stripped:
142
144
  save_current_item()
143
- current_section = 'users'
145
+ current_section = "users"
144
146
  continue
145
- elif 'After merging share results' in stripped or re.search(r'Found \d+ share\(s\):', stripped):
147
+ elif "After merging share results" in stripped or re.search(
148
+ r"Found \d+ share\(s\):", stripped
149
+ ):
146
150
  save_current_item()
147
- current_section = 'shares'
151
+ current_section = "shares"
148
152
  continue
149
- elif 'After merging group results' in stripped:
153
+ elif "After merging group results" in stripped:
150
154
  save_current_item()
151
- current_section = 'groups'
155
+ current_section = "groups"
152
156
  continue
153
157
 
154
158
  # Extract workgroup/domain
155
- wg_match = re.search(r'Got domain/workgroup name:\s*(\S+)', stripped)
159
+ wg_match = re.search(r"Got domain/workgroup name:\s*(\S+)", stripped)
156
160
  if wg_match:
157
- result['workgroup'] = wg_match.group(1).strip()
161
+ result["workgroup"] = wg_match.group(1).strip()
158
162
 
159
163
  # Extract target
160
- target_match = re.search(r'Target\s+\.+\s+(\S+)', stripped)
164
+ target_match = re.search(r"Target\s+\.+\s+(\S+)", stripped)
161
165
  if target_match:
162
- result['target'] = target_match.group(1).strip()
166
+ result["target"] = target_match.group(1).strip()
163
167
 
164
168
  # Parse user entries in users section
165
- if current_section == 'users':
169
+ if current_section == "users":
166
170
  # Start of new user entry: '1000':
167
171
  rid_match = re.match(r"^'(\d+)':\s*$", stripped)
168
172
  if rid_match:
@@ -172,14 +176,14 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
172
176
 
173
177
  # Username field
174
178
  if in_item:
175
- username_match = re.match(r'^\s+username:\s*(.+)$', stripped)
179
+ username_match = re.match(r"^\s+username:\s*(.+)$", stripped)
176
180
  if username_match:
177
181
  username = username_match.group(1).strip().strip("'\"")
178
- if username and username != '(null)':
179
- current_item['username'] = username
182
+ if username and username != "(null)":
183
+ current_item["username"] = username
180
184
 
181
185
  # Parse share entries in shares section
182
- elif current_section == 'shares':
186
+ elif current_section == "shares":
183
187
  # Start of new share entry: 'print$': or ADMIN$:
184
188
  share_key_match = re.match(r"^'([^']+)':\s*$", stripped)
185
189
  if not share_key_match:
@@ -187,26 +191,26 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
187
191
  share_key_match = re.match(r"^([A-Za-z0-9_$]+):\s*$", stripped)
188
192
  if share_key_match:
189
193
  save_current_item()
190
- current_item = {'name': share_key_match.group(1)}
194
+ current_item = {"name": share_key_match.group(1)}
191
195
  in_item = True
192
196
  continue
193
197
 
194
198
  # Share properties
195
199
  if in_item:
196
- name_match = re.match(r'^\s+name:\s*(.+)$', stripped)
200
+ name_match = re.match(r"^\s+name:\s*(.+)$", stripped)
197
201
  if name_match:
198
- current_item['name'] = name_match.group(1).strip()
202
+ current_item["name"] = name_match.group(1).strip()
199
203
 
200
- type_match = re.match(r'^\s+type:\s*(.+)$', stripped)
204
+ type_match = re.match(r"^\s+type:\s*(.+)$", stripped)
201
205
  if type_match:
202
- current_item['type'] = type_match.group(1).strip()
206
+ current_item["type"] = type_match.group(1).strip()
203
207
 
204
- comment_match = re.match(r'^\s+comment:\s*(.*)$', stripped)
208
+ comment_match = re.match(r"^\s+comment:\s*(.*)$", stripped)
205
209
  if comment_match:
206
- current_item['comment'] = comment_match.group(1).strip()
210
+ current_item["comment"] = comment_match.group(1).strip()
207
211
 
208
212
  # Parse group entries
209
- elif current_section == 'groups':
213
+ elif current_section == "groups":
210
214
  rid_match = re.match(r"^'(\d+)':\s*$", stripped)
211
215
  if rid_match:
212
216
  save_current_item()
@@ -214,11 +218,11 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
214
218
  continue
215
219
 
216
220
  if in_item:
217
- groupname_match = re.match(r'^\s+groupname:\s*(.+)$', stripped)
221
+ groupname_match = re.match(r"^\s+groupname:\s*(.+)$", stripped)
218
222
  if groupname_match:
219
223
  groupname = groupname_match.group(1).strip().strip("'\"")
220
224
  if groupname:
221
- current_item['groupname'] = groupname
225
+ current_item["groupname"] = groupname
222
226
 
223
227
  # Don't forget the last item being parsed
224
228
  save_current_item()
@@ -231,94 +235,100 @@ def _parse_enum4linux_classic_output(output: str, target: str = "") -> Dict[str,
231
235
  Parse original enum4linux table-based output format.
232
236
  """
233
237
  result = {
234
- 'target': target,
235
- 'workgroup': None,
236
- 'domain_sid': None,
237
- 'shares': [],
238
- 'users': [],
239
- 'groups': []
238
+ "target": target,
239
+ "workgroup": None,
240
+ "domain_sid": None,
241
+ "shares": [],
242
+ "users": [],
243
+ "groups": [],
240
244
  }
241
245
 
242
- lines = output.split('\n')
246
+ lines = output.split("\n")
243
247
  current_section = None
244
248
  in_share_table = False
245
249
  share_table_started = False
246
250
 
247
251
  for i, line in enumerate(lines):
248
252
  # Remove ANSI color codes
249
- line = re.sub(r'\x1b\[[0-9;]*m', '', line)
253
+ line = re.sub(r"\x1b\[[0-9;]*m", "", line)
250
254
  line = line.strip()
251
255
 
252
256
  # Extract target
253
- if line.startswith('Target ...........'):
254
- target_match = re.search(r'Target\s+\.+\s+(\S+)', line)
257
+ if line.startswith("Target ..........."):
258
+ target_match = re.search(r"Target\s+\.+\s+(\S+)", line)
255
259
  if target_match:
256
- result['target'] = target_match.group(1)
260
+ result["target"] = target_match.group(1)
257
261
 
258
262
  # Extract workgroup/domain
259
- elif '[+] Got domain/workgroup name:' in line:
260
- wg_match = re.search(r'Got domain/workgroup name:\s+(\S+)', line)
263
+ elif "[+] Got domain/workgroup name:" in line:
264
+ wg_match = re.search(r"Got domain/workgroup name:\s+(\S+)", line)
261
265
  if wg_match:
262
- result['workgroup'] = wg_match.group(1)
266
+ result["workgroup"] = wg_match.group(1)
263
267
 
264
268
  # Extract domain SID
265
- elif line.startswith('Domain Sid:'):
266
- sid_match = re.search(r'Domain Sid:\s+(.+)', line)
269
+ elif line.startswith("Domain Sid:"):
270
+ sid_match = re.search(r"Domain Sid:\s+(.+)", line)
267
271
  if sid_match:
268
272
  sid = sid_match.group(1).strip()
269
- if sid != '(NULL SID)':
270
- result['domain_sid'] = sid
273
+ if sid != "(NULL SID)":
274
+ result["domain_sid"] = sid
271
275
 
272
276
  # Detect share enumeration section
273
- elif 'Share Enumeration on' in line:
274
- current_section = 'shares'
277
+ elif "Share Enumeration on" in line:
278
+ current_section = "shares"
275
279
  in_share_table = False
276
280
  share_table_started = False
277
281
 
278
282
  # Parse share table header
279
- elif current_section == 'shares' and 'Sharename' in line and 'Type' in line:
283
+ elif current_section == "shares" and "Sharename" in line and "Type" in line:
280
284
  in_share_table = True
281
285
  share_table_started = False
282
286
  continue
283
287
 
284
288
  # Parse share separator line
285
- elif current_section == 'shares' and line.startswith('---'):
289
+ elif current_section == "shares" and line.startswith("---"):
286
290
  if in_share_table:
287
291
  share_table_started = True
288
292
  continue
289
293
 
290
294
  # Parse share lines
291
- elif current_section == 'shares' and in_share_table and share_table_started:
295
+ elif current_section == "shares" and in_share_table and share_table_started:
292
296
  # Check if we've left the table
293
- if not line or line.startswith('Reconnecting') or line.startswith('Server') or line.startswith('Workgroup') or line.startswith('['):
297
+ if (
298
+ not line
299
+ or line.startswith("Reconnecting")
300
+ or line.startswith("Server")
301
+ or line.startswith("Workgroup")
302
+ or line.startswith("[")
303
+ ):
294
304
  in_share_table = False
295
305
  continue
296
306
 
297
307
  # Parse share line: "sharename Type Comment"
298
308
  share = _parse_share_line(line)
299
309
  if share:
300
- result['shares'].append(share)
310
+ result["shares"].append(share)
301
311
 
302
312
  # Parse share mapping results
303
- elif current_section == 'shares' and line.startswith('//'):
313
+ elif current_section == "shares" and line.startswith("//"):
304
314
  mapping_info = _parse_share_mapping(line)
305
315
  if mapping_info:
306
316
  # Find and update the matching share
307
- for share in result['shares']:
308
- if mapping_info['name'] in line:
317
+ for share in result["shares"]:
318
+ if mapping_info["name"] in line:
309
319
  share.update(mapping_info)
310
320
  break
311
321
 
312
322
  # Parse users section
313
- elif 'Users on' in line or 'user(s) returned' in line:
314
- current_section = 'users'
323
+ elif "Users on" in line or "user(s) returned" in line:
324
+ current_section = "users"
315
325
 
316
326
  # Parse groups section
317
- elif 'Groups on' in line or 'group(s) returned' in line:
318
- current_section = 'groups'
327
+ elif "Groups on" in line or "group(s) returned" in line:
328
+ current_section = "groups"
319
329
 
320
330
  # Parse user lines from RID cycling output (Local User or Domain User)
321
- elif current_section == 'users' and line and not line.startswith('='):
331
+ elif current_section == "users" and line and not line.startswith("="):
322
332
  # Format variations:
323
333
  # "S-1-5-21-...-RID DOMAIN\username (Local User)"
324
334
  # "S-1-5-21-...-RID DOMAIN\\username (Local User)"
@@ -326,34 +336,50 @@ def _parse_enum4linux_classic_output(output: str, target: str = "") -> Dict[str,
326
336
  # "[+] DOMAIN\username" - alternate prefix
327
337
 
328
338
  # Try full SID format first (flexible escaping)
329
- user_match = re.search(r'S-1-5-21-[\d-]+\s+\S+[\\]+(\S+)\s+\((Local|Domain)\s*User\)', line, re.IGNORECASE)
339
+ user_match = re.search(
340
+ r"S-1-5-21-[\d-]+\s+\S+[\\]+(\S+)\s+\((Local|Domain)\s*User\)",
341
+ line,
342
+ re.IGNORECASE,
343
+ )
330
344
  if user_match:
331
345
  username = user_match.group(1)
332
- if username and username not in result['users']:
333
- result['users'].append(username)
346
+ if username and username not in result["users"]:
347
+ result["users"].append(username)
334
348
  else:
335
349
  # Try simpler DOMAIN\username format
336
- user_match = re.search(r'[\[\+\]\s]*\S+[\\]+(\S+)\s+\((Local|Domain)\s*User\)', line, re.IGNORECASE)
350
+ user_match = re.search(
351
+ r"[\[\+\]\s]*\S+[\\]+(\S+)\s+\((Local|Domain)\s*User\)",
352
+ line,
353
+ re.IGNORECASE,
354
+ )
337
355
  if user_match:
338
356
  username = user_match.group(1)
339
- if username and username not in result['users']:
340
- result['users'].append(username)
357
+ if username and username not in result["users"]:
358
+ result["users"].append(username)
341
359
 
342
360
  # Also parse group lines from RID cycling (Domain Group, Local Group)
343
- elif current_section == 'groups' and line and not line.startswith('='):
361
+ elif current_section == "groups" and line and not line.startswith("="):
344
362
  # Format variations similar to users
345
- group_match = re.search(r'S-1-5-21-[\d-]+\s+\S+[\\]+(\S+)\s+\((Domain|Local)\s*Group\)', line, re.IGNORECASE)
363
+ group_match = re.search(
364
+ r"S-1-5-21-[\d-]+\s+\S+[\\]+(\S+)\s+\((Domain|Local)\s*Group\)",
365
+ line,
366
+ re.IGNORECASE,
367
+ )
346
368
  if group_match:
347
369
  groupname = group_match.group(1)
348
- if groupname and groupname not in result['groups']:
349
- result['groups'].append(groupname)
370
+ if groupname and groupname not in result["groups"]:
371
+ result["groups"].append(groupname)
350
372
  else:
351
373
  # Try simpler format
352
- group_match = re.search(r'[\[\+\]\s]*\S+[\\]+(\S+)\s+\((Domain|Local)\s*Group\)', line, re.IGNORECASE)
374
+ group_match = re.search(
375
+ r"[\[\+\]\s]*\S+[\\]+(\S+)\s+\((Domain|Local)\s*Group\)",
376
+ line,
377
+ re.IGNORECASE,
378
+ )
353
379
  if group_match:
354
380
  groupname = group_match.group(1)
355
- if groupname and groupname not in result['groups']:
356
- result['groups'].append(groupname)
381
+ if groupname and groupname not in result["groups"]:
382
+ result["groups"].append(groupname)
357
383
 
358
384
  return result
359
385
 
@@ -373,50 +399,52 @@ def _parse_share_line(line: str) -> Dict[str, Any]:
373
399
  # Try multiple parsing strategies for different formats
374
400
 
375
401
  # Strategy 1: Split on 2+ whitespace (most common)
376
- parts = re.split(r'\s{2,}', line)
402
+ parts = re.split(r"\s{2,}", line)
377
403
  if len(parts) >= 2:
378
404
  share_name = parts[0].strip()
379
405
  share_type = parts[1].strip()
380
- comment = parts[2].strip() if len(parts) > 2 else ''
406
+ comment = parts[2].strip() if len(parts) > 2 else ""
381
407
 
382
408
  # Validate share type is a known type
383
- if share_type.upper() in ['DISK', 'IPC', 'PRINT', 'PRINTER', 'COMM', 'DEVICE']:
409
+ if share_type.upper() in ["DISK", "IPC", "PRINT", "PRINTER", "COMM", "DEVICE"]:
384
410
  return {
385
- 'name': share_name,
386
- 'type': share_type,
387
- 'comment': comment,
388
- 'mapping': None,
389
- 'listing': None,
390
- 'writing': None
411
+ "name": share_name,
412
+ "type": share_type,
413
+ "comment": comment,
414
+ "mapping": None,
415
+ "listing": None,
416
+ "writing": None,
391
417
  }
392
418
 
393
419
  # Strategy 2: Tab-separated
394
- parts = line.split('\t')
420
+ parts = line.split("\t")
395
421
  if len(parts) >= 2:
396
422
  share_name = parts[0].strip()
397
423
  share_type = parts[1].strip()
398
- comment = parts[2].strip() if len(parts) > 2 else ''
424
+ comment = parts[2].strip() if len(parts) > 2 else ""
399
425
 
400
- if share_type.upper() in ['DISK', 'IPC', 'PRINT', 'PRINTER', 'COMM', 'DEVICE']:
426
+ if share_type.upper() in ["DISK", "IPC", "PRINT", "PRINTER", "COMM", "DEVICE"]:
401
427
  return {
402
- 'name': share_name,
403
- 'type': share_type,
404
- 'comment': comment,
405
- 'mapping': None,
406
- 'listing': None,
407
- 'writing': None
428
+ "name": share_name,
429
+ "type": share_type,
430
+ "comment": comment,
431
+ "mapping": None,
432
+ "listing": None,
433
+ "writing": None,
408
434
  }
409
435
 
410
436
  # Strategy 3: Regex for flexible whitespace (single space minimum)
411
- match = re.match(r'^(\S+)\s+(Disk|IPC|Print|Printer|Comm|Device)\s*(.*)?$', line, re.IGNORECASE)
437
+ match = re.match(
438
+ r"^(\S+)\s+(Disk|IPC|Print|Printer|Comm|Device)\s*(.*)?$", line, re.IGNORECASE
439
+ )
412
440
  if match:
413
441
  return {
414
- 'name': match.group(1),
415
- 'type': match.group(2),
416
- 'comment': match.group(3).strip() if match.group(3) else '',
417
- 'mapping': None,
418
- 'listing': None,
419
- 'writing': None
442
+ "name": match.group(1),
443
+ "type": match.group(2),
444
+ "comment": match.group(3).strip() if match.group(3) else "",
445
+ "mapping": None,
446
+ "listing": None,
447
+ "writing": None,
420
448
  }
421
449
 
422
450
  return None
@@ -431,22 +459,22 @@ def _parse_share_mapping(line: str) -> Dict[str, Any]:
431
459
  """
432
460
  try:
433
461
  # Extract share name from path
434
- share_match = re.search(r'//[^/]+/(\S+)', line)
462
+ share_match = re.search(r"//[^/]+/(\S+)", line)
435
463
  if not share_match:
436
464
  return None
437
465
 
438
466
  share_name = share_match.group(1)
439
467
 
440
468
  # Extract mapping status
441
- mapping_match = re.search(r'Mapping:\s*(\S+)', line)
442
- listing_match = re.search(r'Listing:\s*(\S+)', line)
443
- writing_match = re.search(r'Writing:\s*(\S+)', line)
469
+ mapping_match = re.search(r"Mapping:\s*(\S+)", line)
470
+ listing_match = re.search(r"Listing:\s*(\S+)", line)
471
+ writing_match = re.search(r"Writing:\s*(\S+)", line)
444
472
 
445
473
  return {
446
- 'name': share_name,
447
- 'mapping': mapping_match.group(1) if mapping_match else None,
448
- 'listing': listing_match.group(1) if listing_match else None,
449
- 'writing': writing_match.group(1) if writing_match else None
474
+ "name": share_name,
475
+ "mapping": mapping_match.group(1) if mapping_match else None,
476
+ "listing": listing_match.group(1) if listing_match else None,
477
+ "writing": writing_match.group(1) if writing_match else None,
450
478
  }
451
479
  except Exception:
452
480
  return None
@@ -459,18 +487,20 @@ def get_smb_stats(parsed: Dict[str, Any]) -> Dict[str, Any]:
459
487
  Returns:
460
488
  Dict with counts and summary info
461
489
  """
462
- accessible_shares = sum(1 for s in parsed.get('shares', [])
463
- if s.get('mapping') == 'OK')
490
+ accessible_shares = sum(
491
+ 1 for s in parsed.get("shares", []) if s.get("mapping") == "OK"
492
+ )
464
493
 
465
- writable_shares = sum(1 for s in parsed.get('shares', [])
466
- if s.get('writing') == 'OK')
494
+ writable_shares = sum(
495
+ 1 for s in parsed.get("shares", []) if s.get("writing") == "OK"
496
+ )
467
497
 
468
498
  return {
469
- 'total_shares': len(parsed.get('shares', [])),
470
- 'accessible_shares': accessible_shares,
471
- 'writable_shares': writable_shares,
472
- 'workgroup': parsed.get('workgroup'),
473
- 'has_domain_sid': parsed.get('domain_sid') is not None
499
+ "total_shares": len(parsed.get("shares", [])),
500
+ "accessible_shares": accessible_shares,
501
+ "writable_shares": writable_shares,
502
+ "workgroup": parsed.get("workgroup"),
503
+ "has_domain_sid": parsed.get("domain_sid") is not None,
474
504
  }
475
505
 
476
506
 
@@ -480,15 +510,15 @@ def categorize_share(share: Dict[str, Any]) -> str:
480
510
 
481
511
  Returns: 'open', 'readable', 'restricted', 'denied'
482
512
  """
483
- mapping = share.get('mapping', 'N/A')
484
- listing = share.get('listing', 'N/A')
485
- writing = share.get('writing', 'N/A')
486
-
487
- if writing == 'OK':
488
- return 'open' # Writable = high risk
489
- elif listing == 'OK':
490
- return 'readable' # Readable = medium risk
491
- elif mapping == 'OK':
492
- return 'restricted' # Accessible but limited
513
+ mapping = share.get("mapping", "N/A")
514
+ listing = share.get("listing", "N/A")
515
+ writing = share.get("writing", "N/A")
516
+
517
+ if writing == "OK":
518
+ return "open" # Writable = high risk
519
+ elif listing == "OK":
520
+ return "readable" # Readable = medium risk
521
+ elif mapping == "OK":
522
+ return "restricted" # Accessible but limited
493
523
  else:
494
- return 'denied' # Not accessible
524
+ return "denied" # Not accessible
@@ -10,7 +10,7 @@ def parse_ffuf(log_path: str, target: str) -> Dict[str, Any]:
10
10
  """Parse ffuf JSON output."""
11
11
 
12
12
  try:
13
- with open(log_path, 'r', encoding='utf-8') as f:
13
+ with open(log_path, "r", encoding="utf-8") as f:
14
14
  # ffuf writes JSON on first line, then appends text output
15
15
  # Read only the first line to get the JSON
16
16
  first_line = f.readline()
@@ -20,35 +20,39 @@ def parse_ffuf(log_path: str, target: str) -> Dict[str, Any]:
20
20
  raise ValueError("Empty log file")
21
21
  except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
22
22
  return {
23
- 'tool': 'ffuf',
24
- 'target': target,
25
- 'error': str(e),
26
- 'results_found': 0,
27
- 'paths': []
23
+ "tool": "ffuf",
24
+ "target": target,
25
+ "error": str(e),
26
+ "results_found": 0,
27
+ "paths": [],
28
28
  }
29
29
 
30
30
  results = []
31
31
 
32
- for result in data.get('results', []):
33
- results.append({
34
- 'url': result.get('url'),
35
- 'status_code': result.get('status'), # Normalized to match gobuster
36
- 'size': result.get('length'), # Normalized to match gobuster
37
- 'redirect': result.get('redirectlocation', ''), # Normalized to match gobuster
38
- 'words': result.get('words'),
39
- 'lines': result.get('lines'),
40
- 'content_type': result.get('content-type', ''),
41
- 'input': result.get('input', {})
42
- })
32
+ for result in data.get("results", []):
33
+ results.append(
34
+ {
35
+ "url": result.get("url"),
36
+ "status_code": result.get("status"), # Normalized to match gobuster
37
+ "size": result.get("length"), # Normalized to match gobuster
38
+ "redirect": result.get(
39
+ "redirectlocation", ""
40
+ ), # Normalized to match gobuster
41
+ "words": result.get("words"),
42
+ "lines": result.get("lines"),
43
+ "content_type": result.get("content-type", ""),
44
+ "input": result.get("input", {}),
45
+ }
46
+ )
43
47
 
44
- config = data.get('config', {})
48
+ config = data.get("config", {})
45
49
 
46
50
  return {
47
- 'tool': 'ffuf',
48
- 'target': target,
49
- 'results_found': len(results),
50
- 'wordlist': config.get('wordlist'),
51
- 'method': config.get('method', 'GET'),
52
- 'paths': results,
53
- 'config': config
51
+ "tool": "ffuf",
52
+ "target": target,
53
+ "results_found": len(results),
54
+ "wordlist": config.get("wordlist"),
55
+ "method": config.get("method", "GET"),
56
+ "paths": results,
57
+ "config": config,
54
58
  }