# Project export: ShadowGuard

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: ShadowGuard is a network-layer AI firewall that detects and redacts sensitive patient data in real time, before it ever leaves a hospital network.
- Devpost: https://devpost.com/software/shadowguard-l6yv7p
- GitHub: https://github.com/shamanthak-hegde/ShadowGuard
- Video: https://www.youtube.com/embed/n0ntGM6gGk8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([OpenEvidence] Healthcare Track Grand Prize)
- Team: 1 GitHub contributor(s) — Shamanthak Hegde (3 commits)

## Devpost submission (written by the team)

### Inspiration

Healthcare is rapidly adopting AI tools, cloud platforms, and smart medical devices. While this improves efficiency and patient outcomes, it also introduces a serious compliance risk: sensitive patient data can leave the hospital network in seconds. A simple AI prompt containing a name, diagnosis, or SSN can unintentionally create a HIPAA violation. We were inspired by a core question: What if hospital networks could defend themselves automatically? Instead of relying on post-incident audits and manual reviews, we wanted to build a system that enforces privacy in real time, at the network layer, before sensitive data ever leaves the building. That vision became ShadowGuard.

### What it does

ShadowGuard is a real-time AI-powered PII firewall for hospitals. It sits inline as a Layer 7 proxy between internal devices and the internet. ShadowGuard: Intercepts outgoing HTTP/HTTPS traffic Reconstructs payloads (JSON, form data, AI prompts) Detects Protected Health Information (PHI/PII) Redacts or tokenizes sensitive data in transit Alerts administrators for high-risk incidents Instead of blocking workflows, ShadowGuard intelligently modifies packets before forwarding them, ensuring compliance without disrupting clinical operations.

### How we built it

We designed ShadowGuard as a modular network-layer system. 1️⃣ Packet Interception We implemented a proxy-based Man-in-the-Middle architecture to route outbound traffic through ShadowGuard for inspection. 2️⃣ Payload Reconstruction We parse and reconstruct application-layer content from packets to analyze real data rather than raw bytes. 3️⃣ Hybrid PII Detection We combined: Regex-based pattern detection (SSNs, phone numbers, MRNs) Named Entity Recognition (NER) LLM-based contextual classification 4️⃣ Inline Redaction Engine Instead of blocking traffic, we dynamically rewrite payloads: Before After 5️⃣ Real-Time Alerting High-severity events trigger logging, alerts, and escalation workflows.

### Challenges we ran into

🔐 HTTPS Decryption Intercepting HTTPS traffic requires certificate injection and careful trust-chain handling, especially sensitive in medical environments. We used the MITMProxy to handle this. ⚡ Latency vs Accuracy LLMs provide contextual detection but introduce delay. We engineered a layered system where fast deterministic checks run first, and AI scoring is applied selectively. 🎯 False Positives Over-redaction can break workflows. We prioritized precision and designed conservative thresholds to maintain usability. ⚖️ Compliance Considerations Modifying packets inline raises auditing and legal questions. We ensured every action is logged and traceable for compliance review.

### Accomplishments we're proud of

Successfully implemented real-time packet interception and rewriting Built a hybrid AI + deterministic PII detection engine Reduced latency through layered scoring Designed a system that preserves workflow instead of blocking traffic Created a proactive privacy enforcement model rather than reactive logging Most importantly, we demonstrated that network-layer AI governance is possible in real time.

### What we learned

Security is most powerful when implemented at the network layer. AI systems must include deterministic fallbacks for reliability. Compliance solutions must balance protection with usability. Real-world healthcare systems require transparency and auditability. We also learned that privacy enforcement should not rely solely on user behavior, it should be architected into the system itself.

### What's next

Optimizing detection models for lower latency Training lightweight on-device classifiers for IoT medical devices Integrating automated voice-based incident response Building adaptive policy learning for hospital-specific compliance rules Exploring zero-trust deployment architectures Our long-term vision is to make ShadowGuard the foundation for AI-native compliance infrastructure in healthcare. Privacy. Enforced in Transit.

## README (from the GitHub repository)

# ShadowGuard

**Healthcare Shadow AI Detection & Governance System** | TreeHacks 2026

ShadowGuard intercepts HTTPS traffic to AI services (ChatGPT, Claude, Gemini), detects Protected Health Information (PHI) using NLP, redacts it in real-time, and provides a cybersecurity-themed governance dashboard with live WebSocket updates and automated voice alerts.

---

## Architecture

```
                    ┌──────────────┐
  Browser/App ───►  │  mitmproxy   │ ──► AI Service (OpenAI, Anthropic, Google)
                    │  + addon     │
                    └──────┬───────┘
                           │ POST /api/events
                    ┌──────▼───────┐
                    │   FastAPI    │ ──► VAPI Voice Calls (high-risk alerts)
                    │   Backend    │
                    └──────┬───────┘
                           │ WebSocket + REST
                    ┌──────▼───────┐
                    │    React     │
                    │  Dashboard   │
                    └──────────────┘
```

| Component | Stack | Port |
|-----------|-------|------|
| Proxy | mitmproxy + Python addon | 8080 |
| Backend | FastAPI, PostgreSQL 14, psycopg2 | 8000 |
| Dashboard | React 18, Vite, D3.js, TailwindCSS | 3000 |
| Voice Alerts | VAPI + GPT-5.2 + ElevenLabs | - |

---

## Quick Start

### 1. Start the backend + dashboard + database

```bash
docker compose up --build
```

This starts PostgreSQL, the FastAPI backend, and the React dashboard.

### 2. Seed demo data

```bash
curl -X POST http://localhost:8000/api/seed
```

### 3. Open the dashboard

Navigate to [http://localhost:3000](http://localhost:3000)

### 4. Start the proxy (separate terminal)

```bash
# Activate the conda environment
source /path/to/anaconda3/etc/profile.d/conda.sh && conda activate shadow

# Run mitmproxy with the ShadowGuard addon
mitmproxy -s shadowguard_addon.py
```

### 5. Route traffic through the proxy

```bash
export HTTPS_PROXY=http://localhost:8080
export HTTP_PROXY=http://localhost:8080
export SSL_CERT_FILE=~/.mitmproxy/mitmproxy-ca-cert.pem
```

Or launch a proxied Chrome:

```bash
# macOS
open -na "Google Chrome" --args \
  --proxy-server="http://localhost:8080" \
  --user-data-dir="/tmp/chrome-proxy-test"
```

---

## VAPI Voice Alerts

When a high-risk PHI exposure is detected (severity critical/high, risk score >= 70), ShadowGuard can automatically call the responsible staff via VAPI to notify them.

### Setup

1. Create a VAPI account at [vapi.ai](https://vapi.ai)
2. Configure a phone number and an assistant on the VAPI dashboard
3. The assistant should use template variables: `{{service}}`, `{{phi_types}}`, `{{risk_score}}`, `{{action_taken}}`, `{{timestamp}}`, `{{department}}`
4. Add your credentials to `.env`:

```env
VAPI_ENABLED=true
VAPI_API_KEY=your-private-key
VAPI_PHONE_NUMBER_ID=your-vapi-phone-id
VAPI_ASSISTANT_ID=your-assistant-id
ALERT_PHONE_NUMBER=+1XXXXXXXXXX
CALL_COOLDOWN_SECONDS=300
```

### How it works

- Calls are **only triggered** when `VAPI_ENABLED=true` — safe to run without it
- Seeding demo data does **not** trigger real calls (only inserts fake call records)
- Per-IP cooldown prevents call spam (default: 5 minutes)
- Test a call manually: `curl -X POST http://localhost:8000/api/calls/test`

---

## Environment Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `DATABASE_URL` | Yes (Docker sets it) | `postgresql://shadowguard:shadowguard@localhost:5432/shadowguard` | PostgreSQL connection string |
| `VAPI_ENABLED` | No | `false` | Enable voice call alerts |
| `VAPI_API_KEY` | For calls | - | VAPI private API key |
| `VAPI_PHONE_NUMBER_ID` | For calls | - | VAPI phone number ID |
| `VAPI_ASSISTANT_ID` | For calls | - | Pre-configured VAPI assistant ID |
| `ALERT_PHONE_NUMBER` | For calls | - | Phone number to receive alert calls |
| `CALL_COOLDOWN_SECONDS` | No | `300` | Minimum seconds between calls per source IP |

---

## API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/health` | Health check |
| `POST` | `/api/events` | Ingest event from mitmproxy |
| `GET` | `/api/events` | List events (supports `limit`, `offset`, `severity`, `service`, `status`) |
| `GET` | `/api/events/:id` | Get single event |
| `PATCH` | `/api/events/:id/status` | Update event status (active/mitigated/resolved) |
| `GET` | `/api/stats` | Dashboard aggregate statistics |
| `POST` | `/api/seed` | Seed database with demo data |
| `GET` | `/api/calls` | List VAPI call records |
| `GET` | `/api/calls/stats` | Voice call aggregate stats |
| `POST` | `/api/calls/test` | Trigger a test VAPI call |
| `WS` | `/api/ws` | WebSocket for real-time updates |

---

## Testing Interception

### Terminal test (with proxy running)

```bash
# Clean request (no PHI) — should be logged
curl -X POST https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk-fake-test-key" \
  -d '{"model": "gpt-4", "messages": [{"role": "user", "content": "How do I sort a list in Python?"}]}'

# PHI request — should be detected and redacted
curl -X POST https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk-fake-test-key" \
  -d '{"model": "gpt-4", "messages": [{"role": "user", "content": "Summarize notes for patient John Doe, SSN: 423-91-8847, DOB: 03/15/1958. Diagnosis E11.9 Type 2 Diabetes."}]}'
```

### Browser test

1. Launch Chrome with proxy: `--proxy-server="http://localhost:8080"`
2. Navigate to `http://mitm.it` and install the mitmproxy CA certificate
3. Go to `https://chatgpt.com` and type a message
4. Watch the dashboard update in real-time

---

## Project Structure

```
ShadowGuard/
├── shadowguard_addon.py    # mitmproxy addon — intercepts, detects PHI, posts to backend
├── phi_redactor.py         # PHI detection engine (Presidio + regex fallback)
├── docker-compose.yml      # PostgreSQL + backend + dashboard
├── .env                    # VAPI and other environment variables
├── backend/
│   ├── main.py             # FastAPI app, routes, WebSocket manager
│   ├── database.py         # PostgreSQL connection pool, table creation
│   ├── models.py           # Pydantic request/response models
│   ├── seed.py             # Demo data generator
│   ├── vapi_caller.py      # VAPI voice call integration
│   ├── requirements.txt    # Python dependencies
│   └── Dockerfile
└── dashboard/
    ├── src/
    │   ├── App.jsx         # Main app with state management + WebSocket
    │   ├── components/
    │   │   ├── StatsCards.jsx       # Summary stat cards
    │   │   ├── ThreatFeed.jsx       # Live threat feed sidebar
    │   │   ├── TrafficTimeline.jsx  # D3 traffic timeline chart
    │   │   ├── RiskHeatmap.jsx      # D3 risk heatmap
    │   │   ├── NetworkGraph.jsx     # D3 force-directed network graph
    │   │   ├── AuditLog.jsx         # Sortable/paginated audit table
    │   │   └── RedactionViewer.jsx  # Side-by-side original/redacted modal
    │   ├── hooks/
    │   │   └── useWebSocket.js      # WebSocket hook with auto-reconnect
    │   └── lib/
    │       └── api.js               # REST API client functions
    ├── package.json
    └── Dockerfile
```

---

## Troubleshooting

**"SSL certificate verify failed"**
Install/trust the mitmproxy CA cert: `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem`

**"Connection refused" on port 8000**
Make sure `docker compose up` is running and the backend container is healthy.

**VAPI calls failing with 403**
Ensure `User-Agent` header is set (already handled in code). Check your VAPI API key is the **private** key, not the public one.

**VAPI calls failing with SSL errors**
The backend Docker container disables SSL verification for outbound VAPI calls to avoid certificate issues with proxies.

**Dashboard not updating**
Check the WebSocket connection indicator in

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 29 recognized source files, 150 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (34 of 34)

```
.gitignore
backend/database.py
backend/Dockerfile
backend/main.py
backend/models.py
backend/requirements.txt
backend/seed.py
backend/vapi_caller.py
dashboard/Dockerfile
dashboard/index.html
dashboard/package.json
dashboard/postcss.config.js
dashboard/src/App.jsx
dashboard/src/components/AuditLog.jsx
dashboard/src/components/Header.jsx
dashboard/src/components/NetworkGraph.jsx
dashboard/src/components/RedactionViewer.jsx
dashboard/src/components/RiskHeatmap.jsx
dashboard/src/components/StatsCards.jsx
dashboard/src/components/StatusBadge.jsx
dashboard/src/components/ThreatFeed.jsx
dashboard/src/components/TrafficTimeline.jsx
dashboard/src/hooks/useAnimatedCounter.js
dashboard/src/hooks/useWebSocket.js
dashboard/src/index.css
dashboard/src/lib/api.js
dashboard/src/main.jsx
dashboard/tailwind.config.js
dashboard/vite.config.js
docker-compose.yml
phi_redactor.py
README.md
setup.sh
shadowguard_addon.py
```

### Dependencies

- backend/requirements.txt: certifi@==2024.12.14, fastapi@==0.115.6, psycopg2-binary@==2.9.10, pydantic@==2.10.4, python-dotenv@==1.0.1, uvicorn[standard]@==0.34.0
- dashboard/package.json: @vitejs/plugin-react@^4.3.4, autoprefixer@^10.4.20, d3@^7.9.0, postcss@^8.4.49, react@^18.3.1, react-dom@^18.3.1, tailwindcss@^3.4.17, vite@^5.4.11

### Recent commits (newest first)

- Final Version
- Voice fix
- Voice Response added
- initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### docker-compose.yml

```yaml
version: '3.8'
services:
  postgres:
    image: postgres:14.20
    environment:
      POSTGRES_DB: shadowguard
      POSTGRES_USER: shadowguard
      POSTGRES_PASSWORD: shadowguard
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U shadowguard"]
      interval: 5s
      timeout: 5s
      retries: 5

  backend:
    build: ./backend
    ports:
      - "8000:8000"
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql://shadowguard:shadowguard@postgres:5432/shadowguard
      VAPI_ENABLED: ${VAPI_ENABLED:-false}
      VAPI_API_KEY: ${VAPI_API_KEY:-}
      VAPI_PHONE_NUMBER_ID: ${VAPI_PHONE_NUMBER_ID:-}
      VAPI_ASSISTANT_ID: ${VAPI_ASSISTANT_ID:-}
      ALERT_PHONE_NUMBER: ${ALERT_PHONE_NUMBER:-}
      CALL_COOLDOWN_SECONDS: ${CALL_COOLDOWN_SECONDS:-300}

  dashboard:
    build: ./dashboard
    ports:
      - "3000:3000"
    depends_on:
      - backend

volumes:
  pgdata:

```

### backend/requirements.txt

```
fastapi==0.115.6
uvicorn[standard]==0.34.0
psycopg2-binary==2.9.10
pydantic==2.10.4
python-dotenv==1.0.1
certifi==2024.12.14

```

### dashboard/Dockerfile

```
FROM node:20-alpine

WORKDIR /app

COPY package.json .
RUN npm config set strict-ssl false && npm install

COPY . .

EXPOSE 3000

CMD ["npm", "run", "dev", "--", "--host"]

```

### backend/Dockerfile

```
FROM python:3.12-slim

RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --trusted-host pypi.org --trusted-host files.pythonhosted.org -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

```

### dashboard/package.json

```
{
  "name": "shadowguard-dashboard",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "d3": "^7.9.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.4",
    "vite": "^5.4.11",
    "tailwindcss": "^3.4.17",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.49"
  }
}

```

### backend/main.py

```python
"""
ShadowGuard FastAPI Backend
Healthcare Shadow AI Detection & Governance System

Run: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
"""

import asyncio
import json
from contextlib import asynccontextmanager
from datetime import datetime, timezone

from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query, HTTPException
from fastapi.middleware.cors import CORSMiddleware

from database import init_db, get_cursor, dict_cursor, close_pool
from models import EventCreate, EventResponse, StatusUpdate, StatsResponse
from seed import generate_seed_events, insert_seed_events
from vapi_caller import maybe_trigger_call, is_configured as vapi_is_configured, _make_vapi_call, ALERT_PHONE_NUMBER


# ============================================================
# WebSocket Manager
# ============================================================

class ConnectionManager:
    """Manages active WebSocket connections for real-time broadcasts."""

    def __init__(self):
        self.active_connections: list[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        if websocket in self.active_connections:
            self.active_connections.remove(websocket)

    async def broadcast(self, message: dict):
        dead = []
        for conn in self.active_connections:
            try:
                await conn.send_json(message)
            except Exception:
                dead.append(conn)
        for conn in dead:
            self.disconnect(conn)


manager = ConnectionManager()


def broadcast_from_thread(message: dict):
    """Schedule an async broadcast from a synchronous background thread."""
    try:
        loop = asyncio.get_event_loop()
        if loop.is_running():
            asyncio.run_coroutine_threadsafe(manager.broadcast(message), loop)
    except RuntimeError:
        pass


# ============================================================
# App Lifecycle
# ============================================================

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    init_db()
    print("✅ Database initialized")
    yield
    # Shutdown
    close_pool()
    print("🛑 Database pool closed")


app = FastAPI(
    title="ShadowGuard API",
    description="Healthcare Shadow AI Detection & Governance",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS — allow all origins for hackathon
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ============================================================
# Helper
# ============================================================

def _row_to_dict(row: dict) -> dict:
    """Convert a database row to a JSON-serializable dict."""
    d = dict(row)
    for key in ("timestamp", "created_at"):
        if key in d and d[key] is not None:
            d[key] = d[key].isoformat()
    if "event_id" in d and d["event_id"] is not None:
        d["event_id"] = str(d["event_id"])
    # Parse JSONB fields that might be strings
    for key in ("phi_types", "phi_findings"):
        if key in d and isinstance(d[key], str):
            try:
                d[key] = json.loads(d[key])
            except (json.JSONDecodeError, TypeError):
                pass
    return d


# ============================================================
# Routes
# ============================================================

@app.get("/api/health")
def health_check():
    return {"status": "ok"}


def _sanitize(val):
    """Strip NUL bytes that PostgreSQL text fields reject."""
    if isinstance(val, str):
        return val.replace("\x00", "")
    return val


@app.post("/api/events", status_code=201)
async def create_event(event: EventCreate):
    """Ingest a new event from mitmproxy."""
    phi_types_json = json.dumps(event.phi_types) if event.phi_types else json.dumps([])
    phi_findings_json = json.dumps(event.phi_findings) if event.phi_findings else json.dumps([])

    with get_cursor(cursor_factory=dict_cursor()) as cur:
        cur.execute(
            """
            INSERT INTO events (
                source_ip, user_agent, ai_service, request_method, request_path,
                risk_score, severity, phi_detected, phi_count,
                phi_types, phi_findings, original_text, redacted_text,
                action, engine, response_time_ms
            ) VALUES (
                %s, %s, %s, %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s
            )
            RETURNING *
            """,
            (
                _sanitize(event.source_ip), _sanitize(event.user_agent), _sanitize(event.ai_service),
                _sanitize(event.request_method), _sanitize(event.request_path),
                event.risk_score, _sanitize(event.severity), event.phi_detected, event.phi_count,
                _sanitize(phi_types_json), _sanitize(phi_findings_json),
                _sanitize(event.original_text), _sanitize(event.redacted_text),
                _sanitize(event.action), _sanitize(event.engine), event.response_time_ms,
            ),
        )
        row = cur.fetchone()

    created = _row_to_dict(row)

    # Broadcast to WebSocket clients
    await manager.broadcast({"type": "new_event", "data": created})

    # Trigger VAPI voice call for high-risk events (non-blocking)
    maybe_trigger_call(created, broadcast_fn=broadcast_from_thread)

    return created


@app.get("/api/events")
def list_events(
    limit: int = Query(50, ge=1, le=500),
    offset: int = Query(0, ge=0),
    severity: str | None = Query(None),
    service: str | None = Query(None),
    status: str | None = Query(None),
):
    """List events with pagination and optional filters."""
    query = "SELECT * FROM events WHERE 1=1"
    params: list = []

    if severity:
        query +=
[truncated — 8933 more characters]
```

### dashboard/src/main.jsx

```javascript
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

```

### dashboard/src/App.jsx

```javascript
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import Header from './components/Header';
import StatsCards from './components/StatsCards';
import ThreatFeed from './components/ThreatFeed';
import TrafficTimeline from './components/TrafficTimeline';
import RiskHeatmap from './components/RiskHeatmap';
import NetworkGraph from './components/NetworkGraph';
import AuditLog from './components/AuditLog';
import RedactionViewer from './components/RedactionViewer';
import { useWebSocket } from './hooks/useWebSocket';
import { fetchStats, fetchEvents, fetchCalls, fetchCallStats } from './lib/api';

const defaultStats = {
  total_requests: 0,
  phi_detected: 0,
  requests_redacted: 0,
  requests_clean: 0,
  avg_risk_score: 0,
  by_service: {},
  by_severity: {},
  by_hour: [],
  recent_phi_types: {},
  timeline: [],
};

export default function App() {
  const [stats, setStats] = useState(defaultStats);
  const [events, setEvents] = useState([]);
  const [selectedEvent, setSelectedEvent] = useState(null);
  const [newEventIds, setNewEventIds] = useState(new Set());
  const [callStats, setCallStats] = useState({ total_calls: 0, completed_calls: 0, failed_calls: 0 });
  const newEventTimeouts = useRef(new Map());

  // Load initial data
  useEffect(() => {
    async function loadData() {
      try {
        const [statsData, eventsData, callData, callsList] = await Promise.all([
          fetchStats(),
          fetchEvents({ limit: 200 }),
          fetchCallStats().catch(() => ({ total_calls: 0, completed_calls: 0, failed_calls: 0 })),
          fetchCalls({ limit: 200 }).catch(() => []),
        ]);
        setStats(statsData);
        setCallStats(callData);

        // Merge call data into events
        const callMap = {};
        callsList.forEach((c) => {
          callMap[c.event_id] = { call_id: c.call_id, status: c.status, phone_number: c.phone_number };
        });
        const enrichedEvents = eventsData.map((e) => ({
          ...e,
          voice_call: callMap[e.event_id] || null,
        }));
        setEvents(enrichedEvents);
      } catch (err) {
        console.error('Failed to load initial data:', err);
      }
    }
    loadData();
  }, []);

  // Handle WebSocket messages
  const handleWsMessage = useCallback((msg) => {
    if (msg.type === 'new_event') {
      const event = msg.data;

      // Add to events list
      setEvents((prev) => [event, ...prev]);

      // Update stats incrementally
      setStats((prev) => ({
        ...prev,
        total_requests: prev.total_requests + 1,
        phi_detected: prev.phi_detected + (event.phi_detected ? 1 : 0),
        requests_redacted: prev.requests_redacted + (event.action === 'redacted' ? 1 : 0),
        requests_clean: prev.requests_clean + (event.action === 'clean' ? 1 : 0),
        avg_risk_score:
          prev.total_requests > 0
            ? parseFloat(
                (
                  (prev.avg_risk_score * prev.total_requests + (event.risk_score || 0)) /
                  (prev.total_requests + 1)
                ).toFixed(1)
              )
            : event.risk_score || 0,
        by_service: {
          ...prev.by_service,
          [event.ai_service]: (prev.by_service[event.ai_service] || 0) + 1,
        },
        by_severity: {
          ...prev.by_severity,
          [event.severity]: (prev.by_severity[event.severity] || 0) + 1,
        },
      }));

      // Mark as new for animation
      setNewEventIds((prev) => {
        const next = new Set(prev);
        next.add(event.event_id);
        return next;
      });

      // Remove "new" marker after animation completes
      const timeout = setTimeout(() => {
        setNewEventIds((prev) => {
          const next = new Set(prev);
          next.delete(event.event_id);
          return next;
        });
      }, 2000);

      newEventTimeouts.current.set(event.event_id, timeout);
    }

    if (msg.type === 'status_update') {
      const { event_id, status } = msg.data;
      setEvents((prev) =>
        prev.map((e) =>
          e.event_id === event_id ? { ...e, status } : e
        )
      );
    }

    if (msg.type === 'voice_call') {
      const { event_id, call_id, status, phone_number } = msg.data;
      setEvents((prev) =>
        prev.map((e) =>
          e.event_id === event_id
            ? { ...e, voice_call: { call_id, status, phone_number } }
            : e
        )
      );
      setCallStats((prev) => ({ ...prev, total_calls: prev.total_calls + 1 }));
    }
  }, []);

  const { connected } = useWebSocket(handleWsMessage);

  // Cleanup timeouts on unmount
  useEffect(() => {
    return () => {
      newEventTimeouts.current.forEach((t) => clearTimeout(t));
    };
  }, []);

  // Derive stats from PHI-detected events only
  const phiStats = useMemo(() => {
    const phiEvents = events.filter((e) => e.phi_detected);
    const total = phiEvents.length;
    const avgRisk = total > 0
      ? parseFloat((phiEvents.reduce((s, e) => s + (e.risk_score || 0), 0) / total).toFixed(1))
      : 0;
    return {
      total_requests: events.length,
      phi_detected: total,
      requests_redacted: phiEvents.filter((e) => e.action === 'redacted').length,
      requests_clean: phiEvents.filter((e) => e.action === 'clean').length,
      avg_risk_score: avgRisk,
    };
  }, [events]);

  return (
    <div className="min-h-screen p-4">
      <Header connected={connected} totalEvents={events.length} />

      <StatsCards stats={phiStats} callStats={callStats} />

      {/* Main grid: ThreatFeed | Charts | NetworkGraph */}
      <div className="grid grid-cols-12 gap-4 mb-4">
        {/* Left: Threat Feed */}
        <div className="col-span-3">
          <ThreatFeed
            events={events}
            newEventIds={newEventIds}
            onSelectEvent={setSelectedEvent}
          />
        </div>

        {/* Center: Charts */}
        <div className="col-span-6 space-y-4">
          <TrafficTimeline events={events} />

[truncated — 513 more characters]
```

### setup.sh

```shell
#!/bin/bash
# ShadowGuard - Quick Setup Script
# Run this on your laptop (macOS or Linux)

set -e

echo "🛡️  ShadowGuard Test Setup"
echo "========================="
echo ""

# Detect OS
OS=$(uname -s)
echo "Detected OS: $OS"

# Step 1: Install mitmproxy
echo ""
echo "📦 Step 1: Installing mitmproxy..."
if command -v mitmdump &> /dev/null; then
    echo "  ✅ mitmproxy already installed: $(mitmdump --version | head -1)"
else
    if [ "$OS" == "Darwin" ]; then
        echo "  Installing via brew..."
        brew install mitmproxy
    else
        echo "  Installing via pip..."
        pip install mitmproxy
    fi
    echo "  ✅ mitmproxy installed"
fi

# Step 2: Install Python dependencies for the addon
echo ""
echo "📦 Step 2: Installing Python dependencies..."
pip install requests 2>/dev/null || pip install requests --break-system-packages 2>/dev/null
echo "  ✅ Dependencies installed"

# Step 3: Generate mitmproxy CA cert (first run creates it)
echo ""
echo "🔐 Step 3: Generating mitmproxy CA certificate..."
# Start and immediately stop mitmdump to generate certs
timeout 2 mitmdump --listen-port 18080 2>/dev/null || true
CERT_DIR="$HOME/.mitmproxy"
if [ -f "$CERT_DIR/mitmproxy-ca-cert.pem" ]; then
    echo "  ✅ CA cert generated at: $CERT_DIR/mitmproxy-ca-cert.pem"
else
    echo "  ⚠️  Cert not found. It'll be generated on first run."
fi

# Step 4: Install CA cert into system trust store
echo ""
echo "🔐 Step 4: Installing CA certificate..."
if [ "$OS" == "Darwin" ]; then
    echo "  On macOS, we'll open Keychain Access."
    echo "  You need to:"
    echo "    1. Double-click the cert to add it"
    echo "    2. Find 'mitmproxy' in Keychain"
    echo "    3. Double-click it → Trust → 'Always Trust'"
    echo ""
    read -p "  Press Enter to open the cert in Keychain Access..."
    open "$CERT_DIR/mitmproxy-ca-cert.pem" 2>/dev/null || echo "  Open manually: $CERT_DIR/mitmproxy-ca-cert.pem"
    echo ""
    echo "  ⚠️  IMPORTANT: After adding, set it to 'Always Trust'!"
    echo "  (Double-click the cert in Keychain → Trust → Always Trust)"
    read -p "  Press Enter once you've trusted the cert..."
elif [ "$OS" == "Linux" ]; then
    echo "  Installing cert system-wide (needs sudo)..."
    sudo cp "$CERT_DIR/mitmproxy-ca-cert.pem" /usr/local/share/ca-certificates/mitmproxy.crt
    sudo update-ca-certificates
    echo "  ✅ CA cert installed system-wide"
fi

# Step 5: Summary
echo ""
echo "========================================="
echo "✅ Setup complete!"
echo ""
echo "To start intercepting:"
echo "  1. Start the proxy:"
echo "     mitmdump -s shadowguard_addon.py --listen-port 8080"
echo ""
echo "  2. Test with browser (open a new Chrome window):"
echo "     On macOS:"
echo "       open -na 'Google Chrome' --args --proxy-server='http://localhost:8080'"
echo "     On Linux:"
echo "       google-chrome --proxy-server='http://localhost:8080'"
echo ""
echo "  3. Test with terminal:"
echo "     export HTTPS_PROXY=http://localhost:8080"
echo "     export SSL_CERT_FILE=$CERT_DIR/mitmproxy-ca-cert.pem"
echo "     curl https://api.openai.com/v1/models"
echo ""
echo "  4. Or run the test script:"
echo "     python3 test_interception.py"
echo "========================================="
```

### shadowguard_addon.py

```python
"""
ShadowGuard - mitmproxy Addon
Intercepts HTTPS traffic to AI services and detects PHI.

Usage:
    mitmdump -s shadowguard_addon.py --listen-port 8080
"""

import mitmproxy.http
from mitmproxy import ctx
import json
import re
import time
import urllib.request
import threading
from datetime import datetime

# Import the PHI redactor
from phi_redactor import PHIRedactor


# ============================================================
# CONFIGURATION
# ============================================================

# PHI engine: "regex" (default) or "ollama" (requires local Ollama with llama3.2:3b)
PHI_ENGINE = "regex"

# Known AI service domains → friendly names
AI_DOMAINS = {
    # OpenAI
    "api.openai.com": "OpenAI API",
    "chat.openai.com": "ChatGPT",
    "chatgpt.com": "ChatGPT",
    "cdn.oaistatic.com": "OpenAI Static",
    # Anthropic
    "api.anthropic.com": "Anthropic API",
    "claude.ai": "Claude",
    # Google
    "generativelanguage.googleapis.com": "Gemini API",
    "gemini.google.com": "Gemini",
    "bard.google.com": "Bard",
    # Others
    "chat.deepseek.com": "DeepSeek",
    "api.cohere.ai": "Cohere",
    "api.perplexity.ai": "Perplexity",
    "api-inference.huggingface.co": "HuggingFace",
    "api.mistral.ai": "Mistral",
    "api.together.xyz": "Together AI",
    "api.groq.com": "Groq",
    "copilot.microsoft.com": "MS Copilot",
}

# Domains to ignore (static assets, telemetry, etc.)
IGNORE_PATHS = {
    "/v1/models",  # Just listing models, not sending data
    "/favicon.ico",
    "/_next/",
    "/assets/",
}

# PHI detection patterns
PHI_PATTERNS = {
    "SSN": r"\b\d{3}-\d{2}-\d{4}\b",
    "MRN": r"\bMRN[\s:#]*\d{5,}\b",
    "Phone": r"\b\d{3}[-.)]\s*\d{3}[-.)]\s*\d{4}\b",
    "DOB": r"\b(?:DOB|date\s*of\s*birth)[\s:]*\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b",
    "Patient_Name": r"\b(?:patient|pt|name)[\s:]+[A-Z][a-z]+\s+[A-Z][a-z]+\b",
    "Diagnosis_Code": r"\b[A-Z]\d{2}\.?\d{0,4}\b",  # ICD-10 codes like E11.9
    "Email": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
    "Address": r"\b\d{1,5}\s+[A-Z][a-z]+\s+(?:St|Ave|Blvd|Dr|Rd|Ln|Way)\b",
}

# Medical keywords that boost risk score
MEDICAL_KEYWORDS = [
    "patient",
    "diagnosis",
    "prescribed",
    "medication",
    "discharge",
    "admission",
    "lab results",
    "radiology",
    "MRI",
    "CT scan",
    "blood pressure",
    "heart rate",
    "allergies",
    "surgery",
    "prognosis",
    "treatment plan",
    "medical record",
    "clinical notes",
    "HIPAA",
    "PHI",
    "EHR",
    "ICD",
    "CPT",
    "vital signs",
]


# ============================================================
# RISK SCORING
# ============================================================


def detect_phi(text: str) -> dict:
    """Scan text for PHI patterns. Returns dict of pattern_name → matches."""
    findings = {}
    for name, pattern in PHI_PATTERNS.items():
        matches = re.findall(pattern, text, re.IGNORECASE)
        if matches:
            findings[name] = matches
    return findings


def count_medical_keywords(text: str) -> int:
    """Count how many medical keywords appear in the text."""
    text_lower = text.lower()
    return sum(1 for kw in MEDICAL_KEYWORDS if kw.lower() in text_lower)


def score_risk(body: str, service: str, method: str) -> dict:
    """
    Score the risk of a request on a 0-100 scale.
    Returns a dict with score, breakdown, and PHI findings.
    """
    score = 0
    reasons = []

    # Base score: any request to unauthorized AI service
    score += 15
    reasons.append(f"Unauthorized AI service: {service} (+15)")

    # PHI detection (biggest signal)
    phi = detect_phi(body)
    if phi:
        phi_boost = min(len(phi) * 15, 45)  # up to +45
        score += phi_boost
        for ptype, matches in phi.items():
            reasons.append(f"PHI detected [{ptype}]: {len(matches)} match(es) (+15)")

    # Medical keyword density
    med_count = count_medical_keywords(body)
    if med_count >= 3:
        med_boost = min(med_count * 3, 15)
        score += med_boost
        reasons.append(f"Medical keywords: {med_count} found (+{med_boost})")

    # Payload size (large = likely pasting documents)
    body_len = len(body)
    if body_len > 10000:
        score += 15
        reasons.append(f"Very large payload: {body_len} chars (+15)")
    elif body_len > 3000:
        score += 8
        reasons.append(f"Large payload: {body_len} chars (+8)")

    # POST/PUT = sending data (vs GET = just browsing)
    if method in ("POST", "PUT", "PATCH"):
        score += 5
        reasons.append(f"Data submission method: {method} (+5)")

    # Time-based risk (off-hours)
    hour = time.localtime().tm_hour
    if hour < 6 or hour > 22:
        score += 5
        reasons.append(f"Off-hours access: {hour}:00 (+5)")

    return {
        "score": min(score, 100),
        "reasons": reasons,
        "phi_findings": {k: len(v) for k, v in phi.items()},
        "phi_detected": len(phi) > 0,
        "medical_keyword_count": med_count,
        "payload_size": body_len,
    }


# ============================================================
# PRETTY PRINTING
# ============================================================


def risk_color(score: int) -> str:
    if score >= 70:
        return "\033[91m"  # red
    elif score >= 40:
        return "\033[93m"  # yellow
    else:
        return "\033[92m"  # green


RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"


def print_event(flow, service, risk, body):
    """Pretty-print a detected event to the terminal."""
    color = risk_color(risk["score"])
    ts = datetime.now().strftime("%H:%M:%S")

    print(f"\n{'=' * 70}")
    print(f"{BOLD}🛡️  SHADOWGUARD INTERCEPT{RESET}")
    print(f"{'=' * 70}")
    print(f"  ⏰ Time:      {ts}")
    print(f"  🌐 Service:   {service}")
    print(f"  📡 Method:    {flow.request.method} {flow.request.path}")
    print(f"  📦 Size:      {len(body)} chars")
    print(f"  {color}{BOLD}⚠️  Risk Score: {risk['score'
[truncated — 9416 more characters]
```

[22 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]