Immutable audit_log table in the scanner DB records every significant admin action (profile save/delete, token create/revoke, PIN changes, source add/update/delete, scheduler job changes, scan start/stop, SMTP save, dispositions, item delete/redact). GET /api/audit_log exposes entries newest-first. New Audit Log tab in the Settings modal renders the table on demand. Settings modal widened 540→640 px and tab labels set to white-space:nowrap so the six-tab row fits on one line. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
168 lines
5.8 KiB
Python
168 lines
5.8 KiB
Python
"""
|
|
Scheduler API routes — multi-job CRUD, status, history, run-now.
|
|
"""
|
|
from __future__ import annotations
|
|
from flask import Blueprint, jsonify, request
|
|
import sys, os, threading
|
|
try:
|
|
from gdpr_db import log_audit_event as _audit
|
|
except ImportError:
|
|
def _audit(*a, **kw): pass # type: ignore[misc]
|
|
|
|
bp = Blueprint("scheduler", __name__)
|
|
|
|
# Return JSON for any unhandled exception in this blueprint
|
|
@bp.errorhandler(Exception)
|
|
def _handle_error(e):
|
|
import traceback; traceback.print_exc()
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
# Ensure the project root is on sys.path so `import scheduler` finds
|
|
# our scheduler.py and not any stdlib module.
|
|
def _sm():
|
|
import scan_scheduler as _s
|
|
return _s
|
|
|
|
|
|
def _sched():
|
|
import scan_scheduler as _s
|
|
return _s.scan_scheduler
|
|
|
|
def _db():
|
|
import gdpr_scanner as _m
|
|
return _m._get_db() if _m.DB_OK else None
|
|
|
|
|
|
# ── Job list ──────────────────────────────────────────────────────────────────
|
|
|
|
@bp.route("/api/scheduler/jobs", methods=["GET"])
|
|
def scheduler_jobs_list():
|
|
return jsonify({"jobs": _sm().load_jobs()})
|
|
|
|
|
|
@bp.route("/api/scheduler/jobs/save", methods=["POST"])
|
|
def scheduler_jobs_save():
|
|
try:
|
|
sm = _sm()
|
|
data = request.get_json() or {}
|
|
jobs = sm.load_jobs()
|
|
job_id = (data.get("id") or "").strip()
|
|
if job_id:
|
|
for i, j in enumerate(jobs):
|
|
if j["id"] == job_id:
|
|
jobs[i] = {**sm._DEFAULT_JOB, **j, **data}
|
|
sm.save_jobs(jobs)
|
|
try:
|
|
_sched().reload()
|
|
except Exception:
|
|
pass
|
|
_audit("scheduler_job_save",
|
|
f"id={job_id!r} name={jobs[i].get('name','')!r}",
|
|
ip=request.remote_addr or "")
|
|
return jsonify({"ok": True, "job": jobs[i]})
|
|
# New job
|
|
job = sm._new_job(data)
|
|
jobs.append(job)
|
|
sm.save_jobs(jobs)
|
|
try:
|
|
_sched().reload()
|
|
except Exception:
|
|
pass
|
|
_audit("scheduler_job_save",
|
|
f"id={job.get('id','')!r} name={job.get('name','')!r}",
|
|
ip=request.remote_addr or "")
|
|
return jsonify({"ok": True, "job": job})
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@bp.route("/api/scheduler/jobs/delete", methods=["POST"])
|
|
def scheduler_jobs_delete():
|
|
try:
|
|
sm = _sm()
|
|
job_id = (request.get_json() or {}).get("id", "")
|
|
if not job_id:
|
|
return jsonify({"error": "id required"}), 400
|
|
jobs = [j for j in sm.load_jobs() if j["id"] != job_id]
|
|
sm.save_jobs(jobs)
|
|
try:
|
|
_sched().reload()
|
|
except Exception:
|
|
pass
|
|
_audit("scheduler_job_delete", f"id={job_id!r}", ip=request.remote_addr or "")
|
|
return jsonify({"ok": True})
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
# ── Run now ───────────────────────────────────────────────────────────────────
|
|
|
|
@bp.route("/api/scheduler/jobs/run_now", methods=["POST"])
|
|
def scheduler_jobs_run_now():
|
|
job_id = (request.get_json() or {}).get("id", "")
|
|
s = _sched()
|
|
if job_id in s._running_jobs:
|
|
return jsonify({"error": "Job already running"}), 409
|
|
if s.is_running:
|
|
return jsonify({"error": "Another scan is already running"}), 409
|
|
threading.Thread(target=s._execute_scan, args=[job_id], daemon=True).start()
|
|
return jsonify({"status": "started"})
|
|
|
|
|
|
# ── Status ────────────────────────────────────────────────────────────────────
|
|
|
|
@bp.route("/api/scheduler/status")
|
|
def scheduler_status():
|
|
return jsonify(_sched().get_status())
|
|
|
|
|
|
# ── History ───────────────────────────────────────────────────────────────────
|
|
|
|
@bp.route("/api/scheduler/history")
|
|
def scheduler_history():
|
|
db = _db()
|
|
if not db:
|
|
return jsonify({"runs": []})
|
|
try:
|
|
limit = int(request.args.get("limit", 20))
|
|
job_id = request.args.get("job_id")
|
|
try:
|
|
runs = db.get_schedule_runs(limit=limit, job_id=job_id)
|
|
except TypeError:
|
|
runs = db.get_schedule_runs(limit=limit)
|
|
return jsonify({"runs": runs})
|
|
except Exception as e:
|
|
return jsonify({"runs": [], "error": str(e)})
|
|
|
|
|
|
# ── Backward-compat single-job endpoints ─────────────────────────────────────
|
|
|
|
@bp.route("/api/scheduler/config", methods=["GET"])
|
|
def scheduler_config_get():
|
|
return jsonify(_sm().load_schedule_config())
|
|
|
|
|
|
@bp.route("/api/scheduler/config", methods=["POST"])
|
|
def scheduler_config_save():
|
|
sm = _sm()
|
|
data = request.get_json() or {}
|
|
merged = {**sm.load_schedule_config(), **data}
|
|
sm.save_schedule_config(merged)
|
|
s = _sched()
|
|
s.reload()
|
|
return jsonify({"status": "saved", "config": merged,
|
|
"next_run": s.next_run_time()})
|
|
|
|
|
|
@bp.route("/api/scheduler/run_now", methods=["POST"])
|
|
def scheduler_run_now():
|
|
s = _sched()
|
|
if s.is_running:
|
|
return jsonify({"error": "Scheduled scan already running"}), 409
|
|
threading.Thread(target=s._execute_scan, args=[None], daemon=True).start()
|
|
return jsonify({"status": "started"})
|