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
@@ -50,7 +50,7 @@ class SplunkSIEMClient(SIEMClient):
50
50
  default_index: Default index to search
51
51
  sourcetypes: List of sourcetypes to include in searches
52
52
  """
53
- self.api_url = api_url.rstrip('/')
53
+ self.api_url = api_url.rstrip("/")
54
54
  self.username = username
55
55
  self.password = password
56
56
  self.verify_ssl = verify_ssl
@@ -60,7 +60,7 @@ class SplunkSIEMClient(SIEMClient):
60
60
  self._session_key: Optional[str] = None
61
61
 
62
62
  @classmethod
63
- def from_config(cls, config: Dict[str, Any]) -> 'SplunkSIEMClient':
63
+ def from_config(cls, config: Dict[str, Any]) -> "SplunkSIEMClient":
64
64
  """Create client from configuration dictionary.
65
65
 
66
66
  Args:
@@ -70,19 +70,19 @@ class SplunkSIEMClient(SIEMClient):
70
70
  SplunkSIEMClient instance
71
71
  """
72
72
  return cls(
73
- api_url=config.get('api_url', ''),
74
- username=config.get('username', ''),
75
- password=config.get('password', ''),
76
- verify_ssl=config.get('verify_ssl', False),
77
- token=config.get('token'),
78
- default_index=config.get('default_index', 'main'),
79
- sourcetypes=config.get('sourcetypes', []),
73
+ api_url=config.get("api_url", ""),
74
+ username=config.get("username", ""),
75
+ password=config.get("password", ""),
76
+ verify_ssl=config.get("verify_ssl", False),
77
+ token=config.get("token"),
78
+ default_index=config.get("default_index", "main"),
79
+ sourcetypes=config.get("sourcetypes", []),
80
80
  )
81
81
 
82
82
  @property
83
83
  def siem_type(self) -> str:
84
84
  """Return the SIEM type identifier."""
85
- return 'splunk'
85
+ return "splunk"
86
86
 
87
87
  def _get_session_key(self) -> str:
88
88
  """Get Splunk session key for authentication.
@@ -100,17 +100,18 @@ class SplunkSIEMClient(SIEMClient):
100
100
  url = f"{self.api_url}/services/auth/login"
101
101
  response = requests.post(
102
102
  url,
103
- data={'username': self.username, 'password': self.password},
103
+ data={"username": self.username, "password": self.password},
104
104
  verify=self.verify_ssl,
105
- timeout=30
105
+ timeout=30,
106
106
  )
107
107
  response.raise_for_status()
108
108
 
109
109
  # Parse XML response to get session key
110
110
  # XML is from authenticated Splunk API response, not untrusted input
111
111
  import xml.etree.ElementTree as ET
112
+
112
113
  root = ET.fromstring(response.text) # nosec B314
113
- session_key = root.find('.//sessionKey')
114
+ session_key = root.find(".//sessionKey")
114
115
  if session_key is not None:
115
116
  self._session_key = session_key.text
116
117
  return self._session_key
@@ -149,7 +150,7 @@ class SplunkSIEMClient(SIEMClient):
149
150
  params=params,
150
151
  data=data,
151
152
  verify=self.verify_ssl,
152
- timeout=60
153
+ timeout=60,
153
154
  )
154
155
  return response
155
156
 
@@ -162,38 +163,34 @@ class SplunkSIEMClient(SIEMClient):
162
163
  try:
163
164
  # Get server info
164
165
  response = self._request(
165
- "GET",
166
- "/services/server/info",
167
- params={'output_mode': 'json'}
166
+ "GET", "/services/server/info", params={"output_mode": "json"}
168
167
  )
169
168
  response.raise_for_status()
170
169
  data = response.json()
171
170
 
172
- entry = data.get('entry', [{}])[0]
173
- content = entry.get('content', {})
171
+ entry = data.get("entry", [{}])[0]
172
+ content = entry.get("content", {})
174
173
 
175
174
  return SIEMConnectionStatus(
176
175
  connected=True,
177
- version=content.get('version', 'unknown'),
178
- siem_type='splunk',
176
+ version=content.get("version", "unknown"),
177
+ siem_type="splunk",
179
178
  details={
180
- 'server_name': content.get('serverName', ''),
181
- 'build': content.get('build', ''),
182
- 'os': content.get('os_name', ''),
183
- 'license': content.get('licenseState', ''),
184
- }
179
+ "server_name": content.get("serverName", ""),
180
+ "build": content.get("build", ""),
181
+ "os": content.get("os_name", ""),
182
+ "license": content.get("licenseState", ""),
183
+ },
185
184
  )
186
185
  except requests.exceptions.ConnectionError as e:
187
186
  return SIEMConnectionStatus(
188
187
  connected=False,
189
188
  error=f"Connection failed: {str(e)}",
190
- siem_type='splunk'
189
+ siem_type="splunk",
191
190
  )
192
191
  except Exception as e:
193
192
  return SIEMConnectionStatus(
194
- connected=False,
195
- error=str(e),
196
- siem_type='splunk'
193
+ connected=False, error=str(e), siem_type="splunk"
197
194
  )
198
195
 
199
196
  def _run_search(
@@ -201,7 +198,7 @@ class SplunkSIEMClient(SIEMClient):
201
198
  spl_query: str,
202
199
  earliest_time: str = "-1h",
203
200
  latest_time: str = "now",
204
- max_results: int = 100
201
+ max_results: int = 100,
205
202
  ) -> List[Dict[str, Any]]:
206
203
  """Run a SPL search and return results.
207
204
 
@@ -219,15 +216,15 @@ class SplunkSIEMClient(SIEMClient):
219
216
  "POST",
220
217
  "/services/search/jobs",
221
218
  data={
222
- 'search': f'search {spl_query}',
223
- 'earliest_time': earliest_time,
224
- 'latest_time': latest_time,
225
- 'output_mode': 'json',
226
- }
219
+ "search": f"search {spl_query}",
220
+ "earliest_time": earliest_time,
221
+ "latest_time": latest_time,
222
+ "output_mode": "json",
223
+ },
227
224
  )
228
225
  response.raise_for_status()
229
226
  data = response.json()
230
- sid = data.get('sid')
227
+ sid = data.get("sid")
231
228
 
232
229
  if not sid:
233
230
  return []
@@ -237,15 +234,13 @@ class SplunkSIEMClient(SIEMClient):
237
234
  waited = 0
238
235
  while waited < max_wait:
239
236
  status_resp = self._request(
240
- "GET",
241
- f"/services/search/jobs/{sid}",
242
- params={'output_mode': 'json'}
237
+ "GET", f"/services/search/jobs/{sid}", params={"output_mode": "json"}
243
238
  )
244
239
  status_data = status_resp.json()
245
- entry = status_data.get('entry', [{}])[0]
246
- content = entry.get('content', {})
240
+ entry = status_data.get("entry", [{}])[0]
241
+ content = entry.get("content", {})
247
242
 
248
- if content.get('isDone'):
243
+ if content.get("isDone"):
249
244
  break
250
245
 
251
246
  time.sleep(1)
@@ -255,12 +250,12 @@ class SplunkSIEMClient(SIEMClient):
255
250
  results_resp = self._request(
256
251
  "GET",
257
252
  f"/services/search/jobs/{sid}/results",
258
- params={'output_mode': 'json', 'count': max_results}
253
+ params={"output_mode": "json", "count": max_results},
259
254
  )
260
255
 
261
256
  if results_resp.status_code == 200:
262
257
  results_data = results_resp.json()
263
- return results_data.get('results', [])
258
+ return results_data.get("results", [])
264
259
 
265
260
  return []
266
261
 
@@ -272,7 +267,7 @@ class SplunkSIEMClient(SIEMClient):
272
267
  dest_ip: Optional[str] = None,
273
268
  rule_ids: Optional[List[str]] = None,
274
269
  search_text: Optional[str] = None,
275
- limit: int = 100
270
+ limit: int = 100,
276
271
  ) -> List[SIEMAlert]:
277
272
  """Query alerts from Splunk.
278
273
 
@@ -289,12 +284,12 @@ class SplunkSIEMClient(SIEMClient):
289
284
  List of normalized SIEMAlert objects
290
285
  """
291
286
  # Build SPL query
292
- query_parts = [f'index={self.default_index}']
287
+ query_parts = [f"index={self.default_index}"]
293
288
 
294
289
  # Add sourcetype filter
295
290
  if self.sourcetypes:
296
- st_filter = ' OR '.join(f'sourcetype="{st}"' for st in self.sourcetypes)
297
- query_parts.append(f'({st_filter})')
291
+ st_filter = " OR ".join(f'sourcetype="{st}"' for st in self.sourcetypes)
292
+ query_parts.append(f"({st_filter})")
298
293
 
299
294
  # IP filters - search common field names and raw text
300
295
  if source_ip:
@@ -316,14 +311,14 @@ class SplunkSIEMClient(SIEMClient):
316
311
 
317
312
  # Rule IDs (saved search names or correlation rule IDs)
318
313
  if rule_ids:
319
- rule_filter = ' OR '.join(f'savedsearch_name="{r}"' for r in rule_ids)
320
- query_parts.append(f'({rule_filter})')
314
+ rule_filter = " OR ".join(f'savedsearch_name="{r}"' for r in rule_ids)
315
+ query_parts.append(f"({rule_filter})")
321
316
 
322
- spl = ' '.join(query_parts)
317
+ spl = " ".join(query_parts)
323
318
 
324
319
  # Time range formatting
325
- earliest = start_time.strftime('%Y-%m-%dT%H:%M:%S')
326
- latest = end_time.strftime('%Y-%m-%dT%H:%M:%S')
320
+ earliest = start_time.strftime("%Y-%m-%dT%H:%M:%S")
321
+ latest = end_time.strftime("%Y-%m-%dT%H:%M:%S")
327
322
 
328
323
  results = self._run_search(spl, earliest, latest, limit)
329
324
  return [self._normalize_alert(r) for r in results]
@@ -341,17 +336,20 @@ class SplunkSIEMClient(SIEMClient):
341
336
 
342
337
  # Try to parse _raw if it's JSON (HEC events store data here)
343
338
  event_data = {}
344
- raw_str = raw_result.get('_raw', '')
339
+ raw_str = raw_result.get("_raw", "")
345
340
  if raw_str and isinstance(raw_str, str):
346
341
  try:
347
342
  parsed = json_lib.loads(raw_str)
348
343
  # HEC wraps in 'event' key, or data might be at top level
349
- event_data = parsed.get('event', parsed) if isinstance(parsed, dict) else {}
344
+ event_data = (
345
+ parsed.get("event", parsed) if isinstance(parsed, dict) else {}
346
+ )
350
347
  except (json_lib.JSONDecodeError, TypeError):
351
348
  # Try to extract embedded JSON from syslog lines
352
349
  # Format: "Jan 7 14:23:38 host program {json...}"
353
350
  import re
354
- json_match = re.search(r'\{.*\}', raw_str)
351
+
352
+ json_match = re.search(r"\{.*\}", raw_str)
355
353
  if json_match:
356
354
  try:
357
355
  event_data = json_lib.loads(json_match.group())
@@ -359,7 +357,7 @@ class SplunkSIEMClient(SIEMClient):
359
357
  pass
360
358
 
361
359
  # Helper to get field from event_data first, then raw_result
362
- def get_field(*keys, default=''):
360
+ def get_field(*keys, default=""):
363
361
  for key in keys:
364
362
  if event_data.get(key):
365
363
  return event_data[key]
@@ -368,21 +366,21 @@ class SplunkSIEMClient(SIEMClient):
368
366
  return default
369
367
 
370
368
  # Parse timestamp
371
- timestamp_str = raw_result.get('_time', '')
369
+ timestamp_str = raw_result.get("_time", "")
372
370
  try:
373
- timestamp = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
371
+ timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
374
372
  except (ValueError, AttributeError):
375
373
  timestamp = datetime.now()
376
374
 
377
375
  # Extract rule/alert info - check event_data first
378
- rule_id = get_field('rule_id', 'rule_name', 'savedsearch_name', 'alert')
379
- rule_name = get_field('rule_name', 'search_name') or rule_id
376
+ rule_id = get_field("rule_id", "rule_name", "savedsearch_name", "alert")
377
+ rule_name = get_field("rule_name", "search_name") or rule_id
380
378
 
381
379
  # For plain log events (no alert fields), use event_type or sourcetype
382
380
  if not rule_id:
383
381
  # Prefer Suricata event_type over generic sourcetype
384
- event_type = get_field('event_type')
385
- sourcetype = raw_result.get('sourcetype', '')
382
+ event_type = get_field("event_type")
383
+ sourcetype = raw_result.get("sourcetype", "")
386
384
  if event_type:
387
385
  rule_id = event_type
388
386
  rule_name = f"Suricata: {event_type}"
@@ -391,89 +389,104 @@ class SplunkSIEMClient(SIEMClient):
391
389
  rule_name = f"Log: {sourcetype}"
392
390
 
393
391
  # Map severity - check event_data first
394
- severity_raw = get_field('severity', default='info')
392
+ severity_raw = get_field("severity", default="info")
395
393
  severity = self._map_severity(severity_raw)
396
394
 
397
395
  # Extract IPs - check event_data first
398
- source_ip = get_field('src_ip', 'src', 'source_ip')
399
- dest_ip = get_field('dest_ip', 'dest', 'destination_ip')
396
+ source_ip = get_field("src_ip", "src", "source_ip")
397
+ dest_ip = get_field("dest_ip", "dest", "destination_ip")
400
398
 
401
399
  # For plain log events, use 'host' as source
402
400
  if not source_ip:
403
- source_ip = raw_result.get('host', '')
401
+ source_ip = raw_result.get("host", "")
404
402
 
405
403
  # Extract description - try multiple sources
406
- description = get_field('description', 'signature', 'message')
404
+ description = get_field("description", "signature", "message")
407
405
 
408
406
  # Suricata-specific: check nested alert object
409
- if not description and event_data.get('alert'):
410
- alert_obj = event_data['alert']
407
+ if not description and event_data.get("alert"):
408
+ alert_obj = event_data["alert"]
411
409
  if isinstance(alert_obj, dict):
412
- description = alert_obj.get('signature', alert_obj.get('category', ''))
410
+ description = alert_obj.get("signature", alert_obj.get("category", ""))
413
411
 
414
412
  # Suricata event_type with context
415
413
  if not description:
416
- event_type = get_field('event_type', 'category')
414
+ event_type = get_field("event_type", "category")
417
415
  if event_type:
418
416
  # Add context based on event type
419
- if event_type == 'dns' and event_data.get('dns'):
420
- dns = event_data['dns']
421
- rrname = dns.get('rrname', '') if isinstance(dns, dict) else ''
417
+ if event_type == "dns" and event_data.get("dns"):
418
+ dns = event_data["dns"]
419
+ rrname = dns.get("rrname", "") if isinstance(dns, dict) else ""
422
420
  description = f"DNS: {rrname}" if rrname else f"DNS query"
423
- elif event_type == 'http' and event_data.get('http'):
424
- http = event_data['http']
425
- hostname = http.get('hostname', '') if isinstance(http, dict) else ''
421
+ elif event_type == "http" and event_data.get("http"):
422
+ http = event_data["http"]
423
+ hostname = (
424
+ http.get("hostname", "") if isinstance(http, dict) else ""
425
+ )
426
426
  description = f"HTTP: {hostname}" if hostname else "HTTP request"
427
- elif event_type == 'flow':
428
- app_proto = get_field('app_proto', default='')
427
+ elif event_type == "flow":
428
+ app_proto = get_field("app_proto", default="")
429
429
  description = f"Flow: {app_proto}" if app_proto else "Network flow"
430
- elif event_type == 'alert':
430
+ elif event_type == "alert":
431
431
  description = "Suricata alert"
432
432
  else:
433
- description = f"{event_type}: {get_field('action', default='detected')}"
433
+ description = (
434
+ f"{event_type}: {get_field('action', default='detected')}"
435
+ )
434
436
 
435
437
  # For plain log events, try to extract something useful from _raw
436
438
  if not description and raw_str:
437
439
  # Skip syslog header to get actual message
438
440
  # Format: "Mon DD HH:MM:SS hostname program: message"
439
441
  import re
442
+
440
443
  # Try to extract message after "program:" or "program["
441
- msg_match = re.search(r'^\w+\s+\d+\s+[\d:]+\s+\S+\s+\S+[:\[]\s*(.+)', raw_str)
444
+ msg_match = re.search(
445
+ r"^\w+\s+\d+\s+[\d:]+\s+\S+\s+\S+[:\[]\s*(.+)", raw_str
446
+ )
442
447
  if msg_match:
443
448
  description = msg_match.group(1).strip()[:150]
444
449
  else:
445
450
  # Fallback: clean up raw log
446
- clean_raw = raw_str.replace('\n', ' ').strip()
451
+ clean_raw = raw_str.replace("\n", " ").strip()
447
452
  # Skip if it's just timestamps/IPs with no real content
448
453
  if len(clean_raw) > 50:
449
- description = clean_raw[:150] + ('...' if len(clean_raw) > 150 else '')
454
+ description = clean_raw[:150] + (
455
+ "..." if len(clean_raw) > 150 else ""
456
+ )
450
457
  else:
451
- description = clean_raw if clean_raw else 'No details available'
458
+ description = clean_raw if clean_raw else "No details available"
452
459
 
453
460
  # Extract MITRE info - check event_data first
454
461
  mitre_tactics = []
455
462
  mitre_techniques = []
456
- mitre_tactic = get_field('mitre_tactic', 'mitre_attack_tactic')
457
- mitre_tech = get_field('mitre_technique', 'mitre_attack_technique_id')
463
+ mitre_tactic = get_field("mitre_tactic", "mitre_attack_tactic")
464
+ mitre_tech = get_field("mitre_technique", "mitre_attack_technique_id")
458
465
  if mitre_tactic:
459
- mitre_tactics = [mitre_tactic] if isinstance(mitre_tactic, str) else mitre_tactic
466
+ mitre_tactics = (
467
+ [mitre_tactic] if isinstance(mitre_tactic, str) else mitre_tactic
468
+ )
460
469
  if mitre_tech:
461
- mitre_techniques = [mitre_tech] if isinstance(mitre_tech, str) else mitre_tech
470
+ mitre_techniques = (
471
+ [mitre_tech] if isinstance(mitre_tech, str) else mitre_tech
472
+ )
462
473
 
463
474
  # Store both raw_result and parsed event_data
464
475
  full_raw = raw_result.copy()
465
476
  if event_data:
466
- full_raw['_parsed_event'] = event_data
477
+ full_raw["_parsed_event"] = event_data
467
478
 
468
479
  return SIEMAlert(
469
- id=raw_result.get('_cd', raw_result.get('_serial', str(hash(raw_str))[:12])),
480
+ id=raw_result.get(
481
+ "_cd", raw_result.get("_serial", str(hash(raw_str))[:12])
482
+ ),
470
483
  timestamp=timestamp,
471
- rule_id=str(rule_id) if rule_id else '',
472
- rule_name=str(rule_name) if rule_name else '',
484
+ rule_id=str(rule_id) if rule_id else "",
485
+ rule_name=str(rule_name) if rule_name else "",
473
486
  severity=severity,
474
487
  source_ip=source_ip if source_ip else None,
475
488
  dest_ip=dest_ip if dest_ip else None,
476
- description=str(description)[:200] if description else '',
489
+ description=str(description)[:200] if description else "",
477
490
  raw_data=full_raw,
478
491
  mitre_tactics=mitre_tactics,
479
492
  mitre_techniques=mitre_techniques,
@@ -482,20 +495,18 @@ class SplunkSIEMClient(SIEMClient):
482
495
  def _map_severity(self, severity: str) -> str:
483
496
  """Map Splunk severity to normalized severity."""
484
497
  severity_lower = str(severity).lower()
485
- if severity_lower in ('critical', 'crit', '1'):
486
- return 'critical'
487
- elif severity_lower in ('high', '2'):
488
- return 'high'
489
- elif severity_lower in ('medium', 'med', '3'):
490
- return 'medium'
491
- elif severity_lower in ('low', '4'):
492
- return 'low'
493
- return 'info'
498
+ if severity_lower in ("critical", "crit", "1"):
499
+ return "critical"
500
+ elif severity_lower in ("high", "2"):
501
+ return "high"
502
+ elif severity_lower in ("medium", "med", "3"):
503
+ return "medium"
504
+ elif severity_lower in ("low", "4"):
505
+ return "low"
506
+ return "info"
494
507
 
495
508
  def get_rules(
496
- self,
497
- rule_ids: Optional[List[str]] = None,
498
- enabled_only: bool = True
509
+ self, rule_ids: Optional[List[str]] = None, enabled_only: bool = True
499
510
  ) -> List[SIEMRule]:
500
511
  """Get saved searches/correlation rules from Splunk.
501
512
 
@@ -510,34 +521,34 @@ class SplunkSIEMClient(SIEMClient):
510
521
  response = self._request(
511
522
  "GET",
512
523
  "/servicesNS/-/-/saved/searches",
513
- params={'output_mode': 'json', 'count': 500}
524
+ params={"output_mode": "json", "count": 500},
514
525
  )
515
526
 
516
527
  if response.status_code != 200:
517
528
  return []
518
529
 
519
530
  data = response.json()
520
- entries = data.get('entry', [])
531
+ entries = data.get("entry", [])
521
532
 
522
533
  rules = []
523
534
  for entry in entries:
524
- name = entry.get('name', '')
525
- content = entry.get('content', {})
535
+ name = entry.get("name", "")
536
+ content = entry.get("content", {})
526
537
 
527
538
  # Filter by rule_ids if provided
528
539
  if rule_ids and name not in rule_ids:
529
540
  continue
530
541
 
531
542
  # Filter disabled if requested
532
- if enabled_only and content.get('disabled', False):
543
+ if enabled_only and content.get("disabled", False):
533
544
  continue
534
545
 
535
546
  rule = SIEMRule(
536
547
  id=name,
537
548
  name=name,
538
- description=content.get('description', ''),
539
- severity=self._map_severity(content.get('alert.severity', '')),
540
- enabled=not content.get('disabled', False),
549
+ description=content.get("description", ""),
550
+ severity=self._map_severity(content.get("alert.severity", "")),
551
+ enabled=not content.get("disabled", False),
541
552
  mitre_tactics=[],
542
553
  mitre_techniques=[],
543
554
  raw_data=content,
@@ -547,9 +558,7 @@ class SplunkSIEMClient(SIEMClient):
547
558
  return rules
548
559
 
549
560
  def get_hosts(
550
- self,
551
- time_range: str = "-24h",
552
- limit: int = 100
561
+ self, time_range: str = "-24h", limit: int = 100
553
562
  ) -> List[Dict[str, Any]]:
554
563
  """Query hosts that have sent data to Splunk.
555
564
 
@@ -564,36 +573,42 @@ class SplunkSIEMClient(SIEMClient):
564
573
  """
565
574
  # Query to get unique hosts with stats
566
575
  spl = (
567
- f'index={self.default_index} '
568
- f'| stats count as event_count, latest(_time) as last_seen, '
569
- f'values(sourcetype) as sourcetypes by host '
570
- f'| sort -last_seen '
571
- f'| head {limit}'
576
+ f"index={self.default_index} "
577
+ f"| stats count as event_count, latest(_time) as last_seen, "
578
+ f"values(sourcetype) as sourcetypes by host "
579
+ f"| sort -last_seen "
580
+ f"| head {limit}"
572
581
  )
573
582
 
574
- results = self._run_search(spl, earliest_time=time_range, latest_time="now", max_results=limit)
583
+ results = self._run_search(
584
+ spl, earliest_time=time_range, latest_time="now", max_results=limit
585
+ )
575
586
 
576
587
  hosts = []
577
588
  for r in results:
578
589
  # Parse sourcetypes (may be multivalue)
579
- sourcetypes_raw = r.get('sourcetypes', '')
590
+ sourcetypes_raw = r.get("sourcetypes", "")
580
591
  if isinstance(sourcetypes_raw, list):
581
592
  sourcetypes = sourcetypes_raw
582
593
  elif isinstance(sourcetypes_raw, str):
583
- sourcetypes = [s.strip() for s in sourcetypes_raw.split(',') if s.strip()]
594
+ sourcetypes = [
595
+ s.strip() for s in sourcetypes_raw.split(",") if s.strip()
596
+ ]
584
597
  else:
585
598
  sourcetypes = []
586
599
 
587
600
  # Infer OS from sourcetypes and hostname
588
- os_name = self._infer_os(sourcetypes, r.get('host', ''))
601
+ os_name = self._infer_os(sourcetypes, r.get("host", ""))
589
602
 
590
- hosts.append({
591
- 'name': r.get('host', 'unknown'),
592
- 'last_seen': r.get('last_seen', ''),
593
- 'event_count': int(r.get('event_count', 0)),
594
- 'sourcetypes': sourcetypes,
595
- 'os': os_name,
596
- })
603
+ hosts.append(
604
+ {
605
+ "name": r.get("host", "unknown"),
606
+ "last_seen": r.get("last_seen", ""),
607
+ "event_count": int(r.get("event_count", 0)),
608
+ "sourcetypes": sourcetypes,
609
+ "os": os_name,
610
+ }
611
+ )
597
612
 
598
613
  return hosts
599
614
 
@@ -613,38 +628,45 @@ class SplunkSIEMClient(SIEMClient):
613
628
  # Check sourcetype patterns
614
629
  for st in sourcetypes_lower:
615
630
  # macOS patterns
616
- if 'macos' in st or 'osx' in st or 'darwin' in st:
617
- return 'macOS'
631
+ if "macos" in st or "osx" in st or "darwin" in st:
632
+ return "macOS"
618
633
  # Windows patterns
619
- if 'winevent' in st or 'windows' in st or 'win:' in st:
620
- return 'Windows'
634
+ if "winevent" in st or "windows" in st or "win:" in st:
635
+ return "Windows"
621
636
  # Linux patterns
622
- if 'linux' in st:
623
- return 'Linux'
637
+ if "linux" in st:
638
+ return "Linux"
624
639
 
625
640
  # Check hostname patterns
626
- if 'mac' in hostname_lower or 'macbook' in hostname_lower or 'imac' in hostname_lower:
627
- return 'macOS'
628
- if 'win' in hostname_lower or 'desktop-' in hostname_lower:
629
- return 'Windows'
641
+ if (
642
+ "mac" in hostname_lower
643
+ or "macbook" in hostname_lower
644
+ or "imac" in hostname_lower
645
+ ):
646
+ return "macOS"
647
+ if "win" in hostname_lower or "desktop-" in hostname_lower:
648
+ return "Windows"
630
649
 
631
650
  # Infer from common sourcetypes
632
651
  for st in sourcetypes_lower:
633
- if st in ('linux_secure', 'linux_audit', 'linux_messages', 'linux_syslog'):
634
- return 'Linux'
635
- if st in ('syslog',):
652
+ if st in ("linux_secure", "linux_audit", "linux_messages", "linux_syslog"):
653
+ return "Linux"
654
+ if st in ("syslog",):
636
655
  # Generic syslog - could be Linux, BSD, or network device
637
656
  # Check hostname for clues
638
- if any(x in hostname_lower for x in ['ubuntu', 'debian', 'centos', 'rhel', 'fedora']):
639
- return 'Linux'
640
- if 'metasploitable' in hostname_lower:
641
- return 'Linux'
657
+ if any(
658
+ x in hostname_lower
659
+ for x in ["ubuntu", "debian", "centos", "rhel", "fedora"]
660
+ ):
661
+ return "Linux"
662
+ if "metasploitable" in hostname_lower:
663
+ return "Linux"
642
664
  # Default syslog to Linux (most common)
643
- return 'Linux'
644
- if 'apache' in st or 'nginx' in st:
645
- return 'Linux' # Most common, though not guaranteed
665
+ return "Linux"
666
+ if "apache" in st or "nginx" in st:
667
+ return "Linux" # Most common, though not guaranteed
646
668
 
647
- return 'Unknown'
669
+ return "Unknown"
648
670
 
649
671
  def get_vulnerabilities(
650
672
  self,
@@ -653,7 +675,7 @@ class SplunkSIEMClient(SIEMClient):
653
675
  severity: Optional[str] = None,
654
676
  agent_name: Optional[str] = None,
655
677
  limit: int = 1000,
656
- time_range: str = "-7d"
678
+ time_range: str = "-7d",
657
679
  ) -> List[Dict[str, Any]]:
658
680
  """Query vulnerability data from Splunk.
659
681
 
@@ -671,7 +693,7 @@ class SplunkSIEMClient(SIEMClient):
671
693
  List of vulnerability dictionaries
672
694
  """
673
695
  # Build SPL query
674
- query_parts = [f'index={index} sourcetype={sourcetype}']
696
+ query_parts = [f"index={index} sourcetype={sourcetype}"]
675
697
 
676
698
  if severity:
677
699
  query_parts.append(f'severity="{severity}"')
@@ -679,30 +701,36 @@ class SplunkSIEMClient(SIEMClient):
679
701
  query_parts.append(f'agent_name="*{agent_name}*"')
680
702
 
681
703
  # Dedup by CVE and agent, get latest
682
- spl = ' '.join(query_parts) + (
683
- f' | dedup cve, agent_name'
684
- f' | table cve, severity, cvss_score, package_name, package_version, '
685
- f'os_name, agent_name, agent_id, detected_at, description'
686
- f' | sort -cvss_score'
687
- f' | head {limit}'
704
+ spl = " ".join(query_parts) + (
705
+ f" | dedup cve, agent_name"
706
+ f" | table cve, severity, cvss_score, package_name, package_version, "
707
+ f"os_name, agent_name, agent_id, detected_at, description"
708
+ f" | sort -cvss_score"
709
+ f" | head {limit}"
688
710
  )
689
711
 
690
- results = self._run_search(spl, earliest_time=time_range, latest_time="now", max_results=limit)
712
+ results = self._run_search(
713
+ spl, earliest_time=time_range, latest_time="now", max_results=limit
714
+ )
691
715
 
692
716
  vulns = []
693
717
  for r in results:
694
- vulns.append({
695
- 'cve_id': r.get('cve', ''),
696
- 'severity': r.get('severity', 'Medium'),
697
- 'cvss_score': float(r.get('cvss_score', 0)) if r.get('cvss_score') else None,
698
- 'package_name': r.get('package_name', ''),
699
- 'package_version': r.get('package_version', ''),
700
- 'os_name': r.get('os_name', ''),
701
- 'agent_name': r.get('agent_name', ''),
702
- 'agent_id': r.get('agent_id', ''),
703
- 'detected_at': r.get('detected_at', ''),
704
- 'description': r.get('description', ''),
705
- })
718
+ vulns.append(
719
+ {
720
+ "cve_id": r.get("cve", ""),
721
+ "severity": r.get("severity", "Medium"),
722
+ "cvss_score": (
723
+ float(r.get("cvss_score", 0)) if r.get("cvss_score") else None
724
+ ),
725
+ "package_name": r.get("package_name", ""),
726
+ "package_version": r.get("package_version", ""),
727
+ "os_name": r.get("os_name", ""),
728
+ "agent_name": r.get("agent_name", ""),
729
+ "agent_id": r.get("agent_id", ""),
730
+ "detected_at": r.get("detected_at", ""),
731
+ "description": r.get("description", ""),
732
+ }
733
+ )
706
734
 
707
735
  return vulns
708
736
 
@@ -710,7 +738,7 @@ class SplunkSIEMClient(SIEMClient):
710
738
  self,
711
739
  index: str = "wazuh_vulns",
712
740
  sourcetype: str = "wazuh:vulnerabilities",
713
- time_range: str = "-7d"
741
+ time_range: str = "-7d",
714
742
  ) -> Dict[str, Any]:
715
743
  """Get vulnerability summary statistics from Splunk.
716
744
 
@@ -724,38 +752,44 @@ class SplunkSIEMClient(SIEMClient):
724
752
  """
725
753
  # Get counts by severity
726
754
  spl = (
727
- f'index={index} sourcetype={sourcetype}'
728
- f' | dedup cve, agent_name'
729
- f' | stats dc(cve) as unique_cves, count as total by severity'
755
+ f"index={index} sourcetype={sourcetype}"
756
+ f" | dedup cve, agent_name"
757
+ f" | stats dc(cve) as unique_cves, count as total by severity"
730
758
  )
731
759
 
732
- results = self._run_search(spl, earliest_time=time_range, latest_time="now", max_results=10)
760
+ results = self._run_search(
761
+ spl, earliest_time=time_range, latest_time="now", max_results=10
762
+ )
733
763
 
734
764
  by_severity = {}
735
765
  total = 0
736
766
  unique_cves = 0
737
767
 
738
768
  for r in results:
739
- sev = r.get('severity', 'Unknown')
740
- count = int(r.get('total', 0))
741
- cve_count = int(r.get('unique_cves', 0))
769
+ sev = r.get("severity", "Unknown")
770
+ count = int(r.get("total", 0))
771
+ cve_count = int(r.get("unique_cves", 0))
742
772
  by_severity[sev] = count
743
773
  total += count
744
774
  unique_cves += cve_count
745
775
 
746
776
  # Get affected agents count
747
777
  spl_agents = (
748
- f'index={index} sourcetype={sourcetype}'
749
- f' | stats dc(agent_name) as agent_count'
778
+ f"index={index} sourcetype={sourcetype}"
779
+ f" | stats dc(agent_name) as agent_count"
780
+ )
781
+ agent_results = self._run_search(
782
+ spl_agents, earliest_time=time_range, latest_time="now", max_results=1
783
+ )
784
+ agent_count = (
785
+ int(agent_results[0].get("agent_count", 0)) if agent_results else 0
750
786
  )
751
- agent_results = self._run_search(spl_agents, earliest_time=time_range, latest_time="now", max_results=1)
752
- agent_count = int(agent_results[0].get('agent_count', 0)) if agent_results else 0
753
787
 
754
788
  return {
755
- 'total': total,
756
- 'unique_cves': unique_cves,
757
- 'by_severity': by_severity,
758
- 'agents_affected': agent_count,
789
+ "total": total,
790
+ "unique_cves": unique_cves,
791
+ "by_severity": by_severity,
792
+ "agents_affected": agent_count,
759
793
  }
760
794
 
761
795
  def get_recommended_rules(self, attack_type: str) -> List[Dict[str, Any]]:
@@ -769,25 +803,25 @@ class SplunkSIEMClient(SIEMClient):
769
803
  """
770
804
  # Splunk-specific rule recommendations
771
805
  recommendations_map = {
772
- 'nmap': [
806
+ "nmap": [
773
807
  {
774
- 'rule_id': 'Network_Port_Scan_Detection',
775
- 'rule_name': 'Network Port Scan Detection',
776
- 'spl': 'index=* sourcetype=firewall | stats count by src_ip dest_port | where count > 100',
808
+ "rule_id": "Network_Port_Scan_Detection",
809
+ "rule_name": "Network Port Scan Detection",
810
+ "spl": "index=* sourcetype=firewall | stats count by src_ip dest_port | where count > 100",
777
811
  },
778
812
  ],
779
- 'hydra': [
813
+ "hydra": [
780
814
  {
781
- 'rule_id': 'Brute_Force_Authentication',
782
- 'rule_name': 'Brute Force Authentication Detection',
783
- 'spl': 'index=* sourcetype=*auth* | stats count by src_ip user | where count > 10',
815
+ "rule_id": "Brute_Force_Authentication",
816
+ "rule_name": "Brute Force Authentication Detection",
817
+ "spl": "index=* sourcetype=*auth* | stats count by src_ip user | where count > 10",
784
818
  },
785
819
  ],
786
- 'sqlmap': [
820
+ "sqlmap": [
787
821
  {
788
- 'rule_id': 'SQL_Injection_Attempt',
789
- 'rule_name': 'SQL Injection Attempt Detection',
790
- 'spl': 'index=* sourcetype=*web* | search *UNION* OR *SELECT* | stats count by src_ip',
822
+ "rule_id": "SQL_Injection_Attempt",
823
+ "rule_name": "SQL Injection Attempt Detection",
824
+ "spl": "index=* sourcetype=*web* | search *UNION* OR *SELECT* | stats count by src_ip",
791
825
  },
792
826
  ],
793
827
  }
@@ -797,13 +831,13 @@ class SplunkSIEMClient(SIEMClient):
797
831
 
798
832
  return [
799
833
  {
800
- 'rule_id': r['rule_id'],
801
- 'rule_name': r['rule_name'],
802
- 'description': f"Splunk saved search for detecting {attack_type}",
803
- 'severity': 'high',
804
- 'enabled': True,
805
- 'siem_type': 'splunk',
806
- 'spl_query': r.get('spl', ''),
834
+ "rule_id": r["rule_id"],
835
+ "rule_name": r["rule_name"],
836
+ "description": f"Splunk saved search for detecting {attack_type}",
837
+ "severity": "high",
838
+ "enabled": True,
839
+ "siem_type": "splunk",
840
+ "spl_query": r.get("spl", ""),
807
841
  }
808
842
  for r in recommendations
809
843
  ]