agentic-threat-hunting-framework 0.2.4__py3-none-any.whl → 0.3.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/agent.py ADDED
@@ -0,0 +1,452 @@
1
+ """Agent management commands."""
2
+
3
+ import json
4
+ from typing import Any, List, Optional
5
+
6
+ import click
7
+ from rich.console import Console
8
+
9
+ console = Console()
10
+
11
+ AGENT_EPILOG = """
12
+ \b
13
+ Examples:
14
+ # List all available agents
15
+ athf agent list
16
+
17
+ # Get information about an agent
18
+ athf agent info hypothesis-generator
19
+
20
+ # Run hypothesis generator agent
21
+ athf agent run hypothesis-generator --threat-intel "APT29 targeting SaaS applications"
22
+
23
+ \b
24
+ Agent Types:
25
+ • LLM Agents - AI-powered agents using Claude API via AWS Bedrock
26
+
27
+ \b
28
+ Why Agents:
29
+ • Standardized interfaces for hunt operations
30
+ • Composable building blocks for workflows
31
+ • Consistent error handling and result formats
32
+ • Foundation for AI orchestration
33
+ """
34
+
35
+
36
+ @click.group(epilog=AGENT_EPILOG)
37
+ def agent() -> None:
38
+ """Manage ATHF agents.
39
+
40
+ Agents provide modular capabilities for threat hunting operations.
41
+ LLM agents use Claude API for creative and analytical tasks.
42
+
43
+ \b
44
+ Agent Execution Modes:
45
+ • INTERACTIVE (default): Step-by-step execution with user approval
46
+ • AUTONOMOUS (--auto): Runs all steps without check-ins
47
+ """
48
+ pass
49
+
50
+
51
+ @agent.command()
52
+ def list() -> None:
53
+ """List all available agents.
54
+
55
+ Displays registered agents with their type, status, and description.
56
+ """
57
+ from rich.table import Table
58
+
59
+ agents = [
60
+ {
61
+ "name": "hypothesis-generator",
62
+ "type": "LLM (Claude)",
63
+ "status": "available",
64
+ "description": "Generates creative hunt hypotheses using threat intelligence",
65
+ },
66
+ {
67
+ "name": "hunt-researcher",
68
+ "type": "LLM (Claude)",
69
+ "status": "available",
70
+ "description": "Conducts thorough pre-hunt research using 5-skill methodology",
71
+ },
72
+ ]
73
+
74
+ # Create table
75
+ table = Table(show_header=True, header_style="bold cyan")
76
+ table.add_column("Agent Name", style="cyan", no_wrap=True)
77
+ table.add_column("Type", style="yellow", no_wrap=True, width=15)
78
+ table.add_column("Status", style="green", no_wrap=True, width=12)
79
+ table.add_column("Description", style="white")
80
+
81
+ for agent_info in agents:
82
+ name = agent_info["name"]
83
+ agent_type = agent_info["type"]
84
+ status = agent_info["status"]
85
+ description = agent_info["description"]
86
+
87
+ # Status emoji
88
+ status_display = f"✅ {status}"
89
+ table.add_row(name, agent_type, status_display, description)
90
+
91
+ console.print("\n[bold]Available Agents:[/bold]\n")
92
+ console.print(table)
93
+ console.print()
94
+
95
+
96
+ @agent.command()
97
+ @click.argument("agent_name")
98
+ def info(agent_name: str) -> None:
99
+ """Show detailed information about an agent.
100
+
101
+ \b
102
+ Example:
103
+ athf agent info hypothesis-generator
104
+ athf agent info hunt-researcher
105
+ """
106
+ if agent_name == "hypothesis-generator":
107
+ # Display agent info
108
+ console.print("\n[bold cyan]Agent:[/bold cyan] hypothesis-generator")
109
+ console.print("[bold]Type:[/bold] LLM (Claude)")
110
+ console.print("[bold]Status:[/bold] available")
111
+ console.print("\n[bold]Description:[/bold]")
112
+ console.print(" Generates creative hunt hypotheses using threat intelligence")
113
+
114
+ console.print("\n[bold]Capabilities:[/bold]")
115
+ capabilities = [
116
+ "LOCK format generation",
117
+ "ATT&CK mapping",
118
+ "Environment validation",
119
+ "Past hunt deduplication",
120
+ "Fallback to template generation",
121
+ "Cost tracking",
122
+ ]
123
+ for cap in capabilities:
124
+ console.print(f" • {cap}")
125
+
126
+ console.print("\n[bold]Usage:[/bold]")
127
+ console.print(' athf agent run hypothesis-generator --threat-intel "APT29 targeting SaaS"')
128
+ console.print()
129
+
130
+ elif agent_name == "hunt-researcher":
131
+ console.print("\n[bold cyan]Agent:[/bold cyan] hunt-researcher")
132
+ console.print("[bold]Type:[/bold] LLM (Claude)")
133
+ console.print("[bold]Status:[/bold] available")
134
+ console.print("\n[bold]Description:[/bold]")
135
+ console.print(" Conducts thorough pre-hunt research using 5-skill methodology")
136
+
137
+ console.print("\n[bold]Capabilities:[/bold]")
138
+ capabilities = [
139
+ "System internals research (how it normally works)",
140
+ "Adversary tradecraft research via web search",
141
+ "Telemetry mapping to OCSF fields",
142
+ "Related past hunt discovery",
143
+ "Research synthesis with gaps identification",
144
+ "Recommended hypothesis generation",
145
+ "Cost tracking and metrics",
146
+ ]
147
+ for cap in capabilities:
148
+ console.print(f" • {cap}")
149
+
150
+ console.print("\n[bold]Research Skills:[/bold]")
151
+ console.print(" 1. System Research - How technology normally works")
152
+ console.print(" 2. Adversary Tradecraft - Attack techniques (web search)")
153
+ console.print(" 3. Telemetry Mapping - OCSF field availability")
154
+ console.print(" 4. Related Work - Past hunt correlation")
155
+ console.print(" 5. Synthesis - Key findings and gaps")
156
+
157
+ console.print("\n[bold]Usage:[/bold]")
158
+ console.print(' athf agent run hunt-researcher --topic "LSASS dumping"')
159
+ console.print(' athf agent run hunt-researcher --topic "Pass-the-Hash" --technique T1003.002 --depth basic')
160
+ console.print()
161
+
162
+ else:
163
+ console.print(f"[red]Error: Agent '{agent_name}' not found[/red]")
164
+ console.print("\n[dim]Available agents:[/dim]")
165
+ console.print(" • hypothesis-generator")
166
+ console.print(" • hunt-researcher")
167
+ raise click.Abort()
168
+
169
+
170
+ @agent.command()
171
+ @click.argument("agent_name")
172
+ @click.option("--threat-intel", help="Threat intelligence context (for hypothesis-generator)")
173
+ @click.option("--topic", help="Research topic (for hunt-researcher)")
174
+ @click.option("--technique", help="MITRE ATT&CK technique (for hunt-researcher)")
175
+ @click.option(
176
+ "--depth",
177
+ type=click.Choice(["basic", "advanced"]),
178
+ default="advanced",
179
+ help="Research depth: basic (5 min) or advanced (15-20 min) (for hunt-researcher)",
180
+ )
181
+ @click.option("--no-web-search", is_flag=True, help="Skip web search - offline mode (for hunt-researcher)")
182
+ @click.option("--tactic", help="MITRE tactic filter")
183
+ @click.option("--llm/--no-llm", default=True, help="Enable/disable LLM (default: enabled)")
184
+ @click.option(
185
+ "--output-format",
186
+ "output_format",
187
+ type=click.Choice(["table", "json"]),
188
+ default="table",
189
+ help="Output format",
190
+ )
191
+ def run( # noqa: C901
192
+ agent_name: str,
193
+ threat_intel: Optional[str],
194
+ topic: Optional[str],
195
+ technique: Optional[str],
196
+ depth: str,
197
+ no_web_search: bool,
198
+ tactic: Optional[str],
199
+ llm: bool,
200
+ output_format: str,
201
+ ) -> None:
202
+ """Run an agent.
203
+
204
+ LLM agents use Claude API via AWS Bedrock by default. Use --no-llm for fallback mode.
205
+
206
+ \b
207
+ Examples:
208
+ # Hypothesis Generator
209
+ athf agent run hypothesis-generator --threat-intel "APT29 targeting SaaS applications"
210
+ athf agent run hypothesis-generator --threat-intel "Insider threat data exfiltration" --tactic collection
211
+
212
+ # Hunt Researcher
213
+ athf agent run hunt-researcher --topic "LSASS dumping"
214
+ athf agent run hunt-researcher --topic "Pass-the-Hash" --technique T1003.002 --depth basic
215
+ athf agent run hunt-researcher --topic "Credential Access" --no-web-search
216
+
217
+ # Fallback mode (no LLM)
218
+ athf agent run hypothesis-generator --threat-intel "..." --no-llm
219
+ """
220
+ if agent_name == "hypothesis-generator":
221
+ if not threat_intel:
222
+ console.print("[red]Error: --threat-intel required for hypothesis-generator[/red]")
223
+ raise click.Abort()
224
+
225
+ try:
226
+ # Import LLM agents
227
+ from athf.agents.llm import HypothesisGenerationInput, HypothesisGeneratorAgent
228
+
229
+ hypothesis_agent = HypothesisGeneratorAgent(llm_enabled=llm)
230
+
231
+ # Load context for hypothesis generation
232
+ # Try to load past hunts and environment data if available
233
+ past_hunts: List[dict[str, Any]] = []
234
+ environment = {}
235
+
236
+ # Try to load environment.md if it exists
237
+ try:
238
+ from pathlib import Path
239
+
240
+ env_file = Path("environment.md")
241
+ if env_file.exists():
242
+ # Parse basic environment info (data sources, platforms)
243
+ # TODO: Parse actual content from environment.md
244
+ environment = {
245
+ "data_sources": ["EDR telemetry", "SIEM logs", "Cloud logs"],
246
+ "platforms": ["Windows", "macOS", "Linux"],
247
+ }
248
+ except Exception:
249
+ # Use defaults if environment.md not found
250
+ environment = {
251
+ "data_sources": ["EDR telemetry", "SIEM logs"],
252
+ "platforms": ["Windows", "macOS", "Linux"],
253
+ }
254
+
255
+ # Execute agent
256
+ hypothesis_result = hypothesis_agent.execute(
257
+ HypothesisGenerationInput(
258
+ threat_intel=threat_intel,
259
+ past_hunts=past_hunts,
260
+ environment=environment,
261
+ )
262
+ )
263
+
264
+ if output_format == "json":
265
+ console.print(json.dumps(hypothesis_result.metadata, indent=2))
266
+ else:
267
+ _display_hypothesis_generator_result(hypothesis_result)
268
+
269
+ except ImportError as e:
270
+ console.print(f"[red]Error loading agent: {e}[/red]")
271
+ console.print("\n[dim]Make sure all dependencies are installed:[/dim]")
272
+ console.print(" pip install boto3")
273
+ raise click.Abort()
274
+ except Exception as e:
275
+ console.print(f"[red]Error: {e}[/red]")
276
+ raise click.Abort()
277
+
278
+ elif agent_name == "hunt-researcher":
279
+ if not topic:
280
+ console.print("[red]Error: --topic required for hunt-researcher[/red]")
281
+ raise click.Abort()
282
+
283
+ try:
284
+ from rich.progress import Progress, SpinnerColumn, TextColumn
285
+
286
+ from athf.agents.llm.hunt_researcher import HuntResearcherAgent, ResearchInput
287
+
288
+ console.print("\n[bold cyan]Starting Research[/bold cyan]")
289
+ console.print(f"[bold]Topic:[/bold] {topic}")
290
+ console.print(f"[bold]Depth:[/bold] {depth} ({'~5 min' if depth == 'basic' else '~15-20 min'})")
291
+ if technique:
292
+ console.print(f"[bold]Technique:[/bold] {technique}")
293
+ console.print()
294
+
295
+ research_agent = HuntResearcherAgent(llm_enabled=llm)
296
+
297
+ # Show progress
298
+ with Progress(
299
+ SpinnerColumn(),
300
+ TextColumn("[progress.description]{task.description}"),
301
+ console=console,
302
+ transient=True,
303
+ ) as progress:
304
+ progress.add_task("Conducting research...", total=None)
305
+
306
+ research_result = research_agent.execute(
307
+ ResearchInput(
308
+ topic=topic,
309
+ mitre_technique=technique,
310
+ depth=depth,
311
+ include_past_hunts=True,
312
+ include_telemetry_mapping=True,
313
+ web_search_enabled=not no_web_search,
314
+ )
315
+ )
316
+
317
+ if not research_result.is_success:
318
+ console.print(f"[red]✗ Research failed: {research_result.error}[/red]")
319
+ raise click.Abort()
320
+
321
+ if output_format == "json":
322
+ console.print(json.dumps(research_result.metadata, indent=2))
323
+ else:
324
+ _display_research_result(research_result)
325
+
326
+ except ImportError as e:
327
+ console.print(f"[red]Error loading agent: {e}[/red]")
328
+ console.print("\n[dim]Make sure all dependencies are installed:[/dim]")
329
+ console.print(" pip install boto3 tavily-python")
330
+ raise click.Abort()
331
+ except Exception as e:
332
+ console.print(f"[red]Error: {e}[/red]")
333
+ raise click.Abort()
334
+
335
+ else:
336
+ console.print(f"[red]Error: Unknown agent: {agent_name}[/red]")
337
+ console.print("\n[dim]Available agents:[/dim]")
338
+ console.print(" • hypothesis-generator")
339
+ console.print(" • hunt-researcher")
340
+ raise click.Abort()
341
+
342
+
343
+ def _display_hypothesis_generator_result(result: Any) -> None: # noqa: C901
344
+ """Display hypothesis generator result."""
345
+ if not result.is_success:
346
+ console.print(f"[red]✗ Agent Error: {result.error}[/red]\n")
347
+ return
348
+
349
+ data = result.data
350
+
351
+ console.print("[green]✓ Hypothesis generated successfully[/green]\n")
352
+
353
+ console.print("[bold cyan]Hypothesis:[/bold cyan]")
354
+ console.print(f" {data.hypothesis}\n")
355
+
356
+ console.print("[bold cyan]Justification:[/bold cyan]")
357
+ console.print(f" {data.justification}\n")
358
+
359
+ if data.mitre_techniques:
360
+ console.print("[bold cyan]MITRE ATT&CK Techniques:[/bold cyan]")
361
+ for technique in data.mitre_techniques:
362
+ console.print(f" • {technique}")
363
+ console.print()
364
+
365
+ if data.data_sources:
366
+ console.print("[bold cyan]Data Sources:[/bold cyan]")
367
+ for source in data.data_sources:
368
+ console.print(f" • {source}")
369
+ console.print()
370
+
371
+ if data.expected_observables:
372
+ console.print("[bold cyan]Expected Observables:[/bold cyan]")
373
+ for observable in data.expected_observables:
374
+ console.print(f" • {observable}")
375
+ console.print()
376
+
377
+ if data.known_false_positives:
378
+ console.print("[bold cyan]Known False Positives:[/bold cyan]")
379
+ for fp in data.known_false_positives:
380
+ console.print(f" • {fp}")
381
+ console.print()
382
+
383
+ console.print(f"[bold cyan]Time Range:[/bold cyan] {data.time_range_suggestion}\n")
384
+
385
+ if result.warnings:
386
+ console.print("[bold yellow]Warnings:[/bold yellow]")
387
+ for warning in result.warnings:
388
+ console.print(f" • {warning}")
389
+ console.print()
390
+
391
+ if result.metadata:
392
+ if "cost_usd" in result.metadata:
393
+ console.print(f"[dim]Cost: ${result.metadata['cost_usd']:.4f}[/dim]")
394
+ if "prompt_tokens" in result.metadata:
395
+ console.print(
396
+ f"[dim]Tokens: {result.metadata['prompt_tokens']} input + {result.metadata['completion_tokens']} output[/dim]"
397
+ )
398
+ console.print()
399
+
400
+
401
+ def _display_research_result(result: Any) -> None:
402
+ """Display research result."""
403
+ from rich.panel import Panel
404
+
405
+ if not result.is_success:
406
+ console.print(f"[red]✗ Agent Error: {result.error}[/red]\n")
407
+ return
408
+
409
+ output = result.data
410
+
411
+ # Success panel
412
+ console.print()
413
+ console.print(
414
+ Panel(
415
+ f"[bold green]Research Complete: {output.research_id}[/bold green]\n\n"
416
+ f"[bold]Topic:[/bold] {output.topic}\n"
417
+ f"[bold]Duration:[/bold] {output.total_duration_ms / 1000:.1f} seconds\n"
418
+ f"[bold]Cost:[/bold] ${output.total_cost_usd:.4f}\n"
419
+ f"[bold]Web Searches:[/bold] {output.web_searches_performed}\n"
420
+ f"[bold]LLM Calls:[/bold] {output.llm_calls}",
421
+ title="Research Complete",
422
+ border_style="green",
423
+ )
424
+ )
425
+
426
+ # Summary of findings
427
+ console.print("\n[bold cyan]Key Findings Summary[/bold cyan]")
428
+
429
+ # System Research
430
+ console.print(f"\n[bold]1. System Research:[/bold] {output.system_research.summary[:100]}...")
431
+
432
+ # Adversary Tradecraft
433
+ console.print(f"\n[bold]2. Adversary Tradecraft:[/bold] {output.adversary_tradecraft.summary[:100]}...")
434
+
435
+ # Recommended Hypothesis
436
+ if output.recommended_hypothesis:
437
+ console.print("\n[bold green]Recommended Hypothesis:[/bold green]")
438
+ console.print(f" {output.recommended_hypothesis}")
439
+
440
+ # Gaps
441
+ if output.gaps_identified:
442
+ console.print("\n[bold yellow]Gaps Identified:[/bold yellow]")
443
+ for gap in output.gaps_identified[:3]:
444
+ console.print(f" - {gap}")
445
+
446
+ # Next steps
447
+ console.print("\n[bold]Next Steps:[/bold]")
448
+ console.print(" 1. Use standalone command for full research file:")
449
+ console.print(f" [cyan]athf research view {output.research_id}[/cyan]")
450
+ console.print(" 2. Generate hypothesis: [cyan]athf agent run hypothesis-generator[/cyan]")
451
+ console.print(f" 3. Create hunt: [cyan]athf hunt new --research {output.research_id}[/cyan]")
452
+ console.print()
athf/commands/context.py CHANGED
@@ -120,7 +120,7 @@ def context(
120
120
 
121
121
  # Write to file or stdout
122
122
  if output:
123
- Path(output).write_text(formatted_output, encoding='utf-8')
123
+ Path(output).write_text(formatted_output, encoding="utf-8")
124
124
  console.print(f"[green]✅ Context exported to: {output}[/green]")
125
125
  else:
126
126
  # Use plain print() for JSON/YAML to avoid Rich formatting issues
@@ -130,7 +130,7 @@ def context(
130
130
  console.print(formatted_output)
131
131
 
132
132
 
133
- def _build_context(
133
+ def _build_context( # noqa: C901
134
134
  hunt: Optional[str] = None,
135
135
  tactic: Optional[str] = None,
136
136
  platform: Optional[str] = None,
@@ -211,14 +211,11 @@ def _build_context(
211
211
 
212
212
  def _read_and_optimize(file_path: Path) -> str:
213
213
  """Read file and optimize for token efficiency."""
214
- content = file_path.read_text(encoding='utf-8')
214
+ content = file_path.read_text(encoding="utf-8")
215
215
 
216
216
  # First pass: Remove all control characters except tabs and newlines
217
217
  # Control characters are U+0000 through U+001F (0-31), except tab (9), LF (10), CR (13)
218
- cleaned_content = "".join(
219
- char for char in content
220
- if ord(char) >= 32 or char in "\t\n\r"
221
- )
218
+ cleaned_content = "".join(char for char in content if ord(char) >= 32 or char in "\t\n\r")
222
219
 
223
220
  # Token optimization:
224
221
  # 1. Strip excessive whitespace (but preserve single newlines)
@@ -248,7 +245,7 @@ def _find_hunts_by_tactic(tactic: str) -> List[Path]:
248
245
  normalized_tactic = tactic.replace("-", " ").lower()
249
246
 
250
247
  for hunt_file in hunts_dir.glob("H-*.md"):
251
- content = hunt_file.read_text(encoding='utf-8')
248
+ content = hunt_file.read_text(encoding="utf-8")
252
249
 
253
250
  # Check YAML frontmatter for tactics field
254
251
  if content.startswith("---"):
@@ -277,7 +274,7 @@ def _find_hunts_by_platform(platform: str) -> List[Path]:
277
274
  normalized_platform = platform.lower()
278
275
 
279
276
  for hunt_file in hunts_dir.glob("H-*.md"):
280
- content = hunt_file.read_text(encoding='utf-8')
277
+ content = hunt_file.read_text(encoding="utf-8")
281
278
 
282
279
  # Check YAML frontmatter for platform field
283
280
  if content.startswith("---"):
athf/commands/env.py CHANGED
@@ -54,7 +54,7 @@ def env() -> None:
54
54
  )
55
55
  @click.option("--dev", is_flag=True, help="Install development dependencies")
56
56
  @click.option("--clean", is_flag=True, help="Remove existing venv before creating")
57
- def setup(python: str, dev: bool, clean: bool) -> None:
57
+ def setup(python: str, dev: bool, clean: bool) -> None: # noqa: C901
58
58
  """Setup Python virtual environment with dependencies.
59
59
 
60
60
  Creates .venv directory and installs athf package with
@@ -240,7 +240,7 @@ def clean() -> None:
240
240
 
241
241
 
242
242
  @env.command(name="info")
243
- def info() -> None:
243
+ def info() -> None: # noqa: C901
244
244
  """Show virtual environment information.
245
245
 
246
246
  Display Python version, installed packages, and venv location.
athf/commands/hunt.py CHANGED
@@ -448,7 +448,9 @@ def stats() -> None:
448
448
  # Easter egg: First True Positive milestone
449
449
  if stats["true_positives"] == 1 and stats["completed_hunts"] > 0:
450
450
  console.print("[bold yellow]🎯 First True Positive Detected![/bold yellow]\n")
451
- console.print("[italic]Every expert threat hunter started here. This confirms your hypothesis was testable, your data was sufficient, and your analytical instincts were sound. Document what worked.[/italic]\n")
451
+ console.print(
452
+ "[italic]Every expert threat hunter started here. This confirms your hypothesis was testable, your data was sufficient, and your analytical instincts were sound. Document what worked.[/italic]\n"
453
+ )
452
454
 
453
455
 
454
456
  @hunt.command()