diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..2f52577e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +.git +.gitignore +.worktrees +.agents +.claude +.DS_Store +.env +.env.* +**/.env +**/.env.* +pg_data +tmp +.install-logs +.pytest_cache +**/.pytest_cache +**/__pycache__ +**/*.pyc +frontend/node_modules +frontend/dist +backend/.coverage +backend/.pytest_cache +nginx/certs diff --git a/backend/app/api/v1/permission_monitoring.py b/backend/app/api/v1/permission_monitoring.py index 688f9d79..f219cca2 100644 --- a/backend/app/api/v1/permission_monitoring.py +++ b/backend/app/api/v1/permission_monitoring.py @@ -399,11 +399,11 @@ def _is_non_china_ip(ip_location: IpLocation) -> bool: def _classify_security_access_log(log: SecurityAccessLog, ip_location: IpLocation | None = None) -> dict[str, str]: - if ip_location and _is_non_china_ip(ip_location): - return {"category": "ABNORMAL_IP", "severity": "HIGH"} path = (log.path or "").lower() if any(marker in path for marker in SENSITIVE_PROBE_MARKERS): return {"category": "PROBE", "severity": "CRITICAL"} + if ip_location and _is_non_china_ip(ip_location): + return {"category": "ABNORMAL_IP", "severity": "HIGH"} if log.status_code >= 500: return {"category": "SERVER_ERROR", "severity": "HIGH"} if log.auth_status == "INVALID_TOKEN": diff --git a/backend/tests/test_permission_monitoring_api.py b/backend/tests/test_permission_monitoring_api.py index 916970ae..b48238dd 100644 --- a/backend/tests/test_permission_monitoring_api.py +++ b/backend/tests/test_permission_monitoring_api.py @@ -929,6 +929,41 @@ async def test_security_access_logs_classify_sensitive_path_probe(db_session): assert result["items"][0]["severity"] == "CRITICAL" +@pytest.mark.asyncio +async def test_security_access_logs_prioritize_sensitive_probe_over_foreign_ip(db_session, monkeypatch): + """敏感路径探测应优先于海外 IP 分类,避免降低风险等级。""" + await db_session.execute(text("DELETE FROM security_access_logs")) + monkeypatch.setattr( + permission_monitoring, + "resolve_ip_location", + lambda ip: FakeIpInfo("South Holland", "", "", "Netherlands"), + ) + await db_session.execute( + text( + """ + INSERT INTO security_access_logs + (id, method, path, status_code, elapsed_ms, client_ip, user_agent, auth_status, user_identifier, created_at) + VALUES + ('00000000-0000-4000-8000-000000000505', 'GET', '/api/.git/config', 404, 0.7, + '45.148.10.95', 'probe-bot/1.0', 'ANONYMOUS', NULL, CURRENT_TIMESTAMP) + """ + ) + ) + await db_session.commit() + + result = await permission_monitoring.get_security_access_logs( + db=db_session, + _=AdminUserStub(), + status_min=400, + auth_status=None, + page=1, + page_size=20, + ) + + assert result["items"][0]["category"] == "PROBE" + assert result["items"][0]["severity"] == "CRITICAL" + + @pytest.mark.asyncio async def test_security_access_logs_classify_non_china_ip_as_abnormal(db_session, monkeypatch): """安全事件明细应把非中国公网 IP 归类为异常 IP。""" diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index c0edbe8c..458ae0b5 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -19,7 +19,18 @@ services: container_name: ctms_frontend_dev restart: always working_dir: /app - command: sh -c "npm ci && npm run dev -- --host 0.0.0.0" + command: + - sh + - -c + - | + set -e + lock_hash="$$(sha256sum package-lock.json | awk '{print $$1}')" + marker="node_modules/.ctms-package-lock.sha256" + if [ ! -f "$$marker" ] || [ "$$(cat "$$marker")" != "$$lock_hash" ]; then + npm ci + printf '%s' "$$lock_hash" > "$$marker" + fi + npm run dev -- --host 0.0.0.0 environment: VITE_RUNTIME_ENV: ${ENV:-development} VITE_ALLOW_INSECURE_DEV_LOGIN: ${VITE_ALLOW_INSECURE_DEV_LOGIN:-true} diff --git a/docker-compose.yaml b/docker-compose.yaml index 9de7d6aa..0de00731 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,8 +11,6 @@ services: volumes: # 持久化数据到 Linux 本地,防止重启丢失 - ./pg_data:/var/lib/postgresql/data - ports: - - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ctms_user -d ctms_db"] interval: 5s @@ -28,8 +26,6 @@ services: pull: false container_name: ctms_backend restart: always - ports: - - "8000:8000" environment: DATABASE_URL: postgresql+asyncpg://ctms_user:secret_password@db/ctms_db ENV: ${ENV:-development} diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 644d6d7e..3198cdba 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -52,6 +52,12 @@ http { add_header Cache-Control "public, max-age=31536000, immutable" always; } + location ~* (^|/)(\.git|\.env($|[./_-])|wp-config\.php|config\.php|database\.yml|backup($|[._/-])) { + access_log off; + log_not_found off; + return 404; + } + location /api/ { proxy_pass http://backend; proxy_set_header Host $host; diff --git a/nginx/nginx.dev.conf b/nginx/nginx.dev.conf index a5acc638..36e4eb97 100644 --- a/nginx/nginx.dev.conf +++ b/nginx/nginx.dev.conf @@ -50,6 +50,12 @@ http { add_header Cache-Control "public, max-age=86400" always; } + location ~* (^|/)(\.git|\.env($|[./_-])|wp-config\.php|config\.php|database\.yml|backup($|[._/-])) { + access_log off; + log_not_found off; + return 404; + } + location /api/ { proxy_pass http://backend; proxy_set_header Host $host; diff --git a/scripts/install-ctms.sh b/scripts/install-ctms.sh index d0e6b3f4..8a3ae14d 100755 --- a/scripts/install-ctms.sh +++ b/scripts/install-ctms.sh @@ -552,9 +552,10 @@ PY check_http_endpoint() { local path="$1" + local max_attempts="${2:-10}" local url="${BASE_URL%/}$path" log "探测接口: $url" - local attempt=1 max_attempts=10 delay=2 warmup_delay=3 + local attempt=1 delay=2 warmup_delay=3 log "等待服务预热 ${warmup_delay}s …" sleep "$warmup_delay" while [[ "$attempt" -le "$max_attempts" ]]; do @@ -584,6 +585,9 @@ run_health_checks() { step "探测 HTTP 接口可用性" check_http_endpoint "/health" check_http_endpoint "/api/v1/auth/login-key" + if [[ "$TARGET_ENV" == "dev" ]]; then + check_http_endpoint "/" 60 + fi ok "HTTP 接口全部就绪" }