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.
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
- def coverage() -> None:
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
- 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
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
- Example:
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
- Combine with threat intel to focus on attacker-relevant TTPs.
531
- Example: "Which persistence techniques are we NOT hunting?"
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
- console.print("\n[bold cyan]🎯 MITRE ATT&CK Coverage[/bold cyan]\n")
578
+ summary = coverage["summary"]
579
+ by_tactic = coverage["by_tactic"]
541
580
 
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()
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)