diff --git a/Dockerfile b/Dockerfile index 5a2bde9..2b5f79c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,35 @@ # Use Python 3.10 slim image FROM python:3.10-slim -# Set environment variables -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 -ENV DEBIAN_FRONTEND noninteractive - # Install system dependencies RUN apt-get update && apt-get install -y \ gcc \ libpq-dev \ - docker.io \ - yara \ - libmagic1 \ && rm -rf /var/lib/apt/lists/* -# Set work directory +# Set working directory WORKDIR /app -# Install Python dependencies +# Copy requirements file COPY requirements.txt . + +# Install Python dependencies RUN pip install --no-cache-dir -r requirements.txt -# Copy project -COPY . . +# Copy project files +COPY . /app/ # Create necessary directories -RUN mkdir -p /app/logs /app/uploads /app/instance +RUN mkdir -p /app/static /app/media # Set permissions RUN chmod -R 755 /app -# Run gunicorn -CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--threads", "4", "app:app"] +# Collect static files +RUN python manage.py collectstatic --noinput + +# Expose port +EXPOSE 8000 + +# Command to run the application +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "ghostsec.wsgi:application"] diff --git a/docker-compose.yml b/docker-compose.yml index 4921c84..87398ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,52 +1,58 @@ -version: '3.8' - services: web: build: . - ports: - - "0.0.0.0:5000:5000" + command: gunicorn --bind 0.0.0.0:8000 ghostsec.wsgi:application + volumes: + - static_volume:/app/static + - media_volume:/app/media environment: - - FLASK_APP=app.py - - FLASK_ENV=production - - DATABASE_URL=postgresql://postgres:postgres@db:5432/ghostsec + - DEBUG=0 + - POSTGRES_DB=ghostsec + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_HOST=db + - POSTGRES_PORT=5432 - REDIS_URL=redis://redis:6379/0 - - HOST=0.0.0.0 - volumes: - - .:/app - - /var/run/docker.sock:/var/run/docker.sock depends_on: - db - redis + networks: + - ghostsec_network db: - image: postgres:15 + image: postgres:13 + volumes: + - postgres_data:/var/lib/postgresql/data environment: + - POSTGRES_DB=ghostsec - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=ghostsec - volumes: - - postgres_data:/var/lib/postgresql/data - ports: - - "5432:5432" + networks: + - ghostsec_network redis: - image: redis:7 - volumes: - - redis_data:/data - ports: - - "6379:6379" + image: redis:6 + networks: + - ghostsec_network nginx: - image: nginx:latest - ports: - - "0.0.0.0:80:80" - - "0.0.0.0:443:443" + image: nginx:1.19 volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - - ./ssl:/etc/nginx/ssl:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - static_volume:/app/static + - media_volume:/app/media + ports: + - "80:80" depends_on: - web + networks: + - ghostsec_network + +networks: + ghostsec_network: + driver: bridge volumes: postgres_data: - redis_data: + static_volume: + media_volume: diff --git a/ghostsec/auth/__init__.py b/ghostsec/ghostsec_auth/__init__.py similarity index 100% rename from ghostsec/auth/__init__.py rename to ghostsec/ghostsec_auth/__init__.py diff --git a/ghostsec/auth/forms.py b/ghostsec/ghostsec_auth/forms.py similarity index 100% rename from ghostsec/auth/forms.py rename to ghostsec/ghostsec_auth/forms.py diff --git a/ghostsec/auth/routes.py b/ghostsec/ghostsec_auth/routes.py similarity index 100% rename from ghostsec/auth/routes.py rename to ghostsec/ghostsec_auth/routes.py diff --git a/ghostsec/auth/templates/auth/login.html b/ghostsec/ghostsec_auth/templates/auth/login.html similarity index 100% rename from ghostsec/auth/templates/auth/login.html rename to ghostsec/ghostsec_auth/templates/auth/login.html diff --git a/ghostsec/auth/templates/auth/register.html b/ghostsec/ghostsec_auth/templates/auth/register.html similarity index 100% rename from ghostsec/auth/templates/auth/register.html rename to ghostsec/ghostsec_auth/templates/auth/register.html diff --git a/ghostsec/settings.py b/ghostsec/settings.py new file mode 100644 index 0000000..d313b2b --- /dev/null +++ b/ghostsec/settings.py @@ -0,0 +1,132 @@ +""" +Django settings for ghostsec project. +""" + +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-your-secret-key-here') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.environ.get('DEBUG', '0') == '1' + +ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost 127.0.0.1').split(' ') + +# Application definition +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'ghostsec_auth', + 'ctf', + 'forum', + 'learning', + 'learning_environments', + 'main', + 'marketplace', + 'news', + 'oauth', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'ghostsec.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'ghostsec.wsgi.application' + +# Database +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ.get('POSTGRES_DB', 'ghostsec'), + 'USER': os.environ.get('POSTGRES_USER', 'postgres'), + 'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'postgres'), + 'HOST': os.environ.get('POSTGRES_HOST', 'db'), + 'PORT': os.environ.get('POSTGRES_PORT', '5432'), + } +} + +# Cache +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': os.environ.get('REDIS_URL', 'redis://redis:6379/0'), + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } +} + +# Password validation +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') + +# Media files +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Security settings +if not DEBUG: + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + SECURE_SSL_REDIRECT = True + SESSION_COOKIE_SECURE = True + CSRF_COOKIE_SECURE = True + SECURE_BROWSER_XSS_FILTER = True + SECURE_CONTENT_TYPE_NOSNIFF = True + X_FRAME_OPTIONS = 'DENY' diff --git a/ghostsec/urls.py b/ghostsec/urls.py new file mode 100644 index 0000000..0f93b30 --- /dev/null +++ b/ghostsec/urls.py @@ -0,0 +1,12 @@ +"""ghostsec URL Configuration""" +from django.contrib import admin +from django.urls import path +from django.http import JsonResponse + +def health_check(request): + return JsonResponse({"status": "healthy"}) + +urlpatterns = [ + path('admin/', admin.site.urls), + path('health/', health_check, name='health_check'), +] diff --git a/ghostsec/wsgi.py b/ghostsec/wsgi.py new file mode 100644 index 0000000..41816dd --- /dev/null +++ b/ghostsec/wsgi.py @@ -0,0 +1,11 @@ +""" +WSGI config for ghostsec project. +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ghostsec.settings') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..f6ad2e2 --- /dev/null +++ b/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ghostsec.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/nginx.conf b/nginx.conf index b851db9..9199d74 100644 --- a/nginx.conf +++ b/nginx.conf @@ -40,46 +40,13 @@ http { gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; - # SSL configuration - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers on; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - ssl_session_timeout 1d; - ssl_session_cache shared:SSL:50m; - ssl_session_tickets off; - ssl_stapling on; - ssl_stapling_verify on; - resolver 8.8.8.8 8.8.4.4 valid=300s; - resolver_timeout 5s; - # Main server configuration server { listen 80; - listen [::]:80; - server_name _; - # For local network access - listen 0.0.0.0:80; - return 301 https://$host$request_uri; - } - - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - # For local network access - listen 0.0.0.0:443 ssl http2; server_name _; - - # SSL certificates - ssl_certificate /etc/nginx/ssl/cert.pem; - ssl_certificate_key /etc/nginx/ssl/key.pem; - - # Root directory and index files - root /app; - index index.html; - - # Proxy settings + location / { - proxy_pass http://web:5000; + proxy_pass http://web:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; @@ -92,22 +59,16 @@ http { proxy_read_timeout 86400; } - # Static files location /static/ { alias /app/static/; expires 30d; add_header Cache-Control "public, no-transform"; } - # Media files - location /uploads/ { - alias /app/uploads/; + location /media/ { + alias /app/media/; expires 30d; add_header Cache-Control "public, no-transform"; } - - # Error pages - error_page 404 /404.html; - error_page 500 502 503 504 /500.html; } } diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000..3751ab6 --- /dev/null +++ b/nginx/conf.d/default.conf @@ -0,0 +1,32 @@ +server { + listen 80; + server_name localhost; + client_max_body_size 100M; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + + location / { + proxy_pass http://web:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /static/ { + alias /app/static/; + expires 30d; + add_header Cache-Control "public, no-transform"; + } + + location /media/ { + alias /app/media/; + expires 30d; + add_header Cache-Control "public, no-transform"; + } +} diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..707d3c4 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,33 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name localhost; + client_max_body_size 100M; + + location / { + proxy_pass http://web:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location /static/ { + alias /app/static/; + } + + location /media/ { + alias /app/media/; + } + } +} diff --git a/requirements.txt b/requirements.txt index 3c39a4b..06a87d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,10 @@ -Flask==2.3.3 +Django==4.2.7 +psycopg2-binary==2.9.9 +redis==5.0.1 +django-redis==5.4.0 +gunicorn==21.2.0 +flask==2.3.3 +flask-restful==0.3.10 Flask-SQLAlchemy==3.1.1 Flask-Login==0.6.2 Flask-Mail==0.9.1 @@ -14,9 +20,6 @@ Werkzeug==2.3.7 Jinja2==3.1.2 itsdangerous==2.1.2 click==8.1.7 -redis==5.0.1 -psycopg2-binary==2.9.9 -gunicorn==21.2.0 eventlet==0.33.3 bcrypt==4.0.1 PyJWT==2.8.0