Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 87 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<p align="center">
<a href="https://bugtraceai.com"><img src="https://img.shields.io/badge/by-BugTraceAI-FF6B47?style=for-the-badge" alt="BugTraceAI" /></a>
<img src="https://img.shields.io/badge/vulns-30-red?style=for-the-badge" alt="30 Vulnerabilities" />
<img src="https://img.shields.io/badge/vulns-32-red?style=for-the-badge" alt="32 Vulnerabilities" />
<img src="https://img.shields.io/badge/docker-ready-blue?style=for-the-badge&logo=docker" alt="Docker Ready" />
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License" />
</p>
Expand All @@ -21,7 +21,7 @@

## What is this?

BugStore is a full-featured online shop where you can "adopt" exotic bugs — beetles, mantises, spiders, and ants. It looks real. It works like a real store. But under the hood, it's riddled with **30 deliberately planted security vulnerabilities** spanning the OWASP Top 10 and beyond.
BugStore is a full-featured online shop where you can "adopt" exotic bugs — beetles, mantises, spiders, and ants. It looks real. It works like a real store. But under the hood, it's riddled with **32 deliberately planted security vulnerabilities** spanning the OWASP Top 10 and beyond.

It's the official playground of [**BugTraceAI**](https://bugtraceai.com) — built so you can point your scanners, tools, or bare hands at a real-looking target and practice finding bugs in bugs.

Expand Down Expand Up @@ -58,8 +58,69 @@ Docker: Multi-stage build, single container
- Product reviews
- Admin dashboard with user/product management
- Scoring dashboard to track your progress
- **Secure Portal** with TOTP/2FA authentication

**The Vulns (30 total):**
## Secure Portal (2FA)

BugStore includes a secondary admin portal at `/secure-portal` that requires TOTP-based two-factor authentication. This coexists with the standard `/admin` (no 2FA) for testing both authentication flows.

### Setting Up 2FA

1. Login normally at `/login` with admin credentials
2. Navigate to `/secure-portal/setup`
3. Scan the QR code with Google Authenticator, Authy, or similar
4. Enter the 6-digit code to enable 2FA

### Using the Secure Portal

```bash
# 1. Login normally to get a basic token
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'

# 2. Setup TOTP (requires basic token)
curl -X POST http://localhost:8080/api/secure-portal/setup-totp \
-H "Authorization: Bearer <token>"

# 3. Enable TOTP with code from authenticator app
curl -X POST http://localhost:8080/api/secure-portal/enable-totp \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"totp_code":"123456"}'

# 4. Login to secure portal with 2FA
curl -X POST http://localhost:8080/api/secure-portal/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123","totp_code":"123456"}'
```

### Programmatic TOTP Generation

```python
import pyotp
import requests

# After setup, use the secret to generate codes
totp = pyotp.TOTP("YOUR_TOTP_SECRET")
code = totp.now() # Valid for 30 seconds

# Login with generated code
requests.post("http://localhost:8080/api/secure-portal/login", json={
"username": "admin",
"password": "admin123",
"totp_code": code
})
```

### 2FA Vulnerabilities

| ID | Vulnerability | Description |
|----|---------------|-------------|
| V-030 | TOTP Brute Force | No rate limiting on `/api/secure-portal/login` |
| V-031 | Secret Disclosure | `totp_secret` exposed in login response |

**The Vulns (32 total):**

| Tier | Difficulty | Points | Examples |
|------|-----------|--------|----------|
Expand All @@ -78,6 +139,29 @@ The full list with PoCs is at `/api/debug/vulns` (Level 0 only) or on the Scoreb
| User | john_doe | password123 |
| User | jane_mantis | ilovemantis |

### 2FA Pre-configured User

For testing the Secure Portal without manual setup:

| Field | Value |
|-------|-------|
| Username | `admin2fa` |
| Password | `admin2fa123` |
| TOTP Secret | `JBSWY3DPEHPK3PXP` |

Generate TOTP code:
```bash
python3 -c "import pyotp; print(pyotp.TOTP('JBSWY3DPEHPK3PXP').now())"
```

Quick login test:
```bash
CODE=$(python3 -c "import pyotp; print(pyotp.TOTP('JBSWY3DPEHPK3PXP').now())")
curl -X POST http://localhost:8080/api/secure-portal/login \
-H "Content-Type: application/json" \
-d "{\"username\":\"admin2fa\",\"password\":\"admin2fa123\",\"totp_code\":\"$CODE\"}"
```

## Configuration

| Variable | Default | What it does |
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ cffi==1.16.0
pydantic==2.5.2
pydantic-settings==2.1.0
email-validator>=2.0.0
pyotp==2.9.0
qrcode[pil]==7.4.2
20 changes: 16 additions & 4 deletions seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,26 @@ def get_md5_hash(password: str):
print("Creating Users...")
users = [
User(
username="admin",
email="admin@bugstore.com",
password_hash=get_md5_hash("admin123"),
role="admin",
username="admin",
email="admin@bugstore.com",
password_hash=get_md5_hash("admin123"),
role="admin",
name="Queen Bee",
bio="Ruler of the hive. Approach with detailed reports.",
avatar_url="https://api.dicebear.com/7.x/avataaars/svg?seed=admin"
),
# Admin with TOTP pre-configured for secure-portal testing
User(
username="admin2fa",
email="admin2fa@bugstore.com",
password_hash=get_md5_hash("admin2fa123"),
role="admin",
name="Secure Queen",
bio="2FA-protected administrator for testing.",
avatar_url="https://api.dicebear.com/7.x/avataaars/svg?seed=admin2fa",
totp_secret="JBSWY3DPEHPK3PXP", # Known test secret
totp_enabled=True
),
User(
username="staff",
email="staff@bugstore.com",
Expand Down
7 changes: 7 additions & 0 deletions src/frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import AdminDashboard from './pages/AdminDashboard';
import AdminUsers from './pages/AdminUsers';
import AdminProducts from './pages/AdminProducts';
import ScoringDashboard from './pages/ScoringDashboard';
import SecurePortalLogin from './pages/SecurePortalLogin';
import SecurePortalSetup from './pages/SecurePortalSetup';
import SecurePortalDashboard from './pages/SecurePortalDashboard';
import { useConfig } from './ConfigContext';

const NotFound = () => (
Expand Down Expand Up @@ -54,6 +57,10 @@ function App() {
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/admin/users" element={<AdminUsers />} />
<Route path="/admin/products" element={<AdminProducts />} />
{/* Secure Portal Routes (with 2FA) */}
<Route path="/secure-portal/login" element={<SecurePortalLogin />} />
<Route path="/secure-portal/setup" element={<SecurePortalSetup />} />
<Route path="/secure-portal" element={<SecurePortalDashboard />} />
{config.scoring_enabled && (
<Route path="/scoring" element={<ScoringDashboard />} />
)}
Expand Down
207 changes: 207 additions & 0 deletions src/frontend/src/pages/SecurePortalDashboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import React, { useState, useEffect } from 'react';
import { ShieldCheck, Users, Package, DollarSign, TrendingUp, LogOut, Lock } from 'lucide-react';
import { Link, useNavigate } from 'react-router-dom';

const SecurePortalDashboard = () => {
const navigate = useNavigate();
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
const [user] = useState(() => JSON.parse(localStorage.getItem('secure_user') || 'null'));
const token = localStorage.getItem('secure_token');

useEffect(() => {
if (!user || !token) {
navigate('/secure-portal/login');
return;
}

const fetchStats = async () => {
try {
const res = await fetch('/api/secure-portal/stats', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
const data = await res.json();
setStats(data);
} else if (res.status === 403) {
navigate('/secure-portal/login');
}
} catch (err) {
console.error("Secure portal error:", err);
} finally {
setLoading(false);
}
};

fetchStats();
}, [user, token, navigate]);

const handleLogout = () => {
localStorage.removeItem('secure_token');
localStorage.removeItem('secure_user');
navigate('/secure-portal/login');
};

if (loading) {
return (
<div className="min-h-[50vh] flex items-center justify-center">
<div className="text-coral font-bold flex items-center gap-3">
<ShieldCheck className="w-6 h-6 animate-pulse" />
Verifying 2FA credentials...
</div>
</div>
);
}

if (!stats) {
return (
<div className="min-h-[50vh] flex items-center justify-center">
<div className="text-center space-y-4">
<div className="text-red-400 font-bold">Access denied. 2FA verification required.</div>
<Link to="/secure-portal/login" className="text-coral underline text-sm">
Go to Secure Login
</Link>
</div>
</div>
);
}

return (
<div className="container mx-auto p-4 py-12 max-w-7xl">
{/* Security Banner */}
<div className="bg-coral/10 border border-coral/20 rounded-2xl py-3 px-6 mb-12 flex items-center justify-center gap-2 text-coral text-sm font-bold">
<Lock className="w-4 h-4" />
<span>SECURE SESSION - 2FA VERIFIED</span>
<span className="mx-2 opacity-30">|</span>
<span>User: {user?.username}</span>
</div>

{/* Header */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-8 mb-16">
<div className="space-y-4">
<div className="flex items-center gap-3 text-coral font-black uppercase tracking-[0.3em] text-sm">
<ShieldCheck className="w-5 h-5" /> Secure Portal
</div>
<h1 className="text-5xl font-black text-hive-text uppercase tracking-tighter leading-none">
Protected Dashboard
</h1>
</div>

<div className="flex gap-4">
<Link
to="/admin"
className="flex items-center gap-2 bg-hive-medium/50 border border-hive-border/20 text-hive-muted px-6 py-3 rounded-xl font-black uppercase tracking-widest text-xs hover:bg-hive-medium transition-all"
>
Standard Admin
</Link>
<button
onClick={handleLogout}
className="flex items-center gap-2 bg-red-500/10 border border-red-500/20 text-red-400 px-6 py-3 rounded-xl font-black uppercase tracking-widest text-xs hover:bg-red-500/20 transition-all"
>
<LogOut className="w-4 h-4" /> End Session
</button>
</div>
</div>

{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
{[
{ label: 'Colony Members', val: stats.counters.users, icon: Users, color: 'text-coral' },
{ label: 'Total Orders', val: stats.counters.orders, icon: TrendingUp, color: 'text-blue-400' },
{ label: 'Revenue', val: `$${stats.counters.revenue.toFixed(2)}`, icon: DollarSign, color: 'text-yellow-400' },
{ label: 'Products', val: stats.counters.products, icon: Package, color: 'text-purple-400' }
].map((item, idx) => (
<div key={idx} className="bg-hive-medium/40 backdrop-blur border border-hive-border/20 p-8 rounded-3xl hover:-translate-y-1 transition-all">
<div className={`w-12 h-12 bg-hive-light/10 ${item.color} rounded-2xl flex items-center justify-center mb-6`}>
<item.icon className="w-6 h-6" />
</div>
<div className="text-[10px] font-black uppercase tracking-widest text-hive-subtle mb-1">{item.label}</div>
<div className="text-3xl font-black text-hive-text">{item.val}</div>
</div>
))}
</div>

{/* Recent Orders */}
{stats.recent_orders && stats.recent_orders.length > 0 && (
<div className="bg-hive-medium/40 backdrop-blur border border-hive-border/20 rounded-3xl p-8 mb-16">
<h2 className="text-xl font-black text-hive-text uppercase tracking-tight mb-6">Recent Orders</h2>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="text-left text-[10px] font-black uppercase tracking-widest text-hive-subtle border-b border-hive-border/20">
<th className="pb-4">Order ID</th>
<th className="pb-4">Total</th>
<th className="pb-4">Status</th>
<th className="pb-4">Date</th>
</tr>
</thead>
<tbody className="divide-y divide-hive-border/10">
{stats.recent_orders.map((order) => (
<tr key={order.id} className="text-hive-text">
<td className="py-4 font-mono text-coral">#{order.id}</td>
<td className="py-4">${order.total.toFixed(2)}</td>
<td className="py-4">
<span className={`px-3 py-1 rounded-full text-xs font-bold ${
order.status === 'Delivered' ? 'bg-green-500/20 text-green-400' :
order.status === 'Shipped' ? 'bg-blue-500/20 text-blue-400' :
'bg-yellow-500/20 text-yellow-400'
}`}>
{order.status}
</span>
</td>
<td className="py-4 text-hive-muted text-sm">
{new Date(order.date).toLocaleDateString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}

{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Link
to="/admin/users"
className="bg-hive-medium/40 backdrop-blur border border-hive-border/20 p-8 rounded-3xl hover:border-coral/30 transition-all group"
>
<div className="flex items-center gap-4">
<Users className="w-10 h-10 text-coral group-hover:scale-110 transition-transform" />
<div>
<div className="font-black text-hive-text uppercase tracking-tight text-xl">User Management</div>
<div className="text-xs text-hive-subtle">View and manage colony members</div>
</div>
</div>
</Link>

<Link
to="/admin/products"
className="bg-hive-medium/40 backdrop-blur border border-hive-border/20 p-8 rounded-3xl hover:border-coral/30 transition-all group"
>
<div className="flex items-center gap-4">
<Package className="w-10 h-10 text-coral group-hover:scale-110 transition-transform" />
<div>
<div className="font-black text-hive-text uppercase tracking-tight text-xl">Product Inventory</div>
<div className="text-xs text-hive-subtle">Manage specimens</div>
</div>
</div>
</Link>
</div>

{/* Security Notice */}
<div className="mt-16 bg-coral/5 border border-coral/20 rounded-3xl p-8">
<div className="flex items-center gap-3 text-coral mb-4">
<ShieldCheck className="w-6 h-6" />
<span className="font-black uppercase tracking-widest text-sm">Security Level: Maximum</span>
</div>
<p className="text-hive-muted text-sm">
This session is protected by two-factor authentication. All actions are logged and audited.
Your session will expire after 24 hours of inactivity.
</p>
</div>
</div>
);
};

export default SecurePortalDashboard;
Loading