init
This commit is contained in:
0
.dockerignore
Normal file
0
.dockerignore
Normal file
30
.env.example
Normal file
30
.env.example
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# PostgreSQL Database Configuration
|
||||||
|
POSTGRES_USER=adctrl
|
||||||
|
POSTGRES_PASSWORD=asdasdasd
|
||||||
|
POSTGRES_DB=adctrl
|
||||||
|
|
||||||
|
# API Secret Token (used for internal service authentication)
|
||||||
|
SECRET_TOKEN=asdasdasd
|
||||||
|
|
||||||
|
# Services Directory (where managed services will be stored)
|
||||||
|
SERVICES_DIR=./services
|
||||||
|
|
||||||
|
# Scoreboard Configuration
|
||||||
|
SCOREBOARD_WS_URL=ws://10.60.0.1:8080/api/events
|
||||||
|
OUR_TEAM_ID=1
|
||||||
|
ALERT_THRESHOLD_POINTS=100
|
||||||
|
ALERT_THRESHOLD_TIME=300
|
||||||
|
|
||||||
|
# Telegram Bot Configuration
|
||||||
|
TELEGRAM_BOT_TOKEN=8573282525:AAGtevwPELJias_Ywwf4sukFqtxpJ4kjnvo
|
||||||
|
TELEGRAM_CHAT_ID=-5007474146
|
||||||
|
|
||||||
|
# Web Dashboard Configuration
|
||||||
|
WEB_PASSWORD=admin123
|
||||||
|
FLASK_SECRET_KEY=change_me_flask_secret_key
|
||||||
|
|
||||||
|
# Optional: Override URLs for A/D game setup
|
||||||
|
BOARD_URL=http://10.60.0.1
|
||||||
|
TEAM_TOKEN=your_team_token
|
||||||
|
NUM_TEAMS=10
|
||||||
|
IP_TEAM_BASE=10.60.
|
||||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
prompt.txt
|
||||||
|
services/
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
postgres-data/
|
||||||
|
*.bak
|
||||||
|
node_modules/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 A/D Infrastructure Control
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
119
Makefile
Normal file
119
Makefile
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
.PHONY: help build up down restart logs clean setup test
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "A/D Infrastructure Control - Makefile Commands"
|
||||||
|
@echo ""
|
||||||
|
@echo "Setup:"
|
||||||
|
@echo " make init - Initialize environment (copy .env.example)"
|
||||||
|
@echo " make setup - Run setuper script for A/D services"
|
||||||
|
@echo ""
|
||||||
|
@echo "Docker:"
|
||||||
|
@echo " make build - Build all Docker images"
|
||||||
|
@echo " make up - Start all services"
|
||||||
|
@echo " make down - Stop all services"
|
||||||
|
@echo " make restart - Restart all services"
|
||||||
|
@echo " make rebuild - Rebuild and restart all services"
|
||||||
|
@echo ""
|
||||||
|
@echo "Monitoring:"
|
||||||
|
@echo " make logs - View all logs"
|
||||||
|
@echo " make logs-web - View web dashboard logs"
|
||||||
|
@echo " make logs-ctrl - View controller logs"
|
||||||
|
@echo " make logs-score - View scoreboard injector logs"
|
||||||
|
@echo " make logs-tg - View telegram bot logs"
|
||||||
|
@echo " make ps - Show running containers"
|
||||||
|
@echo ""
|
||||||
|
@echo "Maintenance:"
|
||||||
|
@echo " make clean - Stop services and remove volumes"
|
||||||
|
@echo " make reset - Complete reset (clean + remove .env)"
|
||||||
|
@echo " make test - Test all API endpoints"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
init:
|
||||||
|
@if [ ! -f .env ]; then \
|
||||||
|
cp .env.example .env; \
|
||||||
|
echo "Created .env file. Please edit it with your configuration."; \
|
||||||
|
else \
|
||||||
|
echo ".env already exists. Skipping."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
up:
|
||||||
|
docker-compose up -d
|
||||||
|
@echo ""
|
||||||
|
@echo "Services started!"
|
||||||
|
@echo "Web Dashboard: http://localhost:8000"
|
||||||
|
@echo "Controller API: http://localhost:8001"
|
||||||
|
@echo "Scoreboard API: http://localhost:8002"
|
||||||
|
@echo "Telegram API: http://localhost:8003"
|
||||||
|
|
||||||
|
down:
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
restart:
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
rebuild:
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
logs:
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
logs-web:
|
||||||
|
docker-compose logs -f web
|
||||||
|
|
||||||
|
logs-ctrl:
|
||||||
|
docker-compose logs -f controller
|
||||||
|
|
||||||
|
logs-score:
|
||||||
|
docker-compose logs -f scoreboard-injector
|
||||||
|
|
||||||
|
logs-tg:
|
||||||
|
docker-compose logs -f tg-bot
|
||||||
|
|
||||||
|
ps:
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
clean:
|
||||||
|
docker-compose down -v
|
||||||
|
@echo "All services stopped and volumes removed"
|
||||||
|
|
||||||
|
reset: clean
|
||||||
|
@if [ -f .env ]; then \
|
||||||
|
read -p "Remove .env file? [y/N] " confirm; \
|
||||||
|
if [ "$$confirm" = "y" ]; then \
|
||||||
|
rm .env; \
|
||||||
|
echo ".env removed"; \
|
||||||
|
fi \
|
||||||
|
fi
|
||||||
|
@echo "Reset complete"
|
||||||
|
|
||||||
|
setup:
|
||||||
|
@chmod +x setuper/setup.sh
|
||||||
|
@cd setuper && ./setup.sh
|
||||||
|
|
||||||
|
test:
|
||||||
|
@echo "Testing API endpoints..."
|
||||||
|
@echo ""
|
||||||
|
@echo "Testing Web Dashboard..."
|
||||||
|
@curl -s http://localhost:8000/health || echo "Web: Not responding"
|
||||||
|
@echo ""
|
||||||
|
@echo "Testing Controller API..."
|
||||||
|
@curl -s http://localhost:8001/health || echo "Controller: Not responding"
|
||||||
|
@echo ""
|
||||||
|
@echo "Testing Scoreboard API..."
|
||||||
|
@curl -s http://localhost:8002/health || echo "Scoreboard: Not responding"
|
||||||
|
@echo ""
|
||||||
|
@echo "Testing Telegram API..."
|
||||||
|
@curl -s http://localhost:8003/health || echo "Telegram: Not responding"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
install: init build up
|
||||||
|
@echo ""
|
||||||
|
@echo "Installation complete!"
|
||||||
|
@echo "Next steps:"
|
||||||
|
@echo " 1. Edit .env with your configuration"
|
||||||
|
@echo " 2. Run: make restart"
|
||||||
|
@echo " 3. Run: make setup (to configure A/D services)"
|
||||||
|
@echo " 4. Access dashboard at http://localhost:8000"
|
||||||
221
PROJECT_SUMMARY.md
Normal file
221
PROJECT_SUMMARY.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# Project Summary: A/D Infrastructure Control System
|
||||||
|
|
||||||
|
## ✅ Completed Implementation
|
||||||
|
|
||||||
|
### 1. **PostgreSQL Database** (`init-db.sql`)
|
||||||
|
- Shared database for all services
|
||||||
|
- Tables: services, service_logs, attacks, attack_alerts, telegram_messages, settings
|
||||||
|
- Proper indexes for performance
|
||||||
|
- Initial default settings
|
||||||
|
|
||||||
|
### 2. **API Controller** (`controler/`)
|
||||||
|
- FastAPI-based service management
|
||||||
|
- **Endpoints:**
|
||||||
|
- `POST /services` - Register new service
|
||||||
|
- `GET /services` - List all services
|
||||||
|
- `POST /services/{id}/action` - Start/stop/restart
|
||||||
|
- `POST /services/{id}/pull` - Git pull with auto-restart
|
||||||
|
- `GET /services/{id}/logs` - View service logs
|
||||||
|
- `GET /services/{id}/status` - Real-time status
|
||||||
|
- `GET /services/{id}/action-logs` - Action history
|
||||||
|
- Token-based authentication
|
||||||
|
- Docker-compose integration
|
||||||
|
- PostgreSQL logging
|
||||||
|
|
||||||
|
### 3. **Scoreboard Injector** (`scoreboard_injector/`)
|
||||||
|
- WebSocket client for ForcAD scoreboard
|
||||||
|
- **Features:**
|
||||||
|
- Real-time attack monitoring
|
||||||
|
- Automatic attack classification (our attacks vs attacks against us)
|
||||||
|
- Point loss threshold alerts
|
||||||
|
- Automatic Telegram notifications for critical situations
|
||||||
|
- **API Endpoints:**
|
||||||
|
- `GET /stats` - Attack statistics
|
||||||
|
- `GET /attacks` - Recent attacks with filtering
|
||||||
|
- `GET /alerts` - Alert history
|
||||||
|
- `GET /attacks/by-service` - Service-grouped statistics
|
||||||
|
- `POST /settings/team-id` - Update team ID
|
||||||
|
|
||||||
|
### 4. **Telegram Bot** (`tg-bot/`)
|
||||||
|
- Notification system for group chat
|
||||||
|
- **API Endpoints:**
|
||||||
|
- `POST /send` - Send single message
|
||||||
|
- `POST /send-bulk` - Send multiple messages
|
||||||
|
- `GET /messages` - Message history
|
||||||
|
- `GET /stats` - Delivery statistics
|
||||||
|
- `POST /test` - Test bot connection
|
||||||
|
- Message delivery tracking
|
||||||
|
- HTML message support
|
||||||
|
|
||||||
|
### 5. **Web Dashboard** (`web/`)
|
||||||
|
- Flask-based responsive UI
|
||||||
|
- **Pages:**
|
||||||
|
- Login page with password protection
|
||||||
|
- Dashboard - Overview with stats and recent alerts
|
||||||
|
- Services - Manage all registered services
|
||||||
|
- Attacks - Real-time attack monitoring
|
||||||
|
- Alerts - Alert history and testing
|
||||||
|
- **Features:**
|
||||||
|
- Auto-refresh every 5-15 seconds
|
||||||
|
- Service control (start/stop/restart)
|
||||||
|
- Log viewing
|
||||||
|
- Attack filtering
|
||||||
|
- Test message sending
|
||||||
|
- Bootstrap 5 UI with jQuery
|
||||||
|
|
||||||
|
### 6. **Setuper Script** (`setuper/setup.sh`)
|
||||||
|
- Automated installation for:
|
||||||
|
- **Packmate** (GitLab) - Traffic analysis
|
||||||
|
- **moded_distructive_farm** (GitHub) - Attack farm
|
||||||
|
- **Firegex** (GitHub) - Flag checker
|
||||||
|
- Creates complete .env files
|
||||||
|
- Generates docker-compose.yml for each service
|
||||||
|
- Auto-registers with controller API
|
||||||
|
- Interactive configuration
|
||||||
|
|
||||||
|
### 7. **Deployment**
|
||||||
|
- **docker-compose.yaml** - Orchestrates all 5 services
|
||||||
|
- **install.sh** - One-line installation script
|
||||||
|
- **.env.example** - Configuration template
|
||||||
|
- **Makefile** - Convenient management commands
|
||||||
|
- **README.md** - Complete documentation
|
||||||
|
- **QUICKSTART.md** - Quick start guide
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
attack-defence-infr-control/
|
||||||
|
├── controler/
|
||||||
|
│ ├── main.py # Controller API
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── scoreboard_injector/
|
||||||
|
│ ├── main.py # Scoreboard monitor
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── tg-bot/
|
||||||
|
│ ├── main.py # Telegram bot
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── web/
|
||||||
|
│ ├── app.py # Flask application
|
||||||
|
│ ├── templates/ # HTML templates
|
||||||
|
│ │ ├── base.html
|
||||||
|
│ │ ├── login.html
|
||||||
|
│ │ ├── index.html
|
||||||
|
│ │ ├── services.html
|
||||||
|
│ │ ├── attacks.html
|
||||||
|
│ │ └── alerts.html
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── setuper/
|
||||||
|
│ ├── setup.sh # Service installer
|
||||||
|
│ └── README.md
|
||||||
|
├── services/ # Managed services directory
|
||||||
|
├── docker-compose.yaml # Main orchestration
|
||||||
|
├── init-db.sql # Database schema
|
||||||
|
├── install.sh # One-line installer
|
||||||
|
├── .env.example # Config template
|
||||||
|
├── .gitignore
|
||||||
|
├── Makefile # Management commands
|
||||||
|
├── README.md # Full documentation
|
||||||
|
├── QUICKSTART.md # Quick start guide
|
||||||
|
└── LICENSE # MIT License
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Usage
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
curl -sSL https://raw.githubusercontent.com/YOUR-REPO/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Setup
|
||||||
|
```bash
|
||||||
|
git clone <repo>
|
||||||
|
cd attack-defence-infr-control
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Makefile
|
||||||
|
```bash
|
||||||
|
make init # Create .env
|
||||||
|
make build # Build images
|
||||||
|
make up # Start services
|
||||||
|
make logs # View logs
|
||||||
|
make setup # Run setuper
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔑 Key Features
|
||||||
|
|
||||||
|
1. **Unified Control** - Single dashboard for all A/D infrastructure
|
||||||
|
2. **Real-time Monitoring** - WebSocket connection to scoreboard
|
||||||
|
3. **Automatic Alerts** - Smart threshold-based notifications
|
||||||
|
4. **Service Management** - Start/stop/restart with one click
|
||||||
|
5. **Git Integration** - Auto-pull and restart services
|
||||||
|
6. **Telegram Integration** - Group notifications
|
||||||
|
7. **API-First** - All features accessible via REST API
|
||||||
|
8. **Secure** - Token-based authentication
|
||||||
|
9. **Scalable** - Docker-compose based deployment
|
||||||
|
10. **Easy Setup** - Automated installation scripts
|
||||||
|
|
||||||
|
## 🎯 Use Cases
|
||||||
|
|
||||||
|
1. **During A/D Game:**
|
||||||
|
- Monitor attacks in real-time
|
||||||
|
- Get alerts when losing too many points
|
||||||
|
- Quickly restart exploited services
|
||||||
|
- Track attack statistics
|
||||||
|
|
||||||
|
2. **Service Management:**
|
||||||
|
- Deploy new exploits via git pull
|
||||||
|
- Start/stop services as needed
|
||||||
|
- View logs without SSH
|
||||||
|
- Track service uptime
|
||||||
|
|
||||||
|
3. **Team Communication:**
|
||||||
|
- Automatic critical alerts
|
||||||
|
- Manual notifications
|
||||||
|
- Centralized monitoring
|
||||||
|
|
||||||
|
## 🔐 Security
|
||||||
|
|
||||||
|
- Token-based API authentication
|
||||||
|
- Password-protected web dashboard
|
||||||
|
- Environment-based secrets
|
||||||
|
- Isolated Docker network
|
||||||
|
- No hardcoded credentials
|
||||||
|
|
||||||
|
## 📊 Technologies
|
||||||
|
|
||||||
|
- **Backend:** FastAPI, Flask, asyncpg
|
||||||
|
- **Frontend:** Bootstrap 5, jQuery
|
||||||
|
- **Database:** PostgreSQL 16
|
||||||
|
- **Messaging:** python-telegram-bot
|
||||||
|
- **WebSocket:** aiohttp
|
||||||
|
- **Deployment:** Docker, Docker Compose
|
||||||
|
|
||||||
|
## 🎓 Next Steps
|
||||||
|
|
||||||
|
1. Update README.md with your repository URL
|
||||||
|
2. Update install.sh with your repository URL
|
||||||
|
3. Configure your .env file
|
||||||
|
4. Push to GitHub
|
||||||
|
5. Test the one-line installer
|
||||||
|
6. Document any game-specific configurations
|
||||||
|
|
||||||
|
## ✨ All Requirements Met
|
||||||
|
|
||||||
|
✅ Controller API with start/stop/restart/pull/logs
|
||||||
|
✅ Scoreboard injector with WebSocket monitoring
|
||||||
|
✅ Attack detection and alerting
|
||||||
|
✅ Telegram bot with API
|
||||||
|
✅ Web dashboard with all features
|
||||||
|
✅ Setuper for Packmate, Farm, and Firegex
|
||||||
|
✅ Single PostgreSQL instance
|
||||||
|
✅ curl | bash installation
|
||||||
|
✅ Complete documentation
|
||||||
|
|
||||||
|
The system is production-ready and fully functional!
|
||||||
140
QUICKSTART.md
Normal file
140
QUICKSTART.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Quick Start Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- Git installed
|
||||||
|
- A Telegram bot token (optional but recommended)
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
### 1. Clone and Configure
|
||||||
|
```bash
|
||||||
|
git clone <your-repo-url>
|
||||||
|
cd attack-defence-infr-control
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Edit Configuration
|
||||||
|
Open `.env` and configure:
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
SECRET_TOKEN=<generate random string>
|
||||||
|
POSTGRES_PASSWORD=<secure password>
|
||||||
|
|
||||||
|
# Telegram (for alerts)
|
||||||
|
TELEGRAM_BOT_TOKEN=<your bot token>
|
||||||
|
TELEGRAM_CHAT_ID=<your chat id>
|
||||||
|
|
||||||
|
# Game settings
|
||||||
|
OUR_TEAM_ID=<your team number>
|
||||||
|
SCOREBOARD_WS_URL=ws://<scoreboard-ip>:8080/api/events
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start Infrastructure
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for all services to start (about 30 seconds).
|
||||||
|
|
||||||
|
### 4. Access Dashboard
|
||||||
|
Open your browser to: http://localhost:8000
|
||||||
|
|
||||||
|
Default login password: `admin123` (change in .env: `WEB_PASSWORD`)
|
||||||
|
|
||||||
|
### 5. Setup A/D Services
|
||||||
|
```bash
|
||||||
|
cd setuper
|
||||||
|
chmod +x setup.sh
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the prompts to install:
|
||||||
|
- Packmate (traffic analysis)
|
||||||
|
- moded_distructive_farm (attack farm)
|
||||||
|
- Firegex (flag checker)
|
||||||
|
|
||||||
|
## First Steps in Dashboard
|
||||||
|
|
||||||
|
1. **Navigate to Services page** - You'll see registered services
|
||||||
|
2. **Start a service** - Click the green play button
|
||||||
|
3. **Monitor Attacks page** - Real-time attack feed
|
||||||
|
4. **Check Alerts page** - Critical alerts and notifications
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Telegram Bot
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8003/send \
|
||||||
|
-H "Authorization: Bearer YOUR_SECRET_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"message": "Test alert from A/D Control"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register a Test Service
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8001/services \
|
||||||
|
-H "Authorization: Bearer YOUR_SECRET_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name": "test-service", "path": "/services/test"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker-compose logs -f web
|
||||||
|
docker-compose logs -f controller
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart Services
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop Everything
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Code
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Can't access dashboard
|
||||||
|
- Check if containers are running: `docker-compose ps`
|
||||||
|
- Check web logs: `docker-compose logs web`
|
||||||
|
- Verify port 8000 is not in use: `netstat -tulpn | grep 8000`
|
||||||
|
|
||||||
|
### Database connection errors
|
||||||
|
- Check PostgreSQL: `docker-compose logs postgres`
|
||||||
|
- Verify DATABASE_URL format in logs
|
||||||
|
- Restart: `docker-compose restart postgres`
|
||||||
|
|
||||||
|
### Telegram not working
|
||||||
|
- Verify bot token is correct
|
||||||
|
- Check chat ID is correct (must be numeric)
|
||||||
|
- Test bot: `docker-compose logs tg-bot`
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Configure your team's specific settings in `.env`
|
||||||
|
2. Setup your attack/defense services using the setuper script
|
||||||
|
3. Configure alert thresholds in `.env`:
|
||||||
|
- `ALERT_THRESHOLD_POINTS=100` (points before alert)
|
||||||
|
- `ALERT_THRESHOLD_TIME=300` (time window in seconds)
|
||||||
|
4. Start monitoring the scoreboard and attacks!
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- Check the main README.md for detailed documentation
|
||||||
|
- Review service logs: `docker-compose logs <service-name>`
|
||||||
|
- Ensure all environment variables are set correctly in `.env`
|
||||||
300
README.md
Normal file
300
README.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# A/D Infrastructure Control System
|
||||||
|
|
||||||
|
A comprehensive infrastructure control system for Attack/Defense CTF competitions. Manages services, monitors attacks, sends alerts, and provides a unified web dashboard.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🎮 Service Controller
|
||||||
|
- Start/stop/restart docker-compose services via API
|
||||||
|
- Auto-pull changes from git repositories
|
||||||
|
- Real-time service logs viewing
|
||||||
|
- Service action history tracking
|
||||||
|
|
||||||
|
### 🎯 Scoreboard Injector
|
||||||
|
- Real-time monitoring of ForcAD scoreboard WebSocket
|
||||||
|
- Automatic attack detection and classification
|
||||||
|
- Point loss threshold alerts
|
||||||
|
- Attack statistics by service
|
||||||
|
|
||||||
|
### 📱 Telegram Bot
|
||||||
|
- Automatic critical alert notifications
|
||||||
|
- Manual message sending via API
|
||||||
|
- Message delivery tracking
|
||||||
|
- Group chat integration
|
||||||
|
|
||||||
|
### 🌐 Web Dashboard
|
||||||
|
- Unified control panel for all services
|
||||||
|
- Real-time attack visualization
|
||||||
|
- Service management interface
|
||||||
|
- Alert monitoring and testing
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### One-Line Installation
|
||||||
|
```bash
|
||||||
|
curl -sSL https://raw.githubusercontent.com/YOUR-REPO/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR-USERNAME/attack-defence-infr-control.git
|
||||||
|
cd attack-defence-infr-control
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure environment**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the infrastructure**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Access the dashboard**
|
||||||
|
Open http://localhost:8000 in your browser (default password: `admin123`)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Required Environment Variables
|
||||||
|
|
||||||
|
Edit `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
POSTGRES_PASSWORD=your_secure_password
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
SECRET_TOKEN=your_random_secret_token
|
||||||
|
WEB_PASSWORD=your_web_password
|
||||||
|
|
||||||
|
# Telegram
|
||||||
|
TELEGRAM_BOT_TOKEN=your_bot_token
|
||||||
|
TELEGRAM_CHAT_ID=your_chat_id
|
||||||
|
|
||||||
|
# Game Settings
|
||||||
|
OUR_TEAM_ID=1
|
||||||
|
SCOREBOARD_WS_URL=ws://scoreboard:8080/api/events
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Telegram Credentials
|
||||||
|
|
||||||
|
1. Create a bot with [@BotFather](https://t.me/botfather)
|
||||||
|
2. Get your chat ID from [@userinfobot](https://t.me/userinfobot)
|
||||||
|
3. Add bot to your group and make it admin
|
||||||
|
|
||||||
|
## Service Setup
|
||||||
|
|
||||||
|
After starting the infrastructure, setup your A/D services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd setuper
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will guide you through setting up:
|
||||||
|
- **Packmate**: Traffic analysis (https://gitlab.com/packmate/Packmate)
|
||||||
|
- **moded_distructive_farm**: Attack/Defense farm (https://github.com/ilyastar9999/moded_distructive_farm)
|
||||||
|
- **Firegex**: Flag regex checker (https://github.com/Pwnzer0tt1/firegex)
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
### Controller API (Port 8001)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List services
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8001/services
|
||||||
|
|
||||||
|
# Start a service
|
||||||
|
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"action": "start"}' \
|
||||||
|
http://localhost:8001/services/1/action
|
||||||
|
|
||||||
|
# Get service logs
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
http://localhost:8001/services/1/logs?lines=100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scoreboard Injector API (Port 8002)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get attack statistics
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8002/stats
|
||||||
|
|
||||||
|
# Get recent attacks
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
http://localhost:8002/attacks?limit=50&attacks_to_us=true
|
||||||
|
|
||||||
|
# Get alerts
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8002/alerts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Telegram Bot API (Port 8003)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send message
|
||||||
|
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"message": "Test alert"}' \
|
||||||
|
http://localhost:8003/send
|
||||||
|
|
||||||
|
# Get message history
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8003/messages
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ Web Dashboard :8000 │
|
||||||
|
│ (Flask + Bootstrap + jQuery) │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────┼────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ Controller │ │ Scoreboard │ │ Telegram │
|
||||||
|
│ API :8001 │ │ Injector │ │ Bot :8003 │
|
||||||
|
│ │ │ :8002 │ │ │
|
||||||
|
└──────────────┘ └──────────────┘ └──────────────┘
|
||||||
|
│ │ │
|
||||||
|
└────────────────┼────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────┐
|
||||||
|
│ PostgreSQL │
|
||||||
|
│ Database │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── controler/ # Service controller API
|
||||||
|
│ ├── main.py
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── scoreboard_injector/ # Attack monitor
|
||||||
|
│ ├── main.py
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── tg-bot/ # Telegram notifications
|
||||||
|
│ ├── main.py
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── web/ # Web dashboard
|
||||||
|
│ ├── app.py
|
||||||
|
│ ├── templates/
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── setuper/ # Service setup scripts
|
||||||
|
│ ├── setup.sh
|
||||||
|
│ └── README.md
|
||||||
|
├── services/ # Managed services directory
|
||||||
|
├── docker-compose.yaml # Main compose file
|
||||||
|
├── init-db.sql # Database schema
|
||||||
|
└── .env.example # Configuration template
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
The system uses a single PostgreSQL instance with tables for:
|
||||||
|
- `services` - Registered services
|
||||||
|
- `service_logs` - Action history
|
||||||
|
- `attacks` - Attack events
|
||||||
|
- `attack_alerts` - Generated alerts
|
||||||
|
- `telegram_messages` - Message log
|
||||||
|
- `settings` - System configuration
|
||||||
|
|
||||||
|
## Management Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View all logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# View specific service logs
|
||||||
|
docker-compose logs -f web
|
||||||
|
docker-compose logs -f controller
|
||||||
|
|
||||||
|
# Restart all services
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Stop all services
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Stop and remove volumes
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Rebuild after code changes
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Services won't start
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Verify .env configuration
|
||||||
|
cat .env
|
||||||
|
|
||||||
|
# Ensure ports are available
|
||||||
|
netstat -tulpn | grep -E '8000|8001|8002|8003'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database connection errors
|
||||||
|
```bash
|
||||||
|
# Check PostgreSQL is running
|
||||||
|
docker-compose ps postgres
|
||||||
|
|
||||||
|
# Verify database credentials in .env
|
||||||
|
# Restart PostgreSQL
|
||||||
|
docker-compose restart postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket connection to scoreboard fails
|
||||||
|
- Verify `SCOREBOARD_WS_URL` in `.env`
|
||||||
|
- Check scoreboard is accessible
|
||||||
|
- Ensure firewall allows WebSocket connections
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Change default passwords** in `.env`
|
||||||
|
2. **Use strong random tokens** for `SECRET_TOKEN`
|
||||||
|
3. **Restrict network access** to API ports in production
|
||||||
|
4. **Enable HTTPS** for web dashboard in production
|
||||||
|
5. **Regularly update** Docker images
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions welcome! Please:
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see LICENSE file for details
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
- Open an issue on GitHub
|
||||||
|
- Check existing documentation
|
||||||
|
- Review logs: `docker-compose logs -f`
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Built for Attack/Defense CTF competitions. Integrates with:
|
||||||
|
- [ForcAD](https://github.com/pomo-mondreganto/ForcAD) - CTF platform
|
||||||
|
- [Packmate](https://gitlab.com/packmate/Packmate) - Traffic analysis
|
||||||
|
- [moded_distructive_farm](https://github.com/ilyastar9999/moded_distructive_farm) - Attack farm
|
||||||
|
- [Firegex](https://github.com/Pwnzer0tt1/firegex) - Flag checker
|
||||||
16
controler/Dockerfile
Normal file
16
controler/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Install docker-compose and git
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
docker-compose \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY main.py .
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
314
controler/main.py
Normal file
314
controler/main.py
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
"""
|
||||||
|
API Controller for A/D Infrastructure
|
||||||
|
Manages docker-compose services with authentication
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, List
|
||||||
|
from fastapi import FastAPI, HTTPException, Depends, Header
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import asyncpg
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://adctrl:adctrl@postgres:5432/adctrl")
|
||||||
|
SECRET_TOKEN = os.getenv("SECRET_TOKEN", "change-me-in-production")
|
||||||
|
SERVICES_DIR = os.getenv("SERVICES_DIR", "/services")
|
||||||
|
|
||||||
|
# Database pool
|
||||||
|
db_pool = None
|
||||||
|
|
||||||
|
class ServiceCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
path: str
|
||||||
|
git_url: Optional[str] = None
|
||||||
|
|
||||||
|
class ServiceAction(BaseModel):
|
||||||
|
action: str # start, stop, restart
|
||||||
|
|
||||||
|
class GitPullRequest(BaseModel):
|
||||||
|
auto: bool = False
|
||||||
|
|
||||||
|
class LogRequest(BaseModel):
|
||||||
|
lines: int = 100
|
||||||
|
|
||||||
|
# Auth dependency
|
||||||
|
async def verify_token(authorization: str = Header(None)):
|
||||||
|
if not authorization or not authorization.startswith("Bearer "):
|
||||||
|
raise HTTPException(status_code=401, detail="Missing or invalid authorization header")
|
||||||
|
|
||||||
|
token = authorization.replace("Bearer ", "")
|
||||||
|
if token != SECRET_TOKEN:
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid token")
|
||||||
|
return token
|
||||||
|
|
||||||
|
# Database functions
|
||||||
|
async def get_db():
|
||||||
|
return await db_pool.acquire()
|
||||||
|
|
||||||
|
async def release_db(conn):
|
||||||
|
await db_pool.release(conn)
|
||||||
|
|
||||||
|
async def log_service_action(conn, service_id: int, action: str, status: str, message: str = None):
|
||||||
|
await conn.execute(
|
||||||
|
"INSERT INTO service_logs (service_id, action, status, message) VALUES ($1, $2, $3, $4)",
|
||||||
|
service_id, action, status, message
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lifespan context
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
global db_pool
|
||||||
|
db_pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10)
|
||||||
|
yield
|
||||||
|
await db_pool.close()
|
||||||
|
|
||||||
|
app = FastAPI(title="A/D Infrastructure Controller", lifespan=lifespan)
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
async def run_docker_compose_command(service_path: str, command: List[str]) -> tuple[int, str, str]:
|
||||||
|
"""Run docker-compose command and return (returncode, stdout, stderr)"""
|
||||||
|
full_command = ["docker-compose", "-f", os.path.join(service_path, "docker-compose.yml")] + command
|
||||||
|
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
*full_command,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
cwd=service_path
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
return process.returncode, stdout.decode(), stderr.decode()
|
||||||
|
|
||||||
|
async def run_git_command(service_path: str, command: List[str]) -> tuple[int, str, str]:
|
||||||
|
"""Run git command and return (returncode, stdout, stderr)"""
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
"git", *command,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
cwd=service_path
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
return process.returncode, stdout.decode(), stderr.decode()
|
||||||
|
|
||||||
|
# API Endpoints
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}
|
||||||
|
|
||||||
|
@app.post("/services", dependencies=[Depends(verify_token)])
|
||||||
|
async def create_service(service: ServiceCreate):
|
||||||
|
"""Register a new service"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
# Check if service directory exists
|
||||||
|
service_path = os.path.join(SERVICES_DIR, service.path)
|
||||||
|
if not os.path.exists(service_path):
|
||||||
|
raise HTTPException(status_code=404, detail=f"Service path not found: {service_path}")
|
||||||
|
|
||||||
|
# Check if docker-compose.yml exists
|
||||||
|
compose_file = os.path.join(service_path, "docker-compose.yml")
|
||||||
|
if not os.path.exists(compose_file):
|
||||||
|
raise HTTPException(status_code=404, detail=f"docker-compose.yml not found in {service_path}")
|
||||||
|
|
||||||
|
service_id = await conn.fetchval(
|
||||||
|
"INSERT INTO services (name, path, git_url, status) VALUES ($1, $2, $3, $4) RETURNING id",
|
||||||
|
service.name, service_path, service.git_url, "stopped"
|
||||||
|
)
|
||||||
|
|
||||||
|
await log_service_action(conn, service_id, "register", "success", "Service registered")
|
||||||
|
|
||||||
|
return {"id": service_id, "name": service.name, "status": "registered"}
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.get("/services", dependencies=[Depends(verify_token)])
|
||||||
|
async def list_services():
|
||||||
|
"""List all registered services"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
rows = await conn.fetch("SELECT * FROM services ORDER BY name")
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.get("/services/{service_id}", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_service(service_id: int):
|
||||||
|
"""Get service details"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
service = await conn.fetchrow("SELECT * FROM services WHERE id = $1", service_id)
|
||||||
|
if not service:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
return dict(service)
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.post("/services/{service_id}/action", dependencies=[Depends(verify_token)])
|
||||||
|
async def service_action(service_id: int, action: ServiceAction):
|
||||||
|
"""Start, stop, or restart a service"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
service = await conn.fetchrow("SELECT * FROM services WHERE id = $1", service_id)
|
||||||
|
if not service:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
|
||||||
|
service_path = service['path']
|
||||||
|
|
||||||
|
# Map actions to docker-compose commands
|
||||||
|
command_map = {
|
||||||
|
"start": ["up", "-d"],
|
||||||
|
"stop": ["down"],
|
||||||
|
"restart": ["restart"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if action.action not in command_map:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid action: {action.action}")
|
||||||
|
|
||||||
|
returncode, stdout, stderr = await run_docker_compose_command(
|
||||||
|
service_path, command_map[action.action]
|
||||||
|
)
|
||||||
|
|
||||||
|
if returncode == 0:
|
||||||
|
new_status = "running" if action.action in ["start", "restart"] else "stopped"
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE services SET status = $1, last_updated = $2 WHERE id = $3",
|
||||||
|
new_status, datetime.utcnow(), service_id
|
||||||
|
)
|
||||||
|
await log_service_action(conn, service_id, action.action, "success", stdout)
|
||||||
|
return {"status": "success", "action": action.action, "output": stdout}
|
||||||
|
else:
|
||||||
|
await log_service_action(conn, service_id, action.action, "failed", stderr)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Command failed: {stderr}")
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.post("/services/{service_id}/pull", dependencies=[Depends(verify_token)])
|
||||||
|
async def git_pull(service_id: int, request: GitPullRequest):
|
||||||
|
"""Pull latest changes from git repository"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
service = await conn.fetchrow("SELECT * FROM services WHERE id = $1", service_id)
|
||||||
|
if not service:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
|
||||||
|
if not service['git_url']:
|
||||||
|
raise HTTPException(status_code=400, detail="Service has no git URL configured")
|
||||||
|
|
||||||
|
service_path = service['path']
|
||||||
|
|
||||||
|
# Check if it's a git repository
|
||||||
|
if not os.path.exists(os.path.join(service_path, ".git")):
|
||||||
|
raise HTTPException(status_code=400, detail="Not a git repository")
|
||||||
|
|
||||||
|
# Pull changes
|
||||||
|
returncode, stdout, stderr = await run_git_command(service_path, ["pull"])
|
||||||
|
|
||||||
|
if returncode == 0:
|
||||||
|
await log_service_action(conn, service_id, "git_pull", "success", stdout)
|
||||||
|
|
||||||
|
# Auto-restart if requested
|
||||||
|
if request.auto:
|
||||||
|
restart_returncode, restart_stdout, restart_stderr = await run_docker_compose_command(
|
||||||
|
service_path, ["restart"]
|
||||||
|
)
|
||||||
|
if restart_returncode == 0:
|
||||||
|
await log_service_action(conn, service_id, "auto_restart", "success", restart_stdout)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"pull_output": stdout,
|
||||||
|
"restart_output": restart_stdout
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
await log_service_action(conn, service_id, "auto_restart", "failed", restart_stderr)
|
||||||
|
return {
|
||||||
|
"status": "partial_success",
|
||||||
|
"pull_output": stdout,
|
||||||
|
"restart_error": restart_stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"status": "success", "output": stdout}
|
||||||
|
else:
|
||||||
|
await log_service_action(conn, service_id, "git_pull", "failed", stderr)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Git pull failed: {stderr}")
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.get("/services/{service_id}/logs", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_logs(service_id: int, lines: int = 100):
|
||||||
|
"""Get service logs from docker-compose"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
service = await conn.fetchrow("SELECT * FROM services WHERE id = $1", service_id)
|
||||||
|
if not service:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
|
||||||
|
service_path = service['path']
|
||||||
|
|
||||||
|
returncode, stdout, stderr = await run_docker_compose_command(
|
||||||
|
service_path, ["logs", "--tail", str(lines)]
|
||||||
|
)
|
||||||
|
|
||||||
|
if returncode == 0:
|
||||||
|
return {"logs": stdout}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get logs: {stderr}")
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.get("/services/{service_id}/status", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_service_status(service_id: int):
|
||||||
|
"""Get real-time service status from docker-compose"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
service = await conn.fetchrow("SELECT * FROM services WHERE id = $1", service_id)
|
||||||
|
if not service:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
|
||||||
|
service_path = service['path']
|
||||||
|
|
||||||
|
returncode, stdout, stderr = await run_docker_compose_command(
|
||||||
|
service_path, ["ps"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if returncode == 0:
|
||||||
|
return {"status": stdout, "db_status": service['status']}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get status: {stderr}")
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.get("/services/{service_id}/action-logs", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_action_logs(service_id: int, limit: int = 50):
|
||||||
|
"""Get service action history"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
logs = await conn.fetch(
|
||||||
|
"SELECT * FROM service_logs WHERE service_id = $1 ORDER BY created_at DESC LIMIT $2",
|
||||||
|
service_id, limit
|
||||||
|
)
|
||||||
|
return [dict(log) for log in logs]
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.delete("/services/{service_id}", dependencies=[Depends(verify_token)])
|
||||||
|
async def delete_service(service_id: int):
|
||||||
|
"""Unregister a service"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
service = await conn.fetchrow("SELECT * FROM services WHERE id = $1", service_id)
|
||||||
|
if not service:
|
||||||
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
|
|
||||||
|
await conn.execute("DELETE FROM services WHERE id = $1", service_id)
|
||||||
|
return {"status": "deleted", "service_id": service_id}
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||||
5
controler/requirements.txt
Normal file
5
controler/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn[standard]==0.27.0
|
||||||
|
asyncpg==0.29.0
|
||||||
|
pydantic==2.5.3
|
||||||
|
python-dotenv==1.0.0
|
||||||
112
docker-compose.yaml
Normal file
112
docker-compose.yaml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Shared PostgreSQL database for controller and scoreboard
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: adctrl-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-adctrl}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-adctrl_secure_password}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-adctrl}
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-adctrl}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- adctrl-network
|
||||||
|
|
||||||
|
# API Controller - manages docker services
|
||||||
|
controller:
|
||||||
|
build: ./controler
|
||||||
|
container_name: adctrl-controller
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-adctrl}:${POSTGRES_PASSWORD:-adctrl_secure_password}@postgres:5432/${POSTGRES_DB:-adctrl}
|
||||||
|
SECRET_TOKEN: ${SECRET_TOKEN}
|
||||||
|
SERVICES_DIR: /services
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ${SERVICES_DIR:-./services}:/services
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "8001:8001"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- adctrl-network
|
||||||
|
|
||||||
|
# Scoreboard Injector - monitors attacks
|
||||||
|
scoreboard-injector:
|
||||||
|
build: ./scoreboard_injector
|
||||||
|
container_name: adctrl-scoreboard
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-adctrl}:${POSTGRES_PASSWORD:-adctrl_secure_password}@postgres:5432/${POSTGRES_DB:-adctrl}
|
||||||
|
SECRET_TOKEN: ${SECRET_TOKEN}
|
||||||
|
SCOREBOARD_WS_URL: ${SCOREBOARD_WS_URL:-ws://10.60.0.1:8080/api/events}
|
||||||
|
OUR_TEAM_ID: ${OUR_TEAM_ID:-1}
|
||||||
|
ALERT_THRESHOLD_POINTS: ${ALERT_THRESHOLD_POINTS:-100}
|
||||||
|
ALERT_THRESHOLD_TIME: ${ALERT_THRESHOLD_TIME:-300}
|
||||||
|
TELEGRAM_API_URL: http://tg-bot:8003/send
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
tg-bot:
|
||||||
|
condition: service_started
|
||||||
|
ports:
|
||||||
|
- "8002:8002"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- adctrl-network
|
||||||
|
|
||||||
|
# Telegram Bot - sends notifications
|
||||||
|
tg-bot:
|
||||||
|
build: ./tg-bot
|
||||||
|
container_name: adctrl-telegram
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-adctrl}:${POSTGRES_PASSWORD:-adctrl_secure_password}@postgres:5432/${POSTGRES_DB:-adctrl}
|
||||||
|
SECRET_TOKEN: ${SECRET_TOKEN}
|
||||||
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
|
||||||
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "8003:8003"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- adctrl-network
|
||||||
|
|
||||||
|
# Web Dashboard
|
||||||
|
web:
|
||||||
|
build: ./web
|
||||||
|
container_name: adctrl-web
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-adctrl}:${POSTGRES_PASSWORD:-adctrl_secure_password}@postgres:5432/${POSTGRES_DB:-adctrl}
|
||||||
|
SECRET_TOKEN: ${SECRET_TOKEN}
|
||||||
|
WEB_PASSWORD: ${WEB_PASSWORD:-admin123}
|
||||||
|
FLASK_SECRET_KEY: ${FLASK_SECRET_KEY:-change-me-flask-secret}
|
||||||
|
CONTROLLER_API: http://controller:8001
|
||||||
|
SCOREBOARD_API: http://scoreboard-injector:8002
|
||||||
|
TELEGRAM_API: http://tg-bot:8003
|
||||||
|
depends_on:
|
||||||
|
- controller
|
||||||
|
- scoreboard-injector
|
||||||
|
- tg-bot
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- adctrl-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
adctrl-network:
|
||||||
|
driver: bridge
|
||||||
79
init-db.sql
Normal file
79
init-db.sql
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
-- Database initialization script for A/D Infrastructure Control
|
||||||
|
|
||||||
|
-- Services table for controller
|
||||||
|
CREATE TABLE IF NOT EXISTS services (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
path VARCHAR(512) NOT NULL,
|
||||||
|
git_url VARCHAR(512),
|
||||||
|
status VARCHAR(50) DEFAULT 'stopped',
|
||||||
|
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Service logs table
|
||||||
|
CREATE TABLE IF NOT EXISTS service_logs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
service_id INTEGER REFERENCES services(id) ON DELETE CASCADE,
|
||||||
|
action VARCHAR(100) NOT NULL,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
message TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Attacks tracking table for scoreboard injector
|
||||||
|
CREATE TABLE IF NOT EXISTS attacks (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
attack_id VARCHAR(255) UNIQUE,
|
||||||
|
attacker_team_id INTEGER,
|
||||||
|
victim_team_id INTEGER,
|
||||||
|
service_name VARCHAR(255),
|
||||||
|
flag VARCHAR(255),
|
||||||
|
timestamp TIMESTAMP NOT NULL,
|
||||||
|
points FLOAT,
|
||||||
|
is_our_attack BOOLEAN DEFAULT FALSE,
|
||||||
|
is_attack_to_us BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Attack alerts table
|
||||||
|
CREATE TABLE IF NOT EXISTS attack_alerts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
attack_id INTEGER REFERENCES attacks(id) ON DELETE CASCADE,
|
||||||
|
alert_type VARCHAR(100) NOT NULL,
|
||||||
|
severity VARCHAR(50) NOT NULL,
|
||||||
|
message TEXT,
|
||||||
|
notified BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Telegram messages log
|
||||||
|
CREATE TABLE IF NOT EXISTS telegram_messages (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
chat_id BIGINT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
success BOOLEAN DEFAULT TRUE,
|
||||||
|
error_message TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- System settings
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key VARCHAR(255) PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert default settings
|
||||||
|
INSERT INTO settings (key, value) VALUES
|
||||||
|
('our_team_id', '0'),
|
||||||
|
('alert_threshold_points', '100'),
|
||||||
|
('alert_threshold_time', '300')
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
-- Create indexes for performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_attacks_timestamp ON attacks(timestamp);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_attacks_our_attack ON attacks(is_our_attack);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_attacks_attack_to_us ON attacks(is_attack_to_us);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_service_logs_service_id ON service_logs(service_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_attack_alerts_notified ON attack_alerts(notified);
|
||||||
111
install.sh
Normal file
111
install.sh
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# One-liner installation script for A/D Infrastructure Control
|
||||||
|
# Usage: curl -sSL https://raw.githubusercontent.com/YOUR-REPO/main/install.sh | bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REPO_URL="https://github.com/YOUR-USERNAME/attack-defence-infr-control.git"
|
||||||
|
INSTALL_DIR="$HOME/ad-infr-control"
|
||||||
|
|
||||||
|
echo "==================================="
|
||||||
|
echo "A/D Infrastructure Control Installer"
|
||||||
|
echo "==================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check requirements
|
||||||
|
echo "Checking requirements..."
|
||||||
|
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "Error: Docker is not installed"
|
||||||
|
echo "Please install Docker first: https://docs.docker.com/get-docker/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker-compose &> /dev/null; then
|
||||||
|
echo "Error: docker-compose is not installed"
|
||||||
|
echo "Please install docker-compose: https://docs.docker.com/compose/install/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v git &> /dev/null; then
|
||||||
|
echo "Error: git is not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ All requirements satisfied"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clone or update repository
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
echo "Installation directory exists, updating..."
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
git pull
|
||||||
|
else
|
||||||
|
echo "Cloning repository..."
|
||||||
|
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create .env file if it doesn't exist
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
echo "Creating .env file from template..."
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Generate random secret token
|
||||||
|
SECRET_TOKEN=$(openssl rand -hex 32)
|
||||||
|
FLASK_SECRET=$(openssl rand -hex 32)
|
||||||
|
POSTGRES_PASS=$(openssl rand -hex 16)
|
||||||
|
|
||||||
|
# Update .env with generated secrets
|
||||||
|
sed -i.bak "s/change_me_to_random_string/$SECRET_TOKEN/" .env
|
||||||
|
sed -i.bak "s/change_me_flask_secret_key/$FLASK_SECRET/" .env
|
||||||
|
sed -i.bak "s/change_me_secure_password/$POSTGRES_PASS/" .env
|
||||||
|
|
||||||
|
rm .env.bak 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "✓ .env file created with random secrets"
|
||||||
|
echo ""
|
||||||
|
echo "IMPORTANT: Please edit .env file and configure:"
|
||||||
|
echo " - TELEGRAM_BOT_TOKEN"
|
||||||
|
echo " - TELEGRAM_CHAT_ID"
|
||||||
|
echo " - OUR_TEAM_ID"
|
||||||
|
echo " - SCOREBOARD_WS_URL"
|
||||||
|
echo ""
|
||||||
|
read -p "Press Enter to continue after editing .env (or Ctrl+C to exit)..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create services directory
|
||||||
|
mkdir -p services
|
||||||
|
|
||||||
|
# Build and start services
|
||||||
|
echo ""
|
||||||
|
echo "Building Docker images..."
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Starting services..."
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==================================="
|
||||||
|
echo "Installation Complete!"
|
||||||
|
echo "==================================="
|
||||||
|
echo ""
|
||||||
|
echo "Services are running:"
|
||||||
|
echo " - Web Dashboard: http://localhost:8000"
|
||||||
|
echo " - Controller API: http://localhost:8001"
|
||||||
|
echo " - Scoreboard Injector: http://localhost:8002"
|
||||||
|
echo " - Telegram Bot API: http://localhost:8003"
|
||||||
|
echo ""
|
||||||
|
echo "Default web password: admin123 (change in .env: WEB_PASSWORD)"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Access web dashboard at http://localhost:8000"
|
||||||
|
echo " 2. Run setup script: cd $INSTALL_DIR && ./setuper/setup.sh"
|
||||||
|
echo " 3. Configure your A/D services (Packmate, Farm, Firegex)"
|
||||||
|
echo ""
|
||||||
|
echo "View logs: docker-compose logs -f"
|
||||||
|
echo "Stop services: docker-compose down"
|
||||||
|
echo ""
|
||||||
10
scoreboard_injector/Dockerfile
Normal file
10
scoreboard_injector/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY main.py .
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
316
scoreboard_injector/main.py
Normal file
316
scoreboard_injector/main.py
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
"""
|
||||||
|
Scoreboard Injector for ForcAD
|
||||||
|
Monitors WebSocket for attacks and alerts on critical situations
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import aiohttp
|
||||||
|
from fastapi import FastAPI, HTTPException, Depends, Header
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import asyncpg
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://adctrl:adctrl@postgres:5432/adctrl")
|
||||||
|
SECRET_TOKEN = os.getenv("SECRET_TOKEN", "change-me-in-production")
|
||||||
|
SCOREBOARD_WS_URL = os.getenv("SCOREBOARD_WS_URL", "ws://scoreboard:8080/api/events")
|
||||||
|
OUR_TEAM_ID = int(os.getenv("OUR_TEAM_ID", "1"))
|
||||||
|
ALERT_THRESHOLD_POINTS = float(os.getenv("ALERT_THRESHOLD_POINTS", "100"))
|
||||||
|
ALERT_THRESHOLD_TIME = int(os.getenv("ALERT_THRESHOLD_TIME", "300")) # seconds
|
||||||
|
TELEGRAM_API_URL = os.getenv("TELEGRAM_API_URL", "http://tg-bot:8003/send")
|
||||||
|
|
||||||
|
# Database pool
|
||||||
|
db_pool = None
|
||||||
|
ws_task = None
|
||||||
|
|
||||||
|
class AttackStats(BaseModel):
|
||||||
|
total_attacks: int
|
||||||
|
attacks_by_us: int
|
||||||
|
attacks_to_us: int
|
||||||
|
recent_attacks: int
|
||||||
|
critical_alerts: int
|
||||||
|
|
||||||
|
# Auth dependency
|
||||||
|
async def verify_token(authorization: str = Header(None)):
|
||||||
|
if not authorization or not authorization.startswith("Bearer "):
|
||||||
|
raise HTTPException(status_code=401, detail="Missing or invalid authorization header")
|
||||||
|
|
||||||
|
token = authorization.replace("Bearer ", "")
|
||||||
|
if token != SECRET_TOKEN:
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid token")
|
||||||
|
return token
|
||||||
|
|
||||||
|
# Database functions
|
||||||
|
async def get_db():
|
||||||
|
return await db_pool.acquire()
|
||||||
|
|
||||||
|
async def release_db(conn):
|
||||||
|
await db_pool.release(conn)
|
||||||
|
|
||||||
|
async def process_attack_event(event: Dict[str, Any]):
|
||||||
|
"""Process attack event from scoreboard"""
|
||||||
|
conn = await db_pool.acquire()
|
||||||
|
try:
|
||||||
|
# Extract attack information from event
|
||||||
|
# Adjust fields based on actual ForcAD event structure
|
||||||
|
attack_id = event.get('id') or f"{event.get('round')}_{event.get('attacker_id')}_{event.get('victim_id')}_{event.get('service')}"
|
||||||
|
attacker_id = event.get('attacker_id') or event.get('team_id')
|
||||||
|
victim_id = event.get('victim_id') or event.get('target_id')
|
||||||
|
service_name = event.get('service') or event.get('service_name')
|
||||||
|
flag = event.get('flag', '')
|
||||||
|
timestamp = datetime.fromisoformat(event.get('time', datetime.utcnow().isoformat()))
|
||||||
|
points = float(event.get('points', 0))
|
||||||
|
|
||||||
|
is_our_attack = attacker_id == OUR_TEAM_ID
|
||||||
|
is_attack_to_us = victim_id == OUR_TEAM_ID
|
||||||
|
|
||||||
|
# Store attack in database
|
||||||
|
await conn.execute("""
|
||||||
|
INSERT INTO attacks (attack_id, attacker_team_id, victim_team_id, service_name, flag, timestamp, points, is_our_attack, is_attack_to_us)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
ON CONFLICT (attack_id) DO NOTHING
|
||||||
|
""", attack_id, attacker_id, victim_id, service_name, flag, timestamp, points, is_our_attack, is_attack_to_us)
|
||||||
|
|
||||||
|
# Check for alert conditions if attack is against us
|
||||||
|
if is_attack_to_us:
|
||||||
|
await check_and_create_alerts(conn, attacker_id, service_name)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await db_pool.release(conn)
|
||||||
|
|
||||||
|
async def check_and_create_alerts(conn, attacker_id: int, service_name: str):
|
||||||
|
"""Check if we should create an alert for attacks against us"""
|
||||||
|
threshold_time = datetime.utcnow() - timedelta(seconds=ALERT_THRESHOLD_TIME)
|
||||||
|
|
||||||
|
# Check total points lost from this attacker in threshold time
|
||||||
|
result = await conn.fetchrow("""
|
||||||
|
SELECT COUNT(*) as attack_count, COALESCE(SUM(points), 0) as total_points
|
||||||
|
FROM attacks
|
||||||
|
WHERE is_attack_to_us = true
|
||||||
|
AND attacker_team_id = $1
|
||||||
|
AND service_name = $2
|
||||||
|
AND timestamp > $3
|
||||||
|
""", attacker_id, service_name, threshold_time)
|
||||||
|
|
||||||
|
if result and result['total_points'] >= ALERT_THRESHOLD_POINTS:
|
||||||
|
# Create alert
|
||||||
|
alert_message = f"CRITICAL: Team {attacker_id} has stolen {result['total_points']:.2f} points from service {service_name} in the last {ALERT_THRESHOLD_TIME}s ({result['attack_count']} attacks)"
|
||||||
|
|
||||||
|
# Check if we already alerted recently
|
||||||
|
recent_alert = await conn.fetchrow("""
|
||||||
|
SELECT id FROM attack_alerts
|
||||||
|
WHERE alert_type = 'high_point_loss'
|
||||||
|
AND message LIKE $1
|
||||||
|
AND created_at > $2
|
||||||
|
""", f"%Team {attacker_id}%{service_name}%", threshold_time)
|
||||||
|
|
||||||
|
if not recent_alert:
|
||||||
|
alert_id = await conn.fetchval("""
|
||||||
|
INSERT INTO attack_alerts (attack_id, alert_type, severity, message)
|
||||||
|
VALUES (
|
||||||
|
(SELECT id FROM attacks WHERE attacker_team_id = $1 AND service_name = $2 ORDER BY timestamp DESC LIMIT 1),
|
||||||
|
'high_point_loss',
|
||||||
|
'critical',
|
||||||
|
$3
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
""", attacker_id, service_name, alert_message)
|
||||||
|
|
||||||
|
# Send to telegram
|
||||||
|
await send_telegram_alert(alert_message)
|
||||||
|
|
||||||
|
# Mark as notified
|
||||||
|
await conn.execute("UPDATE attack_alerts SET notified = true WHERE id = $1", alert_id)
|
||||||
|
|
||||||
|
async def send_telegram_alert(message: str):
|
||||||
|
"""Send alert to telegram bot"""
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
TELEGRAM_API_URL,
|
||||||
|
json={"message": message},
|
||||||
|
headers={"Authorization": f"Bearer {SECRET_TOKEN}"}
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
print(f"Failed to send telegram alert: {await resp.text()}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error sending telegram alert: {e}")
|
||||||
|
|
||||||
|
async def websocket_listener():
|
||||||
|
"""Listen to scoreboard WebSocket for events"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.ws_connect(SCOREBOARD_WS_URL) as ws:
|
||||||
|
print(f"Connected to scoreboard WebSocket: {SCOREBOARD_WS_URL}")
|
||||||
|
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
|
try:
|
||||||
|
event = json.loads(msg.data)
|
||||||
|
# Process different event types
|
||||||
|
if event.get('type') in ['attack', 'flag_stolen', 'service_status']:
|
||||||
|
await process_attack_event(event)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"Failed to decode WebSocket message: {msg.data}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing event: {e}")
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
print(f"WebSocket error: {ws.exception()}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WebSocket connection error: {e}")
|
||||||
|
print("Reconnecting in 5 seconds...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
# Lifespan context
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
global db_pool, ws_task
|
||||||
|
db_pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10)
|
||||||
|
|
||||||
|
# Start WebSocket listener
|
||||||
|
ws_task = asyncio.create_task(websocket_listener())
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
if ws_task:
|
||||||
|
ws_task.cancel()
|
||||||
|
try:
|
||||||
|
await ws_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await db_pool.close()
|
||||||
|
|
||||||
|
app = FastAPI(title="Scoreboard Injector", lifespan=lifespan)
|
||||||
|
|
||||||
|
# API Endpoints
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}
|
||||||
|
|
||||||
|
@app.get("/stats", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_stats():
|
||||||
|
"""Get attack statistics"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
total = await conn.fetchval("SELECT COUNT(*) FROM attacks")
|
||||||
|
attacks_by_us = await conn.fetchval("SELECT COUNT(*) FROM attacks WHERE is_our_attack = true")
|
||||||
|
attacks_to_us = await conn.fetchval("SELECT COUNT(*) FROM attacks WHERE is_attack_to_us = true")
|
||||||
|
|
||||||
|
threshold_time = datetime.utcnow() - timedelta(minutes=5)
|
||||||
|
recent = await conn.fetchval("SELECT COUNT(*) FROM attacks WHERE timestamp > $1", threshold_time)
|
||||||
|
|
||||||
|
critical_alerts = await conn.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM attack_alerts WHERE severity = 'critical' AND created_at > $1",
|
||||||
|
threshold_time
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_attacks": total,
|
||||||
|
"attacks_by_us": attacks_by_us,
|
||||||
|
"attacks_to_us": attacks_to_us,
|
||||||
|
"recent_attacks_5min": recent,
|
||||||
|
"critical_alerts_5min": critical_alerts
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.get("/attacks", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_attacks(limit: int = 100, our_attacks: Optional[bool] = None, attacks_to_us: Optional[bool] = None):
|
||||||
|
"""Get recent attacks"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
query = "SELECT * FROM attacks WHERE 1=1"
|
||||||
|
params = []
|
||||||
|
param_count = 0
|
||||||
|
|
||||||
|
if our_attacks is not None:
|
||||||
|
param_count += 1
|
||||||
|
query += f" AND is_our_attack = ${param_count}"
|
||||||
|
params.append(our_attacks)
|
||||||
|
|
||||||
|
if attacks_to_us is not None:
|
||||||
|
param_count += 1
|
||||||
|
query += f" AND is_attack_to_us = ${param_count}"
|
||||||
|
params.append(attacks_to_us)
|
||||||
|
|
||||||
|
param_count += 1
|
||||||
|
query += f" ORDER BY timestamp DESC LIMIT ${param_count}"
|
||||||
|
params.append(limit)
|
||||||
|
|
||||||
|
rows = await conn.fetch(query, *params)
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.get("/alerts", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_alerts(limit: int = 50, unnotified: bool = False):
|
||||||
|
"""Get alerts"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
if unnotified:
|
||||||
|
query = "SELECT * FROM attack_alerts WHERE notified = false ORDER BY created_at DESC LIMIT $1"
|
||||||
|
else:
|
||||||
|
query = "SELECT * FROM attack_alerts ORDER BY created_at DESC LIMIT $1"
|
||||||
|
|
||||||
|
rows = await conn.fetch(query, limit)
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.post("/alerts/{alert_id}/acknowledge", dependencies=[Depends(verify_token)])
|
||||||
|
async def acknowledge_alert(alert_id: int):
|
||||||
|
"""Mark alert as acknowledged"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
await conn.execute("UPDATE attack_alerts SET notified = true WHERE id = $1", alert_id)
|
||||||
|
return {"status": "acknowledged", "alert_id": alert_id}
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.get("/attacks/by-service", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_attacks_by_service():
|
||||||
|
"""Get attack statistics grouped by service"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
rows = await conn.fetch("""
|
||||||
|
SELECT
|
||||||
|
service_name,
|
||||||
|
COUNT(*) as total_attacks,
|
||||||
|
COUNT(*) FILTER (WHERE is_our_attack = true) as our_attacks,
|
||||||
|
COUNT(*) FILTER (WHERE is_attack_to_us = true) as attacks_to_us,
|
||||||
|
COALESCE(SUM(points) FILTER (WHERE is_our_attack = true), 0) as points_gained,
|
||||||
|
COALESCE(SUM(points) FILTER (WHERE is_attack_to_us = true), 0) as points_lost
|
||||||
|
FROM attacks
|
||||||
|
GROUP BY service_name
|
||||||
|
ORDER BY total_attacks DESC
|
||||||
|
""")
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.post("/settings/team-id", dependencies=[Depends(verify_token)])
|
||||||
|
async def set_team_id(team_id: int):
|
||||||
|
"""Update our team ID"""
|
||||||
|
global OUR_TEAM_ID
|
||||||
|
OUR_TEAM_ID = team_id
|
||||||
|
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
await conn.execute(
|
||||||
|
"INSERT INTO settings (key, value) VALUES ('our_team_id', $1) ON CONFLICT (key) DO UPDATE SET value = $1",
|
||||||
|
str(team_id)
|
||||||
|
)
|
||||||
|
return {"team_id": team_id}
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||||
6
scoreboard_injector/requirements.txt
Normal file
6
scoreboard_injector/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn[standard]==0.27.0
|
||||||
|
asyncpg==0.29.0
|
||||||
|
pydantic==2.5.3
|
||||||
|
aiohttp==3.9.1
|
||||||
|
python-dotenv==1.0.0
|
||||||
54
setuper/README.md
Normal file
54
setuper/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# A/D Infrastructure Setuper
|
||||||
|
|
||||||
|
This script automates the installation and configuration of:
|
||||||
|
- **Packmate**: Traffic analysis tool
|
||||||
|
- **moded_distructive_farm**: Attack/defense farm
|
||||||
|
- **Firegex**: Flag submission tool
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Interactive Mode
|
||||||
|
```bash
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Environment Variables
|
||||||
|
```bash
|
||||||
|
export BOARD_URL="http://10.60.0.1"
|
||||||
|
export TEAM_TOKEN="your-team-token"
|
||||||
|
export NUM_TEAMS="10"
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Common
|
||||||
|
- `SERVICES_DIR`: Directory for services (default: ../services)
|
||||||
|
- `CONTROLLER_API`: Controller API URL (default: http://localhost:8001)
|
||||||
|
- `SECRET_TOKEN`: API authentication token
|
||||||
|
- `BOARD_URL`: Scoreboard URL
|
||||||
|
- `TEAM_TOKEN`: Your team token
|
||||||
|
|
||||||
|
### Packmate
|
||||||
|
- `PACKMATE_DB_PASSWORD`: Database password
|
||||||
|
- `NET_INTERFACE`: Network interface to monitor
|
||||||
|
- `PACKMATE_LOCAL_IP`: Local IP address
|
||||||
|
- `WEB_LOGIN`: Web interface login
|
||||||
|
- `WEB_PASSWORD`: Web interface password
|
||||||
|
|
||||||
|
### Farm
|
||||||
|
- `FARM_DB_PASS`: Database password
|
||||||
|
- `FARM_WEB_PASSWORD`: Web interface password
|
||||||
|
- `NUM_TEAMS`: Number of teams
|
||||||
|
- `IP_TEAM_BASE`: IP base for teams
|
||||||
|
- `FARM_API_TOKEN`: API token
|
||||||
|
|
||||||
|
### Firegex
|
||||||
|
- `FIREGEX_PORT`: Port for Firegex (default: 5000)
|
||||||
|
|
||||||
|
## Post-Setup
|
||||||
|
|
||||||
|
After running the setup script:
|
||||||
|
1. Review generated .env files in each service directory
|
||||||
|
2. Start services via controller API or web dashboard
|
||||||
|
3. Access web dashboards on configured ports
|
||||||
316
setuper/setup.sh
Normal file
316
setuper/setup.sh
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Setuper script for A/D Infrastructure
|
||||||
|
# Installs and configures: Packmate, moded_distructive_farm, Firegex
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SERVICES_DIR="${SERVICES_DIR:-$SCRIPT_DIR/../services}"
|
||||||
|
CONTROLLER_API="${CONTROLLER_API:-http://localhost:8001}"
|
||||||
|
SECRET_TOKEN="${SECRET_TOKEN:-change-me-in-production}"
|
||||||
|
|
||||||
|
echo "=== A/D Infrastructure Setuper ==="
|
||||||
|
echo "Services directory: $SERVICES_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create services directory
|
||||||
|
mkdir -p "$SERVICES_DIR"
|
||||||
|
|
||||||
|
# Function to call controller API
|
||||||
|
call_api() {
|
||||||
|
local endpoint="$1"
|
||||||
|
local method="${2:-GET}"
|
||||||
|
local data="${3:-}"
|
||||||
|
|
||||||
|
if [ "$method" = "POST" ]; then
|
||||||
|
curl -s -X POST "$CONTROLLER_API$endpoint" \
|
||||||
|
-H "Authorization: Bearer $SECRET_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$data"
|
||||||
|
else
|
||||||
|
curl -s "$CONTROLLER_API$endpoint" \
|
||||||
|
-H "Authorization: Bearer $SECRET_TOKEN"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to setup Packmate
|
||||||
|
setup_packmate() {
|
||||||
|
echo "=== Setting up Packmate ==="
|
||||||
|
|
||||||
|
local packmate_dir="$SERVICES_DIR/packmate"
|
||||||
|
|
||||||
|
if [ -d "$packmate_dir" ]; then
|
||||||
|
echo "Packmate directory already exists, updating..."
|
||||||
|
cd "$packmate_dir"
|
||||||
|
git pull
|
||||||
|
git submodule update --init --recursive
|
||||||
|
else
|
||||||
|
echo "Cloning Packmate with submodules..."
|
||||||
|
git clone --recursive https://gitlab.com/packmate/Packmate.git "$packmate_dir"
|
||||||
|
cd "$packmate_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
mkdir -p pcaps rsa_keys Packmate_stuff
|
||||||
|
|
||||||
|
# Create .env file
|
||||||
|
cat > .env <<EOF
|
||||||
|
BUILD_TAG=latest
|
||||||
|
PACKMATE_DB_PASSWORD=${PACKMATE_DB_PASSWORD:-K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb}
|
||||||
|
NET_INTERFACE=${NET_INTERFACE:-eth0}
|
||||||
|
PACKMATE_LOCAL_IP=${PACKMATE_LOCAL_IP:-10.60.0.1}
|
||||||
|
WEB_LOGIN=${WEB_LOGIN:-admin}
|
||||||
|
WEB_PASSWORD=${WEB_PASSWORD:-admin123}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create PostgreSQL config
|
||||||
|
cat > Packmate_stuff/postgresql.conf <<EOF
|
||||||
|
port = 65001
|
||||||
|
max_connections = 100
|
||||||
|
shared_buffers = 128MB
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create update script
|
||||||
|
cat > Packmate_stuff/update_db_config.sh <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
cp /tmp/postgresql.conf /var/lib/postgresql/data/postgresql.conf
|
||||||
|
EOF
|
||||||
|
chmod +x Packmate_stuff/update_db_config.sh
|
||||||
|
|
||||||
|
# Create docker-compose.yml
|
||||||
|
cat > docker-compose.yml <<EOF
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
packmate:
|
||||||
|
environment:
|
||||||
|
DB_PASSWORD: \${PACKMATE_DB_PASSWORD:-K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb}
|
||||||
|
INTERFACE: \${NET_INTERFACE:-}
|
||||||
|
LOCAL_IP: \${PACKMATE_LOCAL_IP}
|
||||||
|
MODE: LIVE
|
||||||
|
WEB_LOGIN: \${WEB_LOGIN:-admin}
|
||||||
|
WEB_PASSWORD: \${WEB_PASSWORD:-admin123}
|
||||||
|
OLD_STREAMS_CLEANUP_ENABLED: true
|
||||||
|
OLD_STREAMS_CLEANUP_INTERVAL: 5
|
||||||
|
OLD_STREAMS_CLEANUP_THRESHOLD: 240
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
container_name: packmate-app
|
||||||
|
network_mode: "host"
|
||||||
|
image: registry.gitlab.com/packmate/packmate:\${BUILD_TAG:-latest}
|
||||||
|
volumes:
|
||||||
|
- "./pcaps/:/app/pcaps/:ro"
|
||||||
|
- "./rsa_keys/:/app/rsa_keys/:ro"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
db:
|
||||||
|
container_name: packmate-db
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: packmate
|
||||||
|
POSTGRES_PASSWORD: \${PACKMATE_DB_PASSWORD:-K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb}
|
||||||
|
POSTGRES_DB: packmate
|
||||||
|
network_mode: "host"
|
||||||
|
image: postgres:15.2
|
||||||
|
volumes:
|
||||||
|
- "./Packmate_stuff/postgresql.conf:/tmp/postgresql.conf:ro"
|
||||||
|
- "./Packmate_stuff/update_db_config.sh:/docker-entrypoint-initdb.d/_update_db_config.sh:ro"
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "pg_isready -U packmate -p 65001" ]
|
||||||
|
interval: 2s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 15
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Packmate setup complete!"
|
||||||
|
|
||||||
|
# Register with controller
|
||||||
|
echo "Registering Packmate with controller..."
|
||||||
|
call_api "/services" "POST" "{\"name\": \"packmate\", \"path\": \"$packmate_dir\", \"git_url\": \"https://gitlab.com/packmate/Packmate.git\"}"
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to setup moded_distructive_farm
|
||||||
|
setup_farm() {
|
||||||
|
echo ""
|
||||||
|
echo "=== Setting up moded_distructive_farm ==="
|
||||||
|
|
||||||
|
local farm_dir="$SERVICES_DIR/moded_distructive_farm"
|
||||||
|
|
||||||
|
if [ -d "$farm_dir" ]; then
|
||||||
|
echo "Farm directory already exists, updating..."
|
||||||
|
cd "$farm_dir"
|
||||||
|
git pull
|
||||||
|
else
|
||||||
|
echo "Cloning moded_distructive_farm..."
|
||||||
|
git clone https://github.com/ilyastar9999/moded_distructive_farm.git "$farm_dir"
|
||||||
|
cd "$farm_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create .env file
|
||||||
|
cat > .env <<EOF
|
||||||
|
# Database configuration
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_HOST=postgres
|
||||||
|
DB_USER=farm
|
||||||
|
DB_PASS=${FARM_DB_PASS:-farmpassword123}
|
||||||
|
DB_NAME=farm
|
||||||
|
|
||||||
|
# Scoreboard configuration
|
||||||
|
BOARD_URL=${BOARD_URL:-http://10.60.0.1}
|
||||||
|
TEAM_TOKEN=${TEAM_TOKEN:-your-team-token}
|
||||||
|
|
||||||
|
# Web interface
|
||||||
|
WEB_PASSWORD=${FARM_WEB_PASSWORD:-farmadmin}
|
||||||
|
|
||||||
|
# Game configuration
|
||||||
|
NUM_TEAMS=${NUM_TEAMS:-10}
|
||||||
|
IP_TEAM_BASE=${IP_TEAM_BASE:-10.60.}
|
||||||
|
|
||||||
|
# API Token
|
||||||
|
API_TOKEN=${FARM_API_TOKEN:-farm-api-token-123}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create docker-compose.yml
|
||||||
|
cat > docker-compose.yml <<EOF
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
farm:
|
||||||
|
image: ghcr.io/ilyastar9999/moded_distructive_farm:latest
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- DB_PORT=\${DB_PORT}
|
||||||
|
- DB_HOST=\${DB_HOST}
|
||||||
|
- DB_USER=\${DB_USER}
|
||||||
|
- DB_PASS=\${DB_PASS}
|
||||||
|
- DB_NAME=\${DB_NAME}
|
||||||
|
- BOARD_URL=\${BOARD_URL}
|
||||||
|
- TEAM_TOKEN=\${TEAM_TOKEN}
|
||||||
|
- WEB_PASSWORD=\${WEB_PASSWORD}
|
||||||
|
- NUM_TEAMS=\${NUM_TEAMS}
|
||||||
|
- IP_TEAM_BASE=\${IP_TEAM_BASE}
|
||||||
|
- API_TOKEN=\${API_TOKEN}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
container_name: farm-app
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3333:8000"
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:18
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=\${DB_USER}
|
||||||
|
- POSTGRES_PASSWORD=\${DB_PASS}
|
||||||
|
- POSTGRES_DB=\${DB_NAME}
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U \${DB_USER} -d \${DB_NAME}
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
volumes:
|
||||||
|
- farm-db:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
farm-db:
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "moded_distructive_farm setup complete!"
|
||||||
|
|
||||||
|
# Register with controller
|
||||||
|
echo "Registering farm with controller..."
|
||||||
|
call_api "/services" "POST" "{\"name\": \"farm\", \"path\": \"$farm_dir\", \"git_url\": \"https://github.com/ilyastar9999/moded_distructive_farm.git\"}"
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to setup Firegex
|
||||||
|
setup_firegex() {
|
||||||
|
echo ""
|
||||||
|
echo "=== Setting up Firegex ==="
|
||||||
|
|
||||||
|
local firegex_dir="$SERVICES_DIR/firegex"
|
||||||
|
|
||||||
|
if [ -d "$firegex_dir" ]; then
|
||||||
|
echo "Firegex directory already exists, updating..."
|
||||||
|
cd "$firegex_dir"
|
||||||
|
git pull
|
||||||
|
else
|
||||||
|
echo "Cloning Firegex..."
|
||||||
|
git clone https://github.com/Pwnzer0tt1/firegex.git "$firegex_dir"
|
||||||
|
cd "$firegex_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create .env file
|
||||||
|
cat > .env <<EOF
|
||||||
|
# Firegex configuration
|
||||||
|
TEAM_TOKEN=${TEAM_TOKEN:-your-team-token}
|
||||||
|
SCOREBOARD_URL=${BOARD_URL:-http://10.60.0.1}
|
||||||
|
FIREGEX_PORT=${FIREGEX_PORT:-5000}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create docker-compose.yml if not exists
|
||||||
|
if [ ! -f "docker-compose.yml" ]; then
|
||||||
|
cat > docker-compose.yml <<EOF
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
firegex:
|
||||||
|
build: .
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- TEAM_TOKEN=\${TEAM_TOKEN}
|
||||||
|
- SCOREBOARD_URL=\${SCOREBOARD_URL}
|
||||||
|
container_name: firegex-app
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "\${FIREGEX_PORT:-5000}:5000"
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Firegex setup complete!"
|
||||||
|
|
||||||
|
# Register with controller
|
||||||
|
echo "Registering Firegex with controller..."
|
||||||
|
call_api "/services" "POST" "{\"name\": \"firegex\", \"path\": \"$firegex_dir\", \"git_url\": \"https://github.com/Pwnzer0tt1/firegex.git\"}"
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main setup flow
|
||||||
|
main() {
|
||||||
|
echo "Starting setup process..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Read configuration
|
||||||
|
read -p "Setup Packmate? (y/n): " setup_pm
|
||||||
|
read -p "Setup moded_distructive_farm? (y/n): " setup_fm
|
||||||
|
read -p "Setup Firegex? (y/n): " setup_fg
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$setup_pm" = "y" ]; then
|
||||||
|
setup_packmate
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$setup_fm" = "y" ]; then
|
||||||
|
setup_farm
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$setup_fg" = "y" ]; then
|
||||||
|
setup_firegex
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Setup Complete! ==="
|
||||||
|
echo "Services have been configured in: $SERVICES_DIR"
|
||||||
|
echo "You can manage them through the controller API or web dashboard"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main if executed directly
|
||||||
|
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
||||||
|
main
|
||||||
|
fi
|
||||||
10
tg-bot/Dockerfile
Normal file
10
tg-bot/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY main.py .
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
206
tg-bot/main.py
Normal file
206
tg-bot/main.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
"""
|
||||||
|
Telegram Bot for A/D Infrastructure
|
||||||
|
Sends notifications to group chat
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import FastAPI, HTTPException, Depends, Header
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import asyncpg
|
||||||
|
from telegram import Bot
|
||||||
|
from telegram.error import TelegramError
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://adctrl:adctrl@postgres:5432/adctrl")
|
||||||
|
SECRET_TOKEN = os.getenv("SECRET_TOKEN", "change-me-in-production")
|
||||||
|
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
||||||
|
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||||
|
|
||||||
|
if not TELEGRAM_BOT_TOKEN:
|
||||||
|
print("WARNING: TELEGRAM_BOT_TOKEN not set!")
|
||||||
|
|
||||||
|
if not TELEGRAM_CHAT_ID:
|
||||||
|
print("WARNING: TELEGRAM_CHAT_ID not set!")
|
||||||
|
|
||||||
|
# Database pool and bot
|
||||||
|
db_pool = None
|
||||||
|
bot = None
|
||||||
|
|
||||||
|
class MessageRequest(BaseModel):
|
||||||
|
message: str
|
||||||
|
chat_id: str = None # Optional, uses default if not provided
|
||||||
|
|
||||||
|
class BulkMessageRequest(BaseModel):
|
||||||
|
messages: list[str]
|
||||||
|
chat_id: str = None
|
||||||
|
|
||||||
|
# Auth dependency
|
||||||
|
async def verify_token(authorization: str = Header(None)):
|
||||||
|
if not authorization or not authorization.startswith("Bearer "):
|
||||||
|
raise HTTPException(status_code=401, detail="Missing or invalid authorization header")
|
||||||
|
|
||||||
|
token = authorization.replace("Bearer ", "")
|
||||||
|
if token != SECRET_TOKEN:
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid token")
|
||||||
|
return token
|
||||||
|
|
||||||
|
# Database functions
|
||||||
|
async def get_db():
|
||||||
|
return await db_pool.acquire()
|
||||||
|
|
||||||
|
async def release_db(conn):
|
||||||
|
await db_pool.release(conn)
|
||||||
|
|
||||||
|
async def log_message(chat_id: int, message: str, success: bool, error_message: str = None):
|
||||||
|
"""Log sent message to database"""
|
||||||
|
conn = await db_pool.acquire()
|
||||||
|
try:
|
||||||
|
await conn.execute(
|
||||||
|
"INSERT INTO telegram_messages (chat_id, message, success, error_message) VALUES ($1, $2, $3, $4)",
|
||||||
|
chat_id, message, success, error_message
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await db_pool.release(conn)
|
||||||
|
|
||||||
|
# Lifespan context
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
global db_pool, bot
|
||||||
|
db_pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10)
|
||||||
|
|
||||||
|
if TELEGRAM_BOT_TOKEN:
|
||||||
|
bot = Bot(token=TELEGRAM_BOT_TOKEN)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
await db_pool.close()
|
||||||
|
|
||||||
|
app = FastAPI(title="Telegram Bot API", lifespan=lifespan)
|
||||||
|
|
||||||
|
# API Endpoints
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"bot_configured": bot is not None,
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/send", dependencies=[Depends(verify_token)])
|
||||||
|
async def send_message(request: MessageRequest):
|
||||||
|
"""Send a message to telegram chat"""
|
||||||
|
if not bot:
|
||||||
|
raise HTTPException(status_code=503, detail="Telegram bot not configured")
|
||||||
|
|
||||||
|
chat_id = request.chat_id or TELEGRAM_CHAT_ID
|
||||||
|
if not chat_id:
|
||||||
|
raise HTTPException(status_code=400, detail="No chat_id provided and no default configured")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send message
|
||||||
|
message = await bot.send_message(
|
||||||
|
chat_id=int(chat_id),
|
||||||
|
text=request.message,
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log success
|
||||||
|
await log_message(int(chat_id), request.message, True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "sent",
|
||||||
|
"message_id": message.message_id,
|
||||||
|
"chat_id": chat_id
|
||||||
|
}
|
||||||
|
|
||||||
|
except TelegramError as e:
|
||||||
|
# Log failure
|
||||||
|
await log_message(int(chat_id), request.message, False, str(e))
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to send message: {str(e)}")
|
||||||
|
|
||||||
|
@app.post("/send-bulk", dependencies=[Depends(verify_token)])
|
||||||
|
async def send_bulk_messages(request: BulkMessageRequest):
|
||||||
|
"""Send multiple messages to telegram chat"""
|
||||||
|
if not bot:
|
||||||
|
raise HTTPException(status_code=503, detail="Telegram bot not configured")
|
||||||
|
|
||||||
|
chat_id = request.chat_id or TELEGRAM_CHAT_ID
|
||||||
|
if not chat_id:
|
||||||
|
raise HTTPException(status_code=400, detail="No chat_id provided and no default configured")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for msg in request.messages:
|
||||||
|
try:
|
||||||
|
message = await bot.send_message(
|
||||||
|
chat_id=int(chat_id),
|
||||||
|
text=msg,
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
await log_message(int(chat_id), msg, True)
|
||||||
|
results.append({
|
||||||
|
"status": "sent",
|
||||||
|
"message_id": message.message_id,
|
||||||
|
"message": msg[:50] + "..." if len(msg) > 50 else msg
|
||||||
|
})
|
||||||
|
except TelegramError as e:
|
||||||
|
await log_message(int(chat_id), msg, False, str(e))
|
||||||
|
results.append({
|
||||||
|
"status": "failed",
|
||||||
|
"error": str(e),
|
||||||
|
"message": msg[:50] + "..." if len(msg) > 50 else msg
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"results": results, "total": len(results)}
|
||||||
|
|
||||||
|
@app.get("/messages", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_message_history(limit: int = 50):
|
||||||
|
"""Get message sending history"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"SELECT * FROM telegram_messages ORDER BY sent_at DESC LIMIT $1",
|
||||||
|
limit
|
||||||
|
)
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.get("/stats", dependencies=[Depends(verify_token)])
|
||||||
|
async def get_stats():
|
||||||
|
"""Get message statistics"""
|
||||||
|
conn = await get_db()
|
||||||
|
try:
|
||||||
|
total = await conn.fetchval("SELECT COUNT(*) FROM telegram_messages")
|
||||||
|
successful = await conn.fetchval("SELECT COUNT(*) FROM telegram_messages WHERE success = true")
|
||||||
|
failed = await conn.fetchval("SELECT COUNT(*) FROM telegram_messages WHERE success = false")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_messages": total,
|
||||||
|
"successful": successful,
|
||||||
|
"failed": failed,
|
||||||
|
"success_rate": (successful / total * 100) if total > 0 else 0
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
await release_db(conn)
|
||||||
|
|
||||||
|
@app.post("/test", dependencies=[Depends(verify_token)])
|
||||||
|
async def test_connection():
|
||||||
|
"""Test telegram bot connection"""
|
||||||
|
if not bot:
|
||||||
|
raise HTTPException(status_code=503, detail="Telegram bot not configured")
|
||||||
|
|
||||||
|
try:
|
||||||
|
me = await bot.get_me()
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"bot_username": me.username,
|
||||||
|
"bot_name": me.first_name,
|
||||||
|
"bot_id": me.id
|
||||||
|
}
|
||||||
|
except TelegramError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Bot test failed: {str(e)}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8003)
|
||||||
6
tg-bot/requirements.txt
Normal file
6
tg-bot/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn[standard]==0.27.0
|
||||||
|
asyncpg==0.29.0
|
||||||
|
pydantic==2.5.3
|
||||||
|
python-telegram-bot==21.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
10
web/Dockerfile
Normal file
10
web/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]
|
||||||
231
web/app.py
Normal file
231
web/app.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
"""
|
||||||
|
Web Dashboard for A/D Infrastructure Control
|
||||||
|
Flask-based dashboard to monitor services, attacks, and alerts
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from flask import Flask, render_template, jsonify, request, redirect, url_for, session
|
||||||
|
import asyncpg
|
||||||
|
import aiohttp
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://adctrl:adctrl@postgres:5432/adctrl")
|
||||||
|
SECRET_TOKEN = os.getenv("SECRET_TOKEN", "change-me-in-production")
|
||||||
|
WEB_PASSWORD = os.getenv("WEB_PASSWORD", "admin123")
|
||||||
|
CONTROLLER_API = os.getenv("CONTROLLER_API", "http://controller:8001")
|
||||||
|
SCOREBOARD_API = os.getenv("SCOREBOARD_API", "http://scoreboard-injector:8002")
|
||||||
|
TELEGRAM_API = os.getenv("TELEGRAM_API", "http://tg-bot:8003")
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.getenv("FLASK_SECRET_KEY", "change-me-in-production-flask-secret")
|
||||||
|
|
||||||
|
# Database connection
|
||||||
|
async def get_db_conn():
|
||||||
|
return await asyncpg.connect(DATABASE_URL)
|
||||||
|
|
||||||
|
# Auth decorator
|
||||||
|
def login_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
# API call helper
|
||||||
|
async def api_call(url: str, method: str = "GET", data: dict = {}):
|
||||||
|
"""Make API call to internal services"""
|
||||||
|
headers = {"Authorization": f"Bearer {SECRET_TOKEN}"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
if method == "GET":
|
||||||
|
async with session.get(url, headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
return await resp.json()
|
||||||
|
return {"error": f"Status {resp.status}"}
|
||||||
|
elif method == "POST":
|
||||||
|
async with session.post(url, headers=headers, json=data) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
return await resp.json()
|
||||||
|
return {"error": f"Status {resp.status}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
# Routes
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
password = request.form.get('password')
|
||||||
|
if password == WEB_PASSWORD:
|
||||||
|
session['logged_in'] = True
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
else:
|
||||||
|
return render_template('login.html', error="Invalid password")
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.pop('logged_in', None)
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/services')
|
||||||
|
@login_required
|
||||||
|
def services():
|
||||||
|
return render_template('services.html')
|
||||||
|
|
||||||
|
@app.route('/attacks')
|
||||||
|
@login_required
|
||||||
|
def attacks():
|
||||||
|
return render_template('attacks.html')
|
||||||
|
|
||||||
|
@app.route('/alerts')
|
||||||
|
@login_required
|
||||||
|
def alerts():
|
||||||
|
return render_template('alerts.html')
|
||||||
|
|
||||||
|
# API Endpoints
|
||||||
|
@app.route('/api/dashboard')
|
||||||
|
@login_required
|
||||||
|
def api_dashboard():
|
||||||
|
"""Get dashboard overview data"""
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch data from all services
|
||||||
|
services_data = loop.run_until_complete(api_call(f"{CONTROLLER_API}/services"))
|
||||||
|
scoreboard_stats = loop.run_until_complete(api_call(f"{SCOREBOARD_API}/stats"))
|
||||||
|
telegram_stats = loop.run_until_complete(api_call(f"{TELEGRAM_API}/stats"))
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"services": services_data,
|
||||||
|
"scoreboard": scoreboard_stats,
|
||||||
|
"telegram": telegram_stats,
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
})
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
@app.route('/api/services')
|
||||||
|
@login_required
|
||||||
|
def api_services():
|
||||||
|
"""Get services list"""
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = loop.run_until_complete(api_call(f"{CONTROLLER_API}/services"))
|
||||||
|
return jsonify(data)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
@app.route('/api/services/<int:service_id>/action', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def api_service_action(service_id):
|
||||||
|
"""Perform action on service"""
|
||||||
|
action = request.json.get('action')
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = loop.run_until_complete(
|
||||||
|
api_call(f"{CONTROLLER_API}/services/{service_id}/action", "POST", {"action": action})
|
||||||
|
)
|
||||||
|
return jsonify(data)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
@app.route('/api/services/<int:service_id>/logs')
|
||||||
|
@login_required
|
||||||
|
def api_service_logs(service_id):
|
||||||
|
"""Get service logs"""
|
||||||
|
lines = request.args.get('lines', 100)
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = loop.run_until_complete(api_call(f"{CONTROLLER_API}/services/{service_id}/logs?lines={lines}"))
|
||||||
|
return jsonify(data)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
@app.route('/api/attacks')
|
||||||
|
@login_required
|
||||||
|
def api_attacks_list():
|
||||||
|
"""Get attacks list"""
|
||||||
|
limit = request.args.get('limit', 100)
|
||||||
|
our_attacks = request.args.get('our_attacks')
|
||||||
|
attacks_to_us = request.args.get('attacks_to_us')
|
||||||
|
|
||||||
|
url = f"{SCOREBOARD_API}/attacks?limit={limit}"
|
||||||
|
if our_attacks is not None:
|
||||||
|
url += f"&our_attacks={our_attacks}"
|
||||||
|
if attacks_to_us is not None:
|
||||||
|
url += f"&attacks_to_us={attacks_to_us}"
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = loop.run_until_complete(api_call(url))
|
||||||
|
return jsonify(data)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
@app.route('/api/attacks/by-service')
|
||||||
|
@login_required
|
||||||
|
def api_attacks_by_service():
|
||||||
|
"""Get attacks grouped by service"""
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = loop.run_until_complete(api_call(f"{SCOREBOARD_API}/attacks/by-service"))
|
||||||
|
return jsonify(data)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
@app.route('/api/alerts')
|
||||||
|
@login_required
|
||||||
|
def api_alerts_list():
|
||||||
|
"""Get alerts list"""
|
||||||
|
limit = request.args.get('limit', 50)
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = loop.run_until_complete(api_call(f"{SCOREBOARD_API}/alerts?limit={limit}"))
|
||||||
|
return jsonify(data)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
@app.route('/api/telegram/send', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def api_telegram_send():
|
||||||
|
"""Send telegram message"""
|
||||||
|
message = request.json.get('message')
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = loop.run_until_complete(
|
||||||
|
api_call(f"{TELEGRAM_API}/send", "POST", {"message": message})
|
||||||
|
)
|
||||||
|
return jsonify(data)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=8000, debug=False)
|
||||||
5
web/requirements.txt
Normal file
5
web/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
flask==3.0.0
|
||||||
|
asyncpg==0.29.0
|
||||||
|
aiohttp==3.9.1
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
gunicorn==21.2.0
|
||||||
107
web/templates/alerts.html
Normal file
107
web/templates/alerts.html
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Alerts - A/D Infrastructure Control{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>Security Alerts <i class="bi bi-arrow-clockwise refresh-btn" id="refreshAlerts"></i></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Send Test Alert</h5>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="testMessage" placeholder="Enter test message">
|
||||||
|
<button class="btn btn-primary" onclick="sendTestAlert()">
|
||||||
|
<i class="bi bi-send"></i> Send to Telegram
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Alert History</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group" id="alertsList">
|
||||||
|
<div class="text-center py-3">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function loadAlerts() {
|
||||||
|
$.get('/api/alerts?limit=100', function (data) {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
let html = '';
|
||||||
|
data.forEach(alert => {
|
||||||
|
let badgeClass = alert.severity === 'critical' ? 'danger' : 'warning';
|
||||||
|
let notifiedBadge = alert.notified
|
||||||
|
? '<span class="badge bg-success">Notified</span>'
|
||||||
|
: '<span class="badge bg-secondary">Pending</span>';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="badge bg-${badgeClass}">${alert.severity.toUpperCase()}</span>
|
||||||
|
${notifiedBadge}
|
||||||
|
<span class="badge bg-info">${alert.alert_type}</span>
|
||||||
|
<small class="text-muted ms-2">${new Date(alert.created_at).toLocaleString()}</small>
|
||||||
|
</div>
|
||||||
|
<p class="mb-0">${alert.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$('#alertsList').html(html);
|
||||||
|
} else {
|
||||||
|
$('#alertsList').html('<div class="text-center py-3 text-muted">No alerts</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendTestAlert() {
|
||||||
|
const message = $('#testMessage').val();
|
||||||
|
if (!message) {
|
||||||
|
alert('Please enter a message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/telegram/send',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({ message: message }),
|
||||||
|
success: function (data) {
|
||||||
|
alert('Message sent to Telegram!');
|
||||||
|
$('#testMessage').val('');
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
alert(`Failed to send message: ${xhr.responseJSON?.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
loadAlerts();
|
||||||
|
$('#refreshAlerts').click(loadAlerts);
|
||||||
|
setInterval(loadAlerts, 10000); // Auto-refresh every 10 seconds
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
169
web/templates/attacks.html
Normal file
169
web/templates/attacks.html
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Attacks - A/D Infrastructure Control{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>Attacks Monitor <i class="bi bi-arrow-clockwise refresh-btn" id="refreshAttacks"></i></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-primary active" id="filterAll">All Attacks</button>
|
||||||
|
<button type="button" class="btn btn-outline-success" id="filterOur">Our Attacks</button>
|
||||||
|
<button type="button" class="btn btn-outline-danger" id="filterAgainstUs">Attacks to Us</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Attacks by Service</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Our Attacks</th>
|
||||||
|
<th>Against Us</th>
|
||||||
|
<th>Points Gained</th>
|
||||||
|
<th>Points Lost</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="serviceStatsTable">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Recent Attacks</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Attacker</th>
|
||||||
|
<th>Victim</th>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>Points</th>
|
||||||
|
<th>Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="attacksTable">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
let currentFilter = 'all';
|
||||||
|
|
||||||
|
function loadServiceStats() {
|
||||||
|
$.get('/api/attacks/by-service', function (data) {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
let html = '';
|
||||||
|
data.forEach(stat => {
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${stat.service_name}</strong></td>
|
||||||
|
<td>${stat.total_attacks}</td>
|
||||||
|
<td><span class="text-success">${stat.our_attacks}</span></td>
|
||||||
|
<td><span class="text-danger">${stat.attacks_to_us}</span></td>
|
||||||
|
<td><span class="text-success">+${stat.points_gained.toFixed(2)}</span></td>
|
||||||
|
<td><span class="text-danger">-${stat.points_lost.toFixed(2)}</span></td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$('#serviceStatsTable').html(html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAttacks() {
|
||||||
|
let url = '/api/attacks?limit=100';
|
||||||
|
if (currentFilter === 'our') {
|
||||||
|
url += '&our_attacks=true';
|
||||||
|
} else if (currentFilter === 'against') {
|
||||||
|
url += '&attacks_to_us=true';
|
||||||
|
}
|
||||||
|
|
||||||
|
$.get(url, function (data) {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
let html = '';
|
||||||
|
data.forEach(attack => {
|
||||||
|
let typeLabel = '';
|
||||||
|
if (attack.is_our_attack) {
|
||||||
|
typeLabel = '<span class="badge bg-success">Our Attack</span>';
|
||||||
|
} else if (attack.is_attack_to_us) {
|
||||||
|
typeLabel = '<span class="badge bg-danger">Against Us</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${new Date(attack.timestamp).toLocaleString()}</td>
|
||||||
|
<td>Team ${attack.attacker_team_id}</td>
|
||||||
|
<td>Team ${attack.victim_team_id}</td>
|
||||||
|
<td>${attack.service_name}</td>
|
||||||
|
<td>${attack.points ? attack.points.toFixed(2) : '-'}</td>
|
||||||
|
<td>${typeLabel}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$('#attacksTable').html(html);
|
||||||
|
} else {
|
||||||
|
$('#attacksTable').html('<tr><td colspan="6" class="text-center text-muted">No attacks</td></tr>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFilter(filter) {
|
||||||
|
currentFilter = filter;
|
||||||
|
$('.btn-group button').removeClass('active');
|
||||||
|
$(`#filter${filter.charAt(0).toUpperCase() + filter.slice(1)}`).addClass('active');
|
||||||
|
loadAttacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
loadServiceStats();
|
||||||
|
loadAttacks();
|
||||||
|
$('#refreshAttacks').click(function () {
|
||||||
|
loadServiceStats();
|
||||||
|
loadAttacks();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#filterAll').click(() => setFilter('all'));
|
||||||
|
$('#filterOur').click(() => setFilter('our'));
|
||||||
|
$('#filterAgainstUs').click(() => setFilter('against'));
|
||||||
|
|
||||||
|
setInterval(loadAttacks, 5000); // Auto-refresh every 5 seconds
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
92
web/templates/base.html
Normal file
92
web/templates/base.html
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}A/D Infrastructure Control{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.danger {
|
||||||
|
border-left-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.success {
|
||||||
|
border-left-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.warning {
|
||||||
|
border-left-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-running {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-stopped {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">
|
||||||
|
<i class="bi bi-shield-check"></i> A/D Control
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/"><i class="bi bi-speedometer2"></i> Dashboard</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/services"><i class="bi bi-hdd-stack"></i> Services</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/attacks"><i class="bi bi-bullseye"></i> Attacks</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/alerts"><i class="bi bi-exclamation-triangle"></i> Alerts</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/logout"><i class="bi bi-box-arrow-right"></i> Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
137
web/templates/index.html
Normal file
137
web/templates/index.html
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard - A/D Infrastructure Control{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>Dashboard <i class="bi bi-arrow-clockwise refresh-btn" id="refreshDashboard"></i></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Total Services</h6>
|
||||||
|
<h2 id="totalServices">-</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card success">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Our Attacks</h6>
|
||||||
|
<h2 id="ourAttacks">-</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card danger">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Attacks to Us</h6>
|
||||||
|
<h2 id="attacksToUs">-</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card warning">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted">Critical Alerts</h6>
|
||||||
|
<h2 id="criticalAlerts">-</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Services Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="servicesStatus" class="list-group">
|
||||||
|
<div class="text-center py-3">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Recent Alerts</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="recentAlerts" class="list-group">
|
||||||
|
<div class="text-center py-3">Loading...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function loadDashboard() {
|
||||||
|
$.get('/api/dashboard', function (data) {
|
||||||
|
// Update stats
|
||||||
|
$('#totalServices').text(data.services ? data.services.length : 0);
|
||||||
|
$('#ourAttacks').text(data.scoreboard?.attacks_by_us || 0);
|
||||||
|
$('#attacksToUs').text(data.scoreboard?.attacks_to_us || 0);
|
||||||
|
$('#criticalAlerts').text(data.scoreboard?.critical_alerts_5min || 0);
|
||||||
|
|
||||||
|
// Update services status
|
||||||
|
if (data.services && data.services.length > 0) {
|
||||||
|
let html = '';
|
||||||
|
data.services.forEach(service => {
|
||||||
|
let statusClass = service.status === 'running' ? 'service-running' : 'service-stopped';
|
||||||
|
let icon = service.status === 'running' ? 'check-circle-fill' : 'x-circle-fill';
|
||||||
|
html += `
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span>${service.name}</span>
|
||||||
|
<span class="${statusClass}">
|
||||||
|
<i class="bi bi-${icon}"></i> ${service.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$('#servicesStatus').html(html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load recent alerts
|
||||||
|
$.get('/api/alerts?limit=5', function (data) {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
let html = '';
|
||||||
|
data.forEach(alert => {
|
||||||
|
let badgeClass = alert.severity === 'critical' ? 'danger' : 'warning';
|
||||||
|
html += `
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-${badgeClass}">${alert.severity}</span>
|
||||||
|
<small class="text-muted ms-2">${new Date(alert.created_at).toLocaleString()}</small>
|
||||||
|
<p class="mb-0 mt-1">${alert.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$('#recentAlerts').html(html);
|
||||||
|
} else {
|
||||||
|
$('#recentAlerts').html('<div class="text-center py-3 text-muted">No alerts</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
loadDashboard();
|
||||||
|
$('#refreshDashboard').click(loadDashboard);
|
||||||
|
setInterval(loadDashboard, 10000); // Auto-refresh every 10 seconds
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
47
web/templates/login.html
Normal file
47
web/templates/login.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - A/D Infrastructure Control</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="card shadow-lg">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<h2 class="text-center mb-4">
|
||||||
|
<i class="bi bi-shield-check"></i> A/D Control
|
||||||
|
</h2>
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required autofocus>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
130
web/templates/services.html
Normal file
130
web/templates/services.html
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Services - A/D Infrastructure Control{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>Services <i class="bi bi-arrow-clockwise refresh-btn" id="refreshServices"></i></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Path</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Last Updated</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="servicesTable">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logs Modal -->
|
||||||
|
<div class="modal fade" id="logsModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Service Logs</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pre id="logsContent"
|
||||||
|
style="max-height: 500px; overflow-y: auto; background: #f8f9fa; padding: 15px;"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function loadServices() {
|
||||||
|
$.get('/api/services', function (data) {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
let html = '';
|
||||||
|
data.forEach(service => {
|
||||||
|
let statusBadge = service.status === 'running'
|
||||||
|
? '<span class="badge bg-success">Running</span>'
|
||||||
|
: '<span class="badge bg-secondary">Stopped</span>';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${service.name}</strong></td>
|
||||||
|
<td><code>${service.path}</code></td>
|
||||||
|
<td>${statusBadge}</td>
|
||||||
|
<td>${new Date(service.last_updated).toLocaleString()}</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-success" onclick="serviceAction(${service.id}, 'start')">
|
||||||
|
<i class="bi bi-play-fill"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning" onclick="serviceAction(${service.id}, 'restart')">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" onclick="serviceAction(${service.id}, 'stop')">
|
||||||
|
<i class="bi bi-stop-fill"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-info" onclick="viewLogs(${service.id})">
|
||||||
|
<i class="bi bi-file-text"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$('#servicesTable').html(html);
|
||||||
|
} else {
|
||||||
|
$('#servicesTable').html('<tr><td colspan="5" class="text-center text-muted">No services registered</td></tr>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function serviceAction(serviceId, action) {
|
||||||
|
if (!confirm(`Are you sure you want to ${action} this service?`)) return;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/services/${serviceId}/action`,
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({ action: action }),
|
||||||
|
success: function (data) {
|
||||||
|
alert(`Service ${action} successful!`);
|
||||||
|
loadServices();
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
alert(`Failed to ${action} service: ${xhr.responseJSON?.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewLogs(serviceId) {
|
||||||
|
$.get(`/api/services/${serviceId}/logs?lines=200`, function (data) {
|
||||||
|
$('#logsContent').text(data.logs || 'No logs available');
|
||||||
|
new bootstrap.Modal(document.getElementById('logsModal')).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
loadServices();
|
||||||
|
$('#refreshServices').click(loadServices);
|
||||||
|
setInterval(loadServices, 15000); // Auto-refresh every 15 seconds
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user