agentic-threat-hunting-framework 0.1.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.
athf/commands/hunt.py ADDED
@@ -0,0 +1,596 @@
1
+ """Hunt management commands."""
2
+
3
+ import random
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional, Tuple
7
+
8
+ import click
9
+ import yaml
10
+ from rich import box
11
+ from rich.console import Console
12
+ from rich.prompt import Prompt
13
+ from rich.table import Table
14
+
15
+ from athf.core.hunt_manager import HuntManager
16
+ from athf.core.hunt_parser import validate_hunt_file
17
+ from athf.core.template_engine import render_hunt_template
18
+
19
+ console = Console()
20
+
21
+
22
+ def get_config_path() -> Path:
23
+ """Get config file path, checking new location first, then falling back to root."""
24
+ new_location = Path("config/.athfconfig.yaml")
25
+ old_location = Path(".athfconfig.yaml")
26
+
27
+ if new_location.exists():
28
+ return new_location
29
+ if old_location.exists():
30
+ return old_location
31
+ return new_location # Default to new location for creation
32
+
33
+
34
+ HUNT_EPILOG = """
35
+ \b
36
+ Examples:
37
+ # Interactive hunt creation (guided prompts)
38
+ athf hunt new
39
+
40
+ # Non-interactive with all options
41
+ athf hunt new --technique T1003.001 --title "LSASS Dumping" --non-interactive
42
+
43
+ # List hunts with filters
44
+ athf hunt list --status completed --tactic credential-access
45
+
46
+ # Search hunts for keywords
47
+ athf hunt search "kerberoasting"
48
+
49
+ # Get JSON output for scripting
50
+ athf hunt list --format json
51
+
52
+ # Show coverage gaps
53
+ athf hunt coverage
54
+
55
+ # Validate hunt structure
56
+ athf hunt validate H-0042
57
+
58
+ \b
59
+ Workflow:
60
+ 1. Create hunt → athf hunt new
61
+ 2. Edit hunt file → hunts/H-XXXX.md (use LOCK pattern)
62
+ 3. Create query → queries/H-XXXX.spl
63
+ 4. Execute hunt → document findings in runs/H-XXXX_YYYY-MM-DD.md
64
+ 5. Track results → athf hunt stats
65
+
66
+ \b
67
+ Learn more: https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/CLI_REFERENCE.md
68
+ """
69
+
70
+
71
+ @click.group(epilog=HUNT_EPILOG)
72
+ def hunt() -> None:
73
+ """Manage threat hunting activities and track program metrics.
74
+
75
+ \b
76
+ Hunt commands help you:
77
+ • Create structured hunt hypotheses
78
+ • Track hunts across your program
79
+ • Search past work to avoid duplication
80
+ • Calculate success rates and coverage
81
+ • Validate hunt file structure
82
+ """
83
+
84
+
85
+ @hunt.command()
86
+ @click.option("--technique", help="MITRE ATT&CK technique (e.g., T1003.001)")
87
+ @click.option("--title", help="Hunt title")
88
+ @click.option("--tactic", multiple=True, help="MITRE tactics (can specify multiple)")
89
+ @click.option("--platform", multiple=True, help="Target platforms (can specify multiple)")
90
+ @click.option("--data-source", multiple=True, help="Data sources (can specify multiple)")
91
+ @click.option("--non-interactive", is_flag=True, help="Skip interactive prompts")
92
+ @click.option("--hypothesis", help="Full hypothesis statement")
93
+ @click.option("--threat-context", help="Threat intel or context motivating the hunt")
94
+ @click.option("--actor", help="Threat actor (for ABLE framework)")
95
+ @click.option("--behavior", help="Behavior description (for ABLE framework)")
96
+ @click.option("--location", help="Location/scope (for ABLE framework)")
97
+ @click.option("--evidence", help="Evidence description (for ABLE framework)")
98
+ @click.option("--hunter", help="Hunter name", default="AI Assistant")
99
+ def new(
100
+ technique: Optional[str],
101
+ title: Optional[str],
102
+ tactic: Tuple[str, ...],
103
+ platform: Tuple[str, ...],
104
+ data_source: Tuple[str, ...],
105
+ non_interactive: bool,
106
+ hypothesis: Optional[str],
107
+ threat_context: Optional[str],
108
+ actor: Optional[str],
109
+ behavior: Optional[str],
110
+ location: Optional[str],
111
+ evidence: Optional[str],
112
+ hunter: Optional[str],
113
+ ) -> None:
114
+ """Create a new hunt hypothesis with LOCK structure.
115
+
116
+ \b
117
+ Creates a hunt file with:
118
+ • Auto-generated hunt ID (H-XXXX format)
119
+ • YAML frontmatter with metadata
120
+ • LOCK pattern sections (Learn, Observe, Check, Keep)
121
+ • MITRE ATT&CK mapping
122
+
123
+ \b
124
+ Interactive mode (default):
125
+ Guides you through hunt creation with prompts and suggestions.
126
+ Example: athf hunt new
127
+
128
+ \b
129
+ Non-interactive mode:
130
+ Provide all details via options for scripting.
131
+ Example: athf hunt new --technique T1003.001 --title "LSASS Dumping" \\
132
+ --tactic credential-access --platform Windows --non-interactive
133
+
134
+ \b
135
+ After creation:
136
+ 1. Edit hunts/H-XXXX.md to flesh out your hypothesis
137
+ 2. Create query in queries/H-XXXX.spl
138
+ 3. Execute hunt and document in runs/H-XXXX_YYYY-MM-DD.md
139
+ """
140
+ console.print("\n[bold cyan]🎯 Creating new hunt[/bold cyan]\n")
141
+
142
+ # Load config
143
+ config_path = get_config_path()
144
+ if config_path.exists():
145
+ with open(config_path, "r", encoding="utf-8") as f:
146
+ config = yaml.safe_load(f)
147
+ else:
148
+ config = {"hunt_prefix": "H-"}
149
+
150
+ hunt_prefix = config.get("hunt_prefix", "H-")
151
+
152
+ # Get next hunt ID
153
+ manager = HuntManager()
154
+ hunt_id = manager.get_next_hunt_id(prefix=hunt_prefix)
155
+
156
+ console.print(f"[bold]Hunt ID:[/bold] {hunt_id}")
157
+
158
+ # Gather hunt details
159
+ if non_interactive:
160
+ if not title:
161
+ console.print("[red]Error: --title required in non-interactive mode[/red]")
162
+ return
163
+ hunt_title = title
164
+ hunt_technique = technique or "T1005"
165
+ hunt_tactics = list(tactic) if tactic else ["collection"]
166
+ hunt_platforms = list(platform) if platform else ["Windows"]
167
+ hunt_data_sources = list(data_source) if data_source else ["SIEM", "EDR"]
168
+ else:
169
+ # Interactive prompts
170
+ console.print("\n[bold]🔍 Let's build your hypothesis:[/bold]")
171
+
172
+ # Technique
173
+ hunt_technique = Prompt.ask("1. MITRE ATT&CK Technique (e.g., T1003.001)", default=technique or "")
174
+
175
+ # Title
176
+ hunt_title = Prompt.ask("2. Hunt Title", default=title or f"Hunt for {hunt_technique}")
177
+
178
+ # Tactics
179
+ console.print("\n3. Primary Tactic(s) (comma-separated):")
180
+ console.print(" Common: [cyan]persistence, credential-access, collection, lateral-movement[/cyan]")
181
+ tactic_input = Prompt.ask(" Tactics", default=",".join(tactic) if tactic else "collection")
182
+ hunt_tactics = [t.strip() for t in tactic_input.split(",")]
183
+
184
+ # Platform
185
+ console.print("\n4. Target Platform(s) (comma-separated):")
186
+ console.print(" Options: [cyan]Windows, Linux, macOS, Cloud, Network[/cyan]")
187
+ platform_input = Prompt.ask(" Platforms", default=",".join(platform) if platform else "Windows")
188
+ hunt_platforms = [p.strip() for p in platform_input.split(",")]
189
+
190
+ # Data sources
191
+ console.print("\n5. Data Sources (comma-separated):")
192
+ console.print(f" Examples: [cyan]{config.get('siem', 'SIEM')}, {config.get('edr', 'EDR')}, Network Logs[/cyan]")
193
+ default_sources = ",".join(data_source) if data_source else f"{config.get('siem', 'SIEM')}, {config.get('edr', 'EDR')}"
194
+ ds_input = Prompt.ask(" Data Sources", default=default_sources)
195
+ hunt_data_sources = [ds.strip() for ds in ds_input.split(",")]
196
+
197
+ # Render template
198
+ hunt_content = render_hunt_template(
199
+ hunt_id=hunt_id,
200
+ title=hunt_title,
201
+ technique=hunt_technique,
202
+ tactics=hunt_tactics,
203
+ platform=hunt_platforms,
204
+ data_sources=hunt_data_sources,
205
+ hunter=hunter or "AI Assistant",
206
+ hypothesis=hypothesis,
207
+ threat_context=threat_context,
208
+ actor=actor,
209
+ behavior=behavior,
210
+ location=location,
211
+ evidence=evidence,
212
+ )
213
+
214
+ # Write hunt file
215
+ hunt_file = Path("hunts") / f"{hunt_id}.md"
216
+ hunt_file.parent.mkdir(exist_ok=True)
217
+
218
+ with open(hunt_file, "w", encoding="utf-8") as f:
219
+ f.write(hunt_content)
220
+
221
+ console.print(f"\n[bold green]✅ Created {hunt_id}: {hunt_title}[/bold green]")
222
+
223
+ # Easter egg: Hunt #100 milestone
224
+ if hunt_id.endswith("0100"):
225
+ console.print("\n[bold yellow]✨ Milestone Achievement: Hunt #100 ✨[/bold yellow]\n")
226
+ console.print("[italic]You've built serious hunting muscle memory.")
227
+ console.print("This is where threat hunting programs transform from reactive to proactive.")
228
+ console.print("Keep building that institutional knowledge.[/italic]\n")
229
+
230
+ console.print("\n[bold]Next steps:[/bold]")
231
+ console.print(f" 1. Edit [cyan]{hunt_file}[/cyan] to flesh out your hypothesis")
232
+ console.print(" 2. Document your hunt using the LOCK pattern")
233
+ console.print(" 3. View all hunts: [cyan]athf hunt list[/cyan]")
234
+
235
+
236
+ @hunt.command(name="list")
237
+ @click.option("--status", help="Filter by status (planning, active, completed)")
238
+ @click.option("--tactic", help="Filter by MITRE tactic")
239
+ @click.option("--technique", help="Filter by MITRE technique (e.g., T1003.001)")
240
+ @click.option("--platform", help="Filter by platform")
241
+ @click.option("--output", type=click.Choice(["table", "json", "yaml"]), default="table", help="Output format")
242
+ def list_hunts(status: str, tactic: str, technique: str, platform: str, output: str) -> None:
243
+ """List all hunts with filtering and formatting options.
244
+
245
+ \b
246
+ Displays hunt catalog with:
247
+ • Hunt ID and title
248
+ • Current status
249
+ • MITRE ATT&CK techniques
250
+ • True/False positive counts
251
+
252
+ \b
253
+ Examples:
254
+ # List all hunts
255
+ athf hunt list
256
+
257
+ # Show only completed hunts
258
+ athf hunt list --status completed
259
+
260
+ # Filter by tactic
261
+ athf hunt list --tactic credential-access
262
+
263
+ # Combine filters
264
+ athf hunt list --tactic persistence --platform Linux
265
+
266
+ # JSON output for scripting
267
+ athf hunt list --output json
268
+
269
+ \b
270
+ Output formats:
271
+ • table (default): Human-readable table with colors
272
+ • json: Machine-readable for scripts and automation
273
+ • yaml: Structured format for configuration management
274
+
275
+ Note: Use --output instead of --format for specifying output format.
276
+ """
277
+ manager = HuntManager()
278
+ hunts = manager.list_hunts(status=status, tactic=tactic, technique=technique, platform=platform)
279
+
280
+ if not hunts:
281
+ console.print("[yellow]No hunts found.[/yellow]")
282
+ console.print("\nCreate your first hunt: [cyan]athf hunt new[/cyan]")
283
+ return
284
+
285
+ if output == "json":
286
+ import json
287
+
288
+ console.print(json.dumps(hunts, indent=2))
289
+ return
290
+
291
+ if output == "yaml":
292
+ console.print(yaml.dump(hunts, default_flow_style=False))
293
+ return
294
+
295
+ # Table format
296
+ console.print(f"\n[bold]📋 Hunt Catalog ({len(hunts)} total)[/bold]\n")
297
+
298
+ table = Table(box=box.ROUNDED)
299
+ table.add_column("Hunt ID", style="cyan", no_wrap=True)
300
+ table.add_column("Title", style="white")
301
+ table.add_column("Status", style="yellow")
302
+ table.add_column("Technique", style="magenta")
303
+ table.add_column("Findings", style="green")
304
+
305
+ for hunt in hunts:
306
+ hunt_id = hunt.get("hunt_id", "")
307
+ title_full = hunt.get("title") or ""
308
+ title = title_full[:30] + ("..." if len(title_full) > 30 else "")
309
+ status_val = hunt.get("status", "")
310
+ techniques = hunt.get("techniques", [])
311
+ technique_str = techniques[0] if techniques else "-"
312
+
313
+ tp = hunt.get("true_positives", 0)
314
+ fp = hunt.get("false_positives", 0)
315
+ findings_str = f"{tp + fp} ({tp} TP)" if (tp + fp) > 0 else "-"
316
+
317
+ table.add_row(hunt_id, title, status_val, technique_str, findings_str)
318
+
319
+ console.print(table)
320
+ console.print()
321
+
322
+
323
+ @hunt.command()
324
+ @click.argument("hunt_id", required=False)
325
+ def validate(hunt_id: str) -> None:
326
+ """Validate hunt file structure and metadata.
327
+
328
+ \b
329
+ Validates:
330
+ • YAML frontmatter syntax
331
+ • Required metadata fields
332
+ • LOCK section structure
333
+ • MITRE ATT&CK technique format
334
+ • File naming conventions
335
+
336
+ \b
337
+ Examples:
338
+ # Validate specific hunt
339
+ athf hunt validate H-0042
340
+
341
+ # Validate all hunts
342
+ athf hunt validate
343
+
344
+ \b
345
+ Use this to:
346
+ • Catch formatting errors before committing
347
+ • Ensure consistency across hunt documentation
348
+ • Verify hunt files are AI-assistant readable
349
+ """
350
+ if hunt_id:
351
+ # Validate specific hunt
352
+ hunt_file = Path("hunts") / f"{hunt_id}.md"
353
+ if not hunt_file.exists():
354
+ console.print(f"[red]Hunt not found: {hunt_id}[/red]")
355
+ return
356
+
357
+ _validate_single_hunt(hunt_file)
358
+ else:
359
+ # Validate all hunts
360
+ console.print("\n[bold]🔍 Validating all hunts...[/bold]\n")
361
+
362
+ hunts_dir = Path("hunts")
363
+ if not hunts_dir.exists():
364
+ console.print("[yellow]No hunts directory found.[/yellow]")
365
+ return
366
+
367
+ hunt_files = list(hunts_dir.glob("*.md"))
368
+
369
+ if not hunt_files:
370
+ console.print("[yellow]No hunt files found.[/yellow]")
371
+ return
372
+
373
+ valid_count = 0
374
+ invalid_count = 0
375
+
376
+ for hunt_file in hunt_files:
377
+ is_valid, errors = validate_hunt_file(hunt_file)
378
+
379
+ if is_valid:
380
+ valid_count += 1
381
+ console.print(f"[green]✓[/green] {hunt_file.name}")
382
+ else:
383
+ invalid_count += 1
384
+ console.print(f"[red]✗[/red] {hunt_file.name}")
385
+ for error in errors:
386
+ console.print(f" - {error}")
387
+
388
+ console.print(f"\n[bold]Results:[/bold] {valid_count} valid, {invalid_count} invalid")
389
+
390
+
391
+ def _validate_single_hunt(hunt_file: Path) -> None:
392
+ """Validate a single hunt file."""
393
+ console.print(f"\n[bold]🔍 Validating {hunt_file.name}...[/bold]\n")
394
+
395
+ is_valid, errors = validate_hunt_file(hunt_file)
396
+
397
+ if is_valid:
398
+ console.print("[green]✅ Hunt is valid![/green]")
399
+ else:
400
+ console.print("[red]❌ Hunt has validation errors:[/red]\n")
401
+ for error in errors:
402
+ console.print(f" - {error}")
403
+
404
+
405
+ @hunt.command()
406
+ def stats() -> None:
407
+ """Show hunt program statistics and success metrics.
408
+
409
+ \b
410
+ Calculates and displays:
411
+ • Total hunts vs completed hunts
412
+ • Total findings (True Positives + False Positives)
413
+ • Success rate (hunts with TPs / completed hunts)
414
+ • TP/FP ratio (quality of detections)
415
+ • Hunt velocity metrics
416
+
417
+ \b
418
+ Example:
419
+ athf hunt stats
420
+
421
+ \b
422
+ Use this to:
423
+ • Track hunting program effectiveness over time
424
+ • Identify areas for improvement
425
+ • Demonstrate hunting value to leadership
426
+ • Set quarterly goals and OKRs
427
+ """
428
+ manager = HuntManager()
429
+ stats = manager.calculate_stats()
430
+
431
+ console.print("\n[bold cyan]📊 Hunt Program Statistics[/bold cyan]\n")
432
+
433
+ table = Table(box=box.SIMPLE, show_header=False)
434
+ table.add_column("Metric", style="cyan")
435
+ table.add_column("Value", style="white", justify="right")
436
+
437
+ table.add_row("Total Hunts", str(stats["total_hunts"]))
438
+ table.add_row("Completed Hunts", str(stats["completed_hunts"]))
439
+ table.add_row("Total Findings", str(stats["total_findings"]))
440
+ table.add_row("True Positives", str(stats["true_positives"]))
441
+ table.add_row("False Positives", str(stats["false_positives"]))
442
+ table.add_row("Success Rate", f"{stats['success_rate']}%")
443
+ table.add_row("TP/FP Ratio", str(stats["tp_fp_ratio"]))
444
+
445
+ console.print(table)
446
+ console.print()
447
+
448
+ # Easter egg: First True Positive milestone
449
+ if stats["true_positives"] == 1 and stats["completed_hunts"] > 0:
450
+ console.print("[bold yellow]🎯 First True Positive Detected![/bold yellow]\n")
451
+ console.print("[italic]Every expert threat hunter started here.")
452
+ console.print("This confirms your hypothesis was testable, your data was sufficient,")
453
+ console.print("and your analytical instincts were sound. Document what worked.[/italic]\n")
454
+
455
+
456
+ @hunt.command()
457
+ @click.argument("query")
458
+ def search(query: str) -> None:
459
+ """Full-text search across all hunt files.
460
+
461
+ \b
462
+ Searches through:
463
+ • Hunt titles and descriptions
464
+ • YAML frontmatter metadata
465
+ • LOCK section content
466
+ • Lessons learned
467
+ • Query comments
468
+
469
+ \b
470
+ Examples:
471
+ # Search for specific TTP
472
+ athf hunt search "kerberoasting"
473
+
474
+ # Search for technology
475
+ athf hunt search "powershell"
476
+
477
+ # Search by hunt ID
478
+ athf hunt search "H-0042"
479
+
480
+ # Search for data source
481
+ athf hunt search "sysmon"
482
+
483
+ \b
484
+ Use this to:
485
+ • Avoid duplicate hunts
486
+ • Find related past work
487
+ • Reference lessons learned
488
+ • Check if a TTP has been hunted before
489
+ """
490
+ manager = HuntManager()
491
+ results = manager.search_hunts(query)
492
+
493
+ if not results:
494
+ console.print(f"[yellow]No hunts found matching '{query}'[/yellow]")
495
+ return
496
+
497
+ console.print(f"\n[bold]🔍 Search results for '{query}' ({len(results)} found)[/bold]\n")
498
+
499
+ for result in results:
500
+ console.print(f"[cyan]{result['hunt_id']}[/cyan]: {result['title']}")
501
+ console.print(f" Status: {result['status']} | File: {result['file_path']}")
502
+ console.print()
503
+
504
+
505
+ @hunt.command()
506
+ def coverage() -> None:
507
+ """Show MITRE ATT&CK technique coverage across hunts.
508
+
509
+ \b
510
+ Analyzes and displays:
511
+ • Which tactics you've hunted (e.g., Persistence, Credential Access)
512
+ • Which techniques per tactic
513
+ • Coverage gaps (tactics with few/no hunts)
514
+ • Hunt distribution across the ATT&CK matrix
515
+
516
+ \b
517
+ Example:
518
+ athf hunt coverage
519
+
520
+ \b
521
+ Use this to:
522
+ • Identify blind spots in your hunting program
523
+ • Prioritize future hunt topics
524
+ • Demonstrate coverage to stakeholders
525
+ • Align hunting with threat intelligence priorities
526
+ • Balance hunt portfolio across tactics
527
+
528
+ \b
529
+ Pro tip:
530
+ Combine with threat intel to focus on attacker-relevant TTPs.
531
+ Example: "Which persistence techniques are we NOT hunting?"
532
+ """
533
+ manager = HuntManager()
534
+ coverage = manager.calculate_attack_coverage()
535
+
536
+ if not coverage:
537
+ console.print("[yellow]No hunt coverage data available.[/yellow]")
538
+ return
539
+
540
+ console.print("\n[bold cyan]🎯 MITRE ATT&CK Coverage[/bold cyan]\n")
541
+
542
+ for tactic, techniques in sorted(coverage.items()):
543
+ console.print(f"[bold]{tactic.title()}[/bold] ({len(techniques)} techniques)")
544
+ for technique in techniques:
545
+ console.print(f" • {technique}")
546
+ console.print()
547
+
548
+
549
+ @hunt.command(hidden=True)
550
+ def coffee() -> None:
551
+ """Check your caffeine levels (critical for threat hunting)."""
552
+ now = datetime.now()
553
+ hour = now.hour
554
+
555
+ # Random caffeine level
556
+ caffeine_level = random.randint(0, 100)
557
+
558
+ # Time-aware status
559
+ if 3 <= hour < 5:
560
+ status = "Incident Response Mode"
561
+ time_message = "Running on pure incident response adrenaline."
562
+ elif 0 <= hour < 6:
563
+ status = "Night Hunter"
564
+ time_message = "The real threat hunting happens in the dark."
565
+ elif 6 <= hour < 9:
566
+ status = "Early Bird"
567
+ time_message = "Morning hunts catch the adversaries."
568
+ elif 18 <= hour < 24:
569
+ status = "Evening Detective"
570
+ time_message = "Picking up where the day shift left off."
571
+ else:
572
+ status = "Operational"
573
+ time_message = "Sustainable hunting pace detected."
574
+
575
+ # Caffeine-level specific recommendations
576
+ if caffeine_level < 30:
577
+ recommendation = "Consider refueling. Even the best hunters need breaks."
578
+ elif caffeine_level > 90:
579
+ recommendation = "Peak operational capacity. Time to chase that hypothesis."
580
+ else:
581
+ recommendation = time_message
582
+
583
+ console.print("\n[bold]☕ Threat Hunter Caffeine Check[/bold]\n")
584
+ console.print(f"Current Level: [cyan]{caffeine_level}%[/cyan]")
585
+ console.print(f"Status: [yellow]{status}[/yellow]")
586
+ console.print(f"Recommendation: [italic]{recommendation}[/italic]\n")
587
+
588
+ # Random wisdom quote
589
+ wisdom_quotes = [
590
+ "The best hunts are fueled by curiosity, not just caffeine.",
591
+ "Caffeine enables the hunt. Rigor validates the findings.",
592
+ "Stay sharp, stay curious, stay caffeinated.",
593
+ "Coffee: because threat actors don't work business hours.",
594
+ "Fuel your hypotheses with coffee. Validate them with data.",
595
+ ]
596
+ console.print(f"[dim italic]{random.choice(wisdom_quotes)}[/dim italic]\n")