|
| 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