"""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 import logging # Import BackgroundScheduler from apscheduler.schedulers.background from apscheduler.schedulers.background import BackgroundScheduler # Import SessionLocal from app.database from app.database import SessionLocal # Import sync_all_jira_links from app.jobs.jira_sync_job from app.jobs.jira_sync_job import sync_all_jira_links # Import run_retention_job from app.jobs.retention_job from app.jobs.retention_job import run_retention_job # Import check_and_run_recurring_campaigns from app.services.campaign_scheduler_service from app.services.campaign_scheduler_service import check_and_run_recurring_campaigns # Import scan_intel from app.services.intel_service from app.services.intel_service import scan_intel # Import sync_mitre from app.services.mitre_sync_service from app.services.mitre_sync_service import sync_mitre # Import cleanup_old_notifications from app.services.notification_service from app.services.notification_service import cleanup_old_notifications # Import enrich_all_techniques from app.services.osint_enrichment_service from app.services.osint_enrichment_service import enrich_all_techniques # Import cleanup_old_snapshots, create_snapshot from app.services.snapshot_service from app.services.snapshot_service import cleanup_old_snapshots, create_snapshot # Import detect_stale_coverage from app.services.stale_detection_service from app.services.stale_detection_service import detect_stale_coverage # Assign logger = logging.getLogger(__name__) 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.""" # Log info: "Scheduled MITRE sync job starting..." logger.info("Scheduled MITRE sync job starting...") # Assign db = SessionLocal() db = SessionLocal() # Attempt the following; catch errors below try: # Assign summary = sync_mitre(db) summary = sync_mitre(db) # Log info: "Scheduled MITRE sync job finished — %s", summary logger.info("Scheduled MITRE sync job finished — %s", summary) # Handle Exception except Exception: # Log exception: "Scheduled MITRE sync job failed" logger.exception("Scheduled MITRE sync job failed") # Always execute this cleanup block finally: # Close the database session db.close() # Define function _run_notification_cleanup def _run_notification_cleanup() -> None: """Clean up old read notifications.""" # Log info: "Scheduled notification cleanup job starting..." logger.info("Scheduled notification cleanup job starting...") # Assign db = SessionLocal() db = SessionLocal() # Attempt the following; catch errors below try: # Assign deleted = cleanup_old_notifications(db, days=90) deleted = cleanup_old_notifications(db, days=90) # Log info: "Notification cleanup finished — deleted %d old no logger.info("Notification cleanup finished — deleted %d old notifications", deleted) # Handle Exception except Exception: # Log exception: "Notification cleanup job failed" logger.exception("Notification cleanup job failed") # Always execute this cleanup block finally: # Close the database session db.close() # Define function _run_weekly_snapshot def _run_weekly_snapshot() -> None: """Create a weekly coverage snapshot and clean up old ones.""" # Log info: "Scheduled weekly snapshot job starting..." logger.info("Scheduled weekly snapshot job starting...") # Assign db = SessionLocal() db = SessionLocal() # Attempt the following; catch errors below try: # Assign snapshot = create_snapshot(db, name="Auto-weekly") snapshot = create_snapshot(db, name="Auto-weekly") # Log info: logger.info( # Literal argument value "Weekly snapshot created — score %.1f, %d techniques", snapshot.organization_score, snapshot.total_techniques, ) # Assign deleted = cleanup_old_snapshots(db, keep_last=52) deleted = cleanup_old_snapshots(db, keep_last=52) # Check: deleted if deleted: # Log info: "Cleaned up %d old snapshots", deleted logger.info("Cleaned up %d old snapshots", deleted) # Handle Exception except Exception: # Log exception: "Weekly snapshot job failed" logger.exception("Weekly snapshot job failed") # Always execute this cleanup block finally: # Close the database session db.close() # Define function _run_recurring_campaigns def _run_recurring_campaigns() -> None: """Check and run any due recurring campaigns.""" # Log info: "Scheduled recurring campaigns check starting..." logger.info("Scheduled recurring campaigns check starting...") # Assign db = SessionLocal() db = SessionLocal() # Attempt the following; catch errors below try: # Assign spawned = check_and_run_recurring_campaigns(db) spawned = check_and_run_recurring_campaigns(db) # Log info: "Recurring campaigns check finished — spawned %d c logger.info("Recurring campaigns check finished — spawned %d campaigns", spawned) # Handle Exception except Exception: # Log exception: "Recurring campaigns check failed" logger.exception("Recurring campaigns check failed") # Always execute this cleanup block finally: # Close the database session db.close() # Define function _run_intel_scan def _run_intel_scan() -> None: """Execute an intel scan inside its own DB session.""" # Log info: "Scheduled intel scan job starting..." logger.info("Scheduled intel scan job starting...") # Assign db = SessionLocal() db = SessionLocal() # Attempt the following; catch errors below try: # Assign summary = scan_intel(db) summary = scan_intel(db) # Log info: "Scheduled intel scan job finished — %s", summary logger.info("Scheduled intel scan job finished — %s", summary) # Handle Exception except Exception: # Log exception: "Scheduled intel scan job failed" logger.exception("Scheduled intel scan job failed") # Always execute this cleanup block finally: # Close the database session db.close() # Define function _run_osint_enrichment def _run_osint_enrichment() -> None: """Execute weekly OSINT enrichment inside its own DB session.""" # Log info: "Scheduled OSINT enrichment job starting..." logger.info("Scheduled OSINT enrichment job starting...") # Assign db = SessionLocal() db = SessionLocal() # Attempt the following; catch errors below try: # Assign total = enrich_all_techniques(db) total = enrich_all_techniques(db) # Log info: "OSINT enrichment finished — %d new items", total logger.info("OSINT enrichment finished — %d new items", total) # Handle Exception except Exception: # Log exception: "OSINT enrichment job failed" logger.exception("OSINT enrichment job failed") # Always execute this cleanup block finally: # Close the database session db.close() # Define function _run_stale_detection def _run_stale_detection() -> None: """Execute daily stale coverage detection inside its own DB session.""" # Log info: "Scheduled stale coverage detection starting..." logger.info("Scheduled stale coverage detection starting...") # Assign db = SessionLocal() db = SessionLocal() # Attempt the following; catch errors below try: # Assign count = detect_stale_coverage(db) count = detect_stale_coverage(db) # Log info: "Stale detection finished — %d techniques flagged" logger.info("Stale detection finished — %d techniques flagged", count) # Handle Exception except Exception: # Log exception: "Stale coverage detection job failed" logger.exception("Stale coverage detection job failed") # Always execute this cleanup block finally: # Close the database session 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. """ # Call scheduler.add_job() scheduler.add_job( _run_mitre_sync, # Keyword argument: trigger trigger="interval", # Keyword argument: hours hours=24, # Keyword argument: id id="mitre_sync", # Keyword argument: name name="MITRE ATT&CK sync (every 24h)", # Keyword argument: replace_existing replace_existing=True, ) # Call scheduler.add_job() scheduler.add_job( _run_intel_scan, # Keyword argument: trigger trigger="interval", # Keyword argument: weeks weeks=1, # Keyword argument: id id="intel_scan", # Keyword argument: name name="Intel scan (every 7d)", # Keyword argument: replace_existing replace_existing=True, ) # Call scheduler.add_job() scheduler.add_job( _run_notification_cleanup, # Keyword argument: trigger trigger="interval", # Keyword argument: hours hours=24, # Keyword argument: id id="notification_cleanup", # Keyword argument: name name="Notification cleanup (daily)", # Keyword argument: replace_existing replace_existing=True, ) # Call scheduler.add_job() scheduler.add_job( _run_weekly_snapshot, # Keyword argument: trigger trigger="cron", # Keyword argument: day_of_week day_of_week="sun", # Keyword argument: hour hour=0, # Keyword argument: minute minute=0, # Keyword argument: id id="weekly_snapshot", # Keyword argument: name name="Weekly coverage snapshot (Sundays 00:00)", # Keyword argument: replace_existing replace_existing=True, ) # Call scheduler.add_job() scheduler.add_job( _run_recurring_campaigns, # Keyword argument: trigger trigger="interval", # Keyword argument: hours hours=24, # Keyword argument: id id="recurring_campaigns", # Keyword argument: name name="Recurring campaigns check (daily)", # Keyword argument: replace_existing replace_existing=True, ) # Call scheduler.add_job() scheduler.add_job( sync_all_jira_links, # Keyword argument: trigger trigger="interval", # Keyword argument: hours hours=1, # Keyword argument: id id="jira_sync", # Keyword argument: name name="Jira link sync (hourly)", # Keyword argument: replace_existing replace_existing=True, ) # Call scheduler.add_job() scheduler.add_job( _run_osint_enrichment, # Keyword argument: trigger trigger="interval", # Keyword argument: weeks weeks=1, # Keyword argument: id id="osint_enrichment", # Keyword argument: name name="OSINT enrichment (weekly)", # Keyword argument: replace_existing replace_existing=True, ) # Call scheduler.add_job() scheduler.add_job( _run_stale_detection, # Keyword argument: trigger trigger="interval", # Keyword argument: hours hours=24, # Keyword argument: id id="stale_detection", # Keyword argument: name name="Stale coverage detection (daily)", # Keyword argument: replace_existing replace_existing=True, ) # Call scheduler.add_job() scheduler.add_job( run_retention_job, # Keyword argument: trigger trigger="interval", # Keyword argument: hours hours=24, # Keyword argument: id id="retention_policies", # Keyword argument: name name="Data retention policies (daily)", # Keyword argument: replace_existing replace_existing=True, ) # Call scheduler.start() scheduler.start() # Log info: logger.info( # Literal argument value "Background scheduler started — mitre_sync (24h), intel_scan (7d), " # Literal argument value "notification_cleanup (24h), weekly_snapshot (Sundays 00:00), " # Literal argument value "recurring_campaigns (daily), jira_sync (1h), " # Literal argument value "osint_enrichment (weekly), stale_detection (daily), " # Literal argument value "retention_policies (daily)" )