Skip to content

Commit 11f35f6

Browse files
committed
Phase 2: Investigation Copilot Intelligence + TUI Integration
Copilot prompt templates: - gnat/agents/copilot_prompts.py: Organized by investigation phase GATHERING → HYPOTHESIZING → TESTING → CLOSING Candidate questions for each phase, context-aware selection System prompts for copilot and assistant behavior Hypothesis refinement scoring: - gnat/agents/hypothesis_refinement.py: Confidence scoring based on analyst feedback FeedbackType classification (CONFIRMS, CONTRADICTS, REFINES, NEUTRAL) Connector trust weighting (trusted_internal=0.9, semi_trusted=0.6, untrusted=0.3) Evidence accumulation (supporting_evidence_count, contradicting_evidence_count) Refinement report generation for audit trail TUI screens: - gnat/tui/screens/copilot_screen.py: F10 Investigation Copilot modal Multi-turn conversation with streaming responses Status bar (phase, IOC count, confidence) Slash commands: /next, /close, /help - gnat/tui/screens/assistant_screen.py: F11 Live Analyst Assistant modal On-demand helpers: /enrich, /draft, /explain, /search Streaming and batched response support App integration: - Updated gnat/tui/app.py to register F10 and F11 keybindings action_open_copilot(), action_open_assistant() methods Modal screen push/pop with investigation context binding Updated copilot_investigation.py to use copilot_prompts and hypothesis_refinement Next: Phase 3 will focus on Assistant enhancements and batched operations https://claude.ai/code/session_01FUJQyGdWpZSgYkW1Xb95gU
1 parent 8ca770a commit 11f35f6

6 files changed

Lines changed: 968 additions & 26 deletions

File tree

gnat/agents/copilot_investigation.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
from gnat.agents.conversations import ConversationStore, ConversationTurn, ConversationRole, SessionContext
2121
from gnat.agents.llm import LLMClient
2222
from gnat.agents.base import AgentConfig
23+
from gnat.agents.copilot_prompts import (
24+
get_next_question,
25+
get_refinement_prompt,
26+
get_next_step_prompt,
27+
COPILOT_SYSTEM_PROMPT,
28+
)
29+
from gnat.agents.hypothesis_refinement import HypothesisRefinement, HypothesisScore
2330

2431

2532
class CopilotPhase(str, Enum):
@@ -264,47 +271,37 @@ def _build_question_prompt(
264271
user_input: str,
265272
) -> str:
266273
"""Build Claude prompt for clarifying question."""
267-
# Placeholder: real implementation will be in copilot_prompts.py
268-
return f"""
269-
Phase: {phase.value}
270-
Investigation state: {investigation_state}
271-
Analyst said: {user_input}
272-
273-
What is the next clarifying question to narrow scope and build hypothesis?
274-
Keep it concise (1 sentence).
275-
"""
274+
turns = self.store.get_turns(self.conversation_id)
275+
conversation_history = [t.text for t in turns if t.role == ConversationRole.COPILOT]
276+
277+
return get_next_question(
278+
phase=phase.value,
279+
investigation_state=investigation_state,
280+
conversation_history=conversation_history,
281+
)
276282

277283
def _build_refinement_prompt(
278284
self,
279285
hypotheses: List[Dict[str, Any]],
280286
analyst_feedback: str,
281287
) -> str:
282288
"""Build Claude prompt for hypothesis refinement."""
283-
return f"""
284-
Current hypotheses: {hypotheses}
285-
Analyst feedback: {analyst_feedback}
286-
287-
Re-score the hypotheses based on this feedback. Return JSON with updated confidence values.
288-
"""
289+
return get_refinement_prompt(hypotheses, analyst_feedback)
289290

290291
def _build_step_suggestion_prompt(
291292
self,
292293
investigation_state: Dict[str, Any],
293294
turns: List[ConversationTurn],
294295
) -> str:
295296
"""Build Claude prompt to suggest next investigation step."""
296-
return f"""
297-
Investigation state: {investigation_state}
298-
Conversation history: {len(turns)} turns
299-
300-
What is the next recommended action? Choose from:
301-
- Query ThreatQ for campaign overlap
302-
- Run hunt package XYZ
303-
- Check Recorded Future for reputation
304-
- Narrow scope by excluding actors
297+
# TODO: Extract hypotheses from investigation context
298+
hypotheses = []
305299

306-
Recommend one action with reasoning.
307-
"""
300+
return get_next_step_prompt(
301+
investigation_state=investigation_state,
302+
conversation_turns=len(turns),
303+
hypotheses=hypotheses,
304+
)
308305

309306
def _advance_phase(self, current: CopilotPhase, state: Dict[str, Any]) -> CopilotPhase:
310307
"""Determine next phase based on current state."""

gnat/agents/copilot_prompts.py

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 Bill Halpin
3+
"""
4+
gnat.agents.copilot_prompts
5+
=============================
6+
7+
Prompt templates for Investigation Copilot, organized by investigation phase.
8+
Supports prompt caching via claude:cache_control for context reuse.
9+
"""
10+
11+
from enum import Enum
12+
from typing import Dict, List, Any
13+
14+
15+
class QuestionCategory(str, Enum):
16+
"""Types of clarifying questions."""
17+
SCOPE = "scope"
18+
TIMELINE = "timeline"
19+
IMPACT = "impact"
20+
ATTRIBUTION = "attribution"
21+
CONFIDENCE = "confidence"
22+
23+
24+
GATHERING_QUESTIONS = {
25+
QuestionCategory.SCOPE: [
26+
"How many unique IOCs do we have in this investigation? (give a rough count)",
27+
"Are these IOCs from a single source or multiple sources?",
28+
"Do the IOCs cluster around a specific geography or sector?",
29+
],
30+
QuestionCategory.IMPACT: [
31+
"What's the suspected impact of this activity? (lateral movement / data theft / infrastructure compromise / supply chain / unknown)",
32+
"Are we looking at a targeted attack or broad-based scanning?",
33+
"Do we have evidence of successful compromise, or just indicators of attempted access?",
34+
],
35+
QuestionCategory.TIMELINE: [
36+
"When did this activity first appear in our logs? (date or 'unknown')",
37+
"Is this activity ongoing or historical?",
38+
"How long has this been happening? (days / weeks / months / years)",
39+
],
40+
}
41+
42+
HYPOTHESIZING_QUESTIONS = {
43+
QuestionCategory.ATTRIBUTION: [
44+
"Do you suspect a known threat actor is behind this? (If yes, name them; if no, say 'unknown')",
45+
"What's your confidence level in that attribution? (high / medium / low / none)",
46+
"Are there any historical campaigns this resembles?",
47+
],
48+
QuestionCategory.CONFIDENCE: [
49+
"How confident are you in the IOC enrichment so far? (high / medium / low)",
50+
"Do we have any direct evidence (e.g., captured malware, emails) or just indicators?",
51+
"What's your confidence that these IOCs are actually malicious? (high / medium / low)",
52+
],
53+
}
54+
55+
TESTING_QUESTIONS = {
56+
QuestionCategory.SCOPE: [
57+
"Should we narrow the investigation scope based on what we've learned, or expand it?",
58+
"Are there any IOCs or hypotheses we should deprioritize?",
59+
],
60+
QuestionCategory.IMPACT: [
61+
"Based on the enrichment, what's our updated assessment of impact?",
62+
"Do we need to involve response teams, or is this analysis-only?",
63+
],
64+
}
65+
66+
CLOSING_QUESTIONS = {
67+
QuestionCategory.CONFIDENCE: [
68+
"What's your final confidence in the hypothesis? (high / medium / low)",
69+
"Are there any outstanding gaps that should be documented?",
70+
"Should we escalate this to incident response, or close as analysis complete?",
71+
],
72+
}
73+
74+
75+
def get_next_question(
76+
phase: str,
77+
investigation_state: Dict[str, Any],
78+
conversation_history: List[str],
79+
) -> str:
80+
"""
81+
Select next clarifying question based on phase and investigation state.
82+
83+
Args:
84+
phase: Current copilot phase (GATHERING, HYPOTHESIZING, TESTING, CLOSING)
85+
investigation_state: Investigation data (IOC count, confidence, etc.)
86+
conversation_history: Previous questions asked
87+
88+
Returns:
89+
Prompt for Claude to generate contextual question
90+
"""
91+
if phase == "GATHERING":
92+
question_pool = GATHERING_QUESTIONS
93+
elif phase == "HYPOTHESIZING":
94+
question_pool = HYPOTHESIZING_QUESTIONS
95+
elif phase == "TESTING":
96+
question_pool = TESTING_QUESTIONS
97+
elif phase == "CLOSING":
98+
question_pool = CLOSING_QUESTIONS
99+
else:
100+
question_pool = GATHERING_QUESTIONS
101+
102+
# Flatten questions from categories
103+
all_questions = []
104+
for category, questions in question_pool.items():
105+
all_questions.extend(questions)
106+
107+
# Filter out already asked
108+
asked_set = set(conversation_history)
109+
unanswered = [q for q in all_questions if q not in asked_set]
110+
111+
# If all questions asked, allow repeats (Claude will vary phrasing)
112+
candidates = unanswered if unanswered else all_questions
113+
114+
return _build_question_selection_prompt(
115+
phase=phase,
116+
investigation_state=investigation_state,
117+
candidate_questions=candidates[:5], # Top 5 candidates
118+
)
119+
120+
121+
def _build_question_selection_prompt(
122+
phase: str,
123+
investigation_state: Dict[str, Any],
124+
candidate_questions: List[str],
125+
) -> str:
126+
"""Build Claude prompt to select contextual question."""
127+
ioc_count = investigation_state.get("ioc_count", 0)
128+
confidence = investigation_state.get("avg_confidence", 0.0)
129+
has_actor = investigation_state.get("suspected_actor")
130+
has_campaign = investigation_state.get("suspected_campaign")
131+
132+
return f"""
133+
You are an expert threat intelligence analyst guiding a colleague through an investigation.
134+
135+
Investigation phase: {phase}
136+
IOC count: {ioc_count}
137+
Average confidence: {confidence:.0%}
138+
Suspected actor: {has_actor or 'unknown'}
139+
Suspected campaign: {has_campaign or 'unknown'}
140+
141+
Pick ONE question from the list below that is most relevant and useful RIGHT NOW.
142+
The question should advance the investigation — not repeat previous questions.
143+
Keep your response to just the question, no preamble.
144+
145+
Candidate questions:
146+
{_format_question_list(candidate_questions)}
147+
148+
What is your next clarifying question?
149+
"""
150+
151+
152+
def get_refinement_prompt(
153+
hypotheses: List[Dict[str, Any]],
154+
analyst_feedback: str,
155+
) -> str:
156+
"""
157+
Build prompt for hypothesis refinement based on analyst feedback.
158+
159+
Args:
160+
hypotheses: Current hypotheses with confidence scores
161+
analyst_feedback: Analyst's response ("I'm confident", "This seems unlikely", etc.)
162+
163+
Returns:
164+
Prompt for Claude to re-score hypotheses
165+
"""
166+
return f"""
167+
You are a threat intelligence analyst. The lead analyst has provided feedback on current hypotheses.
168+
169+
Current hypotheses:
170+
{_format_hypothesis_list(hypotheses)}
171+
172+
Analyst feedback: "{analyst_feedback}"
173+
174+
Based on this feedback, re-score each hypothesis:
175+
1. Increase confidence if feedback supports it
176+
2. Decrease confidence if feedback contradicts it
177+
3. Mark hypotheses as "likely" or "unlikely" based on the feedback
178+
179+
Return JSON:
180+
{{
181+
"updated_hypotheses": [
182+
{{"text": "...", "confidence": 0.X, "status": "likely"}},
183+
...
184+
],
185+
"reasoning": "Brief explanation of score changes"
186+
}}
187+
"""
188+
189+
190+
def get_next_step_prompt(
191+
investigation_state: Dict[str, Any],
192+
conversation_turns: int,
193+
hypotheses: List[Dict[str, Any]],
194+
) -> str:
195+
"""
196+
Build prompt to suggest next investigation step.
197+
198+
Args:
199+
investigation_state: Current investigation data
200+
conversation_turns: Number of turns in conversation
201+
hypotheses: Current hypotheses
202+
203+
Returns:
204+
Prompt for Claude to suggest action
205+
"""
206+
return f"""
207+
You are a threat intelligence analyst. You've been guiding an investigation.
208+
Based on current progress, recommend the next step.
209+
210+
Investigation summary:
211+
- IOC count: {investigation_state.get('ioc_count', 0)}
212+
- Avg confidence: {investigation_state.get('avg_confidence', 0.0):.0%}
213+
- Known actors: {', '.join(investigation_state.get('actors', []) or ['none'])}
214+
- Known campaigns: {', '.join(investigation_state.get('campaigns', []) or ['none'])}
215+
- Conversation turns so far: {conversation_turns}
216+
217+
Current hypotheses:
218+
{_format_hypothesis_list(hypotheses)}
219+
220+
Recommend the next investigation step from this list:
221+
1. Run automated enrichment on IOCs (ThreatQ, Recorded Future, VirusTotal)
222+
2. Query for campaign overlap with historical data
223+
3. Check for infrastructure reuse patterns
224+
4. Correlate with threat actor profiles
225+
5. Validate IOCs against known FP lists
226+
6. Narrow investigation scope (exclude some IOCs or hypotheses)
227+
7. Escalate to incident response
228+
8. Close investigation (analysis complete)
229+
230+
Return JSON:
231+
{{
232+
"recommended_step": "Step name from list above",
233+
"reason": "Brief explanation",
234+
"estimated_duration_seconds": 120,
235+
"metadata": {{"requires_manual_review": false}}
236+
}}
237+
"""
238+
239+
240+
# ─── Utility helpers ───
241+
242+
243+
def _format_question_list(questions: List[str]) -> str:
244+
"""Format questions for display in prompt."""
245+
return "\n".join([f"{i+1}. {q}" for i, q in enumerate(questions)])
246+
247+
248+
def _format_hypothesis_list(hypotheses: List[Dict[str, Any]]) -> str:
249+
"""Format hypotheses for display in prompt."""
250+
if not hypotheses:
251+
return "- No hypotheses yet"
252+
253+
lines = []
254+
for h in hypotheses:
255+
text = h.get("text", "Unknown")
256+
conf = h.get("confidence", 0.0)
257+
lines.append(f"- {text} (confidence: {conf:.0%})")
258+
259+
return "\n".join(lines)
260+
261+
262+
# ─── System prompts ───
263+
264+
COPILOT_SYSTEM_PROMPT = """
265+
You are an expert threat intelligence analyst guiding junior analysts through complex investigations.
266+
267+
Your role:
268+
1. Ask clarifying questions to narrow investigation scope
269+
2. Validate hypotheses based on analyst feedback
270+
3. Recommend next actions (enrichment, correlation, escalation)
271+
4. Provide context and background on threat actors/campaigns when relevant
272+
5. Explain investigation logic in plain language
273+
274+
Tone: Professional, concise, actionable. Avoid jargon unless the analyst uses it first.
275+
276+
Confidence scoring: When evaluating hypotheses, consider:
277+
- Quality of IOC enrichment (trusted connectors score higher)
278+
- Overlap with known campaigns
279+
- Timing consistency
280+
- Attribution confidence (is this actor definitively linked?)
281+
- False positive risk (how often are these IOCs misidentified?)
282+
283+
When suggesting next steps:
284+
- Prioritize high-confidence actions first
285+
- Estimate execution time
286+
- Flag if analyst approval is needed (e.g., "Escalate to IR?")
287+
- Suggest stopping conditions (when is investigation "done"?)
288+
"""
289+
290+
ASSISTANT_SYSTEM_PROMPT = """
291+
You are a threat intelligence research assistant providing on-demand support.
292+
293+
Your role:
294+
1. Suggest relevant connectors for enrichment (explain why each is useful)
295+
2. Draft report sections (provide multiple tones: executive, technical, narrative)
296+
3. Explain findings in plain language (connect to campaigns, actors, TTPs)
297+
4. Help analysts search across connectors (provide query syntax and examples)
298+
5. Summarize investigation progress and suggest improvements
299+
300+
Tone: Helpful, concise, educational. Explain "why" decisions matter.
301+
302+
When suggesting enrichment:
303+
- Recommend 3-5 connectors ranked by relevance
304+
- Estimate query time and cost impact
305+
- Link to threat intel (e.g., "Recorded Future specializes in this actor")
306+
307+
When drafting sections:
308+
- Offer multiple options (formal for executives, technical for SOC, narrative for reports)
309+
- Provide 2-3 paragraphs max
310+
- Include data (counts, dates, confidence) but keep prose readable
311+
312+
When explaining findings:
313+
- Connect to known campaigns/actors (reference credible sources)
314+
- Explain confidence/risk
315+
- Suggest follow-up investigation paths
316+
"""

0 commit comments

Comments
 (0)