Files
security-alert-center/backend/app/main.py
T

164 lines
4.9 KiB
Python

import asyncio
import logging
from contextlib import asynccontextmanager, suppress
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import select
from app.api.v1.router import api_router
from app.auth.api_key import hash_api_key
from app.config import get_settings
from app.database import SessionLocal
from app.middleware.ingest_body_limit import IngestBodySizeLimitMiddleware
from app.models import ApiKey
from app.security_bootstrap import validate_security_settings, warn_relaxed_security
from app.services.user_auth import bootstrap_admin_user
from app.version import APP_VERSION, APP_VERSION_LABEL
ROOT = Path(__file__).resolve().parents[2]
logger = logging.getLogger("sac")
def bootstrap_api_key() -> None:
settings = get_settings()
if not settings.sac_bootstrap_api_key:
return
db = SessionLocal()
try:
key_hash = hash_api_key(settings.sac_bootstrap_api_key)
exists = db.scalar(select(ApiKey).where(ApiKey.key_hash == key_hash))
if exists:
return
prefix = settings.sac_bootstrap_api_key[:12]
db.add(
ApiKey(
name="bootstrap",
key_prefix=prefix,
key_hash=key_hash,
is_active=True,
)
)
db.commit()
finally:
db.close()
def bootstrap_users() -> None:
db = SessionLocal()
try:
bootstrap_admin_user(db)
finally:
db.close()
def bootstrap_stale_remote_actions() -> None:
db = SessionLocal()
try:
from app.services.host_remote_actions import clear_stale_running_remote_actions
cleared = clear_stale_running_remote_actions(db)
for hostname in cleared:
logger.info("Stale remote action reset on startup: %s", hostname)
finally:
db.close()
@asynccontextmanager
async def lifespan(_app: FastAPI):
settings = get_settings()
validate_security_settings(settings)
warn_relaxed_security(settings)
logger.info("%s — application startup (version %s)", APP_VERSION_LABEL, APP_VERSION)
bootstrap_api_key()
bootstrap_users()
bootstrap_stale_remote_actions()
stop_scan = asyncio.Event()
scan_task: asyncio.Task | None = None
if settings.sac_host_silence_scan_enabled:
from app.jobs.host_silence_background import host_silence_scan_loop
scan_task = asyncio.create_task(host_silence_scan_loop(stop_scan))
yield
if scan_task is not None:
stop_scan.set()
scan_task.cancel()
with suppress(asyncio.CancelledError):
await scan_task
logger.info("%s — application shutdown", APP_VERSION_LABEL)
def configure_logging() -> None:
if logging.getLogger().handlers:
return
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s: %(message)s",
)
def create_app() -> FastAPI:
configure_logging()
settings = get_settings()
app = FastAPI(
title="Security Alert Center",
version=APP_VERSION,
lifespan=lifespan,
)
origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()]
wildcard = origins == ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins if not wildcard else ["*"],
allow_credentials=not wildcard,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(
IngestBodySizeLimitMiddleware,
max_bytes=settings.sac_ingest_max_body_bytes,
)
from app.api.v1.health import router as health_router
app.include_router(api_router, prefix="/api/v1")
app.include_router(health_router)
frontend_dist = ROOT / "frontend" / "dist"
if frontend_dist.is_dir():
assets_dir = frontend_dist / "assets"
if assets_dir.is_dir():
app.mount(
"/assets",
StaticFiles(directory=str(assets_dir)),
name="ui-assets",
)
@app.get("/{spa_path:path}")
def spa_fallback(spa_path: str) -> FileResponse:
if spa_path.startswith("api/"):
raise HTTPException(status_code=404, detail="Not Found")
dist_root = frontend_dist.resolve()
if spa_path:
candidate = (frontend_dist / spa_path).resolve()
try:
candidate.relative_to(dist_root)
except ValueError:
raise HTTPException(status_code=404, detail="Not Found") from None
if candidate.is_file():
return FileResponse(candidate)
index = frontend_dist / "index.html"
if not index.is_file():
raise HTTPException(status_code=404, detail="UI not built")
return FileResponse(index)
return app
app = create_app()