d2a46feba8
Task D — Google-style docstrings (Args/Returns) on every public function, method, and class across all 158 Python files in the backend. Zero ruff D violations (pydocstyle Google convention). Task E — Explanatory one-line comment before every code line (~11600 new comments). ruff check passes clean after isort re-sort.
395 lines
14 KiB
Python
395 lines
14 KiB
Python
"""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)"
|
|
)
|