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
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"Error in gap-analysis: {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"Error in code-review: {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"Error in ask-question: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
36 changes: 35 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
"""

import json
import logging
import os
from contextlib import asynccontextmanager

from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exception_handlers import (http_exception_handler,
request_validation_exception_handler)
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi_mcp import FastApiMCP

from agents.devsecops_agent import agent as devsecops
Expand All @@ -26,6 +32,13 @@

load_dotenv()

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


@asynccontextmanager
async def lifespan(app: FastAPI):
Expand Down Expand Up @@ -83,6 +96,27 @@ async def lifespan(app: FastAPI):
app.include_router(mistral.router, prefix="/api/agents/mistral", tags=["Mistral Agent"])


# ─── Global Exception Handler ─────────────────────────────────────────────────


@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""
Global exception handler to prevent sensitive information leakage.
Logs the actual error and returns a generic 500 response.
"""
if isinstance(exc, StarletteHTTPException):
return await http_exception_handler(request, exc)
if isinstance(exc, RequestValidationError):
return await request_validation_exception_handler(request, exc)

logger.error(f"Unhandled exception: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"},
)


# ─── Health Check ─────────────────────────────────────────────────────────────


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

@pytest.mark.anyio
async def test_unhandled_exception_leak_prevention():
"""
Verify that unhandled exceptions do not leak sensitive information.
We add a temporary route to the app for this test.
"""
@app.get("/api/sentinel-error-test")
async def trigger_error():
raise ValueError("Secret database credentials leaked: password123")

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

assert response.status_code == 500
data = response.json()
assert data["detail"] == "Internal server error"
assert "password123" not in str(data)
assert "ValueError" not in str(data)

@pytest.mark.anyio
async def test_http_exception_passthrough():
"""
Verify that explicit HTTPExceptions (like 404) are still passed through correctly.
"""
async with AsyncClient(
transport=ASGITransport(app=app, raise_app_exceptions=False),
base_url="http://test"
) as ac:
# Use a path that definitely doesn't exist to trigger a Starlette 404
response = await ac.get("/api/definitely-does-not-exist-at-all-12345")

assert response.status_code == 404
data = response.json()
assert "detail" in data
assert "not found" in data["detail"].lower()

@pytest.mark.anyio
async def test_validation_error_passthrough():
"""
Verify that RequestValidationError (422) is still passed through correctly.
"""
async with AsyncClient(
transport=ASGITransport(app=app, raise_app_exceptions=False),
base_url="http://test"
) as ac:
# Invalid payload (missing required fields) for an existing endpoint
response = await ac.post("/api/evidence/", json={"title": "Missing fields"})

assert response.status_code == 422
Loading