111 lines
3.8 KiB
Python
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()
|