agentic-threat-hunting-framework 0.1.0__py3-none-any.whl → 0.2.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.1.0.dist-info → agentic_threat_hunting_framework-0.2.0.dist-info}/METADATA +64 -53
- agentic_threat_hunting_framework-0.2.0.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 +373 -0
- athf/commands/hunt.py +92 -15
- athf/commands/investigate.py +744 -0
- athf/commands/similar.py +376 -0
- athf/core/attack_matrix.py +116 -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.0.dist-info}/WHEEL +0 -0
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.0.dist-info}/entry_points.txt +0 -0
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {agentic_threat_hunting_framework-0.1.0.dist-info → agentic_threat_hunting_framework-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-threat-hunting-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Agentic Threat Hunting Framework - Memory and AI for threat hunters
|
|
5
5
|
Author-email: Sydney Marrone <athf@nebulock.io>
|
|
6
6
|
Maintainer-email: Sydney Marrone <athf@nebulock.io>
|
|
@@ -46,30 +46,23 @@ Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
|
|
|
46
46
|
Provides-Extra: docs
|
|
47
47
|
Requires-Dist: mkdocs>=1.5.0; extra == "docs"
|
|
48
48
|
Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
|
|
49
|
+
Provides-Extra: similarity
|
|
50
|
+
Requires-Dist: scikit-learn>=1.0.0; extra == "similarity"
|
|
49
51
|
Dynamic: license-file
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
<img src="assets/athf_logo.png" alt="ATHF Logo" width="400"/>
|
|
53
|
-
</p>
|
|
53
|
+
# Agentic Threat Hunting Framework (ATHF)
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+

|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
[](https://pypi.org/project/agentic-threat-hunting-framework/)
|
|
58
|
+
[](https://pypi.org/project/agentic-threat-hunting-framework/)
|
|
59
|
+
[](https://www.python.org/downloads/)
|
|
60
|
+
[](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/LICENSE)
|
|
61
|
+
[](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/stargazers)
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
<strong><a href="#-quick-start">Quick Start</a></strong> •
|
|
65
|
-
<strong><a href="#installation">Installation</a></strong> •
|
|
66
|
-
<strong><a href="#documentation">Documentation</a></strong> •
|
|
67
|
-
<strong><a href="SHOWCASE.md">Examples</a></strong>
|
|
68
|
-
</p>
|
|
63
|
+
**[Quick Start](#-quick-start)** • **[Installation](#installation)** • **[Documentation](#documentation)** • **[Examples](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/SHOWCASE.md)**
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
<em>Give your threat hunting program memory and agency.</em>
|
|
72
|
-
</p>
|
|
65
|
+
*Give your threat hunting program memory and agency.*
|
|
73
66
|
|
|
74
67
|
The **Agentic Threat Hunting Framework (ATHF)** is the memory and automation layer for your threat hunting program. It gives your hunts structure, persistence, and context - making every past investigation accessible to both humans and AI.
|
|
75
68
|
|
|
@@ -92,13 +85,13 @@ Even AI tools start from zero every time without access to your environment, you
|
|
|
92
85
|
|
|
93
86
|
ATHF changes that by giving your hunts structure, persistence, and context.
|
|
94
87
|
|
|
95
|
-
**Read more:** [docs/why-athf.md](docs/why-athf.md)
|
|
88
|
+
**Read more:** [docs/why-athf.md](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/why-athf.md)
|
|
96
89
|
|
|
97
90
|
## The LOCK Pattern
|
|
98
91
|
|
|
99
92
|
Every threat hunt follows the same basic loop: **Learn → Observe → Check → Keep**.
|
|
100
93
|
|
|
101
|
-

|
|
94
|
+

|
|
102
95
|
|
|
103
96
|
- **Learn:** Gather context from threat intel, alerts, or anomalies
|
|
104
97
|
- **Observe:** Form a hypothesis about adversary behavior
|
|
@@ -107,7 +100,7 @@ Every threat hunt follows the same basic loop: **Learn → Observe → Check →
|
|
|
107
100
|
|
|
108
101
|
**Why LOCK?** It's small enough to use and strict enough for agents to interpret. By capturing every hunt in this format, ATHF makes it possible for AI assistants to recall prior work and suggest refined queries based on past results.
|
|
109
102
|
|
|
110
|
-
**Read more:** [docs/lock-pattern.md](docs/lock-pattern.md)
|
|
103
|
+
**Read more:** [docs/lock-pattern.md](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/lock-pattern.md)
|
|
111
104
|
|
|
112
105
|
## The Five Levels of Agentic Hunting
|
|
113
106
|
|
|
@@ -115,7 +108,7 @@ ATHF defines a simple maturity model. Each level builds on the previous one.
|
|
|
115
108
|
|
|
116
109
|
**Most teams will live at Levels 1–2. Everything beyond that is optional maturity.**
|
|
117
110
|
|
|
118
|
-

|
|
111
|
+

|
|
119
112
|
|
|
120
113
|
| Level | Capability | What You Get |
|
|
121
114
|
|-------|-----------|--------------|
|
|
@@ -130,17 +123,15 @@ ATHF defines a simple maturity model. Each level builds on the previous one.
|
|
|
130
123
|
**Level 3:** 2-4 weeks (optional)
|
|
131
124
|
**Level 4:** 1-3 months (optional)
|
|
132
125
|
|
|
133
|
-
**Read more:** [docs/maturity-model.md](docs/maturity-model.md)
|
|
126
|
+
**Read more:** [docs/maturity-model.md](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/maturity-model.md)
|
|
134
127
|
|
|
135
128
|
## 🚀 Quick Start
|
|
136
129
|
|
|
137
|
-
### Option 1:
|
|
130
|
+
### Option 1: Install from PyPI (Recommended)
|
|
138
131
|
|
|
139
132
|
```bash
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
cd agentic-threat-hunting-framework
|
|
143
|
-
pip install -e .
|
|
133
|
+
# Install ATHF
|
|
134
|
+
pip install agentic-threat-hunting-framework
|
|
144
135
|
|
|
145
136
|
# Initialize your hunt program
|
|
146
137
|
athf init
|
|
@@ -149,7 +140,20 @@ athf init
|
|
|
149
140
|
athf hunt new --technique T1003.001 --title "LSASS Credential Dumping"
|
|
150
141
|
```
|
|
151
142
|
|
|
152
|
-
### Option 2:
|
|
143
|
+
### Option 2: Install from Source (Development)
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Clone and install from source
|
|
147
|
+
git clone https://github.com/Nebulock-Inc/agentic-threat-hunting-framework
|
|
148
|
+
cd agentic-threat-hunting-framework
|
|
149
|
+
pip install -e .
|
|
150
|
+
|
|
151
|
+
# Initialize and start hunting
|
|
152
|
+
athf init
|
|
153
|
+
athf hunt new --technique T1003.001
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Option 3: Pure Markdown (No Installation)
|
|
153
157
|
|
|
154
158
|
```bash
|
|
155
159
|
# Clone the repository
|
|
@@ -165,7 +169,7 @@ cp templates/HUNT_LOCK.md hunts/H-0001.md
|
|
|
165
169
|
|
|
166
170
|
**Choose your AI assistant:** Claude Code, GitHub Copilot, or Cursor - any tool that can read your repository files.
|
|
167
171
|
|
|
168
|
-
**Full guide:** [docs/getting-started.md](docs/getting-started.md)
|
|
172
|
+
**Full guide:** [docs/getting-started.md](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/getting-started.md)
|
|
169
173
|
|
|
170
174
|
## 🔧 CLI Commands
|
|
171
175
|
|
|
@@ -206,32 +210,39 @@ athf hunt stats # Show statistics
|
|
|
206
210
|
athf hunt coverage # MITRE ATT&CK coverage
|
|
207
211
|
```
|
|
208
212
|
|
|
209
|
-
**Full documentation:** [CLI Reference](docs/CLI_REFERENCE.md)
|
|
213
|
+
**Full documentation:** [CLI Reference](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/CLI_REFERENCE.md)
|
|
210
214
|
|
|
211
215
|
## 📺 See It In Action
|
|
212
216
|
|
|
213
|
-

|
|
217
|
+

|
|
214
218
|
|
|
215
219
|
Watch ATHF in action: initialize a workspace, create hunts, and explore your threat hunting catalog in under 60 seconds.
|
|
216
220
|
|
|
217
|
-
**[View example hunts →](SHOWCASE.md)**
|
|
221
|
+
**[View example hunts →](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/SHOWCASE.md)**
|
|
218
222
|
|
|
219
223
|
## Installation
|
|
220
224
|
|
|
221
225
|
### Prerequisites
|
|
222
226
|
- Python 3.8-3.13 (for CLI option)
|
|
223
|
-
- Git
|
|
224
227
|
- Your favorite AI code assistant
|
|
225
228
|
|
|
226
|
-
###
|
|
229
|
+
### From PyPI (Recommended)
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pip install agentic-threat-hunting-framework
|
|
233
|
+
athf init
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### From Source (Development)
|
|
227
237
|
|
|
228
238
|
```bash
|
|
229
239
|
git clone https://github.com/Nebulock-Inc/agentic-threat-hunting-framework
|
|
230
240
|
cd agentic-threat-hunting-framework
|
|
231
241
|
pip install -e .
|
|
242
|
+
athf init
|
|
232
243
|
```
|
|
233
244
|
|
|
234
|
-
### Markdown-Only Setup (No
|
|
245
|
+
### Markdown-Only Setup (No Installation)
|
|
235
246
|
|
|
236
247
|
```bash
|
|
237
248
|
git clone https://github.com/Nebulock-Inc/agentic-threat-hunting-framework
|
|
@@ -244,24 +255,24 @@ Start documenting hunts in the `hunts/` directory using the LOCK pattern.
|
|
|
244
255
|
|
|
245
256
|
### Core Concepts
|
|
246
257
|
|
|
247
|
-
- [Why ATHF Exists](docs/why-athf.md) - The problem and solution
|
|
248
|
-
- [The LOCK Pattern](docs/lock-pattern.md) - Structure for all hunts
|
|
249
|
-
- [Maturity Model](docs/maturity-model.md) - The five levels explained
|
|
250
|
-
- [Getting Started](docs/getting-started.md) - Step-by-step onboarding
|
|
258
|
+
- [Why ATHF Exists](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/why-athf.md) - The problem and solution
|
|
259
|
+
- [The LOCK Pattern](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/lock-pattern.md) - Structure for all hunts
|
|
260
|
+
- [Maturity Model](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/maturity-model.md) - The five levels explained
|
|
261
|
+
- [Getting Started](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/getting-started.md) - Step-by-step onboarding
|
|
251
262
|
|
|
252
263
|
### Level-Specific Guides
|
|
253
264
|
|
|
254
|
-
- [Level 1: Documented Hunts](docs/maturity-model.md#level-1-documented-hunts)
|
|
255
|
-
- [Level 2: Searchable Memory](docs/maturity-model.md#level-2-searchable-memory)
|
|
256
|
-
- [Level 3: Generative Capabilities](docs/level4-agentic-workflows.md)
|
|
257
|
-
- [Level 4: Agentic Workflows](docs/level4-agentic-workflows.md)
|
|
265
|
+
- [Level 1: Documented Hunts](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/maturity-model.md#level-1-documented-hunts)
|
|
266
|
+
- [Level 2: Searchable Memory](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/maturity-model.md#level-2-searchable-memory)
|
|
267
|
+
- [Level 3: Generative Capabilities](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/level4-agentic-workflows.md)
|
|
268
|
+
- [Level 4: Agentic Workflows](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/level4-agentic-workflows.md)
|
|
258
269
|
|
|
259
270
|
### Integration & Customization
|
|
260
271
|
|
|
261
|
-
- [Installation & Development](docs/INSTALL.md) - Setup, fork customization, testing
|
|
262
|
-
- [MCP Catalog](integrations/MCP_CATALOG.md) - Available tool integrations
|
|
263
|
-
- [Quickstart Guides](integrations/quickstart/) - Setup for specific tools
|
|
264
|
-
- [Using ATHF](USING_ATHF.md) - Adoption and customization
|
|
272
|
+
- [Installation & Development](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/INSTALL.md) - Setup, fork customization, testing
|
|
273
|
+
- [MCP Catalog](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/integrations/MCP_CATALOG.md) - Available tool integrations
|
|
274
|
+
- [Quickstart Guides](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/tree/main/integrations/quickstart/) - Setup for specific tools
|
|
275
|
+
- [Using ATHF](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/USING_ATHF.md) - Adoption and customization
|
|
265
276
|
|
|
266
277
|
## 🎖️ Featured Hunts
|
|
267
278
|
|
|
@@ -272,7 +283,7 @@ Detected Atomic Stealer collecting Safari cookies via AppleScript.
|
|
|
272
283
|
|
|
273
284
|
**Key Insight:** Behavior-based detection outperformed signature-based approaches. Process signature validation identified unsigned malware attempting data collection.
|
|
274
285
|
|
|
275
|
-
[View full hunt →](hunts/H-0001.md) | [See more examples →](SHOWCASE.md)
|
|
286
|
+
[View full hunt →](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/hunts/H-0001.md) | [See more examples →](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/SHOWCASE.md)
|
|
276
287
|
|
|
277
288
|
## Why This Matters
|
|
278
289
|
|
|
@@ -290,7 +301,7 @@ When your framework has memory, you stop losing knowledge to turnover or forgott
|
|
|
290
301
|
|
|
291
302
|
- **GitHub Discussions:** [Ask questions, share hunts](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/discussions)
|
|
292
303
|
- **Issues:** [Report bugs or request features](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/issues)
|
|
293
|
-
- **Adoption Guide:** See [USING_ATHF.md](USING_ATHF.md) for how to use ATHF in your organization
|
|
304
|
+
- **Adoption Guide:** See [USING_ATHF.md](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/USING_ATHF.md) for how to use ATHF in your organization
|
|
294
305
|
- **LinkedIn:** [Nebulock Inc.](https://www.linkedin.com/company/nebulock-inc) - Follow for updates
|
|
295
306
|
|
|
296
307
|
## 📖 Using ATHF
|
|
@@ -299,7 +310,7 @@ ATHF is a framework to internalize, not a platform to extend. Fork it, customize
|
|
|
299
310
|
|
|
300
311
|
**Repository:** [https://github.com/Nebulock-Inc/agentic-threat-hunting-framework](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework)
|
|
301
312
|
|
|
302
|
-
See [USING_ATHF.md](USING_ATHF.md) for adoption guidance. Your hunts stay yours—sharing back is optional but appreciated ([Discussions](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/discussions)).
|
|
313
|
+
See [USING_ATHF.md](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/USING_ATHF.md) for adoption guidance. Your hunts stay yours—sharing back is optional but appreciated ([Discussions](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/discussions)).
|
|
303
314
|
|
|
304
315
|
The goal is to help every threat hunting team move from ad-hoc memory to structured, agentic capability.
|
|
305
316
|
|
|
@@ -309,7 +320,7 @@ The goal is to help every threat hunting team move from ad-hoc memory to structu
|
|
|
309
320
|
|
|
310
321
|
ATHF is designed to be forked and customized for your organization.
|
|
311
322
|
|
|
312
|
-
**See [docs/INSTALL.md#development--customization](docs/INSTALL.md#development--customization) for:**
|
|
323
|
+
**See [docs/INSTALL.md#development--customization](https://github.com/Nebulock-Inc/agentic-threat-hunting-framework/blob/main/docs/INSTALL.md#development--customization) for:**
|
|
313
324
|
- Setting up your fork for development
|
|
314
325
|
- Pre-commit hooks for code quality
|
|
315
326
|
- Testing and type checking
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
agentic_threat_hunting_framework-0.2.0.dist-info/licenses/LICENSE,sha256=_KObErRfiKoolznt-DF0nJnr3U9Rdh7Z4Ba7G5qqckk,1071
|
|
2
|
+
athf/__init__.py,sha256=OrjZe8P97_BTEkscapnwSsqKSjwXNP9d8-HtGr19Ni0,241
|
|
3
|
+
athf/__version__.py,sha256=qT3kQNuJrw6IXvX9OpVNABZ8axShk7vcwAcufNBLlls,59
|
|
4
|
+
athf/cli.py,sha256=XLNRXEs9kHPH6utJ7_SnzLFcldbGAnACPMTe0xMOkhQ,4492
|
|
5
|
+
athf/commands/__init__.py,sha256=uDyr0bz-agpGO8fraXQl24wuQCxqbeCevZsJ2bDK29s,25
|
|
6
|
+
athf/commands/context.py,sha256=nWETwEqPMTxxkUdsfVwH-K3Td41_EKQkxutdPbbIwos,11908
|
|
7
|
+
athf/commands/env.py,sha256=Y1UZXn5sStpkRYMJ0ZMjr_ox3ve4ZuhqGGJPBo6Ytko,11828
|
|
8
|
+
athf/commands/hunt.py,sha256=2KORNWAqEvLY-Wc1q-a894g8kOpcqw_iJfnenKJeTDI,23019
|
|
9
|
+
athf/commands/init.py,sha256=L_29fvZF8SZ1BKh2D6NyDuacCC5JXOTezIxdBnnK88E,10941
|
|
10
|
+
athf/commands/investigate.py,sha256=WjwPtafs9bOSu09RC1QW4CVFYJjdn2C96wRa9M_o2PI,24650
|
|
11
|
+
athf/commands/similar.py,sha256=d8AArbknc08qlyGw8kTzF35q9Dk-qBXN4SMP5n0z4-I,11793
|
|
12
|
+
athf/core/__init__.py,sha256=yG7C8ljx3UW4QZoYvDjUxsWHlbS8M-GLGB7Je7rRfqo,31
|
|
13
|
+
athf/core/attack_matrix.py,sha256=Tp-519BLjjov8NAQ84iRvIv7STegLBtF09E5vf7jO9s,2958
|
|
14
|
+
athf/core/hunt_manager.py,sha256=5fxGXbtRGfUR8B0E2jb62peSQhwISmim71SZPRrJRr0,11361
|
|
15
|
+
athf/core/hunt_parser.py,sha256=FUj0yyBIcZnaS9aItMImeBDhegQwpkewIwUMNXW_ZWU,5122
|
|
16
|
+
athf/core/investigation_parser.py,sha256=tZnUqrFGLMUif9rayu7hgb6sKBWIvui46siUdDokAAA,6797
|
|
17
|
+
athf/core/template_engine.py,sha256=vNTVhlxIXZpxU7VmQyrqCSt6ORS0IVjAV54TOmUDMTE,5636
|
|
18
|
+
athf/utils/__init__.py,sha256=aEAPI1xnAsowOtc036cCb9ZOek5nrrfevu8PElhbNgk,30
|
|
19
|
+
agentic_threat_hunting_framework-0.2.0.dist-info/METADATA,sha256=4XD3KtzPLvRcA4a4lfjmRhLAuA5AAkQBF1IdXFM7ZvQ,15472
|
|
20
|
+
agentic_threat_hunting_framework-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
agentic_threat_hunting_framework-0.2.0.dist-info/entry_points.txt,sha256=GopR2iTiBs-yNMWiUZ2DaFIFglXxWJx1XPjTa3ePtfE,39
|
|
22
|
+
agentic_threat_hunting_framework-0.2.0.dist-info/top_level.txt,sha256=Cxxg6SMLfawDJWBITsciRzq27XV8fiaAor23o9Byoes,5
|
|
23
|
+
agentic_threat_hunting_framework-0.2.0.dist-info/RECORD,,
|
athf/__version__.py
CHANGED
athf/cli.py
CHANGED
|
@@ -6,7 +6,7 @@ import click
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
8
|
from athf.__version__ import __version__
|
|
9
|
-
from athf.commands import hunt, init
|
|
9
|
+
from athf.commands import context, env, hunt, init, investigate, similar
|
|
10
10
|
|
|
11
11
|
console = Console()
|
|
12
12
|
|
|
@@ -79,6 +79,12 @@ def cli() -> None:
|
|
|
79
79
|
# Register command groups
|
|
80
80
|
cli.add_command(init.init)
|
|
81
81
|
cli.add_command(hunt.hunt)
|
|
82
|
+
cli.add_command(investigate.investigate)
|
|
83
|
+
|
|
84
|
+
# Phase 1 commands (env, context, similar)
|
|
85
|
+
cli.add_command(env.env)
|
|
86
|
+
cli.add_command(context.context)
|
|
87
|
+
cli.add_command(similar.similar)
|
|
82
88
|
|
|
83
89
|
|
|
84
90
|
@cli.command(hidden=True)
|
athf/commands/context.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Context export command for AI-optimized context loading."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import yaml
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
CONTEXT_EPILOG = """
|
|
14
|
+
\b
|
|
15
|
+
Examples:
|
|
16
|
+
# Export context for specific hunt
|
|
17
|
+
athf context --hunt H-0013
|
|
18
|
+
|
|
19
|
+
# Export context for all credential access hunts
|
|
20
|
+
athf context --tactic credential-access
|
|
21
|
+
|
|
22
|
+
# Export context for macOS platform hunts
|
|
23
|
+
athf context --platform macos
|
|
24
|
+
|
|
25
|
+
# Export full repository context (large output)
|
|
26
|
+
athf context --full
|
|
27
|
+
|
|
28
|
+
# Export as JSON (default)
|
|
29
|
+
athf context --hunt H-0013 --format json
|
|
30
|
+
|
|
31
|
+
# Export as markdown
|
|
32
|
+
athf context --hunt H-0013 --format markdown
|
|
33
|
+
|
|
34
|
+
\b
|
|
35
|
+
Why This Helps AI:
|
|
36
|
+
• Single tool call instead of 5+ Read operations
|
|
37
|
+
• Pre-filtered, relevant content only
|
|
38
|
+
• Structured format (easier to parse)
|
|
39
|
+
• Token optimization (strips unnecessary formatting)
|
|
40
|
+
• Saves ~2,000 tokens per hunt
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@click.command(epilog=CONTEXT_EPILOG)
|
|
45
|
+
@click.option("--hunt", help="Hunt ID to export context for (e.g., H-0013)")
|
|
46
|
+
@click.option(
|
|
47
|
+
"--tactic",
|
|
48
|
+
help="MITRE tactic to filter hunts (e.g., credential-access)",
|
|
49
|
+
)
|
|
50
|
+
@click.option("--platform", help="Platform to filter hunts (e.g., macos, windows, linux)")
|
|
51
|
+
@click.option("--full", is_flag=True, help="Export full repository context (use sparingly)")
|
|
52
|
+
@click.option(
|
|
53
|
+
"--format",
|
|
54
|
+
"output_format",
|
|
55
|
+
type=click.Choice(["json", "markdown", "yaml"]),
|
|
56
|
+
default="json",
|
|
57
|
+
help="Output format (default: json)",
|
|
58
|
+
)
|
|
59
|
+
@click.option("--output", type=click.Path(), help="Output file path (default: stdout)")
|
|
60
|
+
def context(
|
|
61
|
+
hunt: Optional[str],
|
|
62
|
+
tactic: Optional[str],
|
|
63
|
+
platform: Optional[str],
|
|
64
|
+
full: bool,
|
|
65
|
+
output_format: str,
|
|
66
|
+
output: Optional[str],
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Export AI-optimized context bundle.
|
|
69
|
+
|
|
70
|
+
Combines relevant files into single structured output:
|
|
71
|
+
- environment.md (tech stack, data sources)
|
|
72
|
+
- hunts/INDEX.md (hunt metadata index)
|
|
73
|
+
- Hunt files (filtered by hunt ID, tactic, or platform)
|
|
74
|
+
- Domain knowledge (if relevant)
|
|
75
|
+
|
|
76
|
+
\b
|
|
77
|
+
Use Cases:
|
|
78
|
+
• AI assistants: Reduce context-loading from ~5 tool calls to 1
|
|
79
|
+
• Token optimization: Pre-filtered, structured content only
|
|
80
|
+
• Hunt planning: Get all relevant context in one shot
|
|
81
|
+
• Query generation: Include past hunt lessons and data sources
|
|
82
|
+
|
|
83
|
+
\b
|
|
84
|
+
Token Savings:
|
|
85
|
+
• Without context: ~5 Read operations, ~3,000 tokens
|
|
86
|
+
• With context: 1 command, ~1,000 tokens
|
|
87
|
+
• Savings: ~2,000 tokens per hunt (~$0.03 per hunt)
|
|
88
|
+
"""
|
|
89
|
+
# Validate mutually exclusive options
|
|
90
|
+
exclusive_options = sum([bool(hunt), bool(tactic), bool(platform), full])
|
|
91
|
+
if exclusive_options == 0:
|
|
92
|
+
console.print("[red]Error: Must specify one of: --hunt, --tactic, --platform, or --full[/red]")
|
|
93
|
+
console.print("\n[dim]Examples:[/dim]")
|
|
94
|
+
console.print(" athf context --hunt H-0013")
|
|
95
|
+
console.print(" athf context --tactic credential-access")
|
|
96
|
+
console.print(" athf context --platform macos")
|
|
97
|
+
raise click.Abort()
|
|
98
|
+
|
|
99
|
+
if exclusive_options > 1:
|
|
100
|
+
console.print("[red]Error: Only one filter option allowed at a time[/red]")
|
|
101
|
+
raise click.Abort()
|
|
102
|
+
|
|
103
|
+
# Build context bundle
|
|
104
|
+
context_data = _build_context(hunt=hunt, tactic=tactic, platform=platform, full=full)
|
|
105
|
+
|
|
106
|
+
# Format output
|
|
107
|
+
if output_format == "json":
|
|
108
|
+
# Use ensure_ascii=True to force proper escaping of all special characters
|
|
109
|
+
# This fixes issues with unescaped control characters and newlines
|
|
110
|
+
formatted_output = json.dumps(context_data, indent=2, ensure_ascii=True)
|
|
111
|
+
elif output_format == "yaml":
|
|
112
|
+
formatted_output = yaml.dump(context_data, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
113
|
+
else: # markdown
|
|
114
|
+
formatted_output = _format_as_markdown(context_data)
|
|
115
|
+
|
|
116
|
+
# Write to file or stdout
|
|
117
|
+
if output:
|
|
118
|
+
Path(output).write_text(formatted_output, encoding='utf-8')
|
|
119
|
+
console.print(f"[green]✅ Context exported to: {output}[/green]")
|
|
120
|
+
else:
|
|
121
|
+
# Use plain print() for JSON/YAML to avoid Rich formatting issues
|
|
122
|
+
if output_format in ("json", "yaml"):
|
|
123
|
+
print(formatted_output)
|
|
124
|
+
else:
|
|
125
|
+
console.print(formatted_output)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _build_context(
|
|
129
|
+
hunt: Optional[str] = None,
|
|
130
|
+
tactic: Optional[str] = None,
|
|
131
|
+
platform: Optional[str] = None,
|
|
132
|
+
full: bool = False,
|
|
133
|
+
) -> Dict[str, Any]:
|
|
134
|
+
"""Build context bundle based on filters."""
|
|
135
|
+
context: Dict[str, Any] = {
|
|
136
|
+
"metadata": {
|
|
137
|
+
"generated_by": "athf context",
|
|
138
|
+
"filters": {
|
|
139
|
+
"hunt": hunt,
|
|
140
|
+
"tactic": tactic,
|
|
141
|
+
"platform": platform,
|
|
142
|
+
"full": full,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
"environment": None,
|
|
146
|
+
"hunt_index": None,
|
|
147
|
+
"hunts": [],
|
|
148
|
+
"domain_knowledge": [],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Always include environment.md
|
|
152
|
+
env_path = Path("environment.md")
|
|
153
|
+
if env_path.exists():
|
|
154
|
+
context["environment"] = _read_and_optimize(env_path)
|
|
155
|
+
|
|
156
|
+
# Always include hunts/INDEX.md
|
|
157
|
+
index_path = Path("hunts/INDEX.md")
|
|
158
|
+
if index_path.exists():
|
|
159
|
+
context["hunt_index"] = _read_and_optimize(index_path)
|
|
160
|
+
|
|
161
|
+
# Load hunts based on filter
|
|
162
|
+
if hunt:
|
|
163
|
+
hunt_files = [Path(f"hunts/{hunt}.md")]
|
|
164
|
+
elif tactic:
|
|
165
|
+
hunt_files = _find_hunts_by_tactic(tactic)
|
|
166
|
+
elif platform:
|
|
167
|
+
hunt_files = _find_hunts_by_platform(platform)
|
|
168
|
+
elif full:
|
|
169
|
+
hunt_files = list(Path("hunts").glob("H-*.md"))
|
|
170
|
+
else:
|
|
171
|
+
hunt_files = []
|
|
172
|
+
|
|
173
|
+
# Load hunt content
|
|
174
|
+
for hunt_file in hunt_files:
|
|
175
|
+
if hunt_file.exists():
|
|
176
|
+
context["hunts"].append(
|
|
177
|
+
{
|
|
178
|
+
"hunt_id": hunt_file.stem,
|
|
179
|
+
"content": _read_and_optimize(hunt_file),
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Load relevant domain knowledge
|
|
184
|
+
if tactic or full:
|
|
185
|
+
domain_files = _get_relevant_domain_files(tactic)
|
|
186
|
+
for domain_file in domain_files:
|
|
187
|
+
if domain_file.exists():
|
|
188
|
+
context["domain_knowledge"].append(
|
|
189
|
+
{
|
|
190
|
+
"file": str(domain_file),
|
|
191
|
+
"content": _read_and_optimize(domain_file),
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return context
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _read_and_optimize(file_path: Path) -> str:
|
|
199
|
+
"""Read file and optimize for token efficiency."""
|
|
200
|
+
content = file_path.read_text()
|
|
201
|
+
|
|
202
|
+
# First pass: Remove all control characters except tabs and newlines
|
|
203
|
+
# Control characters are U+0000 through U+001F (0-31), except tab (9), LF (10), CR (13)
|
|
204
|
+
cleaned_content = "".join(
|
|
205
|
+
char for char in content
|
|
206
|
+
if ord(char) >= 32 or char in "\t\n\r"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Token optimization:
|
|
210
|
+
# 1. Strip excessive whitespace (but preserve single newlines)
|
|
211
|
+
lines = cleaned_content.split("\n")
|
|
212
|
+
optimized_lines = []
|
|
213
|
+
prev_empty = False
|
|
214
|
+
|
|
215
|
+
for line in lines:
|
|
216
|
+
stripped = line.strip()
|
|
217
|
+
if not stripped:
|
|
218
|
+
if not prev_empty:
|
|
219
|
+
optimized_lines.append("")
|
|
220
|
+
prev_empty = True
|
|
221
|
+
else:
|
|
222
|
+
optimized_lines.append(line.rstrip())
|
|
223
|
+
prev_empty = False
|
|
224
|
+
|
|
225
|
+
return "\n".join(optimized_lines)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _find_hunts_by_tactic(tactic: str) -> List[Path]:
|
|
229
|
+
"""Find hunt files matching MITRE tactic."""
|
|
230
|
+
hunts_dir = Path("hunts")
|
|
231
|
+
matching_hunts = []
|
|
232
|
+
|
|
233
|
+
# Normalize tactic name (e.g., "credential-access" -> "credential access")
|
|
234
|
+
normalized_tactic = tactic.replace("-", " ").lower()
|
|
235
|
+
|
|
236
|
+
for hunt_file in hunts_dir.glob("H-*.md"):
|
|
237
|
+
content = hunt_file.read_text()
|
|
238
|
+
|
|
239
|
+
# Check YAML frontmatter for tactics field
|
|
240
|
+
if content.startswith("---"):
|
|
241
|
+
try:
|
|
242
|
+
# Extract YAML frontmatter
|
|
243
|
+
yaml_end = content.find("---", 3)
|
|
244
|
+
if yaml_end > 0:
|
|
245
|
+
frontmatter = content[3:yaml_end]
|
|
246
|
+
metadata = yaml.safe_load(frontmatter)
|
|
247
|
+
|
|
248
|
+
if metadata and "tactics" in metadata:
|
|
249
|
+
hunt_tactics = [t.lower().replace("-", " ") for t in metadata["tactics"]]
|
|
250
|
+
if normalized_tactic in hunt_tactics:
|
|
251
|
+
matching_hunts.append(hunt_file)
|
|
252
|
+
except yaml.YAMLError:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
return matching_hunts
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _find_hunts_by_platform(platform: str) -> List[Path]:
|
|
259
|
+
"""Find hunt files matching platform."""
|
|
260
|
+
hunts_dir = Path("hunts")
|
|
261
|
+
matching_hunts = []
|
|
262
|
+
|
|
263
|
+
normalized_platform = platform.lower()
|
|
264
|
+
|
|
265
|
+
for hunt_file in hunts_dir.glob("H-*.md"):
|
|
266
|
+
content = hunt_file.read_text()
|
|
267
|
+
|
|
268
|
+
# Check YAML frontmatter for platform field
|
|
269
|
+
if content.startswith("---"):
|
|
270
|
+
try:
|
|
271
|
+
yaml_end = content.find("---", 3)
|
|
272
|
+
if yaml_end > 0:
|
|
273
|
+
frontmatter = content[3:yaml_end]
|
|
274
|
+
metadata = yaml.safe_load(frontmatter)
|
|
275
|
+
|
|
276
|
+
if metadata and "platform" in metadata:
|
|
277
|
+
hunt_platforms = [p.lower() for p in metadata["platform"]]
|
|
278
|
+
if normalized_platform in hunt_platforms:
|
|
279
|
+
matching_hunts.append(hunt_file)
|
|
280
|
+
except yaml.YAMLError:
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
return matching_hunts
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _get_relevant_domain_files(tactic: Optional[str] = None) -> List[Path]:
|
|
287
|
+
"""Get relevant domain knowledge files based on tactic."""
|
|
288
|
+
domain_files = []
|
|
289
|
+
|
|
290
|
+
# Always include core hunting knowledge
|
|
291
|
+
domain_files.append(Path("knowledge/hunting-knowledge.md"))
|
|
292
|
+
|
|
293
|
+
# Add tactic-specific domain files
|
|
294
|
+
if tactic:
|
|
295
|
+
tactic_lower = tactic.lower().replace("-", " ")
|
|
296
|
+
|
|
297
|
+
# Map tactics to domain files
|
|
298
|
+
tactic_domain_map = {
|
|
299
|
+
"credential access": [Path("knowledge/domains/iam-security.md")],
|
|
300
|
+
"persistence": [Path("knowledge/domains/endpoint-security.md")],
|
|
301
|
+
"privilege escalation": [Path("knowledge/domains/endpoint-security.md")],
|
|
302
|
+
"defense evasion": [Path("knowledge/domains/endpoint-security.md")],
|
|
303
|
+
"execution": [Path("knowledge/domains/endpoint-security.md")],
|
|
304
|
+
"initial access": [
|
|
305
|
+
Path("knowledge/domains/endpoint-security.md"),
|
|
306
|
+
Path("knowledge/domains/iam-security.md"),
|
|
307
|
+
],
|
|
308
|
+
"collection": [Path("knowledge/domains/insider-threat.md")],
|
|
309
|
+
"exfiltration": [Path("knowledge/domains/insider-threat.md")],
|
|
310
|
+
"impact": [Path("knowledge/domains/insider-threat.md")],
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if tactic_lower in tactic_domain_map:
|
|
314
|
+
domain_files.extend(tactic_domain_map[tactic_lower])
|
|
315
|
+
|
|
316
|
+
return list(set(domain_files)) # Remove duplicates
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _format_as_markdown(context_data: Dict[str, Any]) -> str:
|
|
320
|
+
"""Format context data as markdown."""
|
|
321
|
+
md = "# ATHF Context Export\n\n"
|
|
322
|
+
|
|
323
|
+
# Metadata
|
|
324
|
+
filters = context_data["metadata"]["filters"]
|
|
325
|
+
active_filters = [f"{k}={v}" for k, v in filters.items() if v]
|
|
326
|
+
md += f"**Filters:** {', '.join(active_filters)}\n\n"
|
|
327
|
+
|
|
328
|
+
md += "---\n\n"
|
|
329
|
+
|
|
330
|
+
# Environment
|
|
331
|
+
if context_data.get("environment"):
|
|
332
|
+
md += "## Environment\n\n"
|
|
333
|
+
md += context_data["environment"]
|
|
334
|
+
md += "\n\n---\n\n"
|
|
335
|
+
|
|
336
|
+
# Hunt Index
|
|
337
|
+
if context_data.get("hunt_index"):
|
|
338
|
+
md += "## Hunt Index\n\n"
|
|
339
|
+
md += context_data["hunt_index"]
|
|
340
|
+
md += "\n\n---\n\n"
|
|
341
|
+
|
|
342
|
+
# Hunts
|
|
343
|
+
if context_data.get("hunts"):
|
|
344
|
+
md += "## Hunts\n\n"
|
|
345
|
+
for hunt in context_data["hunts"]:
|
|
346
|
+
md += f"### {hunt['hunt_id']}\n\n"
|
|
347
|
+
md += hunt["content"]
|
|
348
|
+
md += "\n\n---\n\n"
|
|
349
|
+
|
|
350
|
+
# Domain Knowledge
|
|
351
|
+
if context_data.get("domain_knowledge"):
|
|
352
|
+
md += "## Domain Knowledge\n\n"
|
|
353
|
+
for domain in context_data["domain_knowledge"]:
|
|
354
|
+
md += f"### {domain['file']}\n\n"
|
|
355
|
+
md += domain["content"]
|
|
356
|
+
md += "\n\n---\n\n"
|
|
357
|
+
|
|
358
|
+
return md
|