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.
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.0.dist-info}/METADATA +64 -53
- agentic_threat_hunting_framework-0.2.0.dist-info/RECORD +23 -0
- athf/__version__.py +1 -1
- athf/cli.py +7 -1
- athf/commands/context.py +358 -0
- athf/commands/env.py +373 -0
- athf/commands/hunt.py +92 -15
- athf/commands/investigate.py +744 -0
- athf/commands/similar.py +376 -0
- athf/core/attack_matrix.py +116 -0
- athf/core/hunt_manager.py +78 -10
- athf/core/investigation_parser.py +211 -0
- agentic_threat_hunting_framework-0.1.0.dist-info/RECORD +0 -17
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.0.dist-info}/WHEEL +0 -0
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.0.dist-info}/entry_points.txt +0 -0
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -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")
|