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.
@@ -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")