agentic-threat-hunting-framework 0.3.1__py3-none-any.whl → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-threat-hunting-framework
3
- Version: 0.3.1
3
+ Version: 0.5.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>
@@ -33,6 +33,7 @@ Requires-Dist: click>=8.0.0
33
33
  Requires-Dist: pyyaml>=6.0
34
34
  Requires-Dist: rich>=10.0.0
35
35
  Requires-Dist: jinja2>=3.0.0
36
+ Requires-Dist: python-dotenv>=0.19.0
36
37
  Requires-Dist: importlib_resources>=5.0.0; python_version < "3.9"
37
38
  Provides-Extra: dev
38
39
  Requires-Dist: pytest>=7.0.0; extra == "dev"
@@ -49,6 +50,8 @@ Requires-Dist: mkdocs>=1.5.0; extra == "docs"
49
50
  Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
50
51
  Provides-Extra: similarity
51
52
  Requires-Dist: scikit-learn>=1.0.0; extra == "similarity"
53
+ Provides-Extra: splunk
54
+ Requires-Dist: requests>=2.25.0; extra == "splunk"
52
55
  Dynamic: license-file
53
56
 
54
57
  # Agentic Threat Hunting Framework (ATHF)
@@ -1,39 +1,49 @@
1
- agentic_threat_hunting_framework-0.3.1.dist-info/licenses/LICENSE,sha256=_KObErRfiKoolznt-DF0nJnr3U9Rdh7Z4Ba7G5qqckk,1071
1
+ agentic_threat_hunting_framework-0.5.0.dist-info/licenses/LICENSE,sha256=_KObErRfiKoolznt-DF0nJnr3U9Rdh7Z4Ba7G5qqckk,1071
2
2
  athf/__init__.py,sha256=OrjZe8P97_BTEkscapnwSsqKSjwXNP9d8-HtGr19Ni0,241
3
- athf/__version__.py,sha256=BCXUHJGbpcGJIqpp7wAIo-a46Xl8TlClrdkqx9_kTW8,59
4
- athf/cli.py,sha256=rkg_Nx9Yy_UqTXBOh-pwaiD-lXO0_IXQMA1SQpDj7g0,4639
3
+ athf/__version__.py,sha256=wCIQoU9b7qKcSNQiIOgHaD2buzBC-dlQYtvg8X5WS4A,59
4
+ athf/cli.py,sha256=108PnDRlaytJj9KzjzcTLljB3DeerMIXZOeAQJrmtPU,5052
5
+ athf/plugin_system.py,sha256=c_9-oUiR6tuYpWpEmeVRayU8-TXlkjvZC3EUxuYWW4M,1515
5
6
  athf/agents/__init__.py,sha256=iaSJpvnXm9rz4QS7gBrsaLEjm49uvsMs4BLPOJeyp78,346
6
7
  athf/agents/base.py,sha256=lnVDIOQUOyP-Apa9UM2E1mRXUPnNJ4hVqQXOwVw2u4c,4286
7
8
  athf/agents/llm/__init__.py,sha256=qSGA-NaInjsDkMpGQwnTz3S1OgCVlzetpMcDS_to1co,671
8
9
  athf/agents/llm/hunt_researcher.py,sha256=dIyD2Izh3zdf62kCHug1DwXFgmWhOMQUTim7qM3UAIs,27071
9
10
  athf/agents/llm/hypothesis_generator.py,sha256=XkbJz8IS4zwQjEy-ZD0zy2XW5uRnAy87Lii-5XTY0WU,8564
10
- athf/commands/__init__.py,sha256=KbpUcLPjmltq5a_m1MjhrIe4sk3DvqsnAw1wCAZfZNo,85
11
- athf/commands/agent.py,sha256=k-NWiLppt2oWbiJ-hx1inkK51jhfsAYiFhixbzzQmQI,16565
11
+ athf/commands/__init__.py,sha256=-ZOfg6uV1eSh7RDW7dKzdufuYvQTT0KGMF4JB6waHsY,635
12
+ athf/commands/agent.py,sha256=c7ZeZa3OArXyXTgVjmUB2JXa3m9IpLFJ_FEVDhaDLE8,19000
12
13
  athf/commands/context.py,sha256=V-at81-OgKcLY-In48-AccTnHfTgdofmnjE8S5kypoI,12678
13
14
  athf/commands/env.py,sha256=JPKRsv48cgsIAjSFaGJ1-Nu0nQKGSVg4AbiFxb9jVX4,11887
14
- athf/commands/hunt.py,sha256=PcYz0Zj9qqB10s9mkbfHk-hl2IbcfJekeB6cA2exXPo,22991
15
+ athf/commands/hunt.py,sha256=aQdgNddqy_VrxZOkxhuPxIr4KLZtX5a2ZLb9079vLlw,25169
15
16
  athf/commands/init.py,sha256=Qn0iETNyuQvM-ySqCeoDz-pPemeuzROX_karQF5yN_o,12685
16
17
  athf/commands/investigate.py,sha256=mK_id5vjfN_ukqB_-fyia0FNa0pBmtn0Xv6CKHQI1Qo,24663
17
18
  athf/commands/research.py,sha256=FrLph4agaGQ_rIxMh0OQwh1MIGDFtj40zJ3E1ZFwaAw,18112
18
19
  athf/commands/similar.py,sha256=FTTVr4zzP9bdJrirscp6pOxdQbE8zot6pa20-_TYiuo,11804
20
+ athf/commands/splunk.py,sha256=7n7Jl1ExqZCNxUhG0kAKgAvZMqbIoGSgx2Moq7vAu-Y,11622
19
21
  athf/core/__init__.py,sha256=yG7C8ljx3UW4QZoYvDjUxsWHlbS8M-GLGB7Je7rRfqo,31
20
22
  athf/core/attack_matrix.py,sha256=QZKKmxckQ6-U7lqVdGUJoj2jEAhP3Juvr3sqaNx2oTw,3238
23
+ athf/core/clickhouse_connection.py,sha256=8thmJvd2pUeeRZmDE7K491NgbC0myNZsdA29ooJRfVM,13561
21
24
  athf/core/hunt_manager.py,sha256=PFsg8Ecg94NCpuFZpApo82lyORkgK5IfOIih-7-XsmM,11580
22
25
  athf/core/hunt_parser.py,sha256=FUj0yyBIcZnaS9aItMImeBDhegQwpkewIwUMNXW_ZWU,5122
23
26
  athf/core/investigation_parser.py,sha256=wbfjnq4gFgIc0a4bHIAnidVNPhbHDpIXWY1SGLk0Xls,6804
27
+ athf/core/metrics_tracker.py,sha256=VYEiO5QVteTtR4ddyHkL61KrO4QVNUDdNaDOVFcHy4Q,18873
28
+ athf/core/query_executor.py,sha256=OtzUkxoOdDC4ZErVIbf0Qov82uHRJ8dJ965r4pLbiVA,6271
29
+ athf/core/query_parser.py,sha256=Uz3ZMpd4YWKLPoge16uKZLlcMQrg49Z0NLXSceg893w,6722
30
+ athf/core/query_suggester.py,sha256=i3P624tXb9uRKGxTpcSZx4ZVbOwnCiJqLnkxQD_UqyA,7736
31
+ athf/core/query_validator.py,sha256=mfwdtLcPZS6ON4AlR-4d8YbQ12cqpnIq6526obdPDx8,9101
24
32
  athf/core/research_manager.py,sha256=i4fUjuZJcAik8I4pwbLkQlu6cuxkWDlqaIRQrzAfB0s,14512
25
- athf/core/template_engine.py,sha256=vNTVhlxIXZpxU7VmQyrqCSt6ORS0IVjAV54TOmUDMTE,5636
33
+ athf/core/session_manager.py,sha256=8Mz082ex87VXPiSFYRFNAb9e3ED6luCy0Q6zilyaz9A,25108
34
+ athf/core/splunk_client.py,sha256=Xib2zVwV2l8eChzqUahI3PZ7Z2XS2wz01sPbF1E0Q18,11611
35
+ athf/core/template_engine.py,sha256=Awp0n9E5Q1dYA35XDKKAd5VJLdpaDl2N967hackUVa8,6010
26
36
  athf/core/web_search.py,sha256=B9IhmwH7gy2RVA6WSN3L7yGp3Q4L8OsiiwcEvnnZejU,10320
27
37
  athf/data/__init__.py,sha256=QtgONloCaS3E9Ow995FMxyy6BbszpfmYeWpySQ2b9Mc,502
28
- athf/data/docs/CHANGELOG.md,sha256=2omJArkID-VADL0gNDfBSS0_E9GnP9OfZLn9ls-l5eA,7074
29
- athf/data/docs/CLI_REFERENCE.md,sha256=zqUp-tu8OAcqzpOwx3XvzEq7UV6woDraUOcWasZI0a8,43748
38
+ athf/data/docs/CHANGELOG.md,sha256=JKkzzs1n5jSERHFi6fDt6sYEe52MSaY127dfzthkUA8,8655
39
+ athf/data/docs/CLI_REFERENCE.md,sha256=pb76UqkY_WHJMBEXwEmK0TJR8kcGzoBPlJ0WdGMKDQM,54875
30
40
  athf/data/docs/INSTALL.md,sha256=JOWxk6q2-rdpgCnWdSPb3-Cp8rX1y4nQm7ObKz2G0uM,13117
31
41
  athf/data/docs/README.md,sha256=rp-XQZeqteXJz7M2qKX3sl6o0AVfhGmz8GcNNKAt8pM,1061
32
42
  athf/data/docs/environment.md,sha256=K88NBWZM2bI1Jztd0ORa6AYaMgPVjVB-K2fJl8S5-g8,8306
33
- athf/data/docs/getting-started.md,sha256=j4SAXe-Rm1RhYBDvWaNpV8XS0rc_mZ2Ew0yPCxE4_wQ,14156
34
- athf/data/docs/level4-agentic-workflows.md,sha256=DX54qu8LbJysjDfQLGSEPSO_Q6BUACLpa-XCsR6xUp4,13439
43
+ athf/data/docs/getting-started.md,sha256=dUCpXHzucRLfUYzDylvnCtdqv9VCukfQCtGg7hTGmrI,15316
44
+ athf/data/docs/level4-agentic-workflows.md,sha256=68crKsDaLyrgxVG37nPIuJyO9NobLi09Obv7D1AnpYs,14123
35
45
  athf/data/docs/lock-pattern.md,sha256=eICjNh5SAgIhkOYBDhHg1tgw4A29xgnRDWC9vH1wLEQ,4863
36
- athf/data/docs/maturity-model.md,sha256=S2m8JSQDe9R5ROBWS4Gy0-sRF5I7mo-CI3cUnmNpxmk,16347
46
+ athf/data/docs/maturity-model.md,sha256=O1FDIKPkO9twNdZmA0w-TUwPvLP331tul2fPpUnCXD4,18181
37
47
  athf/data/docs/why-athf.md,sha256=rIoUb7iqdZKbuWNyRlGxhZrRkLx7gWAGS-kurEZDt04,2148
38
48
  athf/data/hunts/FORMAT_GUIDELINES.md,sha256=lMyBekmOzhtO1olO1P-M0Gi_n5oY60k7qkRZE63sTgw,15010
39
49
  athf/data/hunts/H-0001.md,sha256=rdUIpQ_uN8bx7XS1ED85rW5aRKxFOpMg0X7PANY7eCY,23220
@@ -49,8 +59,8 @@ athf/data/prompts/ai-workflow.md,sha256=rZtOcGuAEi35qx7182TwHJEORdz1-RxkZMBVkg61
49
59
  athf/data/prompts/basic-prompts.md,sha256=2bunpO35RoBdJWYthXVi40RNl2UWrfwOaFthBLHF5sU,8463
50
60
  athf/data/templates/HUNT_LOCK.md,sha256=zXxHaKMWbRDLewLTegYJMbXRM72s9gFFvjdwFfGNeJE,7386
51
61
  athf/utils/__init__.py,sha256=aEAPI1xnAsowOtc036cCb9ZOek5nrrfevu8PElhbNgk,30
52
- agentic_threat_hunting_framework-0.3.1.dist-info/METADATA,sha256=KgED__EriZvPR42CCSDQHf7md832CFyd7RyRTbdtQbU,15838
53
- agentic_threat_hunting_framework-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- agentic_threat_hunting_framework-0.3.1.dist-info/entry_points.txt,sha256=GopR2iTiBs-yNMWiUZ2DaFIFglXxWJx1XPjTa3ePtfE,39
55
- agentic_threat_hunting_framework-0.3.1.dist-info/top_level.txt,sha256=Cxxg6SMLfawDJWBITsciRzq27XV8fiaAor23o9Byoes,5
56
- agentic_threat_hunting_framework-0.3.1.dist-info/RECORD,,
62
+ agentic_threat_hunting_framework-0.5.0.dist-info/METADATA,sha256=mM_lQGR-f8k7s905FXJ5xucVAoc6hp5yrq8cQmKJ-T0,15949
63
+ agentic_threat_hunting_framework-0.5.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
64
+ agentic_threat_hunting_framework-0.5.0.dist-info/entry_points.txt,sha256=GopR2iTiBs-yNMWiUZ2DaFIFglXxWJx1XPjTa3ePtfE,39
65
+ agentic_threat_hunting_framework-0.5.0.dist-info/top_level.txt,sha256=Cxxg6SMLfawDJWBITsciRzq27XV8fiaAor23o9Byoes,5
66
+ agentic_threat_hunting_framework-0.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
athf/__version__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version information for ATHF."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.4.0"
athf/cli.py CHANGED
@@ -3,11 +3,15 @@
3
3
  import random
4
4
 
5
5
  import click
6
+ from dotenv import load_dotenv
6
7
  from rich.console import Console
7
8
 
8
- from athf.__version__ import __version__
9
- from athf.commands import context, env, hunt, init, investigate, research, similar
10
- from athf.commands.agent import agent
9
+ # Load .env file from current directory (if it exists)
10
+ load_dotenv()
11
+
12
+ from athf.__version__ import __version__ # noqa: E402
13
+ from athf.commands import context, env, hunt, init, investigate, research, similar, splunk # noqa: E402
14
+ from athf.commands.agent import agent # noqa: E402
11
15
 
12
16
  console = Console()
13
17
 
@@ -78,19 +82,30 @@ def cli() -> None:
78
82
 
79
83
 
80
84
  # Register command groups
81
- cli.add_command(init.init)
82
- cli.add_command(hunt.hunt)
83
- cli.add_command(investigate.investigate)
84
- cli.add_command(research.research)
85
+ cli.add_command(init)
86
+ cli.add_command(hunt)
87
+ cli.add_command(investigate)
88
+ cli.add_command(research)
85
89
 
86
90
  # Phase 1 commands (env, context, similar)
87
- cli.add_command(env.env)
88
- cli.add_command(context.context)
89
- cli.add_command(similar.similar)
91
+ cli.add_command(env)
92
+ cli.add_command(context)
93
+ cli.add_command(similar)
90
94
 
91
95
  # Agent commands
92
96
  cli.add_command(agent)
93
97
 
98
+ # Integration commands (optional, requires additional dependencies)
99
+ if splunk is not None:
100
+ cli.add_command(splunk)
101
+
102
+ # Load and register plugins
103
+ from athf.plugin_system import PluginRegistry
104
+
105
+ PluginRegistry.load_plugins()
106
+ for name, cmd in PluginRegistry._commands.items():
107
+ cli.add_command(cmd, name=name)
108
+
94
109
 
95
110
  @cli.command(hidden=True)
96
111
  def wisdom() -> None:
athf/commands/__init__.py CHANGED
@@ -1,5 +1,26 @@
1
- """ATHF CLI commands."""
1
+ """ATHF CLI commands (base commands only)."""
2
2
 
3
- from athf.commands.agent import agent
3
+ from athf.commands.context import context
4
+ from athf.commands.env import env
5
+ from athf.commands.hunt import hunt
6
+ from athf.commands.init import init
7
+ from athf.commands.investigate import investigate
8
+ from athf.commands.research import research
9
+ from athf.commands.similar import similar
4
10
 
5
- __all__ = ["agent"]
11
+ # Optional: Splunk integration (requires requests package)
12
+ try:
13
+ from athf.commands.splunk import splunk
14
+ except ImportError:
15
+ splunk = None # type: ignore[assignment]
16
+
17
+ __all__ = [
18
+ "init",
19
+ "hunt",
20
+ "investigate",
21
+ "research",
22
+ "context",
23
+ "similar",
24
+ "env",
25
+ "splunk",
26
+ ]
athf/commands/agent.py CHANGED
@@ -125,6 +125,7 @@ def info(agent_name: str) -> None:
125
125
 
126
126
  console.print("\n[bold]Usage:[/bold]")
127
127
  console.print(' athf agent run hypothesis-generator --threat-intel "APT29 targeting SaaS"')
128
+ console.print(' athf agent run hypothesis-generator --threat-intel "..." --research R-0001')
128
129
  console.print()
129
130
 
130
131
  elif agent_name == "hunt-researcher":
@@ -170,6 +171,7 @@ def info(agent_name: str) -> None:
170
171
  @agent.command()
171
172
  @click.argument("agent_name")
172
173
  @click.option("--threat-intel", help="Threat intelligence context (for hypothesis-generator)")
174
+ @click.option("--research", help="Research document ID (e.g., R-0001) to load context from")
173
175
  @click.option("--topic", help="Research topic (for hunt-researcher)")
174
176
  @click.option("--technique", help="MITRE ATT&CK technique (for hunt-researcher)")
175
177
  @click.option(
@@ -191,6 +193,7 @@ def info(agent_name: str) -> None:
191
193
  def run( # noqa: C901
192
194
  agent_name: str,
193
195
  threat_intel: Optional[str],
196
+ research: Optional[str],
194
197
  topic: Optional[str],
195
198
  technique: Optional[str],
196
199
  depth: str,
@@ -208,6 +211,7 @@ def run( # noqa: C901
208
211
  # Hypothesis Generator
209
212
  athf agent run hypothesis-generator --threat-intel "APT29 targeting SaaS applications"
210
213
  athf agent run hypothesis-generator --threat-intel "Insider threat data exfiltration" --tactic collection
214
+ athf agent run hypothesis-generator --threat-intel "Credential dumping" --research R-0001
211
215
 
212
216
  # Hunt Researcher
213
217
  athf agent run hunt-researcher --topic "LSASS dumping"
@@ -232,6 +236,32 @@ def run( # noqa: C901
232
236
  # Try to load past hunts and environment data if available
233
237
  past_hunts: List[dict[str, Any]] = []
234
238
  environment = {}
239
+ research_context = None
240
+
241
+ # Load research document if provided
242
+ if research:
243
+ try:
244
+ from pathlib import Path
245
+
246
+ from athf.core.research_manager import ResearchManager
247
+
248
+ research_mgr = ResearchManager(Path.cwd())
249
+ research_doc = research_mgr.get_research(research)
250
+
251
+ if research_doc:
252
+ # Extract relevant research context
253
+ research_context = {
254
+ "research_id": research_doc.get("metadata", {}).get("research_id"),
255
+ "topic": research_doc.get("metadata", {}).get("topic"),
256
+ "recommended_hypothesis": research_doc.get("synthesis", {}).get("recommended_hypothesis"),
257
+ "gaps": research_doc.get("synthesis", {}).get("gaps_identified", []),
258
+ "key_findings": research_doc.get("synthesis", {}).get("key_findings", []),
259
+ }
260
+ console.print(f"[green]✓ Loaded research context from {research}[/green]\n")
261
+ else:
262
+ console.print(f"[yellow]⚠ Research document {research} not found[/yellow]\n")
263
+ except Exception as e:
264
+ console.print(f"[yellow]⚠ Could not load research document: {e}[/yellow]\n")
235
265
 
236
266
  # Try to load environment.md if it exists
237
267
  try:
@@ -252,10 +282,22 @@ def run( # noqa: C901
252
282
  "platforms": ["Windows", "macOS", "Linux"],
253
283
  }
254
284
 
285
+ # If research context is provided, append it to threat intel
286
+ threat_intel_with_research = threat_intel
287
+ if research_context:
288
+ threat_intel_with_research = (
289
+ f"{threat_intel}\n\n"
290
+ f"Research Context from {research_context['research_id']}:\n"
291
+ f"- Topic: {research_context['topic']}\n"
292
+ f"- Recommended Hypothesis: {research_context.get('recommended_hypothesis', 'N/A')}\n"
293
+ )
294
+ if research_context.get("gaps"):
295
+ threat_intel_with_research += f"- Gaps: {', '.join(research_context['gaps'][:3])}\n"
296
+
255
297
  # Execute agent
256
298
  hypothesis_result = hypothesis_agent.execute(
257
299
  HypothesisGenerationInput(
258
- threat_intel=threat_intel,
300
+ threat_intel=threat_intel_with_research,
259
301
  past_hunts=past_hunts,
260
302
  environment=environment,
261
303
  )
athf/commands/hunt.py CHANGED
@@ -40,6 +40,9 @@ Examples:
40
40
  # Non-interactive with all options
41
41
  athf hunt new --technique T1003.001 --title "LSASS Dumping" --non-interactive
42
42
 
43
+ # Link research document to hunt
44
+ athf hunt new --research R-0001 --title "Hunt Title" --non-interactive
45
+
43
46
  # List hunts with filters
44
47
  athf hunt list --status completed --tactic credential-access
45
48
 
@@ -52,6 +55,9 @@ Examples:
52
55
  # Show coverage gaps
53
56
  athf hunt coverage
54
57
 
58
+ # Filter coverage by tactic
59
+ athf hunt coverage --tactic credential-access
60
+
55
61
  # Validate hunt structure
56
62
  athf hunt validate H-0042
57
63
 
@@ -96,6 +102,7 @@ def hunt() -> None:
96
102
  @click.option("--location", help="Location/scope (for ABLE framework)")
97
103
  @click.option("--evidence", help="Evidence description (for ABLE framework)")
98
104
  @click.option("--hunter", help="Hunter name", default="AI Assistant")
105
+ @click.option("--research", help="Research document ID (e.g., R-0001) this hunt is based on")
99
106
  def new(
100
107
  technique: Optional[str],
101
108
  title: Optional[str],
@@ -110,6 +117,7 @@ def new(
110
117
  location: Optional[str],
111
118
  evidence: Optional[str],
112
119
  hunter: Optional[str],
120
+ research: Optional[str],
113
121
  ) -> None:
114
122
  """Create a new hunt hypothesis with LOCK structure.
115
123
 
@@ -119,6 +127,7 @@ def new(
119
127
  • YAML frontmatter with metadata
120
128
  • LOCK pattern sections (Learn, Observe, Check, Keep)
121
129
  • MITRE ATT&CK mapping
130
+ • Optional link to research document
122
131
 
123
132
  \b
124
133
  Interactive mode (default):
@@ -131,6 +140,11 @@ def new(
131
140
  Example: athf hunt new --technique T1003.001 --title "LSASS Dumping" \\
132
141
  --tactic credential-access --platform Windows --non-interactive
133
142
 
143
+ \b
144
+ With research document:
145
+ Link a pre-hunt research document to the hunt.
146
+ Example: athf hunt new --research R-0001 --title "Hunt Title" --non-interactive
147
+
134
148
  \b
135
149
  After creation:
136
150
  1. Edit hunts/H-XXXX.md to flesh out your hypothesis
@@ -155,6 +169,13 @@ def new(
155
169
 
156
170
  console.print(f"[bold]Hunt ID:[/bold] {hunt_id}")
157
171
 
172
+ # Validate research document if provided
173
+ if research:
174
+ research_file = Path("research") / f"{research}.md"
175
+ if not research_file.exists():
176
+ console.print(f"[yellow]Warning: Research document {research} not found at {research_file}[/yellow]")
177
+ console.print("[yellow]Hunt will still be created, but research link may be broken.[/yellow]\n")
178
+
158
179
  # Gather hunt details
159
180
  if non_interactive:
160
181
  if not title:
@@ -209,6 +230,7 @@ def new(
209
230
  behavior=behavior,
210
231
  location=location,
211
232
  evidence=evidence,
233
+ spawned_from=research,
212
234
  )
213
235
 
214
236
  # Write hunt file
@@ -529,8 +551,9 @@ def _render_progress_bar(covered: int, total: int, width: int = 20) -> str:
529
551
 
530
552
 
531
553
  @hunt.command()
554
+ @click.option("--tactic", help="Filter by specific tactic (or 'all' for all tactics)")
532
555
  @click.option("--detailed", is_flag=True, help="Show detailed technique coverage with hunt references")
533
- def coverage(detailed: bool) -> None:
556
+ def coverage(tactic: Optional[str], detailed: bool) -> None:
534
557
  """Show MITRE ATT&CK technique coverage across hunts.
535
558
 
536
559
  \b
@@ -542,11 +565,17 @@ def coverage(detailed: bool) -> None:
542
565
 
543
566
  \b
544
567
  Examples:
545
- # Show coverage overview
568
+ # Show coverage overview for all tactics
546
569
  athf hunt coverage
547
570
 
548
- # Show detailed technique mapping
549
- athf hunt coverage --detailed
571
+ # Show all tactics explicitly
572
+ athf hunt coverage --tactic all
573
+
574
+ # Show coverage for a specific tactic
575
+ athf hunt coverage --tactic credential-access
576
+
577
+ # Show detailed technique mapping for execution tactic
578
+ athf hunt coverage --tactic execution --detailed
550
579
 
551
580
  \b
552
581
  Note on technique counts:
@@ -578,12 +607,31 @@ def coverage(detailed: bool) -> None:
578
607
  summary = coverage["summary"]
579
608
  by_tactic = coverage["by_tactic"]
580
609
 
610
+ # Determine which tactics to display
611
+ tactics_to_display = []
612
+ if tactic and tactic.lower() != "all":
613
+ # Validate tactic exists
614
+ if tactic not in ATTACK_TACTICS:
615
+ console.print(f"[red]Error: Unknown tactic '{tactic}'[/red]")
616
+ console.print("\n[bold]Valid tactics:[/bold]")
617
+ for tactic_key in get_sorted_tactics():
618
+ console.print(f" • {tactic_key}")
619
+ return
620
+ tactics_to_display = [tactic]
621
+ else:
622
+ # Show all tactics
623
+ tactics_to_display = get_sorted_tactics()
624
+
581
625
  # Display title
582
- console.print("\n[bold]MITRE ATT&CK Coverage[/bold]")
626
+ if tactic and tactic.lower() != "all":
627
+ tactic_display_name = ATTACK_TACTICS[tactic]["name"]
628
+ console.print(f"\n[bold]MITRE ATT&CK Coverage - {tactic_display_name}[/bold]")
629
+ else:
630
+ console.print("\n[bold]MITRE ATT&CK Coverage[/bold]")
583
631
  console.print("─" * 60 + "\n")
584
632
 
585
- # Display all tactics in ATT&CK order with hunt counts
586
- for tactic_key in get_sorted_tactics():
633
+ # Display selected tactics in ATT&CK order with hunt counts
634
+ for tactic_key in tactics_to_display:
587
635
  data = by_tactic.get(tactic_key, {})
588
636
  tactic_name = ATTACK_TACTICS[tactic_key]["name"]
589
637
 
@@ -596,16 +644,19 @@ def coverage(detailed: bool) -> None:
596
644
  else:
597
645
  console.print(f"{tactic_name:<24} [dim]no coverage[/dim]")
598
646
 
599
- # Display overall coverage
600
- console.print(
601
- f"\n[bold]Overall: {summary['unique_techniques']}/{summary['total_techniques']} techniques ({summary['overall_coverage_pct']:.0f}%)[/bold]\n"
602
- )
647
+ # Display overall coverage only if showing all tactics
648
+ if not tactic or tactic.lower() == "all":
649
+ console.print(
650
+ f"\n[bold]Overall: {summary['unique_techniques']}/{summary['total_techniques']} techniques ({summary['overall_coverage_pct']:.0f}%)[/bold]\n"
651
+ )
652
+ else:
653
+ console.print()
603
654
 
604
655
  # Display detailed technique coverage if requested
605
656
  if detailed:
606
657
  console.print("\n[bold cyan]🔍 Detailed Technique Coverage[/bold cyan]\n")
607
658
 
608
- for tactic_key in get_sorted_tactics():
659
+ for tactic_key in tactics_to_display:
609
660
  data = by_tactic.get(tactic_key, {})
610
661
  if data.get("hunt_count", 0) == 0:
611
662
  continue # Skip tactics with no hunts in detailed view