#!/usr/bin/env python3 """Secure a Monitor installation before first production use. The script uses only the Python standard library. It backs up the SQLite database, replaces all login users with one new administrator, clears sessions, and rotates file-backed runtime secrets. Stop Monitor before running it. """ import argparse import base64 import getpass import hashlib import os import secrets import shutil import sqlite3 import sys from datetime import datetime from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) BUSINESS_TABLES = ( "av_alarm", "av_zone_algorithms", "av_zone", "av_biz_algorithm", "av_llm", "av_recording", "av_stream", "av_algorithm", "av_log", ) def _password_hash(password): iterations = 720000 salt = secrets.token_urlsafe(12) digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations) return "pbkdf2_sha256$%d$%s$%s" % ( iterations, salt, base64.b64encode(digest).decode("ascii") ) def _read_password(username): password = getpass.getpass("New administrator password: ") confirm = getpass.getpass("Confirm administrator password: ") if password != confirm: raise ValueError("password confirmation does not match") if len(password) < 12: raise ValueError("administrator password must be at least 12 characters") if username.lower() in password.lower(): raise ValueError("administrator password must not contain the username") return password def main(): parser = argparse.ArgumentParser(description="Securely initialize a Monitor installation") parser.add_argument("--database", default=str(ROOT / "monitor.sqlite3")) parser.add_argument("--admin-username", default="admin") parser.add_argument("--admin-email", default="") parser.add_argument( "--purge-business-data", action="store_true", help="also remove cameras, models, rules, alarms, recordings and operation logs", ) args = parser.parse_args() username = args.admin_username.strip() if not username or len(username) > 150: raise ValueError("invalid administrator username") password = _read_password(username) database = Path(args.database).resolve() if not database.is_file(): raise FileNotFoundError(database) stamp = datetime.now().strftime("%Y%m%d%H%M%S") backup = database.with_name(database.name + ".backup-" + stamp) shutil.copy2(database, backup) con = sqlite3.connect(str(database), timeout=20) try: con.execute("PRAGMA foreign_keys=ON") con.execute("BEGIN IMMEDIATE") con.execute("DELETE FROM auth_user_groups") con.execute("DELETE FROM auth_user_user_permissions") con.execute("DELETE FROM django_admin_log") con.execute("DELETE FROM django_session") con.execute("DELETE FROM auth_user") if args.purge_business_data: for table in BUSINESS_TABLES: con.execute('DELETE FROM "%s"' % table) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") con.execute( """INSERT INTO auth_user (password,last_login,is_superuser,username,last_name,email,is_staff,is_active,date_joined,first_name) VALUES (?,NULL,1,?,'',?,1,1,?,'cec=0')""", (_password_hash(password), username, args.admin_email.strip(), now), ) con.commit() except Exception: con.rollback() raise finally: con.close() from app.utils.Secrets import rotate_runtime_secrets secret_backup = rotate_runtime_secrets() print("Database backup:", backup) if secret_backup: print("Runtime-secret backup:", secret_backup) print("Secure initialization complete. Restart Monitor before signing in.") if __name__ == "__main__": main()