admin / Synapse-NetscanXi
publicNetwork Scanning, Vulnerability and Compliance Application
Synapse-NetscanXi / NetscanXiVersion13 / app / scheduler.py
6128 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | """ Scheduler for recurring scans (v6). UI-managed: schedules are persisted in the `schedules` table and edited from the web app. A single background thread ticks every ~15s, and any schedule whose `next_run` is due gets enqueued. Backward compatibility: the legacy env-configured single schedule (NETSCAN_SCHEDULE_MINUTES / _TARGET / _PROFILE) is still honoured. On first start, if those env vars define a schedule and the table is empty, it is seeded into the DB as a normal editable schedule. """ import os import threading import time from datetime import datetime, timedelta from . import db TICK_SECONDS = 15 def next_weekly_run(days, hour, minute, after=None): """Compute the next epoch timestamp matching one of `days` (0=Mon..6=Sun) at local hour:minute, strictly after `after` (defaults to now).""" if not days: return None base = datetime.fromtimestamp(after if after is not None else time.time()) hour = int(hour or 0) minute = int(minute or 0) for add in range(0, 8): cand = (base + timedelta(days=add)).replace( hour=hour, minute=minute, second=0, microsecond=0) if cand.weekday() in days and cand.timestamp() > base.timestamp(): return cand.timestamp() return None class Scheduler: def __init__(self, manager, default_target_fn, db_path=None): self.manager = manager self.default_target_fn = default_target_fn self.db_path = db_path or db.DEFAULT_DB_PATH self._stop = threading.Event() self._thread = None self._seed_from_env() # -- legacy env seed --------------------------------------------------- def _seed_from_env(self): try: interval = int(os.environ.get("NETSCAN_SCHEDULE_MINUTES", "0") or "0") except ValueError: interval = 0 if interval <= 0: return # Only seed if there are no schedules yet (don't duplicate on restart). if db.list_schedules(db_path=self.db_path): return target = os.environ.get("NETSCAN_SCHEDULE_TARGET", "").strip() profile = os.environ.get("NETSCAN_SCHEDULE_PROFILE", "standard").strip() sid = db.create_schedule( name="Default (env)", interval_minutes=interval, target=target, profile=profile or "standard", enabled=True, db_path=self.db_path) # Prime next_run so it fires after one interval, not immediately. db.update_schedule(sid, db_path=self.db_path, next_run=time.time() + interval * 60) # -- lifecycle --------------------------------------------------------- @property def enabled(self): """True if any enabled schedule exists.""" return any(s["enabled"] for s in db.list_schedules(db_path=self.db_path)) def start(self): if self._thread: return # Ensure every enabled schedule has a next_run set. now = time.time() for s in db.list_schedules(only_enabled=True, db_path=self.db_path): if not s.get("next_run"): db.update_schedule(s["id"], db_path=self.db_path, next_run=self._compute_next(s, after=now)) self._thread = threading.Thread(target=self._loop, daemon=True) self._thread.start() def stop(self): self._stop.set() def _loop(self): while not self._stop.is_set(): if self._stop.wait(TICK_SECONDS): break self._run_due() @staticmethod def _compute_next(s, after=None): """Next run for a schedule: weekly day+time if set, else interval.""" if s.get("days"): nr = next_weekly_run(s["days"], s.get("hour"), s.get("minute"), after=after) if nr: return nr base = after if after is not None else time.time() return base + s["interval_minutes"] * 60 def _run_due(self): now = time.time() for s in db.list_schedules(only_enabled=True, db_path=self.db_path): nxt = s.get("next_run") if nxt is None: db.update_schedule(s["id"], db_path=self.db_path, next_run=self._compute_next(s, after=now)) continue if nxt <= now: target = s["target"].strip() or self.default_target_fn() scan_type = s.get("scan_type") or "active" try: self.manager.enqueue(target, s["profile"], triggered_by="schedule", options=s.get("options"), scan_type=scan_type) except TypeError: # manager.enqueue without options/scan_type kwarg (defensive) self.manager.enqueue(target, s["profile"], triggered_by="schedule") db.update_schedule( s["id"], db_path=self.db_path, last_run=now, next_run=self._compute_next(s, after=now)) # -- introspection (used by /api/scheduler and the dashboard header) ---- def info(self): schedules = db.list_schedules(db_path=self.db_path) enabled = [s for s in schedules if s["enabled"]] # Soonest next_run across enabled schedules drives the header summary. next_run = None for s in enabled: if s.get("next_run") and (next_run is None or s["next_run"] < next_run): next_run = s["next_run"] first = enabled[0] if enabled else None return { "enabled": bool(enabled), "count": len(schedules), "enabled_count": len(enabled), "interval_minutes": first["interval_minutes"] if first else 0, "target": (first["target"] or "(auto)") if first else "(auto)", "profile": first["profile"] if first else "standard", "next_run": next_run, "schedules": schedules, } |