Flask-based VPS management panel with SSH remote command execution. Includes E2E encrypted SSH tunnel (AES-256-GCM + Go agent), setup wizard, security hardening tools, DNS management, firewall configs, monitoring, backup, and .sec patch update system. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2725 lines
97 KiB
Python
2725 lines
97 KiB
Python
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/<action>/<name>", 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/<name>")
|
|
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/<action>", 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/<record_id>", 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/<site>")
|
|
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/<site>", 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/<name>")
|
|
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)
|