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.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +22827 -10678
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {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]) ->
|
|
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(
|
|
74
|
-
username=config.get(
|
|
75
|
-
password=config.get(
|
|
76
|
-
verify_ssl=config.get(
|
|
77
|
-
token=config.get(
|
|
78
|
-
default_index=config.get(
|
|
79
|
-
sourcetypes=config.get(
|
|
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
|
|
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={
|
|
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(
|
|
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(
|
|
173
|
-
content = entry.get(
|
|
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(
|
|
178
|
-
siem_type=
|
|
176
|
+
version=content.get("version", "unknown"),
|
|
177
|
+
siem_type="splunk",
|
|
179
178
|
details={
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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=
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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(
|
|
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(
|
|
246
|
-
content = entry.get(
|
|
240
|
+
entry = status_data.get("entry", [{}])[0]
|
|
241
|
+
content = entry.get("content", {})
|
|
247
242
|
|
|
248
|
-
if content.get(
|
|
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={
|
|
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(
|
|
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
|
|
287
|
+
query_parts = [f"index={self.default_index}"]
|
|
293
288
|
|
|
294
289
|
# Add sourcetype filter
|
|
295
290
|
if self.sourcetypes:
|
|
296
|
-
st_filter =
|
|
297
|
-
query_parts.append(f
|
|
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 =
|
|
320
|
-
query_parts.append(f
|
|
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 =
|
|
317
|
+
spl = " ".join(query_parts)
|
|
323
318
|
|
|
324
319
|
# Time range formatting
|
|
325
|
-
earliest = start_time.strftime(
|
|
326
|
-
latest = end_time.strftime(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
369
|
+
timestamp_str = raw_result.get("_time", "")
|
|
372
370
|
try:
|
|
373
|
-
timestamp = datetime.fromisoformat(timestamp_str.replace(
|
|
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(
|
|
379
|
-
rule_name = get_field(
|
|
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(
|
|
385
|
-
sourcetype = raw_result.get(
|
|
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(
|
|
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(
|
|
399
|
-
dest_ip = get_field(
|
|
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(
|
|
401
|
+
source_ip = raw_result.get("host", "")
|
|
404
402
|
|
|
405
403
|
# Extract description - try multiple sources
|
|
406
|
-
description = get_field(
|
|
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(
|
|
410
|
-
alert_obj = event_data[
|
|
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(
|
|
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(
|
|
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 ==
|
|
420
|
-
dns = event_data[
|
|
421
|
-
rrname = dns.get(
|
|
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 ==
|
|
424
|
-
http = event_data[
|
|
425
|
-
hostname =
|
|
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 ==
|
|
428
|
-
app_proto = get_field(
|
|
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 ==
|
|
430
|
+
elif event_type == "alert":
|
|
431
431
|
description = "Suricata alert"
|
|
432
432
|
else:
|
|
433
|
-
description =
|
|
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(
|
|
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(
|
|
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] + (
|
|
454
|
+
description = clean_raw[:150] + (
|
|
455
|
+
"..." if len(clean_raw) > 150 else ""
|
|
456
|
+
)
|
|
450
457
|
else:
|
|
451
|
-
description = clean_raw if clean_raw else
|
|
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(
|
|
457
|
-
mitre_tech = get_field(
|
|
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 =
|
|
466
|
+
mitre_tactics = (
|
|
467
|
+
[mitre_tactic] if isinstance(mitre_tactic, str) else mitre_tactic
|
|
468
|
+
)
|
|
460
469
|
if mitre_tech:
|
|
461
|
-
mitre_techniques =
|
|
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[
|
|
477
|
+
full_raw["_parsed_event"] = event_data
|
|
467
478
|
|
|
468
479
|
return SIEMAlert(
|
|
469
|
-
id=raw_result.get(
|
|
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 (
|
|
486
|
-
return
|
|
487
|
-
elif severity_lower in (
|
|
488
|
-
return
|
|
489
|
-
elif severity_lower in (
|
|
490
|
-
return
|
|
491
|
-
elif severity_lower in (
|
|
492
|
-
return
|
|
493
|
-
return
|
|
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={
|
|
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(
|
|
531
|
+
entries = data.get("entry", [])
|
|
521
532
|
|
|
522
533
|
rules = []
|
|
523
534
|
for entry in entries:
|
|
524
|
-
name = entry.get(
|
|
525
|
-
content = entry.get(
|
|
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(
|
|
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(
|
|
539
|
-
severity=self._map_severity(content.get(
|
|
540
|
-
enabled=not content.get(
|
|
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
|
|
568
|
-
f
|
|
569
|
-
f
|
|
570
|
-
f
|
|
571
|
-
f
|
|
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(
|
|
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(
|
|
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 = [
|
|
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(
|
|
601
|
+
os_name = self._infer_os(sourcetypes, r.get("host", ""))
|
|
589
602
|
|
|
590
|
-
hosts.append(
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
617
|
-
return
|
|
631
|
+
if "macos" in st or "osx" in st or "darwin" in st:
|
|
632
|
+
return "macOS"
|
|
618
633
|
# Windows patterns
|
|
619
|
-
if
|
|
620
|
-
return
|
|
634
|
+
if "winevent" in st or "windows" in st or "win:" in st:
|
|
635
|
+
return "Windows"
|
|
621
636
|
# Linux patterns
|
|
622
|
-
if
|
|
623
|
-
return
|
|
637
|
+
if "linux" in st:
|
|
638
|
+
return "Linux"
|
|
624
639
|
|
|
625
640
|
# Check hostname patterns
|
|
626
|
-
if
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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 (
|
|
634
|
-
return
|
|
635
|
-
if st in (
|
|
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(
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
644
|
-
if
|
|
645
|
-
return
|
|
665
|
+
return "Linux"
|
|
666
|
+
if "apache" in st or "nginx" in st:
|
|
667
|
+
return "Linux" # Most common, though not guaranteed
|
|
646
668
|
|
|
647
|
-
return
|
|
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
|
|
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 =
|
|
683
|
-
f
|
|
684
|
-
f
|
|
685
|
-
f
|
|
686
|
-
f
|
|
687
|
-
f
|
|
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(
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
728
|
-
f
|
|
729
|
-
f
|
|
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(
|
|
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(
|
|
740
|
-
count = int(r.get(
|
|
741
|
-
cve_count = int(r.get(
|
|
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
|
|
749
|
-
f
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
806
|
+
"nmap": [
|
|
773
807
|
{
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
813
|
+
"hydra": [
|
|
780
814
|
{
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
820
|
+
"sqlmap": [
|
|
787
821
|
{
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
]
|