Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@
**Vulnerability:** Not a direct security vulnerability, but an environmental instability. The `requirements.txt` allowed `mistralai>=1.1.0`, which pulled in version 2.x.
**Learning:** MistralAI 2.x introduces breaking changes in the client import structure (`from mistralai import Mistral` fails if not using the new client correctly or if expecting the old one). This caused the entire application (including security tests) to fail on startup.
**Prevention:** Pin critical dependencies like `mistralai==1.1.0` in `requirements.txt` to ensure consistent behavior across development and CI environments, especially when using agents that rely on specific API structures.

## 2025-05-15 - Information Exposure through Error Messages
**Vulnerability:** Raw exception details (`str(e)`) were being returned to the client in HTTP 500 responses across multiple endpoints, potentially leaking sensitive system state, file paths, or credentials.
**Learning:** Defaulting to returning exception strings in API responses is a common but dangerous practice. A centralized exception handler in FastAPI provides a robust safety net, but must be carefully implemented to avoid swallowing intentional `HTTPException` or `RequestValidationError` responses.
**Prevention:** Implement a global `@app.exception_handler(Exception)` that logs full tracebacks server-side but returns generic messages to clients. Avoid `detail=str(e)` in all production code.
10 changes: 7 additions & 3 deletions agents/mistral_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

import json
import logging
import os
import uuid
from datetime import UTC, datetime
Expand Down Expand Up @@ -318,7 +319,8 @@ async def gap_analysis(req: GapAnalysisRequest, db: AsyncSession = Depends(get_d
"model": MISTRAL_MODEL,
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
logging.error(f"Mistral Gap Analysis Error: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")


@router.post("/code-review", summary="DevSecOps code security analysis with Codestral")
Expand All @@ -333,7 +335,8 @@ async def code_review(req: CodeReviewRequest, db: AsyncSession = Depends(get_db)
)
return {"analysis": result, "model": MISTRAL_CODE_MODEL}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
logging.error(f"Mistral Code Review Error: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")


@router.post("/ask", summary="Ask a CMMC/ZT compliance question")
Expand All @@ -343,4 +346,5 @@ async def ask_question(req: QuestionRequest):
answer = await agent.answer_compliance_question(req.question, req.context)
return {"question": req.question, "answer": answer, "model": MISTRAL_MODEL}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
logging.error(f"Mistral Ask Error: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
44 changes: 43 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
"""

import json
import logging
import os
from contextlib import asynccontextmanager

from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi_mcp import FastApiMCP

from agents.devsecops_agent import agent as devsecops
Expand All @@ -34,6 +37,13 @@ async def lifespan(app: FastAPI):
yield


# ─── Logging ───────────────────────────────────────────────────────────────────

logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

# ─── FastAPI Application ───────────────────────────────────────────────────────

app = FastAPI(
Expand Down Expand Up @@ -64,6 +74,38 @@ async def lifespan(app: FastAPI):
# Add Security Headers Middleware
app.add_middleware(SecurityHeadersMiddleware)


# ─── Exception Handlers ───────────────────────────────────────────────────────


@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""
Global exception handler to prevent sensitive information leakage.
Logs the full error server-side and returns a generic 500 message.
"""
# Allow FastAPI's built-in exception handlers to handle their specific cases
if isinstance(exc, HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
)
if isinstance(exc, RequestValidationError):
return JSONResponse(
status_code=422,
content={"detail": exc.errors()},
)

# Log the actual exception for debugging
logger.error(f"Unhandled error: {str(exc)}", exc_info=True)

# Return a safe, generic error message to the client
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"},
)


# ─── Routers ──────────────────────────────────────────────────────────────────

# Core Routers
Expand Down
35 changes: 35 additions & 0 deletions tests/test_sentinel_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import pytest
from httpx import AsyncClient, ASGITransport
from backend.main import app

@pytest.mark.asyncio
async def test_error_leakage():
# Adding a temporary route to trigger an error
@app.get("/api/test-error-leakage")
async def trigger_error():
raise ValueError("Sensitive database connection string: postgresql://user:password@localhost:5432/db")

async with AsyncClient(transport=ASGITransport(app=app, raise_app_exceptions=False), base_url="http://test") as ac:
response = await ac.get("/api/test-error-leakage")

assert response.status_code == 500
assert "Sensitive database connection string" not in response.text
assert "password" not in response.text
assert "Internal server error" in response.text

@pytest.mark.asyncio
async def test_http_exception_passthrough():
async with AsyncClient(transport=ASGITransport(app=app, raise_app_exceptions=False), base_url="http://test") as ac:
response = await ac.get("/api/controls/NONEXISTENT")

assert response.status_code == 404
assert "Control NONEXISTENT not found" in response.text

@pytest.mark.asyncio
async def test_validation_error_passthrough():
async with AsyncClient(transport=ASGITransport(app=app, raise_app_exceptions=False), base_url="http://test") as ac:
# Sending invalid data to a POST endpoint
response = await ac.post("/api/agents/mistral/gap-analysis", json={"invalid": "data"})

assert response.status_code == 422
assert "detail" in response.json()
Loading