agentic-threat-hunting-framework 0.1.0__py3-none-any.whl → 0.2.1__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.1.dist-info}/METADATA +64 -53
- agentic_threat_hunting_framework-0.2.1.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 +374 -0
- athf/commands/hunt.py +92 -15
- athf/commands/investigate.py +744 -0
- athf/commands/similar.py +376 -0
- athf/core/attack_matrix.py +131 -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.1.dist-info}/WHEEL +0 -0
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.1.dist-info}/entry_points.txt +0 -0
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.1.dist-info}/top_level.txt +0 -0
athf/commands/env.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Environment management commands."""
|
|
2
|
+
|
|
3
|
+
import subprocess # nosec B404
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
ENV_EPILOG = """
|
|
15
|
+
\b
|
|
16
|
+
Examples:
|
|
17
|
+
# Setup virtual environment with default Python
|
|
18
|
+
athf env setup
|
|
19
|
+
|
|
20
|
+
# Setup with specific Python version
|
|
21
|
+
athf env setup --python python3.13
|
|
22
|
+
|
|
23
|
+
# Include dev dependencies
|
|
24
|
+
athf env setup --dev
|
|
25
|
+
|
|
26
|
+
# Clean up existing venv and recreate
|
|
27
|
+
athf env setup --clean
|
|
28
|
+
|
|
29
|
+
\b
|
|
30
|
+
After setup:
|
|
31
|
+
# Activate venv (bash/zsh)
|
|
32
|
+
source .venv/bin/activate
|
|
33
|
+
|
|
34
|
+
# Deactivate
|
|
35
|
+
deactivate
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@click.group(epilog=ENV_EPILOG)
|
|
40
|
+
def env() -> None:
|
|
41
|
+
"""Manage Python virtual environment.
|
|
42
|
+
|
|
43
|
+
Commands for setting up, cleaning, and managing the Python
|
|
44
|
+
virtual environment for ATHF development.
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@env.command(name="setup")
|
|
50
|
+
@click.option(
|
|
51
|
+
"--python",
|
|
52
|
+
default="python3",
|
|
53
|
+
help="Python executable to use (default: python3)",
|
|
54
|
+
)
|
|
55
|
+
@click.option("--dev", is_flag=True, help="Install development dependencies")
|
|
56
|
+
@click.option("--clean", is_flag=True, help="Remove existing venv before creating")
|
|
57
|
+
def setup(python: str, dev: bool, clean: bool) -> None:
|
|
58
|
+
"""Setup Python virtual environment with dependencies.
|
|
59
|
+
|
|
60
|
+
Creates .venv directory and installs athf package with
|
|
61
|
+
all dependencies from pyproject.toml.
|
|
62
|
+
|
|
63
|
+
\b
|
|
64
|
+
Steps:
|
|
65
|
+
1. Create .venv directory (or clean existing)
|
|
66
|
+
2. Install athf package in editable mode
|
|
67
|
+
3. Install scikit-learn for semantic search
|
|
68
|
+
4. Show activation instructions
|
|
69
|
+
|
|
70
|
+
\b
|
|
71
|
+
Examples:
|
|
72
|
+
athf env setup
|
|
73
|
+
athf env setup --python python3.13
|
|
74
|
+
athf env setup --dev
|
|
75
|
+
athf env setup --clean
|
|
76
|
+
"""
|
|
77
|
+
venv_path = Path(".venv")
|
|
78
|
+
|
|
79
|
+
# Check if we're in the ATHF directory
|
|
80
|
+
if not Path("pyproject.toml").exists():
|
|
81
|
+
console.print("[red]Error: Not in ATHF directory (pyproject.toml not found)[/red]")
|
|
82
|
+
console.print("[dim]Run this command from the ATHF root directory[/dim]")
|
|
83
|
+
raise click.Abort()
|
|
84
|
+
|
|
85
|
+
# Clean existing venv if requested
|
|
86
|
+
if clean and venv_path.exists():
|
|
87
|
+
console.print("[yellow]🧹 Removing existing .venv directory...[/yellow]")
|
|
88
|
+
try:
|
|
89
|
+
import shutil
|
|
90
|
+
|
|
91
|
+
shutil.rmtree(venv_path)
|
|
92
|
+
console.print("[green]✅ Removed existing .venv[/green]\n")
|
|
93
|
+
except Exception as e:
|
|
94
|
+
console.print(f"[red]Error removing .venv: {e}[/red]")
|
|
95
|
+
raise click.Abort()
|
|
96
|
+
|
|
97
|
+
# Check if venv already exists
|
|
98
|
+
if venv_path.exists():
|
|
99
|
+
console.print("[yellow]⚠️ .venv already exists[/yellow]")
|
|
100
|
+
console.print("[dim]Use --clean to remove and recreate[/dim]\n")
|
|
101
|
+
|
|
102
|
+
# Show helpful usage instructions
|
|
103
|
+
if sys.platform == "win32":
|
|
104
|
+
activate_cmd = ".venv\\Scripts\\activate"
|
|
105
|
+
else:
|
|
106
|
+
activate_cmd = "source .venv/bin/activate"
|
|
107
|
+
|
|
108
|
+
usage_panel = Panel(
|
|
109
|
+
f"[bold cyan]To use the existing virtual environment:[/bold cyan]\n\n"
|
|
110
|
+
f"[green]1. Activate the venv:[/green]\n"
|
|
111
|
+
f" {activate_cmd}\n\n"
|
|
112
|
+
f"[green]2. Run athf commands:[/green]\n"
|
|
113
|
+
f" athf --version\n"
|
|
114
|
+
f" athf hunt --help\n\n"
|
|
115
|
+
f"[green]3. Or use without activating:[/green]\n"
|
|
116
|
+
f" .venv/bin/athf [command]\n\n"
|
|
117
|
+
f"[dim]💡 Your prompt will show (.venv) when activated[/dim]",
|
|
118
|
+
title="✨ Virtual Environment Ready",
|
|
119
|
+
border_style="cyan",
|
|
120
|
+
)
|
|
121
|
+
console.print(usage_panel)
|
|
122
|
+
raise click.Abort()
|
|
123
|
+
|
|
124
|
+
# Create virtual environment
|
|
125
|
+
console.print(f"[cyan]📦 Creating virtual environment with {python}...[/cyan]")
|
|
126
|
+
try:
|
|
127
|
+
subprocess.run(
|
|
128
|
+
[python, "-m", "venv", ".venv"],
|
|
129
|
+
check=True,
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True,
|
|
132
|
+
)
|
|
133
|
+
console.print("[green]✅ Virtual environment created[/green]\n")
|
|
134
|
+
except subprocess.CalledProcessError as e:
|
|
135
|
+
console.print(f"[red]Error creating venv: {e.stderr}[/red]")
|
|
136
|
+
raise click.Abort()
|
|
137
|
+
except FileNotFoundError:
|
|
138
|
+
console.print(f"[red]Error: {python} not found[/red]")
|
|
139
|
+
console.print("[dim]Try: athf env setup --python python3.13[/dim]")
|
|
140
|
+
raise click.Abort()
|
|
141
|
+
|
|
142
|
+
# Determine pip path
|
|
143
|
+
if sys.platform == "win32":
|
|
144
|
+
pip_path = venv_path / "Scripts" / "pip"
|
|
145
|
+
else:
|
|
146
|
+
pip_path = venv_path / "bin" / "pip"
|
|
147
|
+
|
|
148
|
+
# Upgrade pip
|
|
149
|
+
console.print("[cyan]📦 Upgrading pip...[/cyan]")
|
|
150
|
+
try:
|
|
151
|
+
subprocess.run(
|
|
152
|
+
[str(pip_path), "install", "--upgrade", "pip"],
|
|
153
|
+
check=True,
|
|
154
|
+
capture_output=True,
|
|
155
|
+
text=True,
|
|
156
|
+
)
|
|
157
|
+
console.print("[green]✅ pip upgraded[/green]\n")
|
|
158
|
+
except subprocess.CalledProcessError as e:
|
|
159
|
+
console.print(f"[yellow]Warning: Failed to upgrade pip: {e.stderr}[/yellow]\n")
|
|
160
|
+
|
|
161
|
+
# Install athf package
|
|
162
|
+
console.print("[cyan]📦 Installing ATHF package...[/cyan]")
|
|
163
|
+
install_cmd = [str(pip_path), "install", "-e", "."]
|
|
164
|
+
if dev:
|
|
165
|
+
install_cmd.append("[dev]")
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
subprocess.run(
|
|
169
|
+
install_cmd,
|
|
170
|
+
check=True,
|
|
171
|
+
capture_output=True,
|
|
172
|
+
text=True,
|
|
173
|
+
)
|
|
174
|
+
console.print("[green]✅ ATHF installed[/green]\n")
|
|
175
|
+
except subprocess.CalledProcessError as e:
|
|
176
|
+
console.print(f"[red]Error installing package: {e.stderr}[/red]")
|
|
177
|
+
raise click.Abort()
|
|
178
|
+
|
|
179
|
+
# Install scikit-learn for athf similar command
|
|
180
|
+
console.print("[cyan]📦 Installing scikit-learn for semantic search...[/cyan]")
|
|
181
|
+
try:
|
|
182
|
+
subprocess.run(
|
|
183
|
+
[str(pip_path), "install", "scikit-learn"],
|
|
184
|
+
check=True,
|
|
185
|
+
capture_output=True,
|
|
186
|
+
text=True,
|
|
187
|
+
)
|
|
188
|
+
console.print("[green]✅ scikit-learn installed[/green]\n")
|
|
189
|
+
except subprocess.CalledProcessError as e:
|
|
190
|
+
console.print(f"[yellow]Warning: Failed to install scikit-learn: {e.stderr}[/yellow]")
|
|
191
|
+
console.print("[dim]athf similar command will not work without scikit-learn[/dim]\n")
|
|
192
|
+
|
|
193
|
+
# Success message
|
|
194
|
+
console.print("[bold green]🎉 Virtual environment setup complete![/bold green]\n")
|
|
195
|
+
|
|
196
|
+
# Show activation instructions
|
|
197
|
+
if sys.platform == "win32":
|
|
198
|
+
activate_cmd = ".venv\\Scripts\\activate"
|
|
199
|
+
else:
|
|
200
|
+
activate_cmd = "source .venv/bin/activate"
|
|
201
|
+
|
|
202
|
+
activation_panel = Panel(
|
|
203
|
+
f"[cyan]{activate_cmd}[/cyan]\n\n"
|
|
204
|
+
f"[dim]Then verify installation:[/dim]\n"
|
|
205
|
+
f"[white]athf --version[/white]\n"
|
|
206
|
+
f"[white]athf hunt --help[/white]",
|
|
207
|
+
title="🚀 Next Steps",
|
|
208
|
+
border_style="green",
|
|
209
|
+
)
|
|
210
|
+
console.print(activation_panel)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@env.command(name="clean")
|
|
214
|
+
def clean() -> None:
|
|
215
|
+
"""Remove virtual environment.
|
|
216
|
+
|
|
217
|
+
Deletes the .venv directory to start fresh.
|
|
218
|
+
|
|
219
|
+
\b
|
|
220
|
+
Example:
|
|
221
|
+
athf env clean
|
|
222
|
+
athf env setup
|
|
223
|
+
"""
|
|
224
|
+
venv_path = Path(".venv")
|
|
225
|
+
|
|
226
|
+
if not venv_path.exists():
|
|
227
|
+
console.print("[yellow]No .venv directory found[/yellow]")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
console.print("[yellow]🧹 Removing .venv directory...[/yellow]")
|
|
231
|
+
try:
|
|
232
|
+
import shutil
|
|
233
|
+
|
|
234
|
+
shutil.rmtree(venv_path)
|
|
235
|
+
console.print("[green]✅ Virtual environment removed[/green]")
|
|
236
|
+
console.print("[dim]Run 'athf env setup' to recreate[/dim]")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
console.print(f"[red]Error removing .venv: {e}[/red]")
|
|
239
|
+
raise click.Abort()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@env.command(name="info")
|
|
243
|
+
def info() -> None:
|
|
244
|
+
"""Show virtual environment information.
|
|
245
|
+
|
|
246
|
+
Display Python version, installed packages, and venv location.
|
|
247
|
+
|
|
248
|
+
\b
|
|
249
|
+
Example:
|
|
250
|
+
athf env info
|
|
251
|
+
"""
|
|
252
|
+
venv_path = Path(".venv")
|
|
253
|
+
|
|
254
|
+
if not venv_path.exists():
|
|
255
|
+
console.print("[yellow]No .venv directory found[/yellow]")
|
|
256
|
+
console.print("[dim]Run 'athf env setup' to create[/dim]")
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
# Determine python path
|
|
260
|
+
if sys.platform == "win32":
|
|
261
|
+
python_path = venv_path / "Scripts" / "python"
|
|
262
|
+
else:
|
|
263
|
+
python_path = venv_path / "bin" / "python"
|
|
264
|
+
|
|
265
|
+
if not python_path.exists():
|
|
266
|
+
console.print("[red]Error: Virtual environment appears corrupted[/red]")
|
|
267
|
+
console.print("[dim]Run 'athf env setup --clean' to recreate[/dim]")
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
# Get Python version
|
|
271
|
+
try:
|
|
272
|
+
result = subprocess.run(
|
|
273
|
+
[str(python_path), "--version"],
|
|
274
|
+
check=True,
|
|
275
|
+
capture_output=True,
|
|
276
|
+
text=True,
|
|
277
|
+
)
|
|
278
|
+
python_version = result.stdout.strip()
|
|
279
|
+
except subprocess.CalledProcessError:
|
|
280
|
+
python_version = "Unknown"
|
|
281
|
+
|
|
282
|
+
# Get installed packages count
|
|
283
|
+
pip_path = python_path.parent / "pip"
|
|
284
|
+
package_count: Union[int, str]
|
|
285
|
+
try:
|
|
286
|
+
result = subprocess.run(
|
|
287
|
+
[str(pip_path), "list", "--format", "freeze"],
|
|
288
|
+
check=True,
|
|
289
|
+
capture_output=True,
|
|
290
|
+
text=True,
|
|
291
|
+
)
|
|
292
|
+
package_count = len(result.stdout.strip().split("\n"))
|
|
293
|
+
except subprocess.CalledProcessError:
|
|
294
|
+
package_count = "Unknown"
|
|
295
|
+
|
|
296
|
+
# Check for athf installation
|
|
297
|
+
try:
|
|
298
|
+
result = subprocess.run(
|
|
299
|
+
[str(pip_path), "show", "agentic-threat-hunting-framework"],
|
|
300
|
+
check=True,
|
|
301
|
+
capture_output=True,
|
|
302
|
+
text=True,
|
|
303
|
+
)
|
|
304
|
+
athf_installed = "✅ Installed" if result.returncode == 0 else "❌ Not installed"
|
|
305
|
+
except subprocess.CalledProcessError:
|
|
306
|
+
athf_installed = "❌ Not installed"
|
|
307
|
+
|
|
308
|
+
# Check for scikit-learn
|
|
309
|
+
try:
|
|
310
|
+
result = subprocess.run(
|
|
311
|
+
[str(pip_path), "show", "scikit-learn"],
|
|
312
|
+
check=True,
|
|
313
|
+
capture_output=True,
|
|
314
|
+
text=True,
|
|
315
|
+
)
|
|
316
|
+
sklearn_installed = "✅ Installed" if result.returncode == 0 else "❌ Not installed"
|
|
317
|
+
except subprocess.CalledProcessError:
|
|
318
|
+
sklearn_installed = "❌ Not installed"
|
|
319
|
+
|
|
320
|
+
# Display info
|
|
321
|
+
console.print("\n[bold]Virtual Environment Info:[/bold]\n")
|
|
322
|
+
console.print(f" [cyan]Location:[/cyan] {venv_path.absolute()}")
|
|
323
|
+
console.print(f" [cyan]Python:[/cyan] {python_version}")
|
|
324
|
+
console.print(f" [cyan]Packages:[/cyan] {package_count} installed")
|
|
325
|
+
console.print(f" [cyan]athf:[/cyan] {athf_installed}")
|
|
326
|
+
console.print(f" [cyan]scikit-learn:[/cyan] {sklearn_installed} [dim](required for athf similar)[/dim]")
|
|
327
|
+
console.print()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@env.command(name="activate")
|
|
331
|
+
def activate() -> None:
|
|
332
|
+
"""Show command to activate virtual environment.
|
|
333
|
+
|
|
334
|
+
Note: Cannot activate directly (subprocesses can't modify parent shell).
|
|
335
|
+
Copy and run the printed command to activate.
|
|
336
|
+
|
|
337
|
+
\b
|
|
338
|
+
Example:
|
|
339
|
+
athf env activate
|
|
340
|
+
# Then copy and run the printed command
|
|
341
|
+
"""
|
|
342
|
+
venv_path = Path(".venv")
|
|
343
|
+
|
|
344
|
+
if not venv_path.exists():
|
|
345
|
+
console.print("[yellow]No .venv directory found[/yellow]")
|
|
346
|
+
console.print("[dim]Run 'athf env setup' to create[/dim]")
|
|
347
|
+
raise click.Abort()
|
|
348
|
+
|
|
349
|
+
# Determine activation command based on platform
|
|
350
|
+
if sys.platform == "win32":
|
|
351
|
+
activate_cmd = ".venv\\Scripts\\activate"
|
|
352
|
+
else:
|
|
353
|
+
activate_cmd = "source .venv/bin/activate"
|
|
354
|
+
|
|
355
|
+
console.print("\n[bold cyan]To activate the virtual environment, run:[/bold cyan]\n")
|
|
356
|
+
console.print(f" [green]{activate_cmd}[/green]\n")
|
|
357
|
+
console.print("[dim]💡 Tip: Copy the command above and run it in your shell[/dim]\n")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@env.command(name="deactivate")
|
|
361
|
+
def deactivate_cmd() -> None:
|
|
362
|
+
"""Show command to deactivate virtual environment.
|
|
363
|
+
|
|
364
|
+
Note: Cannot deactivate directly (subprocesses can't modify parent shell).
|
|
365
|
+
Copy and run the printed command to deactivate.
|
|
366
|
+
|
|
367
|
+
\b
|
|
368
|
+
Example:
|
|
369
|
+
athf env deactivate
|
|
370
|
+
# Then copy and run the printed command
|
|
371
|
+
"""
|
|
372
|
+
console.print("\n[bold cyan]To deactivate the virtual environment, run:[/bold cyan]\n")
|
|
373
|
+
console.print(" [green]deactivate[/green]\n")
|
|
374
|
+
console.print("[dim]💡 This will return you to your system Python[/dim]\n")
|
athf/commands/hunt.py
CHANGED
|
@@ -502,21 +502,57 @@ def search(query: str) -> None:
|
|
|
502
502
|
console.print()
|
|
503
503
|
|
|
504
504
|
|
|
505
|
+
def _render_progress_bar(covered: int, total: int, width: int = 20) -> str:
|
|
506
|
+
"""Render a visual progress bar with filled and empty blocks.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
covered: Number of covered techniques
|
|
510
|
+
total: Total number of techniques
|
|
511
|
+
width: Width of the progress bar in characters
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
ASCII progress bar string using simple characters
|
|
515
|
+
"""
|
|
516
|
+
if total == 0:
|
|
517
|
+
return "·" * width
|
|
518
|
+
|
|
519
|
+
# Cap percentage at 100% for visual display
|
|
520
|
+
percentage = min(covered / total, 1.0)
|
|
521
|
+
filled = int(percentage * width)
|
|
522
|
+
empty = width - filled
|
|
523
|
+
|
|
524
|
+
# Use simple characters that render reliably
|
|
525
|
+
filled_char = "■"
|
|
526
|
+
empty_char = "·"
|
|
527
|
+
|
|
528
|
+
return filled_char * filled + empty_char * empty
|
|
529
|
+
|
|
530
|
+
|
|
505
531
|
@hunt.command()
|
|
506
|
-
|
|
532
|
+
@click.option("--detailed", is_flag=True, help="Show detailed technique coverage with hunt references")
|
|
533
|
+
def coverage(detailed: bool) -> None:
|
|
507
534
|
"""Show MITRE ATT&CK technique coverage across hunts.
|
|
508
535
|
|
|
509
536
|
\b
|
|
510
537
|
Analyzes and displays:
|
|
511
|
-
•
|
|
512
|
-
•
|
|
513
|
-
•
|
|
514
|
-
•
|
|
538
|
+
• Hunt count per tactic across all 14 ATT&CK tactics
|
|
539
|
+
• Technique count per tactic (with caveats - see note below)
|
|
540
|
+
• Overall unique technique coverage across all hunts
|
|
541
|
+
• Detailed technique-to-hunt mapping (with --detailed)
|
|
515
542
|
|
|
516
543
|
\b
|
|
517
|
-
|
|
544
|
+
Examples:
|
|
545
|
+
# Show coverage overview
|
|
518
546
|
athf hunt coverage
|
|
519
547
|
|
|
548
|
+
# Show detailed technique mapping
|
|
549
|
+
athf hunt coverage --detailed
|
|
550
|
+
|
|
551
|
+
\b
|
|
552
|
+
Note on technique counts:
|
|
553
|
+
Per-tactic technique counts may include duplicates if hunts cover
|
|
554
|
+
multiple tactics. The overall unique technique count (bottom) is accurate.
|
|
555
|
+
|
|
520
556
|
\b
|
|
521
557
|
Use this to:
|
|
522
558
|
• Identify blind spots in your hunting program
|
|
@@ -527,23 +563,64 @@ def coverage() -> None:
|
|
|
527
563
|
|
|
528
564
|
\b
|
|
529
565
|
Pro tip:
|
|
530
|
-
|
|
531
|
-
|
|
566
|
+
Focus on tactics with no coverage that align with your threat model.
|
|
567
|
+
Use --detailed to see which specific techniques each hunt covers.
|
|
532
568
|
"""
|
|
569
|
+
from athf.core.attack_matrix import ATTACK_TACTICS, get_sorted_tactics
|
|
570
|
+
|
|
533
571
|
manager = HuntManager()
|
|
534
572
|
coverage = manager.calculate_attack_coverage()
|
|
535
573
|
|
|
536
|
-
if not coverage:
|
|
574
|
+
if not coverage or not coverage.get("by_tactic"):
|
|
537
575
|
console.print("[yellow]No hunt coverage data available.[/yellow]")
|
|
538
576
|
return
|
|
539
577
|
|
|
540
|
-
|
|
578
|
+
summary = coverage["summary"]
|
|
579
|
+
by_tactic = coverage["by_tactic"]
|
|
541
580
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
581
|
+
# Display title
|
|
582
|
+
console.print("\n[bold]MITRE ATT&CK Coverage[/bold]")
|
|
583
|
+
console.print("─" * 60 + "\n")
|
|
584
|
+
|
|
585
|
+
# Display all tactics in ATT&CK order with hunt counts
|
|
586
|
+
for tactic_key in get_sorted_tactics():
|
|
587
|
+
data = by_tactic.get(tactic_key, {})
|
|
588
|
+
tactic_name = ATTACK_TACTICS[tactic_key]["name"]
|
|
589
|
+
|
|
590
|
+
hunt_count = data.get("hunt_count", 0)
|
|
591
|
+
techniques_covered = data.get("techniques_covered", 0)
|
|
592
|
+
|
|
593
|
+
# Format: "Tactic Name 2 hunts, 7 techniques"
|
|
594
|
+
if hunt_count > 0:
|
|
595
|
+
console.print(f"{tactic_name:<24} {hunt_count} hunts, {techniques_covered} techniques")
|
|
596
|
+
else:
|
|
597
|
+
console.print(f"{tactic_name:<24} [dim]no coverage[/dim]")
|
|
598
|
+
|
|
599
|
+
# Display overall coverage
|
|
600
|
+
console.print(
|
|
601
|
+
f"\n[bold]Overall: {summary['unique_techniques']}/{summary['total_techniques']} techniques ({summary['overall_coverage_pct']:.0f}%)[/bold]\n"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Display detailed technique coverage if requested
|
|
605
|
+
if detailed:
|
|
606
|
+
console.print("\n[bold cyan]🔍 Detailed Technique Coverage[/bold cyan]\n")
|
|
607
|
+
|
|
608
|
+
for tactic_key in get_sorted_tactics():
|
|
609
|
+
data = by_tactic.get(tactic_key, {})
|
|
610
|
+
if data.get("hunt_count", 0) == 0:
|
|
611
|
+
continue # Skip tactics with no hunts in detailed view
|
|
612
|
+
|
|
613
|
+
tactic_name = ATTACK_TACTICS[tactic_key]["name"]
|
|
614
|
+
console.print(
|
|
615
|
+
f"\n[bold]{tactic_name}[/bold] ({data['hunt_count']} hunts, {len(data['techniques'])} unique techniques)"
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# Show techniques with hunt references
|
|
619
|
+
for technique, hunt_ids in sorted(data["techniques"].items()):
|
|
620
|
+
hunt_refs = ", ".join(sorted(set(hunt_ids))) # Remove duplicates and sort
|
|
621
|
+
console.print(f" • [yellow]{technique}[/yellow] - {hunt_refs}")
|
|
622
|
+
|
|
623
|
+
console.print()
|
|
547
624
|
|
|
548
625
|
|
|
549
626
|
@hunt.command(hidden=True)
|