Search⌘ K
AI Features

Building the Agent for the Security Alert Triage Case

Explore how to build a security alert triage agent by implementing evidence-based judgment, integrating analyst approval for containment, and defending against prompt injection. Understand the collaborative role between AI model recommendations and code-enforced controls to ensure safe, verifiable decisions within an Anthropic Forward Deployed Engineer interview context.

Scoping turned Harborline’s request into a clear brief. Code merges duplicates and closes routine activity, Claude triages the alerts that remain, and an analyst approves every containment action. The interviewer now asks the candidate to build the core of that design.

In a technical round, this rarely means building the whole system. The interviewer asks for one part at a time and adds a constraint after each one. The sections below follow that pattern, with each request answered by extending one file, triage_agent.py.

Triaging one alert with evidence

Suppose the interviewer asks us to triage the alerts that passed the prefilter. Each alert names a rule, a host, a user, and a severity, and that alone is rarely enough to decide anything. An analyst would open the host’s process history and the user’s recent logins before making a call, so the agent needs the same two views.

To solve this, we give Claude two evidence tools, get_process_tree and get_login_history, and run a tool loop until Claude has what it needs. The final answer has to feed code and an analyst queue, so we use structured outputs to fix its shape. Every verdict carries a label, a one-sentence reason, and a recommended action. The code below implements this first version.

Python 3.14.0
import json
import anthropic
client = anthropic.Anthropic(api_key="{{ANTHROPIC_API_KEY}}")
# Alerts that passed the prefilter.
ALERTS = {
"A-101": {"rule": "encoded_powershell", "host": "fin-ws-07", "user": "j.okafor", "severity": "high"},
"A-105": {"rule": "failed_login_burst", "host": "vpn-gw-01", "user": "m.lindqvist", "severity": "medium"},
"A-109": {"rule": "new_service_installed", "host": "srv-app-05", "user": "SYSTEM", "severity": "high"},
}
# Evidence the SIEM holds for each host and user.
PROCESS_TREES = {
"fin-ws-07": ["outlook.exe -> winword.exe Invoice_8812.docm -> powershell.exe -enc SQBFAFgA... -> connects to cdn-update-check.top:443"],
"vpn-gw-01": ["sshd (no unusual child processes)"],
"srv-app-05": ["services.exe -> C:/Users/Public/patch_manager.exe (unsigned, first seen today) -> connects to 185.220.101.4:8443"],
}
LOGINS = {
"j.okafor": ["02:10 fin-ws-07 success, usual office IP"],
"m.lindqvist": ["02:29 to 02:31 vpn-gw-01 7 failures, then success from her usual home IP", "MFA approved on her registered phone"],
"SYSTEM": ["no interactive logins"],
}
TOOLS = [
{"name": "get_process_tree", "description": "Return the recent process chains on a host, with parent processes, paths, and network connections.",
"input_schema": {"type": "object", "properties": {"host": {"type": "string"}}, "required": ["host"]}},
{"name": "get_login_history", "description": "Return a user's recent logins, with times, hosts, results, and source.",
"input_schema": {"type": "object", "properties": {"user": {"type": "string"}}, "required": ["user"]}},
]
VERDICT = {
"type": "object",
"properties": {
"verdict": {"type": "string", "enum": ["malicious", "suspicious", "benign"]},
"reason": {"type": "string", "description": "One short sentence, under 25 words, naming the key evidence."},
"action": {"type": "string", "enum": ["close", "escalate", "request_isolation"]},
},
"required": ["verdict", "reason", "action"],
"additionalProperties": False,
}
SYSTEM = ("You triage security alerts for a managed security provider. Gather evidence with the tools, "
"then give a verdict. When in doubt, escalate rather than close.")
def run_tool(name, args):
if name == "get_process_tree":
return PROCESS_TREES.get(args["host"], ["no data for this host"])
return LOGINS.get(args["user"], ["no data for this user"])
def triage(alert_id):
"""Let Claude gather evidence for one alert and return its structured verdict."""
messages = [{"role": "user", "content": f"Triage alert {alert_id}: {json.dumps(ALERTS[alert_id])}"}]
while True:
response = client.messages.create(
model="claude-sonnet-5-5", max_tokens=2048, system=SYSTEM, tools=TOOLS, messages=messages,
output_config={"format": {"type": "json_schema", "schema": VERDICT}},
)
if response.stop_reason != "tool_use":
return json.loads(next(block.text for block in response.content if block.type == "text"))
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(run_tool(block.name, block.input))}
for block in response.content if block.type == "tool_use"]})
if __name__ == "__main__":
for alert_id in ALERTS:
result = triage(alert_id)
print(f"{alert_id}: {result['verdict']}, {result['action']}")
print(f" {result['reason']}")
  • Line 5: We created the Claude client.

  • Lines 8–12: We listed three alerts that passed the prefilter.

  • Lines 14–23: We created the evidence the SIEM holds: a process tree for each host and a login history for each user. ...