agentic-threat-hunting-framework 0.5.1__tar.gz → 0.7.0__tar.gz

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.
Files changed (79) hide show
  1. {agentic_threat_hunting_framework-0.5.1/agentic_threat_hunting_framework.egg-info → agentic_threat_hunting_framework-0.7.0}/PKG-INFO +1 -1
  2. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0/agentic_threat_hunting_framework.egg-info}/PKG-INFO +1 -1
  3. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/agentic_threat_hunting_framework.egg-info/SOURCES.txt +5 -4
  4. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/__version__.py +1 -1
  5. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/agents/llm/hunt_researcher.py +5 -5
  6. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/cli.py +1 -2
  7. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/agent.py +1 -2
  8. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/hunt.py +75 -7
  9. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/investigate.py +84 -2
  10. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/core/hunt_manager.py +21 -3
  11. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/core/research_manager.py +30 -1
  12. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/INSTALL.md +1 -1
  13. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/getting-started.md +1 -1
  14. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/lock-pattern.md +3 -3
  15. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/hunts/FORMAT_GUIDELINES.md +1 -1
  16. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/hunts/README.md +3 -3
  17. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/prompts/README.md +1 -1
  18. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/prompts/ai-workflow.md +1 -1
  19. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/prompts/basic-prompts.md +1 -1
  20. agentic_threat_hunting_framework-0.7.0/athf/plugin_system.py +72 -0
  21. agentic_threat_hunting_framework-0.7.0/athf/utils/validation.py +170 -0
  22. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/pyproject.toml +1 -1
  23. agentic_threat_hunting_framework-0.5.1/athf/plugin_system.py +0 -48
  24. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/LICENSE +0 -0
  25. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/MANIFEST.in +0 -0
  26. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/README.md +0 -0
  27. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/USING_ATHF.md +0 -0
  28. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/agentic_threat_hunting_framework.egg-info/dependency_links.txt +0 -0
  29. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/agentic_threat_hunting_framework.egg-info/entry_points.txt +0 -0
  30. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/agentic_threat_hunting_framework.egg-info/requires.txt +0 -0
  31. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/agentic_threat_hunting_framework.egg-info/top_level.txt +0 -0
  32. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/ATHF_level_3.png +0 -0
  33. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/athf-cli-workflow.gif +0 -0
  34. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/athf-level0.gif +0 -0
  35. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/athf-level1.gif +0 -0
  36. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/athf-level2.gif +0 -0
  37. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/athf-level3.gif +0 -0
  38. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/athf_fivelevels.png +0 -0
  39. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/athf_lock.png +0 -0
  40. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/athf_logo.png +0 -0
  41. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/assets/athf_manual_v_ai.png +0 -0
  42. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/__init__.py +0 -0
  43. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/agents/__init__.py +0 -0
  44. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/agents/base.py +0 -0
  45. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/agents/llm/__init__.py +0 -0
  46. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/agents/llm/hypothesis_generator.py +0 -0
  47. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/__init__.py +0 -0
  48. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/context.py +0 -0
  49. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/env.py +0 -0
  50. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/init.py +0 -0
  51. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/research.py +0 -0
  52. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/similar.py +0 -0
  53. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/commands/splunk.py +0 -0
  54. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/core/__init__.py +0 -0
  55. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/core/attack_matrix.py +0 -0
  56. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/core/hunt_parser.py +0 -0
  57. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/core/investigation_parser.py +0 -0
  58. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/core/splunk_client.py +0 -0
  59. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/core/template_engine.py +0 -0
  60. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/core/web_search.py +0 -0
  61. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/__init__.py +0 -0
  62. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/CHANGELOG.md +0 -0
  63. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/CLI_REFERENCE.md +0 -0
  64. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/README.md +0 -0
  65. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/environment.md +0 -0
  66. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/level4-agentic-workflows.md +0 -0
  67. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/maturity-model.md +0 -0
  68. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/docs/why-athf.md +0 -0
  69. {agentic_threat_hunting_framework-0.5.1/athf/data/hunts → agentic_threat_hunting_framework-0.7.0/athf/data/hunts/production/2026/Q1}/H-0001.md +0 -0
  70. {agentic_threat_hunting_framework-0.5.1/athf/data/hunts → agentic_threat_hunting_framework-0.7.0/athf/data/hunts/production/2026/Q1}/H-0002.md +0 -0
  71. {agentic_threat_hunting_framework-0.5.1/athf/data/hunts → agentic_threat_hunting_framework-0.7.0/athf/data/hunts/production/2026/Q1}/H-0003.md +0 -0
  72. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/integrations/MCP_CATALOG.md +0 -0
  73. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/integrations/README.md +0 -0
  74. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/integrations/quickstart/splunk.md +0 -0
  75. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/knowledge/hunting-knowledge.md +0 -0
  76. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/data/templates/HUNT_LOCK.md +0 -0
  77. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/athf/utils/__init__.py +0 -0
  78. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/setup.cfg +0 -0
  79. {agentic_threat_hunting_framework-0.5.1 → agentic_threat_hunting_framework-0.7.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-threat-hunting-framework
3
- Version: 0.5.1
3
+ Version: 0.7.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>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-threat-hunting-framework
3
- Version: 0.5.1
3
+ Version: 0.7.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>
@@ -60,10 +60,10 @@ athf/data/docs/lock-pattern.md
60
60
  athf/data/docs/maturity-model.md
61
61
  athf/data/docs/why-athf.md
62
62
  athf/data/hunts/FORMAT_GUIDELINES.md
63
- athf/data/hunts/H-0001.md
64
- athf/data/hunts/H-0002.md
65
- athf/data/hunts/H-0003.md
66
63
  athf/data/hunts/README.md
64
+ athf/data/hunts/production/2026/Q1/H-0001.md
65
+ athf/data/hunts/production/2026/Q1/H-0002.md
66
+ athf/data/hunts/production/2026/Q1/H-0003.md
67
67
  athf/data/integrations/MCP_CATALOG.md
68
68
  athf/data/integrations/README.md
69
69
  athf/data/integrations/quickstart/splunk.md
@@ -72,4 +72,5 @@ athf/data/prompts/README.md
72
72
  athf/data/prompts/ai-workflow.md
73
73
  athf/data/prompts/basic-prompts.md
74
74
  athf/data/templates/HUNT_LOCK.md
75
- athf/utils/__init__.py
75
+ athf/utils/__init__.py
76
+ athf/utils/validation.py
@@ -1,3 +1,3 @@
1
1
  """Version information for ATHF."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.7.0"
@@ -13,7 +13,7 @@ import os
13
13
  import time
14
14
  from dataclasses import dataclass, field
15
15
  from pathlib import Path
16
- from typing import Any, Dict, List, Optional
16
+ from typing import Any, Dict, List, Optional, Tuple
17
17
 
18
18
  from athf.agents.base import AgentResult, LLMAgent
19
19
 
@@ -457,7 +457,7 @@ class HuntResearcherAgent(LLMAgent[ResearchInput, ResearchOutput]):
457
457
  topic: str,
458
458
  sources: List[Dict[str, str]],
459
459
  search_results: Optional[Any],
460
- ) -> tuple[str, List[str]]:
460
+ ) -> Tuple[str, List[str]]:
461
461
  """Use LLM to summarize system research findings."""
462
462
  try:
463
463
  client = self._get_llm_client()
@@ -502,7 +502,7 @@ Return JSON format:
502
502
  technique: Optional[str],
503
503
  sources: List[Dict[str, str]],
504
504
  search_results: Optional[Any],
505
- ) -> tuple[str, List[str]]:
505
+ ) -> Tuple[str, List[str]]:
506
506
  """Use LLM to summarize adversary tradecraft findings."""
507
507
  try:
508
508
  client = self._get_llm_client()
@@ -549,7 +549,7 @@ Return JSON format:
549
549
  technique: Optional[str],
550
550
  ocsf_schema: str,
551
551
  environment_data: str,
552
- ) -> tuple[str, List[str]]:
552
+ ) -> Tuple[str, List[str]]:
553
553
  """Use LLM to map topic to OCSF telemetry fields."""
554
554
  try:
555
555
  client = self._get_llm_client()
@@ -590,7 +590,7 @@ Return JSON format:
590
590
  topic: str,
591
591
  technique: Optional[str],
592
592
  skills: List[ResearchSkillOutput],
593
- ) -> tuple[str, List[str]]:
593
+ ) -> Tuple[str, List[str]]:
594
594
  """Use LLM to synthesize all research findings."""
595
595
  try:
596
596
  client = self._get_llm_client()
@@ -12,6 +12,7 @@ load_dotenv()
12
12
  from athf.__version__ import __version__ # noqa: E402
13
13
  from athf.commands import context, env, hunt, init, investigate, research, similar, splunk # noqa: E402
14
14
  from athf.commands.agent import agent # noqa: E402
15
+ from athf.plugin_system import PluginRegistry # noqa: E402
15
16
 
16
17
  console = Console()
17
18
 
@@ -100,8 +101,6 @@ if splunk is not None:
100
101
  cli.add_command(splunk)
101
102
 
102
103
  # Load and register plugins
103
- from athf.plugin_system import PluginRegistry
104
-
105
104
  PluginRegistry.load_plugins()
106
105
  for name, cmd in PluginRegistry._commands.items():
107
106
  cli.add_command(cmd, name=name)
@@ -41,9 +41,8 @@ def agent() -> None:
41
41
  LLM agents use Claude API for creative and analytical tasks.
42
42
 
43
43
  \b
44
- Agent Execution Modes:
44
+ Agent Execution Mode:
45
45
  • INTERACTIVE (default): Step-by-step execution with user approval
46
- • AUTONOMOUS (--auto): Runs all steps without check-ins
47
46
  """
48
47
  pass
49
48
 
@@ -15,10 +15,29 @@ from rich.table import Table
15
15
  from athf.core.hunt_manager import HuntManager
16
16
  from athf.core.hunt_parser import validate_hunt_file
17
17
  from athf.core.template_engine import render_hunt_template
18
+ from athf.utils.validation import validate_hunt_id, validate_research_id
18
19
 
19
20
  console = Console()
20
21
 
21
22
 
23
+ def get_hunt_directory(is_test: bool = False) -> Path:
24
+ """Calculate hunt directory based on current date.
25
+
26
+ Args:
27
+ is_test: If True, creates in test/ directory, otherwise production/
28
+
29
+ Returns:
30
+ Path to hunt directory (hunts/{environment}/{YYYY}/{QX}/)
31
+ """
32
+ now = datetime.now()
33
+ year = now.year
34
+ quarter = f"Q{(now.month - 1) // 3 + 1}"
35
+
36
+ environment = "test" if is_test else "production"
37
+
38
+ return Path("hunts") / environment / str(year) / quarter
39
+
40
+
22
41
  def get_config_path() -> Path:
23
42
  """Get config file path, checking new location first, then falling back to root."""
24
43
  new_location = Path("config/.athfconfig.yaml")
@@ -94,6 +113,7 @@ def hunt() -> None:
94
113
  @click.option("--tactic", multiple=True, help="MITRE tactics (can specify multiple)")
95
114
  @click.option("--platform", multiple=True, help="Target platforms (can specify multiple)")
96
115
  @click.option("--data-source", multiple=True, help="Data sources (can specify multiple)")
116
+ @click.option("--test", is_flag=True, help="Create as test hunt (hunts/test/...) instead of production")
97
117
  @click.option("--non-interactive", is_flag=True, help="Skip interactive prompts")
98
118
  @click.option("--hypothesis", help="Full hypothesis statement")
99
119
  @click.option("--threat-context", help="Threat intel or context motivating the hunt")
@@ -109,6 +129,7 @@ def new(
109
129
  tactic: Tuple[str, ...],
110
130
  platform: Tuple[str, ...],
111
131
  data_source: Tuple[str, ...],
132
+ test: bool,
112
133
  non_interactive: bool,
113
134
  hypothesis: Optional[str],
114
135
  threat_context: Optional[str],
@@ -171,7 +192,23 @@ def new(
171
192
 
172
193
  # Validate research document if provided
173
194
  if research:
195
+ # Validate research ID format
196
+ if not validate_research_id(research):
197
+ console.print(f"[red]Error: Invalid research ID format: {research}[/red]")
198
+ console.print("[yellow]Expected format: R-0001[/yellow]")
199
+ return
200
+
174
201
  research_file = Path("research") / f"{research}.md"
202
+
203
+ # Validate path is within research directory
204
+ try:
205
+ if not research_file.resolve().is_relative_to(Path("research").resolve()):
206
+ console.print("[red]Error: Invalid research path[/red]")
207
+ return
208
+ except (ValueError, OSError):
209
+ console.print("[red]Error: Invalid research path[/red]")
210
+ return
211
+
175
212
  if not research_file.exists():
176
213
  console.print(f"[yellow]Warning: Research document {research} not found at {research_file}[/yellow]")
177
214
  console.print("[yellow]Hunt will still be created, but research link may be broken.[/yellow]\n")
@@ -233,9 +270,19 @@ def new(
233
270
  spawned_from=research,
234
271
  )
235
272
 
236
- # Write hunt file
237
- hunt_file = Path("hunts") / f"{hunt_id}.md"
238
- hunt_file.parent.mkdir(exist_ok=True)
273
+ # Write hunt file using hierarchical directory structure
274
+ hunt_dir = get_hunt_directory(is_test=test)
275
+ hunt_dir.mkdir(parents=True, exist_ok=True)
276
+ hunt_file = hunt_dir / f"{hunt_id}.md"
277
+
278
+ # Validate path is within hunts directory
279
+ try:
280
+ if not hunt_file.resolve().is_relative_to(Path("hunts").resolve()):
281
+ console.print("[red]Error: Invalid hunt file path[/red]")
282
+ return
283
+ except (ValueError, OSError):
284
+ console.print("[red]Error: Invalid hunt file path[/red]")
285
+ return
239
286
 
240
287
  with open(hunt_file, "w", encoding="utf-8") as f:
241
288
  f.write(hunt_content)
@@ -370,10 +417,31 @@ def validate(hunt_id: str) -> None:
370
417
  • Verify hunt files are AI-assistant readable
371
418
  """
372
419
  if hunt_id:
373
- # Validate specific hunt
374
- hunt_file = Path("hunts") / f"{hunt_id}.md"
420
+ # Validate hunt ID format
421
+ if not validate_hunt_id(hunt_id):
422
+ console.print(f"[red]Error: Invalid hunt ID format: {hunt_id}[/red]")
423
+ console.print("[yellow]Expected format: H-0001[/yellow]")
424
+ return
425
+
426
+ # Validate specific hunt - search recursively for backward compatibility
427
+ hunts_dir = Path("hunts")
428
+ hunt_file = hunts_dir / f"{hunt_id}.md"
429
+
430
+ # If not found in flat structure, search recursively
375
431
  if not hunt_file.exists():
376
- console.print(f"[red]Hunt not found: {hunt_id}[/red]")
432
+ matching_files = list(hunts_dir.rglob(f"{hunt_id}.md"))
433
+ if not matching_files:
434
+ console.print(f"[red]Hunt not found: {hunt_id}[/red]")
435
+ return
436
+ hunt_file = matching_files[0] # Use first match
437
+
438
+ # Validate path is within hunts directory
439
+ try:
440
+ if not hunt_file.resolve().is_relative_to(hunts_dir.resolve()):
441
+ console.print("[red]Error: Invalid hunt file path[/red]")
442
+ return
443
+ except (ValueError, OSError):
444
+ console.print("[red]Error: Invalid hunt file path[/red]")
377
445
  return
378
446
 
379
447
  _validate_single_hunt(hunt_file)
@@ -386,7 +454,7 @@ def validate(hunt_id: str) -> None:
386
454
  console.print("[yellow]No hunts directory found.[/yellow]")
387
455
  return
388
456
 
389
- hunt_files = list(hunts_dir.glob("*.md"))
457
+ hunt_files = list(hunts_dir.rglob("*.md"))
390
458
 
391
459
  if not hunt_files:
392
460
  console.print("[yellow]No hunt files found.[/yellow]")
@@ -13,6 +13,7 @@ from rich.prompt import Prompt
13
13
  from rich.table import Table
14
14
 
15
15
  from athf.core.investigation_parser import get_all_investigations, get_next_investigation_id, validate_investigation_file
16
+ from athf.utils.validation import validate_investigation_id
16
17
 
17
18
  console = Console()
18
19
 
@@ -183,6 +184,15 @@ def new(
183
184
  # Write investigation file
184
185
  investigation_file = investigations_dir / f"{investigation_id}.md"
185
186
 
187
+ # Validate path is within investigations directory
188
+ try:
189
+ if not investigation_file.resolve().is_relative_to(investigations_dir.resolve()):
190
+ console.print("[red]Error: Invalid investigation file path[/red]")
191
+ return
192
+ except (ValueError, OSError):
193
+ console.print("[red]Error: Invalid investigation file path[/red]")
194
+ return
195
+
186
196
  with open(investigation_file, "w", encoding="utf-8") as f:
187
197
  f.write(investigation_content)
188
198
 
@@ -542,9 +552,24 @@ def validate(investigation_id: str) -> None:
542
552
  # Validate after editing
543
553
  athf investigate validate I-0001
544
554
  """
555
+ # Validate investigation ID format
556
+ if not validate_investigation_id(investigation_id):
557
+ console.print(f"[red]Error: Invalid investigation ID format: {investigation_id}[/red]")
558
+ console.print("[yellow]Expected format: I-0001[/yellow]")
559
+ return
560
+
545
561
  investigations_dir = Path("investigations")
546
562
  investigation_file = investigations_dir / f"{investigation_id}.md"
547
563
 
564
+ # Validate path is within investigations directory
565
+ try:
566
+ if not investigation_file.resolve().is_relative_to(investigations_dir.resolve()):
567
+ console.print("[red]Error: Invalid investigation file path[/red]")
568
+ return
569
+ except (ValueError, OSError):
570
+ console.print("[red]Error: Invalid investigation file path[/red]")
571
+ return
572
+
548
573
  if not investigation_file.exists():
549
574
  console.print(f"[red]Error: Investigation file not found: {investigation_file}[/red]")
550
575
  return
@@ -606,10 +631,25 @@ def promote(
606
631
 
607
632
  console.print("\n[bold cyan]🔄 Promoting investigation to hunt[/bold cyan]\n")
608
633
 
634
+ # Validate investigation ID format
635
+ if not validate_investigation_id(investigation_id):
636
+ console.print(f"[red]Error: Invalid investigation ID format: {investigation_id}[/red]")
637
+ console.print("[yellow]Expected format: I-0001[/yellow]")
638
+ return
639
+
609
640
  # Check investigation file exists
610
641
  investigations_dir = Path("investigations")
611
642
  investigation_file = investigations_dir / f"{investigation_id}.md"
612
643
 
644
+ # Validate path is within investigations directory
645
+ try:
646
+ if not investigation_file.resolve().is_relative_to(investigations_dir.resolve()):
647
+ console.print("[red]Error: Invalid investigation file path[/red]")
648
+ return
649
+ except (ValueError, OSError):
650
+ console.print("[red]Error: Invalid investigation file path[/red]")
651
+ return
652
+
613
653
  if not investigation_file.exists():
614
654
  console.print(f"[red]Error: Investigation file not found: {investigation_file}[/red]")
615
655
  return
@@ -724,18 +764,60 @@ def promote(
724
764
  hunts_dir.mkdir(exist_ok=True)
725
765
  hunt_file = hunts_dir / f"{hunt_id}.md"
726
766
 
767
+ # Validate path is within hunts directory
768
+ try:
769
+ if not hunt_file.resolve().is_relative_to(hunts_dir.resolve()):
770
+ console.print("[red]Error: Invalid hunt file path[/red]")
771
+ return
772
+ except (ValueError, OSError):
773
+ console.print("[red]Error: Invalid hunt file path[/red]")
774
+ return
775
+
727
776
  with open(hunt_file, "w", encoding="utf-8") as f:
728
777
  f.write(hunt_content)
729
778
 
730
779
  console.print(f"\n[bold green]✅ Promoted {investigation_id} to {hunt_id}[/bold green]")
731
780
 
732
- # Update investigation with promotion note
781
+ # Update investigation's related_hunts field in frontmatter
782
+ try:
783
+ # Read the investigation file
784
+ with open(investigation_file, "r", encoding="utf-8") as f:
785
+ content = f.read()
786
+
787
+ # Split frontmatter and body
788
+ parts = content.split("---")
789
+ if len(parts) >= 3:
790
+ frontmatter_yaml = parts[1]
791
+ body = "---".join(parts[2:])
792
+
793
+ # Parse and update frontmatter
794
+ frontmatter = yaml.safe_load(frontmatter_yaml)
795
+ if "related_hunts" not in frontmatter or frontmatter["related_hunts"] is None:
796
+ frontmatter["related_hunts"] = []
797
+
798
+ # Add new hunt ID if not already present
799
+ if hunt_id not in frontmatter["related_hunts"]:
800
+ frontmatter["related_hunts"].append(hunt_id)
801
+
802
+ # Rebuild file with updated frontmatter
803
+ updated_yaml = yaml.dump(frontmatter, default_flow_style=False, sort_keys=False)
804
+ updated_content = f"---\n{updated_yaml}---{body}"
805
+
806
+ # Write back to file
807
+ with open(investigation_file, "w", encoding="utf-8") as f:
808
+ f.write(updated_content)
809
+
810
+ console.print(f"[dim]Updated {investigation_file} with hunt reference in related_hunts[/dim]")
811
+ except Exception as e:
812
+ console.print(f"[yellow]Warning: Could not update investigation frontmatter: {e}[/yellow]")
813
+
814
+ # Add promotion note to the end of the document
733
815
  promotion_note = f"\n\n---\n\n**Promoted to Hunt:** {hunt_id} on {today}\n"
734
816
 
735
817
  with open(investigation_file, "a", encoding="utf-8") as f:
736
818
  f.write(promotion_note)
737
819
 
738
- console.print(f"[dim]Updated {investigation_file} with promotion note[/dim]")
820
+ console.print(f"[dim]Added promotion note to {investigation_file}[/dim]")
739
821
 
740
822
  console.print("\n[bold]Next steps:[/bold]")
741
823
  console.print(f" 1. Edit [cyan]{hunt_file}[/cyan] to refine hunt hypothesis")
@@ -6,6 +6,7 @@ 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
9
+ from athf.utils.validation import validate_hunt_id
9
10
 
10
11
 
11
12
  class HuntManager:
@@ -42,8 +43,8 @@ class HuntManager:
42
43
  """
43
44
  hunts = []
44
45
 
45
- # Find all hunt files
46
- hunt_files = sorted(self.hunts_dir.glob("*.md"))
46
+ # Find all hunt files recursively (supports both flat and hierarchical structure)
47
+ hunt_files = sorted(self.hunts_dir.rglob("*.md"))
47
48
 
48
49
  for hunt_file in hunt_files:
49
50
  try:
@@ -102,9 +103,26 @@ class HuntManager:
102
103
  Returns:
103
104
  Hunt data dict or None if not found
104
105
  """
106
+ # Validate hunt ID format and prevent path traversal
107
+ if not validate_hunt_id(hunt_id):
108
+ return None
109
+
110
+ # First try flat structure for backward compatibility
105
111
  hunt_file = self.hunts_dir / f"{hunt_id}.md"
106
112
 
113
+ # If not found in flat structure, search recursively
107
114
  if not hunt_file.exists():
115
+ # Search recursively for the hunt file
116
+ matching_files = list(self.hunts_dir.rglob(f"{hunt_id}.md"))
117
+ if not matching_files:
118
+ return None
119
+ hunt_file = matching_files[0] # Use first match
120
+
121
+ # Validate path is within hunts directory
122
+ try:
123
+ if not hunt_file.resolve().is_relative_to(self.hunts_dir.resolve()):
124
+ return None
125
+ except (ValueError, OSError):
108
126
  return None
109
127
 
110
128
  return parse_hunt_file(hunt_file)
@@ -157,7 +175,7 @@ class HuntManager:
157
175
  # Exclude documentation files
158
176
  exclude_files = {"README.md", "FORMAT_GUIDELINES.md"}
159
177
 
160
- for hunt_file in self.hunts_dir.glob("*.md"):
178
+ for hunt_file in self.hunts_dir.rglob("*.md"):
161
179
  # Skip documentation files
162
180
  if hunt_file.name in exclude_files:
163
181
  continue
@@ -7,6 +7,8 @@ from typing import Any, Dict, List, Optional
7
7
 
8
8
  import yaml
9
9
 
10
+ from athf.utils.validation import validate_research_id
11
+
10
12
 
11
13
  class ResearchParser:
12
14
  """Parser for research files (YAML frontmatter + markdown)."""
@@ -240,15 +242,34 @@ class ResearchManager:
240
242
  Returns:
241
243
  Research data dict or None if not found
242
244
  """
245
+ # Validate research ID format and prevent path traversal
246
+ if not validate_research_id(research_id):
247
+ return None
248
+
243
249
  # Try direct file
244
250
  research_file = self.research_dir / f"{research_id}.md"
251
+
252
+ # Validate path is within research directory
253
+ try:
254
+ if not research_file.resolve().is_relative_to(self.research_dir.resolve()):
255
+ return None
256
+ except (ValueError, OSError):
257
+ return None
258
+
245
259
  if research_file.exists():
246
260
  return parse_research_file(research_file)
247
261
 
248
262
  # Try nested search
249
263
  research_files = list(self.research_dir.rglob(f"{research_id}.md"))
250
264
  if research_files:
251
- return parse_research_file(research_files[0])
265
+ # Validate nested file is also within research directory
266
+ nested_file = research_files[0]
267
+ try:
268
+ if not nested_file.resolve().is_relative_to(self.research_dir.resolve()):
269
+ return None
270
+ except (ValueError, OSError):
271
+ return None
272
+ return parse_research_file(nested_file)
252
273
 
253
274
  return None
254
275
 
@@ -368,6 +389,14 @@ class ResearchManager:
368
389
 
369
390
  # Write file
370
391
  file_path = self.research_dir / f"{research_id}.md"
392
+
393
+ # Validate path is within research directory
394
+ try:
395
+ if not file_path.resolve().is_relative_to(self.research_dir.resolve()):
396
+ raise ValueError("Invalid research file path")
397
+ except (ValueError, OSError) as e:
398
+ raise ValueError(f"Invalid research file path: {e}") from e
399
+
371
400
  with open(file_path, "w", encoding="utf-8") as f:
372
401
  f.write(file_content)
373
402
 
@@ -572,7 +572,7 @@ After installation:
572
572
  2. **Read the getting started guide**: [getting-started.md](getting-started.md)
573
573
  3. **Review the CLI reference**: [CLI_REFERENCE.md](CLI_REFERENCE.md)
574
574
  4. **Create your first hunt**: `athf hunt new`
575
- 5. **Explore example hunts**: [../hunts/H-0001.md](../hunts/H-0001.md)
575
+ 5. **Explore example hunts**: [../hunts/production/2026/Q1/H-0001.md](../hunts/production/2026/Q1/H-0001.md)
576
576
 
577
577
  ---
578
578
 
@@ -120,7 +120,7 @@ athf hunt list
120
120
 
121
121
  ### Example Structure
122
122
 
123
- See [hunts/H-0001.md](../hunts/H-0001.md) for a complete example. Here's a simplified structure:
123
+ See [hunts/H-0001.md](../hunts/production/2026/Q1/H-0001.md) for a complete example. Here's a simplified structure:
124
124
 
125
125
  ```markdown
126
126
  # H-0001: SSH Brute Force Detection
@@ -104,9 +104,9 @@ Next iteration: expand to include remote registry and PSExec telemetry for broad
104
104
  ```
105
105
 
106
106
  **See full hunt examples:**
107
- - [H-0001: macOS Information Stealer Detection](../hunts/H-0001.md) - Complete hunt with YAML frontmatter, detailed LOCK sections, query evolution, and results
108
- - [H-0002: Linux Crontab Persistence Detection](../hunts/H-0002.md) - Multi-query approach with behavioral analysis
109
- - [H-0003: AWS Lambda Persistence Detection](../hunts/H-0003.md) - Cloud hunting with CloudTrail correlation
107
+ - [H-0001: macOS Information Stealer Detection](../hunts/production/2026/Q1/H-0001.md) - Complete hunt with YAML frontmatter, detailed LOCK sections, query evolution, and results
108
+ - [H-0002: Linux Crontab Persistence Detection](../hunts/production/2026/Q1/H-0002.md) - Multi-query approach with behavioral analysis
109
+ - [H-0003: AWS Lambda Persistence Detection](../hunts/production/2026/Q1/H-0003.md) - Cloud hunting with CloudTrail correlation
110
110
  - [Hunt Showcase](../../../SHOWCASE.md) - Side-by-side comparison of all three hunts
111
111
 
112
112
  ## Best Practices
@@ -504,4 +504,4 @@ The result is a condensed, practical template that guides hunters from hypothesi
504
504
 
505
505
  ## Example Reference
506
506
 
507
- See [H-0001.md](H-0001.md), [H-0002.md](H-0002.md), and [H-0003.md](H-0003.md) for complete hunt examples.
507
+ See [H-0001.md](production/2026/Q1/H-0001.md), [H-0002.md](production/2026/Q1/H-0002.md), and [H-0003.md](production/2026/Q1/H-0003.md) for complete hunt examples.
@@ -225,7 +225,7 @@ Claude:
225
225
 
226
226
  ## Example Hunts
227
227
 
228
- - [H-0001.md](H-0001.md) - macOS Data Collection via AppleScript Detection (T1005, T1059.002, T1555.003)
229
- - [H-0002.md](H-0002.md) - Linux Crontab Persistence Detection (T1053.003)
230
- - [H-0003.md](H-0003.md) - AWS Lambda Persistence Detection (T1546.004, T1098)
228
+ - [H-0001.md](production/2026/Q1/H-0001.md) - macOS Data Collection via AppleScript Detection (T1005, T1059.002, T1555.003)
229
+ - [H-0002.md](production/2026/Q1/H-0002.md) - Linux Crontab Persistence Detection (T1053.003)
230
+ - [H-0003.md](production/2026/Q1/H-0003.md) - AWS Lambda Persistence Detection (T1546.004, T1098)
231
231
  - [FORMAT_GUIDELINES.md](FORMAT_GUIDELINES.md) - Template structure reference
@@ -142,7 +142,7 @@ AI: [Searches hunts/, reads environment.md, generates context-aware hypothesis]
142
142
 
143
143
  ### After Level 2 Workflows
144
144
 
145
- 1. See real examples in [hunts/H-0001.md](../hunts/H-0001.md) and [hunts/H-0002.md](../hunts/H-0002.md)
145
+ 1. See real examples in [hunts/H-0001.md](../hunts/production/2026/Q1/H-0001.md) and [hunts/H-0002.md](../hunts/production/2026/Q1/H-0002.md)
146
146
  2. Review format guidelines in [hunts/FORMAT_GUIDELINES.md](../hunts/FORMAT_GUIDELINES.md)
147
147
  3. Consider Level 3 (MCP integrations) in [integrations/](../integrations/)
148
148
 
@@ -575,7 +575,7 @@ Before finalizing any AI-generated content:
575
575
 
576
576
  - **Basic Prompts:** [basic-prompts.md](basic-prompts.md) for Level 0-1
577
577
  - **Hunt Template:** [../templates/HUNT_LOCK.md](../templates/HUNT_LOCK.md)
578
- - **Real Examples:** [../hunts/H-0001.md](../hunts/H-0001.md), [../hunts/H-0002.md](../hunts/H-0002.md)
578
+ - **Real Examples:** [../hunts/H-0001.md](../hunts/production/2026/Q1/H-0001.md), [../hunts/H-0002.md](../hunts/production/2026/Q1/H-0002.md)
579
579
  - **Repository Context:** [AGENTS.md](../../../AGENTS.md)
580
580
 
581
581
  **Remember: AI augments, doesn't replace. Always validate, always learn, always improve.**
@@ -302,7 +302,7 @@ Once you're comfortable with these basic prompts:
302
302
 
303
303
  1. **Build your hunt repository** - Document hunts using [templates/HUNT_LOCK.md](../templates/HUNT_LOCK.md)
304
304
  2. **Progress to Level 2** - Use [ai-workflow.md](ai-workflow.md) for AI tools with repository access
305
- 3. **See real examples** - Review [H-0001.md](../hunts/H-0001.md) and [H-0002.md](../hunts/H-0002.md)
305
+ 3. **See real examples** - Review [H-0001.md](../hunts/production/2026/Q1/H-0001.md) and [H-0002.md](../hunts/production/2026/Q1/H-0002.md)
306
306
 
307
307
  ---
308
308
 
@@ -0,0 +1,72 @@
1
+ """Plugin system for ATHF extensions."""
2
+
3
+ import sys
4
+ from typing import Any, Dict, Optional, Type
5
+
6
+ from click import Command
7
+
8
+ # Handle importlib.metadata API changes across Python versions
9
+ if sys.version_info >= (3, 10):
10
+ from importlib.metadata import entry_points
11
+ else:
12
+ # Python 3.8-3.9: use importlib_metadata backport API
13
+ try:
14
+ from importlib.metadata import entry_points
15
+ except ImportError:
16
+ from importlib_metadata import entry_points # type: ignore
17
+
18
+
19
+ class PluginRegistry:
20
+ """Central registry for ATHF plugins."""
21
+
22
+ _agents: Dict[str, Type[Any]] = {}
23
+ _commands: Dict[str, Command] = {}
24
+
25
+ @classmethod
26
+ def register_agent(cls, name: str, agent_class: Type[Any]) -> None:
27
+ """Register an agent plugin."""
28
+ cls._agents[name] = agent_class
29
+
30
+ @classmethod
31
+ def register_command(cls, name: str, command: Command) -> None:
32
+ """Register a CLI command plugin."""
33
+ cls._commands[name] = command
34
+
35
+ @classmethod
36
+ def get_agent(cls, name: str) -> Optional[Type[Any]]:
37
+ """Get registered agent by name."""
38
+ return cls._agents.get(name)
39
+
40
+ @classmethod
41
+ def get_command(cls, name: str) -> Optional[Command]:
42
+ """Get registered command by name."""
43
+ return cls._commands.get(name)
44
+
45
+ @classmethod
46
+ def load_plugins(cls) -> None:
47
+ """Auto-discover and load all installed plugins."""
48
+ try:
49
+ # Python 3.10+ uses group= parameter, 3.8-3.9 uses dict-like access
50
+ if sys.version_info >= (3, 10):
51
+ eps = entry_points(group="athf.commands")
52
+ else:
53
+ eps = entry_points().get("athf.commands", [])
54
+
55
+ for ep in eps:
56
+ command = ep.load()
57
+ cls.register_command(ep.name, command)
58
+ except Exception:
59
+ pass # No plugins installed yet
60
+
61
+ try:
62
+ # Python 3.10+ uses group= parameter, 3.8-3.9 uses dict-like access
63
+ if sys.version_info >= (3, 10):
64
+ eps = entry_points(group="athf.agents")
65
+ else:
66
+ eps = entry_points().get("athf.agents", [])
67
+
68
+ for ep in eps:
69
+ agent = ep.load()
70
+ cls.register_agent(ep.name, agent)
71
+ except Exception:
72
+ pass # No plugins installed yet
@@ -0,0 +1,170 @@
1
+ """Input validation utilities to prevent path traversal and injection attacks."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+
8
+ def validate_hunt_id(hunt_id: str) -> bool:
9
+ """Validate hunt ID format and prevent path traversal.
10
+
11
+ Args:
12
+ hunt_id: Hunt ID to validate (e.g., H-0001)
13
+
14
+ Returns:
15
+ True if valid, False otherwise
16
+
17
+ Examples:
18
+ >>> validate_hunt_id("H-0001")
19
+ True
20
+ >>> validate_hunt_id("H-9999")
21
+ True
22
+ >>> validate_hunt_id("../../etc/passwd")
23
+ False
24
+ >>> validate_hunt_id("H-0001/../secrets.txt")
25
+ False
26
+ """
27
+ if not hunt_id or not isinstance(hunt_id, str):
28
+ return False
29
+
30
+ # Check format: single letter(s) + dash + 4 digits
31
+ if not re.match(r"^[A-Z]+-\d{4}$", hunt_id):
32
+ return False
33
+
34
+ # Prevent path traversal
35
+ if ".." in hunt_id or "/" in hunt_id or "\\" in hunt_id:
36
+ return False
37
+
38
+ return True
39
+
40
+
41
+ def validate_investigation_id(investigation_id: str) -> bool:
42
+ """Validate investigation ID format and prevent path traversal.
43
+
44
+ Args:
45
+ investigation_id: Investigation ID to validate (e.g., I-0001)
46
+
47
+ Returns:
48
+ True if valid, False otherwise
49
+
50
+ Examples:
51
+ >>> validate_investigation_id("I-0001")
52
+ True
53
+ >>> validate_investigation_id("../../../secrets")
54
+ False
55
+ """
56
+ if not investigation_id or not isinstance(investigation_id, str):
57
+ return False
58
+
59
+ # Check format: single letter + dash + 4 digits
60
+ if not re.match(r"^[A-Z]+-\d{4}$", investigation_id):
61
+ return False
62
+
63
+ # Prevent path traversal
64
+ if ".." in investigation_id or "/" in investigation_id or "\\" in investigation_id:
65
+ return False
66
+
67
+ return True
68
+
69
+
70
+ def validate_research_id(research_id: str) -> bool:
71
+ """Validate research ID format and prevent path traversal.
72
+
73
+ Args:
74
+ research_id: Research ID to validate (e.g., R-0001)
75
+
76
+ Returns:
77
+ True if valid, False otherwise
78
+
79
+ Examples:
80
+ >>> validate_research_id("R-0001")
81
+ True
82
+ >>> validate_research_id("R-0001/../../secrets")
83
+ False
84
+ """
85
+ if not research_id or not isinstance(research_id, str):
86
+ return False
87
+
88
+ # Check format: single letter + dash + 4 digits
89
+ if not re.match(r"^[A-Z]+-\d{4}$", research_id):
90
+ return False
91
+
92
+ # Prevent path traversal
93
+ if ".." in research_id or "/" in research_id or "\\" in research_id:
94
+ return False
95
+
96
+ return True
97
+
98
+
99
+ def validate_file_path(file_path: Path, base_dir: Path) -> bool:
100
+ """Validate that resolved file path is within base directory.
101
+
102
+ Args:
103
+ file_path: File path to validate
104
+ base_dir: Base directory that file must be within
105
+
106
+ Returns:
107
+ True if file is within base directory, False otherwise
108
+
109
+ Examples:
110
+ >>> base = Path("/app/hunts")
111
+ >>> validate_file_path(Path("/app/hunts/H-0001.md"), base)
112
+ True
113
+ >>> validate_file_path(Path("/etc/passwd"), base)
114
+ False
115
+ """
116
+ try:
117
+ # Resolve to absolute paths
118
+ resolved_file = file_path.resolve()
119
+ resolved_base = base_dir.resolve()
120
+
121
+ # Check if file is relative to base directory
122
+ return resolved_file.is_relative_to(resolved_base)
123
+ except (ValueError, OSError):
124
+ return False
125
+
126
+
127
+ def safe_path_join(base_dir: Path, id_value: str, extension: str = ".md") -> Optional[Path]:
128
+ """Safely join base directory with ID to create file path.
129
+
130
+ Validates ID format and ensures resulting path is within base directory.
131
+
132
+ Args:
133
+ base_dir: Base directory (e.g., Path("hunts"))
134
+ id_value: ID value (e.g., "H-0001")
135
+ extension: File extension (default: .md)
136
+
137
+ Returns:
138
+ Validated Path object or None if validation fails
139
+
140
+ Examples:
141
+ >>> safe_path_join(Path("hunts"), "H-0001")
142
+ Path('hunts/H-0001.md')
143
+ >>> safe_path_join(Path("hunts"), "../../etc/passwd")
144
+ None
145
+ """
146
+ # Validate ID format based on first letter
147
+ if id_value.startswith("H-"):
148
+ if not validate_hunt_id(id_value):
149
+ return None
150
+ elif id_value.startswith("I-"):
151
+ if not validate_investigation_id(id_value):
152
+ return None
153
+ elif id_value.startswith("R-"):
154
+ if not validate_research_id(id_value):
155
+ return None
156
+ else:
157
+ # Unknown prefix - validate generic format
158
+ if not re.match(r"^[A-Z]+-\d{4}$", id_value):
159
+ return None
160
+ if ".." in id_value or "/" in id_value or "\\" in id_value:
161
+ return None
162
+
163
+ # Construct path
164
+ file_path = base_dir / f"{id_value}{extension}"
165
+
166
+ # Validate path is within base directory
167
+ if not validate_file_path(file_path, base_dir):
168
+ return None
169
+
170
+ return file_path
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-threat-hunting-framework"
7
- version = "0.5.1"
7
+ version = "0.7.0"
8
8
  description = "Agentic Threat Hunting Framework - Memory and AI for threat hunters"
9
9
  readme = {file = "README.md", content-type = "text/markdown"}
10
10
  requires-python = ">=3.8"
@@ -1,48 +0,0 @@
1
- """Plugin system for ATHF extensions."""
2
- from typing import Dict, Type, Callable
3
- import importlib.metadata
4
- from click import Command
5
-
6
-
7
- class PluginRegistry:
8
- """Central registry for ATHF plugins."""
9
-
10
- _agents: Dict[str, Type] = {}
11
- _commands: Dict[str, Command] = {}
12
-
13
- @classmethod
14
- def register_agent(cls, name: str, agent_class: Type) -> None:
15
- """Register an agent plugin."""
16
- cls._agents[name] = agent_class
17
-
18
- @classmethod
19
- def register_command(cls, name: str, command: Command) -> None:
20
- """Register a CLI command plugin."""
21
- cls._commands[name] = command
22
-
23
- @classmethod
24
- def get_agent(cls, name: str) -> Type:
25
- """Get registered agent by name."""
26
- return cls._agents.get(name)
27
-
28
- @classmethod
29
- def get_command(cls, name: str) -> Command:
30
- """Get registered command by name."""
31
- return cls._commands.get(name)
32
-
33
- @classmethod
34
- def load_plugins(cls) -> None:
35
- """Auto-discover and load all installed plugins."""
36
- try:
37
- for ep in importlib.metadata.entry_points(group='athf.commands'):
38
- command = ep.load()
39
- cls.register_command(ep.name, command)
40
- except Exception:
41
- pass # No plugins installed yet
42
-
43
- try:
44
- for ep in importlib.metadata.entry_points(group='athf.agents'):
45
- agent = ep.load()
46
- cls.register_agent(ep.name, agent)
47
- except Exception:
48
- pass # No plugins installed yet