Files
Aegis/backend/app/jobs/mitre_sync_job.py
Kitos 9b98f60a9a
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
feat(phase-35): Jira + Tempo integration with internal worklogs
Full Jira/Tempo pipeline: link Aegis entities to Jira issues, auto-sync
status hourly, log time internally with integrity hashing, and optionally
push worklogs to Tempo.

- 1.1 JiraLink model + Worklog model: Alembic migration b020 with indexes,
  enums (jiralinkentitytype, jirasyncdirection), and integrity_hash column
- 1.2 Jira service: atlassian-python-api wrapper with lazy singleton client,
  search/create/sync operations, feature-flagged via JIRA_ENABLED
- 1.3 Jira router: CRUD endpoints for /jira/links, /jira/search,
  /jira/create-issue with audit logging and entity-to-issue auto-creation
- 1.4 Tempo service: worklog push via tempo-api-python-client, auto-log from
  test completions when TEMPO_ENABLED, graceful fallback on failure
- 1.5 Worklog service + router: immutable internal time records with SHA-256
  integrity hash, CRUD at /worklogs, /worklogs/{id}/verify endpoint
- 1.6 Frontend: JiraLinkPanel component (search, link, sync, unlink) and
  WorklogTimeline component (timeline view, manual log form) integrated into
  TestDetailPage sidebar, CampaignDetailPage grid, TechniqueDetailPage
- 1.7 Jira sync job: APScheduler hourly job syncs all links from Jira,
  registered in background scheduler alongside existing jobs
2026-02-17 15:57:39 +01:00

182 lines
5.6 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
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
from app.services.snapshot_service import create_snapshot, cleanup_old_snapshots
from app.services.campaign_scheduler_service import check_and_run_recurring_campaigns
from app.jobs.jira_sync_job import sync_all_jira_links
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_weekly_snapshot() -> None:
"""Create a weekly coverage snapshot and clean up old ones."""
logger.info("Scheduled weekly snapshot job starting...")
db = SessionLocal()
try:
snapshot = create_snapshot(db, name="Auto-weekly")
logger.info(
"Weekly snapshot created — score %.1f, %d techniques",
snapshot.organization_score,
snapshot.total_techniques,
)
deleted = cleanup_old_snapshots(db, keep_last=52)
if deleted:
logger.info("Cleaned up %d old snapshots", deleted)
except Exception:
logger.exception("Weekly snapshot job failed")
finally:
db.close()
def _run_recurring_campaigns() -> None:
"""Check and run any due recurring campaigns."""
logger.info("Scheduled recurring campaigns check starting...")
db = SessionLocal()
try:
spawned = check_and_run_recurring_campaigns(db)
logger.info("Recurring campaigns check finished — spawned %d campaigns", spawned)
except Exception:
logger.exception("Recurring campaigns check 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.add_job(
_run_weekly_snapshot,
trigger="cron",
day_of_week="sun",
hour=0,
minute=0,
id="weekly_snapshot",
name="Weekly coverage snapshot (Sundays 00:00)",
replace_existing=True,
)
scheduler.add_job(
_run_recurring_campaigns,
trigger="interval",
hours=24,
id="recurring_campaigns",
name="Recurring campaigns check (daily)",
replace_existing=True,
)
scheduler.add_job(
sync_all_jira_links,
trigger="interval",
hours=1,
id="jira_sync",
name="Jira link sync (hourly)",
replace_existing=True,
)
scheduler.start()
logger.info(
"Background scheduler started — mitre_sync (24h), intel_scan (7d), "
"notification_cleanup (24h), weekly_snapshot (Sundays 00:00), "
"recurring_campaigns (daily), jira_sync (1h)"
)