agentic-threat-hunting-framework 0.3.1__py3-none-any.whl → 0.5.0__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.
- {agentic_threat_hunting_framework-0.3.1.dist-info → agentic_threat_hunting_framework-0.5.0.dist-info}/METADATA +4 -1
- {agentic_threat_hunting_framework-0.3.1.dist-info → agentic_threat_hunting_framework-0.5.0.dist-info}/RECORD +27 -17
- {agentic_threat_hunting_framework-0.3.1.dist-info → agentic_threat_hunting_framework-0.5.0.dist-info}/WHEEL +1 -1
- athf/__version__.py +1 -1
- athf/cli.py +25 -10
- athf/commands/__init__.py +24 -3
- athf/commands/agent.py +43 -1
- athf/commands/hunt.py +63 -12
- athf/commands/splunk.py +323 -0
- athf/core/clickhouse_connection.py +396 -0
- athf/core/metrics_tracker.py +518 -0
- athf/core/query_executor.py +169 -0
- athf/core/query_parser.py +203 -0
- athf/core/query_suggester.py +235 -0
- athf/core/query_validator.py +240 -0
- athf/core/session_manager.py +764 -0
- athf/core/splunk_client.py +360 -0
- athf/core/template_engine.py +7 -1
- athf/data/docs/CHANGELOG.md +29 -0
- athf/data/docs/CLI_REFERENCE.md +518 -12
- athf/data/docs/getting-started.md +47 -3
- athf/data/docs/level4-agentic-workflows.md +9 -1
- athf/data/docs/maturity-model.md +56 -14
- athf/plugin_system.py +48 -0
- {agentic_threat_hunting_framework-0.3.1.dist-info → agentic_threat_hunting_framework-0.5.0.dist-info}/entry_points.txt +0 -0
- {agentic_threat_hunting_framework-0.3.1.dist-info → agentic_threat_hunting_framework-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {agentic_threat_hunting_framework-0.3.1.dist-info → agentic_threat_hunting_framework-0.5.0.dist-info}/top_level.txt +0 -0
athf/commands/splunk.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Splunk integration commands for ATHF.
|
|
2
|
+
|
|
3
|
+
This module provides CLI commands for interacting with Splunk via REST API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from athf.core.splunk_client import SplunkClient
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_client(host: Optional[str], token: Optional[str], verify_ssl: Optional[bool]) -> SplunkClient:
|
|
20
|
+
"""Get Splunk client from CLI args or environment variables.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
host: Splunk host (from CLI)
|
|
24
|
+
token: Auth token (from CLI)
|
|
25
|
+
verify_ssl: Whether to verify SSL (None to read from env)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Configured SplunkClient
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
click.UsageError: If credentials are not provided
|
|
32
|
+
"""
|
|
33
|
+
# Try CLI args first, fall back to environment
|
|
34
|
+
if not host:
|
|
35
|
+
host = os.getenv("SPLUNK_HOST")
|
|
36
|
+
if not token:
|
|
37
|
+
token = os.getenv("SPLUNK_TOKEN")
|
|
38
|
+
|
|
39
|
+
# Read verify_ssl from environment if not provided via CLI
|
|
40
|
+
if verify_ssl is None:
|
|
41
|
+
env_verify = os.getenv("SPLUNK_VERIFY_SSL", "true")
|
|
42
|
+
verify_ssl = env_verify.lower() in ("true", "1", "yes")
|
|
43
|
+
|
|
44
|
+
if not host or not token:
|
|
45
|
+
raise click.UsageError(
|
|
46
|
+
"Splunk credentials required. Provide via:\n"
|
|
47
|
+
" • CLI: --host and --token flags\n"
|
|
48
|
+
" • Environment: SPLUNK_HOST and SPLUNK_TOKEN variables\n"
|
|
49
|
+
" • Config file: Create .env with credentials"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return SplunkClient(host=host, token=token, verify_ssl=verify_ssl)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.group()
|
|
56
|
+
def splunk() -> None:
|
|
57
|
+
"""Splunk REST API integration.
|
|
58
|
+
|
|
59
|
+
\b
|
|
60
|
+
Execute SPL queries and interact with Splunk directly from ATHF CLI.
|
|
61
|
+
|
|
62
|
+
\b
|
|
63
|
+
Setup:
|
|
64
|
+
1. Create a Splunk authentication token:
|
|
65
|
+
Settings → Tokens → New Token
|
|
66
|
+
|
|
67
|
+
2. Set environment variables (recommended):
|
|
68
|
+
export SPLUNK_HOST="splunk.example.com"
|
|
69
|
+
export SPLUNK_TOKEN="your-token-here"
|
|
70
|
+
|
|
71
|
+
3. Or use --host and --token flags with each command
|
|
72
|
+
|
|
73
|
+
\b
|
|
74
|
+
Examples:
|
|
75
|
+
# Test connection
|
|
76
|
+
athf splunk test
|
|
77
|
+
|
|
78
|
+
# List available indexes
|
|
79
|
+
athf splunk indexes
|
|
80
|
+
|
|
81
|
+
# Execute a query
|
|
82
|
+
athf splunk search 'index=main "Failed password" | head 10'
|
|
83
|
+
|
|
84
|
+
# Query with time range
|
|
85
|
+
athf splunk search 'index=* | stats count by sourcetype' \\
|
|
86
|
+
--earliest "-7d" --latest "now" --count 100
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@splunk.command()
|
|
91
|
+
@click.option("--host", envvar="SPLUNK_HOST", help="Splunk host (e.g., splunk.example.com)")
|
|
92
|
+
@click.option("--token", envvar="SPLUNK_TOKEN", help="Splunk authentication token")
|
|
93
|
+
@click.option("--verify-ssl/--no-verify-ssl", default=None, help="Verify SSL certificates")
|
|
94
|
+
def test(host: Optional[str], token: Optional[str], verify_ssl: Optional[bool]) -> None:
|
|
95
|
+
"""Test Splunk connection and authentication.
|
|
96
|
+
|
|
97
|
+
\b
|
|
98
|
+
Validates that:
|
|
99
|
+
• Host is reachable
|
|
100
|
+
• Token is valid
|
|
101
|
+
• API access is working
|
|
102
|
+
|
|
103
|
+
\b
|
|
104
|
+
Example:
|
|
105
|
+
athf splunk test
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
client = get_client(host, token, verify_ssl)
|
|
109
|
+
info = client.test_connection()
|
|
110
|
+
|
|
111
|
+
console.print("\n[bold green]✓ Connection successful![/bold green]\n")
|
|
112
|
+
|
|
113
|
+
# Display server info
|
|
114
|
+
if "entry" in info and len(info["entry"]) > 0:
|
|
115
|
+
content = info["entry"][0].get("content", {})
|
|
116
|
+
console.print(f"[bold]Server:[/bold] {content.get('serverName', 'N/A')}")
|
|
117
|
+
console.print(f"[bold]Version:[/bold] {content.get('version', 'N/A')}")
|
|
118
|
+
console.print(f"[bold]Build:[/bold] {content.get('build', 'N/A')}")
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
console.print(f"\n[bold red]✗ Connection failed:[/bold red] {e}\n", style="red")
|
|
122
|
+
raise click.Abort()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@splunk.command()
|
|
126
|
+
@click.option("--host", envvar="SPLUNK_HOST", help="Splunk host")
|
|
127
|
+
@click.option("--token", envvar="SPLUNK_TOKEN", help="Splunk authentication token")
|
|
128
|
+
@click.option("--verify-ssl/--no-verify-ssl", default=None, help="Verify SSL certificates")
|
|
129
|
+
@click.option("--format", "output_format", type=click.Choice(["table", "json", "list"]), default="list", help="Output format")
|
|
130
|
+
def indexes(host: Optional[str], token: Optional[str], verify_ssl: Optional[bool], output_format: str) -> None:
|
|
131
|
+
"""List available Splunk indexes.
|
|
132
|
+
|
|
133
|
+
\b
|
|
134
|
+
Shows all indexes accessible with current credentials.
|
|
135
|
+
|
|
136
|
+
\b
|
|
137
|
+
Example:
|
|
138
|
+
athf splunk indexes
|
|
139
|
+
athf splunk indexes --format json
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
client = get_client(host, token, verify_ssl)
|
|
143
|
+
index_list = client.get_indexes()
|
|
144
|
+
|
|
145
|
+
if not index_list:
|
|
146
|
+
console.print("[yellow]No indexes found[/yellow]")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if output_format == "json":
|
|
150
|
+
click.echo(json.dumps({"indexes": index_list}, indent=2))
|
|
151
|
+
elif output_format == "table":
|
|
152
|
+
table = Table(title=f"Splunk Indexes ({len(index_list)} total)")
|
|
153
|
+
table.add_column("Index Name", style="cyan")
|
|
154
|
+
for idx in sorted(index_list):
|
|
155
|
+
table.add_row(idx)
|
|
156
|
+
console.print(table)
|
|
157
|
+
else: # list
|
|
158
|
+
console.print(f"\n[bold]Available Indexes ({len(index_list)}):[/bold]\n")
|
|
159
|
+
for idx in sorted(index_list):
|
|
160
|
+
console.print(f" • {idx}")
|
|
161
|
+
console.print()
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
console.print(f"[bold red]Error:[/bold red] {e}", style="red")
|
|
165
|
+
raise click.Abort()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@splunk.command()
|
|
169
|
+
@click.argument("query")
|
|
170
|
+
@click.option("--host", envvar="SPLUNK_HOST", help="Splunk host")
|
|
171
|
+
@click.option("--token", envvar="SPLUNK_TOKEN", help="Splunk authentication token")
|
|
172
|
+
@click.option("--verify-ssl/--no-verify-ssl", default=None, help="Verify SSL certificates")
|
|
173
|
+
@click.option("--earliest", default="-24h", help="Earliest time (e.g., '-24h', '2024-01-01T00:00:00')")
|
|
174
|
+
@click.option("--latest", default="now", help="Latest time (e.g., 'now', '2024-01-02T00:00:00')")
|
|
175
|
+
@click.option("--count", default=100, type=int, help="Maximum results to return")
|
|
176
|
+
@click.option("--format", "output_format", type=click.Choice(["json", "table", "raw"]), default="json", help="Output format")
|
|
177
|
+
@click.option("--async-search/--oneshot", "use_async", default=False, help="Use async search for long queries")
|
|
178
|
+
@click.option("--max-wait", default=300, type=int, help="Max wait time for async searches (seconds)")
|
|
179
|
+
def search(
|
|
180
|
+
query: str,
|
|
181
|
+
host: Optional[str],
|
|
182
|
+
token: Optional[str],
|
|
183
|
+
verify_ssl: Optional[bool],
|
|
184
|
+
earliest: str,
|
|
185
|
+
latest: str,
|
|
186
|
+
count: int,
|
|
187
|
+
output_format: str,
|
|
188
|
+
use_async: bool,
|
|
189
|
+
max_wait: int,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Execute a Splunk search query.
|
|
192
|
+
|
|
193
|
+
\b
|
|
194
|
+
Runs SPL (Splunk Processing Language) queries and returns results.
|
|
195
|
+
|
|
196
|
+
\b
|
|
197
|
+
Query Examples:
|
|
198
|
+
'index=main "Failed password"'
|
|
199
|
+
'index=* sourcetype=linux_secure | stats count by user'
|
|
200
|
+
'index=web status>=400 | timechart count by status'
|
|
201
|
+
|
|
202
|
+
\b
|
|
203
|
+
Time Format Examples:
|
|
204
|
+
--earliest "-1h" (last hour)
|
|
205
|
+
--earliest "-7d" (last 7 days)
|
|
206
|
+
--earliest "2024-01-01T00:00:00" (absolute time)
|
|
207
|
+
|
|
208
|
+
\b
|
|
209
|
+
Examples:
|
|
210
|
+
# Basic search
|
|
211
|
+
athf splunk search 'index=main error'
|
|
212
|
+
|
|
213
|
+
# With time range
|
|
214
|
+
athf splunk search 'index=* | stats count by sourcetype' \\
|
|
215
|
+
--earliest "-7d" --count 1000
|
|
216
|
+
|
|
217
|
+
# JSON output for parsing
|
|
218
|
+
athf splunk search 'index=main | head 10' --format json
|
|
219
|
+
|
|
220
|
+
# Long-running query (async)
|
|
221
|
+
athf splunk search 'index=* | rare limit=20 sourcetype' \\
|
|
222
|
+
--async-search --max-wait 600
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
client = get_client(host, token, verify_ssl)
|
|
226
|
+
|
|
227
|
+
console.print(f"\n[bold]Executing query:[/bold] {query}")
|
|
228
|
+
console.print(f"[bold]Time range:[/bold] {earliest} to {latest}")
|
|
229
|
+
console.print(f"[bold]Max results:[/bold] {count}\n")
|
|
230
|
+
|
|
231
|
+
# Execute search
|
|
232
|
+
if use_async:
|
|
233
|
+
console.print("[dim]Using async search (for longer queries)...[/dim]")
|
|
234
|
+
results = client.search_async(
|
|
235
|
+
query=query, earliest_time=earliest, latest_time=latest, max_results=count, max_wait=max_wait
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
console.print("[dim]Using oneshot search (fast for small queries)...[/dim]")
|
|
239
|
+
results = client.search(query=query, earliest_time=earliest, latest_time=latest, max_count=count)
|
|
240
|
+
|
|
241
|
+
if not results:
|
|
242
|
+
console.print("[yellow]No results found[/yellow]")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
console.print(f"[green]✓ Found {len(results)} results[/green]\n")
|
|
246
|
+
|
|
247
|
+
# Output results
|
|
248
|
+
if output_format == "json":
|
|
249
|
+
click.echo(json.dumps(results, indent=2, default=str))
|
|
250
|
+
elif output_format == "table":
|
|
251
|
+
if not results:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Extract all unique fields
|
|
255
|
+
all_fields: set[str] = set()
|
|
256
|
+
for result in results:
|
|
257
|
+
all_fields.update(result.keys())
|
|
258
|
+
|
|
259
|
+
# Create table
|
|
260
|
+
table = Table(title=f"Search Results ({len(results)} events)")
|
|
261
|
+
for field in sorted(all_fields):
|
|
262
|
+
table.add_column(field, overflow="fold")
|
|
263
|
+
|
|
264
|
+
# Add rows
|
|
265
|
+
for result in results[:count]: # Limit display
|
|
266
|
+
row = [str(result.get(field, "")) for field in sorted(all_fields)]
|
|
267
|
+
table.add_row(*row)
|
|
268
|
+
|
|
269
|
+
console.print(table)
|
|
270
|
+
else: # raw
|
|
271
|
+
for i, result in enumerate(results, 1):
|
|
272
|
+
console.print(f"[bold cyan]Event {i}:[/bold cyan]")
|
|
273
|
+
for key, value in result.items():
|
|
274
|
+
console.print(f" {key}: {value}")
|
|
275
|
+
console.print()
|
|
276
|
+
|
|
277
|
+
except TimeoutError as e:
|
|
278
|
+
console.print(f"\n[bold red]Timeout:[/bold red] {e}", style="red")
|
|
279
|
+
console.print("[yellow]Try using --async-search for long queries[/yellow]")
|
|
280
|
+
raise click.Abort()
|
|
281
|
+
except Exception as e:
|
|
282
|
+
console.print(f"\n[bold red]Error:[/bold red] {e}", style="red")
|
|
283
|
+
raise click.Abort()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@splunk.command()
|
|
287
|
+
@click.option("--host", envvar="SPLUNK_HOST", help="Splunk host")
|
|
288
|
+
@click.option("--token", envvar="SPLUNK_TOKEN", help="Splunk authentication token")
|
|
289
|
+
@click.option("--verify-ssl/--no-verify-ssl", default=None, help="Verify SSL certificates")
|
|
290
|
+
def config(host: Optional[str], token: Optional[str], verify_ssl: Optional[bool]) -> None:
|
|
291
|
+
"""Show current Splunk configuration.
|
|
292
|
+
|
|
293
|
+
\b
|
|
294
|
+
Displays configuration from environment variables and validates credentials.
|
|
295
|
+
|
|
296
|
+
\b
|
|
297
|
+
Example:
|
|
298
|
+
athf splunk config
|
|
299
|
+
"""
|
|
300
|
+
console.print("\n[bold]Splunk Configuration:[/bold]\n")
|
|
301
|
+
|
|
302
|
+
# Check environment
|
|
303
|
+
env_host = os.getenv("SPLUNK_HOST")
|
|
304
|
+
env_token = os.getenv("SPLUNK_TOKEN")
|
|
305
|
+
env_verify = os.getenv("SPLUNK_VERIFY_SSL", "true")
|
|
306
|
+
|
|
307
|
+
console.print(f"[bold]SPLUNK_HOST:[/bold] {env_host or '[red]Not set[/red]'}")
|
|
308
|
+
console.print(f"[bold]SPLUNK_TOKEN:[/bold] {'[green]Set[/green]' if env_token else '[red]Not set[/red]'}")
|
|
309
|
+
console.print(f"[bold]SPLUNK_VERIFY_SSL:[/bold] {env_verify}")
|
|
310
|
+
|
|
311
|
+
# Test connection if credentials available
|
|
312
|
+
if (host or env_host) and (token or env_token):
|
|
313
|
+
console.print("\n[dim]Testing connection...[/dim]")
|
|
314
|
+
try:
|
|
315
|
+
# get_client will read environment variable if verify_ssl is None
|
|
316
|
+
client = get_client(host, token, verify_ssl)
|
|
317
|
+
client.test_connection()
|
|
318
|
+
console.print("[bold green]✓ Connection successful[/bold green]\n")
|
|
319
|
+
except Exception as e:
|
|
320
|
+
console.print(f"[bold red]✗ Connection failed:[/bold red] {e}\n")
|
|
321
|
+
else:
|
|
322
|
+
console.print("\n[yellow]⚠ Missing credentials - cannot test connection[/yellow]\n")
|
|
323
|
+
console.print("[dim]Set SPLUNK_HOST and SPLUNK_TOKEN environment variables[/dim]\n")
|