agentic-threat-hunting-framework 0.2.4__py3-none-any.whl → 0.3.1__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.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/METADATA +38 -40
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/RECORD +24 -15
- athf/__version__.py +1 -1
- athf/agents/__init__.py +14 -0
- athf/agents/base.py +141 -0
- athf/agents/llm/__init__.py +27 -0
- athf/agents/llm/hunt_researcher.py +762 -0
- athf/agents/llm/hypothesis_generator.py +238 -0
- athf/cli.py +6 -1
- athf/commands/__init__.py +4 -0
- athf/commands/agent.py +452 -0
- athf/commands/context.py +6 -9
- athf/commands/env.py +2 -2
- athf/commands/hunt.py +3 -1
- athf/commands/research.py +530 -0
- athf/commands/similar.py +3 -3
- athf/core/research_manager.py +419 -0
- athf/core/web_search.py +340 -0
- athf/data/__init__.py +6 -1
- athf/data/docs/CHANGELOG.md +23 -0
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/WHEEL +0 -0
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/entry_points.txt +0 -0
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {agentic_threat_hunting_framework-0.2.4.dist-info → agentic_threat_hunting_framework-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Hypothesis generator agent - LLM-powered hypothesis generation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from athf.agents.base import AgentResult, LLMAgent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class HypothesisGenerationInput:
|
|
13
|
+
"""Input for hypothesis generation."""
|
|
14
|
+
|
|
15
|
+
threat_intel: str # User-provided threat context
|
|
16
|
+
past_hunts: List[Dict[str, Any]] # Similar past hunts for context
|
|
17
|
+
environment: Dict[str, Any] # Data sources, platforms, etc.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class HypothesisGenerationOutput:
|
|
22
|
+
"""Output from hypothesis generation."""
|
|
23
|
+
|
|
24
|
+
hypothesis: str
|
|
25
|
+
justification: str
|
|
26
|
+
mitre_techniques: List[str]
|
|
27
|
+
data_sources: List[str]
|
|
28
|
+
expected_observables: List[str]
|
|
29
|
+
known_false_positives: List[str]
|
|
30
|
+
time_range_suggestion: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HypothesisGeneratorAgent(LLMAgent[HypothesisGenerationInput, HypothesisGenerationOutput]):
|
|
34
|
+
"""Generates hunt hypotheses using Claude.
|
|
35
|
+
|
|
36
|
+
Uses Claude API for context-aware hypothesis generation with fallback
|
|
37
|
+
to template-based generation when LLM is disabled.
|
|
38
|
+
|
|
39
|
+
Features:
|
|
40
|
+
- TTP-focused hypothesis generation
|
|
41
|
+
- MITRE ATT&CK technique mapping
|
|
42
|
+
- Data source validation
|
|
43
|
+
- False positive prediction
|
|
44
|
+
- Cost tracking
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def execute(self, input_data: HypothesisGenerationInput) -> AgentResult[HypothesisGenerationOutput]:
|
|
48
|
+
"""Generate hypothesis using LLM.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
input_data: Hypothesis generation input
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
AgentResult with hypothesis output or error
|
|
55
|
+
"""
|
|
56
|
+
if not self.llm_enabled:
|
|
57
|
+
# Fallback to template-based generation
|
|
58
|
+
return self._template_generate(input_data)
|
|
59
|
+
|
|
60
|
+
# Use AWS Bedrock Claude API
|
|
61
|
+
try:
|
|
62
|
+
client = self._get_llm_client()
|
|
63
|
+
|
|
64
|
+
prompt = self._build_prompt(input_data)
|
|
65
|
+
|
|
66
|
+
# Bedrock model ID - using cross-region inference profile for Claude Sonnet 4.5
|
|
67
|
+
# Cross-region inference profiles provide better availability and automatic failover
|
|
68
|
+
model_id = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
|
69
|
+
|
|
70
|
+
# Prepare request body for Bedrock
|
|
71
|
+
request_body = {
|
|
72
|
+
"anthropic_version": "bedrock-2023-05-31",
|
|
73
|
+
"max_tokens": 4096,
|
|
74
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Invoke model via Bedrock (with timing)
|
|
78
|
+
start_time = time.time()
|
|
79
|
+
response = client.invoke_model(modelId=model_id, body=json.dumps(request_body))
|
|
80
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
81
|
+
|
|
82
|
+
# Parse Bedrock response
|
|
83
|
+
response_body = json.loads(response["body"].read())
|
|
84
|
+
|
|
85
|
+
# Extract text from response
|
|
86
|
+
output_text = response_body["content"][0]["text"]
|
|
87
|
+
|
|
88
|
+
# Try to extract JSON from markdown code blocks if present
|
|
89
|
+
if "```json" in output_text:
|
|
90
|
+
json_start = output_text.find("```json") + 7
|
|
91
|
+
json_end = output_text.find("```", json_start)
|
|
92
|
+
output_text = output_text[json_start:json_end].strip()
|
|
93
|
+
elif "```" in output_text:
|
|
94
|
+
json_start = output_text.find("```") + 3
|
|
95
|
+
json_end = output_text.find("```", json_start)
|
|
96
|
+
output_text = output_text[json_start:json_end].strip()
|
|
97
|
+
|
|
98
|
+
# Parse JSON with better error handling
|
|
99
|
+
try:
|
|
100
|
+
output_data = json.loads(output_text)
|
|
101
|
+
except json.JSONDecodeError as e:
|
|
102
|
+
# If JSON parsing fails, log the actual response for debugging
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Failed to parse JSON response from Claude. "
|
|
105
|
+
f"Error: {e}. "
|
|
106
|
+
f"Response text (first 1500 chars): {output_text[:1500]}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
output = HypothesisGenerationOutput(**output_data)
|
|
110
|
+
|
|
111
|
+
# Extract usage metrics from Bedrock response
|
|
112
|
+
usage = response_body.get("usage", {})
|
|
113
|
+
input_tokens = usage.get("input_tokens", 0)
|
|
114
|
+
output_tokens = usage.get("output_tokens", 0)
|
|
115
|
+
cost_usd = self._calculate_cost_bedrock(input_tokens, output_tokens)
|
|
116
|
+
|
|
117
|
+
# Log metrics to centralized tracker
|
|
118
|
+
self._log_llm_metrics(
|
|
119
|
+
agent_name="hypothesis-generator",
|
|
120
|
+
model_id=model_id,
|
|
121
|
+
input_tokens=input_tokens,
|
|
122
|
+
output_tokens=output_tokens,
|
|
123
|
+
cost_usd=cost_usd,
|
|
124
|
+
duration_ms=duration_ms,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return AgentResult(
|
|
128
|
+
success=True,
|
|
129
|
+
data=output,
|
|
130
|
+
error=None,
|
|
131
|
+
warnings=[],
|
|
132
|
+
metadata={
|
|
133
|
+
"llm_model": model_id,
|
|
134
|
+
"prompt_tokens": input_tokens,
|
|
135
|
+
"completion_tokens": output_tokens,
|
|
136
|
+
"cost_usd": cost_usd,
|
|
137
|
+
"duration_ms": duration_ms,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
# Fall back to template generation
|
|
143
|
+
return self._template_generate(input_data, error=str(e))
|
|
144
|
+
|
|
145
|
+
def _build_prompt(self, input_data: HypothesisGenerationInput) -> str:
|
|
146
|
+
"""Build Claude prompt for hypothesis generation.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
input_data: Hypothesis generation input
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Formatted prompt string
|
|
153
|
+
"""
|
|
154
|
+
return f"""You are a threat hunting expert. Generate a hunt hypothesis based on the following:
|
|
155
|
+
|
|
156
|
+
**Threat Intel:**
|
|
157
|
+
{input_data.threat_intel}
|
|
158
|
+
|
|
159
|
+
**Past Similar Hunts:**
|
|
160
|
+
{json.dumps(input_data.past_hunts, indent=2)}
|
|
161
|
+
|
|
162
|
+
**Available Environment:**
|
|
163
|
+
{json.dumps(input_data.environment, indent=2)}
|
|
164
|
+
|
|
165
|
+
Generate a hypothesis following this format:
|
|
166
|
+
- Hypothesis: "Adversaries use [behavior] to [goal] on [target]"
|
|
167
|
+
- Justification: Why this hypothesis is valuable
|
|
168
|
+
- MITRE Techniques: Relevant ATT&CK techniques (e.g., T1003.001)
|
|
169
|
+
- Data Sources: Which data sources to query
|
|
170
|
+
- Expected Observables: What we expect to find
|
|
171
|
+
- Known False Positives: Common benign patterns
|
|
172
|
+
- Time Range: Suggested time window with justification
|
|
173
|
+
|
|
174
|
+
**IMPORTANT:** Return your response as a JSON object matching this schema:
|
|
175
|
+
{{
|
|
176
|
+
"hypothesis": "string",
|
|
177
|
+
"justification": "string",
|
|
178
|
+
"mitre_techniques": ["T1234.001", "T5678.002"],
|
|
179
|
+
"data_sources": ["ClickHouse nocsf_unified_events", "CloudTrail"],
|
|
180
|
+
"expected_observables": ["Process execution", "Network connections"],
|
|
181
|
+
"known_false_positives": ["Legitimate software", "Administrative tools"],
|
|
182
|
+
"time_range_suggestion": "7 days (justification)"
|
|
183
|
+
}}
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def _template_generate(
|
|
187
|
+
self, input_data: HypothesisGenerationInput, error: Optional[str] = None
|
|
188
|
+
) -> AgentResult[HypothesisGenerationOutput]:
|
|
189
|
+
"""Fallback template-based generation (no LLM).
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
input_data: Hypothesis generation input
|
|
193
|
+
error: Optional error message from LLM attempt
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
AgentResult with template-generated hypothesis
|
|
197
|
+
"""
|
|
198
|
+
# Simple template logic
|
|
199
|
+
output = HypothesisGenerationOutput(
|
|
200
|
+
hypothesis=f"Investigate suspicious activity related to: {input_data.threat_intel[:100]}",
|
|
201
|
+
justification="Template-generated hypothesis (LLM disabled or failed)",
|
|
202
|
+
mitre_techniques=[],
|
|
203
|
+
data_sources=["EDR telemetry", "SIEM logs"],
|
|
204
|
+
expected_observables=["Process execution", "Network connections"],
|
|
205
|
+
known_false_positives=["Legitimate software updates", "Administrative tools"],
|
|
206
|
+
time_range_suggestion="7 days (standard baseline)",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
warnings = ["LLM disabled - using template generation"]
|
|
210
|
+
if error:
|
|
211
|
+
warnings.append(f"LLM error: {error}")
|
|
212
|
+
|
|
213
|
+
return AgentResult(
|
|
214
|
+
success=True,
|
|
215
|
+
data=output,
|
|
216
|
+
error=None,
|
|
217
|
+
warnings=warnings,
|
|
218
|
+
metadata={"fallback": True},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def _calculate_cost_bedrock(self, input_tokens: int, output_tokens: int) -> float:
|
|
222
|
+
"""Calculate AWS Bedrock Claude cost.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
input_tokens: Number of input tokens
|
|
226
|
+
output_tokens: Number of output tokens
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Cost in USD
|
|
230
|
+
"""
|
|
231
|
+
# Claude Sonnet 4.5 on Bedrock pricing (as of January 2025)
|
|
232
|
+
input_cost_per_1k = 0.003
|
|
233
|
+
output_cost_per_1k = 0.015
|
|
234
|
+
|
|
235
|
+
input_cost = (input_tokens / 1000) * input_cost_per_1k
|
|
236
|
+
output_cost = (output_tokens / 1000) * output_cost_per_1k
|
|
237
|
+
|
|
238
|
+
return round(input_cost + output_cost, 4)
|
athf/cli.py
CHANGED
|
@@ -6,7 +6,8 @@ import click
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
8
|
from athf.__version__ import __version__
|
|
9
|
-
from athf.commands import context, env, hunt, init, investigate, similar
|
|
9
|
+
from athf.commands import context, env, hunt, init, investigate, research, similar
|
|
10
|
+
from athf.commands.agent import agent
|
|
10
11
|
|
|
11
12
|
console = Console()
|
|
12
13
|
|
|
@@ -80,12 +81,16 @@ def cli() -> None:
|
|
|
80
81
|
cli.add_command(init.init)
|
|
81
82
|
cli.add_command(hunt.hunt)
|
|
82
83
|
cli.add_command(investigate.investigate)
|
|
84
|
+
cli.add_command(research.research)
|
|
83
85
|
|
|
84
86
|
# Phase 1 commands (env, context, similar)
|
|
85
87
|
cli.add_command(env.env)
|
|
86
88
|
cli.add_command(context.context)
|
|
87
89
|
cli.add_command(similar.similar)
|
|
88
90
|
|
|
91
|
+
# Agent commands
|
|
92
|
+
cli.add_command(agent)
|
|
93
|
+
|
|
89
94
|
|
|
90
95
|
@cli.command(hidden=True)
|
|
91
96
|
def wisdom() -> None:
|