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.
- agentic_threat_hunting_framework-0.1.0.dist-info/METADATA +339 -0
- agentic_threat_hunting_framework-0.1.0.dist-info/RECORD +17 -0
- agentic_threat_hunting_framework-0.1.0.dist-info/WHEEL +5 -0
- agentic_threat_hunting_framework-0.1.0.dist-info/entry_points.txt +2 -0
- agentic_threat_hunting_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- agentic_threat_hunting_framework-0.1.0.dist-info/top_level.txt +1 -0
- athf/__init__.py +9 -0
- athf/__version__.py +3 -0
- athf/cli.py +127 -0
- athf/commands/__init__.py +1 -0
- athf/commands/hunt.py +596 -0
- athf/commands/init.py +411 -0
- athf/core/__init__.py +1 -0
- athf/core/hunt_manager.py +245 -0
- athf/core/hunt_parser.py +169 -0
- athf/core/template_engine.py +224 -0
- athf/utils/__init__.py +1 -0
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")
|