agentic-threat-hunting-framework 0.2.1__py3-none-any.whl → 0.2.3__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.1.dist-info → agentic_threat_hunting_framework-0.2.3.dist-info}/METADATA +1 -1
- agentic_threat_hunting_framework-0.2.3.dist-info/RECORD +23 -0
- athf/__version__.py +1 -1
- athf/commands/context.py +32 -18
- athf/commands/investigate.py +8 -8
- athf/commands/similar.py +2 -2
- athf/core/attack_matrix.py +2 -2
- athf/core/hunt_manager.py +9 -2
- athf/core/investigation_parser.py +3 -3
- agentic_threat_hunting_framework-0.2.1.dist-info/RECORD +0 -23
- {agentic_threat_hunting_framework-0.2.1.dist-info → agentic_threat_hunting_framework-0.2.3.dist-info}/WHEEL +0 -0
- {agentic_threat_hunting_framework-0.2.1.dist-info → agentic_threat_hunting_framework-0.2.3.dist-info}/entry_points.txt +0 -0
- {agentic_threat_hunting_framework-0.2.1.dist-info → agentic_threat_hunting_framework-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {agentic_threat_hunting_framework-0.2.1.dist-info → agentic_threat_hunting_framework-0.2.3.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.2.
|
|
3
|
+
Version: 0.2.3
|
|
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>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
agentic_threat_hunting_framework-0.2.3.dist-info/licenses/LICENSE,sha256=_KObErRfiKoolznt-DF0nJnr3U9Rdh7Z4Ba7G5qqckk,1071
|
|
2
|
+
athf/__init__.py,sha256=OrjZe8P97_BTEkscapnwSsqKSjwXNP9d8-HtGr19Ni0,241
|
|
3
|
+
athf/__version__.py,sha256=p9cAuZ-dTEMpo-qoeYkFo2166r8LvKpa5qHBZihGq3w,59
|
|
4
|
+
athf/cli.py,sha256=XLNRXEs9kHPH6utJ7_SnzLFcldbGAnACPMTe0xMOkhQ,4492
|
|
5
|
+
athf/commands/__init__.py,sha256=uDyr0bz-agpGO8fraXQl24wuQCxqbeCevZsJ2bDK29s,25
|
|
6
|
+
athf/commands/context.py,sha256=WvOf0OuttAsEk_h4QDtdfqYI4CulDg2UCtq_5r5iJAA,12686
|
|
7
|
+
athf/commands/env.py,sha256=AisRllJXbyCjK_2ii21qBBmCz9raxhBUemwM7BxqIYg,11859
|
|
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=mK_id5vjfN_ukqB_-fyia0FNa0pBmtn0Xv6CKHQI1Qo,24663
|
|
11
|
+
athf/commands/similar.py,sha256=ROoMs4NP1otCaXwM1XzpLWxmANknoeASlBT7zuMDqas,11793
|
|
12
|
+
athf/core/__init__.py,sha256=yG7C8ljx3UW4QZoYvDjUxsWHlbS8M-GLGB7Je7rRfqo,31
|
|
13
|
+
athf/core/attack_matrix.py,sha256=QZKKmxckQ6-U7lqVdGUJoj2jEAhP3Juvr3sqaNx2oTw,3238
|
|
14
|
+
athf/core/hunt_manager.py,sha256=PFsg8Ecg94NCpuFZpApo82lyORkgK5IfOIih-7-XsmM,11580
|
|
15
|
+
athf/core/hunt_parser.py,sha256=FUj0yyBIcZnaS9aItMImeBDhegQwpkewIwUMNXW_ZWU,5122
|
|
16
|
+
athf/core/investigation_parser.py,sha256=wbfjnq4gFgIc0a4bHIAnidVNPhbHDpIXWY1SGLk0Xls,6804
|
|
17
|
+
athf/core/template_engine.py,sha256=vNTVhlxIXZpxU7VmQyrqCSt6ORS0IVjAV54TOmUDMTE,5636
|
|
18
|
+
athf/utils/__init__.py,sha256=aEAPI1xnAsowOtc036cCb9ZOek5nrrfevu8PElhbNgk,30
|
|
19
|
+
agentic_threat_hunting_framework-0.2.3.dist-info/METADATA,sha256=I3x8s2Rff1A7BjYz-lfy_M6I_qw0-nDBC2Ypc0DcxTA,15472
|
|
20
|
+
agentic_threat_hunting_framework-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
agentic_threat_hunting_framework-0.2.3.dist-info/entry_points.txt,sha256=GopR2iTiBs-yNMWiUZ2DaFIFglXxWJx1XPjTa3ePtfE,39
|
|
22
|
+
agentic_threat_hunting_framework-0.2.3.dist-info/top_level.txt,sha256=Cxxg6SMLfawDJWBITsciRzq27XV8fiaAor23o9Byoes,5
|
|
23
|
+
agentic_threat_hunting_framework-0.2.3.dist-info/RECORD,,
|
athf/__version__.py
CHANGED
athf/commands/context.py
CHANGED
|
@@ -22,6 +22,9 @@ Examples:
|
|
|
22
22
|
# Export context for macOS platform hunts
|
|
23
23
|
athf context --platform macos
|
|
24
24
|
|
|
25
|
+
# Combine filters: persistence hunts on Linux
|
|
26
|
+
athf context --tactic persistence --platform linux
|
|
27
|
+
|
|
25
28
|
# Export full repository context (large output)
|
|
26
29
|
athf context --full
|
|
27
30
|
|
|
@@ -86,18 +89,20 @@ def context(
|
|
|
86
89
|
• With context: 1 command, ~1,000 tokens
|
|
87
90
|
• Savings: ~2,000 tokens per hunt (~$0.03 per hunt)
|
|
88
91
|
"""
|
|
89
|
-
# Validate
|
|
90
|
-
|
|
91
|
-
if
|
|
92
|
-
console.print("[red]Error: Must specify one of: --hunt, --tactic, --platform, or --full[/red]")
|
|
92
|
+
# Validate that at least one filter is provided
|
|
93
|
+
has_filter = any([hunt, tactic, platform, full])
|
|
94
|
+
if not has_filter:
|
|
95
|
+
console.print("[red]Error: Must specify at least one of: --hunt, --tactic, --platform, or --full[/red]")
|
|
93
96
|
console.print("\n[dim]Examples:[/dim]")
|
|
94
97
|
console.print(" athf context --hunt H-0013")
|
|
95
98
|
console.print(" athf context --tactic credential-access")
|
|
96
99
|
console.print(" athf context --platform macos")
|
|
100
|
+
console.print(" athf context --tactic persistence --platform linux")
|
|
97
101
|
raise click.Abort()
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
# --full flag is mutually exclusive with other filters
|
|
104
|
+
if full and (hunt or tactic or platform):
|
|
105
|
+
console.print("[red]Error: --full cannot be combined with other filters[/red]")
|
|
101
106
|
raise click.Abort()
|
|
102
107
|
|
|
103
108
|
# Build context bundle
|
|
@@ -158,17 +163,26 @@ def _build_context(
|
|
|
158
163
|
if index_path.exists():
|
|
159
164
|
context["hunt_index"] = _read_and_optimize(index_path)
|
|
160
165
|
|
|
161
|
-
# Load hunts based on
|
|
162
|
-
if
|
|
163
|
-
|
|
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:
|
|
166
|
+
# Load hunts based on filters (can be combined)
|
|
167
|
+
if full:
|
|
168
|
+
# Full export: include all hunts
|
|
169
169
|
hunt_files = list(Path("hunts").glob("H-*.md"))
|
|
170
|
+
elif hunt:
|
|
171
|
+
# Specific hunt: only load that one
|
|
172
|
+
hunt_files = [Path(f"hunts/{hunt}.md")]
|
|
170
173
|
else:
|
|
171
|
-
|
|
174
|
+
# Combine tactic and platform filters
|
|
175
|
+
if tactic and platform:
|
|
176
|
+
# Both filters: find hunts matching both criteria
|
|
177
|
+
tactic_hunts = set(_find_hunts_by_tactic(tactic))
|
|
178
|
+
platform_hunts = set(_find_hunts_by_platform(platform))
|
|
179
|
+
hunt_files = list(tactic_hunts & platform_hunts) # Intersection
|
|
180
|
+
elif tactic:
|
|
181
|
+
hunt_files = _find_hunts_by_tactic(tactic)
|
|
182
|
+
elif platform:
|
|
183
|
+
hunt_files = _find_hunts_by_platform(platform)
|
|
184
|
+
else:
|
|
185
|
+
hunt_files = []
|
|
172
186
|
|
|
173
187
|
# Load hunt content
|
|
174
188
|
for hunt_file in hunt_files:
|
|
@@ -197,7 +211,7 @@ def _build_context(
|
|
|
197
211
|
|
|
198
212
|
def _read_and_optimize(file_path: Path) -> str:
|
|
199
213
|
"""Read file and optimize for token efficiency."""
|
|
200
|
-
content = file_path.read_text()
|
|
214
|
+
content = file_path.read_text(encoding='utf-8')
|
|
201
215
|
|
|
202
216
|
# First pass: Remove all control characters except tabs and newlines
|
|
203
217
|
# Control characters are U+0000 through U+001F (0-31), except tab (9), LF (10), CR (13)
|
|
@@ -234,7 +248,7 @@ def _find_hunts_by_tactic(tactic: str) -> List[Path]:
|
|
|
234
248
|
normalized_tactic = tactic.replace("-", " ").lower()
|
|
235
249
|
|
|
236
250
|
for hunt_file in hunts_dir.glob("H-*.md"):
|
|
237
|
-
content = hunt_file.read_text()
|
|
251
|
+
content = hunt_file.read_text(encoding='utf-8')
|
|
238
252
|
|
|
239
253
|
# Check YAML frontmatter for tactics field
|
|
240
254
|
if content.startswith("---"):
|
|
@@ -263,7 +277,7 @@ def _find_hunts_by_platform(platform: str) -> List[Path]:
|
|
|
263
277
|
normalized_platform = platform.lower()
|
|
264
278
|
|
|
265
279
|
for hunt_file in hunts_dir.glob("H-*.md"):
|
|
266
|
-
content = hunt_file.read_text()
|
|
280
|
+
content = hunt_file.read_text(encoding='utf-8')
|
|
267
281
|
|
|
268
282
|
# Check YAML frontmatter for platform field
|
|
269
283
|
if content.startswith("---"):
|
athf/commands/investigate.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import List, Optional, Tuple
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
import yaml
|
|
@@ -82,8 +82,8 @@ def new(
|
|
|
82
82
|
title: Optional[str],
|
|
83
83
|
investigation_type: Optional[str],
|
|
84
84
|
tags: Optional[str],
|
|
85
|
-
data_source:
|
|
86
|
-
related_hunt:
|
|
85
|
+
data_source: Tuple[str, ...],
|
|
86
|
+
related_hunt: Tuple[str, ...],
|
|
87
87
|
investigator: Optional[str],
|
|
88
88
|
non_interactive: bool,
|
|
89
89
|
) -> None:
|
|
@@ -200,9 +200,9 @@ def _render_investigation_template(
|
|
|
200
200
|
title: str,
|
|
201
201
|
investigator: str,
|
|
202
202
|
investigation_type: str,
|
|
203
|
-
tags:
|
|
204
|
-
data_sources:
|
|
205
|
-
related_hunts:
|
|
203
|
+
tags: List[str],
|
|
204
|
+
data_sources: List[str],
|
|
205
|
+
related_hunts: List[str],
|
|
206
206
|
) -> str:
|
|
207
207
|
"""Render investigation template with provided values.
|
|
208
208
|
|
|
@@ -569,8 +569,8 @@ def validate(investigation_id: str) -> None:
|
|
|
569
569
|
def promote(
|
|
570
570
|
investigation_id: str,
|
|
571
571
|
technique: Optional[str],
|
|
572
|
-
tactic:
|
|
573
|
-
platform:
|
|
572
|
+
tactic: Tuple[str, ...],
|
|
573
|
+
platform: Tuple[str, ...],
|
|
574
574
|
status: str,
|
|
575
575
|
non_interactive: bool,
|
|
576
576
|
) -> None:
|
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()
|
|
124
|
+
return hunt_file.read_text(encoding='utf-8')
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
def _find_similar_hunts(
|
|
@@ -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()
|
|
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)
|
athf/core/attack_matrix.py
CHANGED
|
@@ -4,7 +4,7 @@ This module contains reference data for the MITRE ATT&CK Enterprise matrix,
|
|
|
4
4
|
including tactic ordering and technique counts.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Dict, TypedDict
|
|
7
|
+
from typing import Dict, List, TypedDict
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class TacticInfo(TypedDict):
|
|
@@ -122,7 +122,7 @@ def get_tactic_technique_count(tactic_key: str) -> int:
|
|
|
122
122
|
return 0
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
def get_sorted_tactics() ->
|
|
125
|
+
def get_sorted_tactics() -> List[str]:
|
|
126
126
|
"""Get all tactic keys sorted by ATT&CK matrix order.
|
|
127
127
|
|
|
128
128
|
Returns:
|
athf/core/hunt_manager.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
|
5
|
+
from typing import Any, Dict, List, Optional, Set
|
|
6
6
|
|
|
7
7
|
from athf.core.attack_matrix import ATTACK_TACTICS, TOTAL_TECHNIQUES, get_sorted_tactics
|
|
8
8
|
from athf.core.hunt_parser import parse_hunt_file
|
|
@@ -154,7 +154,14 @@ class HuntManager:
|
|
|
154
154
|
results = []
|
|
155
155
|
query_lower = query.lower()
|
|
156
156
|
|
|
157
|
+
# Exclude documentation files
|
|
158
|
+
exclude_files = {"README.md", "FORMAT_GUIDELINES.md"}
|
|
159
|
+
|
|
157
160
|
for hunt_file in self.hunts_dir.glob("*.md"):
|
|
161
|
+
# Skip documentation files
|
|
162
|
+
if hunt_file.name in exclude_files:
|
|
163
|
+
continue
|
|
164
|
+
|
|
158
165
|
try:
|
|
159
166
|
with open(hunt_file, "r", encoding="utf-8") as f:
|
|
160
167
|
content = f.read()
|
|
@@ -261,7 +268,7 @@ class HuntManager:
|
|
|
261
268
|
"total_techniques": ATTACK_TACTICS[tactic_key]["technique_count"],
|
|
262
269
|
}
|
|
263
270
|
|
|
264
|
-
all_unique_techniques:
|
|
271
|
+
all_unique_techniques: Set[str] = set()
|
|
265
272
|
|
|
266
273
|
for hunt in hunts:
|
|
267
274
|
hunt_id = hunt.get("hunt_id", "UNKNOWN")
|
|
@@ -8,7 +8,7 @@ Investigation parser is simpler than hunt parser:
|
|
|
8
8
|
|
|
9
9
|
import re
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Dict, List
|
|
11
|
+
from typing import Any, Dict, List, Tuple
|
|
12
12
|
|
|
13
13
|
import yaml
|
|
14
14
|
|
|
@@ -85,7 +85,7 @@ class InvestigationParser:
|
|
|
85
85
|
|
|
86
86
|
return content_without_fm.strip()
|
|
87
87
|
|
|
88
|
-
def validate(self) ->
|
|
88
|
+
def validate(self) -> Tuple[bool, List[str]]:
|
|
89
89
|
"""Validate investigation structure.
|
|
90
90
|
|
|
91
91
|
Lightweight validation - only checks minimal required fields.
|
|
@@ -140,7 +140,7 @@ def parse_investigation_file(file_path: Path) -> Dict[str, Any]:
|
|
|
140
140
|
return parser.parse()
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
def validate_investigation_file(file_path: Path) ->
|
|
143
|
+
def validate_investigation_file(file_path: Path) -> Tuple[bool, List[str]]:
|
|
144
144
|
"""Convenience function to validate an investigation file.
|
|
145
145
|
|
|
146
146
|
Args:
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
agentic_threat_hunting_framework-0.2.1.dist-info/licenses/LICENSE,sha256=_KObErRfiKoolznt-DF0nJnr3U9Rdh7Z4Ba7G5qqckk,1071
|
|
2
|
-
athf/__init__.py,sha256=OrjZe8P97_BTEkscapnwSsqKSjwXNP9d8-HtGr19Ni0,241
|
|
3
|
-
athf/__version__.py,sha256=JMSRfysx6Kj8rOPSF3-d9x_3-QAfDP47yKOeqdgkH1M,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=AisRllJXbyCjK_2ii21qBBmCz9raxhBUemwM7BxqIYg,11859
|
|
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=sgCNwI1Ru0lbavNm_3fjOdcJjeRPVD827vGdMF2LMBM,11761
|
|
12
|
-
athf/core/__init__.py,sha256=yG7C8ljx3UW4QZoYvDjUxsWHlbS8M-GLGB7Je7rRfqo,31
|
|
13
|
-
athf/core/attack_matrix.py,sha256=ayr9StkOskgFTsNum8MwTuCd4Z7Rhy5NNCNx1ncLYKs,3232
|
|
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.1.dist-info/METADATA,sha256=WMcuK9M13SlUPyRtud9frS8t4_7lFeRXiwevmicJMgk,15472
|
|
20
|
-
agentic_threat_hunting_framework-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
-
agentic_threat_hunting_framework-0.2.1.dist-info/entry_points.txt,sha256=GopR2iTiBs-yNMWiUZ2DaFIFglXxWJx1XPjTa3ePtfE,39
|
|
22
|
-
agentic_threat_hunting_framework-0.2.1.dist-info/top_level.txt,sha256=Cxxg6SMLfawDJWBITsciRzq27XV8fiaAor23o9Byoes,5
|
|
23
|
-
agentic_threat_hunting_framework-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|