AI-Powered Purple Team Dashboard

Build a cybersecurity training simulator with ML attack and defense modes.

Introduction

30 Second Summary

Every cybersecurity team needs practice on both sides of the battlefield. The best defenders understand how attackers think. The best attackers know what defenders look for.

In this project, you will build CyberSim Arena, a dual-mode cybersecurity training dashboard where you practice offensive and defensive security against ML-powered opponents, with a Claude AI advisor available on demand.

What You'll Build

A full-stack purple team simulation platform with a Python/FastAPI backend running ML models and a React frontend styled like a Security Operations Center.

By the end of this project, you'll have:

  • An Isolation Forest anomaly detection model that scores network events and provides detection hints in defense mode.
  • A Markov chain attack generator trained on MITRE ATT&CK technique sequences for realistic multi-step attacks.
  • A Claude-powered advisor endpoint that provides detailed strategic analysis when you ask for help.
  • Secret Mission: Add a Purple Team Scorecard that tracks your performance across sessions and uses Claude to generate a personalized training report.

Are there any prerequisites?

You should have a basic understanding of cybersecurity concepts like red teaming and blue teaming. Familiarity with Python and JavaScript is also helpful.

You will also need an Anthropic API key for the Claude integration.

Before We Start

Cybersecurity training is often passive. You read about attacks, memorize frameworks, and hope you'll recognize threats when they appear in real logs.

In this project, you'll build CyberSim Arena. It's a dual-mode training dashboard where you practice both attacking and defending against ML-powered opponents, with a Claude advisor available on demand for strategic guidance.

In this step, get ready to:
  • Create the project directory structure with separate backend and frontend folders.
  • Set up a Python virtual environment for the backend.
  • Confirm Node.js is available for the frontend.
Open VS Code and create the project structure

CyberSim Arena has two halves: a Python backend that runs the simulation engine and ML models, and a React frontend that renders the SOC-style dashboard. Keeping them in separate folders lets you develop and scale each independently.

  • 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 built-in terminal by pressing Ctrl+` (both macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project folder with backend and frontend subdirectories by running these commands:
mkdir cybersim-arena
cd cybersim-arena
mkdir backend frontend
  • Confirm the folders exist by running:
ls

You should see backend and frontend listed in the output.

Don't see both folders?

Make sure you ran all three commands in order. If you accidentally skipped the cd cybersim-arena line, the mkdir backend frontend command will have created folders on your Desktop instead of inside the project.

help me fix my project directory structure

Set up a Python virtual environment

A virtual environment isolates your project's Python packages from other projects on your machine. This prevents version conflicts between different projects.

  • Move into the backend folder and create a virtual environment by running these commands:
cd backend
python3 -m venv venv

What does this command do?

python3 -m venv venv tells Python to run its built-in venv module and create a new virtual environment in a folder called venv.

  • Activate the virtual environment by running this command:

macOS

source venv/bin/activate

Windows

venv\Scripts\Activate.ps1

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

On Windows, PowerShell may block the activation script by default. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned first, then try the activation command again.

On macOS, double-check you typed source at the start of the command. Without it, the shell tries to execute the script directly.

help me activate my Python virtual environment

Confirm Node.js is available

The frontend uses Node.js to run Vite's build tooling and serve the React dashboard during development.

  • Check your Node.js version by running:
node --version

✔️ I see v18 or higher

You're all set. Node.js is ready for the frontend work in a later step.

ⓧ I see a version lower than 18

This project requires Node.js 18 or higher. Visit nodejs.org and download the LTS version, then run node --version again to confirm.

ⓧ Command not found

Node.js isn't installed yet. Visit nodejs.org and download the LTS installer for your operating system.

After installation completes, close and reopen your terminal, then run node --version to confirm it shows v18 or higher.

Your project skeleton is in place and your Python environment is isolated and ready for packages. Next up, you'll install the backend dependencies and scaffold the React frontend.

Set Up Project Structure and Dependencies

Before you can build the simulation engine and ML models, you need the right libraries in place.

In this step, you will install your Python backend dependencies and scaffold the React frontend. This gives you the full-stack foundation that the rest of the project builds on.

In this step, get ready to:
  • Create a requirements file and install Python backend packages.
  • Scaffold a React + TypeScript frontend with Vite.
  • Configure your Anthropic API key for Claude integration.
Create the requirements file and install backend dependencies

Python projects use a requirements.txt file to declare all the packages the project needs. This makes it easy to install everything at once.

  • In VS Code, create a new file called requirements.txt inside the backend/ folder.
  • Paste the following content into the file:
fastapi[standard]==0.136.3
anthropic==0.105.2
scikit-learn==1.8.0
numpy>=1.26.0

What are these packages?

  • fastapi[standard] is the web framework for your API. The [standard] extra bundles Uvicorn (the server) so you can run it with one command.
  • anthropic is the official Python SDK for calling Claude.
  • scikit-learn provides the Isolation Forest model you will use for anomaly detection.
  • numpy handles numerical arrays that scikit-learn depends on.
  • Save requirements.txt.
  • In your terminal (with the virtual environment still activated inside backend/), install all the packages by running:
pip install -r requirements.txt
  • Wait for the installation to finish. You should see a line near the end that starts with Successfully installed followed by the package names.

Seeing an error during install?

  • Make sure your virtual environment is activated. You should see (venv) or similar at the start of your terminal prompt.
  • Check that you are inside the backend/ directory where requirements.txt lives.
  • If you see a network timeout, check your internet connection and try again.

Why isn't pip finding my packages?

Scaffold the React frontend with Vite

The frontend will be a React app built with Vite, a fast build tool that provides instant hot-reload during development.

  • Open a second terminal in VS Code by pressing Cmd+Shift+` (macOS) or Ctrl+Shift+` (Windows).
  • Navigate to the project root by running:
cd ~/Desktop/cybersim-arena
  • Scaffold the React + TypeScript app into the frontend/ directory by running:
npm create vite@latest frontend -- --template react-ts

What does this command do?

npm create vite@latest downloads and runs the Vite scaffolding tool. The frontend argument tells it to use the existing frontend/ directory as the target. The -- --template react-ts flag skips all interactive prompts and directly creates a React + TypeScript project.

  • Move into the new frontend directory and install the dependencies by running:
cd frontend
npm install
  • Confirm the scaffold worked by running:
ls src
  • You should see files like App.tsx, main.tsx, and App.css listed in the output.

Don't see any files in src/?

  • Make sure you included the double dash -- before --template. Without it, npm does not pass the flag to Vite.
  • If the frontend/ folder already had files from a previous attempt, delete its contents and run the scaffold command again.

Help me fix my Vite scaffold

Set your Anthropic API key

The Anthropic Python SDK looks for an environment variable called ANTHROPIC_API_KEY to authenticate your requests to Claude.

  • Switch back to the terminal where your virtual environment is activated (the backend/ terminal).
  • Export your API key by running:
export ANTHROPIC_API_KEY=your-api-key-here
  • Replace your-api-key-here with the actual API key from your Anthropic dashboard.

Why an environment variable?

Storing your API key in an environment variable keeps it out of your source code. The Anthropic SDK automatically reads ANTHROPIC_API_KEY from the environment, so you never need to hard-code it.

Make it persist across sessions

The export command only lasts for the current terminal session. If you close and reopen your terminal, you will need to run it again. To avoid this, add the line to your shell profile (~/.zshrc on macOS or ~/.bashrc on Linux).

Not sure if the key is set?

Run echo $ANTHROPIC_API_KEY in the same terminal. You should see your key printed back. If it is blank, re-run the export command.

Help me set up my Anthropic API key

✔️ Awesome, I've got everything!

Great. Double check you have saved requirements.txt and that npm install finished without errors in your frontend terminal.

ⓧ I'd like to double check the full code

Here is the complete requirements.txt file:

fastapi[standard]==0.136.3
anthropic==0.105.2
scikit-learn==1.8.0
numpy>=1.26.0

Your environment is locked and loaded. Next up, you will build the simulation engine that generates realistic network traffic and attack events.

Build the Simulation Engine and Log Generator

Your development environment is set up and all dependencies are installed. Now it's time to build the brains behind CyberSim Arena.

Every security simulation needs realistic data to train against. In this step, you'll define attack technique data based on the MITRE ATT&CK framework, build a log generator that produces both normal and malicious network events, and wire them together inside a simulation engine that manages gameplay state.

In this step, get ready to:
  • Build MITRE ATT&CK technique data with tactic-to-technique mappings and transition probabilities.
  • Create a synthetic log generator that produces normal and malicious network events.
  • Implement a simulation engine that manages game state and processes player actions.
Create the MITRE ATT&CK data structures

The MITRE ATT&CK framework organizes real-world attack techniques into tactics (the attacker's goal) and techniques (how they achieve it). Your simulation needs this data to generate realistic multi-step attack chains.

  • In VS Code, create a new file called mitre_data.py inside your backend/ folder.
  • Add the attack techniques dictionary by pasting this code:
ATTACK_TECHNIQUES = {
    "T1190": {
        "name": "Exploit Public-Facing Application",
        "tactic": "Initial Access",
        "stealth": 0.4,
        "severity": "high",
        "log_signature": "HTTP exploit attempt on /admin endpoint - payload detected",
    },
    "T1078": {
        "name": "Valid Accounts",
        "tactic": "Initial Access",
        "stealth": 0.8,
        "severity": "medium",
        "log_signature": "Login with valid credentials from unusual geolocation",
    },
    "T1059": {
        "name": "Command and Scripting Interpreter",
        "tactic": "Execution",
        "stealth": 0.5,
        "severity": "high",
        "log_signature": "PowerShell execution with encoded command argument",
    },
    "T1053": {
        "name": "Scheduled Task/Job",
        "tactic": "Persistence",
        "stealth": 0.7,
        "severity": "medium",
        "log_signature": "New scheduled task created in /tmp directory",
    },
    "T1548": {
        "name": "Abuse Elevation Control Mechanism",
        "tactic": "Privilege Escalation",
        "stealth": 0.3,
        "severity": "critical",
        "log_signature": "sudo privilege escalation - unusual binary executed",
    },
    "T1070": {
        "name": "Indicator Removal",
        "tactic": "Defense Evasion",
        "stealth": 0.9,
        "severity": "high",
        "log_signature": "Auth log entries deleted - timestomping detected",
    },
    "T1003": {
        "name": "OS Credential Dumping",
        "tactic": "Credential Access",
        "stealth": 0.4,
        "severity": "critical",
        "log_signature": "Access to /etc/shadow - credential harvesting attempt",
    },
    "T1021": {
        "name": "Remote Services",
        "tactic": "Lateral Movement",
        "stealth": 0.6,
        "severity": "high",
        "log_signature": "SSH lateral movement - new internal connection from compromised host",
    },
    "T1041": {
        "name": "Exfiltration Over C2 Channel",
        "tactic": "Exfiltration",
        "stealth": 0.5,
        "severity": "critical",
        "log_signature": "Large outbound data transfer to unknown external IP",
    },
}

What does this code do?

Each technique ID (like T1190) is a real MITRE ATT&CK identifier. The dictionary maps each one to its properties:

  • name and tactic describe the technique and where it fits in the attack lifecycle.
  • stealth (0 to 1) controls how likely the ML model is to catch it. Higher stealth means harder to detect.
  • log_signature is the event text that appears in the log viewer when this technique executes.
  • Below the ATTACK_TECHNIQUES dictionary, add the tactic chain and transition probabilities:
TACTIC_CHAIN = [
    "Initial Access",
    "Execution",
    "Persistence",
    "Privilege Escalation",
    "Defense Evasion",
    "Credential Access",
    "Lateral Movement",
    "Exfiltration",
]

TRANSITION_PROBABILITIES = {
    "Initial Access": {"Execution": 0.6, "Persistence": 0.3, "Defense Evasion": 0.1},
    "Execution": {"Persistence": 0.4, "Privilege Escalation": 0.4, "Defense Evasion": 0.2},
    "Persistence": {"Privilege Escalation": 0.5, "Defense Evasion": 0.3, "Credential Access": 0.2},
    "Privilege Escalation": {"Defense Evasion": 0.3, "Credential Access": 0.5, "Lateral Movement": 0.2},
    "Defense Evasion": {"Credential Access": 0.5, "Lateral Movement": 0.3, "Exfiltration": 0.2},
    "Credential Access": {"Lateral Movement": 0.7, "Exfiltration": 0.3},
    "Lateral Movement": {"Exfiltration": 0.6, "Credential Access": 0.2, "Privilege Escalation": 0.2},
    "Exfiltration": {},
}

What are transition probabilities?

TACTIC_CHAIN defines the typical order attackers progress through. TRANSITION_PROBABILITIES maps each tactic to the likelihood of moving to the next one. For example, after "Initial Access", there's a 60% chance the attacker moves to "Execution" and a 30% chance they jump to "Persistence".

These probabilities power the Markov chain attack generator you'll build in Step 4. They make AI-generated attack sequences feel realistic rather than random.

  • Save mitre_data.py.

Seeing a syntax error?

Make sure every dictionary entry has a trailing comma after the closing brace. Python is forgiving about trailing commas, but a missing comma between entries causes a syntax error.

help me fix a syntax error in my mitre_data.py file

✔️ Awesome, I've got everything!

Great. Make sure you've saved mitre_data.py before moving on.

ⓧ I'd like to double check the full code

ATTACK_TECHNIQUES = {
    "T1190": {
        "name": "Exploit Public-Facing Application",
        "tactic": "Initial Access",
        "stealth": 0.4,
        "severity": "high",
        "log_signature": "HTTP exploit attempt on /admin endpoint - payload detected",
    },
    "T1078": {
        "name": "Valid Accounts",
        "tactic": "Initial Access",
        "stealth": 0.8,
        "severity": "medium",
        "log_signature": "Login with valid credentials from unusual geolocation",
    },
    "T1059": {
        "name": "Command and Scripting Interpreter",
        "tactic": "Execution",
        "stealth": 0.5,
        "severity": "high",
        "log_signature": "PowerShell execution with encoded command argument",
    },
    "T1053": {
        "name": "Scheduled Task/Job",
        "tactic": "Persistence",
        "stealth": 0.7,
        "severity": "medium",
        "log_signature": "New scheduled task created in /tmp directory",
    },
    "T1548": {
        "name": "Abuse Elevation Control Mechanism",
        "tactic": "Privilege Escalation",
        "stealth": 0.3,
        "severity": "critical",
        "log_signature": "sudo privilege escalation - unusual binary executed",
    },
    "T1070": {
        "name": "Indicator Removal",
        "tactic": "Defense Evasion",
        "stealth": 0.9,
        "severity": "high",
        "log_signature": "Auth log entries deleted - timestomping detected",
    },
    "T1003": {
        "name": "OS Credential Dumping",
        "tactic": "Credential Access",
        "stealth": 0.4,
        "severity": "critical",
        "log_signature": "Access to /etc/shadow - credential harvesting attempt",
    },
    "T1021": {
        "name": "Remote Services",
        "tactic": "Lateral Movement",
        "stealth": 0.6,
        "severity": "high",
        "log_signature": "SSH lateral movement - new internal connection from compromised host",
    },
    "T1041": {
        "name": "Exfiltration Over C2 Channel",
        "tactic": "Exfiltration",
        "stealth": 0.5,
        "severity": "critical",
        "log_signature": "Large outbound data transfer to unknown external IP",
    },
}

TACTIC_CHAIN = [
    "Initial Access",
    "Execution",
    "Persistence",
    "Privilege Escalation",
    "Defense Evasion",
    "Credential Access",
    "Lateral Movement",
    "Exfiltration",
]

TRANSITION_PROBABILITIES = {
    "Initial Access": {"Execution": 0.6, "Persistence": 0.3, "Defense Evasion": 0.1},
    "Execution": {"Persistence": 0.4, "Privilege Escalation": 0.4, "Defense Evasion": 0.2},
    "Persistence": {"Privilege Escalation": 0.5, "Defense Evasion": 0.3, "Credential Access": 0.2},
    "Privilege Escalation": {"Defense Evasion": 0.3, "Credential Access": 0.5, "Lateral Movement": 0.2},
    "Defense Evasion": {"Credential Access": 0.5, "Lateral Movement": 0.3, "Exfiltration": 0.2},
    "Credential Access": {"Lateral Movement": 0.7, "Exfiltration": 0.3},
    "Lateral Movement": {"Exfiltration": 0.6, "Credential Access": 0.2, "Privilege Escalation": 0.2},
    "Exfiltration": {},
}
Build the synthetic log generator

A security analyst's primary input is log data. Your log generator creates realistic network events that mimic what a real SOC dashboard would display.

  • Create a new file called log_generator.py inside your backend/ folder.
  • Add the imports and baseline data by pasting this code:
import random
import string
import time
import uuid


NORMAL_ACTIONS = [
    "SSH login successful",
    "HTTP GET /api/health",
    "DNS query resolved",
    "File read /var/log/syslog",
    "Process started: nginx",
    "Cron job executed",
    "HTTP POST /api/data",
    "Database query executed",
    "User session created",
    "Certificate renewed",
]

INTERNAL_IPS = ["10.0.1.5", "10.0.1.12", "10.0.1.30", "10.0.2.7", "10.0.2.15"]
EXTERNAL_IPS = ["203.0.113.42", "198.51.100.77", "192.0.2.100", "45.33.32.156"]
USERS = ["admin", "deploy", "www-data", "jenkins", "developer", "root"]


class LogGenerator:
    def generate_normal_traffic(self, count: int = 20) -> list:
        events = []
        for _ in range(count):
            events.append(self._normal_event())
        return events

    def generate_mixed_traffic(self, normal_count: int = 20, attack_count: int = 5) -> list:
        events = self.generate_normal_traffic(normal_count)
        for _ in range(attack_count):
            events.append(self._malicious_event())
        random.shuffle(events)
        return events

What does this code do?

  • NORMAL_ACTIONS lists benign events a SOC analyst would see in everyday traffic.
  • INTERNAL_IPS and EXTERNAL_IPS simulate a network with internal hosts and external attackers.
  • generate_normal_traffic() creates a batch of safe events. generate_mixed_traffic() injects malicious events into normal traffic and shuffles the order so attacks are hidden.
  • Below the generate_mixed_traffic method, add the attack event generator and private helper methods:
    def generate_attack_event(self, technique_id: str) -> list:
        from mitre_data import ATTACK_TECHNIQUES

        tech = ATTACK_TECHNIQUES.get(technique_id, {})
        return [
            {
                "id": str(uuid.uuid4())[:8],
                "timestamp": time.time(),
                "source_ip": random.choice(EXTERNAL_IPS),
                "dest_ip": random.choice(INTERNAL_IPS),
                "user": random.choice(USERS),
                "action": tech.get("log_signature", "Unknown action"),
                "severity": tech.get("severity", "medium"),
                "is_malicious": True,
                "technique": technique_id,
            }
        ]

    def _normal_event(self) -> dict:
        return {
            "id": str(uuid.uuid4())[:8],
            "timestamp": time.time() - random.randint(0, 3600),
            "source_ip": random.choice(INTERNAL_IPS),
            "dest_ip": random.choice(INTERNAL_IPS),
            "user": random.choice(USERS),
            "action": random.choice(NORMAL_ACTIONS),
            "severity": "low",
            "is_malicious": False,
            "technique": None,
        }

    def _malicious_event(self) -> dict:
        from mitre_data import ATTACK_TECHNIQUES

        technique_id = random.choice(list(ATTACK_TECHNIQUES.keys()))
        tech = ATTACK_TECHNIQUES[technique_id]
        return {
            "id": str(uuid.uuid4())[:8],
            "timestamp": time.time() - random.randint(0, 1800),
            "source_ip": random.choice(EXTERNAL_IPS),
            "dest_ip": random.choice(INTERNAL_IPS),
            "user": random.choice(USERS),
            "action": tech["log_signature"],
            "severity": tech["severity"],
            "is_malicious": True,
            "technique": technique_id,
        }

What does this code do?

  • generate_attack_event() creates a log entry for a specific technique when the player executes an attack in attack mode.
  • _normal_event() builds a single benign event with internal IPs, low severity, and no technique tag.
  • _malicious_event() picks a random technique from ATTACK_TECHNIQUES and generates a corresponding log entry with external source IPs and the technique's severity level.

Notice that malicious events use EXTERNAL_IPS as the source while normal events use INTERNAL_IPS. This is one of the features the ML model will learn to detect.

  • Save log_generator.py.
  • Verify the file works by running this in your terminal:
python -c "from log_generator import LogGenerator; lg = LogGenerator(); print(len(lg.generate_mixed_traffic()))"

You should see 25 printed (20 normal + 5 attack events).

Seeing a ModuleNotFoundError?

Make sure you're running the command from inside the backend/ folder. Run cd backend first if you're in the project root. Also confirm your virtual environment is still activated.

help me fix a ModuleNotFoundError in my log generator

✔️ Awesome, I've got everything!

Great. Make sure you've saved log_generator.py before moving on.

ⓧ I'd like to double check the full code

import random
import string
import time
import uuid


NORMAL_ACTIONS = [
    "SSH login successful",
    "HTTP GET /api/health",
    "DNS query resolved",
    "File read /var/log/syslog",
    "Process started: nginx",
    "Cron job executed",
    "HTTP POST /api/data",
    "Database query executed",
    "User session created",
    "Certificate renewed",
]

INTERNAL_IPS = ["10.0.1.5", "10.0.1.12", "10.0.1.30", "10.0.2.7", "10.0.2.15"]
EXTERNAL_IPS = ["203.0.113.42", "198.51.100.77", "192.0.2.100", "45.33.32.156"]
USERS = ["admin", "deploy", "www-data", "jenkins", "developer", "root"]


class LogGenerator:
    def generate_normal_traffic(self, count: int = 20) -> list:
        events = []
        for _ in range(count):
            events.append(self._normal_event())
        return events

    def generate_mixed_traffic(self, normal_count: int = 20, attack_count: int = 5) -> list:
        events = self.generate_normal_traffic(normal_count)
        for _ in range(attack_count):
            events.append(self._malicious_event())
        random.shuffle(events)
        return events

    def generate_attack_event(self, technique_id: str) -> list:
        from mitre_data import ATTACK_TECHNIQUES

        tech = ATTACK_TECHNIQUES.get(technique_id, {})
        return [
            {
                "id": str(uuid.uuid4())[:8],
                "timestamp": time.time(),
                "source_ip": random.choice(EXTERNAL_IPS),
                "dest_ip": random.choice(INTERNAL_IPS),
                "user": random.choice(USERS),
                "action": tech.get("log_signature", "Unknown action"),
                "severity": tech.get("severity", "medium"),
                "is_malicious": True,
                "technique": technique_id,
            }
        ]

    def _normal_event(self) -> dict:
        return {
            "id": str(uuid.uuid4())[:8],
            "timestamp": time.time() - random.randint(0, 3600),
            "source_ip": random.choice(INTERNAL_IPS),
            "dest_ip": random.choice(INTERNAL_IPS),
            "user": random.choice(USERS),
            "action": random.choice(NORMAL_ACTIONS),
            "severity": "low",
            "is_malicious": False,
            "technique": None,
        }

    def _malicious_event(self) -> dict:
        from mitre_data import ATTACK_TECHNIQUES

        technique_id = random.choice(list(ATTACK_TECHNIQUES.keys()))
        tech = ATTACK_TECHNIQUES[technique_id]
        return {
            "id": str(uuid.uuid4())[:8],
            "timestamp": time.time() - random.randint(0, 1800),
            "source_ip": random.choice(EXTERNAL_IPS),
            "dest_ip": random.choice(INTERNAL_IPS),
            "user": random.choice(USERS),
            "action": tech["log_signature"],
            "severity": tech["severity"],
            "is_malicious": True,
            "technique": technique_id,
        }
Implement the simulation state manager

The simulation engine ties everything together. It manages what mode you're playing, tracks scores, and decides what happens when you take an action.

  • Create a new file called simulation.py inside your backend/ folder.
  • Add the imports, constructor, and game start logic by pasting this code:
import random
import time
from log_generator import LogGenerator
from mitre_data import ATTACK_TECHNIQUES, TACTIC_CHAIN


class SimulationEngine:
    def __init__(self):
        self.current_mode = None
        self.round_number = 0
        self.events = []
        self.score = {"player": 0, "ml": 0}
        self.log_gen = LogGenerator()
        self.attack_chain = []
        self.detected_techniques = []

    def start(self, mode: str):
        self.current_mode = mode
        self.round_number = 0
        self.events = []
        self.score = {"player": 0, "ml": 0}
        self.attack_chain = []
        self.detected_techniques = []
        if mode == "defense":
            self.events = self.log_gen.generate_mixed_traffic(normal_count=20, attack_count=5)
        else:
            self.events = self.log_gen.generate_normal_traffic(count=20)

What does this code do?

  • The constructor initializes all game state: mode, round counter, event log, scores, and tracking lists.
  • start() resets everything for a new game. In defense mode, it generates mixed traffic (normal + attacks hidden inside). In attack mode, it starts with only normal traffic that you'll try to blend into.
  • Below the start method, add the action processing logic:
    def process_action(self, action: str) -> dict:
        if self.current_mode == "defense":
            return self._process_defense_action(action)
        else:
            return self._process_attack_action(action)

    def _process_defense_action(self, action: str) -> dict:
        if action.startswith("flag:"):
            event_id = action.split(":")[1]
            event = next((e for e in self.events if e["id"] == event_id), None)
            if event and event.get("is_malicious"):
                self.score["player"] += 10
                self.detected_techniques.append(event.get("technique", "Unknown"))
                return {"result": "correct", "message": "Threat identified!", "score": self.score}
            else:
                self.score["player"] -= 5
                return {"result": "incorrect", "message": "False positive - that was normal traffic.", "score": self.score}
        return {"result": "invalid", "message": "Unknown action"}

    def _process_attack_action(self, action: str) -> dict:
        technique = action
        if technique in ATTACK_TECHNIQUES:
            tech_data = ATTACK_TECHNIQUES[technique]
            stealth = tech_data["stealth"]
            detection_roll = random.random()
            if detection_roll > stealth:
                self.score["ml"] += 10
                return {
                    "result": "detected",
                    "message": f"Your {tech_data['name']} was detected by the ML model!",
                    "score": self.score,
                }
            else:
                self.score["player"] += 10
                self.attack_chain.append(technique)
                new_events = self.log_gen.generate_attack_event(technique)
                self.events.extend(new_events)
                return {
                    "result": "success",
                    "message": f"{tech_data['name']} executed successfully. Stayed under the radar.",
                    "score": self.score,
                }
        return {"result": "invalid", "message": "Unknown technique"}

How does the scoring work?

In defense mode, you flag suspicious events. Correctly flagging a malicious event earns +10 points. Flagging normal traffic (a false positive) costs -5. This mirrors real SOC work where false alarms waste resources.

In attack mode, you pick techniques to execute. The stealth value is compared against a random roll. If the roll exceeds stealth, you're caught (ML scores +10). If you stay under, you score +10 and the technique is added to your attack chain.

  • Below the _process_attack_action method, add the round advancement and state retrieval methods:
    def advance_round(self) -> dict:
        self.round_number += 1
        if self.current_mode == "defense":
            new_events = self.log_gen.generate_mixed_traffic(normal_count=15, attack_count=3)
            self.events.extend(new_events)
        return {"round": self.round_number, "new_events": len(self.events), "score": self.score}

    def get_state(self) -> dict:
        return {
            "mode": self.current_mode,
            "round": self.round_number,
            "score": self.score,
            "total_events": len(self.events),
            "attack_chain": self.attack_chain,
            "detected_techniques": self.detected_techniques,
        }

    def get_recent_events(self) -> list:
        return self.events[-30:]

What does this code do?

  • advance_round() moves the game forward. In defense mode, it floods in fresh events (15 normal + 3 attacks) to keep the pressure on.
  • get_state() returns a snapshot of the full game state. This is what gets passed to Claude as context when you ask for advice.
  • get_recent_events() returns only the last 30 events. This keeps the UI manageable instead of showing hundreds of accumulated log lines.
  • Save simulation.py.
  • Verify the simulation engine works by running this in your terminal:
python -c "from simulation import SimulationEngine; e = SimulationEngine(); e.start('defense'); print(e.get_state())"

You should see a dictionary with mode set to defense, total_events at 25, and scores at 0.

Seeing an ImportError?

Check that all three files (mitre_data.py, log_generator.py, simulation.py) are in the same backend/ folder and that you're running the command from inside that folder.

help me fix an ImportError in my simulation engine

✔️ Awesome, I've got everything!

Great. Make sure you've saved simulation.py before moving on.

ⓧ I'd like to double check the full code

import random
import time
from log_generator import LogGenerator
from mitre_data import ATTACK_TECHNIQUES, TACTIC_CHAIN


class SimulationEngine:
    def __init__(self):
        self.current_mode = None
        self.round_number = 0
        self.events = []
        self.score = {"player": 0, "ml": 0}
        self.log_gen = LogGenerator()
        self.attack_chain = []
        self.detected_techniques = []

    def start(self, mode: str):
        self.current_mode = mode
        self.round_number = 0
        self.events = []
        self.score = {"player": 0, "ml": 0}
        self.attack_chain = []
        self.detected_techniques = []
        if mode == "defense":
            self.events = self.log_gen.generate_mixed_traffic(normal_count=20, attack_count=5)
        else:
            self.events = self.log_gen.generate_normal_traffic(count=20)

    def process_action(self, action: str) -> dict:
        if self.current_mode == "defense":
            return self._process_defense_action(action)
        else:
            return self._process_attack_action(action)

    def _process_defense_action(self, action: str) -> dict:
        if action.startswith("flag:"):
            event_id = action.split(":")[1]
            event = next((e for e in self.events if e["id"] == event_id), None)
            if event and event.get("is_malicious"):
                self.score["player"] += 10
                self.detected_techniques.append(event.get("technique", "Unknown"))
                return {"result": "correct", "message": "Threat identified!", "score": self.score}
            else:
                self.score["player"] -= 5
                return {"result": "incorrect", "message": "False positive - that was normal traffic.", "score": self.score}
        return {"result": "invalid", "message": "Unknown action"}

    def _process_attack_action(self, action: str) -> dict:
        technique = action
        if technique in ATTACK_TECHNIQUES:
            tech_data = ATTACK_TECHNIQUES[technique]
            stealth = tech_data["stealth"]
            detection_roll = random.random()
            if detection_roll > stealth:
                self.score["ml"] += 10
                return {
                    "result": "detected",
                    "message": f"Your {tech_data['name']} was detected by the ML model!",
                    "score": self.score,
                }
            else:
                self.score["player"] += 10
                self.attack_chain.append(technique)
                new_events = self.log_gen.generate_attack_event(technique)
                self.events.extend(new_events)
                return {
                    "result": "success",
                    "message": f"{tech_data['name']} executed successfully. Stayed under the radar.",
                    "score": self.score,
                }
        return {"result": "invalid", "message": "Unknown technique"}

    def advance_round(self) -> dict:
        self.round_number += 1
        if self.current_mode == "defense":
            new_events = self.log_gen.generate_mixed_traffic(normal_count=15, attack_count=3)
            self.events.extend(new_events)
        return {"round": self.round_number, "new_events": len(self.events), "score": self.score}

    def get_state(self) -> dict:
        return {
            "mode": self.current_mode,
            "round": self.round_number,
            "score": self.score,
            "total_events": len(self.events),
            "attack_chain": self.attack_chain,
            "detected_techniques": self.detected_techniques,
        }

    def get_recent_events(self) -> list:
        return self.events[-30:]

Your simulation engine can now generate realistic network traffic and process player actions in both modes. Next up, you'll build the ML detection layer that acts as the blue team's brain.

Build the ML Detection and Attack Models

Your simulation engine can generate events and track game state. Now you need the brains behind both sides of the battle.

In this step, you'll build two ML models that power the core gameplay. The DetectionModel uses an Isolation Forest to score network events for anomalies (the blue team brain). The AttackGenerator uses a Markov chain to produce realistic multi-step attack sequences (the red team brain).

In this step, get ready to:
  • Train an Isolation Forest anomaly detection model on synthetic normal traffic.
  • Build an anomaly scoring pipeline with an ambient hint system.
  • Create a Markov chain attack generator using MITRE ATT&CK transition probabilities.
Train the Isolation Forest anomaly detection model

An Isolation Forest works by randomly partitioning data points. Anomalies are easier to isolate because they sit far from normal clusters. The model learns what "normal" looks like by training on clean traffic, then flags anything that deviates.

  • Create a new file called ml_models.py inside your backend/ folder.
  • Add the imports and the DetectionModel class with its training logic by copying this code:
import numpy as np
from sklearn.ensemble import IsolationForest


class DetectionModel:
    def __init__(self):
        self.model = IsolationForest(n_estimators=100, contamination=0.15, random_state=42)
        self._train_on_normal()

    def _train_on_normal(self):
        # Generate synthetic normal traffic and fit the model
        normal_data = self._generate_normal_features(200)
        self.model.fit(normal_data)

    def _generate_normal_features(self, count: int) -> np.ndarray:
        # Each event becomes 5 numeric features representing normal behavior
        features = []
        for _ in range(count):
            features.append([
                np.random.normal(8, 2),
                np.random.normal(0.1, 0.05),
                np.random.normal(1024, 200),
                np.random.normal(5, 1),
                np.random.normal(0.02, 0.01),
            ])
        return np.array(features)

What does this code do?

The DetectionModel trains itself immediately on construction. It generates 200 synthetic "normal" data points, each with 5 features (action length, external flag, severity, time factor, and risk). The Isolation Forest learns the boundaries of normal behavior from these samples.

The contamination=0.15 parameter tells the model to expect roughly 15% of real-world data to be anomalous. This tunes how aggressively it flags outliers.

Why Isolation Forest for anomaly detection?

Unlike supervised classifiers that need labeled attack data, Isolation Forest is unsupervised. It only needs examples of normal traffic. This mirrors real-world SOC environments where you know what "normal" looks like but can't predict every possible attack pattern.

The algorithm isolates anomalies by randomly splitting features. Outliers require fewer splits to isolate, so they get shorter path lengths and lower anomaly scores.

  • Save ml_models.py.
  • You won't see a visual change yet. The next chunk adds the scoring methods that use this trained model.
Build the anomaly scoring and hint system

The trained model needs a way to convert raw log events into anomaly scores. It also needs a hint system that nudges the player toward suspicious events without revealing the answer directly.

  • In ml_models.py, add the following methods inside the DetectionModel class, below _generate_normal_features:
    def _event_to_features(self, event: dict) -> list:
        # Convert a log event dict into the 5 numeric features the model expects
        severity_map = {"low": 0.1, "medium": 0.5, "high": 0.8, "critical": 1.0}
        is_external = 1.0 if event.get("source_ip", "").startswith(("203", "198", "192.0.2", "45")) else 0.0
        severity_score = severity_map.get(event.get("severity", "low"), 0.1)
        action_len = len(event.get("action", ""))
        hour_factor = (event.get("timestamp", 0) % 86400) / 86400
        return [
            action_len / 10.0,
            is_external,
            severity_score * 1000,
            hour_factor * 10,
            severity_score,
        ]

    def score_events(self, events: list) -> list:
        # Score each event and assign a risk level based on anomaly score
        if not events:
            return []
        features = np.array([self._event_to_features(e) for e in events])
        scores = self.model.score_samples(features)
        results = []
        for i, event in enumerate(events):
            anomaly_score = float(-scores[i])
            risk_level = "low"
            if anomaly_score > 0.1:
                risk_level = "medium"
            if anomaly_score > 0.3:
                risk_level = "high"
            if anomaly_score > 0.5:
                risk_level = "critical"
            results.append({
                "event_id": event["id"],
                "anomaly_score": round(anomaly_score, 3),
                "risk_level": risk_level,
                "hint": self._generate_hint(event, anomaly_score),
            })
        return results

    def _generate_hint(self, event: dict, score: float) -> str:
        # Return a vague hint based on score thresholds (never a direct answer)
        if score > 0.5:
            return "Unusual pattern detected in this event cluster."
        elif score > 0.3:
            return "This event has some anomalous characteristics."
        elif score > 0.1:
            return "Slightly elevated risk indicators present."
        return ""

What does this code do?

  • _event_to_features() transforms a raw log event dictionary into the same 5-dimensional feature vector the model was trained on. It checks whether the IP is external, maps severity strings to numbers, and calculates time-of-day factors.
  • score_events() calls the Isolation Forest's score_samples() method. Lower raw scores mean more anomalous. The code negates the scores so that higher values mean more suspicious, then maps them to risk levels.
  • _generate_hint() provides vague nudges based on score thresholds. It never tells the player exactly what's wrong. This forces active learning.
  • Save ml_models.py.
  • Test that the detection model works by running this in your terminal:
python -c "from ml_models import DetectionModel; d = DetectionModel(); print('DetectionModel trained successfully!')"

You should see DetectionModel trained successfully! printed in the terminal.

Seeing a ModuleNotFoundError?

  • Make sure your virtual environment is activated. You should see (venv) in your terminal prompt.
  • Confirm you're running the command from inside the backend/ folder where ml_models.py lives.
  • Check that scikit-learn installed correctly by running pip list | grep scikit.

Help me debug my DetectionModel import error

Build the Markov chain attack generator

A Markov chain models sequences where the next step depends only on the current step. Real attackers follow predictable patterns. After gaining initial access, they typically move to execution or persistence. These transition probabilities let the red team brain generate realistic multi-step attack chains.

  • In ml_models.py, add the AttackGenerator class below the DetectionModel class:
class AttackGenerator:
    def __init__(self):
        from mitre_data import TRANSITION_PROBABILITIES, ATTACK_TECHNIQUES
        self.transitions = TRANSITION_PROBABILITIES
        self.techniques = ATTACK_TECHNIQUES

    def generate_attack_chain(self, length: int = 5) -> list:
        # Walk the Markov chain to produce a realistic attack sequence
        chain = []
        current_tactic = "Initial Access"
        for _ in range(length):
            candidates = [t for t in self.techniques.values() if t["tactic"] == current_tactic]
            if candidates:
                chosen = candidates[0]
                tech_id = next(k for k, v in self.techniques.items() if v == chosen)
                chain.append({"technique_id": tech_id, "name": chosen["name"], "tactic": current_tactic})
            next_tactics = self.transitions.get(current_tactic, {})
            if not next_tactics:
                break
            tactics = list(next_tactics.keys())
            weights = list(next_tactics.values())
            current_tactic = np.random.choice(tactics, p=weights)
        return chain

    def get_evasion_hints(self, state: dict) -> list:
        # Provide contextual hints to the attacker based on chain progress
        hints = []
        chain = state.get("attack_chain", [])
        if len(chain) == 0:
            hints.append({"type": "suggestion", "message": "Consider starting with a stealthy initial access technique."})
        elif len(chain) < 3:
            hints.append({"type": "suggestion", "message": "The detection model is learning your patterns. Vary your approach."})
        else:
            hints.append({"type": "warning", "message": "Extended activity increases detection probability. Consider exfiltrating soon."})
        return hints

What does this code do?

  • generate_attack_chain() walks through the MITRE ATT&CK tactic chain using probability-weighted transitions. Starting from "Initial Access", it picks the next tactic based on TRANSITION_PROBABILITIES from your mitre_data.py file.
  • get_evasion_hints() provides ambient guidance to the attacker. If the attack chain is getting long, it warns that detection probability increases. This mirrors the ML hint system on the defense side.
  • np.random.choice(tactics, p=weights) selects the next tactic probabilistically. A 0.6 weight on "Execution" after "Initial Access" means 60% of the time the chain moves there next.
  • Save ml_models.py.
  • Test both models together by running this in your terminal:
python -c "from ml_models import DetectionModel, AttackGenerator; d = DetectionModel(); a = AttackGenerator(); print('Chain:', [s['tactic'] for s in a.generate_attack_chain()])"

You should see a list of tactics printed, starting with "Initial Access" and progressing through the attack chain. The exact sequence varies each run because of the probabilistic transitions.

Seeing an ImportError from mitre_data?

  • Confirm that mitre_data.py exists in the same backend/ folder as ml_models.py.
  • Check that TRANSITION_PROBABILITIES and ATTACK_TECHNIQUES are defined in your mitre_data.py file from the previous step.

Help me fix my AttackGenerator import issue

✔️ Awesome, I've got everything!

Great. Double check you've saved ml_models.py and that both test commands printed successfully.

ⓧ I'd like to double check the full code

import numpy as np
from sklearn.ensemble import IsolationForest


class DetectionModel:
    def __init__(self):
        self.model = IsolationForest(n_estimators=100, contamination=0.15, random_state=42)
        self._train_on_normal()

    def _train_on_normal(self):
        normal_data = self._generate_normal_features(200)
        self.model.fit(normal_data)

    def _generate_normal_features(self, count: int) -> np.ndarray:
        features = []
        for _ in range(count):
            features.append([
                np.random.normal(8, 2),
                np.random.normal(0.1, 0.05),
                np.random.normal(1024, 200),
                np.random.normal(5, 1),
                np.random.normal(0.02, 0.01),
            ])
        return np.array(features)

    def _event_to_features(self, event: dict) -> list:
        severity_map = {"low": 0.1, "medium": 0.5, "high": 0.8, "critical": 1.0}
        is_external = 1.0 if event.get("source_ip", "").startswith(("203", "198", "192.0.2", "45")) else 0.0
        severity_score = severity_map.get(event.get("severity", "low"), 0.1)
        action_len = len(event.get("action", ""))
        hour_factor = (event.get("timestamp", 0) % 86400) / 86400
        return [
            action_len / 10.0,
            is_external,
            severity_score * 1000,
            hour_factor * 10,
            severity_score,
        ]

    def score_events(self, events: list) -> list:
        if not events:
            return []
        features = np.array([self._event_to_features(e) for e in events])
        scores = self.model.score_samples(features)
        results = []
        for i, event in enumerate(events):
            anomaly_score = float(-scores[i])
            risk_level = "low"
            if anomaly_score > 0.1:
                risk_level = "medium"
            if anomaly_score > 0.3:
                risk_level = "high"
            if anomaly_score > 0.5:
                risk_level = "critical"
            results.append({
                "event_id": event["id"],
                "anomaly_score": round(anomaly_score, 3),
                "risk_level": risk_level,
                "hint": self._generate_hint(event, anomaly_score),
            })
        return results

    def _generate_hint(self, event: dict, score: float) -> str:
        if score > 0.5:
            return "Unusual pattern detected in this event cluster."
        elif score > 0.3:
            return "This event has some anomalous characteristics."
        elif score > 0.1:
            return "Slightly elevated risk indicators present."
        return ""


class AttackGenerator:
    def __init__(self):
        from mitre_data import TRANSITION_PROBABILITIES, ATTACK_TECHNIQUES
        self.transitions = TRANSITION_PROBABILITIES
        self.techniques = ATTACK_TECHNIQUES

    def generate_attack_chain(self, length: int = 5) -> list:
        chain = []
        current_tactic = "Initial Access"
        for _ in range(length):
            candidates = [t for t in self.techniques.values() if t["tactic"] == current_tactic]
            if candidates:
                chosen = candidates[0]
                tech_id = next(k for k, v in self.techniques.items() if v == chosen)
                chain.append({"technique_id": tech_id, "name": chosen["name"], "tactic": current_tactic})
            next_tactics = self.transitions.get(current_tactic, {})
            if not next_tactics:
                break
            tactics = list(next_tactics.keys())
            weights = list(next_tactics.values())
            current_tactic = np.random.choice(tactics, p=weights)
        return chain

    def get_evasion_hints(self, state: dict) -> list:
        hints = []
        chain = state.get("attack_chain", [])
        if len(chain) == 0:
            hints.append({"type": "suggestion", "message": "Consider starting with a stealthy initial access technique."})
        elif len(chain) < 3:
            hints.append({"type": "suggestion", "message": "The detection model is learning your patterns. Vary your approach."})
        else:
            hints.append({"type": "warning", "message": "Extended activity increases detection probability. Consider exfiltrating soon."})
        return hints

Both ML brains are ready. The DetectionModel scores events for the blue team defender, and the AttackGenerator produces realistic attack chains for the red team attacker. Next up, you'll integrate Claude as the on-demand strategic advisor that gives detailed guidance when asked.

Integrate Claude as the On-Demand Advisor

Your ML models provide ambient hints that nudge the learner without spelling out answers. But sometimes you need deeper strategic guidance from an expert who understands the full picture.

In this step, you'll integrate Claude as an on-demand advisor that can see the current game state and provide detailed tactical guidance. The key difference from the ML hints is that Claude only speaks when asked.

In this step, get ready to:
  • Create a security-focused system prompt that defines Claude's advisor role.
  • Build the ClaudeAdvisor class that formats game context and calls the API.
  • Pass real-time game state (mode, scores, attack chains) so Claude gives contextual advice.
Create the security-focused system prompt

A well-crafted system prompt transforms a general-purpose LLM into a domain expert. For CyberSim Arena, the advisor needs to understand both offensive and defensive perspectives, reference MITRE ATT&CK technique IDs, and tailor its advice to the learner's current mode.

  • Create a new file called advisor.py inside your backend/ folder.
  • Add the imports and system prompt by pasting the following code:
import os
from anthropic import Anthropic

SYSTEM_PROMPT = """You are CyberSim Advisor, an expert cybersecurity instructor embedded in a purple team training simulation. Your role:

1. When the user is in DEFENSE MODE: Help them identify threats, understand attack patterns, and improve their detection skills. Explain MITRE ATT&CK techniques, suggest what to look for in logs, and teach defensive strategies.

2. When the user is in ATTACK MODE: Help them understand offensive techniques, explain how to chain attacks effectively, and teach them about evasion strategies. Frame everything as educational.

3. Always reference the current game state (score, techniques used, events) to give contextual advice.

4. Be direct and specific. Give actionable advice, not vague suggestions.

5. When asked for strategy, provide step-by-step tactical guidance appropriate to the current scenario.

Keep responses concise but informative (2-4 paragraphs max). Use MITRE ATT&CK technique IDs when referencing attacks."""

What does this code do?

The SYSTEM_PROMPT defines Claude's persona and constraints. It tells Claude to behave differently depending on whether the player is in defense or attack mode.

The prompt also instructs Claude to stay concise (2-4 paragraphs) and reference MITRE ATT&CK technique IDs. This keeps responses actionable and grounded in the same framework the simulation uses.

  • Save advisor.py.

Seeing a red underline on the import?

If VS Code shows a warning on from anthropic import Anthropic, make sure your Python interpreter is set to the virtual environment in your backend/ folder. The anthropic package is installed there.

Help me fix my Python interpreter in VS Code.

Build the ClaudeAdvisor class with context passing

For Claude to give useful advice, it needs to see the player's current game state. The advisor class formats runtime data (mode, round number, scores, techniques) into a context string that gets prepended to every question.

  • In advisor.py, add the ClaudeAdvisor class below the SYSTEM_PROMPT by pasting this code:

class ClaudeAdvisor:
    def __init__(self):
        self.client = Anthropic()

    def ask(self, question: str, context: dict) -> str:
        context_str = (
            f"Current mode: {context.get('mode', 'unknown')}\n"
            f"Round: {context.get('round', 0)}\n"
            f"Score - Player: {context.get('score', {}).get('player', 0)}, "
            f"ML: {context.get('score', {}).get('ml', 0)}\n"
            f"Attack chain so far: {context.get('attack_chain', [])}\n"
            f"Detected techniques: {context.get('detected_techniques', [])}\n"
        )

        message = self.client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            messages=[
                {
                    "role": "user",
                    "content": f"GAME STATE:\n{context_str}\n\nMY QUESTION:\n{question}",
                }
            ],
        )
        return message.content[0].text

What does this code do?

  • Anthropic() creates the API client. It automatically reads the ANTHROPIC_API_KEY environment variable you set earlier.
  • context_str formats the live game state (mode, round, scores, techniques used) into a readable string that Claude receives with every question.
  • client.messages.create() sends a single request to claude-sonnet-4-5 with the system prompt and the user's question prepended by the game state.
  • The method returns just the text content of Claude's response, ready to display in the UI.

Why pass game state as context?

Without context, Claude would give generic cybersecurity advice. By injecting the current mode, score, and attack chain, Claude can say things like "You've used T1078 for initial access. Consider T1059 next for execution since it pairs well with valid credential access." The advice becomes specific to the player's situation.

  • Save advisor.py.
  • Verify the file has no syntax errors by running this in your terminal:
python -c "import advisor; print('Advisor module loaded successfully')"

You should see Advisor module loaded successfully printed in your terminal.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is activated and you are running the command from inside the backend/ directory. The anthropic package must be installed in the active environment.

Help me fix my ModuleNotFoundError for the anthropic package.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete backend/advisor.py file:

import os
from anthropic import Anthropic

SYSTEM_PROMPT = """You are CyberSim Advisor, an expert cybersecurity instructor embedded in a purple team training simulation. Your role:

1. When the user is in DEFENSE MODE: Help them identify threats, understand attack patterns, and improve their detection skills. Explain MITRE ATT&CK techniques, suggest what to look for in logs, and teach defensive strategies.

2. When the user is in ATTACK MODE: Help them understand offensive techniques, explain how to chain attacks effectively, and teach them about evasion strategies. Frame everything as educational.

3. Always reference the current game state (score, techniques used, events) to give contextual advice.

4. Be direct and specific. Give actionable advice, not vague suggestions.

5. When asked for strategy, provide step-by-step tactical guidance appropriate to the current scenario.

Keep responses concise but informative (2-4 paragraphs max). Use MITRE ATT&CK technique IDs when referencing attacks."""


class ClaudeAdvisor:
    def __init__(self):
        self.client = Anthropic()

    def ask(self, question: str, context: dict) -> str:
        context_str = (
            f"Current mode: {context.get('mode', 'unknown')}\n"
            f"Round: {context.get('round', 0)}\n"
            f"Score - Player: {context.get('score', {}).get('player', 0)}, "
            f"ML: {context.get('score', {}).get('ml', 0)}\n"
            f"Attack chain so far: {context.get('attack_chain', [])}\n"
            f"Detected techniques: {context.get('detected_techniques', [])}\n"
        )

        message = self.client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            messages=[
                {
                    "role": "user",
                    "content": f"GAME STATE:\n{context_str}\n\nMY QUESTION:\n{question}",
                }
            ],
        )
        return message.content[0].text

Your AI advisor is ready to provide strategic guidance on demand. Next up, you'll build the React dashboard that ties the simulation engine, ML models, and Claude advisor together into a unified cybersecurity training interface.

Build the FastAPI Backend

You've built all the individual modules for CyberSim Arena. The simulation engine, ML models, log generator, and Claude advisor each handle a piece of the puzzle.

Now they need a unified entry point that the React frontend can talk to. FastAPI turns your Python modules into a REST API with typed endpoints, automatic docs, and async support. In this step, you'll create the main application file and start the development server.

In this step, get ready to:
  • Create the FastAPI app with CORS middleware for frontend communication.
  • Wire up all API endpoints for game control, ML hints, and Claude advice.
  • Start the development server and confirm it responds.
Create the FastAPI app with CORS middleware

The React frontend will run on port 5173 while the backend runs on port 8000. Browsers block cross-origin requests by default, so you need CORS middleware to allow the frontend to reach your API.

  • Create a new file called main.py inside your backend/ folder.
  • Add the following imports, app setup, and module instances:
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from simulation import SimulationEngine
from ml_models import DetectionModel, AttackGenerator
from advisor import ClaudeAdvisor

# Create the FastAPI application
app = FastAPI(title="CyberSim Arena API")

# Allow the React frontend (port 5173) to make requests
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Instantiate all simulation components
engine = SimulationEngine()
detector = DetectionModel()
attacker = AttackGenerator()
advisor = ClaudeAdvisor()

What does this code do?

The top section imports FastAPI and all the modules you built in previous steps. Each module handles one responsibility: simulation logic, ML detection, attack generation, and Claude advice.

The CORS middleware specifically allows requests from http://localhost:5173 (where Vite serves the React app). Without this, the browser would block every API call from the frontend.

Add the game control endpoints

The frontend needs endpoints to start games, submit player actions, fetch events, and advance rounds. These form the core game loop that the UI will call during gameplay.

  • Below your module instances in main.py, add the game control route handlers:
# Status check - confirms the API is online
@app.get("/api/status")
def get_status():
    return {"status": "online", "mode": engine.current_mode}


# Start a new simulation in attack or defense mode
@app.post("/api/start")
def start_simulation(payload: dict):
    mode = payload.get("mode", "defense")
    engine.start(mode)
    return {"message": f"Simulation started in {mode} mode", "state": engine.get_state()}


# Process a player action (flagging an event or executing a technique)
@app.post("/api/action")
def player_action(payload: dict):
    action = payload.get("action", "")
    result = engine.process_action(action)
    return result


# Retrieve the most recent log events
@app.get("/api/events")
def get_events():
    return {"events": engine.get_recent_events()}


# Advance to the next round of the simulation
@app.post("/api/next-round")
def next_round():
    result = engine.advance_round()
    return result

What does this code do?

  • /api/status lets the frontend confirm the server is alive before starting a game.
  • /api/start resets the simulation and generates initial events based on the chosen mode (attack or defense).
  • /api/action handles both flagging suspicious events (defense) and executing attack techniques (attack).
  • /api/events returns the latest 30 log events for the event viewer panel.
  • /api/next-round generates a fresh batch of events and advances the round counter.
Add the intelligence endpoints

These final two endpoints are where ML and Claude come in. The hints endpoint surfaces ambient clues from your trained models. The advisor endpoint lets the player ask Claude for strategic guidance.

  • Below the next_round function in main.py, add the intelligence route handlers:
# ML-powered hints based on current mode
@app.get("/api/hints")
def get_hints():
    events = engine.get_recent_events()
    if engine.current_mode == "defense":
        # Isolation Forest scores each event for anomalies
        scores = detector.score_events(events)
        return {"hints": scores}
    else:
        # Markov chain suggests evasion strategies
        evasion_tips = attacker.get_evasion_hints(engine.get_state())
        return {"hints": evasion_tips}


# Claude advisor for on-demand strategic guidance
@app.post("/api/advisor")
def ask_advisor(payload: dict):
    question = payload.get("question", "")
    context = engine.get_state()
    response = advisor.ask(question, context)
    return {"response": response}

What does this code do?

The /api/hints endpoint behaves differently depending on the game mode. In defense mode, it runs the Isolation Forest model against recent events and returns anomaly scores. In attack mode, it returns evasion suggestions from the Markov chain model.

The /api/advisor endpoint passes the player's question plus the full game state to Claude. This gives Claude the context it needs to provide relevant tactical advice.

  • Save main.py.

Seeing red underlines in your editor?

If VS Code shows import errors for simulation, ml_models, or advisor, make sure all five Python files are in the same backend/ folder. Also confirm your virtual environment is selected as the Python interpreter in VS Code.

Help me debug import errors in my FastAPI app

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py before moving on to start the server.

ⓧ I'd like to double check the full code

import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from simulation import SimulationEngine
from ml_models import DetectionModel, AttackGenerator
from advisor import ClaudeAdvisor

app = FastAPI(title="CyberSim Arena API")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

engine = SimulationEngine()
detector = DetectionModel()
attacker = AttackGenerator()
advisor = ClaudeAdvisor()


@app.get("/api/status")
def get_status():
    return {"status": "online", "mode": engine.current_mode}


@app.post("/api/start")
def start_simulation(payload: dict):
    mode = payload.get("mode", "defense")
    engine.start(mode)
    return {"message": f"Simulation started in {mode} mode", "state": engine.get_state()}


@app.post("/api/action")
def player_action(payload: dict):
    action = payload.get("action", "")
    result = engine.process_action(action)
    return result


@app.get("/api/events")
def get_events():
    return {"events": engine.get_recent_events()}


@app.get("/api/hints")
def get_hints():
    events = engine.get_recent_events()
    if engine.current_mode == "defense":
        scores = detector.score_events(events)
        return {"hints": scores}
    else:
        evasion_tips = attacker.get_evasion_hints(engine.get_state())
        return {"hints": evasion_tips}


@app.post("/api/advisor")
def ask_advisor(payload: dict):
    question = payload.get("question", "")
    context = engine.get_state()
    response = advisor.ask(question, context)
    return {"response": response}


@app.post("/api/next-round")
def next_round():
    result = engine.advance_round()
    return result
Start the development server

With all endpoints defined, it's time to boot up the API and confirm everything connects.

  • Make sure your terminal is in the backend/ folder with your virtual environment activated.
  • Start the FastAPI development server by running this command:
uvicorn main:app --reload

What does this command do?

  • uvicorn is the ASGI server that runs your FastAPI app.
  • main:app tells it to load the app object from main.py.
  • --reload enables auto-reload so changes to your code take effect without restarting the server.
  • You should see output indicating the server is running on http://127.0.0.1:8000.
  • Open your browser and navigate to http://localhost:8000/api/status to test the status endpoint.
  • You should see a JSON response: {"status":"online","mode":null}.

Auto-generated API docs

FastAPI automatically generates interactive documentation at http://localhost:8000/docs. You can use this page to test any endpoint directly from the browser without needing the React frontend yet.

Server not starting?

  • If you see ModuleNotFoundError, confirm your virtual environment is activated (you should see the environment name in your terminal prompt).
  • If you get an import error for anthropic or sklearn, re-run your pip install command from Step 1 to ensure all packages are installed.
  • If port 8000 is already in use, stop whatever process is using it or add --port 8001 to the command.

Help me debug my FastAPI server startup error

Your API is live and responding. All seven endpoints are ready to serve the React frontend. Next up, you'll build the dashboard UI that calls these endpoints and brings the simulation to life.

Build the React Dashboard UI

Your backend API is running and ready to serve simulation data, anomaly scores, and Claude advice. Now you need a frontend that lets you actually interact with it.

In this step, you will build a dark SOC-style dashboard in React that gives you two game modes, a live event log, attack technique buttons, and a chat panel for asking Claude questions.

In this step, get ready to:
  • Set up a dark SOC-style theme using CSS variables.
  • Build the mode selector and core API functions.
  • Create the event log viewer, attack technique panel, and Claude advisor chat.
Set up the dark SOC-style theme

Security Operations Centers use dark interfaces to reduce eye strain during long monitoring sessions. Your dashboard will follow the same convention with deep navy backgrounds and color-coded severity indicators.

  • In VS Code, open frontend/src/App.css.
  • Select all the existing content and delete it.
  • Replace it with the following dark theme styles:
:root {
  --bg-primary: #0a0e17;
  --bg-secondary: #111827;
  --bg-panel: #1a2332;
  --text-primary: #e2e8f0;
  --text-secondary: #94a3b8;
  --color-critical: #ef4444;
  --color-high: #f97316;
  --color-medium: #eab308;
  --color-low: #22c55e;
  --color-accent: #3b82f6;
  --color-defense: #06b6d4;
  --color-attack: #ef4444;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background: var(--bg-primary);
  color: var(--text-primary);
  font-family: "JetBrains Mono", "Fira Code", monospace;
}

.app {
  min-height: 100vh;
  padding: 1rem;
}

.mode-select {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  gap: 2rem;
}

.mode-select h1 {
  font-size: 3rem;
  background: linear-gradient(135deg, var(--color-defense), var(--color-attack));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.mode-buttons {
  display: flex;
  gap: 2rem;
}

.mode-buttons button {
  padding: 2rem 3rem;
  border: 2px solid var(--bg-panel);
  border-radius: 12px;
  background: var(--bg-secondary);
  color: var(--text-primary);
  font-size: 1.5rem;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  transition: all 0.2s;
}

.mode-buttons button span {
  font-size: 0.875rem;
  color: var(--text-secondary);
}

.btn-defense:hover {
  border-color: var(--color-defense);
}

.btn-attack:hover {
  border-color: var(--color-attack);
}

What does this code do?

The :root block defines CSS variables for the entire app. Colors are grouped by purpose: backgrounds, text, and severity levels (critical through low).

The .mode-select styles create a centered landing screen where players choose their game mode. The gradient title fades from defense cyan to attack red.

  • Still in App.css, add the dashboard layout and component styles below the mode-select styles:
.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem;
  background: var(--bg-secondary);
  border-radius: 8px;
  margin-bottom: 1rem;
}

.header h1 {
  font-size: 1.25rem;
}

.score-display {
  display: flex;
  gap: 1.5rem;
  font-size: 1.1rem;
}

.player-score {
  color: var(--color-low);
}

.ml-score {
  color: var(--color-attack);
}

.mode-badge {
  padding: 0.4rem 1rem;
  border-radius: 20px;
  font-size: 0.8rem;
  font-weight: bold;
}

.mode-badge.defense {
  background: rgba(6, 182, 212, 0.2);
  color: var(--color-defense);
}

.mode-badge.attack {
  background: rgba(239, 68, 68, 0.2);
  color: var(--color-attack);
}

.feedback-bar {
  padding: 0.75rem 1rem;
  background: var(--bg-panel);
  border-left: 3px solid var(--color-accent);
  border-radius: 4px;
  margin-bottom: 1rem;
}

.main-grid {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 1rem;
  height: calc(100vh - 140px);
}

.events-panel,
.attack-panel,
.advisor-panel {
  background: var(--bg-panel);
  border-radius: 8px;
  padding: 1rem;
  overflow-y: auto;
}

.events-panel h2,
.attack-panel h2,
.advisor-panel h2 {
  font-size: 0.9rem;
  color: var(--text-secondary);
  margin-bottom: 1rem;
  text-transform: uppercase;
  letter-spacing: 1px;
}

.event-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.event-row {
  padding: 0.75rem;
  background: var(--bg-secondary);
  border-radius: 4px;
  border-left: 3px solid var(--color-low);
  position: relative;
}

.event-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.75rem;
  color: var(--text-secondary);
  margin-bottom: 0.25rem;
}

.event-action {
  font-size: 0.85rem;
}

.event-hint {
  font-size: 0.75rem;
  color: var(--color-medium);
  font-style: italic;
  margin-top: 0.25rem;
}

.btn-flag {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  padding: 0.25rem 0.5rem;
  background: rgba(239, 68, 68, 0.2);
  border: 1px solid var(--color-attack);
  color: var(--color-attack);
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.7rem;
}

.technique-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.btn-technique {
  padding: 0.5rem;
  background: var(--bg-secondary);
  border: 1px solid var(--color-attack);
  color: var(--text-primary);
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.8rem;
}

.btn-technique:hover {
  background: rgba(239, 68, 68, 0.2);
}

.evasion-hint {
  padding: 0.5rem;
  background: rgba(234, 179, 8, 0.1);
  border-left: 2px solid var(--color-medium);
  border-radius: 4px;
  font-size: 0.8rem;
  color: var(--color-medium);
  margin-top: 0.5rem;
}

.advisor-messages {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  max-height: calc(100% - 80px);
  overflow-y: auto;
  margin-bottom: 1rem;
}

.msg {
  padding: 0.75rem;
  border-radius: 8px;
  font-size: 0.8rem;
  line-height: 1.5;
}

.msg-user {
  background: var(--bg-secondary);
  align-self: flex-end;
}

.msg-advisor {
  background: rgba(59, 130, 246, 0.1);
  border: 1px solid rgba(59, 130, 246, 0.3);
}

.advisor-input {
  display: flex;
  gap: 0.5rem;
}

.advisor-input input {
  flex: 1;
  padding: 0.5rem;
  background: var(--bg-secondary);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  color: var(--text-primary);
  font-size: 0.8rem;
}

.advisor-input button {
  padding: 0.5rem 1rem;
  background: var(--color-accent);
  border: none;
  border-radius: 4px;
  color: white;
  cursor: pointer;
  font-size: 0.8rem;
}

What does this code do?

The .main-grid creates a two-column layout: the event log fills the left, and the advisor chat sits in a narrow right column. Each .event-row gets a colored left border indicating its risk level.

The technique buttons use a grid layout with a red accent border. The advisor panel styles create a chat interface with user messages on the right and advisor messages on the left.

  • Save App.css.

Not sure if you got everything?

The CSS file is long. If you see rendering issues later, double-check that both chunks are pasted in order with no missing closing braces.

Ask for help debugging your CSS

Build the mode selector and API functions

The React component needs TypeScript types to describe the data flowing from your backend. It also needs state hooks and async functions that call each API endpoint.

  • Open frontend/src/App.tsx.
  • Select all the existing Vite boilerplate content and delete it.
  • Replace it with the following imports, types, state declarations, and API functions:
import { useState } from "react";
import "./App.css";

type GameMode = "defense" | "attack" | null;
type Event = {
  id: string;
  timestamp: number;
  source_ip: string;
  dest_ip: string;
  user: string;
  action: string;
  severity: string;
};
type Hint = {
  event_id?: string;
  anomaly_score?: number;
  risk_level?: string;
  hint?: string;
  type?: string;
  message?: string;
};

const API_BASE = "http://localhost:8000";

function App() {
  const [mode, setMode] = useState<GameMode>(null);
  const [events, setEvents] = useState<Event[]>([]);
  const [hints, setHints] = useState<Hint[]>([]);
  const [score, setScore] = useState({ player: 0, ml: 0 });
  const [messages, setMessages] = useState<{ role: string; content: string }[]>([]);
  const [advisorInput, setAdvisorInput] = useState("");
  const [feedback, setFeedback] = useState("");
  const [loading, setLoading] = useState(false);

  async function startGame(selectedMode: GameMode) {
    const res = await fetch(`${API_BASE}/api/start`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ mode: selectedMode }),
    });
    await res.json();
    setMode(selectedMode);
    await refreshEvents();
    await refreshHints();
  }

  async function refreshEvents() {
    const res = await fetch(`${API_BASE}/api/events`);
    const data = await res.json();
    setEvents(data.events);
  }

  async function refreshHints() {
    const res = await fetch(`${API_BASE}/api/hints`);
    const data = await res.json();
    setHints(data.hints);
  }

  async function flagEvent(eventId: string) {
    const res = await fetch(`${API_BASE}/api/action`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ action: `flag:${eventId}` }),
    });
    const data = await res.json();
    setScore(data.score);
    setFeedback(data.message);
    await refreshHints();
  }

  async function executeAttack(techniqueId: string) {
    const res = await fetch(`${API_BASE}/api/action`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ action: techniqueId }),
    });
    const data = await res.json();
    setScore(data.score);
    setFeedback(data.message);
    await refreshEvents();
    await refreshHints();
  }

  async function askAdvisor() {
    if (!advisorInput.trim()) return;
    setLoading(true);
    setMessages((prev) => [...prev, { role: "user", content: advisorInput }]);
    const res = await fetch(`${API_BASE}/api/advisor`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ question: advisorInput }),
    });
    const data = await res.json();
    setMessages((prev) => [...prev, { role: "advisor", content: data.response }]);
    setAdvisorInput("");
    setLoading(false);
  }

  function getHintForEvent(eventId: string): Hint | undefined {
    return hints.find((h) => h.event_id === eventId);
  }

  function getRiskColor(level: string): string {
    switch (level) {
      case "critical": return "var(--color-critical)";
      case "high": return "var(--color-high)";
      case "medium": return "var(--color-medium)";
      default: return "var(--color-low)";
    }
  }

  if (!mode) {
    return (
      <div className="app">
        <div className="mode-select">
          <h1>CyberSim Arena</h1>
          <p>Choose your training mode</p>
          <div className="mode-buttons">
            <button className="btn-defense" onClick={() => startGame("defense")}>
              🛡️ Defense Mode
              <span>ML attacks. You defend.</span>
            </button>
            <button className="btn-attack" onClick={() => startGame("attack")}>
              ⚔️ Attack Mode
              <span>You attack. ML detects.</span>
            </button>
          </div>
        </div>
      </div>
    );
  }

What does this code do?

The GameMode type is a union of "defense", "attack", or null. When mode is null, the component renders the mode-select landing screen. Once the player clicks a button, startGame calls /api/start on your backend and switches the view to the full dashboard.

Each async function maps to one backend endpoint: flagEvent flags a suspicious log entry, executeAttack fires an attack technique, and askAdvisor sends a question to Claude. The getRiskColor helper maps severity levels to the CSS variables you defined earlier.

The file is not complete yet. You will add the main dashboard JSX in the next substep.

Create the main dashboard panels

The dashboard has three panels: the event log (always visible), the attack technique selector (attack mode only), and the Claude advisor chat (always visible).

  • Still in App.tsx, add the following code directly below the closing } of the if (!mode) block:
  return (
    <div className="app">
      <header className="header">
        <h1>CyberSim Arena</h1>
        <div className="score-display">
          <span className="player-score">You: {score.player}</span>
          <span className="ml-score">ML: {score.ml}</span>
        </div>
        <span className={`mode-badge ${mode}`}>
          {mode === "defense" ? "🛡️ DEFENSE" : "⚔️ ATTACK"}
        </span>
      </header>

      {feedback && <div className="feedback-bar">{feedback}</div>}

      <div className="main-grid">
        <section className="events-panel">
          <h2>Event Log</h2>
          <div className="event-list">
            {events.map((event) => {
              const hint = getHintForEvent(event.id);
              return (
                <div
                  key={event.id}
                  className="event-row"
                  style={{ borderLeftColor: getRiskColor(hint?.risk_level || event.severity) }}
                >
                  <div className="event-meta">
                    <span className="event-ip">{event.source_ip}</span>
                    <span className="event-user">{event.user}</span>
                  </div>
                  <div className="event-action">{event.action}</div>
                  {hint?.hint && <div className="event-hint">{hint.hint}</div>}
                  {mode === "defense" && (
                    <button className="btn-flag" onClick={() => flagEvent(event.id)}>
                      🚨 Flag
                    </button>
                  )}
                </div>
              );
            })}
          </div>
        </section>

        {mode === "attack" && (
          <section className="attack-panel">
            <h2>Available Techniques</h2>
            <div className="technique-list">
              {["T1190", "T1078", "T1059", "T1053", "T1548", "T1070", "T1003", "T1021", "T1041"].map((tid) => (
                <button key={tid} className="btn-technique" onClick={() => executeAttack(tid)}>
                  {tid}
                </button>
              ))}
            </div>
            {hints.map((h, i) => (
              <div key={i} className="evasion-hint">
                {h.message}
              </div>
            ))}
          </section>
        )}

        <section className="advisor-panel">
          <h2>Ask Claude</h2>
          <div className="advisor-messages">
            {messages.map((msg, i) => (
              <div key={i} className={`msg msg-${msg.role}`}>
                {msg.content}
              </div>
            ))}
          </div>
          <div className="advisor-input">
            <input
              value={advisorInput}
              onChange={(e) => setAdvisorInput(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && askAdvisor()}
              placeholder="Ask for strategic advice..."
              disabled={loading}
            />
            <button onClick={askAdvisor} disabled={loading}>
              {loading ? "..." : "Ask"}
            </button>
          </div>
        </section>
      </div>
    </div>
  );
}

export default App;

What does this code do?

The header shows the live score (You vs ML) and a mode badge that changes color based on whether you are in defense or attack mode.

The events panel maps over the events array and renders each log entry with its source IP, user, and action text. The left border color comes from the ML model's anomaly score via getRiskColor. In defense mode, each event gets a Flag button.

The attack panel only renders in attack mode. It shows buttons for each MITRE ATT&CK technique ID. The advisor panel is a simple chat interface where you type a question and press Enter (or click Ask) to get Claude's strategic advice.

  • Save App.tsx.
  • Switch to the terminal for your frontend directory and start the dev server by running:
npm run dev
  • Open http://localhost:5173 in your browser.
  • You should see a dark screen with "CyberSim Arena" in gradient text and two large buttons: Defense Mode and Attack Mode.

Seeing a blank white page?

Check that you saved both App.css and App.tsx. If the terminal shows a TypeScript error, make sure the if (!mode) block's closing brace is immediately followed by the return ( statement from the dashboard code.

Help me fix my CyberSim Arena frontend

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the complete App.tsx:

import { useState } from "react";
import "./App.css";

type GameMode = "defense" | "attack" | null;
type Event = {
  id: string;
  timestamp: number;
  source_ip: string;
  dest_ip: string;
  user: string;
  action: string;
  severity: string;
};
type Hint = {
  event_id?: string;
  anomaly_score?: number;
  risk_level?: string;
  hint?: string;
  type?: string;
  message?: string;
};

const API_BASE = "http://localhost:8000";

function App() {
  const [mode, setMode] = useState<GameMode>(null);
  const [events, setEvents] = useState<Event[]>([]);
  const [hints, setHints] = useState<Hint[]>([]);
  const [score, setScore] = useState({ player: 0, ml: 0 });
  const [messages, setMessages] = useState<{ role: string; content: string }[]>([]);
  const [advisorInput, setAdvisorInput] = useState("");
  const [feedback, setFeedback] = useState("");
  const [loading, setLoading] = useState(false);

  async function startGame(selectedMode: GameMode) {
    const res = await fetch(`${API_BASE}/api/start`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ mode: selectedMode }),
    });
    await res.json();
    setMode(selectedMode);
    await refreshEvents();
    await refreshHints();
  }

  async function refreshEvents() {
    const res = await fetch(`${API_BASE}/api/events`);
    const data = await res.json();
    setEvents(data.events);
  }

  async function refreshHints() {
    const res = await fetch(`${API_BASE}/api/hints`);
    const data = await res.json();
    setHints(data.hints);
  }

  async function flagEvent(eventId: string) {
    const res = await fetch(`${API_BASE}/api/action`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ action: `flag:${eventId}` }),
    });
    const data = await res.json();
    setScore(data.score);
    setFeedback(data.message);
    await refreshHints();
  }

  async function executeAttack(techniqueId: string) {
    const res = await fetch(`${API_BASE}/api/action`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ action: techniqueId }),
    });
    const data = await res.json();
    setScore(data.score);
    setFeedback(data.message);
    await refreshEvents();
    await refreshHints();
  }

  async function askAdvisor() {
    if (!advisorInput.trim()) return;
    setLoading(true);
    setMessages((prev) => [...prev, { role: "user", content: advisorInput }]);
    const res = await fetch(`${API_BASE}/api/advisor`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ question: advisorInput }),
    });
    const data = await res.json();
    setMessages((prev) => [...prev, { role: "advisor", content: data.response }]);
    setAdvisorInput("");
    setLoading(false);
  }

  function getHintForEvent(eventId: string): Hint | undefined {
    return hints.find((h) => h.event_id === eventId);
  }

  function getRiskColor(level: string): string {
    switch (level) {
      case "critical": return "var(--color-critical)";
      case "high": return "var(--color-high)";
      case "medium": return "var(--color-medium)";
      default: return "var(--color-low)";
    }
  }

  if (!mode) {
    return (
      <div className="app">
        <div className="mode-select">
          <h1>CyberSim Arena</h1>
          <p>Choose your training mode</p>
          <div className="mode-buttons">
            <button className="btn-defense" onClick={() => startGame("defense")}>
              🛡️ Defense Mode
              <span>ML attacks. You defend.</span>
            </button>
            <button className="btn-attack" onClick={() => startGame("attack")}>
              ⚔️ Attack Mode
              <span>You attack. ML detects.</span>
            </button>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="app">
      <header className="header">
        <h1>CyberSim Arena</h1>
        <div className="score-display">
          <span className="player-score">You: {score.player}</span>
          <span className="ml-score">ML: {score.ml}</span>
        </div>
        <span className={`mode-badge ${mode}`}>
          {mode === "defense" ? "🛡️ DEFENSE" : "⚔️ ATTACK"}
        </span>
      </header>

      {feedback && <div className="feedback-bar">{feedback}</div>}

      <div className="main-grid">
        <section className="events-panel">
          <h2>Event Log</h2>
          <div className="event-list">
            {events.map((event) => {
              const hint = getHintForEvent(event.id);
              return (
                <div
                  key={event.id}
                  className="event-row"
                  style={{ borderLeftColor: getRiskColor(hint?.risk_level || event.severity) }}
                >
                  <div className="event-meta">
                    <span className="event-ip">{event.source_ip}</span>
                    <span className="event-user">{event.user}</span>
                  </div>
                  <div className="event-action">{event.action}</div>
                  {hint?.hint && <div className="event-hint">{hint.hint}</div>}
                  {mode === "defense" && (
                    <button className="btn-flag" onClick={() => flagEvent(event.id)}>
                      🚨 Flag
                    </button>
                  )}
                </div>
              );
            })}
          </div>
        </section>

        {mode === "attack" && (
          <section className="attack-panel">
            <h2>Available Techniques</h2>
            <div className="technique-list">
              {["T1190", "T1078", "T1059", "T1053", "T1548", "T1070", "T1003", "T1021", "T1041"].map((tid) => (
                <button key={tid} className="btn-technique" onClick={() => executeAttack(tid)}>
                  {tid}
                </button>
              ))}
            </div>
            {hints.map((h, i) => (
              <div key={i} className="evasion-hint">
                {h.message}
              </div>
            ))}
          </section>
        )}

        <section className="advisor-panel">
          <h2>Ask Claude</h2>
          <div className="advisor-messages">
            {messages.map((msg, i) => (
              <div key={i} className={`msg msg-${msg.role}`}>
                {msg.content}
              </div>
            ))}
          </div>
          <div className="advisor-input">
            <input
              value={advisorInput}
              onChange={(e) => setAdvisorInput(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && askAdvisor()}
              placeholder="Ask for strategic advice..."
              disabled={loading}
            />
            <button onClick={askAdvisor} disabled={loading}>
              {loading ? "..." : "Ask"}
            </button>
          </div>
        </section>
      </div>
    </div>
  );
}

export default App;

And the complete App.css:

:root {
  --bg-primary: #0a0e17;
  --bg-secondary: #111827;
  --bg-panel: #1a2332;
  --text-primary: #e2e8f0;
  --text-secondary: #94a3b8;
  --color-critical: #ef4444;
  --color-high: #f97316;
  --color-medium: #eab308;
  --color-low: #22c55e;
  --color-accent: #3b82f6;
  --color-defense: #06b6d4;
  --color-attack: #ef4444;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background: var(--bg-primary);
  color: var(--text-primary);
  font-family: "JetBrains Mono", "Fira Code", monospace;
}

.app {
  min-height: 100vh;
  padding: 1rem;
}

.mode-select {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  gap: 2rem;
}

.mode-select h1 {
  font-size: 3rem;
  background: linear-gradient(135deg, var(--color-defense), var(--color-attack));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.mode-buttons {
  display: flex;
  gap: 2rem;
}

.mode-buttons button {
  padding: 2rem 3rem;
  border: 2px solid var(--bg-panel);
  border-radius: 12px;
  background: var(--bg-secondary);
  color: var(--text-primary);
  font-size: 1.5rem;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  transition: all 0.2s;
}

.mode-buttons button span {
  font-size: 0.875rem;
  color: var(--text-secondary);
}

.btn-defense:hover {
  border-color: var(--color-defense);
}

.btn-attack:hover {
  border-color: var(--color-attack);
}

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem;
  background: var(--bg-secondary);
  border-radius: 8px;
  margin-bottom: 1rem;
}

.header h1 {
  font-size: 1.25rem;
}

.score-display {
  display: flex;
  gap: 1.5rem;
  font-size: 1.1rem;
}

.player-score {
  color: var(--color-low);
}

.ml-score {
  color: var(--color-attack);
}

.mode-badge {
  padding: 0.4rem 1rem;
  border-radius: 20px;
  font-size: 0.8rem;
  font-weight: bold;
}

.mode-badge.defense {
  background: rgba(6, 182, 212, 0.2);
  color: var(--color-defense);
}

.mode-badge.attack {
  background: rgba(239, 68, 68, 0.2);
  color: var(--color-attack);
}

.feedback-bar {
  padding: 0.75rem 1rem;
  background: var(--bg-panel);
  border-left: 3px solid var(--color-accent);
  border-radius: 4px;
  margin-bottom: 1rem;
}

.main-grid {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 1rem;
  height: calc(100vh - 140px);
}

.events-panel,
.attack-panel,
.advisor-panel {
  background: var(--bg-panel);
  border-radius: 8px;
  padding: 1rem;
  overflow-y: auto;
}

.events-panel h2,
.attack-panel h2,
.advisor-panel h2 {
  font-size: 0.9rem;
  color: var(--text-secondary);
  margin-bottom: 1rem;
  text-transform: uppercase;
  letter-spacing: 1px;
}

.event-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.event-row {
  padding: 0.75rem;
  background: var(--bg-secondary);
  border-radius: 4px;
  border-left: 3px solid var(--color-low);
  position: relative;
}

.event-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.75rem;
  color: var(--text-secondary);
  margin-bottom: 0.25rem;
}

.event-action {
  font-size: 0.85rem;
}

.event-hint {
  font-size: 0.75rem;
  color: var(--color-medium);
  font-style: italic;
  margin-top: 0.25rem;
}

.btn-flag {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  padding: 0.25rem 0.5rem;
  background: rgba(239, 68, 68, 0.2);
  border: 1px solid var(--color-attack);
  color: var(--color-attack);
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.7rem;
}

.technique-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.btn-technique {
  padding: 0.5rem;
  background: var(--bg-secondary);
  border: 1px solid var(--color-attack);
  color: var(--text-primary);
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.8rem;
}

.btn-technique:hover {
  background: rgba(239, 68, 68, 0.2);
}

.evasion-hint {
  padding: 0.5rem;
  background: rgba(234, 179, 8, 0.1);
  border-left: 2px solid var(--color-medium);
  border-radius: 4px;
  font-size: 0.8rem;
  color: var(--color-medium);
  margin-top: 0.5rem;
}

.advisor-messages {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  max-height: calc(100% - 80px);
  overflow-y: auto;
  margin-bottom: 1rem;
}

.msg {
  padding: 0.75rem;
  border-radius: 8px;
  font-size: 0.8rem;
  line-height: 1.5;
}

.msg-user {
  background: var(--bg-secondary);
  align-self: flex-end;
}

.msg-advisor {
  background: rgba(59, 130, 246, 0.1);
  border: 1px solid rgba(59, 130, 246, 0.3);
}

.advisor-input {
  display: flex;
  gap: 0.5rem;
}

.advisor-input input {
  flex: 1;
  padding: 0.5rem;
  background: var(--bg-secondary);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  color: var(--text-primary);
  font-size: 0.8rem;
}

.advisor-input button {
  padding: 0.5rem 1rem;
  background: var(--color-accent);
  border: none;
  border-radius: 4px;
  color: white;
  cursor: pointer;
  font-size: 0.8rem;
}

Your dashboard is live. Next up, you will wire the frontend to the backend so clicking those buttons actually launches a simulation.

Secret mission

Purple Team Scorecard

You have a working training simulator. But how do you actually improve over time? Add a SQLite-backed session history that tracks your performance, calculates metrics like detection rate and evasion rate, and uses Claude to generate a personalized training report with recommendations.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine. The only ongoing cost comes from Anthropic API calls, which only happen when you actively use the Claude advisor or generate training reports.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • CyberSim Arena project folder (contains backend/ and frontend/ directories with all source code).
  • Python virtual environment (inside the project folder).
  • Node modules (inside frontend/).
  • SQLite database cybersim_sessions.db (inside backend/).
  • ANTHROPIC_API_KEY environment variable (set in terminal session).

Keep everything running

No action needed. Choose this if you are still actively training in CyberSim Arena and want to keep both the backend and frontend dev servers running.

  • Your FastAPI backend continues serving on port 8000.
  • Your Vite frontend continues serving on port 5173.
  • Your session history in cybersim_sessions.db persists between sessions.
  • No API costs are incurred unless you actively click Ask Claude or generate training reports.

Pause - I'll come back to this later

Shut down running processes to free up memory and terminal windows. Your project files stay on disk, ready to restart later.

  • Switch to the terminal running your FastAPI backend and press Ctrl+C to stop it.
  • Switch to the terminal running your Vite frontend and press Ctrl+C to stop it.
  • Close VS Code if you want to free up additional memory.

To restart later, open a terminal in your project folder, activate your virtual environment, then run fastapi dev in the backend/ folder and npm run dev in the frontend/ folder.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes all source code, the virtual environment, node modules, and your session history database.

  • Switch to the terminal running your FastAPI backend and press Ctrl+C to stop it.
  • Switch to the terminal running your Vite frontend and press Ctrl+C to stop it.
  • Delete the entire project folder by running the command for your operating system:

macOS

cd ~/Desktop
rm -rf cybersim-arena

Windows

cd ~\Desktop
Remove-Item -Recurse -Force cybersim-arena
  • If you exported ANTHROPIC_API_KEY in your terminal, close that terminal window to clear the variable. The key itself remains valid in your Anthropic account for use in other projects.

Nice Work!

Nice Work!

You just built CyberSim Arena, a full-stack cybersecurity training simulator powered by machine learning and AI. You can now practice both offensive and defensive security against intelligent ML opponents, with Claude available as your on-demand strategic advisor.

Here's what you learned:

  • Built a dual-mode purple team training platform with a FastAPI backend and React frontend, featuring attack mode and defense mode game loops.
  • Trained an Isolation Forest anomaly detection model and a Markov chain attack generator based on MITRE ATT&CK technique sequences to serve as intelligent ML opponents.
  • Integrated Claude as an on-demand strategic advisor that sees the current game state and provides contextual security guidance for both attack and defense scenarios.
  • Secret Mission: Added a Purple Team Scorecard with SQLite-backed session history and AI-generated personalized training reports.

Ready to quiz yourself?