Build a Cybersecurity Threat Agent Team
Build 3 AI agents that collaborate to analyze security logs and map attacks to MITRE ATT&CK.
Introduction
30 Second Summary
Every day, servers generate thousands of security log entries. Most of them are noise, but buried in that stream are the signs of real attacks: brute force login attempts, port scans probing for weaknesses, and unauthorized access. The challenge is not collecting logs. It is making sense of them fast enough to respond.
In this project, you will build a team of three AI agents that collaborate to analyze security logs. Each agent has a specialized role: one parses raw logs, one classifies attacks, and one generates a remediation playbook.
What You'll Build
You will run a single command and watch three Claude-powered agents pass data between each other automatically, transforming messy log text into a prioritized incident response plan you could hand to a security analyst.
By the end of this project, you'll have:
- A working multi-agent pipeline where you feed in raw security logs and your Log Parser Agent extracts structured indicators like IPs, timestamps, and failed attempt counts.
- Attack classifications mapped to the MITRE ATT&CK framework, where your Threat Classifier Agent identifies techniques like T1110 Brute Force and T1046 Network Service Discovery.
- An actionable remediation playbook generated by your Response Advisor Agent with concrete steps like blocking IPs, enforcing MFA, and hardening SSH config.
- Secret Mission: Add a severity scoring system that calculates a composite threat score based on attempt count, success indicators, and service criticality.
Are there any prerequisites?
You need basic Python knowledge (variables, functions, loops) and a Claude API key with credits. The project uses claude-haiku-4-5 which costs $1 per million input tokens and $5 per million output tokens. The full project costs roughly $0.50 to $1.00 in API usage.
Before We Start
Security operations teams analyze thousands of log entries daily, hunting for patterns that indicate active attacks. A single AI prompt can summarize text, but complex threat analysis benefits from specialized agents working together.
You are going to build a team of three AI agents that collaborate through a pipeline: one parses raw logs, one classifies attacks against the MITRE ATT&CK framework, and one recommends remediation. First, you need the right tools in place.
In this step, get ready to:
- Verify Python is installed and install the Anthropic SDK.
- Configure your Claude API key as an environment variable.
- Create your project folder and starter file.
Install Python and the Anthropic SDK
The Anthropic Python SDK requires Python 3.9 or higher. You need to confirm you have the right version before installing the SDK.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (backtick key, top-left of keyboard).
- Check your Python version by running this command:
macOS
python3 --version
Windows
python --version
✔️ I see version 3.9 or higher
You should see something like Python 3.11.9 or Python 3.12.4. Any version 3.9 or above works for this project. Move on to the next instruction below.
ⓧ I see an older version
If you see a version lower than 3.9 (for example Python 3.8.10), you need to install a newer version.
- Go to python.org/downloads and download the latest Python installer for your operating system.
- Run the installer. On Windows, check the Add Python to PATH box before clicking Install Now.
- On macOS, the installer places the new Python alongside any existing version. After installing, use python3 to access the new version.
- Close and reopen your VS Code terminal, then re-run the version check to confirm 3.9 or higher.
ⓧ Command not found
Python is not installed on your machine yet.
- Go to python.org/downloads and download the latest Python installer for your operating system.
- Run the installer. On Windows, check the Add Python to PATH box before clicking Install Now.
- On macOS, double-click the .pkg file and follow the prompts. The installer places Python at /usr/local/bin/python3.
- Close and reopen your VS Code terminal, then re-run the version check to confirm Python is available.
- Install the Anthropic SDK by running this command:
pip install anthropic==0.120.2
What does this command do?
This installs version 0.120.2 of the anthropic package, which gives your Python code access to the Claude API. Pinning the version ensures your code works exactly as written in this project.
- Verify the installation succeeded by running:
python -c "import anthropic; print(anthropic.__version__)"
You should see 0.120.2 printed in your terminal.
Seeing an error with pip or import?
- If pip is not found, try pip3 install anthropic==0.120.2 on macOS.
- If python is not found in the verify command, try python3 instead.
- On Windows 11, if python opens the Microsoft Store, disable the App Installer aliases in Settings, then Apps, then Advanced app settings, then App execution aliases.
Still stuck? Help me fix my Python or pip installation issue.
Set up your API key
Your agents need a Claude API key to communicate with the model. You will store it as an environment variable so your code can read it without hardcoding the secret.
- Set the environment variable in your VS Code terminal by running the command for your platform:
macOS
export ANTHROPIC_API_KEY=your-api-key-here
Replace your-api-key-here with the actual API key from your Anthropic account. Do not add quotes around the value.
Windows
In Command Prompt (CMD):
set ANTHROPIC_API_KEY=your-api-key-here
In PowerShell:
$env:ANTHROPIC_API_KEY="your-api-key-here"
Replace your-api-key-here with the actual API key from your Anthropic account.
- Verify the key is set by running:
python -c "import os; print('Key set!' if os.environ.get('ANTHROPIC_API_KEY') else 'NOT SET')"
You should see Key set! printed. If you see NOT SET, re-run the export/set command above.
This key only lasts for your current session
Environment variables set with export or set disappear when you close the terminal. If you restart VS Code later, you will need to set it again before running your agent code.
Create your project folder and starter file
You need a dedicated folder for your project files. You will create it on your Desktop so it is easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir threat-agent-team
cd threat-agent-team
- Create an empty main.py file by running:
macOS
touch main.py
Windows
type nul > main.py
- Confirm the file was created by running:
ls
You should see main.py listed in the output.
- Open this folder in VS Code by running:
code .
What does code . do?
The code . command opens the current folder in VS Code. The dot (.) means "this directory." After running it, you should see threat-agent-team in the VS Code Explorer sidebar with main.py inside it.
code command not found?
If code . does not work, open VS Code manually. Click File, then Open Folder, and navigate to your Desktop. Select the threat-agent-team folder.
Need more help? Help me open my project folder in VS Code.
Your environment is ready. Next up, you will build your first AI agent and watch it parse raw security logs into structured data.
Set Up the Project File
Your environment is ready with the anthropic SDK installed and your API key configured. Now you need a Python file that connects to the Claude API so your agents can communicate.
In this step, you will add the foundational code to main.py: the imports your agents will need, an initialized API client, and the model your agents will use for every request.
In this step, get ready to:
- Add imports and initialize the Anthropic client in main.py.
- Verify the client initializes without errors by running the file.
Add the imports and client initialization
Your agent team will need several Python modules: os to read environment variables, re for parsing log patterns, json for structured data, and anthropic to talk to the Claude API.
- In VS Code, switch to your open main.py file inside the threat-agent-team folder.
- Add the following code to set up imports, the API client, and the model constant:
import os
import re
import json
import anthropic
# Initialize the Anthropic client using your API key
client = anthropic.Anthropic(
api_key=os.environ.get("ANTHROPIC_API_KEY"),
)
# Model all agents will use for requests
MODEL = "claude-haiku-4-5"
What does this code do?
The four imports bring in the modules your agents will use throughout the project.
- os.environ.get("ANTHROPIC_API_KEY") reads the API key from the environment variable you set earlier. This keeps your key out of the source code.
- anthropic.Anthropic() creates the client object that sends requests to the Claude API.
- MODEL is set to claude-haiku-4-5, a fast and cost-effective model that handles tool use well at $1 per million input tokens.
Before you run this, what do you think will happen? The file initializes a client but never prints anything or calls the API.
- Save main.py.
- In your terminal, run the file to confirm the client initializes without error:
python main.py
The command should complete silently with no output and no errors. That silence means Python loaded all four modules and created the client successfully.
Seeing an error?
- ModuleNotFoundError: No module named 'anthropic' means the package is not installed in the Python environment you are using. Run pip install anthropic==0.120.2 and try again.
- If you see a SyntaxError, check that the parentheses and quotes in the Anthropic() call match the code above exactly.
Still stuck? Help me debug why python main.py is throwing an error when I try to import anthropic and initialize the client.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.py so far:
import os
import re
import json
import anthropic
client = anthropic.Anthropic(
api_key=os.environ.get("ANTHROPIC_API_KEY"),
)
MODEL = "claude-haiku-4-5"
Your project file is wired up and the client can reach the Claude API. Next up, you will build your first agent: a Log Parser that extracts structured threat indicators from raw security logs.
Build the Log Parser Agent
Your project skeleton can connect to Claude, but it has no job to do yet. Real security logs are messy walls of text. An analyst scanning them by hand can miss indicators or take too long to correlate events.
In this step you will build your first working agent: the Log Parser. It takes raw SSH auth logs, calls a custom tool to extract structured indicators of compromise, and hands back organized JSON you can actually reason about.
In this step, get ready to:
- Create sample security log data and define a tool schema that tells Claude how to call your parser.
- Implement the manual agentic loop that lets Claude call tools repeatedly until it is done.
- Run the full Log Parser Agent and see structured threat indicators in your terminal.
Create sample log data and define the parsing tool
Before you can parse logs, you need logs to parse. You will hardcode a realistic SSH brute-force scenario so you can test your agent without needing a live server.
- In main.py, add the sample log data below your MODEL line:
# --- Sample Security Logs ---
SAMPLE_LOG_BRUTE_FORCE = """
Aug 2 03:14:22 server1 sshd[12345]: Failed password for admin from 203.0.113.42 port 52341 ssh2
Aug 2 03:14:25 server1 sshd[12346]: Failed password for admin from 203.0.113.42 port 52342 ssh2
Aug 2 03:14:28 server1 sshd[12347]: Failed password for admin from 203.0.113.42 port 52343 ssh2
Aug 2 03:14:31 server1 sshd[12348]: Failed password for admin from 203.0.113.42 port 52344 ssh2
Aug 2 03:14:34 server1 sshd[12349]: Failed password for admin from 203.0.113.42 port 52345 ssh2
Aug 2 03:14:37 server1 sshd[12350]: Failed password for root from 203.0.113.42 port 52346 ssh2
Aug 2 03:14:40 server1 sshd[12351]: Failed password for root from 203.0.113.42 port 52347 ssh2
Aug 2 03:14:43 server1 sshd[12352]: Accepted password for admin from 203.0.113.42 port 52348 ssh2
"""
What does this log show?
This is a classic SSH brute-force pattern. An attacker at IP 203.0.113.42 hammers the admin and root accounts with repeated password guesses.
The last line is the scary one: an Accepted login. That means the attacker got in.
- Below the sample data, add the tool schema and the dispatcher function that Claude will use to call your parser:
# --- Tool Definitions ---
PARSE_LOG_TOOL = {
"name": "parse_log_indicators",
"description": "Parse raw security log text and extract structured indicators of compromise including IP addresses, timestamps, usernames targeted, services, and event counts.",
"input_schema": {
"type": "object",
"properties": {
"log_text": {
"type": "string",
"description": "The raw log text to parse"
}
},
"required": ["log_text"]
}
}
# --- Tool Implementations ---
def execute_tool(tool_name, tool_input):
"""Execute a tool and return the result as a string."""
if tool_name == "parse_log_indicators":
return execute_parse_log(tool_input["log_text"])
else:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
What does this code do?
PARSE_LOG_TOOL is a JSON schema that tells Claude exactly what parameters the tool accepts. Claude reads this schema and decides when to call the tool based on the user's request.
execute_tool is a dispatcher. When Claude asks to call a tool by name, your code routes that request to the correct Python function.
- Save main.py and confirm your code has no syntax errors by running:
python main.py
You should see no output and no errors. The file defines data and functions but nothing calls them yet.
Seeing a syntax error?
Check that the triple-quoted string for SAMPLE_LOG_BRUTE_FORCE uses three double quotes """ on both sides. Also confirm PARSE_LOG_TOOL has matching curly braces and square brackets.
help me fix a syntax error in my tool schema definition
Now you need the function that actually does the parsing. This function uses regex patterns to pull structured data out of the raw log text.
- Add the first part of the execute_parse_log function below execute_tool:
def execute_parse_log(log_text):
"""Parse log text and extract indicators of compromise."""
# Regex patterns to match different log entry types
failed_pattern = r"Failed password for (\w+) from ([\d.]+) port (\d+)"
accepted_pattern = r"Accepted password for (\w+) from ([\d.]+) port (\d+)"
connection_pattern = r"connection attempt from ([\d.]+) port (\d+) \((\w[\w-]*)\)"
# Extract all matches from the log text
failed_attempts = re.findall(failed_pattern, log_text)
successful_logins = re.findall(accepted_pattern, log_text)
connection_attempts = re.findall(connection_pattern, log_text)
# Build the structured indicators dictionary
indicators = {
"failed_login_attempts": [],
"successful_logins": [],
"port_scan_indicators": [],
"summary": {}
}
source_ips = set()
targeted_users = set()
# Process each failed login attempt
for user, ip, port in failed_attempts:
indicators["failed_login_attempts"].append({
"username": user,
"source_ip": ip,
"port": int(port)
})
source_ips.add(ip)
targeted_users.add(user)
What does this code do?
Three regex patterns each target a different log entry type: failed logins, successful logins, and connection attempts. The re.findall function pulls all matches at once from the raw text.
The function then loops through each match and builds a structured dictionary with the extracted fields (username, IP, port).
- Complete the execute_parse_log function by adding this code directly below the block you just pasted (still inside the function, at the same indent level):
# Process successful logins
for user, ip, port in successful_logins:
indicators["successful_logins"].append({
"username": user,
"source_ip": ip,
"port": int(port)
})
source_ips.add(ip)
# Process port scan indicators
scanned_ports = set()
scan_ips = set()
for ip, port, service in connection_attempts:
indicators["port_scan_indicators"].append({
"source_ip": ip,
"port": int(port),
"service": service
})
scanned_ports.add(int(port))
scan_ips.add(ip)
# Build summary of all indicators
indicators["summary"] = {
"total_failed_logins": len(failed_attempts),
"total_successful_logins": len(successful_logins),
"unique_source_ips": list(source_ips | scan_ips),
"targeted_usernames": list(targeted_users),
"ports_scanned": sorted(list(scanned_ports)),
"has_successful_breach": len(successful_logins) > 0
}
return json.dumps(indicators, indent=2)
What does this code do?
The second half processes successful logins and connection attempts the same way. It then builds a summary dictionary counting totals and flagging whether the attacker achieved a successful breach.
The function returns everything as a JSON string. Claude will receive this structured data as a tool result and use it to form its analysis.
- Save main.py and test your parser directly by running:
python -c "from main import execute_parse_log, SAMPLE_LOG_BRUTE_FORCE; print(execute_parse_log(SAMPLE_LOG_BRUTE_FORCE))"
You should see structured JSON with 7 failed login attempts, 1 successful login, source IP 203.0.113.42, and has_successful_breach set to true.
Seeing an IndentationError?
The second code block must continue at the same indentation level inside execute_parse_log. Each line should start with exactly 4 spaces of indentation. Make sure there is no blank line between the two halves that breaks the function body.
help me fix the indentation in my execute_parse_log function
Write the agentic loop
Your parser works, but right now you are calling it directly from Python. For an agent to use it, you need the manual agentic loop: send a message to Claude with tools available, check if Claude wants to call a tool, execute it, feed the result back, and repeat until Claude says it is done.
- Add the first part of the run_agent function below execute_parse_log:
# --- Agent Runner (Manual Agentic Loop) ---
def run_agent(system_prompt, tools, user_message, agent_name="Agent"):
"""Run a single agent with the manual agentic loop pattern."""
print(f"\n{'='*60}")
print(f" {agent_name}")
print(f"{'='*60}")
messages = [{"role": "user", "content": user_message}]
while True:
# Send the conversation to Claude with available tools
response = client.messages.create(
model=MODEL,
max_tokens=4096,
system=system_prompt,
tools=tools,
messages=messages,
)
# If Claude is done talking, return the final text
if response.stop_reason == "end_turn":
final_text = ""
for block in response.content:
if block.type == "text":
final_text += block.text
print(f"\n[{agent_name} Output]:")
print(final_text)
return final_text
What is the manual agentic loop?
Claude's API uses a stop_reason field to signal what it wants to do next. When stop_reason is end_turn, Claude is finished and has a text response ready.
When stop_reason is anything else, Claude wants to call a tool. Your code executes the tool and sends the result back as a new message, then loops.
- Add the second half of run_agent directly below (still inside the while True loop, at the same indent level as the if response.stop_reason block):
# Check if Claude wants to call any tools
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
# Safety check: if no tool calls and not end_turn, return what we have
if not tool_use_blocks:
final_text = ""
for block in response.content:
if block.type == "text":
final_text += block.text
print(f"\n[{agent_name} Output]:")
print(final_text)
return final_text
# Add Claude's response (with tool_use blocks) to the conversation
messages.append({"role": "assistant", "content": response.content})
# Execute each tool Claude requested and collect results
tool_results = []
for tool_block in tool_use_blocks:
print(f" [Tool Call] {tool_block.name}({json.dumps(tool_block.input)[:100]}...)")
result = execute_tool(tool_block.name, tool_block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_block.id,
"content": result,
})
# Feed tool results back to Claude as a user message
messages.append({"role": "user", "content": tool_results})
Why does the tool result go in a user message?
Claude's API models the conversation as alternating assistant and user messages. Tool results are sent as a user message with type: tool_result and a matching tool_use_id. This tells Claude which tool call the result belongs to so it can continue reasoning.
- Save main.py and confirm the agentic loop parses correctly by running:
python main.py
You should see no output and no errors. The function is defined but nothing calls it yet.
- Now add the Log Parser Agent's system prompt below run_agent:
# --- Agent Configurations ---
LOG_PARSER_SYSTEM = """You are a Security Log Parser Agent. Your job is to analyze raw security logs and extract structured indicators of compromise (IoCs).
When given raw log data:
1. Use the parse_log_indicators tool to extract structured data
2. Summarize what you found in a clear format including:
- Source IPs involved
- Targeted usernames
- Number of failed vs successful attempts
- Any port scanning activity
- Whether a breach may have occurred
Be concise and factual. Output your findings as a structured analysis."""
What does the system prompt do?
The system prompt gives Claude its role and instructions. It tells Claude to use the parse_log_indicators tool and specifies what format the output should take. This is what makes it a "Log Parser Agent" instead of a generic chatbot.
Run the Log Parser Agent
Every piece is in place: sample data, a tool schema, the parsing function, and the agentic loop. Time to wire them together and watch the agent work.
- Add the main execution block at the bottom of main.py:
# --- Main Execution ---
if __name__ == "__main__":
print("="*60)
print(" THREAT AGENT TEAM - Multi-Agent Cybersecurity Analysis")
print("="*60)
print("\n[Scenario 1] SSH Brute Force Attack")
print("-"*40)
run_agent(
system_prompt=LOG_PARSER_SYSTEM,
tools=[PARSE_LOG_TOOL],
user_message=f"Analyze these security logs and extract all indicators of compromise:\n\n{SAMPLE_LOG_BRUTE_FORCE}",
agent_name="LOG PARSER AGENT",
)
What does this code do?
The if __name__ == "__main__" block is Python's standard entry point. It calls run_agent with the Log Parser's system prompt, the parse tool, and the brute-force log as input.
Claude will receive the raw log, decide it needs to call parse_log_indicators, your code will execute the tool, send the result back, and Claude will summarize the findings.
- Save main.py.
Before you run this, what do you think will happen? Will Claude return raw JSON or will it summarize the parsed indicators in natural language?
- Run the agent by executing:
python main.py
You should see the agentic loop in action. First a [Tool Call] line shows Claude requesting parse_log_indicators. Then the [LOG PARSER AGENT Output] prints a structured analysis listing the source IP 203.0.113.42, targeted usernames (admin, root), 7 failed attempts, 1 successful login, and a warning that a breach occurred.
Seeing an AuthenticationError?
This means your ANTHROPIC_API_KEY environment variable is not set in this terminal session. Re-run the set or $env: command from Step 1 to configure it again. Environment variables reset when you close the terminal.
help me fix an AuthenticationError when calling the Claude API
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py and your agent output includes extracted indicators.
ⓧ I'd like to double check the full code
Your main.py should contain all of the following sections in order. Scroll up through this step and confirm each block is present:
- Imports and client setup (from Step 1).
- SAMPLE_LOG_BRUTE_FORCE constant with 8 log lines.
- PARSE_LOG_TOOL schema dictionary.
- execute_tool dispatcher function.
- execute_parse_log function (both halves joined together as one continuous function).
- run_agent function (both halves joined together inside the while True loop).
- LOG_PARSER_SYSTEM prompt string.
- if __name__ == "__main__" block calling run_agent.
Your first agent is alive. It takes messy raw logs and produces structured threat indicators. Next up, you will build a second agent that takes these indicators and classifies the attack using the MITRE ATT&CK framework.
Add the Threat Classifier Agent
Your Log Parser Agent extracts structured indicators from raw logs. But a list of IPs and failed-login counts does not tell you what kind of attack you are dealing with. Without classification, a security analyst cannot prioritize response.
In this step you will build a second agent that receives the parser's output and maps it to the MITRE ATT&CK framework. This is where agent collaboration begins: one agent's output becomes another agent's input.
In this step, get ready to:
- Define a MITRE ATT&CK lookup tool with a local technique database.
- Create a Threat Classifier Agent that calls the lookup tool to classify attack patterns.
- Build a pipeline function that chains the Log Parser output into the Threat Classifier.
Define the MITRE lookup tool schema
Claude needs a tool schema to know what capabilities it can invoke. The schema tells it the tool's name, what it does, and what inputs it expects.
- In main.py, find the closing } of PARSE_LOG_TOOL and add this new tool schema below it:
MITRE_LOOKUP_TOOL = {
"name": "lookup_mitre_technique",
"description": "Look up a MITRE ATT&CK technique by attack pattern description. Returns the technique ID, name, tactic, and description.",
"input_schema": {
"type": "object",
"properties": {
"attack_pattern": {
"type": "string",
"description": "Description of the observed attack pattern, e.g. 'multiple failed SSH login attempts from single IP'"
}
},
"required": ["attack_pattern"]
}
}
What does this tool schema do?
This dictionary describes the tool to Claude. The agent reads the description and input_schema to decide when and how to call it. You are giving Claude a way to look up attack classifications by describing what it observed.
Now you need the actual data the tool will search against. The MITRE ATT&CK framework organizes thousands of techniques, but your local database will cover the ones relevant to this project's log scenarios.
- Find the # --- Tool Implementations --- comment. Add this database dictionary directly below it, before the execute_tool function:
MITRE_DATABASE = {
"brute_force": {
"id": "T1110",
"name": "Brute Force",
"tactic": "Credential Access",
"description": "Adversaries may use brute force techniques to gain access to accounts when passwords are unknown or when password hashes are obtained."
},
"password_guessing": {
"id": "T1110.001",
"name": "Brute Force: Password Guessing",
"tactic": "Credential Access",
"description": "Adversaries with no prior knowledge of legitimate credentials may guess passwords to attempt access to accounts."
},
"password_spraying": {
"id": "T1110.003",
"name": "Brute Force: Password Spraying",
"tactic": "Credential Access",
"description": "Adversaries may use a single or small list of commonly used passwords against many different accounts."
},
What are these entries?
Each key maps a plain-language attack category to its official MITRE ATT&CK technique ID and metadata. T1110 is the parent technique for brute force. T1110.001 and T1110.003 are sub-techniques for specific variants.
- The dictionary is not complete yet. Add these discovery and reconnaissance entries directly below the password_spraying entry (still inside the same MITRE_DATABASE dictionary):
"network_service_discovery": {
"id": "T1046",
"name": "Network Service Discovery",
"tactic": "Discovery",
"description": "Adversaries may attempt to get a listing of services running on remote hosts and local network infrastructure devices."
},
"active_scanning": {
"id": "T1595",
"name": "Active Scanning",
"tactic": "Reconnaissance",
"description": "Adversaries may execute active reconnaissance scans to gather information that can be used during targeting."
},
"scanning_ip_blocks": {
"id": "T1595.001",
"name": "Active Scanning: Scanning IP Blocks",
"tactic": "Reconnaissance",
"description": "Adversaries may scan victim IP blocks to gather information for targeting."
}
}
What do these entries cover?
T1046 (Network Service Discovery) covers port scanning activity. T1595 and T1595.001 cover active reconnaissance scans. Together with the credential-access entries above, your database handles the two attack types your sample logs contain.
- Save main.py.
- Run your script to confirm there are no syntax errors by running:
python main.py
You should still see the Log Parser Agent execute and print parsed indicators, the same as before. The new code has not broken anything.
Seeing a SyntaxError?
Check that the MITRE_DATABASE dictionary has a closing } at the end. Each inner entry needs a closing }, except the last one which has just }.
Also verify MITRE_DATABASE is placed before the execute_tool function, not inside it.
Help me fix a SyntaxError in my MITRE_DATABASE dictionary
Implement the MITRE lookup function
The tool schema tells Claude the tool exists. Now you need the Python function that actually runs when Claude calls it. This function takes an attack pattern description, matches it against keywords, and returns the relevant MITRE techniques.
- Add this function below the execute_parse_log function:
def execute_mitre_lookup(attack_pattern):
"""Look up MITRE ATT&CK technique based on attack pattern description."""
pattern_lower = attack_pattern.lower()
matches = []
# Match brute force patterns
if any(word in pattern_lower for word in ["brute force", "failed login", "password guess", "repeated login"]):
matches.append(MITRE_DATABASE["password_guessing"])
matches.append(MITRE_DATABASE["brute_force"])
# Match password spraying patterns
if any(word in pattern_lower for word in ["password spray", "single password", "many accounts"]):
matches.append(MITRE_DATABASE["password_spraying"])
# Match scanning patterns
if any(word in pattern_lower for word in ["port scan", "network scan", "service discovery", "multiple ports", "connection attempt"]):
matches.append(MITRE_DATABASE["network_service_discovery"])
matches.append(MITRE_DATABASE["active_scanning"])
if not matches:
return json.dumps({"message": "No matching technique found", "suggestion": "Try describing the attack pattern differently"})
return json.dumps({"matches": matches}, indent=2)
What does this function do?
It converts Claude's free-text attack description to lowercase, then checks for keyword matches. If the description mentions "brute force" or "failed login", it returns the T1110 family of techniques. If it mentions "port scan" or "connection attempt", it returns T1046 and T1595.
This keyword-matching approach is simple but effective for a local lookup. In production, you might call the real MITRE ATT&CK API instead.
The execute_tool function is the dispatcher that routes tool calls to the correct implementation. You need to tell it about the new lookup tool.
- In main.py, find the execute_tool function. Locate this line:
else:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
- Add this elif branch directly above the else, so the function now looks like this:
def execute_tool(tool_name, tool_input):
"""Execute a tool and return the result as a string."""
if tool_name == "parse_log_indicators":
return execute_parse_log(tool_input["log_text"])
elif tool_name == "lookup_mitre_technique":
return execute_mitre_lookup(tool_input["attack_pattern"])
else:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
Why a central dispatcher?
The agentic loop calls execute_tool for every tool_use block Claude returns. By routing through one function, you can add new tools without changing the loop itself. Each new tool just needs an elif branch.
- Save main.py and test the lookup function directly by running:
python -c "from main import execute_mitre_lookup; print(execute_mitre_lookup('brute force SSH login attempts'))"
You should see JSON output containing T1110.001 (Password Guessing) and T1110 (Brute Force) in the matches array.
Seeing a NameError or empty matches?
A NameError: name 'MITRE_DATABASE' is not defined means execute_mitre_lookup is placed above the dictionary in the file. Move the function below MITRE_DATABASE.
If you see an empty matches list, check that the keyword strings in your if any(...) blocks match exactly: "brute force", "failed login", etc.
Help me debug my execute_mitre_lookup function
Chain the agents into a pipeline
With the lookup tool working, you can now create the Threat Classifier Agent and wire both agents together. The classifier's system prompt tells it how to use the MITRE lookup tool, and a pipeline function handles the data handoff automatically.
- Add this system prompt below LOG_PARSER_SYSTEM in the Agent Configurations section:
THREAT_CLASSIFIER_SYSTEM = """You are a Threat Classifier Agent. Your job is to take parsed security indicators and classify the attack types using the MITRE ATT&CK framework.
When given parsed indicators:
1. Identify each distinct attack pattern present in the data
2. Use the lookup_mitre_technique tool for EACH pattern to get the proper ATT&CK classification
3. Present your classifications clearly with:
- The MITRE ATT&CK Technique ID and name
- The tactic category
- Brief explanation of why this classification applies to the observed indicators
Classify ALL distinct attack patterns you observe. Do not combine different attack types."""
What does this system prompt do?
The prompt gives the Threat Classifier its identity and operating procedure. Step 2 is critical: it tells Claude to call the lookup_mitre_technique tool for each pattern it finds. Without this instruction, Claude might try to classify from memory instead of using the tool.
Now you need the orchestrator that passes data from one agent to the next.
- Add a new section comment and the run_pipeline function below the agent configurations, above the # --- Main Execution --- comment:
# --- Pipeline Orchestrator ---
def run_pipeline(log_data):
"""Run the full multi-agent analysis pipeline."""
print("\n" + "#"*60)
print("# CYBERSECURITY THREAT ANALYSIS - AGENT TEAM PIPELINE")
print("#"*60)
print(f"\n[Input] Raw log data ({len(log_data.strip().splitlines())} lines)")
# Agent 1: Log Parser
parser_output = run_agent(
system_prompt=LOG_PARSER_SYSTEM,
tools=[PARSE_LOG_TOOL],
user_message=f"Analyze these security logs and extract all indicators of compromise:\n\n{log_data}",
agent_name="LOG PARSER AGENT",
)
# Agent 2: Threat Classifier
classifier_output = run_agent(
system_prompt=THREAT_CLASSIFIER_SYSTEM,
tools=[MITRE_LOOKUP_TOOL],
user_message=f"Classify the following parsed security indicators using MITRE ATT&CK:\n\n{parser_output}",
agent_name="THREAT CLASSIFIER AGENT",
)
return {
"parsed_indicators": parser_output,
"threat_classifications": classifier_output,
}
What does this pipeline do?
This is agent collaboration in action. The parser_output string becomes the user_message for the Threat Classifier. Each agent runs independently through the agentic loop, but the pipeline connects their outputs to inputs.
The print statements at the top create a visible header so you can see the pipeline start in your terminal.
Finally, update the main block to use the pipeline instead of calling the Log Parser directly.
- Find the # --- Main Execution --- section at the bottom of the file. Locate this block:
run_agent(
system_prompt=LOG_PARSER_SYSTEM,
tools=[PARSE_LOG_TOOL],
user_message=f"Analyze these security logs and extract all indicators of compromise:\n\n{SAMPLE_LOG_BRUTE_FORCE}",
agent_name="LOG PARSER AGENT",
)
- Replace that entire run_agent(...) call with a single line:
run_pipeline(SAMPLE_LOG_BRUTE_FORCE)
Before you run this, what do you expect to see? The Log Parser will still extract indicators. But now the Threat Classifier should receive those indicators and call the MITRE lookup tool to classify them.
- Save main.py and run the full pipeline:
python main.py
You should see both agents execute in sequence. The Log Parser prints its parsed indicators, then the Threat Classifier calls the lookup_mitre_technique tool and outputs attack classifications with MITRE ATT&CK IDs like T1110.001 (Password Guessing).
What should I see?
Your terminal shows two agent sections separated by === lines. The LOG PARSER AGENT section shows extracted IPs and failed-login counts. The THREAT CLASSIFIER AGENT section shows [Tool Call] lookup_mitre_technique(...) followed by its classification output referencing T1110 (Brute Force) and T1110.001 (Password Guessing).
Classifier agent not calling the tool?
If the Threat Classifier outputs a classification without any [Tool Call] lines, it is answering from memory instead of using the tool. Check that you passed [MITRE_LOOKUP_TOOL] in the tools parameter of the run_agent call inside run_pipeline.
If you see an Unknown tool error in the output, the elif branch in execute_tool may have a typo in the tool name string. It must be exactly "lookup_mitre_technique".
Help me debug why my Threat Classifier is not using the MITRE lookup tool
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py and can see both agents producing output when you run the script.
ⓧ I'd like to double check the full code
Scroll up through your file and confirm these sections are present, in this order:
- Imports (os, re, json, anthropic) and client setup at the top.
- SAMPLE_LOG_BRUTE_FORCE multi-line string.
- PARSE_LOG_TOOL schema, then MITRE_LOOKUP_TOOL schema.
- MITRE_DATABASE dictionary with 6 entries (brute_force through scanning_ip_blocks).
- execute_tool with two branches (parse_log_indicators and lookup_mitre_technique).
- execute_parse_log function, then execute_mitre_lookup function.
- run_agent function (unchanged from previous step).
- LOG_PARSER_SYSTEM prompt, then THREAT_CLASSIFIER_SYSTEM prompt.
- run_pipeline function chaining both agents.
- Main block calling run_pipeline(SAMPLE_LOG_BRUTE_FORCE).
Your agents are collaborating. The Log Parser extracts data and the Threat Classifier maps it to known attack techniques. Next up, you will complete the team with a Response Advisor that turns those classifications into actionable remediation steps.
Complete the Team with the Response Advisor
Your Threat Classifier now tells you what kind of attack you're facing. But knowing an attack maps to "T1110 Brute Force" doesn't tell an analyst what to actually do about it.
In this step, you'll add the Response Advisor Agent to complete your team. It takes the Classifier's output and generates an actionable playbook with specific steps like blocking IPs, enabling MFA, and hardening SSH configuration. This is where your agent team delivers a full threat report from raw logs to remediation.
In this step, get ready to:
- Define the remediation playbook tool and its knowledge base.
- Create the Response Advisor Agent and wire it into the pipeline.
- Run the full three-agent team and observe the complete threat report.
Define the remediation playbook tool
The Response Advisor needs a tool that can look up standard remediation steps for any given MITRE ATT&CK technique. You'll define the tool schema, build a local knowledge base of playbooks, and implement the execution logic.
- In main.py, find the MITRE_LOOKUP_TOOL schema definition.
- Add the REMEDIATION_TOOL schema below it by copying this code:
REMEDIATION_TOOL = {
"name": "get_remediation_playbook",
"description": "Get a remediation playbook for a specific MITRE ATT&CK technique ID. Returns concrete actions to mitigate the threat.",
"input_schema": {
"type": "object",
"properties": {
"technique_id": {
"type": "string",
"description": "The MITRE ATT&CK technique ID, e.g. 'T1110' or 'T1110.001'"
},
"context": {
"type": "string",
"description": "Additional context about the specific incident to tailor remediation"
}
},
"required": ["technique_id"]
}
}
What does this tool do?
This tool takes a MITRE ATT&CK technique ID (like T1110.001) and returns concrete remediation actions. The context parameter lets the agent pass incident-specific details so the playbook advice can be tailored.
- Now add the REMEDIATION_DATABASE dictionary below the MITRE_DATABASE section. Start with the brute force playbooks by copying this code:
REMEDIATION_DATABASE = {
"T1110": {
"actions": [
"Implement account lockout policies after 5 failed attempts",
"Enable multi-factor authentication (MFA) for all accounts",
"Deploy fail2ban or similar intrusion prevention",
"Review and strengthen password complexity requirements",
"Monitor for distributed brute force patterns across multiple source IPs"
]
},
"T1110.001": {
"actions": [
"Block the offending source IP at the firewall immediately",
"Force password reset for targeted accounts",
"Enable MFA on targeted accounts as priority",
"Configure SSH to use key-based authentication only (PasswordAuthentication no)",
"Install and configure fail2ban with aggressive SSH jail settings",
"Review if the successful login (if any) was legitimate with the account owner"
]
},
}
What is this database?
This is a local knowledge base mapping technique IDs to remediation steps. Each entry contains concrete actions a security analyst would take. In a production system, this might query a live threat intelligence platform instead.
- Save main.py and run the pipeline to confirm your additions don't introduce any syntax errors:
python main.py
You should see the same Log Parser and Threat Classifier output as before. The new tool definition and database are valid Python, so the pipeline still runs without issues.
Seeing a syntax error?
Check that the REMEDIATION_DATABASE dictionary has matching braces and a trailing comma after the last entry. Each string in the actions list needs surrounding quotes.
Help me debug a syntax error in my REMEDIATION_DATABASE dictionary
- Now add the port scanning and active scanning playbooks. Find the closing } of REMEDIATION_DATABASE and add these entries before it:
"T1046": {
"actions": [
"Block the scanning source IP at the network perimeter",
"Review firewall rules to ensure only necessary ports are exposed",
"Enable port scan detection in IDS/IPS",
"Audit running services and disable unnecessary ones",
"Implement network segmentation to limit lateral movement",
"Check for any successful connections from the scanning IP"
]
},
"T1595": {
"actions": [
"Block the scanning source IP at the perimeter firewall",
"Review external attack surface for unnecessary exposed services",
"Enable rate limiting on external-facing services",
"Monitor for follow-up exploitation attempts from the same IP range"
]
}
What does the full dictionary cover?
You now have playbooks for four technique IDs: T1110 (general brute force), T1110.001 (password guessing), T1046 (network service discovery), and T1595 (active scanning). These map directly to the techniques your Threat Classifier identifies.
- Add the execute_remediation function below execute_mitre_lookup by copying this code:
def execute_remediation(technique_id, context):
"""Get remediation playbook for a MITRE technique."""
playbook = REMEDIATION_DATABASE.get(technique_id) or REMEDIATION_DATABASE.get(technique_id.split(".")[0])
if not playbook:
return json.dumps({"error": f"No playbook found for {technique_id}", "suggestion": "Check the technique ID format"})
result = {
"technique_id": technique_id,
"remediation_actions": playbook["actions"],
"context_notes": context if context else "No additional context provided"
}
return json.dumps(result, indent=2)
What does this function do?
It first tries an exact match on the technique ID. If that fails, it splits at the dot and looks up the parent technique (so T1110.001 falls back to T1110). This gives the agent a useful response even if the sub-technique has no specific playbook.
- Save main.py and run the pipeline again to confirm everything still works:
python main.py
The output is still the same two-agent pipeline. Your new function is defined but not yet called. The important thing is no errors appeared.
- Now update execute_tool to route calls to the new function. Find this code in execute_tool:
elif tool_name == "lookup_mitre_technique":
return execute_mitre_lookup(tool_input["attack_pattern"])
else:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
- Replace it with this version that adds the get_remediation_playbook handler:
elif tool_name == "lookup_mitre_technique":
return execute_mitre_lookup(tool_input["attack_pattern"])
elif tool_name == "get_remediation_playbook":
return execute_remediation(tool_input["technique_id"], tool_input.get("context", ""))
else:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
- Save main.py and test the tool directly by running:
python -c "from main import execute_tool; print(execute_tool('get_remediation_playbook', {'technique_id': 'T1110.001', 'context': 'SSH brute force on admin account'}))"
You should see a JSON response listing specific remediation actions like blocking the source IP and forcing a password reset. This confirms your tool works independently before any agent calls it.
Seeing an import error or empty output?
Make sure you saved main.py before running the test. If you see Unknown tool in the output, double-check that the elif branch for get_remediation_playbook is in the right position inside execute_tool.
Help me fix my execute_tool function
Create the Response Advisor Agent and update the pipeline
Your tool works. Now you need an agent that knows how to use it. The Response Advisor takes classified threats and turns them into a prioritized incident response plan.
- Add the RESPONSE_ADVISOR_SYSTEM prompt below THREAT_CLASSIFIER_SYSTEM by copying this code:
RESPONSE_ADVISOR_SYSTEM = """You are a Response Advisor Agent. Your job is to take classified threats and generate actionable remediation playbooks.
When given threat classifications:
1. Use the get_remediation_playbook tool for EACH identified technique
2. Compile the results into a prioritized incident response plan with:
- Immediate actions (block, contain)
- Short-term actions (harden, patch)
- Long-term actions (monitor, prevent recurrence)
3. Tailor the advice to the specific context of the incident
Be specific and actionable. Reference the actual IPs and accounts from the incident."""
What makes this prompt effective?
The prompt tells the agent to call the tool for EACH technique separately and to organize the response by urgency (immediate, short-term, long-term). It also instructs the agent to reference specific IPs and accounts from the incident rather than giving generic advice.
- Now update run_pipeline to add the third agent and a final summary. Find the existing return statement at the end of run_pipeline:
return {
"parsed_indicators": parser_output,
"threat_classifications": classifier_output,
}
- Replace it with this code that adds the Response Advisor agent call, a summary block, and an updated return:
# Agent 3: Response Advisor
advisor_output = run_agent(
system_prompt=RESPONSE_ADVISOR_SYSTEM,
tools=[REMEDIATION_TOOL],
user_message=f"Generate a remediation playbook for the following classified threats:\n\n{classifier_output}",
agent_name="RESPONSE ADVISOR AGENT",
)
# Final Summary
print("\n" + "#"*60)
print("# ANALYSIS COMPLETE")
print("#"*60)
print("\nThe agent team has completed the threat analysis pipeline.")
print("Review the output from each agent above for the full report.")
return {
"parsed_indicators": parser_output,
"threat_classifications": classifier_output,
"remediation_playbook": advisor_output,
}
What does this add to the pipeline?
The Response Advisor receives the Classifier's output as its input, just like how the Classifier received the Parser's output. Each agent builds on the previous agent's work. The summary block prints a clear "ANALYSIS COMPLETE" marker so you can see when the full pipeline finishes.
- Save main.py.
Run the full three-agent pipeline
All three agents are defined and chained together. The data flows from raw logs through parsing, classification, and now remediation.
- Before you run this, predict what you expect to see. Will the Response Advisor produce generic security advice, or will it reference the specific IP and accounts from the brute force log?
- Run the full pipeline:
python main.py
You should see all three agents execute in sequence. The Response Advisor calls the get_remediation_playbook tool and produces a prioritized incident response plan with specific actions like blocking 203.0.113.42, forcing a password reset for the admin account, and configuring fail2ban.
What should the output look like?
You'll see three labeled agent sections in the terminal. The LOG PARSER AGENT extracts indicators. The THREAT CLASSIFIER AGENT maps them to MITRE techniques. The RESPONSE ADVISOR AGENT produces a remediation playbook organized by priority.
At the very end, you'll see the "ANALYSIS COMPLETE" banner confirming the full pipeline ran successfully.
Pipeline hangs or times out?
Each agent makes an API call to Claude, so the pipeline takes 10-20 seconds to complete. If it seems stuck for over a minute, check your internet connection and verify your ANTHROPIC_API_KEY environment variable is still set.
Help me debug my three-agent pipeline
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and that all three agents produced output in the terminal.
ⓧ I'd like to double check the full code
Your main.py is now quite long. Scroll through it and confirm these sections are present, in this order:
- Imports and client setup at the top.
- SAMPLE_LOG_BRUTE_FORCE log data.
- Three tool schemas: PARSE_LOG_TOOL, MITRE_LOOKUP_TOOL, REMEDIATION_TOOL.
- MITRE_DATABASE dictionary (6 entries).
- REMEDIATION_DATABASE dictionary (4 entries: T1110, T1110.001, T1046, T1595).
- execute_tool with three branches (parse_log_indicators, lookup_mitre_technique, get_remediation_playbook).
- execute_parse_log, execute_mitre_lookup, execute_remediation functions.
- run_agent function (the agentic loop).
- Three system prompts: LOG_PARSER_SYSTEM, THREAT_CLASSIFIER_SYSTEM, RESPONSE_ADVISOR_SYSTEM.
- run_pipeline calling all three agents with a final summary.
- Main block running run_pipeline(SAMPLE_LOG_BRUTE_FORCE).
Your agent team is complete. Three specialized agents now collaborate to turn raw security logs into an actionable remediation playbook. Next up, you'll test this team against a more complex scenario with multiple simultaneous attack types.
Test with a Multi-Attack Scenario
Your three-agent pipeline successfully analyzed a single brute force attack in the last step. That is a good start, but real security logs rarely contain just one type of threat.
Real-world servers face multiple simultaneous attacks from different sources. In this step, you will feed your agent team a log containing both SSH brute force attempts AND port scanning activity from a different IP. This tests whether your agents can separate, classify, and remediate multiple distinct threats in a single pass.
In this step, get ready to:
- Add a complex multi-attack log with brute force and port scan entries from different IPs.
- Run both scenarios through the pipeline and observe separate classifications for each threat.
- Confirm the full threat report maps each attack to the correct MITRE ATT&CK technique with distinct remediation.
Add the multi-attack sample log
A multi-attack log contains entries from multiple threat actors doing different things. Your new log will have SSH brute force attempts from one IP address (203.0.113.42) and port scanning from a completely different IP (198.51.100.77).
- In main.py, find the closing """ of SAMPLE_LOG_BRUTE_FORCE and add this new constant directly below it:
SAMPLE_LOG_MULTI_ATTACK = """
Aug 2 03:14:22 server1 sshd[12345]: Failed password for admin from 203.0.113.42 port 52341 ssh2
Aug 2 03:14:25 server1 sshd[12346]: Failed password for admin from 203.0.113.42 port 52342 ssh2
Aug 2 03:14:28 server1 sshd[12347]: Failed password for admin from 203.0.113.42 port 52343 ssh2
Aug 2 03:14:31 server1 sshd[12348]: Failed password for admin from 203.0.113.42 port 52344 ssh2
Aug 2 03:14:34 server1 sshd[12349]: Failed password for admin from 203.0.113.42 port 52345 ssh2
Aug 2 03:14:37 server1 sshd[12350]: Accepted password for admin from 203.0.113.42 port 52346 ssh2
Aug 2 04:22:01 server1 kernel: TCP: connection attempt from 198.51.100.77 port 80 (http)
Aug 2 04:22:01 server1 kernel: TCP: connection attempt from 198.51.100.77 port 443 (https)
Aug 2 04:22:02 server1 kernel: TCP: connection attempt from 198.51.100.77 port 22 (ssh)
Aug 2 04:22:02 server1 kernel: TCP: connection attempt from 198.51.100.77 port 3306 (mysql)
Aug 2 04:22:03 server1 kernel: TCP: connection attempt from 198.51.100.77 port 5432 (postgres)
Aug 2 04:22:03 server1 kernel: TCP: connection attempt from 198.51.100.77 port 6379 (redis)
Aug 2 04:22:04 server1 kernel: TCP: connection attempt from 198.51.100.77 port 27017 (mongodb)
Aug 2 04:22:04 server1 kernel: TCP: connection attempt from 198.51.100.77 port 8080 (http-alt)
"""
What does this log contain?
This log simulates two simultaneous attacks from different adversaries. The first cluster (lines 1-6) is an SSH brute force from 203.0.113.42 that ends in a successful login. The second cluster (lines 7-14) is a port scan from 198.51.100.77 probing 8 different services.
Both IP ranges (203.0.113.x and 198.51.100.x) are reserved for documentation per RFC 5737. They will never route to real hosts.
Update the main block to run both scenarios
Your main block currently runs only the brute force scenario. You need to add a second pipeline run that feeds in the multi-attack log.
- Scroll to the bottom of main.py and find the line run_pipeline(SAMPLE_LOG_BRUTE_FORCE).
- Add the following code directly below that line to run a second scenario:
print("\n\n")
print("="*60)
print("\n[Scenario 2] Multi-Attack: Brute Force + Port Scan")
print("-"*40)
run_pipeline(SAMPLE_LOG_MULTI_ATTACK)
What does this code do?
This adds a clearly labelled second scenario to your program's output. The separator lines and header make it easy to distinguish where Scenario 1 ends and Scenario 2 begins when you read the terminal output.
- Save main.py.
Run the full pipeline and observe the multi-attack report
Before you run this, think about what you expect to happen. The brute force scenario worked with one attack type. Will the agents correctly separate two distinct attacks from different IPs and give each its own classification?
- Run the pipeline by executing this command in your terminal:
python main.py
The program runs both scenarios sequentially. Scenario 1 (brute force only) runs first, then Scenario 2 (multi-attack) follows.
What should I see in the Scenario 2 output?
Look for these results in the Scenario 2 output to confirm your agents handled the multi-attack correctly:
- The Log Parser extracts two distinct indicator clusters: failed logins from 203.0.113.42 AND port scan activity from 198.51.100.77.
- The Threat Classifier maps the brute force to T1110 (Brute Force) and the port scan to T1046 (Network Service Discovery).
- The Response Advisor provides different remediation for each attack. Brute force remediation focuses on account lockout, MFA, and fail2ban. Port scan remediation focuses on firewall rules and service audits.
Not seeing two distinct attack classifications?
- Check that your SAMPLE_LOG_MULTI_ATTACK includes both the SSH entries (lines with sshd) and the TCP connection entries (lines with kernel: TCP).
- If you see an API error, check that your ANTHROPIC_API_KEY environment variable is still set in this terminal session.
- If the Classifier only identifies one attack type, the LLM may occasionally combine them. Run the script again. The agents should separate them on subsequent runs.
Why aren't my agents classifying both attacks separately?
Why does this prove multi-agent collaboration works?
Each agent handled its piece of the puzzle without knowing the full picture. The Log Parser did not classify. The Classifier did not remediate. Yet the final report is a complete threat analysis with distinct technique IDs and tailored playbooks for each attack.
A single monolithic prompt would need to parse, classify, and remediate all at once. Splitting responsibilities into specialized agents produces more focused, accurate results on each sub-task.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py with both the SAMPLE_LOG_MULTI_ATTACK constant and the updated main block running both scenarios.
ⓧ I'd like to double check the full code
Your main.py is too long to show in one block, so confirm these sections are present in order:
- Imports and client setup at the top (import os, re, json, anthropic).
- SAMPLE_LOG_BRUTE_FORCE followed by SAMPLE_LOG_MULTI_ATTACK (the new constant with 14 log lines).
- Tool definitions (PARSE_LOG_TOOL, MITRE_LOOKUP_TOOL, REMEDIATION_TOOL).
- MITRE_DATABASE and REMEDIATION_DATABASE dictionaries.
- execute_tool, execute_parse_log, execute_mitre_lookup, and execute_remediation functions.
- run_agent function (manual agentic loop).
- Agent system prompts (LOG_PARSER_SYSTEM, THREAT_CLASSIFIER_SYSTEM, RESPONSE_ADVISOR_SYSTEM).
- run_pipeline function chaining all three agents.
- Main block running Scenario 1 (SAMPLE_LOG_BRUTE_FORCE) then Scenario 2 (SAMPLE_LOG_MULTI_ATTACK).
Your agent team just proved it can handle real-world complexity. Multiple threats, multiple sources, and each one correctly classified and remediated. Next up, the Secret Mission challenges you to add severity scoring so your team can prioritize which threats need attention first.
Secret mission
Add Severity Scoring
Real SOC teams don't just classify threats. They prioritize them. Can you extend your agent team with a severity scoring system that calculates a composite threat score based on attempt count, success indicators, and targeted service criticality?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally with pay-per-use API calls, so there are no ongoing costs unless you actively run the script again.
Resources you used:
- The threat-agent-team project folder on your Desktop containing main.py.
- The anthropic Python package installed via pip.
- The ANTHROPIC_API_KEY environment variable set in your terminal session.
Keep everything running
No action needed. Choose this if you are still actively building or want to experiment with new log scenarios and additional agents.
- Your threat-agent-team folder stays on your Desktop, ready to run anytime.
- The anthropic package remains installed in your Python environment.
- Your ANTHROPIC_API_KEY environment variable persists until you close the terminal session.
- Each run of the script costs a few cents in Claude API usage. You are only charged when you actively run the script.
Pause - I'll come back to this later
Close your terminal to clear the environment variable. Your project files and installed package stay intact for when you return.
- Close your terminal window. This clears the ANTHROPIC_API_KEY environment variable from memory.
- Your threat-agent-team folder and the anthropic package remain on disk.
- When you come back, re-set the environment variable in a new terminal session before running the script.
Delete - I don't want to use this again
Remove all project files and uninstall the package to start fresh.
- Close your terminal window to clear the ANTHROPIC_API_KEY environment variable.
- Delete the project folder by running this command in a new terminal:
macOS
rm -rf ~/Desktop/threat-agent-team
Windows
Remove-Item -Recurse -Force ~\Desktop\threat-agent-team
- Uninstall the anthropic package by running:
pip uninstall anthropic -y
- Confirm both are gone by running ls ~/Desktop (macOS) or dir ~\Desktop (Windows). The threat-agent-team folder should no longer appear.
Nice Work!
Nice Work!
You just built a multi-agent cybersecurity threat analysis system from scratch. Your three specialized AI agents collaborate to turn raw security logs into classified threats with actionable remediation playbooks.
You've learned how to:
- Build a multi-agent pipeline where 3 specialized AI agents (Log Parser, Threat Classifier, Response Advisor) pass structured data between each other to solve a complex problem.
- Implement Claude's tool use API with custom tool schemas and the manual agentic loop pattern that runs until the model signals end_turn.
- Map real attack patterns to MITRE ATT&CK technique IDs (T1110, T1046, T1595) and generate prioritized incident response playbooks from raw security logs.
- Secret Mission: Added composite severity scoring to prioritize threats for SOC analysts.
Ready to quiz yourself?