diff --git a/.env b/.env new file mode 100644 index 0000000..4dd999d --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +DATABASE_URL=postgresql://admin:secure_password_123@database:5432/threat_hunter +SECRET_KEY=your-very-secret-key-change-in-production +FLASK_ENV=production diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b90bdbc --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create directories +RUN mkdir -p uploads output + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/backend/Dockerfile.prod b/backend/Dockerfile.prod new file mode 100644 index 0000000..9e8832f --- /dev/null +++ b/backend/Dockerfile.prod @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN useradd --create-home --shell /bin/bash app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . +RUN chown -R app:app /app + +USER app + +EXPOSE 5000 + +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"] diff --git a/backend/app.py b/backend/app.py index d8da87c..320bc7b 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,9 +1,11 @@ import os import logging +from datetime import datetime, timedelta from flask import Flask, request, jsonify, send_from_directory +from flask_sqlalchemy import SQLAlchemy +from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity from werkzeug.utils import secure_filename -from werkzeug.exceptions import RequestEntityTooLarge -from datetime import datetime +import bcrypt # Try to import flask-cors try: @@ -19,6 +21,24 @@ logger = logging.getLogger(__name__) app = Flask(__name__, static_folder="../frontend/dist") +# Configuration +app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'postgresql://admin:secure_password_123@localhost:5432/threat_hunter') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['JWT_SECRET_KEY'] = os.getenv('SECRET_KEY', 'change-this-in-production') +app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=24) +app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 +app.config['UPLOAD_FOLDER'] = 'uploaded' +app.config['OUTPUT_FOLDER'] = 'output' +app.config['ALLOWED_EXTENSIONS'] = {'csv', 'json', 'txt', 'log'} + +# Ensure upload directories exist +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) +os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True) + +# Initialize extensions +db = SQLAlchemy(app) +jwt = JWTManager(app) + # Enable CORS if CORS_AVAILABLE: CORS(app) @@ -30,16 +50,6 @@ else: response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE') return response -# Configuration -app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 -app.config['UPLOAD_FOLDER'] = 'uploaded' -app.config['OUTPUT_FOLDER'] = 'output' -app.config['ALLOWED_EXTENSIONS'] = {'csv', 'json', 'txt', 'log'} - -# Ensure upload directories exist -os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) -os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True) - def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS'] @@ -53,6 +63,144 @@ def handle_exception(e): logger.error(f"Unhandled exception: {e}") return jsonify({'error': 'Internal server error'}), 500 +# Database Models +class User(db.Model): + __tablename__ = 'users' + id = db.Column(db.String, primary_key=True, default=lambda: str(uuid.uuid4())) + username = db.Column(db.String(50), unique=True, nullable=False) + email = db.Column(db.String(100), unique=True, nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + last_login = db.Column(db.DateTime) + +class Hunt(db.Model): + __tablename__ = 'hunts' + id = db.Column(db.String, primary_key=True, default=lambda: str(uuid.uuid4())) + name = db.Column(db.String(100), nullable=False) + description = db.Column(db.Text) + created_by = db.Column(db.String, db.ForeignKey('users.id')) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow) + status = db.Column(db.String(20), default='active') + +# Authentication Routes +@app.route('/api/auth/login', methods=['POST']) +def login(): + try: + data = request.get_json() + username = data.get('username') + password = data.get('password') + + user = User.query.filter_by(username=username).first() + + if user and bcrypt.checkpw(password.encode('utf-8'), user.password_hash.encode('utf-8')): + access_token = create_access_token(identity=user.id) + user.last_login = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'access_token': access_token, + 'user': { + 'id': user.id, + 'username': user.username, + 'email': user.email + } + }) + else: + return jsonify({'error': 'Invalid credentials'}), 401 + + except Exception as e: + logger.error(f"Login error: {e}") + return jsonify({'error': 'Login failed'}), 500 + +@app.route('/api/auth/register', methods=['POST']) +def register(): + try: + data = request.get_json() + username = data.get('username') + email = data.get('email') + password = data.get('password') + + # Check if user exists + if User.query.filter_by(username=username).first(): + return jsonify({'error': 'Username already exists'}), 400 + + if User.query.filter_by(email=email).first(): + return jsonify({'error': 'Email already exists'}), 400 + + # Hash password + password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + + # Create user + user = User(username=username, email=email, password_hash=password_hash) + db.session.add(user) + db.session.commit() + + access_token = create_access_token(identity=user.id) + + return jsonify({ + 'access_token': access_token, + 'user': { + 'id': user.id, + 'username': user.username, + 'email': user.email + } + }) + + except Exception as e: + logger.error(f"Registration error: {e}") + return jsonify({'error': 'Registration failed'}), 500 + +# Hunt Management Routes +@app.route('/api/hunts', methods=['GET']) +@jwt_required() +def get_hunts(): + try: + user_id = get_jwt_identity() + hunts = Hunt.query.filter_by(created_by=user_id).all() + + return jsonify({ + 'hunts': [{ + 'id': hunt.id, + 'name': hunt.name, + 'description': hunt.description, + 'created_at': hunt.created_at.isoformat(), + 'status': hunt.status + } for hunt in hunts] + }) + except Exception as e: + logger.error(f"Get hunts error: {e}") + return jsonify({'error': 'Failed to fetch hunts'}), 500 + +@app.route('/api/hunts', methods=['POST']) +@jwt_required() +def create_hunt(): + try: + user_id = get_jwt_identity() + data = request.get_json() + + hunt = Hunt( + name=data.get('name'), + description=data.get('description'), + created_by=user_id + ) + + db.session.add(hunt) + db.session.commit() + + return jsonify({ + 'hunt': { + 'id': hunt.id, + 'name': hunt.name, + 'description': hunt.description, + 'created_at': hunt.created_at.isoformat(), + 'status': hunt.status + } + }) + except Exception as e: + logger.error(f"Create hunt error: {e}") + return jsonify({'error': 'Failed to create hunt'}), 500 + # API Routes @app.route('/api/health') def health_check(): @@ -135,6 +283,10 @@ def get_stats(): return jsonify({'error': 'Failed to get stats'}), 500 # Static file serving for React app +@app.route("/assets/") +def send_assets(path): + return send_from_directory(os.path.join(app.static_folder, "assets"), path) + @app.route("/") def index(): if os.path.exists(os.path.join(app.static_folder, "index.html")): @@ -151,68 +303,6 @@ def index(): ] }) -if __name__ == "__main__": - print("=" * 50) - print("Starting Cyber Threat Hunter Backend...") - print("API available at: http://localhost:5000") - print("Health check: http://localhost:5000/api/health") - print("=" * 50) - app.run(host="0.0.0.0", port=5000, debug=True) - # Basic security tools detection - tools_found = [] - - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read().lower() - - # Common security tools to detect - security_tools = [ - 'windows defender', 'antimalware', 'avast', 'norton', 'mcafee', - 'crowdstrike', 'carbon black', 'sentinelone', 'cylance', - 'kaspersky', 'bitdefender', 'sophos', 'trend micro', - 'openvpn', 'nordvpn', 'expressvpn', 'cisco anyconnect' - ] - - for tool in security_tools: - if tool in content: - tools_found.append(tool) - - return jsonify({ - 'filename': filename, - 'tools_found': tools_found, - 'total_tools': len(tools_found) - }) - - except Exception as e: - logger.error(f"Security tools analysis error: {e}") - return jsonify({'error': 'Analysis failed'}), 500 - -@app.route('/api/stats') -def get_stats(): - try: - upload_dir = app.config['UPLOAD_FOLDER'] - files_count = len([f for f in os.listdir(upload_dir) if os.path.isfile(os.path.join(upload_dir, f))]) - - return jsonify({ - 'filesUploaded': files_count, - 'analysesCompleted': files_count, - 'threatsDetected': 0 - }) - except Exception as e: - logger.error(f"Stats error: {e}") - return jsonify({'error': 'Failed to get stats'}), 500 - -# Static file serving for React app -@app.route("/assets/") -def send_assets(path): - return send_from_directory(os.path.join(app.static_folder, "assets"), path) - -@app.route("/") -def index(): - if os.path.exists(os.path.join(app.static_folder, "index.html")): - return send_from_directory(app.static_folder, "index.html") - else: - return jsonify({'message': 'Cyber Threat Hunter API', 'status': 'running'}) - # Catch-all route for React Router @app.route("/") def catch_all(path): @@ -222,69 +312,12 @@ def catch_all(path): return jsonify({'error': 'Frontend not built yet'}) if __name__ == "__main__": + with app.app_context(): + db.create_all() + print("=" * 50) print("Starting Cyber Threat Hunter Backend...") print("API available at: http://localhost:5000") - print("Health check: http://localhost:5000/api/health") - print("Available endpoints:") - print(" POST /api/upload - Upload files") - print(" GET /api/files - List uploaded files") - print(" GET /api/analyze/ - Analyze file") - print(" GET /api/security-tools/ - Detect security tools") - print(" GET /api/stats - Get statistics") + print("Database: Connected to PostgreSQL") print("=" * 50) - app.run(host="0.0.0.0", port=5000, debug=True) - }) - except Exception as e: - analysis['csv_error'] = str(e) - elif filename.lower().endswith('.csv'): - # Basic CSV analysis without pandas - try: - with open(filepath, 'r', encoding='utf-8') as f: - lines = f.readlines() - analysis.update({ - 'rows': len(lines) - 1, # Subtract header - 'columns': len(lines[0].split(',')) if lines else 0, - 'preview': lines[:6] if lines else [] - }) - except Exception as e: - analysis['csv_error'] = str(e) - - return jsonify(analysis) - - except Exception as e: - logger.error(f"Analysis error: {e}") - return jsonify({'error': 'Analysis failed'}), 500 - -# Stats endpoint -@app.route('/api/stats') -def get_stats(): - try: - upload_dir = app.config['UPLOAD_FOLDER'] - files_count = len([f for f in os.listdir(upload_dir) if os.path.isfile(os.path.join(upload_dir, f))]) - - return jsonify({ - 'filesUploaded': files_count, - 'analysesCompleted': files_count, # Simplified - 'threatsDetected': 0 # Placeholder - }) - except Exception as e: - logger.error(f"Stats error: {e}") - return jsonify({'error': 'Failed to get stats'}), 500 - -# Static file serving -@app.route("/assets/") -def send_assets(path): - return send_from_directory(os.path.join(app.static_folder, "assets"), path) - -@app.route("/") -def index(): - return send_from_directory(app.static_folder, "index.html") - -# Catch-all route for React Router -@app.route("/") -def catch_all(path): - return send_from_directory(app.static_folder, "index.html") - -if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/backend/requirements.txt b/backend/requirements.txt index e69de29..94fab3a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -0,0 +1,9 @@ +flask==3.0.0 +flask-cors==4.0.0 +flask-sqlalchemy==3.1.1 +flask-jwt-extended==4.6.0 +psycopg2-binary==2.9.9 +python-dotenv==1.0.0 +requests==2.31.0 +werkzeug==3.0.1 +bcrypt==4.1.2 diff --git a/database/init.sql b/database/init.sql new file mode 100644 index 0000000..217df4d --- /dev/null +++ b/database/init.sql @@ -0,0 +1,52 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Users table +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP +); + +-- Hunts table +CREATE TABLE hunts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(100) NOT NULL, + description TEXT, + created_by UUID REFERENCES users(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'completed', 'archived')) +); + +-- Files table +CREATE TABLE files ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + hunt_id UUID REFERENCES hunts(id) ON DELETE CASCADE, + filename VARCHAR(255) NOT NULL, + original_filename VARCHAR(255) NOT NULL, + file_size BIGINT, + file_type VARCHAR(50), + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + analysis_status VARCHAR(20) DEFAULT 'pending' CHECK (analysis_status IN ('pending', 'processing', 'completed', 'failed')) +); + +-- Analysis results table +CREATE TABLE analysis_results ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + file_id UUID REFERENCES files(id) ON DELETE CASCADE, + analysis_type VARCHAR(50) NOT NULL, + results JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create indexes +CREATE INDEX idx_hunts_created_by ON hunts(created_by); +CREATE INDEX idx_files_hunt_id ON files(hunt_id); +CREATE INDEX idx_analysis_file_id ON analysis_results(file_id); + +-- Insert default admin user (password: admin123) +INSERT INTO users (username, email, password_hash) VALUES +('admin', 'admin@threathunter.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyIqhFrMpGLgGi'); diff --git a/deploy/aws-deploy.yml b/deploy/aws-deploy.yml new file mode 100644 index 0000000..4dc621e --- /dev/null +++ b/deploy/aws-deploy.yml @@ -0,0 +1,31 @@ +# AWS ECS Deployment Configuration +version: '3.8' + +services: + database: + image: postgres:15 + environment: + POSTGRES_DB: threat_hunter + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + + backend: + image: your-registry/threat-hunter-backend:latest + environment: + DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@database:5432/threat_hunter + SECRET_KEY: ${SECRET_KEY} + FLASK_ENV: production + depends_on: + - database + + frontend: + image: your-registry/threat-hunter-frontend:latest + ports: + - "80:3000" + depends_on: + - backend + +volumes: + postgres_data: diff --git a/deploy/backup/backup.sh b/deploy/backup/backup.sh new file mode 100644 index 0000000..97b1b98 --- /dev/null +++ b/deploy/backup/backup.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Database backup +BACKUP_DIR="/backups" +DATE=$(date +%Y%m%d_%H%M%S) + +echo "Creating database backup..." +docker exec threat-hunter-db pg_dump -U admin threat_hunter > "$BACKUP_DIR/db_backup_$DATE.sql" + +# File uploads backup +echo "Backing up uploads..." +tar -czf "$BACKUP_DIR/uploads_backup_$DATE.tar.gz" ./uploads + +# Keep only last 7 days of backups +find $BACKUP_DIR -name "*.sql" -mtime +7 -delete +find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete + +echo "Backup completed: $DATE" diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..2aaf0a6 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +echo "πŸš€ Deploying Cyber Threat Hunter..." + +# Build and push images +echo "πŸ“¦ Building Docker images..." +docker build -t your-registry/threat-hunter-backend:latest ./backend +docker build -t your-registry/threat-hunter-frontend:latest ./frontend + +echo "πŸ”„ Pushing to registry..." +docker push your-registry/threat-hunter-backend:latest +docker push your-registry/threat-hunter-frontend:latest + +# Deploy based on environment +if [ "$1" = "kubernetes" ]; then + echo "☸️ Deploying to Kubernetes..." + kubectl apply -f deploy/kubernetes/ +elif [ "$1" = "swarm" ]; then + echo "🐳 Deploying to Docker Swarm..." + docker stack deploy -c deploy/docker-stack.yml threat-hunter +else + echo "πŸ™ Deploying with Docker Compose..." + docker-compose -f docker-compose.prod.yml up -d +fi + +echo "βœ… Deployment complete!" diff --git a/deploy/docker-stack.yml b/deploy/docker-stack.yml new file mode 100644 index 0000000..4ccb672 --- /dev/null +++ b/deploy/docker-stack.yml @@ -0,0 +1,55 @@ +version: '3.8' + +services: + database: + image: postgres:15 + environment: + POSTGRES_DB: threat_hunter + POSTGRES_USER_FILE: /run/secrets/db_user + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + volumes: + - postgres_data:/var/lib/postgresql/data + secrets: + - db_user + - db_password + deploy: + replicas: 1 + placement: + constraints: + - node.role == manager + + backend: + image: your-registry/threat-hunter-backend:latest + environment: + DATABASE_URL: postgresql://admin:secure_password_123@database:5432/threat_hunter + SECRET_KEY_FILE: /run/secrets/secret_key + secrets: + - secret_key + deploy: + replicas: 3 + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure + + frontend: + image: your-registry/threat-hunter-frontend:latest + ports: + - "80:3000" + deploy: + replicas: 2 + update_config: + parallelism: 1 + delay: 10s + +volumes: + postgres_data: + +secrets: + db_user: + external: true + db_password: + external: true + secret_key: + external: true diff --git a/deploy/kubernetes/deployment.yaml b/deploy/kubernetes/deployment.yaml new file mode 100644 index 0000000..404d477 --- /dev/null +++ b/deploy/kubernetes/deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: threat-hunter-backend +spec: + replicas: 3 + selector: + matchLabels: + app: threat-hunter-backend + template: + metadata: + labels: + app: threat-hunter-backend + spec: + containers: + - name: backend + image: your-registry/threat-hunter-backend:latest + ports: + - containerPort: 5000 + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: threat-hunter-secrets + key: database-url +--- +apiVersion: v1 +kind: Service +metadata: + name: threat-hunter-backend-service +spec: + selector: + app: threat-hunter-backend + ports: + - port: 5000 + targetPort: 5000 + type: LoadBalancer diff --git a/deploy/monitoring/docker-compose.monitoring.yml b/deploy/monitoring/docker-compose.monitoring.yml new file mode 100644 index 0000000..38c3bee --- /dev/null +++ b/deploy/monitoring/docker-compose.monitoring.yml @@ -0,0 +1,21 @@ +version: '3.8' + +services: + prometheus: + image: prom/prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + + grafana: + image: grafana/grafana + ports: + - "3001:3000" + environment: + GF_SECURITY_ADMIN_PASSWORD: admin + volumes: + - grafana_data:/var/lib/grafana + +volumes: + grafana_data: diff --git a/deploy/security/security-checklist.md b/deploy/security/security-checklist.md new file mode 100644 index 0000000..0e61971 --- /dev/null +++ b/deploy/security/security-checklist.md @@ -0,0 +1,26 @@ +# Security Deployment Checklist + +## Pre-Deployment +- [ ] Change all default passwords +- [ ] Generate strong SECRET_KEY +- [ ] Setup SSL/TLS certificates +- [ ] Configure firewall rules +- [ ] Set up backup strategy + +## Database Security +- [ ] Use strong database passwords +- [ ] Enable database encryption +- [ ] Configure database firewall +- [ ] Set up regular backups + +## Application Security +- [ ] Update all dependencies +- [ ] Configure CORS properly +- [ ] Enable rate limiting +- [ ] Set up monitoring/logging + +## Infrastructure Security +- [ ] Use private networks +- [ ] Configure load balancer +- [ ] Set up intrusion detection +- [ ] Regular security updates diff --git a/deploy/setup-prod.bat b/deploy/setup-prod.bat new file mode 100644 index 0000000..661d8c1 --- /dev/null +++ b/deploy/setup-prod.bat @@ -0,0 +1,29 @@ +@echo off +echo Setting up production environment... + +REM Create environment file +echo Creating .env.prod file... +( +echo DB_USER=threat_hunter_user +echo DB_PASSWORD=%RANDOM%%RANDOM% +echo SECRET_KEY=%RANDOM%%RANDOM%%RANDOM% +echo FLASK_ENV=production +) > .env.prod + +REM Setup SSL certificates +echo Setting up SSL certificates... +mkdir ssl +REM Add your SSL certificate generation here + +REM Create backup directory +mkdir backups +mkdir logs + +REM Setup firewall rules +echo Configuring firewall... +netsh advfirewall firewall add rule name="HTTP" dir=in action=allow protocol=TCP localport=80 +netsh advfirewall firewall add rule name="HTTPS" dir=in action=allow protocol=TCP localport=443 + +echo Production setup complete! +echo Please update .env.prod with your actual values +pause diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..cc8a7cd --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,72 @@ +version: '3.8' + +services: + database: + image: postgres:15 + environment: + POSTGRES_DB: threat_hunter + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./backups:/backups + restart: unless-stopped + networks: + - internal + + backend: + build: + context: ./backend + dockerfile: Dockerfile.prod + environment: + DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@database:5432/threat_hunter + SECRET_KEY: ${SECRET_KEY} + FLASK_ENV: production + depends_on: + - database + volumes: + - ./uploads:/app/uploads + - ./logs:/app/logs + restart: unless-stopped + networks: + - internal + - web + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.prod + ports: + - "80:80" + - "443:443" + volumes: + - ./ssl:/etc/ssl/certs + depends_on: + - backend + restart: unless-stopped + networks: + - web + + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - ./ssl:/etc/ssl/certs + depends_on: + - frontend + - backend + restart: unless-stopped + networks: + - web + +volumes: + postgres_data: + +networks: + web: + external: true + internal: + internal: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..34946e8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +version: "3.8" + +services: + database: + image: postgres:15 + container_name: threat-hunter-db + environment: + POSTGRES_DB: threat_hunter + POSTGRES_USER: admin + POSTGRES_PASSWORD: secure_password_123 + volumes: + - postgres_data:/var/lib/postgresql/data + - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "5432:5432" + networks: + - threat-hunter-network + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: threat-hunter-backend + environment: + DATABASE_URL: postgresql://admin:secure_password_123@database:5432/threat_hunter + FLASK_ENV: production + SECRET_KEY: your-secret-key-change-in-production + depends_on: + - database + ports: + - "5000:5000" + volumes: + - ./uploads:/app/uploads + - ./output:/app/output + networks: + - threat-hunter-network + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: threat-hunter-frontend + ports: + - "3000:3000" + depends_on: + - backend + networks: + - threat-hunter-network + +volumes: + postgres_data: + +networks: + threat-hunter-network: + driver: bridge diff --git a/docker-start.bat b/docker-start.bat new file mode 100644 index 0000000..440568b --- /dev/null +++ b/docker-start.bat @@ -0,0 +1,20 @@ +@echo off +echo Starting Cyber Threat Hunter with Docker... + +docker-compose down +docker-compose build +docker-compose up -d + +echo. +echo ======================================== +echo Cyber Threat Hunter is starting... +echo Frontend: http://localhost:3000 +echo Backend API: http://localhost:5000 +echo Database: PostgreSQL on port 5432 +echo ======================================== +echo. +echo Default credentials: +echo Username: admin +echo Password: admin123 +echo. +pause diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..f3b0c21 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,20 @@ +FROM node:18-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +RUN npm install + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Serve with a simple server +RUN npm install -g serve + +EXPOSE 3000 + +CMD ["serve", "-s", "dist", "-l", "3000"] diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod new file mode 100644 index 0000000..ac8f0f1 --- /dev/null +++ b/frontend/Dockerfile.prod @@ -0,0 +1,19 @@ +# Build stage +FROM node:18-alpine as build + +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production + +COPY . . +RUN npm run build + +# Production stage +FROM nginx:alpine + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/nginx.conf + +EXPOSE 80 443 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 09ea4eb..fc2048e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ -import React, { Suspense } from "react"; -import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import React, { Suspense, useState, useEffect } from "react"; +import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom"; import { CssBaseline } from "@mui/material"; import { createTheme, ThemeProvider } from "@mui/material/styles"; @@ -12,6 +12,9 @@ import CSVProcessing from "./pages/CSVProcessing"; import SettingsConfig from "./pages/SettingsConfig"; import VirusTotal from "./pages/VirusTotal"; import SecurityTools from "./pages/SecurityTools"; +import LoginPage from "./pages/LoginPage"; +import Dashboard from "./pages/Dashboard"; +import HuntPage from "./pages/HuntPage"; const theme = createTheme({ palette: { @@ -20,6 +23,27 @@ const theme = createTheme({ }); function App() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const token = localStorage.getItem("token"); + if (token) { + // Verify token and get user info + // For now, just set a dummy user + setUser({ username: "User" }); + } + setLoading(false); + }, []); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + return ( @@ -29,7 +53,10 @@ function App() {
Loading...}> - } /> + : } /> + : } /> + : } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 0000000..c3e0ba5 --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,149 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Plus, FolderOpen, Calendar, User } from 'lucide-react' + +const Dashboard = ({ user }) => { + const [hunts, setHunts] = useState([]) + const [loading, setLoading] = useState(true) + const [showNewHunt, setShowNewHunt] = useState(false) + const [newHunt, setNewHunt] = useState({ name: '', description: '' }) + const navigate = useNavigate() + + useEffect(() => { + fetchHunts() + }, []) + + const fetchHunts = async () => { + try { + const token = localStorage.getItem('token') + const response = await fetch('/api/hunts', { + headers: { 'Authorization': `Bearer ${token}` } + }) + + if (response.ok) { + const data = await response.json() + setHunts(data.hunts) + } + } catch (err) { + console.error('Failed to fetch hunts:', err) + } + setLoading(false) + } + + const createHunt = async (e) => { + e.preventDefault() + try { + const token = localStorage.getItem('token') + const response = await fetch('/api/hunts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(newHunt) + }) + + if (response.ok) { + const data = await response.json() + navigate(`/hunt/${data.hunt.id}`) + } + } catch (err) { + console.error('Failed to create hunt:', err) + } + } + + if (loading) { + return
Loading...
+ } + + return ( +
+
+
+

Welcome back, {user.username}

+

Choose a hunt to continue or start a new investigation

+
+ +
+ + {showNewHunt && ( +
+

Create New Hunt

+
+ setNewHunt({...newHunt, name: e.target.value})} + className="w-full p-3 bg-zinc-700 rounded-lg border border-zinc-600 focus:border-cyan-400 outline-none" + required + /> +