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.
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/METADATA +38 -40
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/RECORD +24 -15
- athf/__version__.py +1 -1
- athf/agents/__init__.py +14 -0
- athf/agents/base.py +141 -0
- athf/agents/llm/__init__.py +27 -0
- athf/agents/llm/hunt_researcher.py +762 -0
- athf/agents/llm/hypothesis_generator.py +238 -0
- athf/cli.py +6 -1
- athf/commands/__init__.py +4 -0
- athf/commands/agent.py +452 -0
- athf/commands/context.py +6 -9
- athf/commands/env.py +2 -2
- athf/commands/hunt.py +3 -1
- athf/commands/research.py +530 -0
- athf/commands/similar.py +3 -3
- athf/core/research_manager.py +419 -0
- athf/core/web_search.py +340 -0
- athf/data/__init__.py +6 -1
- athf/data/docs/CHANGELOG.md +23 -0
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/WHEEL +0 -0
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/entry_points.txt +0 -0
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/top_level.txt +0 -0
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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(
|
|
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()
|