From c3f7862f0ac0ebe308ecbfefc21838b41a512b1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:37:22 +0000 Subject: [PATCH 01/22] chore(deps): bump node from 26.8-alpine to 26.10-alpine in /src/frontend Bumps node from 26.8-alpine to 26.10-alpine. --- updated-dependencies: - dependency-name: node dependency-version: 26.10-alpine dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/frontend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index d79740b..edc137f 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -1,6 +1,6 @@ # Multi-stage build for frontend # Stage 1: Build stage -FROM node:26.8-alpine AS builder +FROM node:26.10-alpine AS builder ARG BACKEND_URL ENV VITE_BACKEND_URL=$BACKEND_URL From 61a6e6e987b5b5e6852be99a85b6651902c16a31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:37:29 +0000 Subject: [PATCH 02/22] chore(deps)(deps): bump numpy from 2.5.2 to 2.5.3 in /src/backend Bumps [numpy](https://github.com/numpy/numpy) from 2.5.2 to 2.5.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.5.2...v2.5.3) --- updated-dependencies: - dependency-name: numpy dependency-version: 2.5.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 40d5986..a93748d 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -38,7 +38,7 @@ clamd==1.0.2 # Data Processing pandas==3.0.5 -numpy==2.5.2 +numpy==2.5.3 # Rate Limiting slowapi==0.1.10 From b802ef8c4a9e03054556e9e14625ce2d1471546f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:37:33 +0000 Subject: [PATCH 03/22] chore(deps)(deps): bump pymysql from 1.2.0 to 1.2.3 in /src/backend Bumps [pymysql](https://github.com/PyMySQL/PyMySQL) from 1.2.0 to 1.2.3. - [Release notes](https://github.com/PyMySQL/PyMySQL/releases) - [Changelog](https://github.com/PyMySQL/PyMySQL/blob/main/CHANGELOG.md) - [Commits](https://github.com/PyMySQL/PyMySQL/compare/v1.2.0...v1.2.3) --- updated-dependencies: - dependency-name: pymysql dependency-version: 1.2.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 40d5986..b6beadc 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -9,7 +9,7 @@ pydantic[email]==2.13.5 # Database SQLAlchemy==2.0.52 -PyMySQL==1.2.0 +PyMySQL==1.2.3 # Security & Authentication bcrypt==5.0.0 python-jose[cryptography]==3.5.0 From c099ef989c7362dd4f3c01253b026721196bec97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:37:37 +0000 Subject: [PATCH 04/22] chore(deps)(deps): bump pypdf from 6.16.2 to 6.19.0 in /src/backend Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.16.2 to 6.19.0. - [Release notes](https://github.com/py-pdf/pypdf/releases) - [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md) - [Commits](https://github.com/py-pdf/pypdf/compare/6.16.2...6.19.0) --- updated-dependencies: - dependency-name: pypdf dependency-version: 6.19.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 40d5986..9880f44 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -19,7 +19,7 @@ psutil==7.2.2 # XML Processing (using secure library) lxml==6.1.3 defusedxml==0.7.1 -pypdf==6.16.2 +pypdf==6.19.0 # HTTP Client requests==2.34.2 From 2b2dfc91bcbb317b542d1def897b7b915bc7ae63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:37:46 +0000 Subject: [PATCH 05/22] chore(deps)(deps): update faker requirement in /src/backend Updates the requirements on [faker](https://github.com/joke2k/faker) to permit the latest version. - [Release notes](https://github.com/joke2k/faker/releases) - [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) - [Commits](https://github.com/joke2k/faker/compare/v40.37.0...v40.39.0) --- updated-dependencies: - dependency-name: faker dependency-version: 40.39.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/backend/requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/requirements-test.txt b/src/backend/requirements-test.txt index f5ae641..e0df662 100644 --- a/src/backend/requirements-test.txt +++ b/src/backend/requirements-test.txt @@ -2,4 +2,4 @@ pytest>=9.1.1 pytest-cov>=7.1.0 pytest-asyncio>=1.4.0 httpx>=0.28.1 -faker>=40.37.0 +faker>=40.39.0 From 19260f1359702479bb76065160ae53eca0606f73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:37:48 +0000 Subject: [PATCH 06/22] chore(deps)(deps): bump @primeuix/themes in /src/frontend/app Bumps [@primeuix/themes](https://github.com/primefaces/primeuix/tree/HEAD/packages/themes) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/primefaces/primeuix/releases) - [Commits](https://github.com/primefaces/primeuix/commits/HEAD/packages/themes) --- updated-dependencies: - dependency-name: "@primeuix/themes" dependency-version: 3.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/frontend/app/package-lock.json | 8 ++++---- src/frontend/app/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/frontend/app/package-lock.json b/src/frontend/app/package-lock.json index 738e9e1..05c59ca 100644 --- a/src/frontend/app/package-lock.json +++ b/src/frontend/app/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@mdi/font": "^7.4.47", - "@primeuix/themes": "^3.0.0", + "@primeuix/themes": "^3.0.1", "axios": "^1.20.0", "bootstrap": "^5.3.8", "bootstrap-vue": "^2.23.1", @@ -1271,9 +1271,9 @@ } }, "node_modules/@primeuix/themes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-3.0.0.tgz", - "integrity": "sha512-lNMOhorzu4vHRKbTDIU2/raS7/Z6Oyq2WXGfFDVhOwAIVDF4e7hCX75jlgfC7bo4D0Bl5f+oW5HPGZpG52r6lw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-3.0.1.tgz", + "integrity": "sha512-UmHzvA16DKb3dcE+2J8NbgK6eI5zFJQbCaNBW1YyY+ADZEVId4H275TPTcwlZ/8ywt0AFpunEPiyLQE1oub9pA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@primeuix/styled": "^1.0.0" diff --git a/src/frontend/app/package.json b/src/frontend/app/package.json index 7091a76..2408ab9 100644 --- a/src/frontend/app/package.json +++ b/src/frontend/app/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@mdi/font": "^7.4.47", - "@primeuix/themes": "^3.0.0", + "@primeuix/themes": "^3.0.1", "axios": "^1.20.0", "bootstrap": "^5.3.8", "bootstrap-vue": "^2.23.1", From 5fa204d86e4056cc31d177faf8d67243b5d1507a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:37:56 +0000 Subject: [PATCH 07/22] chore(deps)(deps): bump uvicorn from 0.52.4 to 0.54.0 in /src/backend Bumps [uvicorn](https://github.com/Kludex/uvicorn) from 0.52.4 to 0.54.0. - [Release notes](https://github.com/Kludex/uvicorn/releases) - [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md) - [Commits](https://github.com/Kludex/uvicorn/compare/0.52.4...0.54.0) --- updated-dependencies: - dependency-name: uvicorn dependency-version: 0.54.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 40d5986..6c4c7aa 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -2,7 +2,7 @@ ## Last updated: 2025-11-26 # Core Framework fastapi==0.141.1 -uvicorn[standard]==0.52.4 +uvicorn[standard]==0.54.0 pydantic==2.13.5 pydantic-settings==2.15.0 pydantic[email]==2.13.5 From 006936922468e370b6886649168a465aa0a3ba9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:38:12 +0000 Subject: [PATCH 08/22] chore(deps)(deps): update python-gvm requirement in /src/backend Updates the requirements on [python-gvm](https://github.com/greenbone/python-gvm) to permit the latest version. - [Release notes](https://github.com/greenbone/python-gvm/releases) - [Commits](https://github.com/greenbone/python-gvm/compare/v27.7.0...v27.10.0) --- updated-dependencies: - dependency-name: python-gvm dependency-version: 27.10.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 40d5986..6245e98 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -31,7 +31,7 @@ python-dotenv==1.2.3 ldap3==2.9.1 # Greenbone Management Protocol client -python-gvm>=27.7.0,<28.0 +python-gvm>=27.10.0,<28.0 # Antivirus clamd==1.0.2 From 37fed0640906ae76ae7349df1d4b8e8925cafbe1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:38:39 +0000 Subject: [PATCH 09/22] chore(deps)(deps-dev): bump sass in /src/frontend/app Bumps [sass](https://github.com/sass/dart-sass) from 1.104.0 to 1.105.0. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.104.0...1.105.0) --- updated-dependencies: - dependency-name: sass dependency-version: 1.105.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/frontend/app/package-lock.json | 8 ++++---- src/frontend/app/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/frontend/app/package-lock.json b/src/frontend/app/package-lock.json index 738e9e1..932e589 100644 --- a/src/frontend/app/package-lock.json +++ b/src/frontend/app/package-lock.json @@ -26,7 +26,7 @@ "@vitejs/plugin-vue": "^6.0.8", "@vue/test-utils": "^2.5.0", "jsdom": "^30.0.1", - "sass": "^1.104.0", + "sass": "^1.105.0", "vite-plugin-vue-devtools": "^8.2.1", "vite-plugin-vuetify": "^2.1.3", "vitest": "^4.1.11" @@ -4142,9 +4142,9 @@ } }, "node_modules/sass": { - "version": "1.104.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.104.0.tgz", - "integrity": "sha512-btHMApW2bgolvClhRW8AlQJzgI9lUB3pPSofBQQT+E46GWvf9o0TVQ13SYv5riWZVFyPN+JNz3TKW9XhBlc10w==", + "version": "1.105.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.105.0.tgz", + "integrity": "sha512-4EjSuRTyG8M4yRXXY1PT9OUV5zz23xBxJkbIAprohKHqf6HOU4MCpE3U3vAC+iO1gxobBjESNbcj8rYAQZWnyA==", "devOptional": true, "license": "MIT", "dependencies": { diff --git a/src/frontend/app/package.json b/src/frontend/app/package.json index 7091a76..2d57ef4 100644 --- a/src/frontend/app/package.json +++ b/src/frontend/app/package.json @@ -52,7 +52,7 @@ "@vitejs/plugin-vue": "^6.0.8", "@vue/test-utils": "^2.5.0", "jsdom": "^30.0.1", - "sass": "^1.104.0", + "sass": "^1.105.0", "vite-plugin-vue-devtools": "^8.2.1", "vite-plugin-vuetify": "^2.1.3", "vitest": "^4.1.11" From 73186b948abeed8c3b2cb7717b05c3bb4b47f456 Mon Sep 17 00:00:00 2001 From: maldwg Date: Fri, 2 Oct 2026 07:35:14 +0200 Subject: [PATCH 10/22] patch CSRF Token on production mode --- .../docker-compose.release.yml.template | 5 +- deploy/release/production.env.example | 1 + scripts/release/smoke-release.sh | 30 +++++++++ src/backend/main.py | 6 +- src/backend/middleware/csrf.py | 35 +++++++---- src/backend/tests/middleware/test_csrf.py | 49 +++++++++++++++ src/frontend/app/src/main.ts | 24 ++++++- .../app/src/services/__tests__/csrf.spec.ts | 43 +++++++++++++ src/frontend/app/src/services/csrf.ts | 62 +++++++++++++++++++ .../app/src/views/manage/ParserConfig.vue | 17 ++--- .../app/src/views/manage/Quarantine.vue | 9 +-- src/frontend/app/src/views/manage/Reports.vue | 9 +-- src/frontend/app/src/views/upload/Upload.vue | 11 ++-- 13 files changed, 263 insertions(+), 38 deletions(-) create mode 100644 src/frontend/app/src/services/__tests__/csrf.spec.ts create mode 100644 src/frontend/app/src/services/csrf.ts diff --git a/deploy/release/docker-compose.release.yml.template b/deploy/release/docker-compose.release.yml.template index a4e9aaf..002ff7f 100644 --- a/deploy/release/docker-compose.release.yml.template +++ b/deploy/release/docker-compose.release.yml.template @@ -24,7 +24,9 @@ services: backend: image: ghcr.io/bwinfosec/vulnerabilityhub-backend:__VERSION__@__BACKEND_DIGEST__ ports: - - "${BACKEND_PUBLISHED_PORT:-8000}:8000" + # The browser reaches the API through the frontend's /backend proxy. + # Keep the diagnostic host port local so internet probes cannot bypass it. + - "127.0.0.1:${BACKEND_PUBLISHED_PORT:-8000}:8000" volumes: - report_storage:/app/reports - upload_storage:/app/upload @@ -151,4 +153,3 @@ networks: driver: bridge backend-network: driver: bridge - diff --git a/deploy/release/production.env.example b/deploy/release/production.env.example index 8a7c13a..3008aff 100644 --- a/deploy/release/production.env.example +++ b/deploy/release/production.env.example @@ -2,6 +2,7 @@ # Public endpoints FRONTEND_PUBLISHED_PORT=80 +# The release Compose file binds this diagnostic port to 127.0.0.1 only. BACKEND_PUBLISHED_PORT=8000 FRONTEND_URL=https://vulnerabilityhub.example.com CORS_ORIGINS=https://vulnerabilityhub.example.com diff --git a/scripts/release/smoke-release.sh b/scripts/release/smoke-release.sh index a6cf301..d65e6b1 100755 --- a/scripts/release/smoke-release.sh +++ b/scripts/release/smoke-release.sh @@ -106,4 +106,34 @@ fi curl --fail --silent --show-error http://127.0.0.1:18000/health >/dev/null curl --fail --silent --show-error http://127.0.0.1:18080/ >/dev/null +# Exercise the production-only CSRF middleware through the same /backend proxy +# used by the browser. A valid token must reach routing (404 for this synthetic +# path), while an absent token must be a handled 403 rather than an ASGI 500. +csrf_headers="$smoke_dir/csrf-headers.txt" +csrf_cookies="$smoke_dir/csrf-cookies.txt" +curl --fail --silent --show-error \ + --dump-header "$csrf_headers" \ + --cookie-jar "$csrf_cookies" \ + http://127.0.0.1:18080/backend/users/has-admin >/dev/null +csrf_token="$(awk 'BEGIN { IGNORECASE=1 } /^X-CSRF-Token:/ { gsub("\\r", "", $2); print $2 }' "$csrf_headers")" + +if [ -z "$csrf_token" ]; then + echo "Release backend did not issue a CSRF token." >&2 + exit 1 +fi + +protected_status="$(curl --silent --output /dev/null --write-out '%{http_code}' \ + --request POST \ + --cookie "$csrf_cookies" \ + --header "X-CSRF-Token: $csrf_token" \ + http://127.0.0.1:18080/backend/__csrf-smoke)" +missing_status="$(curl --silent --output /dev/null --write-out '%{http_code}' \ + --request POST \ + http://127.0.0.1:18080/backend/__csrf-smoke)" + +if [ "$protected_status" != "404" ] || [ "$missing_status" != "403" ]; then + echo "Release CSRF smoke test failed (valid=$protected_status, missing=$missing_status)." >&2 + exit 1 +fi + echo "Release image smoke test passed." diff --git a/src/backend/main.py b/src/backend/main.py index 2008910..cc31e6f 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -84,7 +84,10 @@ app.add_middleware( CSRFProtectionMiddleware, secret_key=settings.SECRET_KEY, - secure_cookie=True, # Use secure cookies in production + # Release deployments may run directly over HTTP or terminate TLS at + # the public frontend. Match the cookie flag to the configured public + # URL so browsers do not silently discard it on HTTP installations. + secure_cookie=settings.FRONTEND_URL.lower().startswith("https://"), ) # Rate Limiting Middleware @@ -115,6 +118,7 @@ async def target_managed_network_exception_handler( allow_headers=["Content-Type", "Authorization", "X-CSRF-Token"], # Explicit headers expose_headers=[ "Content-Disposition", + "X-CSRF-Token", "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", diff --git a/src/backend/middleware/csrf.py b/src/backend/middleware/csrf.py index 5eb4ffa..a4a59a5 100644 --- a/src/backend/middleware/csrf.py +++ b/src/backend/middleware/csrf.py @@ -6,17 +6,17 @@ HTTP requests (POST, PUT, PATCH, DELETE). The middleware generates a cryptographically secure token and stores it in an -HttpOnly cookie. Clients must include this token in the X-CSRF-Token header -for protected requests. +HttpOnly cookie. Safe responses expose the same token in the X-CSRF-Token +header so clients can echo it on protected requests. """ import logging import secrets from typing import Callable, Set -from fastapi import Request, HTTPException, status +from fastapi import Request, status from starlette.middleware.base import BaseHTTPMiddleware -from starlette.responses import Response +from starlette.responses import JSONResponse, Response logger = logging.getLogger(__name__) @@ -104,8 +104,6 @@ async def dispatch(self, request: Request, call_next: Callable) -> Response: Returns: Response, potentially with a new CSRF token cookie - Raises: - HTTPException: If CSRF validation fails (403 Forbidden) """ # Skip CSRF protection for exempt paths if self._is_path_exempt(request.url.path): @@ -128,7 +126,11 @@ def _is_path_exempt(self, path: str) -> bool: Returns: True if the path is exempt, False otherwise """ - return any(path.startswith(exempt_path) for exempt_path in self.exempt_paths) + return any( + path == exempt_path + or path.startswith(f"{exempt_path.rstrip('/')}/") + for exempt_path in self.exempt_paths + ) async def _handle_safe_method( self, @@ -181,8 +183,6 @@ async def _handle_protected_method( Returns: Response if validation succeeds - Raises: - HTTPException: If CSRF token is missing or invalid (403 Forbidden) """ csrf_cookie = request.cookies.get(self.CSRF_COOKIE_NAME) csrf_header = request.headers.get(self.CSRF_HEADER_NAME) @@ -195,9 +195,14 @@ async def _handle_protected_method( request.url.path, request.client.host if request.client else "unknown" ) - raise HTTPException( + return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, - detail="CSRF token missing. Include X-CSRF-Token header with your request." + content={ + "detail": ( + "CSRF token missing. Include X-CSRF-Token header " + "with your request." + ) + }, ) # Validate token using constant-time comparison @@ -208,9 +213,13 @@ async def _handle_protected_method( request.url.path, request.client.host if request.client else "unknown" ) - raise HTTPException( + return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, - detail="CSRF token invalid. The token has expired or is incorrect." + content={ + "detail": ( + "CSRF token invalid. The token has expired or is incorrect." + ) + }, ) logger.debug("CSRF token validated successfully for %s", request.url.path) diff --git a/src/backend/tests/middleware/test_csrf.py b/src/backend/tests/middleware/test_csrf.py index b886b7e..8c82bba 100644 --- a/src/backend/tests/middleware/test_csrf.py +++ b/src/backend/tests/middleware/test_csrf.py @@ -35,3 +35,52 @@ def protected(): assert protected_response.status_code == 200 assert protected_response.json() == {"ok": True} + + +def test_missing_token_returns_403_instead_of_unhandled_500(): + app = FastAPI() + + @app.post("/protected") + def protected(): + return {"ok": True} + + app.add_middleware( + CSRFProtectionMiddleware, + secret_key="test-secret", + exempt_paths=set(), + secure_cookie=False, + ) + + with TestClient(app) as client: + response = client.post("/protected") + + assert response.status_code == 403 + assert response.json() == { + "detail": "CSRF token missing. Include X-CSRF-Token header with your request." + } + + +def test_exempt_paths_match_route_boundaries(): + app = FastAPI() + + @app.post("/metrics") + @app.post("/metrics/child") + @app.post("/metrics-other") + def endpoint(): + return {"ok": True} + + app.add_middleware( + CSRFProtectionMiddleware, + secret_key="test-secret", + exempt_paths={"/metrics"}, + secure_cookie=False, + ) + + with TestClient(app) as client: + exact_response = client.post("/metrics") + child_response = client.post("/metrics/child") + sibling_response = client.post("/metrics-other") + + assert exact_response.status_code == 200 + assert child_response.status_code == 200 + assert sibling_response.status_code == 403 diff --git a/src/frontend/app/src/main.ts b/src/frontend/app/src/main.ts index 2516621..e205c1b 100755 --- a/src/frontend/app/src/main.ts +++ b/src/frontend/app/src/main.ts @@ -3,6 +3,11 @@ import { createPinia } from 'pinia'; import axios, { AxiosHeaders } from 'axios'; // Import axios import type { AxiosError, AxiosRequestHeaders, InternalAxiosRequestConfig } from 'axios'; import { useAuthStore } from './stores/auth'; // Import useAuthStore +import { + ensureCsrfToken, + isCsrfProtectedMethod, + rememberCsrfToken, +} from './services/csrf'; // Vuetify import './styles/vuetify.scss' @@ -90,10 +95,24 @@ const setAuthorizationHeader = ( requestConfig.headers = headers as AxiosRequestHeaders; }; +const setCsrfHeader = ( + requestConfig: InternalAxiosRequestConfig, + token: string, +) => { + const headers = AxiosHeaders.from(requestConfig.headers); + headers.set('X-CSRF-Token', token); + requestConfig.headers = headers as AxiosRequestHeaders; +}; + // Axios Interceptor axios.interceptors.request.use(async (requestConfig) => { const authStore = useAuthStore(); + if (isCsrfProtectedMethod(requestConfig.method)) { + setCsrfHeader(requestConfig, await ensureCsrfToken()); + requestConfig.withCredentials = true; + } + if (isPublicAuthEndpoint(requestConfig.url)) { return requestConfig; } @@ -107,7 +126,10 @@ axios.interceptors.request.use(async (requestConfig) => { }); axios.interceptors.response.use( - (response) => response, + (response) => { + rememberCsrfToken(response.headers['x-csrf-token']); + return response; + }, async (error: AxiosError) => { const authStore = useAuthStore(); const originalRequest = error.config as RetriableRequestConfig | undefined; diff --git a/src/frontend/app/src/services/__tests__/csrf.spec.ts b/src/frontend/app/src/services/__tests__/csrf.spec.ts new file mode 100644 index 0000000..4343f30 --- /dev/null +++ b/src/frontend/app/src/services/__tests__/csrf.spec.ts @@ -0,0 +1,43 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +describe('CSRF request helpers', () => { + beforeEach(() => { + vi.resetModules(); + vi.restoreAllMocks(); + }); + + it('initializes a token and adds it to protected fetch requests', async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(null, { + status: 200, + headers: { 'X-CSRF-Token': 'test-token' }, + })) + .mockResolvedValueOnce(new Response(null, { status: 204 })); + vi.stubGlobal('fetch', fetchMock); + + const { csrfFetch } = await import('../csrf'); + await csrfFetch('/backend/protected', { method: 'POST' }); + + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'http://localhost:8000/users/has-admin', + expect.objectContaining({ credentials: 'include' }), + ); + + const protectedInit = fetchMock.mock.calls[1][1] as RequestInit; + expect(protectedInit.credentials).toBe('include'); + expect(new Headers(protectedInit.headers).get('X-CSRF-Token')).toBe('test-token'); + }); + + it('does not initialize or attach a token to safe requests', async () => { + const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 200 })); + vi.stubGlobal('fetch', fetchMock); + + const { csrfFetch } = await import('../csrf'); + await csrfFetch('/backend/safe'); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const requestInit = fetchMock.mock.calls[0][1] as RequestInit; + expect(new Headers(requestInit.headers).has('X-CSRF-Token')).toBe(false); + }); +}); diff --git a/src/frontend/app/src/services/csrf.ts b/src/frontend/app/src/services/csrf.ts new file mode 100644 index 0000000..831bb7f --- /dev/null +++ b/src/frontend/app/src/services/csrf.ts @@ -0,0 +1,62 @@ +import { getApiUrl } from '../config'; + +const CSRF_HEADER_NAME = 'X-CSRF-Token'; +const PROTECTED_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); + +let csrfToken: string | null = null; +let csrfTokenRequest: Promise | null = null; + +export const isCsrfProtectedMethod = (method?: string): boolean => + PROTECTED_METHODS.has((method || 'GET').toUpperCase()); + +export const rememberCsrfToken = (token?: string | null): void => { + if (token) { + csrfToken = token; + } +}; + +export const ensureCsrfToken = async (): Promise => { + if (csrfToken) { + return csrfToken; + } + + if (!csrfTokenRequest) { + csrfTokenRequest = window.fetch(getApiUrl('/users/has-admin'), { + credentials: 'include', + cache: 'no-store', + }).then((response) => { + if (!response.ok) { + throw new Error(`Could not initialize CSRF protection (${response.status})`); + } + + const token = response.headers.get(CSRF_HEADER_NAME); + if (!token) { + throw new Error('Backend response did not include a CSRF token'); + } + + csrfToken = token; + return token; + }).finally(() => { + csrfTokenRequest = null; + }); + } + + return csrfTokenRequest; +}; + +export const csrfFetch = async ( + input: RequestInfo | URL, + init: RequestInit = {}, +): Promise => { + const headers = new Headers(init.headers); + + if (isCsrfProtectedMethod(init.method)) { + headers.set(CSRF_HEADER_NAME, await ensureCsrfToken()); + } + + return window.fetch(input, { + ...init, + credentials: init.credentials ?? 'include', + headers, + }); +}; diff --git a/src/frontend/app/src/views/manage/ParserConfig.vue b/src/frontend/app/src/views/manage/ParserConfig.vue index 885efd2..b50f2d7 100644 --- a/src/frontend/app/src/views/manage/ParserConfig.vue +++ b/src/frontend/app/src/views/manage/ParserConfig.vue @@ -171,6 +171,7 @@ import { ref, onMounted, computed } from 'vue' import { config } from '../../config'; import { useAuthStore } from '../../stores/auth'; +import { csrfFetch } from '../../services/csrf'; interface RegexPath { type: 'regex'; @@ -331,7 +332,7 @@ onMounted(async () => { async function loadCurrentConfig() { try { - const res = await fetch(`${apiBase}/config`, { + const res = await csrfFetch(`${apiBase}/config`, { headers: { 'Authorization': `Bearer ${authStore.token}` }, }) if (!res.ok) throw new Error('Failed to load config') @@ -345,7 +346,7 @@ async function loadCurrentConfig() { async function saveConfig() { try { - const res = await fetch(`${apiBase}/config`, { + const res = await csrfFetch(`${apiBase}/config`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -435,7 +436,7 @@ function markDirty() { async function loadBackupList() { try { - const res = await fetch(`${apiBase}/backups`, { + const res = await csrfFetch(`${apiBase}/backups`, { headers: { 'Authorization': `Bearer ${authStore.token}` }, }) backups.value = await res.json() @@ -446,7 +447,7 @@ async function loadBackupList() { async function createBackup() { try { - const res = await fetch(`${apiBase}/backup`, { + const res = await csrfFetch(`${apiBase}/backup`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -467,7 +468,7 @@ async function createBackup() { async function restoreBackup(id: string) { if (!confirm('Are you sure you want to restore this backup?')) return try { - await fetch(`${apiBase}/restore-backup/${id}`, { + await csrfFetch(`${apiBase}/restore-backup/${id}`, { method: 'POST', headers: { 'Authorization': `Bearer ${authStore.token}` }, }) @@ -482,7 +483,7 @@ async function restoreBackup(id: string) { async function deleteBackup(id: string) { if (!confirm('Are you sure you want to delete this backup?')) return try { - await fetch(`${apiBase}/backup/${id}`, { + await csrfFetch(`${apiBase}/backup/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${authStore.token}` }, }) @@ -495,7 +496,7 @@ async function deleteBackup(id: string) { async function loadTemplateList() { try { - const res = await fetch(`${apiBase}/templates`, { + const res = await csrfFetch(`${apiBase}/templates`, { headers: { 'Authorization': `Bearer ${authStore.token}` }, }) templates.value = await res.json() @@ -505,7 +506,7 @@ async function loadTemplateList() { async function selectTemplate(template: Template) { try { - await fetch(`${apiBase}/load-template/${template.id}`, { + await csrfFetch(`${apiBase}/load-template/${template.id}`, { method: 'POST', headers: { 'Authorization': `Bearer ${authStore.token}` }, }) diff --git a/src/frontend/app/src/views/manage/Quarantine.vue b/src/frontend/app/src/views/manage/Quarantine.vue index 5e61b2a..782aa81 100644 --- a/src/frontend/app/src/views/manage/Quarantine.vue +++ b/src/frontend/app/src/views/manage/Quarantine.vue @@ -58,6 +58,7 @@ import { ref, onMounted } from 'vue'; import { config } from '../../config'; import { useAuthStore } from '../../stores/auth'; // Import useAuthStore +import { csrfFetch } from '../../services/csrf'; const backend_url = config.backendUrl; @@ -79,7 +80,7 @@ function formatDate(iso) { async function fetchQuarantine() { loading.value = true; try { - const r = await fetch(`${backend_url}/quarantine/files`, { + const r = await csrfFetch(`${backend_url}/quarantine/files`, { cache: 'no-store', headers: { 'Authorization': `Bearer ${authStore.token}`, @@ -97,7 +98,7 @@ async function fetchQuarantine() { async function downloadFile(id) { try { - const r = await fetch(`${backend_url}/quarantine/files/${id}/download`, { + const r = await csrfFetch(`${backend_url}/quarantine/files/${id}/download`, { headers: { 'Authorization': `Bearer ${authStore.token}`, }, @@ -119,7 +120,7 @@ async function downloadFile(id) { async function restoreFile(id) { try { - const r = await fetch(`${backend_url}/quarantine/files/${id}/restore`, { + const r = await csrfFetch(`${backend_url}/quarantine/files/${id}/restore`, { method: 'POST', headers: { 'Authorization': `Bearer ${authStore.token}`, @@ -138,7 +139,7 @@ async function restoreFile(id) { async function deleteFile(id) { if (!confirm('Really delete?')) return; try { - const r = await fetch(`${backend_url}/quarantine/files/${id}`, { + const r = await csrfFetch(`${backend_url}/quarantine/files/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${authStore.token}`, diff --git a/src/frontend/app/src/views/manage/Reports.vue b/src/frontend/app/src/views/manage/Reports.vue index d73fc91..a5014fd 100644 --- a/src/frontend/app/src/views/manage/Reports.vue +++ b/src/frontend/app/src/views/manage/Reports.vue @@ -221,6 +221,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useRoute } from 'vue-router' import { config } from '../../config' import { useAuthStore } from '../../stores/auth' +import { csrfFetch } from '../../services/csrf' interface ReportFile { scan_id: number @@ -286,7 +287,7 @@ async function loadReports() { search: searchTerm.value.trim(), formats: selectedTypes.value.join(','), }) - const response = await fetch(`${backendUrl}/scans/report-groups-page?${query}`, { + const response = await csrfFetch(`${backendUrl}/scans/report-groups-page?${query}`, { headers: authorizationHeaders(), signal: controller.signal, }) @@ -360,7 +361,7 @@ function individualFiles(group: ReportGroup) { async function downloadReport(reportFile: ReportFile) { downloadingScanIds.value = new Set(downloadingScanIds.value).add(reportFile.scan_id) try { - const response = await fetch( + const response = await csrfFetch( `${backendUrl}/scans/${reportFile.scan_id}/download`, { headers: authorizationHeaders() }, ) @@ -381,7 +382,7 @@ async function downloadReport(reportFile: ReportFile) { async function downloadSingleHostVisuals(group: ReportGroup) { downloadingGroupIds.value = new Set(downloadingGroupIds.value).add(group.group_id) try { - const response = await fetch( + const response = await csrfFetch( `${backendUrl}/scans/report-groups/${encodeURIComponent(group.group_id)}/single-host-visuals`, { headers: authorizationHeaders() }, ) @@ -410,7 +411,7 @@ async function deleteGroup(group: ReportGroup) { deletingGroupIds.value = new Set(deletingGroupIds.value).add(group.group_id) try { - const response = await fetch( + const response = await csrfFetch( `${backendUrl}/scans/report-groups/${encodeURIComponent(group.group_id)}`, { method: 'DELETE', diff --git a/src/frontend/app/src/views/upload/Upload.vue b/src/frontend/app/src/views/upload/Upload.vue index 24031d1..e242f3f 100644 --- a/src/frontend/app/src/views/upload/Upload.vue +++ b/src/frontend/app/src/views/upload/Upload.vue @@ -168,6 +168,7 @@ import { ref, onMounted } from 'vue' import { config } from '../../config' import { useAuthStore } from '../../stores/auth' // Import useAuthStore +import { csrfFetch } from '../../services/csrf' interface UploadResult { filename: string @@ -250,7 +251,7 @@ function getParserText(item: UploadResult) { async function refreshClamBadge() { try { - const r = await fetch(`${backend_url}/antivirus-status`, { + const r = await csrfFetch(`${backend_url}/antivirus-status`, { cache: 'no-store', headers: { 'Authorization': `Bearer ${authStore.token}`, @@ -318,7 +319,7 @@ async function confirmFilenamePatterns(files: File[]) { if (filesNeedingCheck.length === 0) return true try { - const response = await fetch(`${backend_url}/upload/filename-check`, { + const response = await csrfFetch(`${backend_url}/upload/filename-check`, { method: 'POST', headers: { 'Authorization': `Bearer ${authStore.token}`, @@ -370,7 +371,7 @@ async function uploadValidFiles(validFilesForUpload: File[]) { formData.append('file', file) // Changed from 'files' to 'file' try { - const response = await fetch(`${backend_url}/upload`, { + const response = await csrfFetch(`${backend_url}/upload`, { method: 'POST', body: formData, headers: { @@ -424,7 +425,7 @@ function pollJobStatus(job: UploadResult) { const pollFunction = async () => { try { - const response = await fetch(`${backend_url}/upload/status/${job.job_id}`, { + const response = await csrfFetch(`${backend_url}/upload/status/${job.job_id}`, { headers: { 'Authorization': `Bearer ${authStore.token}`, }, @@ -474,7 +475,7 @@ async function overwrite(index: number) { formData.append('file', item.file); try { - const response = await fetch(`${backend_url}/upload?override=true`, { + const response = await csrfFetch(`${backend_url}/upload?override=true`, { method: 'POST', body: formData, headers: { From 9e35194262258b2382a7e9f5b2d8000a6af67c40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:38:25 +0000 Subject: [PATCH 11/22] chore(deps)(deps): bump vue from 3.5.42 to 3.5.43 in /src/frontend/app Bumps [vue](https://github.com/vuejs/core) from 3.5.42 to 3.5.43. - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](https://github.com/vuejs/core/compare/v3.5.42...v3.5.43) --- updated-dependencies: - dependency-name: vue dependency-version: 3.5.43 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/frontend/app/package-lock.json | 118 ++++++++++++++--------------- src/frontend/app/package.json | 2 +- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/frontend/app/package-lock.json b/src/frontend/app/package-lock.json index 932e589..578021f 100644 --- a/src/frontend/app/package-lock.json +++ b/src/frontend/app/package-lock.json @@ -17,7 +17,7 @@ "lodash.debounce": "^4.0.8", "pinia": "^4.0.3", "primevue": "^5.0.1", - "vue": "^3.5.42", + "vue": "^3.5.43", "vue-router": "^5.3.1", "vuetify": "^3.13.3" }, @@ -1842,53 +1842,53 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz", - "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.43.tgz", + "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.8", - "@vue/shared": "3.5.42", + "@vue/shared": "3.5.43", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", - "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz", + "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.42", - "@vue/shared": "3.5.42" + "@vue/compiler-core": "3.5.43", + "@vue/shared": "3.5.43" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", - "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz", + "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.8", - "@vue/compiler-core": "3.5.42", - "@vue/compiler-dom": "3.5.42", - "@vue/compiler-ssr": "3.5.42", - "@vue/shared": "3.5.42", + "@vue/compiler-core": "3.5.43", + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-ssr": "3.5.43", + "@vue/shared": "3.5.43", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.19", + "postcss": "^8.5.28", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", - "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz", + "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.42", - "@vue/shared": "3.5.42" + "@vue/compiler-dom": "3.5.43", + "@vue/shared": "3.5.43" } }, "node_modules/@vue/devtools-api": { @@ -1939,51 +1939,51 @@ "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz", - "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.43.tgz", + "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.42" + "@vue/shared": "3.5.43" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz", - "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.43.tgz", + "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.42", - "@vue/shared": "3.5.42" + "@vue/reactivity": "3.5.43", + "@vue/shared": "3.5.43" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", - "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz", + "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.42", - "@vue/runtime-core": "3.5.42", - "@vue/shared": "3.5.42", + "@vue/reactivity": "3.5.43", + "@vue/runtime-core": "3.5.43", + "@vue/shared": "3.5.43", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz", - "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.43.tgz", + "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.42", - "@vue/runtime-dom": "3.5.42", - "@vue/shared": "3.5.42" + "@vue/compiler-ssr": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/shared": "3.5.43" } }, "node_modules/@vue/shared": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz", - "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.43.tgz", + "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==", "license": "MIT" }, "node_modules/@vue/test-utils": { @@ -3956,9 +3956,9 @@ } }, "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "funding": [ { "type": "opencollective", @@ -3975,7 +3975,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4789,16 +4789,16 @@ } }, "node_modules/vue": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz", - "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", + "version": "3.5.43", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.43.tgz", + "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.42", - "@vue/compiler-sfc": "3.5.42", - "@vue/runtime-dom": "3.5.42", - "@vue/server-renderer": "3.5.42", - "@vue/shared": "3.5.42" + "@vue/compiler-dom": "3.5.43", + "@vue/compiler-sfc": "3.5.43", + "@vue/runtime-dom": "3.5.43", + "@vue/server-renderer": "3.5.43", + "@vue/shared": "3.5.43" }, "peerDependencies": { "typescript": "*" diff --git a/src/frontend/app/package.json b/src/frontend/app/package.json index 2d57ef4..072fbd5 100644 --- a/src/frontend/app/package.json +++ b/src/frontend/app/package.json @@ -27,7 +27,7 @@ "lodash.debounce": "^4.0.8", "pinia": "^4.0.3", "primevue": "^5.0.1", - "vue": "^3.5.42", + "vue": "^3.5.43", "vue-router": "^5.3.1", "vuetify": "^3.13.3" }, From 39368f7568eeba66d94dc24514287f6e7caa7824 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:38:28 +0000 Subject: [PATCH 12/22] chore(deps)(deps-dev): bump @vue/test-utils in /src/frontend/app Bumps [@vue/test-utils](https://github.com/vuejs/test-utils) from 2.5.0 to 2.5.1. - [Release notes](https://github.com/vuejs/test-utils/releases) - [Commits](https://github.com/vuejs/test-utils/compare/v2.5.0...v2.5.1) --- updated-dependencies: - dependency-name: "@vue/test-utils" dependency-version: 2.5.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/frontend/app/package-lock.json | 8 ++++---- src/frontend/app/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/frontend/app/package-lock.json b/src/frontend/app/package-lock.json index 932e589..5dad1ef 100644 --- a/src/frontend/app/package-lock.json +++ b/src/frontend/app/package-lock.json @@ -24,7 +24,7 @@ "devDependencies": { "@playwright/test": "^1.62.1", "@vitejs/plugin-vue": "^6.0.8", - "@vue/test-utils": "^2.5.0", + "@vue/test-utils": "^2.5.1", "jsdom": "^30.0.1", "sass": "^1.105.0", "vite-plugin-vue-devtools": "^8.2.1", @@ -1987,9 +1987,9 @@ "license": "MIT" }, "node_modules/@vue/test-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.5.0.tgz", - "integrity": "sha512-6Clu5EKR/r6cDPYrKsu+8wenciWJJ3rhS9OEGsfDlZeZIhlJeEPGIZQHxE4lHRJCzPSq3EWMsFxQUqCvrbHQuQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.5.1.tgz", + "integrity": "sha512-V/4a99odJ7hapr0bRt35XxljGk8hefq31gSOnlKTcEq2fUeoj2qb+FcpJYfQu4qfYhW9Ug+O2+CZ3QmZ7gXDzA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/frontend/app/package.json b/src/frontend/app/package.json index 2d57ef4..a745dc9 100644 --- a/src/frontend/app/package.json +++ b/src/frontend/app/package.json @@ -50,7 +50,7 @@ "devDependencies": { "@playwright/test": "^1.62.1", "@vitejs/plugin-vue": "^6.0.8", - "@vue/test-utils": "^2.5.0", + "@vue/test-utils": "^2.5.1", "jsdom": "^30.0.1", "sass": "^1.105.0", "vite-plugin-vue-devtools": "^8.2.1", From 7e6c1ef7df6683969a9d4f935bb76f23e9af0eba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:38:30 +0000 Subject: [PATCH 13/22] chore(deps)(deps-dev): bump vitest in /src/frontend/app Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.11 to 5.0.2. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.2/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 5.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- src/frontend/app/package-lock.json | 292 ++++++++++++----------------- src/frontend/app/package.json | 2 +- 2 files changed, 125 insertions(+), 169 deletions(-) diff --git a/src/frontend/app/package-lock.json b/src/frontend/app/package-lock.json index 932e589..2e87421 100644 --- a/src/frontend/app/package-lock.json +++ b/src/frontend/app/package-lock.json @@ -29,7 +29,7 @@ "sass": "^1.105.0", "vite-plugin-vue-devtools": "^8.2.1", "vite-plugin-vuetify": "^2.1.3", - "vitest": "^4.1.11" + "vitest": "^5.0.2" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -722,6 +722,7 @@ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" @@ -733,6 +734,7 @@ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -743,6 +745,7 @@ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -789,7 +792,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -810,6 +815,7 @@ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@tybys/wasm-util": "^0.10.3" }, @@ -874,6 +880,7 @@ "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "devOptional": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/Boshen" } @@ -1324,6 +1331,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1340,6 +1348,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1356,6 +1365,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1372,6 +1382,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1388,6 +1399,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1407,6 +1419,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1426,6 +1439,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1445,6 +1459,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1464,6 +1479,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1483,6 +1499,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1502,6 +1519,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1518,6 +1536,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1531,6 +1550,7 @@ ], "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", @@ -1552,6 +1572,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1568,6 +1589,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1579,19 +1601,13 @@ "devOptional": true, "license": "MIT" }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1638,34 +1654,17 @@ "vue": "^3.2.25" } }, - "node_modules/@vitest/expect": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", - "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/mocker": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", - "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz", + "integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.11", + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.2", "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "magic-string": "^1.2.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1693,70 +1692,22 @@ "@types/estree": "^1.0.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", - "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", - "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.11", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", - "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", - "@vitest/utils": "4.1.11", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", - "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz", + "integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.11", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, "funding": { "url": "https://opencollective.com/vitest" } @@ -2701,9 +2652,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -2749,9 +2700,9 @@ "license": "MIT" }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2825,6 +2776,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -3265,6 +3217,7 @@ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "devOptional": true, "license": "MPL-2.0", + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -3301,6 +3254,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3321,6 +3275,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3341,6 +3296,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3361,6 +3317,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3381,6 +3338,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3404,6 +3362,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3427,6 +3386,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3450,6 +3410,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3473,6 +3434,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3493,6 +3455,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3513,6 +3476,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3762,15 +3726,18 @@ "license": "MIT" }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/ohash": { "version": "2.0.11", @@ -3850,9 +3817,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "license": "MIT", "engines": { "node": ">=12" @@ -4100,6 +4067,7 @@ "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" @@ -4192,11 +4160,6 @@ "node": ">=10" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, "node_modules/sirv": { "version": "3.0.2", "dev": true, @@ -4227,15 +4190,10 @@ "node": ">=0.10.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "dev": true, - "license": "MIT" - }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -4257,14 +4215,19 @@ "license": "MIT" }, "node_modules/tinybench": { - "version": "2.9.0", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/tinyexec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, "license": "MIT", "engines": { @@ -4287,16 +4250,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyrainbow": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/tldts": { "version": "7.4.10", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", @@ -4349,7 +4302,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD", - "optional": true + "optional": true, + "peer": true }, "node_modules/ufo": { "version": "1.6.4", @@ -4485,6 +4439,7 @@ "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -4699,38 +4654,31 @@ } }, "node_modules/vitest": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", - "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz", + "integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.11", - "@vitest/mocker": "4.1.11", - "@vitest/pretty-format": "4.1.11", - "@vitest/runner": "4.1.11", - "@vitest/snapshot": "4.1.11", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.2", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^3.2.1" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -4738,16 +4686,16 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.11", - "@vitest/browser-preview": "4.1.11", - "@vitest/browser-webdriverio": "4.1.11", - "@vitest/coverage-istanbul": "4.1.11", - "@vitest/coverage-v8": "4.1.11", - "@vitest/ui": "4.1.11", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.2", + "@vitest/browser-preview": "5.0.2", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.2", + "@vitest/coverage-v8": "5.0.2", + "@vitest/ui": "5.0.2", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -4788,6 +4736,16 @@ } } }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, "node_modules/vue": { "version": "3.5.42", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz", @@ -4941,18 +4899,16 @@ } }, "node_modules/why-is-node-running": { - "version": "2.3.0", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz", + "integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==", "dev": true, "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, "bin": { "why-is-node-running": "cli.js" }, "engines": { - "node": ">=8" + "node": ">=20.11" } }, "node_modules/wsl-utils": { diff --git a/src/frontend/app/package.json b/src/frontend/app/package.json index 2d57ef4..493c057 100644 --- a/src/frontend/app/package.json +++ b/src/frontend/app/package.json @@ -55,6 +55,6 @@ "sass": "^1.105.0", "vite-plugin-vue-devtools": "^8.2.1", "vite-plugin-vuetify": "^2.1.3", - "vitest": "^4.1.11" + "vitest": "^5.0.2" } } From e64c5b1c8aec1df216cf52a67bf786fa89c63213 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:38:38 +0000 Subject: [PATCH 14/22] chore(deps)(deps): bump vuetify in /src/frontend/app Bumps [vuetify](https://github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify) from 3.13.3 to 3.13.5. - [Release notes](https://github.com/vuetifyjs/vuetify/releases) - [Commits](https://github.com/vuetifyjs/vuetify/commits/v3.13.5/packages/vuetify) --- updated-dependencies: - dependency-name: vuetify dependency-version: 3.13.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/frontend/app/package-lock.json | 8 ++++---- src/frontend/app/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/frontend/app/package-lock.json b/src/frontend/app/package-lock.json index 932e589..41a9a1c 100644 --- a/src/frontend/app/package-lock.json +++ b/src/frontend/app/package-lock.json @@ -19,7 +19,7 @@ "primevue": "^5.0.1", "vue": "^3.5.42", "vue-router": "^5.3.1", - "vuetify": "^3.13.3" + "vuetify": "^3.13.5" }, "devDependencies": { "@playwright/test": "^1.62.1", @@ -4871,9 +4871,9 @@ } }, "node_modules/vuetify": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.13.3.tgz", - "integrity": "sha512-IFNGRqvL+UQOsAToiKwPjJ0FmMv06Kr6nFTsWkvCNm0mLTeld9EQSAUGOXf50R0OxPtocY5KwajNVdzp3tg2lg==", + "version": "3.13.5", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.13.5.tgz", + "integrity": "sha512-1PKESHKgyIj6BfjVbpF1bLWvKIBOvyDqIcP6C0R173fNZDr4jyXH7ssigAZvfqV5nRmVS/N9sGtY/IekGQ9TSQ==", "license": "MIT", "funding": { "type": "github", diff --git a/src/frontend/app/package.json b/src/frontend/app/package.json index 2d57ef4..0e01fba 100644 --- a/src/frontend/app/package.json +++ b/src/frontend/app/package.json @@ -29,7 +29,7 @@ "primevue": "^5.0.1", "vue": "^3.5.42", "vue-router": "^5.3.1", - "vuetify": "^3.13.3" + "vuetify": "^3.13.5" }, "@eslint/css": "^0.13.0", "@eslint/js": "^9.38.0", From 46a61a8ab5c26a0767eba2c7906babd4432be26a Mon Sep 17 00:00:00 2001 From: maldwg Date: Fri, 2 Oct 2026 07:47:42 +0200 Subject: [PATCH 15/22] update dependabot --- .github/dependabot.yml | 7 --- .github/workflows/dependabot-auto-merge.yml | 48 +++++++++++++++++++++ CONTRIBUTING.md | 12 ++++++ 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/dependabot-auto-merge.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index df71f9a..48855a1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,13 +33,6 @@ updates: commit-message: prefix: "chore(deps)" include: "scope" - ignore: - # Ignore major version updates for Vue and Vuetify to avoid breaking changes - - dependency-name: "vue" - update-types: ["version-update:semver-major"] - - dependency-name: "vuetify" - update-types: ["version-update:semver-major"] - # Docker base images - package-ecosystem: "docker" directory: "/src/backend" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..48aaf3e --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,48 @@ +name: Dependabot Auto-Merge + +on: + pull_request_target: + branches: [dev] + types: [opened, reopened, synchronize, ready_for_review, edited] + +permissions: {} + +concurrency: + group: dependabot-auto-merge-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + auto-merge: + name: Auto-merge minor and patch updates into dev + if: >- + github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.base.ref == 'dev' && + !github.event.pull_request.draft + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + pull-requests: write + steps: + # This privileged workflow never checks out or executes PR code. + # Default metadata verification requires Dependabot-authored commits. + - name: Fetch verified Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + # update-type is the highest version change in the PR, including groups. + # Major and unclassified updates remain available for manual review. + - name: Approve and enable auto-merge for minor and patch updates + if: >- + steps.metadata.outputs.update-type == 'version-update:semver-minor' || + steps.metadata.outputs.update-type == 'version-update:semver-patch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + gh pr review --approve "$PR_URL" + gh pr merge --auto --squash --match-head-commit "$PR_HEAD_SHA" "$PR_URL" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 333ceb8..a906c7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,18 @@ release notes whenever they change operator-visible behavior. promotions or urgent hotfixes. - Keep each pull request focused on one logical change. +Dependabot targets `dev` for dependency updates. Verified minor and patch +updates are automatically approved and set to auto-merge using squash merges, +subject to the branch's required checks and protection rules. Major updates +(including Vue and Vuetify) and updates without a recognized version change +remain open for manual review. + +For this automation, enable **Allow auto-merge** and squash merging in the +repository settings, and **Allow GitHub Actions to create and approve pull +requests** under Actions → General → Workflow permissions. Configure required +CI checks on `dev` to make auto-merge wait for tests. The workflow does not +bypass branch protection or organization policies. + Create a branch from the latest `dev` branch: ```bash From 0f1d6d9cc2fa3325e61012f8033987fe90fd20d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:51:12 +0000 Subject: [PATCH 16/22] chore(deps)(deps-dev): bump @vitejs/plugin-vue in /src/frontend/app Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 6.0.8 to 6.0.9. - [Release notes](https://github.com/vitejs/vite-plugin-vue/releases) - [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@6.0.9/packages/plugin-vue) --- updated-dependencies: - dependency-name: "@vitejs/plugin-vue" dependency-version: 6.0.9 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/frontend/app/package-lock.json | 8 ++++---- src/frontend/app/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/frontend/app/package-lock.json b/src/frontend/app/package-lock.json index 8a8113e..ccbea19 100644 --- a/src/frontend/app/package-lock.json +++ b/src/frontend/app/package-lock.json @@ -23,7 +23,7 @@ }, "devDependencies": { "@playwright/test": "^1.62.1", - "@vitejs/plugin-vue": "^6.0.8", + "@vitejs/plugin-vue": "^6.0.9", "@vue/test-utils": "^2.5.1", "jsdom": "^30.0.1", "sass": "^1.105.0", @@ -1622,9 +1622,9 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", - "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.9.tgz", + "integrity": "sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/frontend/app/package.json b/src/frontend/app/package.json index e25cf22..6e6867a 100644 --- a/src/frontend/app/package.json +++ b/src/frontend/app/package.json @@ -49,7 +49,7 @@ "vue-tsc": "^3.0.6", "devDependencies": { "@playwright/test": "^1.62.1", - "@vitejs/plugin-vue": "^6.0.8", + "@vitejs/plugin-vue": "^6.0.9", "@vue/test-utils": "^2.5.1", "jsdom": "^30.0.1", "sass": "^1.105.0", From 0d1820af4659ae29cd44222eba6eb2c68c0ee544 Mon Sep 17 00:00:00 2001 From: maldwg Date: Fri, 2 Oct 2026 07:52:35 +0200 Subject: [PATCH 17/22] factor in sucessful PRs for dependabot --- .github/workflows/dependabot-auto-merge.yml | 49 ++++++++++++++++++++- CONTRIBUTING.md | 13 +++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 48aaf3e..bcf8434 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -20,8 +20,9 @@ jobs: github.event.pull_request.base.ref == 'dev' && !github.event.pull_request.draft runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 65 permissions: + actions: read contents: write pull-requests: write steps: @@ -35,6 +36,52 @@ jobs: # update-type is the highest version change in the PR, including groups. # Major and unclassified updates remain available for manual review. + - name: Wait for successful PR pipelines on the exact head commit + if: >- + steps.metadata.outputs.update-type == 'version-update:semver-minor' || + steps.metadata.outputs.update-type == 'version-update:semver-patch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + REPOSITORY: ${{ github.repository }} + shell: bash + run: | + set -euo pipefail + deadline=$((SECONDS + 3600)) + + while (( SECONDS < deadline )); do + # Select the latest run of each required PR workflow. Push runs, + # earlier commits, and this automation's own run cannot satisfy it. + pipelines="$(gh api --paginate --slurp \ + "repos/$REPOSITORY/actions/runs?event=pull_request&head_sha=$PR_HEAD_SHA&per_page=100" \ + | jq --arg sha "$PR_HEAD_SHA" ' + [ .[].workflow_runs[] + | select(.event == "pull_request" and .head_sha == $sha) + | select(.path == ".github/workflows/ci.yml" or + .path == ".github/workflows/e2e.yml") ] + | group_by(.path) + | map(max_by(.id)) + ')" + + if jq -e 'any(.[]; .status == "completed" and .conclusion != "success")' \ + <<< "$pipelines" >/dev/null; then + echo "A PR pipeline failed, was cancelled, or was skipped; refusing auto-merge." + exit 1 + fi + + if jq -e 'length == 2 and all(.[]; .status == "completed" and .conclusion == "success")' \ + <<< "$pipelines" >/dev/null; then + echo "CI and E2E passed for $PR_HEAD_SHA." + exit 0 + fi + + echo "Waiting for both PR pipelines to succeed for $PR_HEAD_SHA." + sleep 20 + done + + echo "Timed out waiting for PR pipelines; refusing auto-merge." + exit 1 + - name: Approve and enable auto-merge for minor and patch updates if: >- steps.metadata.outputs.update-type == 'version-update:semver-minor' || diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a906c7e..cf58a8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,16 +24,19 @@ release notes whenever they change operator-visible behavior. - Keep each pull request focused on one logical change. Dependabot targets `dev` for dependency updates. Verified minor and patch -updates are automatically approved and set to auto-merge using squash merges, -subject to the branch's required checks and protection rules. Major updates +updates are automatically approved and set to auto-merge using squash merges +only after both CI Pipeline and E2E Tests succeed for the PR's exact head +commit. Missing, pending, failed, cancelled, or skipped pipelines cannot +authorize a merge. Branch protection rules still apply. Major updates (including Vue and Vuetify) and updates without a recognized version change remain open for manual review. For this automation, enable **Allow auto-merge** and squash merging in the repository settings, and **Allow GitHub Actions to create and approve pull -requests** under Actions → General → Workflow permissions. Configure required -CI checks on `dev` to make auto-merge wait for tests. The workflow does not -bypass branch protection or organization policies. +requests** under Actions → General → Workflow permissions. The explicit +pipeline gate applies even without required-check rules on `dev`; configuring +those rules also protects manual merges. The workflow does not bypass branch +protection or organization policies. Create a branch from the latest `dev` branch: From 864297457fc5cf520100cc8430256f0cbfcb3998 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:53:26 +0000 Subject: [PATCH 18/22] chore(deps)(deps): bump pandas from 3.0.5 to 3.0.6 in /src/backend Bumps [pandas](https://github.com/pandas-dev/pandas) from 3.0.5 to 3.0.6. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Commits](https://github.com/pandas-dev/pandas/compare/v3.0.5...v3.0.6) --- updated-dependencies: - dependency-name: pandas dependency-version: 3.0.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index c429639..1c145f7 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -37,7 +37,7 @@ python-gvm>=27.10.0,<28.0 clamd==1.0.2 # Data Processing -pandas==3.0.5 +pandas==3.0.6 numpy==2.5.3 # Rate Limiting From 42566e71e0bf919cc1481324b7b3c9b7d226b0ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:56:27 +0000 Subject: [PATCH 19/22] chore(deps)(deps): bump sqlalchemy from 2.0.52 to 2.1.1 in /src/backend Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.52 to 2.1.1. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-version: 2.1.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index c429639..f27bb76 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -8,7 +8,7 @@ pydantic-settings==2.15.0 pydantic[email]==2.13.5 # Database -SQLAlchemy==2.0.52 +SQLAlchemy==2.1.1 PyMySQL==1.2.3 # Security & Authentication bcrypt==5.0.0 From a0221bc9b6db9af38c8055421b53fca1a2dbfd4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:57:04 +0000 Subject: [PATCH 20/22] chore(deps)(deps-dev): bump @playwright/test in /src/frontend/app Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.62.1 to 1.63.0. - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.62.1...v1.63.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.63.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/frontend/app/package-lock.json | 42 +++++++++--------------------- src/frontend/app/package.json | 2 +- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/src/frontend/app/package-lock.json b/src/frontend/app/package-lock.json index ccbea19..8ca14c5 100644 --- a/src/frontend/app/package-lock.json +++ b/src/frontend/app/package-lock.json @@ -22,7 +22,7 @@ "vuetify": "^3.13.5" }, "devDependencies": { - "@playwright/test": "^1.62.1", + "@playwright/test": "^1.63.0", "@vitejs/plugin-vue": "^6.0.9", "@vue/test-utils": "^2.5.1", "jsdom": "^30.0.1", @@ -1175,13 +1175,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", - "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.62.1" + "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" @@ -3898,28 +3898,25 @@ } }, "node_modules/playwright": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", - "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.62.1" + "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", - "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3929,21 +3926,6 @@ "node": ">=20" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/popper.js": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", diff --git a/src/frontend/app/package.json b/src/frontend/app/package.json index 6e6867a..99015ad 100644 --- a/src/frontend/app/package.json +++ b/src/frontend/app/package.json @@ -48,7 +48,7 @@ "vitest": "^1.6.0", "vue-tsc": "^3.0.6", "devDependencies": { - "@playwright/test": "^1.62.1", + "@playwright/test": "^1.63.0", "@vitejs/plugin-vue": "^6.0.9", "@vue/test-utils": "^2.5.1", "jsdom": "^30.0.1", From 642df475e65ba5205371b38666b25793fc7abf81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:57:22 +0000 Subject: [PATCH 21/22] chore(deps)(deps-dev): bump jsdom in /src/frontend/app Bumps [jsdom](https://github.com/jsdom/jsdom) from 30.0.1 to 30.1.1. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](https://github.com/jsdom/jsdom/compare/v30.0.1...v30.1.1) --- updated-dependencies: - dependency-name: jsdom dependency-version: 30.1.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/frontend/app/package-lock.json | 150 ++++++++++++++--------------- src/frontend/app/package.json | 2 +- 2 files changed, 75 insertions(+), 77 deletions(-) diff --git a/src/frontend/app/package-lock.json b/src/frontend/app/package-lock.json index ccbea19..4d8d326 100644 --- a/src/frontend/app/package-lock.json +++ b/src/frontend/app/package-lock.json @@ -25,7 +25,7 @@ "@playwright/test": "^1.62.1", "@vitejs/plugin-vue": "^6.0.9", "@vue/test-utils": "^2.5.1", - "jsdom": "^30.0.1", + "jsdom": "^30.1.1", "sass": "^1.105.0", "vite-plugin-vue-devtools": "^8.2.1", "vite-plugin-vuetify": "^2.1.3", @@ -36,36 +36,36 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz", - "integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.2.tgz", + "integrity": "sha512-99DHAnXDB5z6EEK+9GMpVI7Mw4oxj97dY5bpOzMnjADQWxI8rN6TvTduuFLUhUMlS7/CfVZ06tcZsus6cltnNw==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.2.1", - "@csstools/css-color-parser": "^4.1.9", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.5.2" + "@csstools/css-calc": "^3.4.1", + "@csstools/css-color-parser": "^4.2.4", + "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-tokenizer": "^4.0.2", + "lru-cache": "^11.5.3" }, "engines": { - "node": "^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", - "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.2.tgz", + "integrity": "sha512-lSWTBMjAcmu2xn5yEDU7jh6QDV+C8GEKtdJ4pIQhXh26RkKQ7S3FuQlt+zZkUbTdJ4d3XyV1kPI/G0zWXxqaqw==", "dev": true, "license": "MIT", "dependencies": { - "bidi-js": "^1.0.3", + "bidi-js": "^1.1.0", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2" + "lru-cache": "^11.5.3" }, "engines": { - "node": "^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/@babel/code-frame": { @@ -577,9 +577,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", - "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", "dev": true, "funding": [ { @@ -597,9 +597,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.1.tgz", + "integrity": "sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==", "dev": true, "funding": [ { @@ -616,14 +616,14 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", - "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.4.tgz", + "integrity": "sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==", "dev": true, "funding": [ { @@ -637,21 +637,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.3.0" + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.1" }, "engines": { "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.1.tgz", + "integrity": "sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==", "dev": true, "funding": [ { @@ -668,13 +668,13 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", - "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", + "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", "dev": true, "funding": [ { @@ -697,9 +697,9 @@ } }, "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", "dev": true, "funding": [ { @@ -2163,9 +2163,9 @@ } }, "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -2968,16 +2968,16 @@ "license": "MIT" }, "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz", + "integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.6.0" + "@exodus/bytes": "^1.15.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" } }, "node_modules/https-proxy-agent": { @@ -3145,32 +3145,31 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", - "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.1.tgz", + "integrity": "sha512-FahmoPK5vbPc+jxV1iErMHmAZypCZ942NHF4+qqaWAuvaKKTBZxawnmAtrbGWLU7MtlxfqIP0qw6aSI+aWGtLg==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^6.0.5", - "@asamuzakjp/dom-selector": "^8.3.0", + "@asamuzakjp/css-color": "^7.0.0", + "@asamuzakjp/dom-selector": "^9.2.1", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@csstools/css-syntax-patches-for-csstree": "^1.1.13", "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", + "html-encoding-sniffer": "^7.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.2", - "undici": "^8.9.0", - "w3c-xmlserializer": "^5.0.0", + "undici": "^8.10.2", + "w3c-xmlserializer": "^6.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^17.1.0", + "whatwg-url": "^17.1.1", "xml-name-validator": "^5.0.0" }, "engines": { @@ -3209,9 +3208,9 @@ } }, "node_modules/jsdom/node_modules/whatwg-url": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", - "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.2.tgz", + "integrity": "sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3543,9 +3542,9 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -4251,11 +4250,6 @@ "node": ">=8" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "dev": true, - "license": "MIT" - }, "node_modules/tinybench": { "version": "2.9.0", "dev": true, @@ -4358,9 +4352,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", - "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", "dev": true, "license": "MIT", "engines": { @@ -4898,14 +4892,16 @@ } }, "node_modules/w3c-xmlserializer": { - "version": "5.0.0", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz", + "integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==", "dev": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/webidl-conversions": { @@ -4974,6 +4970,8 @@ }, "node_modules/xml-name-validator": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { diff --git a/src/frontend/app/package.json b/src/frontend/app/package.json index 6e6867a..99a2f3b 100644 --- a/src/frontend/app/package.json +++ b/src/frontend/app/package.json @@ -51,7 +51,7 @@ "@playwright/test": "^1.62.1", "@vitejs/plugin-vue": "^6.0.9", "@vue/test-utils": "^2.5.1", - "jsdom": "^30.0.1", + "jsdom": "^30.1.1", "sass": "^1.105.0", "vite-plugin-vue-devtools": "^8.2.1", "vite-plugin-vuetify": "^2.1.3", From 3b16425f18fbce0395ba712c5809c478840ecdb7 Mon Sep 17 00:00:00 2001 From: maldwg Date: Fri, 2 Oct 2026 08:40:58 +0200 Subject: [PATCH 22/22] fix e2e tests --- .github/workflows/ci.yml | 1 + .github/workflows/e2e.yml | 7 ++++ README.md | 6 ++++ docker-compose.e2e.yml | 3 ++ scripts/e2e/run-local.sh | 2 +- src/frontend/app/src/main.ts | 8 ++++- .../app/src/services/__tests__/csrf.spec.ts | 35 ++++++++++++++++++- src/frontend/app/src/services/csrf.ts | 15 ++++---- 8 files changed, 67 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4862ab..5a33e74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,7 @@ jobs: run: >- npm test -- --run src/services/__tests__/passwordPolicy.spec.ts + src/services/__tests__/csrf.spec.ts src/stores/__tests__/auth.spec.ts src/views/__tests__/Login.spec.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ea9d8ce..f3c60e4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -44,6 +44,13 @@ jobs: E2E_FRONTEND_SERVER: preview run: ./scripts/e2e/run-local.sh + - name: Run production CSRF auth smoke tests + env: + E2E_SKIP_CLEANUP: '1' + E2E_FRONTEND_SERVER: preview + E2E_ENV: production + run: ./scripts/e2e/run-local.sh --grep 'bootstraps the initial admin account|authenticates, updates the profile, and rotates the password' --output=test-results/production --reporter=line + - name: Upload Playwright artifacts if: always() uses: actions/upload-artifact@v7 diff --git a/README.md b/README.md index 37d1303..6e6a6b9 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,12 @@ Run it locally with: npm --prefix src/frontend/app run test:e2e:local ``` +The fresh-stack runner explicitly uses `ENV=development`, independent of your local `.env`. To verify bootstrap, profile updates, and password rotation with production CSRF protection enabled: + +```bash +E2E_ENV=production bash scripts/e2e/run-local.sh --grep 'bootstraps the initial admin account|authenticates, updates the profile, and rotates the password' +``` + To watch the browser while the suite runs, use: ```bash diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 2616fd8..4e00931 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -2,6 +2,9 @@ services: backend: command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] environment: + # Keep the dev smoke suite independent of a local release .env. + # Set E2E_ENV=production to exercise production CSRF protection. + - ENV=${E2E_ENV:-development} - CORS_ORIGINS=http://127.0.0.1:${E2E_FRONTEND_PORT:-4173},http://localhost:${E2E_FRONTEND_PORT:-4173} - FRONTEND_URL=http://127.0.0.1:${E2E_FRONTEND_PORT:-4173} - SMTP_SERVER=mailhog diff --git a/scripts/e2e/run-local.sh b/scripts/e2e/run-local.sh index 4b1072e..9d381b5 100755 --- a/scripts/e2e/run-local.sh +++ b/scripts/e2e/run-local.sh @@ -63,7 +63,7 @@ npm --prefix src/frontend/app exec playwright install chromium echo "[e2e] Resetting e2e stack" compose --profile dev down -v --remove-orphans -echo "[e2e] Starting backend services in dev mode" +echo "[e2e] Starting backend services in ${E2E_ENV:-development} mode" compose --profile dev up -d --build db clamav ldap backend mailhog "$ROOT_DIR/scripts/e2e/wait-for-url.sh" "http://127.0.0.1:${E2E_BACKEND_PORT}/health" 180 diff --git a/src/frontend/app/src/main.ts b/src/frontend/app/src/main.ts index e205c1b..cb82705 100755 --- a/src/frontend/app/src/main.ts +++ b/src/frontend/app/src/main.ts @@ -48,6 +48,9 @@ import { config } from './config'; // Axios Configuration axios.defaults.baseURL = config.backendUrl; +// Safe responses issue the CSRF cookie too. Include credentials on reads so +// any token remembered below has its matching cookie retained by the browser. +axios.defaults.withCredentials = true; const isPublicAuthEndpoint = (url?: string): boolean => { if (!url) { @@ -109,7 +112,10 @@ axios.interceptors.request.use(async (requestConfig) => { const authStore = useAuthStore(); if (isCsrfProtectedMethod(requestConfig.method)) { - setCsrfHeader(requestConfig, await ensureCsrfToken()); + const csrfToken = await ensureCsrfToken(); + if (csrfToken) { + setCsrfHeader(requestConfig, csrfToken); + } requestConfig.withCredentials = true; } diff --git a/src/frontend/app/src/services/__tests__/csrf.spec.ts b/src/frontend/app/src/services/__tests__/csrf.spec.ts index 4343f30..9dead9f 100644 --- a/src/frontend/app/src/services/__tests__/csrf.spec.ts +++ b/src/frontend/app/src/services/__tests__/csrf.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; describe('CSRF request helpers', () => { beforeEach(() => { @@ -6,6 +6,10 @@ describe('CSRF request helpers', () => { vi.restoreAllMocks(); }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + it('initializes a token and adds it to protected fetch requests', async () => { const fetchMock = vi.fn() .mockResolvedValueOnce(new Response(null, { @@ -40,4 +44,33 @@ describe('CSRF request helpers', () => { const requestInit = fetchMock.mock.calls[0][1] as RequestInit; expect(new Headers(requestInit.headers).has('X-CSRF-Token')).toBe(false); }); + + it('sends development mutations when the backend does not issue a token', async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response('{}', { status: 200 })) + .mockResolvedValueOnce(new Response(null, { status: 204 })); + vi.stubGlobal('fetch', fetchMock); + + const { csrfFetch } = await import('../csrf'); + const response = await csrfFetch('/backend/users/me/profile', { + method: 'PUT', + headers: { Authorization: 'Bearer test-token' }, + }); + + expect(response.status).toBe(204); + expect(fetchMock).toHaveBeenCalledTimes(2); + const headers = new Headers(fetchMock.mock.calls[1][1].headers); + expect(headers.has('X-CSRF-Token')).toBe(false); + expect(headers.get('Authorization')).toBe('Bearer test-token'); + }); + + it('does not send a mutation when token initialization fails', async () => { + const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 500 })); + vi.stubGlobal('fetch', fetchMock); + + const { csrfFetch } = await import('../csrf'); + await expect(csrfFetch('/backend/protected', { method: 'POST' })) + .rejects.toThrow('Could not initialize CSRF protection (500)'); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/frontend/app/src/services/csrf.ts b/src/frontend/app/src/services/csrf.ts index 831bb7f..14c3b08 100644 --- a/src/frontend/app/src/services/csrf.ts +++ b/src/frontend/app/src/services/csrf.ts @@ -4,7 +4,7 @@ const CSRF_HEADER_NAME = 'X-CSRF-Token'; const PROTECTED_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); let csrfToken: string | null = null; -let csrfTokenRequest: Promise | null = null; +let csrfTokenRequest: Promise | null = null; export const isCsrfProtectedMethod = (method?: string): boolean => PROTECTED_METHODS.has((method || 'GET').toUpperCase()); @@ -15,7 +15,7 @@ export const rememberCsrfToken = (token?: string | null): void => { } }; -export const ensureCsrfToken = async (): Promise => { +export const ensureCsrfToken = async (): Promise => { if (csrfToken) { return csrfToken; } @@ -30,10 +30,8 @@ export const ensureCsrfToken = async (): Promise => { } const token = response.headers.get(CSRF_HEADER_NAME); - if (!token) { - throw new Error('Backend response did not include a CSRF token'); - } - + // Development backends do not enable CSRF middleware and therefore + // return no token. Production enforcement remains on the server. csrfToken = token; return token; }).finally(() => { @@ -51,7 +49,10 @@ export const csrfFetch = async ( const headers = new Headers(init.headers); if (isCsrfProtectedMethod(init.method)) { - headers.set(CSRF_HEADER_NAME, await ensureCsrfToken()); + const token = await ensureCsrfToken(); + if (token) { + headers.set(CSRF_HEADER_NAME, token); + } } return window.fetch(input, {