agentic-threat-hunting-framework 0.2.3__py3-none-any.whl → 0.3.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.2.3.dist-info → agentic_threat_hunting_framework-0.3.0.dist-info}/METADATA +38 -40
- agentic_threat_hunting_framework-0.3.0.dist-info/RECORD +51 -0
- athf/__version__.py +1 -1
- athf/cli.py +7 -2
- 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 -3
- athf/commands/init.py +45 -0
- athf/commands/research.py +530 -0
- athf/commands/similar.py +5 -5
- athf/core/research_manager.py +419 -0
- athf/core/web_search.py +340 -0
- athf/data/__init__.py +19 -0
- athf/data/docs/CHANGELOG.md +147 -0
- athf/data/docs/CLI_REFERENCE.md +1797 -0
- athf/data/docs/INSTALL.md +594 -0
- athf/data/docs/README.md +31 -0
- athf/data/docs/environment.md +256 -0
- athf/data/docs/getting-started.md +419 -0
- athf/data/docs/level4-agentic-workflows.md +480 -0
- athf/data/docs/lock-pattern.md +149 -0
- athf/data/docs/maturity-model.md +400 -0
- athf/data/docs/why-athf.md +44 -0
- athf/data/hunts/FORMAT_GUIDELINES.md +507 -0
- athf/data/hunts/H-0001.md +453 -0
- athf/data/hunts/H-0002.md +436 -0
- athf/data/hunts/H-0003.md +546 -0
- athf/data/hunts/README.md +231 -0
- athf/data/integrations/MCP_CATALOG.md +45 -0
- athf/data/integrations/README.md +129 -0
- athf/data/integrations/quickstart/splunk.md +162 -0
- athf/data/knowledge/hunting-knowledge.md +2375 -0
- athf/data/prompts/README.md +172 -0
- athf/data/prompts/ai-workflow.md +581 -0
- athf/data/prompts/basic-prompts.md +316 -0
- athf/data/templates/HUNT_LOCK.md +228 -0
- agentic_threat_hunting_framework-0.2.3.dist-info/RECORD +0 -23
- {agentic_threat_hunting_framework-0.2.3.dist-info → agentic_threat_hunting_framework-0.3.0.dist-info}/WHEEL +0 -0
- {agentic_threat_hunting_framework-0.2.3.dist-info → agentic_threat_hunting_framework-0.3.0.dist-info}/entry_points.txt +0 -0
- {agentic_threat_hunting_framework-0.2.3.dist-info → agentic_threat_hunting_framework-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {agentic_threat_hunting_framework-0.2.3.dist-info → agentic_threat_hunting_framework-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""Research management commands - thorough pre-hunt investigation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from athf.agents.llm.hunt_researcher import ResearchOutput
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
RESEARCH_EPILOG = """
|
|
18
|
+
\b
|
|
19
|
+
Examples:
|
|
20
|
+
# Start new research (15-20 minute deep dive)
|
|
21
|
+
athf research new --topic "LSASS memory dumping"
|
|
22
|
+
|
|
23
|
+
# Quick research (5 minutes)
|
|
24
|
+
athf research new --topic "Pass-the-Hash" --depth basic
|
|
25
|
+
|
|
26
|
+
# Research with specific technique
|
|
27
|
+
athf research new --topic "Credential Access" --technique T1003.001
|
|
28
|
+
|
|
29
|
+
# List all research
|
|
30
|
+
athf research list
|
|
31
|
+
|
|
32
|
+
# View specific research
|
|
33
|
+
athf research view R-0001
|
|
34
|
+
|
|
35
|
+
# Create hunt from research
|
|
36
|
+
athf hunt new --research R-0001
|
|
37
|
+
|
|
38
|
+
\b
|
|
39
|
+
Research Skills (5-skill methodology):
|
|
40
|
+
1. System Research - How does this technology normally work?
|
|
41
|
+
2. Adversary Tradecraft - How do adversaries abuse it? (web search)
|
|
42
|
+
3. Telemetry Mapping - What OCSF fields capture this?
|
|
43
|
+
4. Related Work - What past hunts are relevant?
|
|
44
|
+
5. Research Synthesis - Key findings, gaps, focus areas
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@click.group(epilog=RESEARCH_EPILOG)
|
|
49
|
+
def research() -> None:
|
|
50
|
+
"""Conduct thorough research before hunting.
|
|
51
|
+
|
|
52
|
+
\b
|
|
53
|
+
The research command helps you:
|
|
54
|
+
* Understand normal system behavior
|
|
55
|
+
* Discover adversary tradecraft via web search
|
|
56
|
+
* Map attacks to available telemetry
|
|
57
|
+
* Find related past work
|
|
58
|
+
* Synthesize actionable insights
|
|
59
|
+
|
|
60
|
+
\b
|
|
61
|
+
Research creates R-XXXX documents that can be linked to hunts.
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@research.command()
|
|
67
|
+
@click.option("--topic", required=True, help="Research topic (e.g., 'LSASS dumping')")
|
|
68
|
+
@click.option("--technique", help="MITRE ATT&CK technique (e.g., T1003.001)")
|
|
69
|
+
@click.option(
|
|
70
|
+
"--depth",
|
|
71
|
+
type=click.Choice(["basic", "advanced"]),
|
|
72
|
+
default="advanced",
|
|
73
|
+
help="Research depth: basic (5 min) or advanced (15-20 min)",
|
|
74
|
+
)
|
|
75
|
+
@click.option("--no-web-search", is_flag=True, help="Skip web search (offline mode)")
|
|
76
|
+
@click.option("--output", "output_format", type=click.Choice(["table", "json"]), default="table")
|
|
77
|
+
def new(
|
|
78
|
+
topic: str,
|
|
79
|
+
technique: Optional[str],
|
|
80
|
+
depth: str,
|
|
81
|
+
no_web_search: bool,
|
|
82
|
+
output_format: str,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Create new research document with thorough analysis.
|
|
85
|
+
|
|
86
|
+
\b
|
|
87
|
+
Performs 5-skill research methodology:
|
|
88
|
+
1. System Research - How does this normally work?
|
|
89
|
+
2. Adversary Tradecraft - Attack techniques (web search)
|
|
90
|
+
3. Telemetry Mapping - OCSF field mapping
|
|
91
|
+
4. Related Work - Past hunts correlation
|
|
92
|
+
5. Synthesis - Key findings and gaps
|
|
93
|
+
|
|
94
|
+
\b
|
|
95
|
+
Examples:
|
|
96
|
+
athf research new --topic "LSASS dumping"
|
|
97
|
+
athf research new --topic "Pass-the-Hash" --technique T1003.002 --depth basic
|
|
98
|
+
"""
|
|
99
|
+
from athf.agents.llm.hunt_researcher import HuntResearcherAgent, ResearchInput
|
|
100
|
+
from athf.core.research_manager import ResearchManager
|
|
101
|
+
|
|
102
|
+
# Get next research ID
|
|
103
|
+
manager = ResearchManager()
|
|
104
|
+
research_id = manager.get_next_research_id()
|
|
105
|
+
|
|
106
|
+
console.print(f"\n[bold cyan]Starting Research: {research_id}[/bold cyan]")
|
|
107
|
+
console.print(f"[bold]Topic:[/bold] {topic}")
|
|
108
|
+
console.print(f"[bold]Depth:[/bold] {depth} ({'~5 min' if depth == 'basic' else '~15-20 min'})")
|
|
109
|
+
if technique:
|
|
110
|
+
console.print(f"[bold]Technique:[/bold] {technique}")
|
|
111
|
+
console.print()
|
|
112
|
+
|
|
113
|
+
# Initialize agent
|
|
114
|
+
agent = HuntResearcherAgent(llm_enabled=True)
|
|
115
|
+
|
|
116
|
+
# Show progress for each skill
|
|
117
|
+
with Progress(
|
|
118
|
+
SpinnerColumn(),
|
|
119
|
+
TextColumn("[progress.description]{task.description}"),
|
|
120
|
+
console=console,
|
|
121
|
+
transient=True,
|
|
122
|
+
) as progress:
|
|
123
|
+
progress.add_task("Conducting research...", total=None)
|
|
124
|
+
|
|
125
|
+
# Execute research
|
|
126
|
+
result = agent.execute(
|
|
127
|
+
ResearchInput(
|
|
128
|
+
topic=topic,
|
|
129
|
+
mitre_technique=technique,
|
|
130
|
+
depth=depth,
|
|
131
|
+
include_past_hunts=True,
|
|
132
|
+
include_telemetry_mapping=True,
|
|
133
|
+
web_search_enabled=not no_web_search,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if not result.success:
|
|
138
|
+
console.print(f"[red]Research failed: {result.error}[/red]")
|
|
139
|
+
raise click.Abort()
|
|
140
|
+
|
|
141
|
+
output = result.data
|
|
142
|
+
if output is None:
|
|
143
|
+
console.print("[red]Research failed: No output data[/red]")
|
|
144
|
+
raise click.Abort()
|
|
145
|
+
|
|
146
|
+
# Generate markdown content
|
|
147
|
+
markdown_content = _generate_research_markdown(output)
|
|
148
|
+
|
|
149
|
+
# Create research file
|
|
150
|
+
frontmatter = {
|
|
151
|
+
"research_id": output.research_id,
|
|
152
|
+
"topic": output.topic,
|
|
153
|
+
"mitre_techniques": output.mitre_techniques,
|
|
154
|
+
"status": "completed",
|
|
155
|
+
"depth": depth,
|
|
156
|
+
"duration_minutes": round(output.total_duration_ms / 60000, 1),
|
|
157
|
+
"linked_hunts": [],
|
|
158
|
+
"web_searches": output.web_searches_performed,
|
|
159
|
+
"llm_calls": output.llm_calls,
|
|
160
|
+
"total_cost_usd": output.total_cost_usd,
|
|
161
|
+
"data_source_availability": output.data_source_availability,
|
|
162
|
+
"estimated_hunt_complexity": output.estimated_hunt_complexity,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
file_path = manager.create_research_file(
|
|
166
|
+
research_id=output.research_id,
|
|
167
|
+
topic=output.topic,
|
|
168
|
+
content=markdown_content,
|
|
169
|
+
frontmatter=frontmatter,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Display results
|
|
173
|
+
if output_format == "json":
|
|
174
|
+
_display_json_output(output)
|
|
175
|
+
else:
|
|
176
|
+
_display_research_summary(output, file_path)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@research.command(name="list")
|
|
180
|
+
@click.option("--status", help="Filter by status (draft, in_progress, completed)")
|
|
181
|
+
@click.option("--technique", help="Filter by MITRE technique")
|
|
182
|
+
@click.option("--output", "output_format", type=click.Choice(["table", "json"]), default="table")
|
|
183
|
+
def list_research(
|
|
184
|
+
status: Optional[str],
|
|
185
|
+
technique: Optional[str],
|
|
186
|
+
output_format: str,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""List all research documents."""
|
|
189
|
+
from athf.core.research_manager import ResearchManager
|
|
190
|
+
|
|
191
|
+
manager = ResearchManager()
|
|
192
|
+
research_list = manager.list_research(status=status, technique=technique)
|
|
193
|
+
|
|
194
|
+
if not research_list:
|
|
195
|
+
console.print("[yellow]No research documents found[/yellow]")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
if output_format == "json":
|
|
199
|
+
console.print(json.dumps(research_list, indent=2))
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
# Table output
|
|
203
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
204
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
205
|
+
table.add_column("Topic", style="white")
|
|
206
|
+
table.add_column("Status", style="yellow")
|
|
207
|
+
table.add_column("Techniques", style="dim")
|
|
208
|
+
table.add_column("Hunts", style="green")
|
|
209
|
+
table.add_column("Cost", style="dim")
|
|
210
|
+
|
|
211
|
+
for r in research_list:
|
|
212
|
+
techniques = ", ".join(r.get("mitre_techniques", [])[:2])
|
|
213
|
+
if len(r.get("mitre_techniques", [])) > 2:
|
|
214
|
+
techniques += "..."
|
|
215
|
+
|
|
216
|
+
linked_hunts = len(r.get("linked_hunts", []))
|
|
217
|
+
cost = f"${r.get('total_cost_usd', 0):.3f}"
|
|
218
|
+
|
|
219
|
+
table.add_row(
|
|
220
|
+
r.get("research_id", ""),
|
|
221
|
+
r.get("topic", "")[:40],
|
|
222
|
+
r.get("status", ""),
|
|
223
|
+
techniques,
|
|
224
|
+
str(linked_hunts) if linked_hunts > 0 else "-",
|
|
225
|
+
cost,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
console.print(table)
|
|
229
|
+
console.print(f"\n[dim]Total: {len(research_list)} research documents[/dim]")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@research.command()
|
|
233
|
+
@click.argument("research_id")
|
|
234
|
+
@click.option("--output", "output_format", type=click.Choice(["markdown", "json"]), default="markdown")
|
|
235
|
+
def view(research_id: str, output_format: str) -> None:
|
|
236
|
+
"""View a specific research document."""
|
|
237
|
+
from athf.core.research_manager import ResearchManager
|
|
238
|
+
|
|
239
|
+
manager = ResearchManager()
|
|
240
|
+
research_data = manager.get_research(research_id)
|
|
241
|
+
|
|
242
|
+
if not research_data:
|
|
243
|
+
console.print(f"[red]Research {research_id} not found[/red]")
|
|
244
|
+
raise click.Abort()
|
|
245
|
+
|
|
246
|
+
if output_format == "json":
|
|
247
|
+
console.print(json.dumps(research_data, indent=2, default=str))
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
# Display markdown content
|
|
251
|
+
file_path = research_data.get("file_path")
|
|
252
|
+
if file_path:
|
|
253
|
+
with open(file_path, "r") as f:
|
|
254
|
+
content = f.read()
|
|
255
|
+
console.print(content)
|
|
256
|
+
else:
|
|
257
|
+
console.print("[red]Research file not found[/red]")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@research.command()
|
|
261
|
+
@click.argument("query")
|
|
262
|
+
@click.option("--output", "output_format", type=click.Choice(["table", "json"]), default="table")
|
|
263
|
+
def search(query: str, output_format: str) -> None:
|
|
264
|
+
"""Search across research documents."""
|
|
265
|
+
from athf.core.research_manager import ResearchManager
|
|
266
|
+
|
|
267
|
+
manager = ResearchManager()
|
|
268
|
+
results = manager.search_research(query)
|
|
269
|
+
|
|
270
|
+
if not results:
|
|
271
|
+
console.print(f"[yellow]No research documents found matching '{query}'[/yellow]")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
if output_format == "json":
|
|
275
|
+
console.print(json.dumps(results, indent=2))
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
# Table output
|
|
279
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
280
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
281
|
+
table.add_column("Topic", style="white")
|
|
282
|
+
table.add_column("Status", style="yellow")
|
|
283
|
+
|
|
284
|
+
for r in results:
|
|
285
|
+
table.add_row(
|
|
286
|
+
r.get("research_id", ""),
|
|
287
|
+
r.get("topic", "")[:50],
|
|
288
|
+
r.get("status", ""),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
console.print(f"\n[bold]Search results for '{query}':[/bold]")
|
|
292
|
+
console.print(table)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@research.command()
|
|
296
|
+
@click.option("--output", "output_format", type=click.Choice(["table", "json"]), default="table")
|
|
297
|
+
def stats(output_format: str) -> None:
|
|
298
|
+
"""Show research program statistics."""
|
|
299
|
+
from athf.core.research_manager import ResearchManager
|
|
300
|
+
|
|
301
|
+
manager = ResearchManager()
|
|
302
|
+
statistics = manager.calculate_stats()
|
|
303
|
+
|
|
304
|
+
if output_format == "json":
|
|
305
|
+
console.print(json.dumps(statistics, indent=2))
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
# Display stats
|
|
309
|
+
console.print("\n[bold cyan]Research Program Statistics[/bold cyan]")
|
|
310
|
+
console.print(f"Total Research: {statistics['total_research']}")
|
|
311
|
+
console.print(f"Completed: {statistics['completed_research']}")
|
|
312
|
+
console.print(f"Total Cost: ${statistics['total_cost_usd']:.4f}")
|
|
313
|
+
console.print(f"Total Duration: {statistics['total_duration_minutes']} min")
|
|
314
|
+
console.print(f"Avg Duration: {statistics['avg_duration_minutes']:.1f} min")
|
|
315
|
+
console.print(f"Linked Hunts: {statistics['total_linked_hunts']}")
|
|
316
|
+
|
|
317
|
+
if statistics["by_status"]:
|
|
318
|
+
console.print("\n[bold]By Status:[/bold]")
|
|
319
|
+
for status, count in statistics["by_status"].items():
|
|
320
|
+
console.print(f" {status}: {count}")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _generate_research_markdown(output: ResearchOutput) -> str: # noqa: C901
|
|
324
|
+
"""Generate markdown content from research output."""
|
|
325
|
+
lines = []
|
|
326
|
+
|
|
327
|
+
# Title
|
|
328
|
+
lines.append(f"# {output.research_id}: {output.topic} Research")
|
|
329
|
+
lines.append("")
|
|
330
|
+
lines.append(f"**Topic:** {output.topic}")
|
|
331
|
+
if output.mitre_techniques:
|
|
332
|
+
lines.append(f"**MITRE ATT&CK:** {', '.join(output.mitre_techniques)}")
|
|
333
|
+
lines.append(f"**Duration:** {output.total_duration_ms / 60000:.1f} minutes")
|
|
334
|
+
lines.append("")
|
|
335
|
+
lines.append("---")
|
|
336
|
+
lines.append("")
|
|
337
|
+
|
|
338
|
+
# Skill 1: System Research
|
|
339
|
+
skill = output.system_research
|
|
340
|
+
lines.append("## 1. System Research: How It Works")
|
|
341
|
+
lines.append("")
|
|
342
|
+
lines.append("### Summary")
|
|
343
|
+
lines.append(skill.summary)
|
|
344
|
+
lines.append("")
|
|
345
|
+
lines.append("### Key Findings")
|
|
346
|
+
for finding in skill.key_findings:
|
|
347
|
+
lines.append(f"- {finding}")
|
|
348
|
+
lines.append("")
|
|
349
|
+
if skill.sources:
|
|
350
|
+
lines.append("### Sources")
|
|
351
|
+
for source in skill.sources:
|
|
352
|
+
lines.append(f"- [{source['title']}]({source['url']})")
|
|
353
|
+
lines.append("")
|
|
354
|
+
lines.append("---")
|
|
355
|
+
lines.append("")
|
|
356
|
+
|
|
357
|
+
# Skill 2: Adversary Tradecraft
|
|
358
|
+
skill = output.adversary_tradecraft
|
|
359
|
+
lines.append("## 2. Adversary Tradecraft: Attack Techniques")
|
|
360
|
+
lines.append("")
|
|
361
|
+
lines.append("### Summary")
|
|
362
|
+
lines.append(skill.summary)
|
|
363
|
+
lines.append("")
|
|
364
|
+
lines.append("### Key Findings")
|
|
365
|
+
for finding in skill.key_findings:
|
|
366
|
+
lines.append(f"- {finding}")
|
|
367
|
+
lines.append("")
|
|
368
|
+
if skill.sources:
|
|
369
|
+
lines.append("### Sources")
|
|
370
|
+
for source in skill.sources:
|
|
371
|
+
lines.append(f"- [{source['title']}]({source['url']})")
|
|
372
|
+
lines.append("")
|
|
373
|
+
lines.append("---")
|
|
374
|
+
lines.append("")
|
|
375
|
+
|
|
376
|
+
# Skill 3: Telemetry Mapping
|
|
377
|
+
skill = output.telemetry_mapping
|
|
378
|
+
lines.append("## 3. Telemetry Mapping: OCSF Fields")
|
|
379
|
+
lines.append("")
|
|
380
|
+
lines.append("### Summary")
|
|
381
|
+
lines.append(skill.summary)
|
|
382
|
+
lines.append("")
|
|
383
|
+
lines.append("### Key Fields")
|
|
384
|
+
for finding in skill.key_findings:
|
|
385
|
+
lines.append(f"- {finding}")
|
|
386
|
+
lines.append("")
|
|
387
|
+
lines.append("### Data Source Availability")
|
|
388
|
+
for data_source, available in output.data_source_availability.items():
|
|
389
|
+
status = "Available" if available else "Limited/Unavailable"
|
|
390
|
+
lines.append(f"- {data_source.replace('_', ' ').title()}: {status}")
|
|
391
|
+
lines.append("")
|
|
392
|
+
lines.append("---")
|
|
393
|
+
lines.append("")
|
|
394
|
+
|
|
395
|
+
# Skill 4: Related Work
|
|
396
|
+
skill = output.related_work
|
|
397
|
+
lines.append("## 4. Related Work: Past Hunts")
|
|
398
|
+
lines.append("")
|
|
399
|
+
lines.append("### Summary")
|
|
400
|
+
lines.append(skill.summary)
|
|
401
|
+
lines.append("")
|
|
402
|
+
if skill.key_findings:
|
|
403
|
+
lines.append("### Related Hunts")
|
|
404
|
+
for finding in skill.key_findings:
|
|
405
|
+
lines.append(f"- {finding}")
|
|
406
|
+
lines.append("")
|
|
407
|
+
lines.append("---")
|
|
408
|
+
lines.append("")
|
|
409
|
+
|
|
410
|
+
# Skill 5: Synthesis
|
|
411
|
+
skill = output.synthesis
|
|
412
|
+
lines.append("## 5. Research Synthesis")
|
|
413
|
+
lines.append("")
|
|
414
|
+
lines.append("### Executive Summary")
|
|
415
|
+
lines.append(skill.summary)
|
|
416
|
+
lines.append("")
|
|
417
|
+
if output.recommended_hypothesis:
|
|
418
|
+
lines.append("### Recommended Hypothesis")
|
|
419
|
+
lines.append(f"> {output.recommended_hypothesis}")
|
|
420
|
+
lines.append("")
|
|
421
|
+
if output.gaps_identified:
|
|
422
|
+
lines.append("### Gaps Identified")
|
|
423
|
+
for gap in output.gaps_identified:
|
|
424
|
+
lines.append(f"- {gap}")
|
|
425
|
+
lines.append("")
|
|
426
|
+
lines.append("### Key Findings")
|
|
427
|
+
for finding in skill.key_findings:
|
|
428
|
+
lines.append(f"- {finding}")
|
|
429
|
+
lines.append("")
|
|
430
|
+
lines.append("### Hunt Complexity Assessment")
|
|
431
|
+
lines.append(f"**Estimated Complexity:** {output.estimated_hunt_complexity.title()}")
|
|
432
|
+
lines.append("")
|
|
433
|
+
|
|
434
|
+
# Appendix: Cost Tracking
|
|
435
|
+
lines.append("---")
|
|
436
|
+
lines.append("")
|
|
437
|
+
lines.append("## Appendix: Research Metrics")
|
|
438
|
+
lines.append("")
|
|
439
|
+
lines.append(f"- Web Searches: {output.web_searches_performed}")
|
|
440
|
+
lines.append(f"- LLM Calls: {output.llm_calls}")
|
|
441
|
+
lines.append(f"- Total Cost: ${output.total_cost_usd:.4f}")
|
|
442
|
+
lines.append(f"- Duration: {output.total_duration_ms / 1000:.1f} seconds")
|
|
443
|
+
lines.append("")
|
|
444
|
+
|
|
445
|
+
return "\n".join(lines)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _display_research_summary(output: ResearchOutput, file_path: Path) -> None:
|
|
449
|
+
"""Display research summary in rich format."""
|
|
450
|
+
# Success panel
|
|
451
|
+
console.print()
|
|
452
|
+
console.print(
|
|
453
|
+
Panel(
|
|
454
|
+
f"[bold green]Research Complete: {output.research_id}[/bold green]\n\n"
|
|
455
|
+
f"[bold]Topic:[/bold] {output.topic}\n"
|
|
456
|
+
f"[bold]Duration:[/bold] {output.total_duration_ms / 1000:.1f} seconds\n"
|
|
457
|
+
f"[bold]Cost:[/bold] ${output.total_cost_usd:.4f}\n"
|
|
458
|
+
f"[bold]File:[/bold] {file_path}",
|
|
459
|
+
title="Research Complete",
|
|
460
|
+
border_style="green",
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Summary of findings
|
|
465
|
+
console.print("\n[bold cyan]Key Findings Summary[/bold cyan]")
|
|
466
|
+
|
|
467
|
+
# System Research
|
|
468
|
+
console.print(f"\n[bold]1. System Research:[/bold] {output.system_research.summary[:100]}...")
|
|
469
|
+
|
|
470
|
+
# Adversary Tradecraft
|
|
471
|
+
console.print(f"\n[bold]2. Adversary Tradecraft:[/bold] {output.adversary_tradecraft.summary[:100]}...")
|
|
472
|
+
|
|
473
|
+
# Recommended Hypothesis
|
|
474
|
+
if output.recommended_hypothesis:
|
|
475
|
+
console.print("\n[bold green]Recommended Hypothesis:[/bold green]")
|
|
476
|
+
console.print(f" {output.recommended_hypothesis}")
|
|
477
|
+
|
|
478
|
+
# Gaps
|
|
479
|
+
if output.gaps_identified:
|
|
480
|
+
console.print("\n[bold yellow]Gaps Identified:[/bold yellow]")
|
|
481
|
+
for gap in output.gaps_identified[:3]:
|
|
482
|
+
console.print(f" - {gap}")
|
|
483
|
+
|
|
484
|
+
# Next steps
|
|
485
|
+
console.print("\n[bold]Next Steps:[/bold]")
|
|
486
|
+
console.print(f" 1. Review full research: athf research view {output.research_id}")
|
|
487
|
+
console.print(" 2. Generate hypothesis: athf agent run hypothesis-generator")
|
|
488
|
+
console.print(f" 3. Create hunt: athf hunt new --research {output.research_id}")
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _display_json_output(output: ResearchOutput) -> None:
|
|
492
|
+
"""Display research output as JSON."""
|
|
493
|
+
data = {
|
|
494
|
+
"research_id": output.research_id,
|
|
495
|
+
"topic": output.topic,
|
|
496
|
+
"mitre_techniques": output.mitre_techniques,
|
|
497
|
+
"system_research": {
|
|
498
|
+
"summary": output.system_research.summary,
|
|
499
|
+
"key_findings": output.system_research.key_findings,
|
|
500
|
+
"sources": output.system_research.sources,
|
|
501
|
+
},
|
|
502
|
+
"adversary_tradecraft": {
|
|
503
|
+
"summary": output.adversary_tradecraft.summary,
|
|
504
|
+
"key_findings": output.adversary_tradecraft.key_findings,
|
|
505
|
+
"sources": output.adversary_tradecraft.sources,
|
|
506
|
+
},
|
|
507
|
+
"telemetry_mapping": {
|
|
508
|
+
"summary": output.telemetry_mapping.summary,
|
|
509
|
+
"key_findings": output.telemetry_mapping.key_findings,
|
|
510
|
+
},
|
|
511
|
+
"related_work": {
|
|
512
|
+
"summary": output.related_work.summary,
|
|
513
|
+
"key_findings": output.related_work.key_findings,
|
|
514
|
+
},
|
|
515
|
+
"synthesis": {
|
|
516
|
+
"summary": output.synthesis.summary,
|
|
517
|
+
"key_findings": output.synthesis.key_findings,
|
|
518
|
+
},
|
|
519
|
+
"recommended_hypothesis": output.recommended_hypothesis,
|
|
520
|
+
"data_source_availability": output.data_source_availability,
|
|
521
|
+
"estimated_hunt_complexity": output.estimated_hunt_complexity,
|
|
522
|
+
"gaps_identified": output.gaps_identified,
|
|
523
|
+
"metrics": {
|
|
524
|
+
"duration_ms": output.total_duration_ms,
|
|
525
|
+
"web_searches": output.web_searches_performed,
|
|
526
|
+
"llm_calls": output.llm_calls,
|
|
527
|
+
"cost_usd": output.total_cost_usd,
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
console.print(json.dumps(data, indent=2))
|
athf/commands/similar.py
CHANGED
|
@@ -121,7 +121,7 @@ def _get_hunt_text(hunt_id: str) -> Optional[str]:
|
|
|
121
121
|
hunt_file = Path(f"hunts/{hunt_id}.md")
|
|
122
122
|
if not hunt_file.exists():
|
|
123
123
|
return None
|
|
124
|
-
return hunt_file.read_text(encoding=
|
|
124
|
+
return hunt_file.read_text(encoding="utf-8")
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
def _find_similar_hunts(
|
|
@@ -144,7 +144,7 @@ def _find_similar_hunts(
|
|
|
144
144
|
hunt_files = list(hunts_dir.glob("H-*.md"))
|
|
145
145
|
|
|
146
146
|
if not hunt_files:
|
|
147
|
-
|
|
147
|
+
# Don't print warning - let the output format handle empty results
|
|
148
148
|
return []
|
|
149
149
|
|
|
150
150
|
# Extract hunt content and metadata
|
|
@@ -156,7 +156,7 @@ def _find_similar_hunts(
|
|
|
156
156
|
if exclude_hunt and hunt_id == exclude_hunt:
|
|
157
157
|
continue
|
|
158
158
|
|
|
159
|
-
content = hunt_file.read_text(encoding=
|
|
159
|
+
content = hunt_file.read_text(encoding="utf-8")
|
|
160
160
|
metadata = _extract_hunt_metadata(content)
|
|
161
161
|
|
|
162
162
|
# Extract searchable text (weighted semantic sections)
|
|
@@ -172,7 +172,7 @@ def _find_similar_hunts(
|
|
|
172
172
|
)
|
|
173
173
|
|
|
174
174
|
if not hunt_data:
|
|
175
|
-
|
|
175
|
+
# Don't print warning - let the output format handle empty results
|
|
176
176
|
return []
|
|
177
177
|
|
|
178
178
|
# Build TF-IDF vectors using searchable text (weighted semantic sections)
|
|
@@ -233,7 +233,7 @@ def _extract_hunt_metadata(content: str) -> Dict[str, Any]:
|
|
|
233
233
|
return {}
|
|
234
234
|
|
|
235
235
|
|
|
236
|
-
def _extract_searchable_text(content: str, metadata: Dict[str, Any]) -> str:
|
|
236
|
+
def _extract_searchable_text(content: str, metadata: Dict[str, Any]) -> str: # noqa: C901
|
|
237
237
|
"""Extract semantically important text for similarity matching.
|
|
238
238
|
|
|
239
239
|
Focuses on key sections and applies weighting to improve match accuracy:
|