Skip to content

Commit 3dee6cb

Browse files
committed
Dedupe backend/core into eval_mcp/core; harden path handling against CodeQL py/path-injection
Finishes the dedupe started in the previous commit. 11 modules in backend/core/ were byte-identical or only-import-line copies of eval_mcp/core/ equivalents (user_storage, eval_results, s3_client, pricing, bedrock_client, document_chunking, judge_config, logging_utils, pdf_report, pipeline_stages, provider_pricing.json). Deleted the backend copies, rewired ~18 imports in backend/api/*, backend/core/agent.py, backend/core/chat.py, backend/core/judge_prompt_builder.py, and backend/core/judge_tools.py to pull from eval_mcp.core.*. One-way dependency (backend -> eval_mcp) is now consistent across the whole tree; eval_mcp is the sole source of truth. Path-injection hardening in the consolidated eval_mcp/core: - New safe_user_path(user_id, *parts) helper in eval_mcp/core/user_storage.py: resolves the target and verifies it stays under both USER_STORAGE_BASE and {base}/{user_id}, rejects user_ids containing '/', '\\', or '..', rejects path parts that try to escape via traversal. This is the pattern CodeQL's py/path-injection rule recognizes as safe. - New _ensure_under_base(path) helper for Path objects assembled outside the helper (used by _load_json_file, _save_json_file, and the two configs-dir reads in eval_results.py). - _get_json_store_dir now validates store_type is a single path segment. - eval_mcp/s3_sync.py: replicate_async and _is_syncable now use Path.resolve() + is_relative_to() instead of relative_to+ValueError. - backend/api/compare.py report-save path routed through safe_user_path. - eval_mcp/viewer.py report-download path routed through safe_user_path. Behavior-neutral for existing EKS storage: Cognito user IDs are UUIDs with only hex+hyphen characters, all directory names in the layout are hardcoded strings ('configs', 'logs', 'store', etc.), and S3 key construction uses plain strings unaffected by filesystem hardening. Test plan: - Unit-tested safe_user_path: accepts valid user/parts; rejects '', '.', '..', '../secret', 'alice/../bob', 'alice/bob', 'alice\\\\bob', and part-based escape attempts like ['..', '..', 'etc', 'passwd']. - Smoke-imported backend.api.main, backend.api.compare, eval_mcp.server in repo venv and inside the built backend Docker image. - Booted 'python -m eval_mcp' with EVAL_MCP_TRANSPORT=http in the container; uvicorn listens on :8002 (what the helm sidecar does).
1 parent 99e780c commit 3dee6cb

21 files changed

Lines changed: 95 additions & 3621 deletions

backend/api/compare.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from fastapi import APIRouter, Depends, HTTPException, Query, Request
1111
from fastapi.responses import Response
1212

13-
from backend.core.eval_results import precompute_eval_results
14-
from backend.core.user_storage import get_user_log_dir, load_eval_detail, load_eval_groups
13+
from eval_mcp.core.eval_results import precompute_eval_results
14+
from eval_mcp.core.user_storage import get_user_log_dir, load_eval_detail, load_eval_groups
1515

1616
logger = logging.getLogger(__name__)
1717

@@ -32,7 +32,7 @@ async def get_comparison_groups(user_id: str = Depends(_get_user_id)):
3232
Serves from pre-computed cache (fast). Merges in running evals
3333
from log headers so they appear without waiting for completion.
3434
"""
35-
from backend.core.eval_results import _read_log_headers, _build_groups_from_headers
35+
from eval_mcp.core.eval_results import _read_log_headers, _build_groups_from_headers
3636

3737
# Serve cached completed evals (instant)
3838
cached = load_eval_groups(user_id)
@@ -63,7 +63,7 @@ async def get_comparison_groups(user_id: str = Depends(_get_user_id)):
6363
@router.get("/detail")
6464
async def get_comparison_detail(group_id: str, user_id: str = Depends(_get_user_id)):
6565
"""Get full comparison data for a specific evaluation group."""
66-
from backend.core.eval_results import _read_log_headers, _build_groups_from_headers
66+
from eval_mcp.core.eval_results import _read_log_headers, _build_groups_from_headers
6767

6868
# For running evals, read partial results directly (skip cache)
6969
log_dir = get_user_log_dir(user_id)
@@ -189,8 +189,8 @@ async def get_sample_detail(
189189
user_id: str = Depends(_get_user_id),
190190
):
191191
"""Get full detail for a single sample including judge reasoning."""
192-
from backend.core.eval_results import _read_full_logs
193-
from backend.core.user_storage import get_user_log_dir
192+
from eval_mcp.core.eval_results import _read_full_logs
193+
from eval_mcp.core.user_storage import get_user_log_dir
194194

195195
log_dir = get_user_log_dir(user_id)
196196
if not log_file.startswith(log_dir) and f"/users/{user_id}/" not in log_file:
@@ -288,8 +288,8 @@ async def generate_report_pdf(
288288
session_id: Optional chat session ID to pull transcript for context.
289289
monthly_volume: Projected monthly call volume for cost projections.
290290
"""
291-
from backend.core.bedrock_client import BedrockClient
292-
from backend.core.pdf_report import generate_pdf_report
291+
from eval_mcp.core.bedrock_client import BedrockClient
292+
from eval_mcp.core.pdf_report import generate_pdf_report
293293

294294
# Load evaluation data
295295
detail = load_eval_detail(user_id, group_id)
@@ -320,8 +320,8 @@ async def generate_report_pdf(
320320
)
321321

322322
# Store the PDF for later access
323-
from backend.core.user_storage import _s3_enabled, _get_s3_client, DATA_BUCKET
324-
from backend.core.user_storage import _get_json_store_dir
323+
from eval_mcp.core.user_storage import _s3_enabled, _get_s3_client, DATA_BUCKET
324+
from eval_mcp.core.user_storage import safe_user_path
325325

326326
safe_id = group_id.replace("/", "_").replace("\\", "_")
327327
filename = f"report_{safe_id}.pdf"
@@ -335,8 +335,9 @@ async def generate_report_pdf(
335335
ContentType="application/pdf",
336336
)
337337
else:
338-
reports_dir = _get_json_store_dir(user_id, "reports")
339-
(reports_dir / filename).write_bytes(pdf_bytes)
338+
pdf_path = safe_user_path(user_id, "store", "reports", filename)
339+
pdf_path.parent.mkdir(parents=True, exist_ok=True)
340+
pdf_path.write_bytes(pdf_bytes)
340341

341342
return Response(
342343
content=pdf_bytes,

backend/api/main.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@
1717
from pydantic import BaseModel
1818

1919
from backend.core.agent import Agent
20-
from backend.core.bedrock_client import BedrockClient
20+
from eval_mcp.core.bedrock_client import BedrockClient
2121
from backend.core.database import Database
2222
from backend.core.mcp_client import MultiMCPClient
23-
from backend.core.s3_client import (
23+
from eval_mcp.core.s3_client import (
2424
is_s3_enabled,
2525
generate_presigned_upload_url,
2626
list_user_s3_documents,
2727
get_s3_document_content,
2828
)
29-
from backend.core.user_storage import save_document
29+
from eval_mcp.core.user_storage import save_document
3030

3131
# Configure logging at module level (runs when uvicorn loads the app)
3232
logging.basicConfig(
@@ -455,7 +455,7 @@ async def process_qa_dataset_content(
455455
- rows_saved: int (if successful)
456456
- error: str (if failed)
457457
"""
458-
from backend.core.user_storage import save_dataset_to_db
458+
from eval_mcp.core.user_storage import save_dataset_to_db
459459
from eval_mcp.tools.save_dataset import parse_content_to_rows, rows_to_test_cases, generate_dataset_name
460460

461461
logger = logging.getLogger(__name__)
@@ -1261,7 +1261,7 @@ async def list_documents(user_id: str = Depends(get_current_user_id)):
12611261
documents = list_user_s3_documents(user_id)
12621262
return {"documents": documents, "storage": "s3"}
12631263
else:
1264-
from backend.core.user_storage import list_user_document_paths
1264+
from eval_mcp.core.user_storage import list_user_document_paths
12651265
paths = list_user_document_paths(user_id)
12661266
documents = [{"path": p} for p in paths]
12671267
return {"documents": documents, "storage": "disk"}

backend/core/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from rich.console import Console
88

9-
from .bedrock_client import BedrockClient
9+
from eval_mcp.core.bedrock_client import BedrockClient
1010
from .mcp_client import MultiMCPClient
1111

1212
console = Console()

0 commit comments

Comments
 (0)