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.
@@ -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:
athf/commands/__init__.py CHANGED
@@ -1 +1,5 @@
1
1
  """ATHF CLI commands."""
2
+
3
+ from athf.commands.agent import agent
4
+
5
+ __all__ = ["agent"]