From d2637ee0a06c00465c7c1c6ae4fcd424a2d82469 Mon Sep 17 00:00:00 2001 From: Anshul Jain Date: Mon, 6 Jul 2026 09:17:17 +0530 Subject: [PATCH 1/2] fix(docker): configure CORS for frontend in docker-compose Issue #1622: the docker-compose configuration was missing CORS setup for the frontend service. Frontend runs on port 5173, API on port 8081, both accessible via multiple origins (localhost, 127.0.0.1, and the Docker service name 'frontend'). Without explicit CORS_ALLOWED_ORIGINS, the backend's CORS middleware would reject requests from the frontend, blocking API calls. Fix: set SECUSCAN_CORS_ALLOWED_ORIGINS environment variable in the api service to allow requests from http://localhost:5173, http://127.0.0.1:5173, and http://frontend:5173 (Docker internal DNS). This ensures the frontend can make API calls whether accessed via localhost, IP, or internal Docker service name. Fixes #1622 --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 307932d9e..fc806d5ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,6 +46,7 @@ services: - SECUSCAN_POSTGRES_DSN=postgresql://secuscan:secuscan@postgres:5432/secuscan - SECUSCAN_REDIS_URL=redis://redis:6379/0 - SECUSCAN_PLUGINS_DIR=/app/plugins + - SECUSCAN_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://frontend:5173 depends_on: postgres: condition: service_healthy From 1a188345e4173f726fc8bf581a68091ae0e78f58 Mon Sep 17 00:00:00 2001 From: Anshul Jain Date: Wed, 8 Jul 2026 09:34:58 +0530 Subject: [PATCH 2/2] test: add static validation for docker-compose CORS configuration Parses docker-compose.yml directly (no Docker required) to assert the api service sets SECUSCAN_CORS_ALLOWED_ORIGINS and that it covers every origin the frontend can be reached through: localhost, 127.0.0.1, and the Docker-internal 'frontend' service DNS name. Addresses review feedback on #1700 asking for focused validation that the compose change is needed and correct. --- .../integration/test_docker_compose_cors.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 testing/backend/integration/test_docker_compose_cors.py diff --git a/testing/backend/integration/test_docker_compose_cors.py b/testing/backend/integration/test_docker_compose_cors.py new file mode 100644 index 000000000..8e466109a --- /dev/null +++ b/testing/backend/integration/test_docker_compose_cors.py @@ -0,0 +1,79 @@ +""" +Static validation for the docker-compose CORS configuration (issue #1622). + +The frontend service (port 5173) and API service (port 8081) can each be +reached through several origins depending on how the stack is accessed -- +localhost, 127.0.0.1, or the Docker-internal service DNS name. Without an +explicit SECUSCAN_CORS_ALLOWED_ORIGINS entry on the `api` service, the +backend's CORS middleware rejects requests from the frontend container. +These tests parse docker-compose.yml directly, so they run without Docker. +""" + +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[3] +COMPOSE_PATH = REPO_ROOT / "docker-compose.yml" + +REQUIRED_ORIGINS = { + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://frontend:5173", +} + + +def _load_compose() -> dict: + assert COMPOSE_PATH.exists(), f"docker-compose.yml not found at {COMPOSE_PATH}" + with open(COMPOSE_PATH) as f: + return yaml.safe_load(f) + + +def _service_env_as_dict(service: dict) -> dict: + """Normalize a compose service's `environment` key to a dict. + + docker-compose supports both list form (`KEY=VALUE` strings) and + mapping form (`KEY: VALUE`) -- normalize to a dict either way. + """ + env = service.get("environment") or [] + if isinstance(env, dict): + return {str(k): str(v) for k, v in env.items()} + result = {} + for entry in env: + if "=" not in entry: + continue + key, _, value = entry.partition("=") + result[key] = value + return result + + +def test_api_service_defines_cors_allowed_origins(): + compose = _load_compose() + api_service = compose.get("services", {}).get("api") + assert api_service is not None, "docker-compose.yml has no 'api' service" + + env = _service_env_as_dict(api_service) + assert "SECUSCAN_CORS_ALLOWED_ORIGINS" in env, ( + "api service must set SECUSCAN_CORS_ALLOWED_ORIGINS so the frontend " + "container can call the API -- without it, the backend's CORS " + "middleware rejects every frontend request." + ) + + +def test_cors_allowed_origins_cover_all_frontend_access_paths(): + compose = _load_compose() + api_service = compose["services"]["api"] + env = _service_env_as_dict(api_service) + + configured = { + origin.strip() + for origin in env["SECUSCAN_CORS_ALLOWED_ORIGINS"].split(",") + if origin.strip() + } + + missing = REQUIRED_ORIGINS - configured + assert not missing, ( + f"SECUSCAN_CORS_ALLOWED_ORIGINS is missing required origin(s): {sorted(missing)}. " + "The frontend must be reachable via localhost, 127.0.0.1, and the " + "Docker-internal 'frontend' service DNS name." + )