from flask import Flask, render_template, request, jsonify, redirect, url_for import os import ssh_client import dns_client import config import detector import docker_store import hardening import security_apps import ssl_audit import monitoring import ddos import backup import sec_updates import clamav import rkhunter import chkrootkit import lynis import ossec import modsecurity import aide import cowrie import iptables import nftables import firewalld import csf import hosting import audit import sanitize import traceback import secrets app = Flask(__name__) cfg_init = config.load() app.secret_key = cfg_init.get("flask_secret", secrets.token_hex(32)) app.config["SESSION_COOKIE_HTTPONLY"] = True app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # ── Helpers ────────────────────────────────────────────────────────── def ssh_run(cmd, timeout=30): """Run command via SSH (uses E2E when enabled).""" try: return ssh_client.run(cmd, timeout=timeout) except Exception as e: return {"stdout": "", "stderr": str(e), "exit_code": -1} def ssh_run_plain(cmd, timeout=30): """Run command via plain SSH (bypasses E2E — for agent deployment only).""" try: return ssh_client.run_plain_always(cmd, timeout=timeout) except Exception as e: return {"stdout": "", "stderr": str(e), "exit_code": -1} def api_wrap(fn): """Wrap an API call, return JSON with error handling.""" try: result = fn() return jsonify({"ok": True, "data": result}) except Exception as e: return jsonify({"ok": False, "error": str(e), "trace": traceback.format_exc()}), 500 @app.after_request def set_security_headers(response): response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-XSS-Protection"] = "1; mode=block" response.headers["Referrer-Policy"] = "same-origin" return response @app.route("/api/wizard/accept-tos", methods=["POST"]) def api_wizard_accept_tos(): """Mark TOS as accepted — this is what unlocks the rest of the app.""" cfg = config.load() cfg["tos_accepted"] = True config.save(cfg) audit.log("tos_accepted", ip=request.remote_addr) return jsonify({"ok": True}) @app.route("/api/audit/log") def api_audit_log(): count = request.args.get("count", 100, type=int) return jsonify({"ok": True, "data": audit.get_recent(count)}) # ── API: E2E Tunnel ────────────────────────────────────────────────── @app.route("/api/e2e/status") def api_e2e_status(): """Check E2E tunnel status.""" import e2e cfg = config.load() return jsonify({"ok": True, "data": { "e2e_enabled": cfg.get("e2e_enabled", False), "e2e_deployed": cfg.get("e2e_tunnel_key_deployed", False), "agent_path": e2e.AGENT_PATH, }}) @app.route("/api/e2e/toggle", methods=["POST"]) def api_e2e_toggle(): """Enable or disable E2E tunnel encryption.""" import e2e data = request.json or {} enabled = data.get("enabled") cfg = config.load() if enabled and not cfg.get("e2e_tunnel_key_deployed", False): return jsonify({"ok": False, "error": "Deploy the E2E agent first before enabling."}), 400 if enabled and not cfg.get("tunnel_key", ""): return jsonify({"ok": False, "error": "No tunnel key configured."}), 400 cfg["e2e_enabled"] = bool(enabled) config.save(cfg) state = "enabled" if enabled else "disabled" audit.log("e2e_toggled", ip=request.remote_addr, details=f"E2E tunnel {state}") return jsonify({"ok": True, "data": {"e2e_enabled": cfg["e2e_enabled"]}}) @app.route("/api/e2e/deploy", methods=["POST"]) def api_e2e_deploy(): """Deploy the E2E agent and tunnel key to the VPS.""" import e2e import subprocess cfg = config.load() # Derive tunnel key or generate a new one tunnel_key_hex = cfg.get("tunnel_key", "") if not tunnel_key_hex: # Generate tunnel key from password-based derivation # The tunnel key is stored in config for future use import os tunnel_key = os.urandom(32) tunnel_key_hex = tunnel_key.hex() cfg["tunnel_key"] = tunnel_key_hex config.save(cfg) else: tunnel_key = bytes.fromhex(tunnel_key_hex) results = [] # Step 1: Deploy tunnel key deploy_cmds = e2e.generate_deploy_commands(tunnel_key) for desc, cmd in deploy_cmds: res = ssh_run_plain(cmd, timeout=10) results.append({"step": desc, "ok": res["exit_code"] == 0, "output": res["stdout"] + res["stderr"]}) if res["exit_code"] != 0: return jsonify({"ok": False, "error": f"Failed at: {desc}", "data": results}) # Step 2: Upload the Go agent binary # Cross-compile if possible, otherwise upload from pre-built agent_dir = os.path.join(os.path.dirname(__file__), "agent") agent_binary = os.path.join(agent_dir, "setec-agent") # Try to cross-compile try: env = dict(os.environ) env["GOOS"] = "linux" env["GOARCH"] = "amd64" env["CGO_ENABLED"] = "0" compile_result = subprocess.run( ["go", "build", "-o", agent_binary, "-ldflags=-s -w", "."], cwd=agent_dir, capture_output=True, text=True, timeout=60, env=env ) if compile_result.returncode != 0: results.append({"step": "Cross-compile agent", "ok": False, "output": compile_result.stderr}) return jsonify({"ok": False, "error": "Go cross-compile failed. Install Go or pre-build the agent.", "data": results}) results.append({"step": "Cross-compile agent", "ok": True, "output": "Built setec-agent for linux/amd64"}) except FileNotFoundError: return jsonify({"ok": False, "error": "Go not found. Install Go to cross-compile the E2E agent.", "data": results}) except Exception as ex: return jsonify({"ok": False, "error": f"Compile error: {str(ex)}", "data": results}) # Step 3: Upload agent via SCP try: key_path = cfg["ssh_key_path"] host = cfg["vps_host"] user = cfg["vps_user"] port = cfg["vps_port"] scp_cmd = ( f'scp -i "{key_path}" -o StrictHostKeyChecking=no -P {port} ' f'"{agent_binary}" {user}@{host}:{e2e.AGENT_PATH}' ) scp_result = subprocess.run(scp_cmd, shell=True, capture_output=True, text=True, timeout=30) if scp_result.returncode != 0: results.append({"step": "Upload agent", "ok": False, "output": scp_result.stderr}) return jsonify({"ok": False, "error": "SCP upload failed", "data": results}) results.append({"step": "Upload agent", "ok": True, "output": "Uploaded to " + e2e.AGENT_PATH}) except Exception as ex: return jsonify({"ok": False, "error": f"Upload error: {str(ex)}", "data": results}) # Step 4: Set permissions on the agent res = ssh_run_plain(f"chmod 755 {e2e.AGENT_PATH} && {e2e.AGENT_PATH} --help 2>&1 || echo 'agent installed'", timeout=10) results.append({"step": "Set agent permissions", "ok": True, "output": res["stdout"]}) # Mark E2E as deployed cfg["e2e_tunnel_key_deployed"] = True config.save(cfg) audit.log("e2e_deployed", ip=request.remote_addr, details="Agent + tunnel key deployed to VPS") return jsonify({"ok": True, "data": results}) @app.route("/api/e2e/test", methods=["POST"]) def api_e2e_test(): """Test E2E encrypted command execution.""" import e2e if not e2e.is_e2e_enabled(): return jsonify({"ok": False, "error": "E2E not deployed yet"}) # Send an encrypted test command result = ssh_run("echo 'E2E tunnel active' && date && whoami", timeout=15) if result["exit_code"] == 0 and "E2E tunnel active" in result["stdout"]: return jsonify({"ok": True, "data": { "message": "E2E tunnel working", "output": result["stdout"], }}) else: return jsonify({"ok": False, "error": "E2E test failed", "data": result}) # ── Pages ──────────────────────────────────────────────────────────── @app.route("/") def dashboard(): cfg = config.load() if not cfg.get("tos_accepted", False): return redirect(url_for("wizard_page")) return render_template("dashboard.html") @app.route("/docker") def docker_page(): return render_template("docker.html") @app.route("/dns") def dns_page(): return render_template("dns.html") @app.route("/nginx") def nginx_page(): return render_template("nginx.html") @app.route("/smtp") def smtp_page(): return render_template("smtp.html") @app.route("/files") def files_page(): return render_template("files.html") @app.route("/terminal") def terminal_page(): return render_template("terminal.html") @app.route("/configs") def configs_page(): return render_template("configs.html") @app.route("/detect") def detect_page(): return render_template("detect.html") @app.route("/fail2ban") def fail2ban_page(): return render_template("fail2ban.html") @app.route("/frontpage") def frontpage_page(): return render_template("frontpage.html") @app.route("/settings") def settings_page(): return render_template("settings.html") @app.route("/firewall") def firewall_page(): return render_template("firewall.html") @app.route("/security") def security_page(): return render_template("security.html") @app.route("/wizard") def wizard_page(): return render_template("wizard.html") @app.route("/docs") def docs_page(): return render_template("docs.html") # ── API: Server Status ────────────────────────────────────────────── @app.route("/api/status") def api_status(): return api_wrap(lambda: ssh_run( "echo '=== HOSTNAME ===' && hostname && " "echo '=== UPTIME ===' && uptime && " "echo '=== MEMORY ===' && free -h && " "echo '=== DISK ===' && df -h / && " "echo '=== LOAD ===' && cat /proc/loadavg && " "echo '=== DOCKER ===' && docker ps --format '{{.Names}}: {{.Status}}' 2>/dev/null && " "echo '=== NGINX ===' && systemctl is-active nginx && " "echo '=== POSTFIX ===' && systemctl is-active postfix 2>/dev/null" )) @app.route("/api/status/domains") def api_domain_status(): cfg = config.load() domain = cfg["domain"] subs = ["", "repo.", "git.", "app.", "files.", "lists."] cmd = " && ".join( f"echo '{s}{domain}: '$(curl -s -o /dev/null -w '%{{http_code}}' --max-time 5 https://{s}{domain})" for s in subs ) return api_wrap(lambda: ssh_run(cmd)) # ── API: Docker ────────────────────────────────────────────────────── @app.route("/api/docker/list") def api_docker_list(): return api_wrap(lambda: ssh_run( "docker ps -a --format '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}'" )) @app.route("/api/docker//", methods=["POST"]) def api_docker_action(action, name): if action not in ("start", "stop", "restart"): return jsonify({"ok": False, "error": "invalid action"}), 400 return api_wrap(lambda: ssh_run(f"docker {action} {name} 2>&1")) @app.route("/api/docker/logs/") def api_docker_logs(name): lines = request.args.get("lines", 50, type=int) return api_wrap(lambda: ssh_run(f"docker logs --tail {lines} {name} 2>&1")) @app.route("/api/docker/compose/", methods=["POST"]) def api_docker_compose(action): cfg = config.load() compose_dir = cfg["compose_path"].rsplit("/", 1)[0] if action == "up": cmd = f"cd {compose_dir} && docker compose up -d 2>&1" elif action == "down": cmd = f"cd {compose_dir} && docker compose down 2>&1" elif action == "pull": cmd = f"cd {compose_dir} && docker compose pull 2>&1" else: return jsonify({"ok": False, "error": "invalid action"}), 400 return api_wrap(lambda: ssh_run(cmd, timeout=120)) # ── API: Docker Store ──────────────────────────────────────────────── @app.route("/api/docker/store") def api_docker_store(): return jsonify({"ok": True, "data": docker_store.STORE, "categories": docker_store.CATEGORIES}) @app.route("/api/docker/store/install", methods=["POST"]) def api_docker_store_install(): """Install an app from the store by appending to docker-compose.yml and running up.""" data = request.json name = data.get("name", "") app_entry = next((a for a in docker_store.STORE if a["name"] == name), None) if not app_entry: return jsonify({"ok": False, "error": f"App '{name}' not found in store"}), 400 cfg = config.load() compose_path = cfg["compose_path"] snippet = app_entry["compose"] # Append the service to docker-compose.yml, then run up cmd = ( f"echo '' >> {compose_path} && " f"cat >> {compose_path} << 'STOREEOF'\n{snippet}\nSTOREEOF\n" f"cd {compose_path.rsplit('/', 1)[0]} && docker compose up -d 2>&1" ) return api_wrap(lambda: ssh_run(cmd, timeout=120)) @app.route("/api/docker/install-git", methods=["POST"]) def api_docker_install_git(): """Clone a git repo and run docker compose up from it.""" data = request.json repo = data.get("repo", "") name = data.get("name", "") if not repo: return jsonify({"ok": False, "error": "no repo URL"}), 400 if not name: # Extract name from repo URL name = repo.rstrip("/").split("/")[-1].replace(".git", "") install_dir = f"/opt/seteclabs/{name}" cmd = ( f"apt-get install -y git > /dev/null 2>&1; " f"git clone '{repo}' '{install_dir}' 2>&1 && " f"cd '{install_dir}' && " f"if [ -f docker-compose.yml ] || [ -f docker-compose.yaml ] || [ -f compose.yml ] || [ -f compose.yaml ]; then " f" docker compose up -d 2>&1; " f"else " f" echo 'No docker-compose file found in repo. Contents:' && ls -la; " f"fi" ) return api_wrap(lambda: ssh_run(cmd, timeout=120)) @app.route("/api/docker/install-url", methods=["POST"]) def api_docker_install_url(): """Download from a URL and run/install it.""" data = request.json url = data.get("url", "") name = data.get("name", "") if not url or not name: return jsonify({"ok": False, "error": "need url and name"}), 400 install_dir = f"/opt/seteclabs/{name}" filename = url.split("/")[-1].split("?")[0] cmd = ( f"mkdir -p '{install_dir}' && cd '{install_dir}' && " f"curl -fSL -o '{filename}' '{url}' 2>&1 && " f"echo 'Downloaded: {filename}' && " f"ls -lh '{filename}' && " f"if echo '{filename}' | grep -qE '\\.(tar\\.gz|tgz)$'; then " f" tar xzf '{filename}' 2>&1 && echo 'Extracted tar.gz'; " f"elif echo '{filename}' | grep -qE '\\.zip$'; then " f" unzip -o '{filename}' 2>&1 && echo 'Extracted zip'; " f"elif echo '{filename}' | grep -qE '\\.tar$'; then " f" tar xf '{filename}' 2>&1 && echo 'Extracted tar'; " f"fi && " f"if [ -f docker-compose.yml ] || [ -f docker-compose.yaml ] || [ -f compose.yml ]; then " f" docker compose up -d 2>&1; " f"else " f" echo 'Contents:' && ls -la; " f"fi" ) return api_wrap(lambda: ssh_run(cmd, timeout=120)) @app.route("/api/docker/install-compose", methods=["POST"]) def api_docker_install_compose(): """Install from a raw docker-compose snippet.""" data = request.json name = data.get("name", "") compose_content = data.get("compose", "") if not name or not compose_content: return jsonify({"ok": False, "error": "need name and compose content"}), 400 install_dir = f"/opt/seteclabs/{name}" cmd = ( f"mkdir -p '{install_dir}' && " f"cat > '{install_dir}/docker-compose.yml' << 'COMPEOF'\n{compose_content}\nCOMPEOF\n" f"cd '{install_dir}' && docker compose up -d 2>&1" ) return api_wrap(lambda: ssh_run(cmd, timeout=120)) # ── API: DNS ───────────────────────────────────────────────────────── @app.route("/api/dns/records") def api_dns_records(): return api_wrap(dns_client.get_records) @app.route("/api/dns/add", methods=["POST"]) def api_dns_add(): data = request.json rtype = data.get("type", "A") name = data.get("name", "") value = data.get("value", "") if rtype == "A": return api_wrap(lambda: dns_client.add_a_record(name, value)) elif rtype == "TXT": return api_wrap(lambda: dns_client.add_txt_record(name, value)) return jsonify({"ok": False, "error": "unsupported type"}), 400 @app.route("/api/dns/delete/", methods=["DELETE"]) def api_dns_delete(record_id): return api_wrap(lambda: dns_client.delete_record(record_id)) # ── API: Nginx ─────────────────────────────────────────────────────── @app.route("/api/nginx/sites") def api_nginx_sites(): return api_wrap(lambda: ssh_run( "ls -1 /etc/nginx/sites-enabled/ 2>/dev/null && echo '---' && nginx -T 2>/dev/null | grep server_name" )) @app.route("/api/nginx/config/") def api_nginx_config(site): return api_wrap(lambda: ssh_run(f"cat /etc/nginx/sites-available/{site} 2>/dev/null")) @app.route("/api/nginx/add-subdomain", methods=["POST"]) def api_nginx_add_subdomain(): data = request.json subdomain = data.get("subdomain", "") proxy_port = data.get("proxy_port") static_root = data.get("static_root") cfg = config.load() fqdn = f"{subdomain}.{cfg['domain']}" if subdomain else cfg["domain"] if proxy_port: block = f"""server {{ listen 80; server_name {fqdn}; location / {{ proxy_pass http://127.0.0.1:{proxy_port}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }} }}""" elif static_root: block = f"""server {{ listen 80; server_name {fqdn}; root {static_root}; index index.html; location / {{ try_files $uri $uri/ =404; }} }}""" else: return jsonify({"ok": False, "error": "need proxy_port or static_root"}), 400 escaped = block.replace("'", "'\\''") cmd = ( f"echo '{escaped}' > /etc/nginx/sites-available/{fqdn} && " f"ln -sf /etc/nginx/sites-available/{fqdn} /etc/nginx/sites-enabled/{fqdn} && " f"nginx -t 2>&1 && systemctl reload nginx 2>&1 && " f"echo 'Site {fqdn} added and nginx reloaded'" ) return api_wrap(lambda: ssh_run(cmd)) @app.route("/api/nginx/ssl/", methods=["POST"]) def api_nginx_ssl(site): return api_wrap(lambda: ssh_run( f"certbot --nginx -d {site} --non-interactive --agree-tos -m admin@{config.load()['domain']} 2>&1", timeout=60, )) @app.route("/api/nginx/reload", methods=["POST"]) def api_nginx_reload(): return api_wrap(lambda: ssh_run("nginx -t 2>&1 && systemctl reload nginx 2>&1")) # ── API: SMTP ──────────────────────────────────────────────────────── @app.route("/api/smtp/status") def api_smtp_status(): return api_wrap(lambda: ssh_run( "echo '=== Postfix ===' && systemctl is-active postfix && " "echo '=== Queue ===' && mailq | tail -5 && " "echo '=== DKIM ===' && systemctl is-active opendkim && " "echo '=== Listmonk ===' && docker ps --filter name=listmonk --format '{{.Names}}: {{.Status}}' && " "echo '=== Recent Log ===' && (journalctl -u postfix --no-pager -n 10 2>/dev/null || tail -10 /var/log/mail.log)" )) @app.route("/api/smtp/send-test", methods=["POST"]) def api_smtp_send_test(): to = request.json.get("to", "") domain = config.load()["domain"] return api_wrap(lambda: ssh_run( f"echo 'SETEC LABS mail test - sent at $(date)' | mail -s 'SETEC LABS - SMTP Test' -r noreply@{domain} '{to}' 2>&1 && echo 'Test email sent to {to}'" )) @app.route("/api/smtp/flush", methods=["POST"]) def api_smtp_flush(): return api_wrap(lambda: ssh_run("postfix flush 2>&1 && echo 'Queue flushed'")) @app.route("/api/smtp/restart", methods=["POST"]) def api_smtp_restart(): return api_wrap(lambda: ssh_run("systemctl restart postfix opendkim 2>&1 && echo 'Postfix + DKIM restarted'")) @app.route("/api/smtp/dns-check") def api_smtp_dns(): domain = config.load()["domain"] return api_wrap(lambda: ssh_run( f"echo '=== SPF ===' && dig +short TXT {domain} | grep spf && " f"echo '=== DKIM ===' && dig +short TXT setec._domainkey.{domain} && " f"echo '=== DMARC ===' && dig +short TXT _dmarc.{domain} && " f"echo '=== MX ===' && dig +short MX {domain}" )) # ── API: SMTP Send ────────────────────────────────────────────────── @app.route("/api/smtp/send", methods=["POST"]) def api_smtp_send(): data = request.json from_addr = data.get("from", "noreply@seteclabs.io") to = data.get("to", "") subject = data.get("subject", "") body = data.get("body", "") if not to or not subject: return jsonify({"ok": False, "error": "need to and subject"}), 400 cmd = ( f"echo '{body}' | mail -s '{subject}' -r '{from_addr}' '{to}' 2>&1 && " f"echo 'Email sent to {to}'" ) return api_wrap(lambda: ssh_run(cmd)) @app.route("/api/smtp/send-mass", methods=["POST"]) def api_smtp_send_mass(): """Send individual emails to each recipient (BCC mode - no one sees others).""" data = request.json from_addr = data.get("from", "noreply@seteclabs.io") recipients = data.get("recipients", []) subject = data.get("subject", "") body = data.get("body", "") if not recipients or not subject: return jsonify({"ok": False, "error": "need recipients and subject"}), 400 # Build a script that sends one email per recipient lines = ["#!/bin/bash", "SENT=0", "FAILED=0"] for addr in recipients: addr = addr.strip().replace("'", "") if not addr or "@" not in addr: continue lines.append( f"echo '{body}' | mail -s '{subject}' -r '{from_addr}' '{addr}' 2>/dev/null " f"&& SENT=$((SENT+1)) || FAILED=$((FAILED+1))" ) lines.append('echo "Sent: $SENT | Failed: $FAILED | Total: $((SENT+FAILED))"') script = "\n".join(lines) cmd = f"bash << 'MASSEOF'\n{script}\nMASSEOF" return api_wrap(lambda: ssh_run(cmd, timeout=min(len(recipients) * 3 + 10, 120))) # ── API: File Manager ─────────────────────────────────────────────── @app.route("/api/files/list") def api_files_list(): path = request.args.get("path", "/var/www") return api_wrap(lambda: ssh_run(f"ls -la {path} 2>&1")) @app.route("/api/files/read") def api_files_read(): path = request.args.get("path", "") return api_wrap(lambda: ssh_run(f"cat {path} 2>&1")) @app.route("/api/files/write", methods=["POST"]) def api_files_write(): data = request.json path = data.get("path", "") content = data.get("content", "") escaped = content.replace("\\", "\\\\").replace("'", "'\\''") return api_wrap(lambda: ssh_run(f"cat > {path} << 'SETECEOF'\n{content}\nSETECEOF")) @app.route("/api/files/mkdir", methods=["POST"]) def api_files_mkdir(): path = request.json.get("path", "") return api_wrap(lambda: ssh_run(f"mkdir -p {path} 2>&1 && echo 'Created {path}'")) @app.route("/api/files/delete", methods=["DELETE"]) def api_files_delete(): path = request.json.get("path", "") return api_wrap(lambda: ssh_run(f"rm -rf {path} 2>&1 && echo 'Deleted {path}'")) # ── API: Deploy ────────────────────────────────────────────────────── @app.route("/api/deploy/site", methods=["POST"]) def api_deploy_site(): """Deploy local site/ folder to VPS via tar over SSH.""" import subprocess, os cfg = config.load() site_dir = os.path.join(os.path.dirname(__file__), "..", "site") site_dir = os.path.abspath(site_dir) if not os.path.isdir(site_dir): return jsonify({"ok": False, "error": f"site/ directory not found at {site_dir}"}), 400 key = cfg["ssh_key_path"] host = cfg["vps_host"] user = cfg["vps_user"] port = cfg["vps_port"] web_root = f"{cfg['web_root']}/{cfg['domain']}" try: # tar the site, pipe over ssh, extract on server cmd = ( f'cd "{site_dir}" && tar cf - . | ' f'ssh -i "{key}" -o StrictHostKeyChecking=no -p {port} {user}@{host} ' f'"mkdir -p {web_root} && tar xf - -C {web_root}"' ) result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30) return jsonify({ "ok": result.returncode == 0, "data": {"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode}, }) except Exception as e: return jsonify({"ok": False, "error": str(e)}), 500 # ── API: Front Page Editor ────────────────────────────────────────── @app.route("/api/frontpage/list") def api_frontpage_list(): cfg = config.load() web_root = f"{cfg['web_root']}/{cfg['domain']}" return api_wrap(lambda: ssh_run( f"find {web_root} -type f \\( -name '*.html' -o -name '*.css' -o -name '*.js' \\) " f"-printf '%P|%s|%T@\\n' 2>/dev/null | sort" )) @app.route("/api/frontpage/read") def api_frontpage_read(): cfg = config.load() web_root = f"{cfg['web_root']}/{cfg['domain']}" filename = request.args.get("file", "") if not filename or ".." in filename: return jsonify({"ok": False, "error": "invalid file"}), 400 path = f"{web_root}/{filename}" return api_wrap(lambda: ssh_run(f"cat '{path}' 2>&1")) @app.route("/api/frontpage/write", methods=["POST"]) def api_frontpage_write(): cfg = config.load() web_root = f"{cfg['web_root']}/{cfg['domain']}" data = request.json filename = data.get("file", "") content = data.get("content", "") if not filename or ".." in filename: return jsonify({"ok": False, "error": "invalid file"}), 400 path = f"{web_root}/{filename}" backup_cmd = f"cp '{path}' '{path}.bak.$(date +%Y%m%d_%H%M%S)' 2>/dev/null; " write_cmd = f"cat > '{path}' << 'SETECEOF'\n{content}\nSETECEOF" return api_wrap(lambda: ssh_run(backup_cmd + write_cmd)) @app.route("/api/frontpage/new", methods=["POST"]) def api_frontpage_new(): cfg = config.load() web_root = f"{cfg['web_root']}/{cfg['domain']}" data = request.json filename = data.get("file", "") if not filename or ".." in filename: return jsonify({"ok": False, "error": "invalid file"}), 400 path = f"{web_root}/{filename}" return api_wrap(lambda: ssh_run( f"mkdir -p $(dirname '{path}') && touch '{path}' 2>&1 && echo 'Created {filename}'" )) @app.route("/api/frontpage/delete", methods=["DELETE"]) def api_frontpage_delete(): cfg = config.load() web_root = f"{cfg['web_root']}/{cfg['domain']}" filename = request.json.get("file", "") if not filename or ".." in filename: return jsonify({"ok": False, "error": "invalid file"}), 400 path = f"{web_root}/{filename}" return api_wrap(lambda: ssh_run(f"rm '{path}' 2>&1 && echo 'Deleted {filename}'")) @app.route("/api/frontpage/preview-url") def api_frontpage_preview(): cfg = config.load() return jsonify({"ok": True, "data": f"https://{cfg['domain']}"}) # ── API: Terminal ──────────────────────────────────────────────────── @app.route("/api/terminal/exec", methods=["POST"]) def api_terminal_exec(): cmd = request.json.get("cmd", "") timeout = request.json.get("timeout", 30) return api_wrap(lambda: ssh_run(cmd, timeout=min(timeout, 120))) # ── API: Settings ──────────────────────────────────────────────────── @app.route("/api/settings", methods=["GET"]) def api_settings_get(): return jsonify({"ok": True, "data": config.safe_config()}) @app.route("/api/settings", methods=["POST"]) def api_settings_save(): data = request.json cfg = config.load() for k in ("vps_host", "vps_user", "vps_port", "ssh_key_path", "domain", "web_root", "compose_path", "flask_port", "hosting_provider"): if k in data: cfg[k] = data[k] if data.get("hostinger_api_key") and "..." not in data["hostinger_api_key"]: cfg["hostinger_api_key"] = data["hostinger_api_key"] # Mark setup complete when saving from wizard if data.get("setup_complete"): cfg["setup_complete"] = True config.save(cfg) ssh_client.close() # reconnect with new settings audit.log("settings_changed", ip=request.remote_addr, details=", ".join(data.keys())) return jsonify({"ok": True}) @app.route("/api/ssh/test") def api_ssh_test(): return api_wrap(lambda: ssh_run("echo 'SSH connection OK' && hostname && whoami")) # ── API: Fail2Ban ──────────────────────────────────────────────────── @app.route("/api/fail2ban/status") def api_f2b_status(): return api_wrap(lambda: ssh_run("fail2ban-client status 2>&1")) @app.route("/api/fail2ban/jail/") def api_f2b_jail(name): return api_wrap(lambda: ssh_run(f"fail2ban-client status {name} 2>&1")) @app.route("/api/fail2ban/ban", methods=["POST"]) def api_f2b_ban(): data = request.json jail = data.get("jail", "sshd") ip = data.get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(f"fail2ban-client set {jail} banip {ip} 2>&1")) @app.route("/api/fail2ban/unban", methods=["POST"]) def api_f2b_unban(): data = request.json jail = data.get("jail", "sshd") ip = data.get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(f"fail2ban-client set {jail} unbanip {ip} 2>&1")) @app.route("/api/fail2ban/unban-all", methods=["POST"]) def api_f2b_unban_all(): return api_wrap(lambda: ssh_run("fail2ban-client unban --all 2>&1")) @app.route("/api/fail2ban/jails") def api_f2b_jails(): return api_wrap(lambda: ssh_run( "for j in $(fail2ban-client status | grep 'Jail list' | sed 's/.*://;s/,//g'); do " "echo \"=== $j ===\"; fail2ban-client status $j 2>&1; echo; done" )) @app.route("/api/fail2ban/config") def api_f2b_config(): return api_wrap(lambda: ssh_run("cat /etc/fail2ban/jail.local 2>&1")) @app.route("/api/fail2ban/config/save", methods=["POST"]) def api_f2b_config_save(): content = request.json.get("content", "") cmd = ( "cp /etc/fail2ban/jail.local /etc/fail2ban/jail.local.bak.$(date +%Y%m%d_%H%M%S) 2>/dev/null; " f"cat > /etc/fail2ban/jail.local << 'F2BEOF'\n{content}\nF2BEOF\n" "fail2ban-client reload 2>&1 && echo 'Config saved and fail2ban reloaded'" ) return api_wrap(lambda: ssh_run(cmd)) @app.route("/api/fail2ban/reload", methods=["POST"]) def api_f2b_reload(): return api_wrap(lambda: ssh_run("fail2ban-client reload 2>&1 && echo 'Reloaded'")) @app.route("/api/fail2ban/log") def api_f2b_log(): lines = request.args.get("lines", 50, type=int) return api_wrap(lambda: ssh_run(f"tail -{lines} /var/log/fail2ban.log 2>&1")) # ── API: Service Detection ────────────────────────────────────────── @app.route("/api/detect/scan") def api_detect_scan(): cmd = detector.build_detect_command() result = ssh_run(cmd, timeout=30) if result["exit_code"] not in (0, -1) and not result["stdout"]: return jsonify({"ok": False, "error": result["stderr"]}), 500 detected = detector.parse_detection(result["stdout"]) return jsonify({"ok": True, "data": detected}) @app.route("/api/detect/all-services") def api_detect_all(): """Return the full service database for browsing.""" by_cat = {} for svc in detector.SERVICES: cat = detector.CATEGORIES.get(svc["cat"], svc["cat"]) by_cat.setdefault(cat, []).append({ "name": svc["name"], "ports": svc["ports"], "configs": svc["configs"], "packages": svc["pkg"], }) return jsonify({"ok": True, "data": by_cat}) # ── API: Config Editor ────────────────────────────────────────────── @app.route("/api/configs/read") def api_configs_read(): path = request.args.get("path", "") if not path: return jsonify({"ok": False, "error": "no path"}), 400 return api_wrap(lambda: ssh_run(f"cat '{path}' 2>&1")) @app.route("/api/configs/write", methods=["POST"]) def api_configs_write(): data = request.json path = data.get("path", "") content = data.get("content", "") if not path: return jsonify({"ok": False, "error": "no path"}), 400 # Backup before writing backup_cmd = f"cp '{path}' '{path}.bak.$(date +%Y%m%d_%H%M%S)' 2>/dev/null; " write_cmd = f"cat > '{path}' << 'SETECEOF'\n{content}\nSETECEOF" return api_wrap(lambda: ssh_run(backup_cmd + write_cmd)) @app.route("/api/configs/test-nginx", methods=["POST"]) def api_configs_test_nginx(): return api_wrap(lambda: ssh_run("nginx -t 2>&1")) @app.route("/api/configs/reload-service", methods=["POST"]) def api_configs_reload_service(): svc = request.json.get("service", "") if not svc: return jsonify({"ok": False, "error": "no service name"}), 400 allowed = {"nginx", "postfix", "opendkim", "sshd", "fail2ban", "ufw", "docker", "redis", "postgresql", "mysql", "grafana-server", "prometheus", "dovecot", "unbound", "bind9", "haproxy", "php8.1-fpm", "php8.2-fpm", "php8.3-fpm", "supervisor"} if svc not in allowed: return api_wrap(lambda: ssh_run(f"systemctl reload {svc} 2>&1 || systemctl restart {svc} 2>&1")) return api_wrap(lambda: ssh_run(f"systemctl reload {svc} 2>&1 || systemctl restart {svc} 2>&1")) @app.route("/api/configs/diff", methods=["POST"]) def api_configs_diff(): path = request.json.get("path", "") return api_wrap(lambda: ssh_run( f"ls -1t '{path}'.bak.* 2>/dev/null | head -1 | xargs -I{{}} diff '{{}}' '{path}' 2>&1 || echo 'No backup found'" )) @app.route("/api/configs/backups") def api_configs_backups(): path = request.args.get("path", "") return api_wrap(lambda: ssh_run(f"ls -lt '{path}'.bak.* 2>/dev/null | head -10 || echo 'No backups'")) @app.route("/api/configs/restore", methods=["POST"]) def api_configs_restore(): data = request.json backup = data.get("backup", "") target = data.get("target", "") if not backup or not target: return jsonify({"ok": False, "error": "need backup and target paths"}), 400 return api_wrap(lambda: ssh_run(f"cp '{backup}' '{target}' 2>&1 && echo 'Restored {backup} -> {target}'")) # ── API: Firewall Dashboard ────────────────────────────────────────── @app.route("/api/firewall/detect") def api_fw_detect(): return api_wrap(lambda: ssh_run( "echo '=== Firewall Detection ===' && " "echo -n 'ufw: ' && (which ufw >/dev/null 2>&1 && ufw status | head -1 || echo 'not installed') && " "echo -n 'iptables: ' && (which iptables >/dev/null 2>&1 && echo \"installed ($(iptables -L -n 2>/dev/null | grep -c '^Chain') chains)\" || echo 'not installed') && " "echo -n 'nftables: ' && (which nft >/dev/null 2>&1 && echo \"installed ($(nft list tables 2>/dev/null | wc -l) tables)\" || echo 'not installed') && " "echo -n 'firewalld: ' && (which firewall-cmd >/dev/null 2>&1 && firewall-cmd --state 2>/dev/null || echo 'not installed') && " "echo -n 'csf: ' && (which csf >/dev/null 2>&1 && csf -v 2>/dev/null | head -1 || echo 'not installed') && " "echo '' && echo '=== Default Firewall ===' && " "if ufw status 2>/dev/null | grep -q 'Status: active'; then echo 'UFW is the active firewall'; " "elif systemctl is-active firewalld >/dev/null 2>&1; then echo 'firewalld is active'; " "elif systemctl is-active nftables >/dev/null 2>&1; then echo 'nftables service is active'; " "elif csf -l >/dev/null 2>&1; then echo 'CSF is active'; " "else echo 'iptables (raw) — no frontend active'; fi" )) @app.route("/api/firewall/ports") def api_fw_ports(): return api_wrap(lambda: ssh_run( "echo '=== Listening Ports ===' && " "ss -tlnp 2>&1 && " "echo '' && echo '=== UDP Listeners ===' && " "ss -ulnp 2>&1" )) @app.route("/api/firewall/connections") def api_fw_connections(): return api_wrap(lambda: ssh_run( "echo '=== Established Connections ===' && " "ss -tnp state established 2>&1 | head -50" )) @app.route("/api/firewall/connection-stats") def api_fw_conn_stats(): return api_wrap(lambda: ssh_run( "echo '=== Connection States ===' && " "ss -s 2>&1 && " "echo '' && echo '=== By State ===' && " "ss -tan 2>/dev/null | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn" )) @app.route("/api/firewall/top-ips") def api_fw_top_ips(): return api_wrap(lambda: ssh_run( "echo '=== Top 20 IPs by Connection Count ===' && " "ss -tn state established 2>/dev/null | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20" )) @app.route("/api/firewall/blocked") def api_fw_blocked(): return api_wrap(lambda: ssh_run( "echo '=== Recent Blocked/Dropped ===' && " "(grep -i 'block\\|drop\\|reject\\|denied\\|UFW BLOCK' /var/log/kern.log 2>/dev/null || " "grep -i 'block\\|drop\\|reject\\|denied\\|UFW BLOCK' /var/log/syslog 2>/dev/null || " "journalctl -k --no-pager -n 50 2>/dev/null | grep -i 'block\\|drop\\|reject') | tail -30" )) @app.route("/api/firewall/log") def api_fw_log(): return api_wrap(lambda: ssh_run( "echo '=== Firewall Log ===' && " "(tail -50 /var/log/ufw.log 2>/dev/null || " "grep -i 'UFW\\|iptables\\|nft\\|firewalld\\|csf\\|lfd' /var/log/syslog 2>/dev/null | tail -50 || " "journalctl -k --no-pager -n 50 2>/dev/null)" )) # ── API: Firewall - UFW extras ────────────────────────────────────── @app.route("/api/firewall/ufw/numbered") def api_fw_ufw_numbered(): return api_wrap(lambda: ssh_run("ufw status numbered 2>&1")) @app.route("/api/firewall/ufw/default", methods=["POST"]) def api_fw_ufw_default(): data = request.json or {} policy = data.get("policy", "deny") direction = data.get("direction", "incoming") return api_wrap(lambda: ssh_run(f"ufw default {policy} {direction} 2>&1 && ufw status verbose 2>&1")) @app.route("/api/firewall/ufw/app-list") def api_fw_ufw_app_list(): return api_wrap(lambda: ssh_run("ufw app list 2>&1")) @app.route("/api/firewall/ufw/log") def api_fw_ufw_log(): return api_wrap(lambda: ssh_run("tail -50 /var/log/ufw.log 2>/dev/null || echo 'No UFW log found'")) @app.route("/api/firewall/ufw/log-level", methods=["POST"]) def api_fw_ufw_log_level(): level = (request.json or {}).get("level", "on") return api_wrap(lambda: ssh_run(f"ufw logging {level} 2>&1")) # ── API: Firewall - iptables ──────────────────────────────────────── @app.route("/api/firewall/iptables/list") def api_fw_ipt_list(): return api_wrap(lambda: ssh_run(iptables.list_cmd())) @app.route("/api/firewall/iptables/list-nat") def api_fw_ipt_list_nat(): return api_wrap(lambda: ssh_run(iptables.list_nat_cmd())) @app.route("/api/firewall/iptables/list-mangle") def api_fw_ipt_list_mangle(): return api_wrap(lambda: ssh_run(iptables.list_mangle_cmd())) @app.route("/api/firewall/iptables/counters") def api_fw_ipt_counters(): return api_wrap(lambda: ssh_run(iptables.counters_cmd())) @app.route("/api/firewall/iptables/ip6") def api_fw_ipt_ip6(): return api_wrap(lambda: ssh_run(iptables.ip6_list_cmd())) @app.route("/api/firewall/iptables/add", methods=["POST"]) def api_fw_ipt_add(): data = request.json or {} chain = data.get("chain", "INPUT") rule = data.get("rule", "") if not rule: return jsonify({"ok": False, "error": "need rule"}), 400 return api_wrap(lambda: ssh_run(iptables.add_rule_cmd(chain, rule))) @app.route("/api/firewall/iptables/insert", methods=["POST"]) def api_fw_ipt_insert(): data = request.json or {} chain = data.get("chain", "INPUT") position = data.get("position", 1) rule = data.get("rule", "") if not rule: return jsonify({"ok": False, "error": "need rule"}), 400 return api_wrap(lambda: ssh_run(iptables.insert_rule_cmd(chain, position, rule))) @app.route("/api/firewall/iptables/delete", methods=["POST"]) def api_fw_ipt_delete(): data = request.json or {} chain = data.get("chain", "INPUT") rule_num = data.get("rule_num", 0) if not rule_num: return jsonify({"ok": False, "error": "need rule_num"}), 400 return api_wrap(lambda: ssh_run(iptables.delete_rule_cmd(chain, rule_num))) @app.route("/api/firewall/iptables/policy", methods=["POST"]) def api_fw_ipt_policy(): data = request.json or {} chain = data.get("chain", "INPUT") target = data.get("target", "ACCEPT") return api_wrap(lambda: ssh_run(iptables.policy_cmd(chain, target))) @app.route("/api/firewall/iptables/block-ip", methods=["POST"]) def api_fw_ipt_block(): ip = (request.json or {}).get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(iptables.block_ip_cmd(ip))) @app.route("/api/firewall/iptables/unblock-ip", methods=["POST"]) def api_fw_ipt_unblock(): ip = (request.json or {}).get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(iptables.unblock_ip_cmd(ip))) @app.route("/api/firewall/iptables/blocked") def api_fw_ipt_blocked(): return api_wrap(lambda: ssh_run(iptables.list_blocked_cmd())) @app.route("/api/firewall/iptables/save", methods=["POST"]) def api_fw_ipt_save(): return api_wrap(lambda: ssh_run(iptables.save_cmd())) @app.route("/api/firewall/iptables/restore", methods=["POST"]) def api_fw_ipt_restore(): return api_wrap(lambda: ssh_run(iptables.restore_cmd())) @app.route("/api/firewall/iptables/flush", methods=["POST"]) def api_fw_ipt_flush(): return api_wrap(lambda: ssh_run(iptables.flush_cmd())) @app.route("/api/firewall/iptables/zero", methods=["POST"]) def api_fw_ipt_zero(): return api_wrap(lambda: ssh_run(iptables.zero_counters_cmd())) @app.route("/api/firewall/iptables/log") def api_fw_ipt_log(): return api_wrap(lambda: ssh_run(iptables.log_cmd())) @app.route("/api/firewall/iptables/install", methods=["POST"]) def api_fw_ipt_install(): return api_wrap(lambda: ssh_run( "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " "DEBIAN_FRONTEND=noninteractive apt-get install -y iptables iptables-persistent 2>&1 && " "echo 'iptables installed'", timeout=60 )) # ── API: Firewall - nftables ──────────────────────────────────────── @app.route("/api/firewall/nftables/list") def api_fw_nft_list(): return api_wrap(lambda: ssh_run(nftables.list_cmd())) @app.route("/api/firewall/nftables/tables") def api_fw_nft_tables(): return api_wrap(lambda: ssh_run(nftables.list_tables_cmd())) @app.route("/api/firewall/nftables/counters") def api_fw_nft_counters(): return api_wrap(lambda: ssh_run(nftables.counters_cmd())) @app.route("/api/firewall/nftables/chains", methods=["POST"]) def api_fw_nft_chains(): table = (request.json or {}).get("table", "inet filter") return api_wrap(lambda: ssh_run(nftables.list_chains_cmd(table))) @app.route("/api/firewall/nftables/add-rule", methods=["POST"]) def api_fw_nft_add_rule(): data = request.json or {} table = data.get("table", "inet filter") chain = data.get("chain", "input") rule = data.get("rule", "") if not rule: return jsonify({"ok": False, "error": "need rule"}), 400 return api_wrap(lambda: ssh_run(nftables.add_rule_cmd(table, chain, rule))) @app.route("/api/firewall/nftables/delete-rule", methods=["POST"]) def api_fw_nft_del_rule(): data = request.json or {} table = data.get("table", "inet filter") chain = data.get("chain", "input") handle = data.get("handle", 0) if not handle: return jsonify({"ok": False, "error": "need handle"}), 400 return api_wrap(lambda: ssh_run(nftables.delete_rule_cmd(table, chain, handle))) @app.route("/api/firewall/nftables/create-table", methods=["POST"]) def api_fw_nft_create_table(): data = request.json or {} family = data.get("family", "inet") name = data.get("name", "") if not name: return jsonify({"ok": False, "error": "need name"}), 400 return api_wrap(lambda: ssh_run(nftables.create_table_cmd(family, name))) @app.route("/api/firewall/nftables/create-chain", methods=["POST"]) def api_fw_nft_create_chain(): data = request.json or {} table = data.get("table", "inet filter") chain = data.get("chain", "") hook = data.get("hook", "input") if not chain: return jsonify({"ok": False, "error": "need chain name"}), 400 return api_wrap(lambda: ssh_run(nftables.create_chain_cmd(table, chain, "filter", hook))) @app.route("/api/firewall/nftables/save", methods=["POST"]) def api_fw_nft_save(): return api_wrap(lambda: ssh_run(nftables.save_cmd())) @app.route("/api/firewall/nftables/restore", methods=["POST"]) def api_fw_nft_restore(): return api_wrap(lambda: ssh_run(nftables.restore_cmd())) @app.route("/api/firewall/nftables/config") def api_fw_nft_config(): return api_wrap(lambda: ssh_run(nftables.config_cmd())) @app.route("/api/firewall/nftables/flush", methods=["POST"]) def api_fw_nft_flush(): return api_wrap(lambda: ssh_run(nftables.flush_cmd())) @app.route("/api/firewall/nftables/install", methods=["POST"]) def api_fw_nft_install(): return api_wrap(lambda: ssh_run(nftables.install_cmd(), timeout=60)) # ── API: Firewall - firewalld ─────────────────────────────────────── @app.route("/api/firewall/firewalld/status") def api_fw_fwd_status(): return api_wrap(lambda: ssh_run(firewalld.status_cmd())) @app.route("/api/firewall/firewalld/zones") def api_fw_fwd_zones(): return api_wrap(lambda: ssh_run(firewalld.zones_cmd())) @app.route("/api/firewall/firewalld/zone-info", methods=["POST"]) def api_fw_fwd_zone_info(): zone = (request.json or {}).get("zone", "public") return api_wrap(lambda: ssh_run(firewalld.zone_info_cmd(zone))) @app.route("/api/firewall/firewalld/default-zone", methods=["POST"]) def api_fw_fwd_default_zone(): zone = (request.json or {}).get("zone", "public") return api_wrap(lambda: ssh_run(firewalld.default_zone_cmd(zone))) @app.route("/api/firewall/firewalld/add-service", methods=["POST"]) def api_fw_fwd_add_service(): data = request.json or {} service = data.get("service", "") zone = data.get("zone", "public") if not service: return jsonify({"ok": False, "error": "need service"}), 400 return api_wrap(lambda: ssh_run(firewalld.add_service_cmd(service, zone))) @app.route("/api/firewall/firewalld/remove-service", methods=["POST"]) def api_fw_fwd_remove_service(): data = request.json or {} service = data.get("service", "") zone = data.get("zone", "public") if not service: return jsonify({"ok": False, "error": "need service"}), 400 return api_wrap(lambda: ssh_run(firewalld.remove_service_cmd(service, zone))) @app.route("/api/firewall/firewalld/add-port", methods=["POST"]) def api_fw_fwd_add_port(): data = request.json or {} port = data.get("port", "") zone = data.get("zone", "public") if not port: return jsonify({"ok": False, "error": "need port"}), 400 return api_wrap(lambda: ssh_run(firewalld.add_port_cmd(port, zone))) @app.route("/api/firewall/firewalld/remove-port", methods=["POST"]) def api_fw_fwd_remove_port(): data = request.json or {} port = data.get("port", "") zone = data.get("zone", "public") if not port: return jsonify({"ok": False, "error": "need port"}), 400 return api_wrap(lambda: ssh_run(firewalld.remove_port_cmd(port, zone))) @app.route("/api/firewall/firewalld/add-rich-rule", methods=["POST"]) def api_fw_fwd_add_rich(): data = request.json or {} rule = data.get("rule", "") zone = data.get("zone", "public") if not rule: return jsonify({"ok": False, "error": "need rule"}), 400 return api_wrap(lambda: ssh_run(firewalld.add_rich_rule_cmd(rule, zone))) @app.route("/api/firewall/firewalld/remove-rich-rule", methods=["POST"]) def api_fw_fwd_remove_rich(): data = request.json or {} rule = data.get("rule", "") zone = data.get("zone", "public") if not rule: return jsonify({"ok": False, "error": "need rule"}), 400 return api_wrap(lambda: ssh_run(firewalld.remove_rich_rule_cmd(rule, zone))) @app.route("/api/firewall/firewalld/block-ip", methods=["POST"]) def api_fw_fwd_block(): ip = (request.json or {}).get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(firewalld.block_ip_cmd(ip))) @app.route("/api/firewall/firewalld/unblock-ip", methods=["POST"]) def api_fw_fwd_unblock(): ip = (request.json or {}).get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(firewalld.unblock_ip_cmd(ip))) @app.route("/api/firewall/firewalld/reload", methods=["POST"]) def api_fw_fwd_reload(): return api_wrap(lambda: ssh_run(firewalld.reload_cmd())) @app.route("/api/firewall/firewalld/panic-on", methods=["POST"]) def api_fw_fwd_panic_on(): return api_wrap(lambda: ssh_run(firewalld.panic_on_cmd())) @app.route("/api/firewall/firewalld/panic-off", methods=["POST"]) def api_fw_fwd_panic_off(): return api_wrap(lambda: ssh_run(firewalld.panic_off_cmd())) @app.route("/api/firewall/firewalld/services-list") def api_fw_fwd_services_list(): return api_wrap(lambda: ssh_run(firewalld.services_list_cmd())) @app.route("/api/firewall/firewalld/log") def api_fw_fwd_log(): return api_wrap(lambda: ssh_run(firewalld.log_cmd())) @app.route("/api/firewall/firewalld/install", methods=["POST"]) def api_fw_fwd_install(): return api_wrap(lambda: ssh_run(firewalld.install_cmd(), timeout=60)) # ── API: Firewall - CSF ───────────────────────────────────────────── @app.route("/api/firewall/csf/status") def api_fw_csf_status(): return api_wrap(lambda: ssh_run(csf.status_cmd())) @app.route("/api/firewall/csf/start", methods=["POST"]) def api_fw_csf_start(): return api_wrap(lambda: ssh_run(csf.start_cmd())) @app.route("/api/firewall/csf/stop", methods=["POST"]) def api_fw_csf_stop(): return api_wrap(lambda: ssh_run(csf.stop_cmd())) @app.route("/api/firewall/csf/restart", methods=["POST"]) def api_fw_csf_restart(): return api_wrap(lambda: ssh_run(csf.restart_cmd())) @app.route("/api/firewall/csf/list") def api_fw_csf_list(): return api_wrap(lambda: ssh_run(csf.list_cmd())) @app.route("/api/firewall/csf/allow", methods=["POST"]) def api_fw_csf_allow(): data = request.json or {} ip = data.get("ip", "") comment = data.get("comment", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(csf.allow_ip_cmd(ip, comment))) @app.route("/api/firewall/csf/deny", methods=["POST"]) def api_fw_csf_deny(): data = request.json or {} ip = data.get("ip", "") comment = data.get("comment", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(csf.deny_ip_cmd(ip, comment))) @app.route("/api/firewall/csf/remove", methods=["POST"]) def api_fw_csf_remove(): ip = (request.json or {}).get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(csf.remove_ip_cmd(ip))) @app.route("/api/firewall/csf/grep", methods=["POST"]) def api_fw_csf_grep(): ip = (request.json or {}).get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(csf.grep_ip_cmd(ip))) @app.route("/api/firewall/csf/temp-allow", methods=["POST"]) def api_fw_csf_temp_allow(): data = request.json or {} ip = data.get("ip", "") ttl = data.get("ttl", 3600) if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(csf.temp_allow_cmd(ip, ttl))) @app.route("/api/firewall/csf/temp-deny", methods=["POST"]) def api_fw_csf_temp_deny(): data = request.json or {} ip = data.get("ip", "") ttl = data.get("ttl", 3600) if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(csf.temp_deny_cmd(ip, ttl))) @app.route("/api/firewall/csf/temp-list") def api_fw_csf_temp_list(): return api_wrap(lambda: ssh_run(csf.temp_list_cmd())) @app.route("/api/firewall/csf/config") def api_fw_csf_config(): return api_wrap(lambda: ssh_run(csf.config_cmd())) @app.route("/api/firewall/csf/log") def api_fw_csf_log(): return api_wrap(lambda: ssh_run(csf.log_cmd())) @app.route("/api/firewall/csf/test") def api_fw_csf_test(): return api_wrap(lambda: ssh_run(csf.test_cmd())) @app.route("/api/firewall/csf/install", methods=["POST"]) def api_fw_csf_install(): return api_wrap(lambda: ssh_run(csf.install_cmd(), timeout=120)) # ── API: Firewall - Migration (UFW↔iptables) ──────────────────────── @app.route("/api/firewall/migrate/ufw-to-iptables", methods=["POST"]) def api_fw_migrate_ufw_to_ipt(): return api_wrap(lambda: ssh_run( "echo '=== Migrating UFW → iptables ===' && " "echo '1. Exporting current UFW rules...' && " "ufw status numbered 2>&1 && " "echo '' && echo '2. Saving iptables state (UFW generates iptables rules)...' && " "mkdir -p /etc/iptables && " "iptables-save > /etc/iptables/rules.v4.pre-migration 2>&1 && " "ip6tables-save > /etc/iptables/rules.v6.pre-migration 2>&1 && " "echo '3. Disabling UFW...' && " "echo 'y' | ufw disable 2>&1 && " "systemctl disable ufw 2>&1 && " "echo '4. Restoring iptables rules from UFW state...' && " "iptables-restore < /etc/iptables/rules.v4.pre-migration 2>&1 && " "echo '5. Installing iptables-persistent...' && " "DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent 2>&1 && " "iptables-save > /etc/iptables/rules.v4 2>&1 && " "ip6tables-save > /etc/iptables/rules.v6 2>&1 && " "systemctl enable netfilter-persistent 2>&1 && " "echo '' && echo '=== Migration Complete ===' && " "echo 'UFW disabled. iptables rules preserved and persisted.' && " "echo 'Current iptables rules:' && iptables -L -n --line-numbers 2>&1 | head -30", timeout=60 )) @app.route("/api/firewall/migrate/iptables-to-ufw", methods=["POST"]) def api_fw_migrate_ipt_to_ufw(): return api_wrap(lambda: ssh_run( "echo '=== Migrating iptables → UFW ===' && " "echo '1. Saving current iptables rules as backup...' && " "mkdir -p /etc/iptables && " "iptables-save > /etc/iptables/rules.v4.pre-ufw-migration 2>&1 && " "echo '2. Current iptables ACCEPT rules (will be converted):' && " "iptables -L INPUT -n --line-numbers 2>/dev/null | grep ACCEPT | " "awk '{if($5==\"tcp\"||$5==\"udp\") print $5\" \"$NF}' | sed 's/dpt://' && " "echo '' && echo '3. Installing and resetting UFW...' && " "DEBIAN_FRONTEND=noninteractive apt-get install -y ufw 2>&1 && " "echo 'y' | ufw reset 2>&1 && " "ufw default deny incoming 2>&1 && " "ufw default allow outgoing 2>&1 && " "echo '4. Converting iptables rules to UFW...' && " "iptables -L INPUT -n 2>/dev/null | grep ACCEPT | " "awk '{if($5==\"tcp\") print \"allow \"$NF\"/tcp\"; if($5==\"udp\") print \"allow \"$NF\"/udp\"}' | " "sed 's/dpt://' | while read rule; do echo \"+ ufw $rule\" && ufw $rule 2>&1; done && " "echo '5. Enabling UFW...' && " "echo 'y' | ufw enable 2>&1 && " "echo '6. Disabling iptables-persistent...' && " "systemctl disable netfilter-persistent 2>/dev/null; " "echo '' && echo '=== Migration Complete ===' && " "ufw status verbose 2>&1", timeout=60 )) # ── API: Security - Hardening ──────────────────────────────────────── @app.route("/api/security/ssh/status") def api_sec_ssh_status(): return api_wrap(lambda: ssh_run(hardening.ssh_status_cmd())) @app.route("/api/security/ssh/harden", methods=["POST"]) def api_sec_ssh_harden(): data = request.json or {} port = data.get("port", 22) disable_root = data.get("disable_root", True) disable_password = data.get("disable_password", True) return api_wrap(lambda: ssh_run(hardening.ssh_harden_cmd(port, disable_root, disable_password))) @app.route("/api/security/kernel/status") def api_sec_kernel_status(): return api_wrap(lambda: ssh_run(hardening.kernel_status_cmd())) @app.route("/api/security/kernel/harden", methods=["POST"]) def api_sec_kernel_harden(): return api_wrap(lambda: ssh_run(hardening.kernel_harden_cmd())) @app.route("/api/security/auto-updates", methods=["POST"]) def api_sec_auto_updates(): return api_wrap(lambda: ssh_run(hardening.auto_updates_cmd(), timeout=60)) @app.route("/api/security/user-audit") def api_sec_user_audit(): return api_wrap(lambda: ssh_run(hardening.user_audit_cmd(), timeout=60)) @app.route("/api/security/port-scan") def api_sec_port_scan(): return api_wrap(lambda: ssh_run(hardening.port_scan_cmd())) # ── API: Security - Firewall ──────────────────────────────────────── @app.route("/api/security/firewall/status") def api_sec_fw_status(): return api_wrap(lambda: ssh_run(hardening.firewall_status_cmd())) @app.route("/api/security/firewall/enable", methods=["POST"]) def api_sec_fw_enable(): port = (request.json or {}).get("ssh_port", 22) return api_wrap(lambda: ssh_run(hardening.firewall_enable_cmd(port), timeout=60)) @app.route("/api/security/firewall/disable", methods=["POST"]) def api_sec_fw_disable(): return api_wrap(lambda: ssh_run("echo 'y' | ufw disable 2>&1 && ufw status verbose 2>&1")) @app.route("/api/security/firewall/add", methods=["POST"]) def api_sec_fw_add(): rule = request.json.get("rule", "") if not rule: return jsonify({"ok": False, "error": "need rule"}), 400 return api_wrap(lambda: ssh_run(hardening.firewall_add_rule_cmd(rule))) @app.route("/api/security/firewall/delete", methods=["POST"]) def api_sec_fw_delete(): rule = request.json.get("rule", "") if not rule: return jsonify({"ok": False, "error": "need rule"}), 400 return api_wrap(lambda: ssh_run(hardening.firewall_delete_rule_cmd(rule))) @app.route("/api/security/firewall/preset", methods=["POST"]) def api_sec_fw_preset(): preset = request.json.get("preset", "") if not preset: return jsonify({"ok": False, "error": "need preset name"}), 400 return api_wrap(lambda: ssh_run(hardening.firewall_preset_cmd(preset))) # ── API: Security - Security Apps ─────────────────────────────────── @app.route("/api/security/apps") def api_sec_apps(): return jsonify({"ok": True, "data": security_apps.SECURITY_APPS, "categories": security_apps.CATEGORIES}) @app.route("/api/security/apps/check", methods=["POST"]) def api_sec_apps_check(): name = request.json.get("name", "") app_entry = next((a for a in security_apps.SECURITY_APPS if a["name"] == name), None) if not app_entry: return jsonify({"ok": False, "error": f"App '{name}' not found"}), 400 return api_wrap(lambda: ssh_run(app_entry["check"])) @app.route("/api/security/apps/install", methods=["POST"]) def api_sec_apps_install(): name = request.json.get("name", "") app_entry = next((a for a in security_apps.SECURITY_APPS if a["name"] == name), None) if not app_entry: return jsonify({"ok": False, "error": f"App '{name}' not found"}), 400 return api_wrap(lambda: ssh_run(app_entry["install"], timeout=120)) @app.route("/api/security/apps/scan", methods=["POST"]) def api_sec_apps_scan(): name = request.json.get("name", "") app_entry = next((a for a in security_apps.SECURITY_APPS if a["name"] == name), None) if not app_entry: return jsonify({"ok": False, "error": f"App '{name}' not found"}), 400 return api_wrap(lambda: ssh_run(app_entry["scan"], timeout=120)) @app.route("/api/security/apps/uninstall", methods=["POST"]) def api_sec_apps_uninstall(): name = request.json.get("name", "") app_entry = next((a for a in security_apps.SECURITY_APPS if a["name"] == name), None) if not app_entry: return jsonify({"ok": False, "error": f"App '{name}' not found"}), 400 return api_wrap(lambda: ssh_run(app_entry["uninstall"], timeout=60)) # ── API: Security - SSL/TLS ───────────────────────────────────────── @app.route("/api/security/ssl/check", methods=["POST"]) def api_sec_ssl_check(): domain = request.json.get("domain", "") if not domain: return jsonify({"ok": False, "error": "need domain"}), 400 return api_wrap(lambda: ssh_run(ssl_audit.ssl_check_cmd(domain), timeout=30)) @app.route("/api/security/ssl/expiry", methods=["POST"]) def api_sec_ssl_expiry(): domain = request.json.get("domain", "") if not domain: return jsonify({"ok": False, "error": "need domain"}), 400 return api_wrap(lambda: ssh_run(ssl_audit.ssl_expiry_cmd(domain))) @app.route("/api/security/ssl/expiry-all") def api_sec_ssl_expiry_all(): return api_wrap(lambda: ssh_run(ssl_audit.ssl_expiry_all_cmd())) @app.route("/api/security/ssl/grade", methods=["POST"]) def api_sec_ssl_grade(): domain = request.json.get("domain", "") if not domain: return jsonify({"ok": False, "error": "need domain"}), 400 return api_wrap(lambda: ssh_run(ssl_audit.ssl_grade_cmd(domain), timeout=60)) @app.route("/api/security/ssl/renew", methods=["POST"]) def api_sec_ssl_renew(): return api_wrap(lambda: ssh_run(ssl_audit.ssl_renew_cmd(), timeout=120)) @app.route("/api/security/ssl/renew-dry") def api_sec_ssl_renew_dry(): return api_wrap(lambda: ssh_run(ssl_audit.ssl_renew_dry_cmd(), timeout=60)) @app.route("/api/security/ssl/autorenew-status") def api_sec_ssl_autorenew(): return api_wrap(lambda: ssh_run(ssl_audit.ssl_autorenew_status_cmd())) @app.route("/api/security/ssl/security-headers", methods=["POST"]) def api_sec_ssl_headers(): domain = request.json.get("domain", "") if not domain: return jsonify({"ok": False, "error": "need domain"}), 400 return api_wrap(lambda: ssh_run(ssl_audit.security_headers_cmd(domain))) # ── API: Security - Monitoring/IDS ────────────────────────────────── @app.route("/api/security/monitoring/auth-log") def api_sec_auth_log(): lines = request.args.get("lines", 100, type=int) return api_wrap(lambda: ssh_run(monitoring.auth_log_cmd(lines))) @app.route("/api/security/monitoring/login-tracker") def api_sec_login_tracker(): return api_wrap(lambda: ssh_run(monitoring.login_tracker_cmd(), timeout=60)) @app.route("/api/security/monitoring/active-sessions") def api_sec_active_sessions(): return api_wrap(lambda: ssh_run(monitoring.active_sessions_cmd())) @app.route("/api/security/monitoring/file-integrity") def api_sec_file_integrity(): paths = request.args.get("paths", "/etc /usr/bin /usr/sbin /var/www") return api_wrap(lambda: ssh_run(monitoring.file_integrity_check_cmd(paths), timeout=120)) @app.route("/api/security/monitoring/file-integrity/init", methods=["POST"]) def api_sec_file_integrity_init(): paths = (request.json or {}).get("paths", "/etc /usr/bin /usr/sbin /var/www") return api_wrap(lambda: ssh_run(monitoring.file_integrity_init_cmd(paths), timeout=120)) @app.route("/api/security/monitoring/process-audit") def api_sec_process_audit(): return api_wrap(lambda: ssh_run(monitoring.process_audit_cmd(), timeout=60)) @app.route("/api/security/monitoring/security-log") def api_sec_security_log(): lines = request.args.get("lines", 100, type=int) return api_wrap(lambda: ssh_run(monitoring.security_log_cmd(lines), timeout=60)) @app.route("/api/security/monitoring/suid-audit") def api_sec_suid_audit(): return api_wrap(lambda: ssh_run(monitoring.suid_audit_cmd(), timeout=60)) @app.route("/api/security/monitoring/world-writable") def api_sec_world_writable(): return api_wrap(lambda: ssh_run(monitoring.world_writable_cmd(), timeout=60)) @app.route("/api/security/monitoring/cron-audit") def api_sec_cron_audit(): return api_wrap(lambda: ssh_run(monitoring.cron_audit_cmd(), timeout=60)) @app.route("/api/security/monitoring/alerts/setup", methods=["POST"]) def api_sec_alert_setup(): data = request.json or {} email = data.get("email", "") webhook = data.get("webhook", "") if not email: return jsonify({"ok": False, "error": "need email"}), 400 return api_wrap(lambda: ssh_run(monitoring.alert_setup_cmd(email, webhook))) @app.route("/api/security/monitoring/alerts/status") def api_sec_alert_status(): return api_wrap(lambda: ssh_run(monitoring.alert_status_cmd())) @app.route("/api/security/monitoring/alerts/remove", methods=["POST"]) def api_sec_alert_remove(): return api_wrap(lambda: ssh_run(monitoring.alert_remove_cmd())) # ── API: Security - DDoS Protection ───────────────────────────────── @app.route("/api/security/ddos/connection-stats") def api_sec_ddos_conn_stats(): return api_wrap(lambda: ssh_run(ddos.connection_stats_cmd())) @app.route("/api/security/ddos/syn-flood") def api_sec_ddos_syn(): return api_wrap(lambda: ssh_run(ddos.syn_flood_check_cmd())) @app.route("/api/security/ddos/bandwidth") def api_sec_ddos_bandwidth(): return api_wrap(lambda: ssh_run(ddos.bandwidth_stats_cmd(), timeout=30)) @app.route("/api/security/ddos/rate-limit/status") def api_sec_ddos_rate_status(): return api_wrap(lambda: ssh_run(ddos.nginx_rate_limit_status_cmd())) @app.route("/api/security/ddos/rate-limit/enable", methods=["POST"]) def api_sec_ddos_rate_enable(): data = request.json or {} rps = data.get("requests_per_second", 10) burst = data.get("burst", 20) return api_wrap(lambda: ssh_run(ddos.nginx_rate_limit_cmd(rps, burst))) @app.route("/api/security/ddos/rate-limit/remove", methods=["POST"]) def api_sec_ddos_rate_remove(): return api_wrap(lambda: ssh_run(ddos.nginx_rate_limit_remove_cmd())) @app.route("/api/security/ddos/auto-blacklist/enable", methods=["POST"]) def api_sec_ddos_blacklist_enable(): data = request.json or {} threshold = data.get("threshold", 100) return api_wrap(lambda: ssh_run(ddos.auto_blacklist_cmd(threshold))) @app.route("/api/security/ddos/auto-blacklist/status") def api_sec_ddos_blacklist_status(): return api_wrap(lambda: ssh_run(ddos.auto_blacklist_status_cmd())) @app.route("/api/security/ddos/auto-blacklist/remove", methods=["POST"]) def api_sec_ddos_blacklist_remove(): return api_wrap(lambda: ssh_run(ddos.auto_blacklist_remove_cmd())) @app.route("/api/security/ddos/blacklist/add", methods=["POST"]) def api_sec_ddos_blacklist_add(): ip = request.json.get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(ddos.blacklist_ip_cmd(ip))) @app.route("/api/security/ddos/blacklist/remove", methods=["POST"]) def api_sec_ddos_unblacklist(): ip = request.json.get("ip", "") if not ip: return jsonify({"ok": False, "error": "need ip"}), 400 return api_wrap(lambda: ssh_run(ddos.unblacklist_ip_cmd(ip))) @app.route("/api/security/ddos/blacklist/list") def api_sec_ddos_blacklist_list(): return api_wrap(lambda: ssh_run(ddos.show_blacklist_cmd())) @app.route("/api/security/ddos/syn-protection", methods=["POST"]) def api_sec_ddos_syn_protect(): return api_wrap(lambda: ssh_run(ddos.syn_protection_cmd())) @app.route("/api/security/ddos/cloudflare/status", methods=["POST"]) def api_sec_ddos_cf_status(): data = request.json or {} zone = data.get("zone_id", "") token = data.get("api_token", "") if not zone or not token: return jsonify({"ok": False, "error": "need zone_id and api_token"}), 400 return api_wrap(lambda: ssh_run(ddos.cloudflare_status_cmd(zone, token))) @app.route("/api/security/ddos/cloudflare/toggle", methods=["POST"]) def api_sec_ddos_cf_toggle(): data = request.json or {} zone = data.get("zone_id", "") token = data.get("api_token", "") enable = data.get("enable", True) if not zone or not token: return jsonify({"ok": False, "error": "need zone_id and api_token"}), 400 return api_wrap(lambda: ssh_run(ddos.cloudflare_under_attack_cmd(zone, token, enable))) @app.route("/api/security/ddos/tor-block", methods=["POST"]) def api_sec_ddos_tor_block(): enable = (request.json or {}).get("enable", True) return api_wrap(lambda: ssh_run(ddos.tor_exit_block_cmd(enable), timeout=60)) @app.route("/api/security/ddos/geoblock", methods=["POST"]) def api_sec_ddos_geoblock(): codes = request.json.get("countries", "") if not codes: return jsonify({"ok": False, "error": "need country codes"}), 400 return api_wrap(lambda: ssh_run(ddos.geoblock_cmd(codes), timeout=60)) # ── API: Security - Backup ────────────────────────────────────────── @app.route("/api/security/backup/now", methods=["POST"]) def api_sec_backup_now(): data = request.json or {} paths = data.get("paths", "/etc /var/www /opt/seteclabs /root") dest = data.get("dest", "/var/backups/setec") encrypt_pass = data.get("encrypt_pass", "") remote_host = data.get("remote_host", "") remote_path = data.get("remote_path", "") return api_wrap(lambda: ssh_run( backup.backup_now_cmd(paths, dest, encrypt_pass, remote_host, remote_path), timeout=120 )) @app.route("/api/security/backup/list") def api_sec_backup_list(): dest = request.args.get("dest", "/var/backups/setec") return api_wrap(lambda: ssh_run(backup.backup_list_cmd(dest))) @app.route("/api/security/backup/restore", methods=["POST"]) def api_sec_backup_restore(): data = request.json or {} archive = data.get("archive", "") encrypt_pass = data.get("encrypt_pass", "") if not archive: return jsonify({"ok": False, "error": "need archive path"}), 400 return api_wrap(lambda: ssh_run(backup.backup_restore_cmd(archive, "/", encrypt_pass), timeout=120)) @app.route("/api/security/backup/delete", methods=["POST"]) def api_sec_backup_delete(): archive = request.json.get("archive", "") if not archive: return jsonify({"ok": False, "error": "need archive path"}), 400 return api_wrap(lambda: ssh_run(backup.backup_delete_cmd(archive))) @app.route("/api/security/backup/schedule", methods=["POST"]) def api_sec_backup_schedule(): data = request.json or {} paths = data.get("paths", "/etc /var/www /opt/seteclabs /root") dest = data.get("dest", "/var/backups/setec") encrypt_pass = data.get("encrypt_pass", "") schedule = data.get("schedule", "daily") keep = data.get("keep", 7) return api_wrap(lambda: ssh_run( backup.backup_schedule_cmd(paths, dest, encrypt_pass, schedule, keep) )) @app.route("/api/security/backup/schedule/status") def api_sec_backup_schedule_status(): return api_wrap(lambda: ssh_run(backup.backup_schedule_status_cmd())) @app.route("/api/security/backup/schedule/remove", methods=["POST"]) def api_sec_backup_schedule_remove(): return api_wrap(lambda: ssh_run(backup.backup_schedule_remove_cmd())) # ── API: Security - ClamAV ─────────────────────────────────────────── @app.route("/api/security/clamav/status") def api_sec_clamav_status(): return api_wrap(lambda: ssh_run(clamav.status_cmd())) @app.route("/api/security/clamav/install", methods=["POST"]) def api_sec_clamav_install(): return api_wrap(lambda: ssh_run(clamav.install_cmd(), timeout=120)) @app.route("/api/security/clamav/update-defs", methods=["POST"]) def api_sec_clamav_update_defs(): return api_wrap(lambda: ssh_run(clamav.update_defs_cmd(), timeout=120)) @app.route("/api/security/clamav/scan", methods=["POST"]) def api_sec_clamav_scan(): data = request.json or {} path = data.get("path", "/var/www") recursive = data.get("recursive", True) return api_wrap(lambda: ssh_run(clamav.scan_cmd(path, recursive), timeout=120)) @app.route("/api/security/clamav/scan-quick", methods=["POST"]) def api_sec_clamav_scan_quick(): return api_wrap(lambda: ssh_run(clamav.scan_quick_cmd(), timeout=120)) @app.route("/api/security/clamav/scan-full", methods=["POST"]) def api_sec_clamav_scan_full(): return api_wrap(lambda: ssh_run(clamav.scan_full_cmd(), timeout=120)) @app.route("/api/security/clamav/quarantine-scan", methods=["POST"]) def api_sec_clamav_quarantine_scan(): data = request.json or {} path = data.get("path", "/var/www") return api_wrap(lambda: ssh_run(clamav.quarantine_scan_cmd(path), timeout=120)) @app.route("/api/security/clamav/quarantine/list") def api_sec_clamav_quarantine_list(): return api_wrap(lambda: ssh_run(clamav.quarantine_list_cmd())) @app.route("/api/security/clamav/quarantine/delete", methods=["POST"]) def api_sec_clamav_quarantine_delete(): return api_wrap(lambda: ssh_run(clamav.quarantine_delete_cmd())) @app.route("/api/security/clamav/log") def api_sec_clamav_log(): lines = request.args.get("lines", 50, type=int) return api_wrap(lambda: ssh_run(clamav.log_cmd(lines))) @app.route("/api/security/clamav/schedule", methods=["POST"]) def api_sec_clamav_schedule(): data = request.json or {} schedule = data.get("schedule", "daily") paths = data.get("paths", "/") return api_wrap(lambda: ssh_run(clamav.schedule_cmd(schedule, paths))) @app.route("/api/security/clamav/schedule/status") def api_sec_clamav_schedule_status(): return api_wrap(lambda: ssh_run(clamav.schedule_status_cmd())) @app.route("/api/security/clamav/schedule/remove", methods=["POST"]) def api_sec_clamav_schedule_remove(): return api_wrap(lambda: ssh_run(clamav.schedule_remove_cmd())) @app.route("/api/security/clamav/config") def api_sec_clamav_config(): return api_wrap(lambda: ssh_run(clamav.config_cmd())) @app.route("/api/security/clamav/uninstall", methods=["POST"]) def api_sec_clamav_uninstall(): return api_wrap(lambda: ssh_run(clamav.uninstall_cmd(), timeout=60)) # ── API: Security - rkhunter ───────────────────────────────────────── @app.route("/api/security/rkhunter/status") def api_sec_rkh_status(): return api_wrap(lambda: ssh_run(rkhunter.status_cmd())) @app.route("/api/security/rkhunter/install", methods=["POST"]) def api_sec_rkh_install(): return api_wrap(lambda: ssh_run(rkhunter.install_cmd(), timeout=120)) @app.route("/api/security/rkhunter/update", methods=["POST"]) def api_sec_rkh_update(): return api_wrap(lambda: ssh_run(rkhunter.update_cmd(), timeout=60)) @app.route("/api/security/rkhunter/check", methods=["POST"]) def api_sec_rkh_check(): return api_wrap(lambda: ssh_run(rkhunter.check_cmd(), timeout=120)) @app.route("/api/security/rkhunter/check-quick", methods=["POST"]) def api_sec_rkh_check_quick(): return api_wrap(lambda: ssh_run(rkhunter.check_quick_cmd(), timeout=60)) @app.route("/api/security/rkhunter/log") def api_sec_rkh_log(): return api_wrap(lambda: ssh_run(rkhunter.log_cmd())) @app.route("/api/security/rkhunter/config") def api_sec_rkh_config(): return api_wrap(lambda: ssh_run(rkhunter.config_cmd())) @app.route("/api/security/rkhunter/whitelist") def api_sec_rkh_whitelist(): return api_wrap(lambda: ssh_run(rkhunter.whitelist_cmd())) @app.route("/api/security/rkhunter/whitelist/add", methods=["POST"]) def api_sec_rkh_whitelist_add(): item = (request.json or {}).get("item", "") if not item: return jsonify({"ok": False, "error": "need item"}), 400 return api_wrap(lambda: ssh_run(rkhunter.whitelist_add_cmd(item))) @app.route("/api/security/rkhunter/schedule", methods=["POST"]) def api_sec_rkh_schedule(): schedule = (request.json or {}).get("schedule", "daily") return api_wrap(lambda: ssh_run(rkhunter.schedule_cmd(schedule))) @app.route("/api/security/rkhunter/schedule/status") def api_sec_rkh_schedule_status(): return api_wrap(lambda: ssh_run(rkhunter.schedule_status_cmd())) @app.route("/api/security/rkhunter/schedule/remove", methods=["POST"]) def api_sec_rkh_schedule_remove(): return api_wrap(lambda: ssh_run(rkhunter.schedule_remove_cmd())) @app.route("/api/security/rkhunter/uninstall", methods=["POST"]) def api_sec_rkh_uninstall(): return api_wrap(lambda: ssh_run(rkhunter.uninstall_cmd(), timeout=60)) # ── API: Security - chkrootkit ─────────────────────────────────────── @app.route("/api/security/chkrootkit/status") def api_sec_chk_status(): return api_wrap(lambda: ssh_run(chkrootkit.status_cmd())) @app.route("/api/security/chkrootkit/install", methods=["POST"]) def api_sec_chk_install(): return api_wrap(lambda: ssh_run(chkrootkit.install_cmd(), timeout=120)) @app.route("/api/security/chkrootkit/check", methods=["POST"]) def api_sec_chk_check(): return api_wrap(lambda: ssh_run(chkrootkit.check_cmd(), timeout=120)) @app.route("/api/security/chkrootkit/check-expert", methods=["POST"]) def api_sec_chk_expert(): return api_wrap(lambda: ssh_run(chkrootkit.check_expert_cmd(), timeout=120)) @app.route("/api/security/chkrootkit/log") def api_sec_chk_log(): return api_wrap(lambda: ssh_run(chkrootkit.log_cmd())) @app.route("/api/security/chkrootkit/config") def api_sec_chk_config(): return api_wrap(lambda: ssh_run(chkrootkit.config_cmd())) @app.route("/api/security/chkrootkit/schedule", methods=["POST"]) def api_sec_chk_schedule(): schedule = (request.json or {}).get("schedule", "daily") return api_wrap(lambda: ssh_run(chkrootkit.schedule_cmd(schedule))) @app.route("/api/security/chkrootkit/schedule/status") def api_sec_chk_schedule_status(): return api_wrap(lambda: ssh_run(chkrootkit.schedule_status_cmd())) @app.route("/api/security/chkrootkit/schedule/remove", methods=["POST"]) def api_sec_chk_schedule_remove(): return api_wrap(lambda: ssh_run(chkrootkit.schedule_remove_cmd())) @app.route("/api/security/chkrootkit/uninstall", methods=["POST"]) def api_sec_chk_uninstall(): return api_wrap(lambda: ssh_run(chkrootkit.uninstall_cmd(), timeout=60)) # ── API: Security - Lynis ─────────────────────────────────────────── @app.route("/api/security/lynis/status") def api_sec_lyn_status(): return api_wrap(lambda: ssh_run(lynis.status_cmd())) @app.route("/api/security/lynis/install", methods=["POST"]) def api_sec_lyn_install(): return api_wrap(lambda: ssh_run(lynis.install_cmd(), timeout=120)) @app.route("/api/security/lynis/audit-quick", methods=["POST"]) def api_sec_lyn_audit_quick(): return api_wrap(lambda: ssh_run(lynis.audit_quick_cmd(), timeout=120)) @app.route("/api/security/lynis/audit-full", methods=["POST"]) def api_sec_lyn_audit_full(): return api_wrap(lambda: ssh_run(lynis.audit_full_cmd(), timeout=120)) @app.route("/api/security/lynis/hardening-index") def api_sec_lyn_index(): return api_wrap(lambda: ssh_run(lynis.hardening_index_cmd())) @app.route("/api/security/lynis/warnings") def api_sec_lyn_warnings(): return api_wrap(lambda: ssh_run(lynis.show_warnings_cmd())) @app.route("/api/security/lynis/suggestions") def api_sec_lyn_suggestions(): return api_wrap(lambda: ssh_run(lynis.show_suggestions_cmd())) @app.route("/api/security/lynis/report") def api_sec_lyn_report(): return api_wrap(lambda: ssh_run(lynis.show_report_cmd())) @app.route("/api/security/lynis/log") def api_sec_lyn_log(): return api_wrap(lambda: ssh_run(lynis.log_cmd())) @app.route("/api/security/lynis/profile") def api_sec_lyn_profile(): return api_wrap(lambda: ssh_run(lynis.profile_cmd())) @app.route("/api/security/lynis/schedule", methods=["POST"]) def api_sec_lyn_schedule(): schedule = (request.json or {}).get("schedule", "weekly") return api_wrap(lambda: ssh_run(lynis.schedule_cmd(schedule))) @app.route("/api/security/lynis/schedule/status") def api_sec_lyn_schedule_status(): return api_wrap(lambda: ssh_run(lynis.schedule_status_cmd())) @app.route("/api/security/lynis/schedule/remove", methods=["POST"]) def api_sec_lyn_schedule_remove(): return api_wrap(lambda: ssh_run(lynis.schedule_remove_cmd())) @app.route("/api/security/lynis/uninstall", methods=["POST"]) def api_sec_lyn_uninstall(): return api_wrap(lambda: ssh_run(lynis.uninstall_cmd(), timeout=60)) # ── API: Security - OSSEC ─────────────────────────────────────────── @app.route("/api/security/ossec/status") def api_sec_osc_status(): return api_wrap(lambda: ssh_run(ossec.status_cmd())) @app.route("/api/security/ossec/install", methods=["POST"]) def api_sec_osc_install(): return api_wrap(lambda: ssh_run(ossec.install_cmd(), timeout=120)) @app.route("/api/security/ossec/start", methods=["POST"]) def api_sec_osc_start(): return api_wrap(lambda: ssh_run(ossec.start_cmd())) @app.route("/api/security/ossec/stop", methods=["POST"]) def api_sec_osc_stop(): return api_wrap(lambda: ssh_run(ossec.stop_cmd())) @app.route("/api/security/ossec/restart", methods=["POST"]) def api_sec_osc_restart(): return api_wrap(lambda: ssh_run(ossec.restart_cmd())) @app.route("/api/security/ossec/alerts") def api_sec_osc_alerts(): return api_wrap(lambda: ssh_run(ossec.alerts_cmd())) @app.route("/api/security/ossec/alerts-today") def api_sec_osc_alerts_today(): return api_wrap(lambda: ssh_run(ossec.alerts_today_cmd())) @app.route("/api/security/ossec/log") def api_sec_osc_log(): return api_wrap(lambda: ssh_run(ossec.log_cmd())) @app.route("/api/security/ossec/syscheck") def api_sec_osc_syscheck(): return api_wrap(lambda: ssh_run(ossec.syscheck_cmd())) @app.route("/api/security/ossec/config") def api_sec_osc_config(): return api_wrap(lambda: ssh_run(ossec.config_cmd())) @app.route("/api/security/ossec/rules") def api_sec_osc_rules(): return api_wrap(lambda: ssh_run(ossec.rules_cmd())) @app.route("/api/security/ossec/active-response") def api_sec_osc_active_response(): return api_wrap(lambda: ssh_run(ossec.active_response_cmd())) @app.route("/api/security/ossec/agents") def api_sec_osc_agents(): return api_wrap(lambda: ssh_run(ossec.agent_list_cmd())) @app.route("/api/security/ossec/uninstall", methods=["POST"]) def api_sec_osc_uninstall(): return api_wrap(lambda: ssh_run(ossec.uninstall_cmd(), timeout=60)) # ── API: Security - ModSecurity ────────────────────────────────────── @app.route("/api/security/modsec/status") def api_sec_mod_status(): return api_wrap(lambda: ssh_run(modsecurity.status_cmd())) @app.route("/api/security/modsec/install", methods=["POST"]) def api_sec_mod_install(): return api_wrap(lambda: ssh_run(modsecurity.install_cmd(), timeout=120)) @app.route("/api/security/modsec/enable", methods=["POST"]) def api_sec_mod_enable(): return api_wrap(lambda: ssh_run(modsecurity.enable_cmd())) @app.route("/api/security/modsec/disable", methods=["POST"]) def api_sec_mod_disable(): return api_wrap(lambda: ssh_run(modsecurity.disable_cmd())) @app.route("/api/security/modsec/audit-log") def api_sec_mod_audit_log(): return api_wrap(lambda: ssh_run(modsecurity.audit_log_cmd())) @app.route("/api/security/modsec/debug-log") def api_sec_mod_debug_log(): return api_wrap(lambda: ssh_run(modsecurity.debug_log_cmd())) @app.route("/api/security/modsec/rules") def api_sec_mod_rules(): return api_wrap(lambda: ssh_run(modsecurity.rules_list_cmd())) @app.route("/api/security/modsec/rule/disable", methods=["POST"]) def api_sec_mod_rule_disable(): rule_id = (request.json or {}).get("rule_id", "") if not rule_id: return jsonify({"ok": False, "error": "need rule_id"}), 400 return api_wrap(lambda: ssh_run(modsecurity.rule_disable_cmd(rule_id))) @app.route("/api/security/modsec/rule/enable", methods=["POST"]) def api_sec_mod_rule_enable(): rule_id = (request.json or {}).get("rule_id", "") if not rule_id: return jsonify({"ok": False, "error": "need rule_id"}), 400 return api_wrap(lambda: ssh_run(modsecurity.rule_enable_cmd(rule_id))) @app.route("/api/security/modsec/crs-update", methods=["POST"]) def api_sec_mod_crs_update(): return api_wrap(lambda: ssh_run(modsecurity.crs_update_cmd(), timeout=60)) @app.route("/api/security/modsec/config") def api_sec_mod_config(): return api_wrap(lambda: ssh_run(modsecurity.config_cmd())) @app.route("/api/security/modsec/crs-config") def api_sec_mod_crs_config(): return api_wrap(lambda: ssh_run(modsecurity.config_crs_cmd())) @app.route("/api/security/modsec/exclusions") def api_sec_mod_exclusions(): return api_wrap(lambda: ssh_run(modsecurity.exclusions_cmd())) @app.route("/api/security/modsec/test", methods=["POST"]) def api_sec_mod_test(): return api_wrap(lambda: ssh_run(modsecurity.test_cmd())) @app.route("/api/security/modsec/nginx-status") def api_sec_mod_nginx_status(): return api_wrap(lambda: ssh_run(modsecurity.nginx_status_cmd())) @app.route("/api/security/modsec/uninstall", methods=["POST"]) def api_sec_mod_uninstall(): return api_wrap(lambda: ssh_run(modsecurity.uninstall_cmd(), timeout=60)) # ── API: Security - AIDE ──────────────────────────────────────────── @app.route("/api/security/aide/status") def api_sec_aide_status(): return api_wrap(lambda: ssh_run(aide.status_cmd())) @app.route("/api/security/aide/install", methods=["POST"]) def api_sec_aide_install(): return api_wrap(lambda: ssh_run(aide.install_cmd(), timeout=120)) @app.route("/api/security/aide/check", methods=["POST"]) def api_sec_aide_check(): return api_wrap(lambda: ssh_run(aide.check_cmd(), timeout=120)) @app.route("/api/security/aide/update", methods=["POST"]) def api_sec_aide_update(): return api_wrap(lambda: ssh_run(aide.update_cmd(), timeout=120)) @app.route("/api/security/aide/init", methods=["POST"]) def api_sec_aide_init(): return api_wrap(lambda: ssh_run(aide.init_cmd(), timeout=120)) @app.route("/api/security/aide/compare", methods=["POST"]) def api_sec_aide_compare(): return api_wrap(lambda: ssh_run(aide.compare_cmd(), timeout=120)) @app.route("/api/security/aide/log") def api_sec_aide_log(): return api_wrap(lambda: ssh_run(aide.log_cmd())) @app.route("/api/security/aide/config") def api_sec_aide_config(): return api_wrap(lambda: ssh_run(aide.config_cmd())) @app.route("/api/security/aide/rules") def api_sec_aide_rules(): return api_wrap(lambda: ssh_run(aide.config_rules_cmd())) @app.route("/api/security/aide/schedule", methods=["POST"]) def api_sec_aide_schedule(): schedule = (request.json or {}).get("schedule", "daily") return api_wrap(lambda: ssh_run(aide.schedule_cmd(schedule))) @app.route("/api/security/aide/schedule/status") def api_sec_aide_schedule_status(): return api_wrap(lambda: ssh_run(aide.schedule_status_cmd())) @app.route("/api/security/aide/schedule/remove", methods=["POST"]) def api_sec_aide_schedule_remove(): return api_wrap(lambda: ssh_run(aide.schedule_remove_cmd())) @app.route("/api/security/aide/uninstall", methods=["POST"]) def api_sec_aide_uninstall(): return api_wrap(lambda: ssh_run(aide.uninstall_cmd(), timeout=60)) # ── API: Security - Cowrie ────────────────────────────────────────── @app.route("/api/security/cowrie/status") def api_sec_cow_status(): return api_wrap(lambda: ssh_run(cowrie.status_cmd())) @app.route("/api/security/cowrie/install", methods=["POST"]) def api_sec_cow_install(): return api_wrap(lambda: ssh_run(cowrie.install_cmd(), timeout=120)) @app.route("/api/security/cowrie/start", methods=["POST"]) def api_sec_cow_start(): return api_wrap(lambda: ssh_run(cowrie.start_cmd())) @app.route("/api/security/cowrie/stop", methods=["POST"]) def api_sec_cow_stop(): return api_wrap(lambda: ssh_run(cowrie.stop_cmd())) @app.route("/api/security/cowrie/restart", methods=["POST"]) def api_sec_cow_restart(): return api_wrap(lambda: ssh_run(cowrie.restart_cmd())) @app.route("/api/security/cowrie/sessions") def api_sec_cow_sessions(): return api_wrap(lambda: ssh_run(cowrie.sessions_cmd())) @app.route("/api/security/cowrie/top-attackers") def api_sec_cow_top_attackers(): return api_wrap(lambda: ssh_run(cowrie.top_attackers_cmd())) @app.route("/api/security/cowrie/credentials") def api_sec_cow_credentials(): return api_wrap(lambda: ssh_run(cowrie.credentials_cmd())) @app.route("/api/security/cowrie/downloads") def api_sec_cow_downloads(): return api_wrap(lambda: ssh_run(cowrie.downloads_cmd())) @app.route("/api/security/cowrie/log") def api_sec_cow_log(): return api_wrap(lambda: ssh_run(cowrie.log_cmd())) @app.route("/api/security/cowrie/log-json") def api_sec_cow_log_json(): return api_wrap(lambda: ssh_run(cowrie.log_json_cmd())) @app.route("/api/security/cowrie/config") def api_sec_cow_config(): return api_wrap(lambda: ssh_run(cowrie.config_cmd())) @app.route("/api/security/cowrie/port-redirect", methods=["POST"]) def api_sec_cow_port_redirect(): enable = (request.json or {}).get("enable", True) return api_wrap(lambda: ssh_run(cowrie.port_redirect_cmd(enable))) @app.route("/api/security/cowrie/uninstall", methods=["POST"]) def api_sec_cow_uninstall(): return api_wrap(lambda: ssh_run(cowrie.uninstall_cmd(), timeout=60)) # ── API: Security - .sec Updates ───────────────────────────────────── @app.route("/api/security/updates/detect") def api_sec_updates_detect(): return api_wrap(lambda: ssh_run(sec_updates.os_detect_cmd())) @app.route("/api/security/updates/list") def api_sec_updates_list(): return api_wrap(lambda: ssh_run(sec_updates.list_available_cmd())) @app.route("/api/security/updates/check", methods=["POST"]) def api_sec_updates_check(): data = request.json or {} distro_id = data.get("distro_id", "") version_id = data.get("version_id", "") if not distro_id or not version_id: return jsonify({"ok": False, "error": "need distro_id and version_id"}), 400 return api_wrap(lambda: ssh_run(sec_updates.check_updates_cmd(distro_id, version_id))) @app.route("/api/security/updates/download", methods=["POST"]) def api_sec_updates_download(): filename = (request.json or {}).get("filename", "") if not filename: return jsonify({"ok": False, "error": "need filename"}), 400 return api_wrap(lambda: ssh_run(sec_updates.download_update_cmd(filename))) @app.route("/api/security/updates/preview", methods=["POST"]) def api_sec_updates_preview(): filename = (request.json or {}).get("filename", "") if not filename: return jsonify({"ok": False, "error": "need filename"}), 400 return api_wrap(lambda: ssh_run(sec_updates.preview_update_cmd(filename))) @app.route("/api/security/updates/apply", methods=["POST"]) def api_sec_updates_apply(): filename = (request.json or {}).get("filename", "") if not filename: return jsonify({"ok": False, "error": "need filename"}), 400 return api_wrap(lambda: ssh_run(sec_updates.parse_and_apply_cmd(filename), timeout=120)) @app.route("/api/security/updates/history") def api_sec_updates_history(): return api_wrap(lambda: ssh_run(sec_updates.update_history_cmd())) # ── API: Hosting Providers ──────────────────────────────────────── @app.route("/api/hosting/providers") def api_hosting_providers(): return jsonify({"ok": True, "data": hosting.PROVIDER_LIST}) # ── API: Wizard Test ───────────────────────────────────────────── @app.route("/api/wizard/test") def api_wizard_test(): """Test SSH connection and optionally DNS API, return verbose results.""" result = {"ssh_ok": False, "api_ok": False} # SSH test try: ssh_res = ssh_run("echo 'SSH OK' && hostname && whoami && uptime", timeout=15) if ssh_res.get("exit_code", -1) == 0 or ssh_res.get("stdout", ""): result["ssh_ok"] = True result["ssh_output"] = ssh_res.get("stdout", "") else: result["ssh_error"] = ssh_res.get("stderr", "") or "SSH connection failed" except Exception as e: result["ssh_error"] = str(e) # DNS API test (only if provider configured) cfg = config.load() provider = cfg.get("hosting_provider", "") api_key = cfg.get("hostinger_api_key", "") if provider and api_key: try: domain = cfg.get("domain", "") prov = hosting.PROVIDERS.get(provider, {}) if prov and domain: base = prov.get("dns_base", "") auth_header = prov.get("auth_header", "Authorization") auth_prefix = prov.get("auth_prefix", "Bearer ") cmd = ( f"curl -s -w '\\nHTTP_CODE:%{{http_code}}' " f"-H '{auth_header}: {auth_prefix}{api_key}' " f"-H 'Content-Type: application/json' " f"'{base}/{domain}' 2>&1" ) api_res = ssh_run(cmd, timeout=15) output = api_res.get("stdout", "") if "HTTP_CODE:200" in output: result["api_ok"] = True else: code = output.rsplit("HTTP_CODE:", 1)[-1].strip() if "HTTP_CODE:" in output else "?" result["api_error"] = f"API returned HTTP {code}" else: result["api_error"] = "Provider or domain not configured" except Exception as e: result["api_error"] = str(e) else: result["api_error"] = "No DNS provider configured (optional)" return jsonify({"ok": True, "data": result}) # ── Run ────────────────────────────────────────────────────────────── if __name__ == "__main__": cfg = config.load() print(f"SETEC LABS Manager starting on http://localhost:{cfg.get('flask_port', 5000)}") app.run(host="127.0.0.1", port=cfg.get("flask_port", 5000), debug=False)