agentic-threat-hunting-framework 0.1.0__py3-none-any.whl → 0.2.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,744 @@
1
+ """Investigation management commands."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional
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.investigation_parser import get_all_investigations, get_next_investigation_id, validate_investigation_file
16
+
17
+ console = Console()
18
+
19
+
20
+ INVESTIGATION_EPILOG = """
21
+ \b
22
+ Examples:
23
+ # Interactive investigation creation
24
+ athf investigate new
25
+
26
+ # Non-interactive with all options
27
+ athf investigate new --title "Alert Triage - PowerShell" --type finding --non-interactive
28
+
29
+ # List investigations with filters
30
+ athf investigate list --type finding
31
+
32
+ # Search investigations for keywords
33
+ athf investigate search "PowerShell"
34
+
35
+ # Validate investigation structure
36
+ athf investigate validate I-0042
37
+
38
+ \b
39
+ Workflow:
40
+ 1. Create investigation → athf investigate new
41
+ 2. Edit investigation file → investigations/I-XXXX.md
42
+ 3. Document findings and analysis
43
+ 4. Optionally promote to formal hunt → athf investigate promote I-XXXX
44
+
45
+ \b
46
+ Learn more: See investigations/README.md for full documentation
47
+ """
48
+
49
+
50
+ @click.group(epilog=INVESTIGATION_EPILOG)
51
+ def investigate() -> None:
52
+ """Manage security investigations and exploratory work.
53
+
54
+ \b
55
+ Investigation commands help you:
56
+ • Triage alerts and findings
57
+ • Baseline new data sources
58
+ • Explore and sandbox queries
59
+ • Document ad-hoc analysis work
60
+ • Promote investigations to formal hunts
61
+
62
+ \b
63
+ Note: Investigations are NOT tracked in metrics.
64
+ They won't contribute to hunt success rates or cost tracking.
65
+ """
66
+
67
+
68
+ @investigate.command()
69
+ @click.option("--title", help="Investigation title")
70
+ @click.option(
71
+ "--type",
72
+ "investigation_type",
73
+ type=click.Choice(["finding", "baseline", "exploratory", "other"]),
74
+ help="Investigation type",
75
+ )
76
+ @click.option("--tags", help="Comma-separated tags (e.g., alert-triage,powershell)")
77
+ @click.option("--data-source", multiple=True, help="Data sources (can specify multiple)")
78
+ @click.option("--related-hunt", multiple=True, help="Related hunt IDs (e.g., H-0013)")
79
+ @click.option("--investigator", help="Investigator name", default="ATHF")
80
+ @click.option("--non-interactive", is_flag=True, help="Skip interactive prompts")
81
+ def new(
82
+ title: Optional[str],
83
+ investigation_type: Optional[str],
84
+ tags: Optional[str],
85
+ data_source: tuple[str, ...],
86
+ related_hunt: tuple[str, ...],
87
+ investigator: Optional[str],
88
+ non_interactive: bool,
89
+ ) -> None:
90
+ """Create a new investigation file.
91
+
92
+ \b
93
+ Creates an investigation file with:
94
+ • Auto-generated investigation ID (I-XXXX format)
95
+ • Minimal YAML frontmatter
96
+ • Optional LOCK structure for flexible documentation
97
+
98
+ \b
99
+ Interactive mode (default):
100
+ Guides you through investigation creation with prompts.
101
+ Example: athf investigate new
102
+
103
+ \b
104
+ Non-interactive mode:
105
+ Provide all details via options for scripting.
106
+ Example: athf investigate new --title "Alert Triage" \\
107
+ --type finding --tags alert-triage --non-interactive
108
+
109
+ \b
110
+ After creation:
111
+ 1. Edit investigations/I-XXXX.md to document your investigation
112
+ 2. Use LOCK pattern sections (optional/flexible)
113
+ 3. Optionally promote to hunt: athf investigate promote I-XXXX
114
+ """
115
+ console.print("\n[bold cyan]🔍 Creating new investigation[/bold cyan]\n")
116
+
117
+ # Get investigations directory
118
+ investigations_dir = Path("investigations")
119
+ investigations_dir.mkdir(exist_ok=True)
120
+
121
+ # Get next investigation ID
122
+ investigation_id = get_next_investigation_id(investigations_dir)
123
+ console.print(f"[bold]Investigation ID:[/bold] {investigation_id}")
124
+
125
+ # Gather investigation details
126
+ if non_interactive:
127
+ if not title:
128
+ console.print("[red]Error: --title required in non-interactive mode[/red]")
129
+ return
130
+ inv_title = title
131
+ inv_type = investigation_type or "exploratory"
132
+ inv_tags = [t.strip() for t in tags.split(",")] if tags else []
133
+ inv_data_sources = list(data_source) if data_source else []
134
+ inv_related_hunts = list(related_hunt) if related_hunt else []
135
+ else:
136
+ # Interactive prompts
137
+ console.print("\n[bold]📋 Let's set up your investigation:[/bold]")
138
+
139
+ # Title
140
+ inv_title = Prompt.ask("1. Investigation Title", default=title or "")
141
+
142
+ # Type
143
+ console.print("\n2. Investigation Type:")
144
+ console.print(" [cyan]finding[/cyan] - Alert triage or specific finding investigation")
145
+ console.print(" [cyan]baseline[/cyan] - Data source baselining or normal behavior analysis")
146
+ console.print(" [cyan]exploratory[/cyan] - Ad-hoc exploration or query sandbox")
147
+ console.print(" [cyan]other[/cyan] - Miscellaneous investigation")
148
+ inv_type = Prompt.ask(
149
+ " Type",
150
+ default=investigation_type or "exploratory",
151
+ choices=["finding", "baseline", "exploratory", "other"],
152
+ )
153
+
154
+ # Tags
155
+ console.print("\n3. Tags (comma-separated, optional):")
156
+ console.print(" Examples: [cyan]alert-triage, powershell, customer-x[/cyan]")
157
+ tags_input = Prompt.ask(" Tags", default=tags or "")
158
+ inv_tags = [t.strip() for t in tags_input.split(",")] if tags_input else []
159
+
160
+ # Data sources
161
+ console.print("\n4. Data Sources (comma-separated, optional):")
162
+ console.print(" Examples: [cyan]ClickHouse, EDR, CloudTrail[/cyan]")
163
+ ds_input = Prompt.ask(" Data Sources", default="")
164
+ inv_data_sources = [ds.strip() for ds in ds_input.split(",")] if ds_input else []
165
+
166
+ # Related hunts
167
+ console.print("\n5. Related Hunts (comma-separated IDs, optional):")
168
+ console.print(" Examples: [cyan]H-0013, H-0042[/cyan]")
169
+ hunts_input = Prompt.ask(" Related Hunts", default="")
170
+ inv_related_hunts = [h.strip() for h in hunts_input.split(",")] if hunts_input else []
171
+
172
+ # Render investigation template
173
+ investigation_content = _render_investigation_template(
174
+ investigation_id=investigation_id,
175
+ title=inv_title,
176
+ investigator=investigator or "ATHF",
177
+ investigation_type=inv_type,
178
+ tags=inv_tags,
179
+ data_sources=inv_data_sources,
180
+ related_hunts=inv_related_hunts,
181
+ )
182
+
183
+ # Write investigation file
184
+ investigation_file = investigations_dir / f"{investigation_id}.md"
185
+
186
+ with open(investigation_file, "w", encoding="utf-8") as f:
187
+ f.write(investigation_content)
188
+
189
+ console.print(f"\n[bold green]✅ Created {investigation_id}: {inv_title}[/bold green]")
190
+
191
+ console.print("\n[bold]Next steps:[/bold]")
192
+ console.print(f" 1. Edit [cyan]{investigation_file}[/cyan] to document your investigation")
193
+ console.print(" 2. Use LOCK pattern sections (optional/flexible)")
194
+ console.print(" 3. View all investigations: [cyan]athf investigate list[/cyan]")
195
+ console.print(" 4. Promote to hunt if valuable: [cyan]athf investigate promote {investigation_id}[/cyan]")
196
+
197
+
198
+ def _render_investigation_template(
199
+ investigation_id: str,
200
+ title: str,
201
+ investigator: str,
202
+ investigation_type: str,
203
+ tags: list[str],
204
+ data_sources: list[str],
205
+ related_hunts: list[str],
206
+ ) -> str:
207
+ """Render investigation template with provided values.
208
+
209
+ Args:
210
+ investigation_id: Investigation ID (e.g., I-0001)
211
+ title: Investigation title
212
+ investigator: Investigator name
213
+ investigation_type: Type (finding, baseline, exploratory, other)
214
+ tags: List of tags
215
+ data_sources: List of data sources
216
+ related_hunts: List of related hunt IDs
217
+
218
+ Returns:
219
+ Rendered investigation content
220
+ """
221
+ today = datetime.now().strftime("%Y-%m-%d")
222
+
223
+ # YAML frontmatter
224
+ frontmatter = {
225
+ "investigation_id": investigation_id,
226
+ "title": title,
227
+ "date": today,
228
+ "investigator": investigator,
229
+ "type": investigation_type,
230
+ "related_hunts": related_hunts,
231
+ "data_sources": data_sources,
232
+ "tags": tags,
233
+ }
234
+
235
+ # Convert to YAML
236
+ yaml_content = yaml.dump(frontmatter, default_flow_style=False, sort_keys=False)
237
+
238
+ # Build investigation content
239
+ content = f"""---
240
+ {yaml_content}---
241
+
242
+ # {investigation_id}: {title}
243
+
244
+ **Investigation Metadata**
245
+
246
+ - **Date:** {today}
247
+ - **Investigator:** {investigator}
248
+ - **Type:** {investigation_type.title()}
249
+
250
+ ---
251
+
252
+ ## LEARN: Context & Background
253
+
254
+ ### Investigation Context
255
+
256
+ [Why are you investigating this? What prompted the investigation?]
257
+
258
+ - **Trigger:** [Alert, customer report, anomaly, data quality check, etc.]
259
+ - **Initial Observations:** [What was initially noticed?]
260
+ - **Scope:** [What are you investigating? Time range? Specific systems?]
261
+
262
+ ### Related Context
263
+
264
+ - **Related Hunts:** {', '.join(related_hunts) if related_hunts else '[None]'}
265
+ - **Past Investigations:** [Reference any related investigations]
266
+ - **Threat Intel/CTI:** [Any relevant threat intelligence or context]
267
+
268
+ ---
269
+
270
+ ## OBSERVE: Initial Analysis
271
+
272
+ ### What You're Looking For
273
+
274
+ [Describe what patterns, behaviors, or anomalies you're investigating]
275
+
276
+ ### Data Sources
277
+
278
+ - **Index/Data Source:** {', '.join(data_sources) if data_sources else '[Specify data sources]'}
279
+ - **Time Range:** [Start datetime] to [End datetime]
280
+ - **Key Fields:** [process.name, user, source_ip, etc.]
281
+
282
+ ### Expected vs Observed
283
+
284
+ **Normal Behavior:**
285
+ - [What should normal activity look like?]
286
+ - [Common false positives to watch for]
287
+
288
+ **Suspicious/Anomalous Behavior:**
289
+ - [What anomalies are you seeing?]
290
+ - [What makes this suspicious or worth investigating?]
291
+
292
+ ---
293
+
294
+ ## CHECK: Investigation Queries & Analysis
295
+
296
+ ### Initial Query
297
+
298
+ ```[language: sql, kql, spl, etc.]
299
+ [Your initial investigation query]
300
+ ```
301
+
302
+ **Query Results:**
303
+
304
+ - **Events Found:** [Count]
305
+ - **Time to Execute:** [X.X seconds]
306
+ - **Initial Findings:** [Brief summary of what was found]
307
+
308
+ ### Refined Analysis
309
+
310
+ ```[language]
311
+ [Follow-up queries or refined analysis]
312
+ ```
313
+
314
+ **Additional Findings:**
315
+
316
+ - [Key observations from refined analysis]
317
+ - [Patterns or correlations discovered]
318
+ - [Anomalies identified]
319
+
320
+ ### Pivots & Follow-ups
321
+
322
+ [Document any pivots you made during the investigation]
323
+
324
+ - **Pivot 1:** [What did you investigate next and why?]
325
+ - **Pivot 2:** [Additional follow-up investigation]
326
+
327
+ ---
328
+
329
+ ## KEEP: Findings & Next Steps
330
+
331
+ ### Summary
332
+
333
+ [3-5 sentence summary of the investigation outcome]
334
+
335
+ - **Verdict:** [Benign | Suspicious | Malicious | Inconclusive | Data Quality Issue]
336
+ - **Confidence:** [High | Medium | Low]
337
+
338
+ ### Key Findings
339
+
340
+ | **Finding** | **Evidence** | **Assessment** |
341
+ |-------------|-------------|----------------|
342
+ | [Finding 1] | [Supporting evidence] | [Benign/Suspicious/Malicious] |
343
+ | [Finding 2] | [Supporting evidence] | [Benign/Suspicious/Malicious] |
344
+
345
+ ### Lessons Learned
346
+
347
+ **What Worked Well:**
348
+
349
+ - [Effective investigation strategies]
350
+ - [Useful queries or data sources]
351
+ - [Tools or techniques that helped]
352
+
353
+ **What Could Be Improved:**
354
+
355
+ - [Data gaps or blind spots identified]
356
+ - [Better approaches for next time]
357
+ - [Telemetry or visibility improvements needed]
358
+
359
+ ### Next Steps
360
+
361
+ - [ ] [Escalate to incident response if malicious]
362
+ - [ ] [Create detection rule if repeatable pattern]
363
+ - [ ] [Promote to formal hunt if hypothesis emerges]
364
+ - [ ] [Document exceptions or false positive filters]
365
+ - [ ] [Address telemetry gaps]
366
+ - [ ] [Follow-up investigation if needed]
367
+
368
+ ---
369
+
370
+ **Investigation Completed:** [Date or "Ongoing"]
371
+ **Status:** [Closed|In Progress|Escalated|Promoted to Hunt]
372
+ """
373
+
374
+ return content
375
+
376
+
377
+ @investigate.command(name="list")
378
+ @click.option("--type", "investigation_type", help="Filter by type (finding, baseline, exploratory, other)")
379
+ @click.option("--tags", help="Filter by tags (comma-separated)")
380
+ @click.option("--output", type=click.Choice(["table", "json", "yaml"]), default="table", help="Output format")
381
+ def list_investigations(
382
+ investigation_type: Optional[str],
383
+ tags: Optional[str],
384
+ output: str,
385
+ ) -> None:
386
+ """List all investigations with filtering options.
387
+
388
+ \b
389
+ Displays investigation catalog with:
390
+ • Investigation ID and title
391
+ • Type (finding, baseline, exploratory, other)
392
+ • Tags and related hunts
393
+
394
+ \b
395
+ Examples:
396
+ # List all investigations
397
+ athf investigate list
398
+
399
+ # Show only finding investigations
400
+ athf investigate list --type finding
401
+
402
+ # Filter by tags
403
+ athf investigate list --tags alert-triage
404
+
405
+ # JSON output for scripting
406
+ athf investigate list --output json
407
+ """
408
+ investigations_dir = Path("investigations")
409
+ investigations = get_all_investigations(investigations_dir)
410
+
411
+ if not investigations:
412
+ console.print("[yellow]No investigations found.[/yellow]")
413
+ console.print("\nCreate your first investigation: [cyan]athf investigate new[/cyan]")
414
+ return
415
+
416
+ # Apply filters
417
+ filtered_investigations = investigations
418
+ if investigation_type:
419
+ filtered_investigations = [
420
+ inv for inv in filtered_investigations if inv.get("frontmatter", {}).get("type") == investigation_type
421
+ ]
422
+
423
+ if tags:
424
+ filter_tags = {t.strip() for t in tags.split(",")}
425
+ filtered_investigations = [
426
+ inv for inv in filtered_investigations if filter_tags.intersection(inv.get("frontmatter", {}).get("tags", []))
427
+ ]
428
+
429
+ if not filtered_investigations:
430
+ console.print("[yellow]No investigations match the filters.[/yellow]")
431
+ return
432
+
433
+ # Output format
434
+ if output == "json":
435
+ console.print(json.dumps(filtered_investigations, indent=2))
436
+ return
437
+
438
+ if output == "yaml":
439
+ console.print(yaml.dump(filtered_investigations, default_flow_style=False))
440
+ return
441
+
442
+ # Table output (default)
443
+ table = Table(title="Investigations", box=box.ROUNDED, show_header=True, header_style="bold cyan")
444
+
445
+ table.add_column("ID", style="cyan", no_wrap=True)
446
+ table.add_column("Title", style="white")
447
+ table.add_column("Type", style="yellow")
448
+ table.add_column("Tags", style="dim")
449
+ table.add_column("Date", style="dim")
450
+
451
+ for investigation in filtered_investigations:
452
+ frontmatter = investigation.get("frontmatter", {})
453
+ inv_id = frontmatter.get("investigation_id", "N/A")
454
+ title = frontmatter.get("title", "Untitled")
455
+ inv_type = frontmatter.get("type", "unknown")
456
+ inv_tags = frontmatter.get("tags", [])
457
+ date = frontmatter.get("date", "N/A")
458
+
459
+ tags_str = ", ".join(inv_tags[:3]) if inv_tags else "-"
460
+ if len(inv_tags) > 3:
461
+ tags_str += f" (+{len(inv_tags) - 3})"
462
+
463
+ table.add_row(inv_id, title, inv_type, tags_str, date)
464
+
465
+ console.print(table)
466
+ console.print(f"\n[dim]Total: {len(filtered_investigations)} investigations[/dim]")
467
+
468
+
469
+ @investigate.command()
470
+ @click.argument("query")
471
+ def search(query: str) -> None:
472
+ """Search investigation files for keywords.
473
+
474
+ \b
475
+ Performs full-text search across all investigation files.
476
+
477
+ \b
478
+ Examples:
479
+ # Search for PowerShell
480
+ athf investigate search "PowerShell"
481
+
482
+ # Search for customer-specific findings
483
+ athf investigate search "customer-x"
484
+
485
+ # Search for baseline work
486
+ athf investigate search "baseline CloudTrail"
487
+ """
488
+ investigations_dir = Path("investigations")
489
+ investigation_files = sorted(investigations_dir.glob("I-*.md"))
490
+
491
+ if not investigation_files:
492
+ console.print("[yellow]No investigation files found.[/yellow]")
493
+ return
494
+
495
+ matches = []
496
+ for file_path in investigation_files:
497
+ with open(file_path, "r", encoding="utf-8") as f:
498
+ content = f.read()
499
+ if query.lower() in content.lower():
500
+ matches.append(file_path)
501
+
502
+ if not matches:
503
+ console.print(f'[yellow]No matches found for "{query}"[/yellow]')
504
+ return
505
+
506
+ console.print(f'\n[bold]Found {len(matches)} investigation(s) matching "{query}":[/bold]\n')
507
+
508
+ for file_path in matches:
509
+ # Extract investigation ID and title from filename
510
+ investigation_id = file_path.stem
511
+
512
+ # Try to get title from frontmatter
513
+ try:
514
+ with open(file_path, "r", encoding="utf-8") as f:
515
+ content = f.read()
516
+ frontmatter_match = yaml.safe_load(content.split("---")[1])
517
+ title = frontmatter_match.get("title", "Untitled")
518
+ except Exception:
519
+ title = "Untitled"
520
+
521
+ console.print(f"[cyan]{investigation_id}[/cyan]: {title}")
522
+ console.print(f" [dim]{file_path}[/dim]\n")
523
+
524
+
525
+ @investigate.command()
526
+ @click.argument("investigation_id")
527
+ def validate(investigation_id: str) -> None:
528
+ """Validate investigation file structure.
529
+
530
+ \b
531
+ Checks:
532
+ • YAML frontmatter is valid
533
+ • Required fields exist (investigation_id, title, date)
534
+ • Investigation ID format (I-XXXX)
535
+ • File name matches investigation ID
536
+
537
+ \b
538
+ Examples:
539
+ # Validate a specific investigation
540
+ athf investigate validate I-0042
541
+
542
+ # Validate after editing
543
+ athf investigate validate I-0001
544
+ """
545
+ investigations_dir = Path("investigations")
546
+ investigation_file = investigations_dir / f"{investigation_id}.md"
547
+
548
+ if not investigation_file.exists():
549
+ console.print(f"[red]Error: Investigation file not found: {investigation_file}[/red]")
550
+ return
551
+
552
+ is_valid, errors = validate_investigation_file(investigation_file)
553
+
554
+ if is_valid:
555
+ console.print(f"[bold green]✅ {investigation_id} is valid[/bold green]")
556
+ else:
557
+ console.print(f"[bold red]❌ {investigation_id} has validation errors:[/bold red]\n")
558
+ for error in errors:
559
+ console.print(f" • {error}")
560
+
561
+
562
+ @investigate.command()
563
+ @click.argument("investigation_id")
564
+ @click.option("--technique", help="MITRE ATT&CK technique (required for hunt)")
565
+ @click.option("--tactic", multiple=True, help="MITRE tactics (can specify multiple)")
566
+ @click.option("--platform", multiple=True, help="Target platforms (can specify multiple)")
567
+ @click.option("--status", default="in-progress", help="Hunt status (default: in-progress)")
568
+ @click.option("--non-interactive", is_flag=True, help="Skip interactive prompts")
569
+ def promote(
570
+ investigation_id: str,
571
+ technique: Optional[str],
572
+ tactic: tuple[str, ...],
573
+ platform: tuple[str, ...],
574
+ status: str,
575
+ non_interactive: bool,
576
+ ) -> None:
577
+ """Promote investigation to formal hunt.
578
+
579
+ \b
580
+ Creates a hunt file (H-XXXX) from an investigation, adding:
581
+ • Hunt-required metadata (tactics, techniques, platform)
582
+ • Hunt status and tracking fields
583
+ • Findings count and TP/FP fields (default: 0)
584
+ • Reference to original investigation (spawned_from)
585
+
586
+ \b
587
+ Examples:
588
+ # Interactive promotion (prompts for details)
589
+ athf investigate promote I-0042
590
+
591
+ # Non-interactive with all options
592
+ athf investigate promote I-0042 \\
593
+ --technique T1059.001 \\
594
+ --tactic execution \\
595
+ --platform Windows \\
596
+ --non-interactive
597
+
598
+ \b
599
+ After promotion:
600
+ • Hunt file created in hunts/ directory
601
+ • Investigation remains in investigations/ directory
602
+ • Both files cross-reference each other
603
+ """
604
+ from athf.core.hunt_manager import HuntManager
605
+ from athf.core.investigation_parser import InvestigationParser
606
+
607
+ console.print("\n[bold cyan]🔄 Promoting investigation to hunt[/bold cyan]\n")
608
+
609
+ # Check investigation file exists
610
+ investigations_dir = Path("investigations")
611
+ investigation_file = investigations_dir / f"{investigation_id}.md"
612
+
613
+ if not investigation_file.exists():
614
+ console.print(f"[red]Error: Investigation file not found: {investigation_file}[/red]")
615
+ return
616
+
617
+ # Parse investigation file
618
+ try:
619
+ parser = InvestigationParser(investigation_file)
620
+ investigation_data = parser.parse()
621
+ inv_frontmatter = investigation_data.get("frontmatter", {})
622
+ inv_content = investigation_data.get("content", "")
623
+ except Exception as e:
624
+ console.print(f"[red]Error parsing investigation file: {e}[/red]")
625
+ return
626
+
627
+ # Get investigation details
628
+ inv_title = inv_frontmatter.get("title", "Untitled")
629
+ inv_investigator = inv_frontmatter.get("investigator", "Unknown")
630
+ inv_data_sources = inv_frontmatter.get("data_sources", [])
631
+ inv_related_hunts = inv_frontmatter.get("related_hunts", [])
632
+ inv_tags = inv_frontmatter.get("tags", [])
633
+
634
+ console.print(f"[bold]Investigation:[/bold] {investigation_id} - {inv_title}")
635
+
636
+ # Gather hunt-required metadata
637
+ if non_interactive:
638
+ if not technique:
639
+ console.print("[red]Error: --technique required in non-interactive mode[/red]")
640
+ return
641
+ hunt_technique = technique
642
+ hunt_tactics = list(tactic) if tactic else []
643
+ hunt_platforms = list(platform) if platform else []
644
+ hunt_status = status
645
+ else:
646
+ # Interactive prompts
647
+ console.print("\n[bold]📋 Let's add hunt-required metadata:[/bold]")
648
+
649
+ # Technique (required)
650
+ console.print("\n1. MITRE ATT&CK Technique (required for hunts):")
651
+ console.print(" Examples: [cyan]T1003.001, T1059.001, T1078[/cyan]")
652
+ hunt_technique = Prompt.ask(" Technique", default=technique or "")
653
+
654
+ # Tactics
655
+ console.print("\n2. MITRE Tactics (comma-separated):")
656
+ console.print(" Examples: [cyan]initial-access, execution, persistence, credential-access[/cyan]")
657
+ tactics_input = Prompt.ask(" Tactics", default=",".join(tactic) if tactic else "")
658
+ hunt_tactics = [t.strip() for t in tactics_input.split(",")] if tactics_input else []
659
+
660
+ # Platforms
661
+ console.print("\n3. Target Platforms (comma-separated):")
662
+ console.print(" Examples: [cyan]Windows, Linux, macOS, Cloud[/cyan]")
663
+ platforms_input = Prompt.ask(" Platforms", default=",".join(platform) if platform else "")
664
+ hunt_platforms = [p.strip() for p in platforms_input.split(",")] if platforms_input else []
665
+
666
+ # Status
667
+ console.print("\n4. Hunt Status:")
668
+ hunt_status = Prompt.ask(
669
+ " Status",
670
+ default=status,
671
+ choices=["planning", "in-progress", "completed", "archived"],
672
+ )
673
+
674
+ # Get next hunt ID
675
+ hunt_manager = HuntManager()
676
+ hunt_id = hunt_manager.get_next_hunt_id()
677
+
678
+ console.print(f"\n[bold]Hunt ID:[/bold] {hunt_id}")
679
+
680
+ # Create hunt frontmatter
681
+ today = datetime.now().strftime("%Y-%m-%d")
682
+ hunt_frontmatter = {
683
+ "hunt_id": hunt_id,
684
+ "title": inv_title,
685
+ "status": hunt_status,
686
+ "date": today,
687
+ "hunter": inv_investigator,
688
+ "platform": hunt_platforms,
689
+ "tactics": hunt_tactics,
690
+ "techniques": [hunt_technique],
691
+ "data_sources": inv_data_sources,
692
+ "related_hunts": inv_related_hunts,
693
+ "spawned_from": investigation_id, # Reference investigation
694
+ "findings_count": 0,
695
+ "true_positives": 0,
696
+ "false_positives": 0,
697
+ "customer_deliverables": [],
698
+ "tags": inv_tags,
699
+ }
700
+
701
+ # Convert to YAML
702
+ yaml_content = yaml.dump(hunt_frontmatter, default_flow_style=False, sort_keys=False)
703
+
704
+ # Build hunt content (preserve investigation content structure)
705
+ hunt_content = f"""---
706
+ {yaml_content}---
707
+
708
+ # {hunt_id}: {inv_title}
709
+
710
+ **Hunt Metadata**
711
+
712
+ - **Date:** {today}
713
+ - **Hunter:** {inv_investigator}
714
+ - **Status:** {hunt_status.title()}
715
+ - **Promoted From:** {investigation_id}
716
+
717
+ ---
718
+
719
+ {inv_content}
720
+ """
721
+
722
+ # Write hunt file
723
+ hunts_dir = Path("hunts")
724
+ hunts_dir.mkdir(exist_ok=True)
725
+ hunt_file = hunts_dir / f"{hunt_id}.md"
726
+
727
+ with open(hunt_file, "w", encoding="utf-8") as f:
728
+ f.write(hunt_content)
729
+
730
+ console.print(f"\n[bold green]✅ Promoted {investigation_id} to {hunt_id}[/bold green]")
731
+
732
+ # Update investigation with promotion note
733
+ promotion_note = f"\n\n---\n\n**Promoted to Hunt:** {hunt_id} on {today}\n"
734
+
735
+ with open(investigation_file, "a", encoding="utf-8") as f:
736
+ f.write(promotion_note)
737
+
738
+ console.print(f"[dim]Updated {investigation_file} with promotion note[/dim]")
739
+
740
+ console.print("\n[bold]Next steps:[/bold]")
741
+ console.print(f" 1. Edit [cyan]{hunt_file}[/cyan] to refine hunt hypothesis")
742
+ console.print(" 2. Add MITRE ATT&CK coverage if needed")
743
+ console.print(f" 3. Validate hunt: [cyan]athf hunt validate {hunt_id}[/cyan]")
744
+ console.print(f" 4. View hunt: [cyan]athf hunt list --status {hunt_status}[/cyan]\n")