video_monitor/scripts/secure_initialize.py
2026-08-30 22:23:12 +08:00

111 lines
3.8 KiB
Python

#!/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()