"""Scheduled background jobs. Registers periodic tasks on an APScheduler ``BackgroundScheduler``: * **MITRE sync** — every 24 hours (see :func:`sync_mitre`) * **Intel scan** — every 7 days (see :func:`scan_intel`) Each job manages its own database session (created on entry, closed in ``finally``) so it is fully independent from FastAPI's request-scoped sessions. """ import logging from apscheduler.schedulers.background import BackgroundScheduler from app.database import SessionLocal from app.services.mitre_sync_service import sync_mitre from app.services.intel_service import scan_intel from app.services.notification_service import cleanup_old_notifications logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Module-level scheduler instance # --------------------------------------------------------------------------- scheduler = BackgroundScheduler() # --------------------------------------------------------------------------- # Job functions # --------------------------------------------------------------------------- def _run_mitre_sync() -> None: """Execute a MITRE sync inside its own DB session.""" logger.info("Scheduled MITRE sync job starting...") db = SessionLocal() try: summary = sync_mitre(db) logger.info("Scheduled MITRE sync job finished — %s", summary) except Exception: logger.exception("Scheduled MITRE sync job failed") finally: db.close() def _run_notification_cleanup() -> None: """Clean up old read notifications.""" logger.info("Scheduled notification cleanup job starting...") db = SessionLocal() try: deleted = cleanup_old_notifications(db, days=90) logger.info("Notification cleanup finished — deleted %d old notifications", deleted) except Exception: logger.exception("Notification cleanup job failed") finally: db.close() def _run_intel_scan() -> None: """Execute an intel scan inside its own DB session.""" logger.info("Scheduled intel scan job starting...") db = SessionLocal() try: summary = scan_intel(db) logger.info("Scheduled intel scan job finished — %s", summary) except Exception: logger.exception("Scheduled intel scan job failed") finally: db.close() # --------------------------------------------------------------------------- # Scheduler bootstrap # --------------------------------------------------------------------------- def start_scheduler() -> None: """Register all periodic jobs and start the background scheduler. Jobs registered: * ``mitre_sync`` — every **24 hours** * ``intel_scan`` — every **7 days** Neither job fires immediately on startup. """ scheduler.add_job( _run_mitre_sync, trigger="interval", hours=24, id="mitre_sync", name="MITRE ATT&CK sync (every 24h)", replace_existing=True, ) scheduler.add_job( _run_intel_scan, trigger="interval", weeks=1, id="intel_scan", name="Intel scan (every 7d)", replace_existing=True, ) scheduler.add_job( _run_notification_cleanup, trigger="interval", hours=24, id="notification_cleanup", name="Notification cleanup (daily)", replace_existing=True, ) scheduler.start() logger.info("Background scheduler started — mitre_sync (24h), intel_scan (7d), notification_cleanup (24h)")