diff --git a/.gitignore b/.gitignore index 0848898..4254f23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ ### Custom Files ### backend/assets/bundle.js -backend/staticfiles/* +frontend/staticfiles/* +frontend/dist/ node_modules backend/*.env .idea diff --git a/API_TEST_GUIDE.md b/API_TEST_GUIDE.md new file mode 100644 index 0000000..7fecff2 --- /dev/null +++ b/API_TEST_GUIDE.md @@ -0,0 +1,88 @@ +# 🔧 Test-Anleitung: Richtiges API-Endpoint testen + +## Das Problem + +```bash +curl http://localhost:8005/api/ +# 404 Not Found HTML +``` + +Das ist **eigentlich OK** - `/api/` ist nur ein Prefix, kein echtes Endpoint! + +## Die Lösung + +Testen Sie mit einem echten API-Endpoint: + +```bash +# ✅ RICHTIG - Login Endpoint +curl -X POST http://localhost:8005/api/login \ + -H "Content-Type: application/json" \ + -d '{"username":"test","password":"test"}' + +# ✅ RICHTIG - User Data Endpoint +curl http://localhost:8005/api/me + +# ✅ RICHTIG - Projects Endpoint +curl http://localhost:8005/api/projects + +# ✅ RICHTIG - Health Check +curl http://localhost:8005/check_health +``` + +## Warum `/api/` allein 404 gibt + +In den URLs ist `/api/` nur ein Prefix: + +```python +path(webserver_path, include("webserver.urls")) +# webserver_path = "api/" +# → /api/ ist nur ein PREFIX! + +# Echte Endpoints sind: +# /api/login ← include("webserver.urls") → path("login") +# /api/me ← include("webserver.urls") → path("me") +# /api/projects ← include("webserver.urls") → path("projects") +``` + +## Richtige Test-Kommandos + +```bash +# Test 1: Login (POST) +curl -X POST http://localhost:8005/api/login \ + -H "Content-Type: application/json" \ + -d '{"username":"secure-user@acme.de","password":"secure"}' + +# Test 2: Authentifizierte Anfrage (braucht Cookie/Token) +curl -i http://localhost:8005/api/me + +# Test 3: Health Check (kein Auth nötig) +curl http://localhost:8005/check_health + +# Test 4: Projects (mit Auth) +curl http://localhost:8005/api/projects +``` + +## Success Criteria + +✅ Endpoints geben JSON zurück (nicht HTML!) +✅ 404 für `/api/` allein ist OK (es ist nur Prefix) +✅ 401 bei Auth-Endpoints ohne Auth ist OK +✅ 200 mit JSON für echte Requests + +## Überprüfung: Backend serviert KEINE HTML mehr + +```bash +# ❌ SCHLECHT: HTML Response + +Not Found +... + +# ✅ GUT: JSON Response +{"detail":"Authentication credentials were not provided."} + +# ✅ GUT: JSON Response +{"username":"...","email":"..."} +``` + +Testen Sie jetzt mit echten Endpoints! + diff --git "a/PROJEKT\303\234BERSICHT_3TIER.md" "b/PROJEKT\303\234BERSICHT_3TIER.md" new file mode 100644 index 0000000..37cafde --- /dev/null +++ "b/PROJEKT\303\234BERSICHT_3TIER.md" @@ -0,0 +1,203 @@ +# SecureCheckPlus Projekt-Übersicht & 3Tier-Refactoring + +## Projektstruktur + +``` +SecureCheckPlus.git/ +├── backend/ # Django Backend (3Tier) +│ ├── analyzer/ # Sicherheitsanalyse App +│ ├── webserver/ # Web API & User Management +│ ├── securecheckplus/ # Django Settings & URLs +│ ├── utilities/ # Shared Constants & Helpers +│ ├── templates/ # Email Templates +│ └── Dockerfile +│ +├── frontend/ # React TypeScript Frontend +│ └── src/ +│ ├── components/ +│ ├── page/ +│ ├── queries/ # API Client +│ └── style/ +│ +├── adapter/ # Zusätzlicher Adapter Service +│ +└── docker-compose-preview.yml # Testumgebung +``` + +## 3Tier-Architektur + +Die Anwendung folgt einer **3-Tier-Architektur**: + +### Tier 1: Presentation Layer (Frontend) +- **React + TypeScript** +- Container: `securecheckplus_frontend` +- Port: 3000 +- **Komponenten:** + - Login-Seite + - Project Dashboard + - Report Viewer + - CVSS Calculator + +### Tier 2: Application Layer (Backend) +- **Django 5.1.2** + REST Framework +- Container: `securecheckplus_server` +- Port: 8000 (intern) → 8005 (extern) +- **Apps:** + - `analyzer`: CVE-Analyse und Dependency-Management + - `webserver`: REST API, User & Project Management + - `utilities`: Gemeinsame Konstanten und Helper + +### Tier 3: Data Layer (Database) +- **PostgreSQL** +- Container: `securecheckplus_db` +- Port: 5432 + +## Hauptkomponenten + +### Backend: Analyzer App (`analyzer/`) +**Modelle:** +- `Project`: Verwaltete Projekte +- `Dependency`: Dependencies mit Versionierung +- `CVEObject`: CVE Informationen (CVSS, EPSS, etc.) +- `Report`: Verbindung zwischen Dependency und CVE mit Status + +**Eigenschaften:** +- CVE-Daten-Integration (NVD API) +- Risk-Score Berechnung basierend auf EPSS +- Dependency Tracking pro Projekt +- Status-Management (REVIEW, NO_THREAT, THREAT_FIXED, etc.) + +### Backend: Webserver App (`webserver/`) +**Modelle:** +- `User`: Authentifizierung & Benachrichtigungen +- `UserWatchProject`: Many-to-Many Beziehung User ↔ Project + +**Funktionalitäten:** +- REST API für Frontend +- User Management (Favoriten, Verlauf) +- LDAP Integration (optional) + +## Docker Compose Services + +### Development Setup (docker-compose-preview.yml) + +1. **securecheckplus_frontend** + - Build: `./frontend` (Dev-Modus) + - Environment: `REACT_APP_API_URL=http://localhost:8005` + - Exposes: Port 3000 + +2. **securecheckplus_server** + - Build: `./backend` (target: dev) + - Environment: Django Config (SECRET_KEY, DB-Credentials, etc.) + - Volumes: `./backend:/backend` (Hot-Reload) + - Exposes: Port 8005 → 8000 (intern) + - Dependencies: PostgreSQL, SMTP Mailserver + +3. **securecheckplus_db** + - Image: postgres (latest) + - Environment: DB Credentials + - Exposes: Port 5432 + +4. **smtp_mailserver** + - Image: maildev/maildev + - Purpose: Email Testing + - Exposes: Port 1080 (MailDev UI) + +## Häufige Kommandos + +### Backend starten/stoppen +```bash +# Mit Preview Compose +docker-compose -f docker-compose-preview.yml up --build + +# Mit Logs +docker-compose -f docker-compose-preview.yml logs -f securecheckplus_server + +# Backend-Shell betreten +docker exec -it securecheckplus_server sh +``` + +### Datenbank-Operationen +```bash +# In der Container-Shell: +python manage.py migrate +python manage.py makemigrations +python manage.py createsuperuser +python manage.py showmigrations +``` + +### Tests ausführen +```bash +# Im Backend Container +python manage.py test + +# Mit Coverage +pytest --cov=analyzer --cov=webserver +``` + +## Konfiguration + +### Umgebungsvariablen (backend) +- `IS_DEV`: Development-Modus (True/False) +- `FULLY_QUALIFIED_DOMAIN_NAME`: Frontend URL (für CORS) +- `DJANGO_SECRET_KEY`: Sicherheitsschlüssel +- `POSTGRES_*`: Datenbankzugangsdaten +- `NVD_API_KEY`: National Vulnerability Database API-Key +- `ADMIN_USERNAME/PASSWORD`: Superuser-Credentials +- `USER_USERNAME/PASSWORD`: Normaler User + +### LDAP Integration (Optional) +- `LDAP_HOST`: LDAP Server +- `LDAP_ADMIN_DN`: Admin Distinguished Name +- `LDAP_ADMIN_PASSWORD`: Admin Passwort +- `LDAP_USER_BASE_DN`: User Search Base +- `LDAP_ADMIN_GROUP_DN`: Admin Group DN +- etc. + +## Problembehebung + +### Issue: "Your models in app(s): 'analyzer' have changes" +**Lösung:** +```bash +python manage.py makemigrations +python manage.py migrate +``` + +### Issue: "The directory '/backend/assets' does not exist" +**Lösung:** +```bash +mkdir -p backend/assets +touch backend/assets/.gitkeep +``` + +### Issue: CSRF deactivated warnings +**Erklärung:** Normal in DEV-Modus, da HTTP verwendet wird. In PROD automatisch HTTPS erzwungen. + +## Migrationen Management + +### Migrationsdatei Struktur +- `analyzer/migrations/0001_initial.py`: Erste Migrationsdatei +- `analyzer/migrations/0002_initial.py`: ForeignKey & Relations Setup + +Beim Refactoring müssen neue Migrationen erstellt werden: +```bash +python manage.py makemigrations analyzer webserver +python manage.py migrate +``` + +## Best Practices für Entwicklung + +1. **Immer in der Container-Shell arbeiten** für Datenbank-Operationen +2. **Hot-Reload aktiviert**: Änderungen am Backend werden automatisch neu geladen +3. **Migrationen versionieren**: Nach Model-Änderungen `makemigrations` ausführen +4. **Environment-Variablen nutzen**: Für Dev/Prod-Unterschiede +5. **Tests schreiben**: Vor Production-Deployment testen + +## Nächste Schritte (3Tier-Refactoring) + +- [ ] Alle Migrationen generieren und testen +- [ ] API-Layer vollständig separieren +- [ ] Frontend-Komponenten refaktorieren +- [ ] Authentifizierung erweitern +- [ ] Performance-Testing durchführen + diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..186d782 --- /dev/null +++ b/QUICK_START.md @@ -0,0 +1,261 @@ +# 🚀 QUICK START: 3Tier Testing + +## Phase 1: Frontend Build (5 Min) + +```bash +cd frontend +npm install +npm run build +ls -la dist/ # Verify: index.html, app.js, login.js vorhanden +``` + +✅ **Erwartet:** `frontend/dist/` existiert mit Content + +--- + +## Phase 2: Docker Start (3 Min) + +```bash +# Alte Container stoppen +docker-compose -f docker-compose-preview.yml down + +# Neu starten mit Build +docker-compose -f docker-compose-preview.yml up --build -d + +# Logs anschauen +docker-compose -f docker-compose-preview.yml logs -f +``` + +✅ **Erwartet:** Alle Container starten ohne Fehler + +--- + +## Phase 3: Schnelle Tests (2 Min) + +### Frontend erreichbar? +```bash +curl -I http://localhost:3000/ +# Erwartet: HTTP/1.1 200 OK +``` + +### Backend API erreichbar? +```bash +curl -I http://localhost:8005/api/ +# Erwartet: HTTP/1.1 200 OK oder 401 (AUTH OK!) +``` + +### Backend serviert KEINE Assets? +```bash +curl -I http://localhost:8005/static/app.js +# Erwartet: HTTP/1.1 404 Not Found ✅ +``` + +### Frontend Assets vorhanden? +```bash +curl -I http://localhost:3000/app.js +# Erwartet: HTTP/1.1 200 OK +``` + +### Migrations OK? +```bash +docker logs securecheckplus_server | grep -i migration +# Erwartet: "Migrations wurden angewendet" oder "No migrations to apply" +``` + +--- + +## Phase 4: Browser Test (1 Min) + +``` +1. Browser: http://localhost:3000 +2. Login mit: + - Username: secure-user@acme.de + - Password: secure +3. Dashboard sollte laden +4. F12 → Network Tab → Überprüfe Requests + - HTML/JS/CSS von localhost:3000 ✅ + - API calls zu localhost:8005/api/ ✅ +``` + +--- + +## 🎯 Checkliste: Alles OK? + +- [ ] `frontend/dist/` existiert +- [ ] Frontend Container läuft +- [ ] Backend Container läuft +- [ ] Keine STATICFILES_DIRS Warnings +- [ ] Frontend erreichbar (Port 3000) +- [ ] Backend API erreichbar (Port 8005) +- [ ] Backend serviert KEINE Assets (404) +- [ ] Migrations angewendet +- [ ] Browser-Test erfolgreich +- [ ] Logs sauber (keine Errors) + +--- + +## ❌ Problem? Schnelle Fixes + +### Frontend nicht erreichbar (Port 3000) +```bash +docker logs securecheckplus_frontend +# → Nginx Fehler? Dockerfile OK? dist/ kopiert? +docker exec securecheckplus_frontend ls -la /usr/share/nginx/html/ +``` + +### Backend zeigt 404 für API +```bash +docker logs securecheckplus_server | tail -50 +# → Django Fehler? URLs OK? +docker exec securecheckplus_server python manage.py check +``` + +### Backend zeigt STATICFILES Warning +```bash +# Das sollte NICHT vorkommen (wir haben es entfernt) +# Falls trotzdem: settings.py überprüfen +docker exec securecheckplus_server grep STATICFILES_DIRS /backend/securecheckplus/settings.py +``` + +### Migrations fehlgeschlagen +```bash +docker exec securecheckplus_server python manage.py showmigrations +docker exec securecheckplus_server python manage.py makemigrations +docker exec securecheckplus_server python manage.py migrate +``` + +### Alles neu starten +```bash +docker-compose -f docker-compose-preview.yml down +docker system prune -f +docker-compose -f docker-compose-preview.yml up --build -d +``` + +--- + +## 📊 Expected Output + +### Docker Logs (gut) +``` +securecheckplus_server | Waiting for postgres... +securecheckplus_server | PostgreSQL started +securecheckplus_server | System check identified no issues. +securecheckplus_server | No migrations to apply. +securecheckplus_server | 0 static files copied +securecheckplus_server | Django version 5.1.2, running development server... +securecheckplus_frontend | nginx: master process started +``` + +### Docker Logs (gut - Migrations) +``` +securecheckplus_server | Migrations created: +securecheckplus_server | 0003_xyz.py (analyzer) +securecheckplus_server | Running migrations... +securecheckplus_server | Applying analyzer.0003_xyz... OK +``` + +### Docker Logs (NICHT OK - alte Fehler) +``` +❌ staticfiles.W004: The directory '/backend/assets' does not exist +❌ Your models in app(s): 'analyzer' have changes +❌ STATICFILES_DIRS = [...] +``` + +--- + +## 📋 Datei-Überprüfung + +```bash +# Frontend webpack Config korrekt? +cat frontend/webpack.common.js | grep -A2 "output:" + +# Frontend TypeScript Config korrekt? +cat frontend/tsconfig.json | grep outDir + +# Backend Settings korrekt? +cat backend/securecheckplus/settings.py | grep -A2 "Static files" + +# Backend URLs korrekt? +cat backend/securecheckplus/urls.py | grep -A5 "urlpatterns =" + +# .gitignore korrekt? +cat .gitignore | grep "frontend/dist" +``` + +--- + +## 📞 Support Commands + +```bash +# Status überprüfen +docker-compose -f docker-compose-preview.yml ps + +# Spezifischen Container anschauen +docker logs securecheckplus_server -n 100 # Letzte 100 Zeilen + +# In Container gehen +docker exec -it securecheckplus_server sh + +# Backend testen +docker exec securecheckplus_server python manage.py check + +# Frontend testen +docker exec securecheckplus_frontend nginx -t + +# DB verbindung testen +docker exec securecheckplus_db psql -U securecheckplus -d some-db-name -c "SELECT version();" + +# Alle Logs +docker-compose -f docker-compose-preview.yml logs --tail=200 +``` + +--- + +## ✅ Success Criteria + +``` +🟢 GRÜN (Alles OK): +✅ Frontend Port 3000 antwortet mit 200 +✅ Backend Port 8005 antwortet mit 200/401 +✅ Backend antwortet mit 404 auf /static/app.js +✅ Migrations wurden angewendet +✅ Keine STATICFILES_DIRS Warnings +✅ Browser-Test erfolgreich + +🔴 ROT (Nicht OK): +❌ Frontend nicht erreichbar +❌ Backend Errors in Logs +❌ STATICFILES_DIRS Warning vorhanden +❌ Browser zeigt 404er +❌ Migrations fehlgeschlagen +``` + +--- + +## 🎯 Nächste Schritte nach erfolgreichem Test + +```bash +# 1. Cleanup (optional) +rm -rf backend/assets/ + +# 2. Docker Cleanup +docker system prune -a + +# 3. Alle Änderungen committen +git add . +git commit -m "refactor: implement 3tier architecture with separate frontend/backend assets" + +# 4. Production Test +docker-compose -f docker-compose.yml build +docker-compose -f docker-compose.yml up -d + +# 5. Feiern! 🎉 +echo "3Tier Architektur erfolgreich implementiert!" +``` + +--- + +**Zeitschätzung für komplette Validierung: ~30 Minuten** + +Viel Erfolg! 🚀 + diff --git a/README-DEV-INSTALLATION.md b/README-DEV-INSTALLATION.md new file mode 100644 index 0000000..01888f0 --- /dev/null +++ b/README-DEV-INSTALLATION.md @@ -0,0 +1,25 @@ +
+ +
+ +This page describes how to install and run the application SecureCheckPlus by Accso locally +for further development and testing SecureCheckPlus or just to have a look at it and try it out. + +# Running the Application using docker-compose + +## Prerequisites + +Your development environment has to meet the following criteria: + +* You must have a local docker demon running. This is usually done by installing + [Docker Desktop](https://www.docker.com/products/docker-desktop/) under Windows and macOS or a + [native Docker daemon](https://docs.docker.com/get-started/get-docker/) under Linux. +* You must have [docker-compose](https://docs.docker.com/compose/install/) installed +* You must be able to start a Docker container in your local environment. +* You require to obtain a registration key from https://nvd.nist.gov/ if you don't have one already at hand. This is + necessary to download the vulnerability data from the NVD database. The registration key is free of charge. + +## Configuration + +You need to edit the docker-compose file [docker-compose-dev.yml](docker-compose-dev.yml) to set the +nvd registration key. To do so, set the environment variable `NVD_API_KEY` in the `backend` service to you diff --git a/README-INSTALLATION.md b/README-PROD-INSTALLATION.md similarity index 93% rename from README-INSTALLATION.md rename to README-PROD-INSTALLATION.md index b437d45..1aec5da 100644 --- a/README-INSTALLATION.md +++ b/README-PROD-INSTALLATION.md @@ -2,6 +2,8 @@ +This page describes how to install and run the application SecureCheckPlus by Accso in a production environment. + # Running the Application as Docker Container The application SecureCheckPlus is provided as Docker image at https://hub.docker.com/u/accso. The name of the image diff --git a/backend/.dockerignore b/backend/.dockerignore index 2fca5d5..933df71 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -1,3 +1,3 @@ *.env .coverage -staticfiles/* \ No newline at end of file +../frontend/staticfiles/* \ No newline at end of file diff --git a/backend/analyzer/management/commands/seed_preview_data.py b/backend/analyzer/management/commands/seed_preview_data.py new file mode 100644 index 0000000..7b6c8f9 --- /dev/null +++ b/backend/analyzer/management/commands/seed_preview_data.py @@ -0,0 +1,228 @@ +""" +Django management command: seed_preview_data +-------------------------------------------- +Creates a demo project "SecureCheckPlus" with realistic dependency and CVE +data so the preview environment shows a populated dashboard out of the box. + +The command is idempotent: running it multiple times is safe. +It is called automatically by entrypoint.sh when IS_DEV=True. +""" +import datetime + +from django.core.management.base import BaseCommand + +from analyzer.models import Project, Dependency, CVEObject, Report +from utilities.constants import ( + BaseSeverity, Status, Solution, Threshold, + AttackVector, AttackComplexity, PrivilegesRequired, + UserInteraction, ConfidentialityImpact, IntegrityImpact, + AvailabilityImpact, Scope, +) + + +# --------------------------------------------------------------------------- +# Static demo data +# --------------------------------------------------------------------------- + +DEMO_PROJECT = { + "project_id": "securecheckplus", + "project_name": "SecureCheckPlus", + "deployment_threshold": Threshold.MEDIUM.name, +} + +# (dependency_name, version, package_manager, license, path) +DEMO_DEPENDENCIES = [ + # --- Frontend: vulnerable packages (used in test fixtures) --- + ("bootstrap", "3.3.6", "javascript", "MIT", "frontend/node_modules/bootstrap"), + ("jquery", "1.11.1", "javascript", "MIT", "frontend/node_modules/jquery"), + ("axios", "0.26.0", "javascript", "MIT", "frontend/node_modules/axios"), + # --- Frontend: clean packages --- + ("react", "17.0.2", "javascript", "MIT", "frontend/node_modules/react"), + ("react-dom", "17.0.2", "javascript", "MIT", "frontend/node_modules/react-dom"), + ("react-query", "3.34.16","javascript", "MIT", "frontend/node_modules/react-query"), + ("react-router-dom", "6.2.2", "javascript", "MIT", "frontend/node_modules/react-router-dom"), + ("webpack", "5.70.0", "javascript", "MIT", "frontend/node_modules/webpack"), + # --- Backend: Python packages --- + ("Django", "5.1.2", "python", "BSD-3", "backend/requirements.txt"), + ("djangorestframework","3.15.2", "python", "BSD-3", "backend/requirements.txt"), + ("django-cors-headers","4.5.0", "python", "MIT", "backend/requirements.txt"), + ("requests", "2.32.3", "python", "Apache-2.0", "backend/requirements.txt"), + ("lxml", "5.3.0", "python", "BSD-3", "backend/requirements.txt"), + ("whitenoise", "6.7.0", "python", "MIT", "backend/requirements.txt"), +] + +# CVE data: (cve_id, base_severity, cvss, epss, description, +# attack_vector, attack_complexity, privileges_required, +# user_interaction, confidentiality, integrity, availability, scope, +# recommended_url, published) +DEMO_CVES = [ + # --- bootstrap 3.3.6 --- + ( + "CVE-2016-10735", BaseSeverity.MEDIUM.name, 6.1, 0.00384, + "In Bootstrap 3.x before 3.4.0, XSS is possible in the data-target attribute.", + AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value, + UserInteraction.Required.value, ConfidentialityImpact.LOW.value, + IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value, + "https://github.com/twbs/bootstrap/pull/27033", + datetime.datetime(2019, 1, 9, tzinfo=datetime.timezone.utc), + ), + ( + "CVE-2018-14040", BaseSeverity.MEDIUM.name, 6.1, 0.00461, + "In Bootstrap before 4.1.2, XSS is possible in the collapse data-parent attribute.", + AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value, + UserInteraction.Required.value, ConfidentialityImpact.LOW.value, + IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value, + "https://github.com/twbs/bootstrap/issues/26630", + datetime.datetime(2018, 7, 13, tzinfo=datetime.timezone.utc), + ), + ( + "CVE-2019-8331", BaseSeverity.MEDIUM.name, 6.1, 0.00512, + "In Bootstrap before 3.4.1 and 4.3.x before 4.3.1, XSS is possible in the " + "tooltip or popover data-template attribute.", + AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value, + UserInteraction.Required.value, ConfidentialityImpact.LOW.value, + IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value, + "https://blog.getbootstrap.com/2019/02/13/bootstrap-4-3-1-and-3-4-1/", + datetime.datetime(2019, 2, 20, tzinfo=datetime.timezone.utc), + ), + # --- jquery 1.11.1 --- + ( + "CVE-2015-9251", BaseSeverity.MEDIUM.name, 6.1, 0.00698, + "jQuery before 3.0.0 is vulnerable to Cross-site Scripting (XSS) attacks when a " + "cross-domain Ajax request is performed without the dataType option.", + AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value, + UserInteraction.Required.value, ConfidentialityImpact.LOW.value, + IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value, + "https://github.com/jquery/jquery/commit/753d591", + datetime.datetime(2018, 1, 18, tzinfo=datetime.timezone.utc), + ), + ( + "CVE-2019-11358", BaseSeverity.MEDIUM.name, 6.1, 0.01174, + "jQuery before 3.4.0 mishandles jQuery.extend(true, {}, ...) because of " + "Object.prototype pollution, allowing attackers to modify Object.prototype.", + AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value, + UserInteraction.Required.value, ConfidentialityImpact.LOW.value, + IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value, + "https://github.com/jquery/jquery/commit/753d591", + datetime.datetime(2019, 4, 20, tzinfo=datetime.timezone.utc), + ), + ( + "CVE-2020-11022", BaseSeverity.MEDIUM.name, 6.9, 0.01987, + "In jQuery versions greater than or equal to 1.2 and before 3.5.0, " + "passing HTML from untrusted sources to jQuery's DOM manipulation methods may execute untrusted code.", + AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value, + UserInteraction.Required.value, ConfidentialityImpact.LOW.value, + IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value, + "https://github.com/jquery/jquery/security/advisories/GHSA-gxr4-xjj5-5px2", + datetime.datetime(2020, 4, 29, tzinfo=datetime.timezone.utc), + ), + # --- axios 0.26.0 --- + ( + "CVE-2021-3749", BaseSeverity.HIGH.name, 7.5, 0.00433, + "axios before 0.21.2 is vulnerable to Regular Expression Denial of Service (ReDoS) " + "via the trim function.", + AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value, + UserInteraction.NONE.value, ConfidentialityImpact.NONE.value, + IntegrityImpact.NONE.value, AvailabilityImpact.HIGH.value, Scope.UNCHANGED.value, + "https://github.com/axios/axios/releases/tag/v0.21.2", + datetime.datetime(2021, 8, 31, tzinfo=datetime.timezone.utc), + ), +] + +# Maps cve_id → (dep_name, dep_version, status, solution, comment) +DEMO_REPORTS = [ + ("CVE-2016-10735", "bootstrap", "3.3.6", Status.THREAT.name, Solution.CHANGE_VERSION, "Upgrade to bootstrap >= 3.4.1."), + ("CVE-2018-14040", "bootstrap", "3.3.6", Status.REVIEW.name, Solution.NO_SOLUTION_NEEDED, ""), + ("CVE-2019-8331", "bootstrap", "3.3.6", Status.THREAT_FIXED.name, Solution.CHANGE_VERSION, "Fixed by upgrading to 4.x in next sprint."), + ("CVE-2015-9251", "jquery", "1.11.1", Status.THREAT.name, Solution.CHANGE_VERSION, "Upgrade to jquery >= 3.5.0."), + ("CVE-2019-11358", "jquery", "1.11.1", Status.REVIEW.name, Solution.NO_SOLUTION_NEEDED, ""), + ("CVE-2020-11022", "jquery", "1.11.1", Status.THREAT_WIP.name, Solution.CHANGE_VERSION, "Migration to jquery 3.x in progress."), + ("CVE-2021-3749", "axios", "0.26.0", Status.THREAT.name, Solution.CHANGE_VERSION, "Upgrade to axios >= 0.21.2."), +] + + +class Command(BaseCommand): + help = "Seeds the preview database with a demo 'SecureCheckPlus' project and sample vulnerability data." + + def handle(self, *args, **options): + self.stdout.write("Seeding preview data …") + + # --- Project --- + project, created = Project.objects.get_or_create( + project_id=DEMO_PROJECT["project_id"], + defaults={ + "project_name": DEMO_PROJECT["project_name"], + "deployment_threshold": DEMO_PROJECT["deployment_threshold"], + }, + ) + if created: + self.stdout.write(f" ✓ Created project '{project.project_id}'") + else: + self.stdout.write(f" · Project '{project.project_id}' already exists – skipping.") + + # --- Dependencies --- + dep_map: dict[tuple, Dependency] = {} + for dep_name, version, pkg_mgr, lic, path in DEMO_DEPENDENCIES: + dep, dep_created = Dependency.objects.get_or_create( + project=project, + dependency_name=dep_name, + version=version, + defaults={ + "package_manager": pkg_mgr, + "license": lic, + "path": path, + "in_use": True, + }, + ) + dep_map[(dep_name, version)] = dep + if dep_created: + self.stdout.write(f" ✓ Dependency {dep_name}@{version}") + + # --- CVE Objects --- + cve_map: dict[str, CVEObject] = {} + for (cve_id, severity, cvss, epss, desc, + av, ac, pr, ui, ci, ii, ai, scope, url, published) in DEMO_CVES: + cve, cve_created = CVEObject.objects.get_or_create( + cve_id=cve_id, + defaults={ + "base_severity": severity, + "cvss": cvss, + "epss": epss, + "description": desc, + "attack_vector": av, + "attack_complexity": ac, + "privileges_required": pr, + "user_interaction": ui, + "confidentiality_impact": ci, + "integrity_impact": ii, + "availability_impact": ai, + "scope": scope, + "recommended_url": url, + "published": published, + "updated": published, + }, + ) + cve_map[cve_id] = cve + if cve_created: + self.stdout.write(f" ✓ CVE {cve_id}") + + # --- Reports --- + for cve_id, dep_name, dep_version, status, solution, comment in DEMO_REPORTS: + dep = dep_map.get((dep_name, dep_version)) + cve = cve_map.get(cve_id) + if not dep or not cve: + continue + _, report_created = Report.objects.get_or_create( + dependency=dep, + cve_object=cve, + defaults={ + "status": status, + "solution": solution, + "comment": comment, + }, + ) + if report_created: + self.stdout.write(f" ✓ Report {dep_name}@{dep_version} → {cve_id} [{status}]") + + self.stdout.write(self.style.SUCCESS("Preview data seeding complete.")) + diff --git a/backend/assets/.gitkeep b/backend/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/assets/icons/GitHubIcon.svg b/backend/assets/icons/GitHubIcon.svg deleted file mode 100644 index 37fa923..0000000 --- a/backend/assets/icons/GitHubIcon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/assets/icons/arrowDoubleDownIcon.svg b/backend/assets/icons/arrowDoubleDownIcon.svg deleted file mode 100644 index 0c44194..0000000 --- a/backend/assets/icons/arrowDoubleDownIcon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/assets/icons/eye.svg b/backend/assets/icons/eye.svg deleted file mode 100644 index 8f2a6fd..0000000 --- a/backend/assets/icons/eye.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/assets/icons/eyeCrossed.svg b/backend/assets/icons/eyeCrossed.svg deleted file mode 100644 index 373b38a..0000000 --- a/backend/assets/icons/eyeCrossed.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/assets/icons/favicon.ico b/backend/assets/icons/favicon.ico deleted file mode 100644 index a45571e..0000000 Binary files a/backend/assets/icons/favicon.ico and /dev/null differ diff --git a/backend/assets/icons/flag_de.svg b/backend/assets/icons/flag_de.svg deleted file mode 100644 index 60e5efc..0000000 --- a/backend/assets/icons/flag_de.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/assets/icons/flag_gb.svg b/backend/assets/icons/flag_gb.svg deleted file mode 100644 index 82b1fad..0000000 --- a/backend/assets/icons/flag_gb.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/assets/images/SecureCheckPlusLogoHorizontal.png b/backend/assets/images/SecureCheckPlusLogoHorizontal.png deleted file mode 100644 index 253205c..0000000 Binary files a/backend/assets/images/SecureCheckPlusLogoHorizontal.png and /dev/null differ diff --git a/backend/assets/images/SecureCheckPlusLogoVertical.svg b/backend/assets/images/SecureCheckPlusLogoVertical.svg deleted file mode 100644 index 210dbe4..0000000 --- a/backend/assets/images/SecureCheckPlusLogoVertical.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - SecureCheckPlus - by Accso - - \ No newline at end of file diff --git a/backend/assets/images/SecureCheckPlusLogoVertical500x500.png b/backend/assets/images/SecureCheckPlusLogoVertical500x500.png deleted file mode 100644 index 3913dba..0000000 Binary files a/backend/assets/images/SecureCheckPlusLogoVertical500x500.png and /dev/null differ diff --git a/backend/assets/images/error404.gif b/backend/assets/images/error404.gif deleted file mode 100644 index cd58daf..0000000 Binary files a/backend/assets/images/error404.gif and /dev/null differ diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 3049d70..deb75bc 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -5,8 +5,15 @@ while ! nc -z $POSTGRES_HOST $POSTGRES_PORT; do sleep 1; done; echo "PostgreSQL started" python manage.py createcachetable rate_limit +python manage.py makemigrations python manage.py migrate +# Collect Django admin staticfiles (not frontend assets - those are served by Nginx in the frontend container) python manage.py collectstatic --no-input +# In dev/preview mode: seed a demo project so the dashboard is not empty on first start +if [ "$IS_DEV" = "True" ]; then + python manage.py seed_preview_data +fi + exec "$@" diff --git a/backend/securecheckplus/settings.py b/backend/securecheckplus/settings.py index 13df1d2..4ea95ae 100644 --- a/backend/securecheckplus/settings.py +++ b/backend/securecheckplus/settings.py @@ -214,8 +214,9 @@ def get_env_variable_or_shutdown_gracefully(var_name): # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ +# Note: In 3Tier architecture, static files are served by the frontend container (Nginx) +# The backend only serves Django admin staticfiles -STATICFILES_DIRS = [os.path.join(BASE_DIR, "assets")] STATIC_URL = f"/{BASE_URL}/static/" if BASE_URL else "/static/" STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") # Location where the staticfiles will be collected @@ -296,6 +297,9 @@ def format(self, record): CSRF_TRUSTED_ORIGINS = [ FULLY_QUALIFIED_DOMAIN_NAME, ] +# Required for cross-origin requests with credentials (cookies/session). +# Needed when REACT_APP_API_URL points directly to the backend port (preview setup). +CORS_ALLOW_CREDENTIALS = True SESSION_EXPIRE_AT_BROWSER_CLOSE = False SESSION_SAVE_EVERY_REQUEST = True diff --git a/backend/securecheckplus/urls.py b/backend/securecheckplus/urls.py index d34eacb..fd654d4 100644 --- a/backend/securecheckplus/urls.py +++ b/backend/securecheckplus/urls.py @@ -16,15 +16,22 @@ import os from django.conf import settings -from django.conf.urls.static import static -from django.contrib.staticfiles.urls import staticfiles_urlpatterns -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django.urls import path, include, re_path from django.views.generic.base import TemplateView from analyzer.views import AnalyzeReport, health_endpoint from securecheckplus.settings import BASE_URL -from webserver.views.misc_views import AppView, HtmlView + + +def api_404_view(request, exception=None): + """Return JSON 404 for API requests instead of HTML""" + return JsonResponse( + {"detail": "Not found."}, + status=404, + content_type="application/json" + ) + webserver_path = f"{BASE_URL}/api/" if BASE_URL else "api/" analyzer_path = f"{BASE_URL}/analyzer/api" if BASE_URL else "analyzer/api" @@ -35,13 +42,15 @@ path("check_health", health_endpoint), path(webserver_path, include("webserver.urls")), path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")), - re_path(rf'{base_url_pattern}html/(?P[-a-z_A-Z0-9]+)\.html$', HtmlView.as_view()), - re_path(rf'{base_url_pattern}(?:.*)/?$', AppView.as_view()), + # HTML views removed in 3Tier architecture - Frontend is now served by Nginx, not Django + # re_path(rf'{base_url_pattern}html/(?P[-a-z_A-Z0-9]+)\.html$', HtmlView.as_view()), + # re_path(rf'{base_url_pattern}(?:.*)/?$', AppView.as_view()), ] -# Serving the media files in development mode -if settings.IS_DEV: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -else: - urlpatterns += staticfiles_urlpatterns() +# Serving static files is removed in 3Tier architecture +# Frontend container (Nginx) serves all frontend assets +# Backend only serves Django admin static files via collectstatic +# (no additional static() URL patterns needed) + +handler404 = api_404_view + diff --git a/backend/webserver/views/misc_views.py b/backend/webserver/views/misc_views.py index bb71ea4..b761dcc 100644 --- a/backend/webserver/views/misc_views.py +++ b/backend/webserver/views/misc_views.py @@ -2,7 +2,6 @@ import os import traceback -from django.shortcuts import render from django.views import View from rest_framework.exceptions import APIException from rest_framework.permissions import IsAuthenticated @@ -21,51 +20,9 @@ logger = logging.getLogger(__name__) -class HtmlView(View): - - - def get(self, request, template_name): - - PREFIX = "/" - - if not IS_DEV and BASE_URL: - PREFIX = f"/{BASE_URL}/static/" - elif not IS_DEV and not BASE_URL: - PREFIX = "/static/" - - context = { - 'IS_DEV': IS_DEV, - 'BASE_URL': "/" + BASE_URL if BASE_URL else "", - 'PREFIX': PREFIX - } - - if request.user.is_authenticated: - return render(request, f"includes/{template_name}.html", context) - else: - return render(request, "login.html", context) - - -class AppView(View): - def get(self, request): - - PREFIX = "/" - - if not IS_DEV and BASE_URL: - PREFIX = f"/{BASE_URL}/static/" - elif not IS_DEV and not BASE_URL: - PREFIX = "/static/" - - context = { - 'IS_DEV': IS_DEV, - 'BASE_URL': BASE_URL, - 'PREFIX': PREFIX - } - - if request.user.is_authenticated: - return render(request, "app.html", context) - else: - return render(request, "login.html", context) - +# HtmlView and AppView have been removed as part of the 2-Tier → 3-Tier migration. +# The frontend is now served by the Nginx container (frontend/Dockerfile). +# Django exclusively provides REST endpoints. class DependenciesAPI(APIView): permission_classes = [IsAuthenticated] diff --git a/docker-compose-preview.yml b/docker-compose-preview.yml new file mode 100644 index 0000000..3037e1c --- /dev/null +++ b/docker-compose-preview.yml @@ -0,0 +1,66 @@ +services: + + securecheckplus_frontend: + container_name: securecheckplus_frontend + build: + context: ./frontend + args: + # Leave empty so the frontend uses the Nginx proxy (same origin, port 3000). + # Set to "http://localhost:8005" only if you want to bypass Nginx and call + # the backend directly (requires CORS_ALLOW_CREDENTIALS=True on the backend). + REACT_APP_API_URL: "" + ports: + - "3000:80" + depends_on: + - securecheckplus_server + + securecheckplus_server: + container_name: securecheckplus_server + user: "${RUNNER_UID}:${GID}" + build: + context: ./backend + target: dev + environment: + - IS_DEV=True + # The frontend is served on http://localhost:3000 in this compose preview setup. + # Set FULLY_QUALIFIED_DOMAIN_NAME to the frontend origin so Django's + # CORS_ALLOWED_ORIGINS and CSRF_TRUSTED_ORIGINS include the browser origin. + - FULLY_QUALIFIED_DOMAIN_NAME=http://localhost:3000 + - USER_USERNAME=secure-user@acme.de + - USER_PASSWORD=secure + - ADMIN_USERNAME=secure-admin@acme.de + - ADMIN_PASSWORD=secure + - POSTGRES_HOST=securecheckplus_db + - POSTGRES_USER=securecheckplus + - POSTGRES_DB=some-db-name + - POSTGRES_PORT=5432 + - POSTGRES_PASSWORD="some-secure-password" + - NVD_API_KEY="PLACEHOLDER_FOR_NVD_API_KEY" + - DJANGO_SECRET_KEY="SOME-RANDOM-KEY" + - SALT="SOME-RANDOM-SALT" + volumes: + - "./backend:/backend" + ports: + - "8005:8000" + depends_on: + - securecheckplus_db + - smtp_mailserver + + securecheckplus_db: + image: postgres + restart: always + container_name: securecheckplus_db + environment: + - POSTGRES_HOST=securecheckplus_db + - POSTGRES_USER=securecheckplus + - POSTGRES_DB=some-db-name + - POSTGRES_PORT=5432 + - POSTGRES_PASSWORD="some-secure-password" + ports: + - "5432:5432" + + smtp_mailserver: + image: maildev/maildev + container_name: mailserver + ports: + - "1080:80" diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..d374d3c --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,27 @@ +# Stage 1: Build +FROM node:18-alpine AS build +WORKDIR /app + +# Accept REACT_APP_API_URL as build arg and expose as env for webpack DefinePlugin +ARG REACT_APP_API_URL='' +ENV REACT_APP_API_URL=${REACT_APP_API_URL} + +COPY package.json yarn.lock* package-lock.json* ./ +RUN yarn install --frozen-lockfile || npm install +COPY . . +RUN yarn build || npm run build + +# Stage 2: Serve +FROM nginx:alpine +# Copy build output from Webpack (dist directory) +COPY --from=build /app/dist /usr/share/nginx/html + +# Also copy Django-managed staticfiles (icons, images, rest_framework) into the nginx html root +# The build context for this Dockerfile is the repo root, so backend/staticfiles is available. +COPY /staticfiles/icons /usr/share/nginx/html/static/icons +COPY /staticfiles/images /usr/share/nginx/html/static/images +COPY /staticfiles/rest_framework /usr/share/nginx/html/static/rest_framework +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..cd1ccad --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,37 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Login page – serve the dedicated login bundle + location = /login { + try_files /login.html =404; + } + location = /login.html { + # No caching so the browser always gets the latest login page + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # All authenticated app routes – serve the main app bundle (SPA fallback) + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to the backend service inside the docker network + location /api/ { + # Keep the /api prefix so Django routes to webserver.urls correctly. + proxy_pass http://securecheckplus_server:8000; + proxy_http_version 1.1; + 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; + proxy_buffering off; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/frontend/package.json b/frontend/package.json index c083bb8..1072372 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "prod": "webpack --config webpack.prod.js --mode production", - "dev": "webpack-dev-server --config webpack.dev.js --mode development" + "dev": "webpack-dev-server --config webpack.dev.js --mode development", + "build": "webpack --config webpack.prod.js --mode production" }, "repository": { "type": "git", diff --git a/frontend/src/components/LoginBox.tsx b/frontend/src/components/LoginBox.tsx index 06900ab..bac6f60 100644 --- a/frontend/src/components/LoginBox.tsx +++ b/frontend/src/components/LoginBox.tsx @@ -11,6 +11,7 @@ import Typography from "@mui/material/Typography"; import {localStorageItemKeys, urlAddress} from "../utilities/constants"; import ImageDropDown from "./ImageDropDown"; import {getSupportedLanguages} from "../utilities/supportedLanguages"; +import apiClient from "../queries/apiClient"; /** * The login box at the login page. Takes the user inputs and requests an authentication process @@ -27,46 +28,83 @@ const LoginBox: React.FunctionComponent = () => { let allLanguages = getSupportedLanguages(); let defaultLanguageIndex = allLanguages.abbreviations.indexOf(defaultLanguage ? defaultLanguage : ""); + // Debug: component mounted + console.debug && console.debug("LoginBox mounted", { usernameInit: username, passwordInit: password }); + /** * If submit button is pressed redirect request to AuthProvider. * Submits on enter or on click. * @param clicked * @param e */ - const onConfirm = (clicked: boolean = false, e?: React.KeyboardEvent): void => { + const onConfirm = async (clicked: boolean = false, e?: React.KeyboardEvent): Promise => { + console.debug("LoginBox.onConfirm called", { clicked, key: e?.key, username, password, stayLoggedIn }); // @ts-ignore handled by first statement - if ((e === undefined && clicked) || (e.key === "Enter")) { - if (username.includes("@")) { - login({ - username: username, - password: password, - keepMeLoggedIn: stayLoggedIn, - }).then(reloadPage).catch(() => { - notification.error(localization.notificationMessage.incorrectLogin) - }) + if ((e === undefined && clicked) || (e && e.key === "Enter")) { + // Previously we required an '@' in the username (email). Some deployments/users use plain + // usernames — allow any non-empty username so the request is triggered. Keep a minimal check. + if (username && username.trim().length > 0) { + // CSRF-Token vorab holen – Fehler werden ignoriert, + // da der Login-POST auch ohne vorheriges Cookie funktioniert. + // GET /api/login liefert 405 (nur POST erlaubt), setzt aber das csrftoken-Cookie. + try { + console.debug("Fetching CSRF token via GET api/login ..."); + await apiClient.get(urlAddress.api.login); + } catch (csrfErr: any) { + // 405 Method Not Allowed ist erwartet – csrftoken-Cookie wird trotzdem gesetzt. + console.debug("CSRF pre-fetch finished (status ignored):", csrfErr?.response?.status); + } + + try { + console.debug("Calling login() with", { username, stayLoggedIn }); + await login({ + username: username, + password: password, + keepMeLoggedIn: stayLoggedIn, + }); + console.debug("login() successful"); + reloadPage(); + } catch (err: any) { + console.error("Login failed:", err); + if (err?.response?.status === 401 || err?.response?.status === 403) { + notification.error(localization.notificationMessage.incorrectLogin); + } else { + notification.error(localization.notificationMessage.serverError || "Server error"); + } + } } else { + console.debug("username empty", username); notification.warn(localization.notificationMessage.usernameIsNotMail) } } } const reloadPage = () => { - window.location.reload(); + // Navigate to the main app (loads app.js bundle via nginx → index.html) + window.location.href = '/'; } - return onConfirm(false, e)} - > - - - + // New: submit handler for semantic form submit + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + console.debug('Login form submitted', { username, password, stayLoggedIn }); + // Call existing onConfirm to run the login flow (clicked=true -> triggers login attempt) + await onConfirm(true); + } + + return ( +
+ + + + {localization.loginPage.login} @@ -79,7 +117,7 @@ const LoginBox: React.FunctionComponent = () => { variant="filled" required value={username} - onChange={e => setUsername(e.target.value)}/> + onChange={(e: React.ChangeEvent) => setUsername(e.target.value)}/> {/* */} @@ -90,10 +128,11 @@ const LoginBox: React.FunctionComponent = () => { variant="filled" type = {visible ? "text" : "password"} required value={password} - onChange={e => setPassword(e.target.value)}/> - setVisible(!visible)}> - {visible ? - : } + onChange={(e: React.ChangeEvent) => setPassword(e.target.value)} + InputProps={{ style: { paddingRight: `${eyeIconSize + 24}px` } }} /> + setVisible(!visible)}> + {visible ? show + : hide} @@ -103,12 +142,14 @@ const LoginBox: React.FunctionComponent = () => { {language.loginPage.checkBoxLabel} + variant="contained" + type="submit" + startIcon={} + >{language.loginPage.buttonLabel} + +
+ ) } diff --git a/frontend/src/context/ApiClientProvider.tsx b/frontend/src/context/ApiClientProvider.tsx index 275a3fd..a172079 100644 --- a/frontend/src/context/ApiClientProvider.tsx +++ b/frontend/src/context/ApiClientProvider.tsx @@ -7,7 +7,19 @@ const ApiClientProvider: React.FC<{ children: React.ReactNode }> = ({ children } const [loading, setLoading] = React.useState(true); useEffect(() => { - apiClient.defaults.baseURL = location.protocol + '//' + location.host + "/" + (baseUrl ? `${baseUrl}/api/` : "api/"); + const envApiUrlRaw = process.env.REACT_APP_API_URL || ''; + // Normalize env var: remove any trailing /api or /api/ and ensure trailing slash + const envApiUrl = envApiUrlRaw.replace(/\/api\/?$/,'').trim(); + if (envApiUrl) { + apiClient.defaults.baseURL = envApiUrl.endsWith('/') ? envApiUrl : envApiUrl + '/'; + } else { + // Use origin (with optional baseUrl) but DO NOT include the 'api' segment here — + // endpoints defined in urlAddress already include the 'api/' prefix. + apiClient.defaults.baseURL = location.protocol + '//' + location.host + "/" + (baseUrl ? `${baseUrl}/` : ""); + } + // Expose for debugging in browser devtools + try { (window as any).__API_BASE__ = apiClient.defaults.baseURL } catch (e) { /* ignore in non-browser env */ } + console.debug && console.debug("ApiClientProvider: resolved api base url:", apiClient.defaults.baseURL); setLoading(false); }, [baseUrl]); diff --git a/frontend/src/context/UserContext.tsx b/frontend/src/context/UserContext.tsx index e9b301f..2da5746 100644 --- a/frontend/src/context/UserContext.tsx +++ b/frontend/src/context/UserContext.tsx @@ -14,16 +14,22 @@ function UserContextProvider({children}: { children: React.ReactNode }) { const [userGroups, setUserGroups] = React.useState([groups.basic.id]); const [username, setUsername] = React.useState(""); - const {data: userData, isError, isSuccess} = useQuery("userData", getUserData) + const {data: userData, error, isError, isSuccess} = useQuery("userData", getUserData) useEffect(() => { if (isSuccess) { setUsername(userData?.data.username); setUserGroups(userData?.data.groups); } else if (isError) { - notification.error(localization.notificationMessage.errorUserDataFetch); + // 401 = not authenticated → redirect to login page + const status = (error as any)?.response?.status; + if (status === 401 || status === 403) { + window.location.href = '/login'; + } else { + notification.error(localization.notificationMessage.errorUserDataFetch); + } } - }, [userData]) + }, [userData, isError]) /** * Checks if the user has at least one of the given group diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 0000000..52645fc --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,11 @@ + + + + + SecureCheckPlus + + +
+ + + diff --git a/frontend/src/login.html b/frontend/src/login.html new file mode 100644 index 0000000..c61b7f1 --- /dev/null +++ b/frontend/src/login.html @@ -0,0 +1,11 @@ + + + + + SecureCheckPlus – Login + + +
+ + + diff --git a/frontend/src/page/ReportOverview.tsx b/frontend/src/page/ReportOverview.tsx index 3f45dd9..4bee49c 100644 --- a/frontend/src/page/ReportOverview.tsx +++ b/frontend/src/page/ReportOverview.tsx @@ -219,7 +219,11 @@ const ReportOverview: React.FunctionComponent = () => { description: localization.ReportPage.toolTips.epss, renderCell: (params: GridRenderCellParams) => ( - {params.value === 0 ? "N/A" : (params.value * 100).toFixed(2) + "%"} + + {typeof params.value === "number" && params.value !== undefined && params.value !== 0 + ? (params.value * 100).toFixed(2) + "%" + : "N/A"} + ), valueGetter: params => params.row.cveObject.epss, diff --git a/frontend/src/queries/apiClient.ts b/frontend/src/queries/apiClient.ts new file mode 100644 index 0000000..0608336 --- /dev/null +++ b/frontend/src/queries/apiClient.ts @@ -0,0 +1,9 @@ +import axios from 'axios'; + +const apiClient = axios.create({ + withCredentials: true, // Wichtig für CSRF-Token-Cookie-Handling + xsrfCookieName: 'csrftoken', // Django's Standard-CSRF-Cookie-Name + xsrfHeaderName: 'X-CSRFToken', // Django's Standard-CSRF-Header-Name +}); + +export default apiClient; diff --git a/frontend/src/utilities/constants.tsx b/frontend/src/utilities/constants.tsx index 9aedcb9..0fa6e53 100644 --- a/frontend/src/utilities/constants.tsx +++ b/frontend/src/utilities/constants.tsx @@ -11,25 +11,25 @@ export const localStorageItemKeys = { export const urlAddress = { api: { - login: "login", - logout: "logout", - me: "me", - myFavorite: "myFavorites", - projectsFlat: "projectsFlat", - projects: "projects", - deleteProjects: "deleteProjects", - projectGroups: "projectGroups", - createProject: (projectId: string) => "projects/" + projectId, - project: (projectId: string) => "projects/" + projectId, - projectAPIKey: (projectId: string) => "projects/" + projectId + "/apiKey", - projectDependencies: (projectId: string) => "projects/" + projectId + "/dependencies", - projectReports: (projectId: string) => "projects/" + projectId + "/reports", - projectUpdateCVEs: (projectId: string) => "projects/" + projectId + "/updateCVE", + login: "api/login", + logout: "api/logout", + me: "api/me", + myFavorite: "api/myFavorites", + projectsFlat: "api/projectsFlat", + projects: "api/projects", + deleteProjects: "api/deleteProjects", + projectGroups: "api/projectGroups", + createProject: (projectId: string) => "api/projects/" + projectId, + project: (projectId: string) => "api/projects/" + projectId, + projectAPIKey: (projectId: string) => "api/projects/" + projectId + "/apiKey", + projectDependencies: (projectId: string) => "api/projects/" + projectId + "/dependencies", + projectReports: (projectId: string) => "api/projects/" + projectId + "/reports", + projectUpdateCVEs: (projectId: string) => "api/projects/" + projectId + "/updateCVE", report: (projectId: string, - reportId: string) => "projects/" + projectId + "/reports/" + reportId, - updateCVE: (cveId: string) => "cveObject/" + cveId + "/update", - updateAllCVEs: "cveObjects/update", - unknownPage: "error404" + reportId: string) => "api/projects/" + projectId + "/reports/" + reportId, + updateCVE: (cveId: string) => "api/cveObject/" + cveId + "/update", + updateAllCVEs: "api/cveObjects/update", + unknownPage: "api/error404" }, media: { rootUrlWithBase: "", // gets set by ConfigContext Provider diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 6cdd469..c51099d 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -19,7 +19,7 @@ "isolatedModules": true, "noEmit": false, "jsx": "react-jsx", - "outDir": "../backend/assets" + "outDir": "./dist" }, "include": [ "src", diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 26ea6d6..9aa4182 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -1,4 +1,6 @@ const path = require("path"); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const webpack = require('webpack'); module.exports = { entry: { app: "./src/App.tsx", @@ -43,8 +45,24 @@ module.exports = { }, output: { filename: "[name].js", - path: path.join(__dirname, "../backend/assets"), + path: path.join(__dirname, "./dist"), publicPath: '/' }, - + plugins: [ + // Main app bundle – served by nginx for all authenticated routes + new HtmlWebpackPlugin({ + template: './src/index.html', + filename: 'index.html', + chunks: ['app'], + }), + // Login bundle – served by nginx at /login + new HtmlWebpackPlugin({ + template: './src/login.html', + filename: 'login.html', + chunks: ['login'], + }), + new webpack.DefinePlugin({ + 'process.env.REACT_APP_API_URL': JSON.stringify(process.env.REACT_APP_API_URL || ''), + }) + ], }